@almadar/ui 5.142.0 → 5.144.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.
@@ -911,6 +911,10 @@ var init_Icon = __esm({
911
911
  function isTilesheet(a) {
912
912
  return typeof a.tileWidth === "number";
913
913
  }
914
+ function isSpriteSheetAtlas(a) {
915
+ const s = a;
916
+ return typeof s.frameWidth === "number" && typeof s.frameHeight === "number" && typeof s.animations === "object";
917
+ }
914
918
  function getAtlas(url, onReady) {
915
919
  if (atlasCache.has(url)) return atlasCache.get(url) ?? void 0;
916
920
  atlasCache.set(url, void 0);
@@ -950,6 +954,7 @@ function subRectFor(atlas, sprite) {
950
954
  sh: atlas.tileHeight
951
955
  };
952
956
  }
957
+ if (isSpriteSheetAtlas(atlas)) return null;
953
958
  const st = atlas.subTextures[sprite];
954
959
  if (!st) return null;
955
960
  return { sx: st.x, sy: st.y, sw: st.width, sh: st.height };
@@ -1259,7 +1264,7 @@ var init_Button = __esm({
1259
1264
  "data-testid": dataTestId ?? (action ? `action-${action}` : void 0),
1260
1265
  children: [
1261
1266
  isLoading ? /* @__PURE__ */ jsxRuntime.jsx(LucideIcons2.Loader2, { className: "h-icon-default w-icon-default animate-spin" }) : resolvedLeftIcon && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-shrink-0", children: resolvedLeftIcon }),
1262
- children || label,
1267
+ (Array.isArray(children) ? children.length > 0 : Boolean(children)) ? children : label,
1263
1268
  resolvedRightIcon && !isLoading && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-shrink-0", children: resolvedRightIcon })
1264
1269
  ]
1265
1270
  }
@@ -7300,6 +7305,34 @@ var init_isometric = __esm({
7300
7305
  };
7301
7306
  }
7302
7307
  });
7308
+ function isAnimationName(name) {
7309
+ return core.ANIMATION_NAMES.includes(name);
7310
+ }
7311
+ function frameRect(frame, row, columns, frameWidth, frameHeight) {
7312
+ return {
7313
+ sx: frame % columns * frameWidth,
7314
+ sy: row * frameHeight,
7315
+ sw: frameWidth,
7316
+ sh: frameHeight
7317
+ };
7318
+ }
7319
+ function getCurrentFrameFromDef(def, elapsed) {
7320
+ const frameDuration = 1e3 / def.frameRate;
7321
+ const totalDuration = def.frames * frameDuration;
7322
+ if (def.loop) {
7323
+ const frame2 = Math.floor(elapsed % totalDuration / frameDuration);
7324
+ return { frame: frame2, finished: false };
7325
+ }
7326
+ if (elapsed >= totalDuration) {
7327
+ return { frame: def.frames - 1, finished: true };
7328
+ }
7329
+ const frame = Math.floor(elapsed / frameDuration);
7330
+ return { frame, finished: false };
7331
+ }
7332
+ var init_spriteAnimation = __esm({
7333
+ "lib/spriteAnimation.ts"() {
7334
+ }
7335
+ });
7303
7336
 
7304
7337
  // lib/gameShared.ts
7305
7338
  var init_gameShared = __esm({
@@ -9160,6 +9193,9 @@ function paintFallbackSquare(painter, node, dctx, reason) {
9160
9193
  painter.strokeRect(rect.x, rect.y, rect.w, rect.h, "#5e564b", Math.max(1, tw / 32));
9161
9194
  painter.restore();
9162
9195
  }
9196
+ function isAnimatedSprite(node) {
9197
+ return node.animation !== void 0 && typeof node.asset?.atlas === "string";
9198
+ }
9163
9199
  function DrawSprite(_props) {
9164
9200
  return null;
9165
9201
  }
@@ -9168,6 +9204,7 @@ var init_DrawSprite = __esm({
9168
9204
  "components/game/atoms/DrawSprite.tsx"() {
9169
9205
  "use client";
9170
9206
  init_atlasSlice();
9207
+ init_spriteAnimation();
9171
9208
  init_imageCache();
9172
9209
  init_contract();
9173
9210
  spriteLog = logger.createLogger("almadar:ui:draw-sprite");
@@ -9181,6 +9218,23 @@ var init_DrawSprite = __esm({
9181
9218
  return;
9182
9219
  }
9183
9220
  let src = typeof node.frame === "object" ? node.frame : void 0;
9221
+ if (!src && node.animation !== void 0 && typeof node.asset.atlas === "string") {
9222
+ const atlas = getAtlas(node.asset.atlas, dctx.invalidate);
9223
+ if (!atlas) {
9224
+ if (atlasFailed(node.asset.atlas)) paintFallbackSquare(painter, node, dctx, "atlas-failed");
9225
+ return;
9226
+ }
9227
+ if (isSpriteSheetAtlas(atlas)) {
9228
+ const def = isAnimationName(node.animation) ? atlas.animations[node.animation] : void 0;
9229
+ if (!def) {
9230
+ paintFallbackSquare(painter, node, dctx, "sprite-missing");
9231
+ return;
9232
+ }
9233
+ const { frame } = getCurrentFrameFromDef(node.loop === void 0 ? def : { ...def, loop: node.loop }, dctx.time);
9234
+ const r = frameRect(frame, def.row, atlas.columns, atlas.frameWidth, atlas.frameHeight);
9235
+ src = { x: r.sx, y: r.sy, w: r.sw, h: r.sh };
9236
+ }
9237
+ }
9184
9238
  if (!src && isAtlasAsset(node.asset)) {
9185
9239
  const atlas = getAtlas(node.asset.atlas, dctx.invalidate);
9186
9240
  if (!atlas) {
@@ -9472,6 +9526,83 @@ var init_DrawText = __esm({
9472
9526
  };
9473
9527
  }
9474
9528
  });
9529
+ function isAnimatedGroup(node) {
9530
+ return Boolean(node.animation && node.animation.durationMs > 0 && node.animation.keyframes.length > 0);
9531
+ }
9532
+ function DrawGroup(props) {
9533
+ const register = React87.useContext(DrawableRegistryContext);
9534
+ if (register) register({ ...props, type: "draw-group" });
9535
+ return null;
9536
+ }
9537
+ var init_DrawGroup = __esm({
9538
+ "components/game/atoms/DrawGroup.tsx"() {
9539
+ "use client";
9540
+ init_registry();
9541
+ }
9542
+ });
9543
+ function applyMeshAnimation(node, timeMs) {
9544
+ const anim = node.animation;
9545
+ if (!anim || !(anim.durationMs > 0) || anim.keyframes.length === 0) return null;
9546
+ const frames = [...anim.keyframes].sort((a, b) => a.at - b.at);
9547
+ const cycle = timeMs / anim.durationMs;
9548
+ const t = anim.loop === false ? Math.min(cycle, 1) : cycle - Math.floor(cycle);
9549
+ const trackValue = (key) => {
9550
+ const defined = frames.filter((f3) => f3[key] !== void 0);
9551
+ if (defined.length === 0) return void 0;
9552
+ let prev;
9553
+ let next;
9554
+ for (const f3 of defined) {
9555
+ if (f3.at <= t) prev = f3;
9556
+ else if (!next) next = f3;
9557
+ }
9558
+ if (!prev) return defined[0][key];
9559
+ if (!next) return prev[key];
9560
+ const span = next.at - prev.at;
9561
+ const k = span > 0 ? (t - prev.at) / span : 1;
9562
+ const a = prev[key];
9563
+ const b = next[key];
9564
+ if (typeof a === "number" && typeof b === "number") return lerp2(a, b, k);
9565
+ return a;
9566
+ };
9567
+ const num = {};
9568
+ for (const key of NUMERIC_TRACKS2) {
9569
+ const v = trackValue(key);
9570
+ if (v !== void 0) num[key] = v;
9571
+ }
9572
+ return {
9573
+ offset: [num.offsetX ?? 0, num.offsetY ?? 0, num.offsetZ ?? 0],
9574
+ rotate: [num.rotateX ?? 0, num.rotateY ?? 0, num.rotateZ ?? 0],
9575
+ scale: num.scale ?? 1,
9576
+ opacity: num.opacity,
9577
+ emissiveIntensity: num.emissiveIntensity,
9578
+ color: trackValue("color"),
9579
+ emissive: trackValue("emissive")
9580
+ };
9581
+ }
9582
+ function DrawMesh(props) {
9583
+ const register = React87.useContext(DrawableRegistryContext);
9584
+ if (register) register({ ...props, type: "draw-mesh" });
9585
+ return null;
9586
+ }
9587
+ var lerp2, NUMERIC_TRACKS2;
9588
+ var init_DrawMesh = __esm({
9589
+ "components/game/atoms/DrawMesh.tsx"() {
9590
+ "use client";
9591
+ init_registry();
9592
+ lerp2 = (a, b, k) => a + (b - a) * k;
9593
+ NUMERIC_TRACKS2 = [
9594
+ "offsetX",
9595
+ "offsetY",
9596
+ "offsetZ",
9597
+ "rotateX",
9598
+ "rotateY",
9599
+ "rotateZ",
9600
+ "scale",
9601
+ "opacity",
9602
+ "emissiveIntensity"
9603
+ ];
9604
+ }
9605
+ });
9475
9606
 
9476
9607
  // components/game/molecules/DrawSpriteLayer.tsx
9477
9608
  function DrawSpriteLayer(_props) {
@@ -9538,18 +9669,22 @@ function paintDrawable(painter, node, dctx) {
9538
9669
  if (!isValidScenePos(node.position)) break;
9539
9670
  if (!Array.isArray(node.items)) break;
9540
9671
  const p = dctx.projector.project(node.position);
9672
+ const anim = dctx.time > 0 && isAnimatedGroup(node) ? applyMeshAnimation(node, dctx.time) : null;
9673
+ const tw = dctx.projector.tileWidth;
9541
9674
  painter.save();
9542
- painter.translate(p.x, p.y);
9543
- if (node.scale !== void 0) painter.scale(node.scale, node.scale);
9544
- if (node.rotate !== void 0) painter.rotate(node.rotate);
9545
- if (node.opacity !== void 0 && node.opacity !== 1) painter.setAlpha(node.opacity);
9675
+ painter.translate(p.x + (anim ? anim.offset[0] * tw : 0), p.y + (anim ? anim.offset[1] * tw : 0));
9676
+ const scale = (node.scale ?? 1) * (anim?.scale ?? 1);
9677
+ if (scale !== 1) painter.scale(scale, scale);
9678
+ const rotate = (node.rotate ?? 0) + (node.rotation?.[2] ?? 0) + (anim?.rotate[2] ?? 0);
9679
+ if (rotate !== 0) painter.rotate(rotate);
9680
+ const opacity = (node.opacity ?? 1) * (anim?.opacity ?? 1);
9681
+ if (opacity !== 1) painter.setAlpha(opacity);
9546
9682
  if (node.clip) {
9547
- const tw = dctx.projector.tileWidth;
9548
9683
  painter.scale(tw, tw);
9549
9684
  painter.clipPath(node.clip);
9550
9685
  painter.scale(1 / tw, 1 / tw);
9551
9686
  }
9552
- const childCtx = node.scale !== void 0 && node.scale !== 1 ? { ...dctx, groupScale: (dctx.groupScale ?? 1) * node.scale } : dctx;
9687
+ const childCtx = scale !== 1 ? { ...dctx, groupScale: (dctx.groupScale ?? 1) * scale } : dctx;
9553
9688
  for (const item of node.items) paintDrawable(painter, item, childCtx);
9554
9689
  painter.restore();
9555
9690
  break;
@@ -9575,6 +9710,8 @@ var init_paintDispatch = __esm({
9575
9710
  init_DrawSprite();
9576
9711
  init_DrawShape();
9577
9712
  init_DrawText();
9713
+ init_DrawGroup();
9714
+ init_DrawMesh();
9578
9715
  init_DrawSpriteLayer();
9579
9716
  init_DrawShapeLayer();
9580
9717
  init_DrawTextLayer();
@@ -9821,8 +9958,11 @@ function Canvas2D({
9821
9958
  const miniMapHeight = gridExtent.height || 10;
9822
9959
  const drawableIsAnimated = (node) => {
9823
9960
  if (node.type === "draw-shape") return isAnimatedShape(node);
9824
- if (node.type === "draw-group") return Array.isArray(node.items) && node.items.some(drawableIsAnimated);
9961
+ if (node.type === "draw-sprite") return isAnimatedSprite(node);
9962
+ if (node.type === "draw-group")
9963
+ return isAnimatedGroup(node) || Array.isArray(node.items) && node.items.some(drawableIsAnimated);
9825
9964
  if (node.type === "draw-shape-layer") return Array.isArray(node.items) && node.items.some(isAnimatedShape);
9965
+ if (node.type === "draw-sprite-layer") return Array.isArray(node.items) && node.items.some(isAnimatedSprite);
9826
9966
  return false;
9827
9967
  };
9828
9968
  const animRafRef = React87.useRef(0);
@@ -10163,6 +10303,8 @@ var init_Canvas2D = __esm({
10163
10303
  init_projector();
10164
10304
  init_paintDispatch();
10165
10305
  init_DrawShape();
10306
+ init_DrawSprite();
10307
+ init_DrawGroup();
10166
10308
  init_registry();
10167
10309
  init_hitTest();
10168
10310
  init_isometric();
@@ -10235,6 +10377,7 @@ function Canvas({
10235
10377
  cameraMode: to3DCameraMode(camera?.mode),
10236
10378
  ...zoom !== void 0 ? { scale: zoom } : {},
10237
10379
  ...camera?.fov !== void 0 ? { fov: camera.fov } : {},
10380
+ ...camera?.azimuth !== void 0 ? { azimuth: camera.azimuth } : {},
10238
10381
  ...camera?.target !== void 0 ? { followTarget: camera.target } : {},
10239
10382
  unitScale,
10240
10383
  backgroundColor,
@@ -22981,6 +23124,14 @@ var init_DashboardLayout = __esm({
22981
23124
  NavLinkBottom.displayName = "NavLinkBottom";
22982
23125
  }
22983
23126
  });
23127
+ function downloadItemUrl(url, label) {
23128
+ const a = document.createElement("a");
23129
+ a.href = url;
23130
+ a.download = url.startsWith("data:") ? label : url.split("/").pop() ?? "download";
23131
+ document.body.appendChild(a);
23132
+ a.click();
23133
+ a.remove();
23134
+ }
22984
23135
  function computeMenuStyle(position, triggerRect) {
22985
23136
  const isTop = position.startsWith("top");
22986
23137
  const isRight = position.endsWith("right") || position.endsWith("end");
@@ -23033,6 +23184,7 @@ function SubMenu({
23033
23184
  onClick: () => {
23034
23185
  if (item.disabled) return;
23035
23186
  if (item.event) eventBus.emit(`UI:${item.event}`, { itemId, label: item.label });
23187
+ if (item.url) downloadItemUrl(item.url, item.label);
23036
23188
  item.onClick?.();
23037
23189
  },
23038
23190
  "aria-disabled": item.disabled || void 0,
@@ -23186,6 +23338,7 @@ var init_Menu = __esm({
23186
23338
  setActiveSubMenu(itemId);
23187
23339
  } else {
23188
23340
  if (item.event) eventBus.emit(`UI:${item.event}`, { itemId, label: item.label });
23341
+ if (item.url) downloadItemUrl(item.url, item.label);
23189
23342
  item.onClick?.();
23190
23343
  setIsOpen(false);
23191
23344
  }
@@ -23221,12 +23374,19 @@ var init_Menu = __esm({
23221
23374
  "bottom-end": "bottom-start"
23222
23375
  };
23223
23376
  const effectivePosition = direction === "rtl" ? rtlMirror[position] ?? position : position;
23224
- const triggerChild = React87__namespace.default.isValidElement(trigger) ? trigger : /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "small", as: "span", children: trigger });
23225
- const triggerElement = React87__namespace.default.cloneElement(
23226
- triggerChild,
23377
+ const triggerElement = React87__namespace.default.isValidElement(trigger) ? React87__namespace.default.cloneElement(trigger, {
23378
+ ref: triggerRef,
23379
+ onClick: handleToggle
23380
+ }) : /* @__PURE__ */ jsxRuntime.jsx(
23381
+ Box,
23227
23382
  {
23228
- ref: triggerRef,
23229
- onClick: handleToggle
23383
+ as: "span",
23384
+ ref: (el) => {
23385
+ triggerRef.current = el;
23386
+ },
23387
+ onClick: handleToggle,
23388
+ className: "inline-flex",
23389
+ children: typeof trigger === "string" || typeof trigger === "number" ? /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "small", as: "span", children: trigger }) : trigger
23230
23390
  }
23231
23391
  );
23232
23392
  const renderMenuItems = (menuItems) => menuItems.map((item, index) => {
@@ -39068,28 +39228,6 @@ var init_DetailPanel = __esm({
39068
39228
  DetailPanel.displayName = "DetailPanel";
39069
39229
  }
39070
39230
  });
39071
- function DrawGroup(props) {
39072
- const register = React87.useContext(DrawableRegistryContext);
39073
- if (register) register({ ...props, type: "draw-group" });
39074
- return null;
39075
- }
39076
- var init_DrawGroup = __esm({
39077
- "components/game/atoms/DrawGroup.tsx"() {
39078
- "use client";
39079
- init_registry();
39080
- }
39081
- });
39082
- function DrawMesh(props) {
39083
- const register = React87.useContext(DrawableRegistryContext);
39084
- if (register) register({ ...props, type: "draw-mesh" });
39085
- return null;
39086
- }
39087
- var init_DrawMesh = __esm({
39088
- "components/game/atoms/DrawMesh.tsx"() {
39089
- "use client";
39090
- init_registry();
39091
- }
39092
- });
39093
39231
  function extractTitle(children) {
39094
39232
  if (!React87__namespace.default.isValidElement(children)) return void 0;
39095
39233
  const props = children.props;
@@ -3,7 +3,7 @@ import React87__default, { createContext, useContext, useMemo, useRef, useEffect
3
3
  import { EventBusContext, useTraitScopeChain, useEntitySchemaOptional, useEntityBindingSnapshot, useTraitScope, TraitScopeProvider, useCurrentPagePath, useGameAudioContextOptional } from '@almadar/ui/providers';
4
4
  import { createLogger, isLogLevelEnabled } from '@almadar/logger';
5
5
  import { createContextFromBindings, interpolateValue } from '@almadar/runtime';
6
- import { ANONYMOUS_USER, isRenderBindingMarker, isInlineTrait } from '@almadar/core';
6
+ import { ANONYMOUS_USER, isRenderBindingMarker, isInlineTrait, ANIMATION_NAMES } from '@almadar/core';
7
7
  export { ANONYMOUS_USER } from '@almadar/core';
8
8
  import { clsx } from 'clsx';
9
9
  import { twMerge } from 'tailwind-merge';
@@ -837,6 +837,10 @@ var init_Icon = __esm({
837
837
  function isTilesheet(a) {
838
838
  return typeof a.tileWidth === "number";
839
839
  }
840
+ function isSpriteSheetAtlas(a) {
841
+ const s = a;
842
+ return typeof s.frameWidth === "number" && typeof s.frameHeight === "number" && typeof s.animations === "object";
843
+ }
840
844
  function getAtlas(url, onReady) {
841
845
  if (atlasCache.has(url)) return atlasCache.get(url) ?? void 0;
842
846
  atlasCache.set(url, void 0);
@@ -876,6 +880,7 @@ function subRectFor(atlas, sprite) {
876
880
  sh: atlas.tileHeight
877
881
  };
878
882
  }
883
+ if (isSpriteSheetAtlas(atlas)) return null;
879
884
  const st = atlas.subTextures[sprite];
880
885
  if (!st) return null;
881
886
  return { sx: st.x, sy: st.y, sw: st.width, sh: st.height };
@@ -1185,7 +1190,7 @@ var init_Button = __esm({
1185
1190
  "data-testid": dataTestId ?? (action ? `action-${action}` : void 0),
1186
1191
  children: [
1187
1192
  isLoading ? /* @__PURE__ */ jsx(Loader2, { className: "h-icon-default w-icon-default animate-spin" }) : resolvedLeftIcon && /* @__PURE__ */ jsx("span", { className: "flex-shrink-0", children: resolvedLeftIcon }),
1188
- children || label,
1193
+ (Array.isArray(children) ? children.length > 0 : Boolean(children)) ? children : label,
1189
1194
  resolvedRightIcon && !isLoading && /* @__PURE__ */ jsx("span", { className: "flex-shrink-0", children: resolvedRightIcon })
1190
1195
  ]
1191
1196
  }
@@ -7226,6 +7231,34 @@ var init_isometric = __esm({
7226
7231
  };
7227
7232
  }
7228
7233
  });
7234
+ function isAnimationName(name) {
7235
+ return ANIMATION_NAMES.includes(name);
7236
+ }
7237
+ function frameRect(frame, row, columns, frameWidth, frameHeight) {
7238
+ return {
7239
+ sx: frame % columns * frameWidth,
7240
+ sy: row * frameHeight,
7241
+ sw: frameWidth,
7242
+ sh: frameHeight
7243
+ };
7244
+ }
7245
+ function getCurrentFrameFromDef(def, elapsed) {
7246
+ const frameDuration = 1e3 / def.frameRate;
7247
+ const totalDuration = def.frames * frameDuration;
7248
+ if (def.loop) {
7249
+ const frame2 = Math.floor(elapsed % totalDuration / frameDuration);
7250
+ return { frame: frame2, finished: false };
7251
+ }
7252
+ if (elapsed >= totalDuration) {
7253
+ return { frame: def.frames - 1, finished: true };
7254
+ }
7255
+ const frame = Math.floor(elapsed / frameDuration);
7256
+ return { frame, finished: false };
7257
+ }
7258
+ var init_spriteAnimation = __esm({
7259
+ "lib/spriteAnimation.ts"() {
7260
+ }
7261
+ });
7229
7262
 
7230
7263
  // lib/gameShared.ts
7231
7264
  var init_gameShared = __esm({
@@ -9086,6 +9119,9 @@ function paintFallbackSquare(painter, node, dctx, reason) {
9086
9119
  painter.strokeRect(rect.x, rect.y, rect.w, rect.h, "#5e564b", Math.max(1, tw / 32));
9087
9120
  painter.restore();
9088
9121
  }
9122
+ function isAnimatedSprite(node) {
9123
+ return node.animation !== void 0 && typeof node.asset?.atlas === "string";
9124
+ }
9089
9125
  function DrawSprite(_props) {
9090
9126
  return null;
9091
9127
  }
@@ -9094,6 +9130,7 @@ var init_DrawSprite = __esm({
9094
9130
  "components/game/atoms/DrawSprite.tsx"() {
9095
9131
  "use client";
9096
9132
  init_atlasSlice();
9133
+ init_spriteAnimation();
9097
9134
  init_imageCache();
9098
9135
  init_contract();
9099
9136
  spriteLog = createLogger("almadar:ui:draw-sprite");
@@ -9107,6 +9144,23 @@ var init_DrawSprite = __esm({
9107
9144
  return;
9108
9145
  }
9109
9146
  let src = typeof node.frame === "object" ? node.frame : void 0;
9147
+ if (!src && node.animation !== void 0 && typeof node.asset.atlas === "string") {
9148
+ const atlas = getAtlas(node.asset.atlas, dctx.invalidate);
9149
+ if (!atlas) {
9150
+ if (atlasFailed(node.asset.atlas)) paintFallbackSquare(painter, node, dctx, "atlas-failed");
9151
+ return;
9152
+ }
9153
+ if (isSpriteSheetAtlas(atlas)) {
9154
+ const def = isAnimationName(node.animation) ? atlas.animations[node.animation] : void 0;
9155
+ if (!def) {
9156
+ paintFallbackSquare(painter, node, dctx, "sprite-missing");
9157
+ return;
9158
+ }
9159
+ const { frame } = getCurrentFrameFromDef(node.loop === void 0 ? def : { ...def, loop: node.loop }, dctx.time);
9160
+ const r = frameRect(frame, def.row, atlas.columns, atlas.frameWidth, atlas.frameHeight);
9161
+ src = { x: r.sx, y: r.sy, w: r.sw, h: r.sh };
9162
+ }
9163
+ }
9110
9164
  if (!src && isAtlasAsset(node.asset)) {
9111
9165
  const atlas = getAtlas(node.asset.atlas, dctx.invalidate);
9112
9166
  if (!atlas) {
@@ -9398,6 +9452,83 @@ var init_DrawText = __esm({
9398
9452
  };
9399
9453
  }
9400
9454
  });
9455
+ function isAnimatedGroup(node) {
9456
+ return Boolean(node.animation && node.animation.durationMs > 0 && node.animation.keyframes.length > 0);
9457
+ }
9458
+ function DrawGroup(props) {
9459
+ const register = useContext(DrawableRegistryContext);
9460
+ if (register) register({ ...props, type: "draw-group" });
9461
+ return null;
9462
+ }
9463
+ var init_DrawGroup = __esm({
9464
+ "components/game/atoms/DrawGroup.tsx"() {
9465
+ "use client";
9466
+ init_registry();
9467
+ }
9468
+ });
9469
+ function applyMeshAnimation(node, timeMs) {
9470
+ const anim = node.animation;
9471
+ if (!anim || !(anim.durationMs > 0) || anim.keyframes.length === 0) return null;
9472
+ const frames = [...anim.keyframes].sort((a, b) => a.at - b.at);
9473
+ const cycle = timeMs / anim.durationMs;
9474
+ const t = anim.loop === false ? Math.min(cycle, 1) : cycle - Math.floor(cycle);
9475
+ const trackValue = (key) => {
9476
+ const defined = frames.filter((f3) => f3[key] !== void 0);
9477
+ if (defined.length === 0) return void 0;
9478
+ let prev;
9479
+ let next;
9480
+ for (const f3 of defined) {
9481
+ if (f3.at <= t) prev = f3;
9482
+ else if (!next) next = f3;
9483
+ }
9484
+ if (!prev) return defined[0][key];
9485
+ if (!next) return prev[key];
9486
+ const span = next.at - prev.at;
9487
+ const k = span > 0 ? (t - prev.at) / span : 1;
9488
+ const a = prev[key];
9489
+ const b = next[key];
9490
+ if (typeof a === "number" && typeof b === "number") return lerp2(a, b, k);
9491
+ return a;
9492
+ };
9493
+ const num = {};
9494
+ for (const key of NUMERIC_TRACKS2) {
9495
+ const v = trackValue(key);
9496
+ if (v !== void 0) num[key] = v;
9497
+ }
9498
+ return {
9499
+ offset: [num.offsetX ?? 0, num.offsetY ?? 0, num.offsetZ ?? 0],
9500
+ rotate: [num.rotateX ?? 0, num.rotateY ?? 0, num.rotateZ ?? 0],
9501
+ scale: num.scale ?? 1,
9502
+ opacity: num.opacity,
9503
+ emissiveIntensity: num.emissiveIntensity,
9504
+ color: trackValue("color"),
9505
+ emissive: trackValue("emissive")
9506
+ };
9507
+ }
9508
+ function DrawMesh(props) {
9509
+ const register = useContext(DrawableRegistryContext);
9510
+ if (register) register({ ...props, type: "draw-mesh" });
9511
+ return null;
9512
+ }
9513
+ var lerp2, NUMERIC_TRACKS2;
9514
+ var init_DrawMesh = __esm({
9515
+ "components/game/atoms/DrawMesh.tsx"() {
9516
+ "use client";
9517
+ init_registry();
9518
+ lerp2 = (a, b, k) => a + (b - a) * k;
9519
+ NUMERIC_TRACKS2 = [
9520
+ "offsetX",
9521
+ "offsetY",
9522
+ "offsetZ",
9523
+ "rotateX",
9524
+ "rotateY",
9525
+ "rotateZ",
9526
+ "scale",
9527
+ "opacity",
9528
+ "emissiveIntensity"
9529
+ ];
9530
+ }
9531
+ });
9401
9532
 
9402
9533
  // components/game/molecules/DrawSpriteLayer.tsx
9403
9534
  function DrawSpriteLayer(_props) {
@@ -9464,18 +9595,22 @@ function paintDrawable(painter, node, dctx) {
9464
9595
  if (!isValidScenePos(node.position)) break;
9465
9596
  if (!Array.isArray(node.items)) break;
9466
9597
  const p = dctx.projector.project(node.position);
9598
+ const anim = dctx.time > 0 && isAnimatedGroup(node) ? applyMeshAnimation(node, dctx.time) : null;
9599
+ const tw = dctx.projector.tileWidth;
9467
9600
  painter.save();
9468
- painter.translate(p.x, p.y);
9469
- if (node.scale !== void 0) painter.scale(node.scale, node.scale);
9470
- if (node.rotate !== void 0) painter.rotate(node.rotate);
9471
- if (node.opacity !== void 0 && node.opacity !== 1) painter.setAlpha(node.opacity);
9601
+ painter.translate(p.x + (anim ? anim.offset[0] * tw : 0), p.y + (anim ? anim.offset[1] * tw : 0));
9602
+ const scale = (node.scale ?? 1) * (anim?.scale ?? 1);
9603
+ if (scale !== 1) painter.scale(scale, scale);
9604
+ const rotate = (node.rotate ?? 0) + (node.rotation?.[2] ?? 0) + (anim?.rotate[2] ?? 0);
9605
+ if (rotate !== 0) painter.rotate(rotate);
9606
+ const opacity = (node.opacity ?? 1) * (anim?.opacity ?? 1);
9607
+ if (opacity !== 1) painter.setAlpha(opacity);
9472
9608
  if (node.clip) {
9473
- const tw = dctx.projector.tileWidth;
9474
9609
  painter.scale(tw, tw);
9475
9610
  painter.clipPath(node.clip);
9476
9611
  painter.scale(1 / tw, 1 / tw);
9477
9612
  }
9478
- const childCtx = node.scale !== void 0 && node.scale !== 1 ? { ...dctx, groupScale: (dctx.groupScale ?? 1) * node.scale } : dctx;
9613
+ const childCtx = scale !== 1 ? { ...dctx, groupScale: (dctx.groupScale ?? 1) * scale } : dctx;
9479
9614
  for (const item of node.items) paintDrawable(painter, item, childCtx);
9480
9615
  painter.restore();
9481
9616
  break;
@@ -9501,6 +9636,8 @@ var init_paintDispatch = __esm({
9501
9636
  init_DrawSprite();
9502
9637
  init_DrawShape();
9503
9638
  init_DrawText();
9639
+ init_DrawGroup();
9640
+ init_DrawMesh();
9504
9641
  init_DrawSpriteLayer();
9505
9642
  init_DrawShapeLayer();
9506
9643
  init_DrawTextLayer();
@@ -9747,8 +9884,11 @@ function Canvas2D({
9747
9884
  const miniMapHeight = gridExtent.height || 10;
9748
9885
  const drawableIsAnimated = (node) => {
9749
9886
  if (node.type === "draw-shape") return isAnimatedShape(node);
9750
- if (node.type === "draw-group") return Array.isArray(node.items) && node.items.some(drawableIsAnimated);
9887
+ if (node.type === "draw-sprite") return isAnimatedSprite(node);
9888
+ if (node.type === "draw-group")
9889
+ return isAnimatedGroup(node) || Array.isArray(node.items) && node.items.some(drawableIsAnimated);
9751
9890
  if (node.type === "draw-shape-layer") return Array.isArray(node.items) && node.items.some(isAnimatedShape);
9891
+ if (node.type === "draw-sprite-layer") return Array.isArray(node.items) && node.items.some(isAnimatedSprite);
9752
9892
  return false;
9753
9893
  };
9754
9894
  const animRafRef = useRef(0);
@@ -10089,6 +10229,8 @@ var init_Canvas2D = __esm({
10089
10229
  init_projector();
10090
10230
  init_paintDispatch();
10091
10231
  init_DrawShape();
10232
+ init_DrawSprite();
10233
+ init_DrawGroup();
10092
10234
  init_registry();
10093
10235
  init_hitTest();
10094
10236
  init_isometric();
@@ -10161,6 +10303,7 @@ function Canvas({
10161
10303
  cameraMode: to3DCameraMode(camera?.mode),
10162
10304
  ...zoom !== void 0 ? { scale: zoom } : {},
10163
10305
  ...camera?.fov !== void 0 ? { fov: camera.fov } : {},
10306
+ ...camera?.azimuth !== void 0 ? { azimuth: camera.azimuth } : {},
10164
10307
  ...camera?.target !== void 0 ? { followTarget: camera.target } : {},
10165
10308
  unitScale,
10166
10309
  backgroundColor,
@@ -22907,6 +23050,14 @@ var init_DashboardLayout = __esm({
22907
23050
  NavLinkBottom.displayName = "NavLinkBottom";
22908
23051
  }
22909
23052
  });
23053
+ function downloadItemUrl(url, label) {
23054
+ const a = document.createElement("a");
23055
+ a.href = url;
23056
+ a.download = url.startsWith("data:") ? label : url.split("/").pop() ?? "download";
23057
+ document.body.appendChild(a);
23058
+ a.click();
23059
+ a.remove();
23060
+ }
22910
23061
  function computeMenuStyle(position, triggerRect) {
22911
23062
  const isTop = position.startsWith("top");
22912
23063
  const isRight = position.endsWith("right") || position.endsWith("end");
@@ -22959,6 +23110,7 @@ function SubMenu({
22959
23110
  onClick: () => {
22960
23111
  if (item.disabled) return;
22961
23112
  if (item.event) eventBus.emit(`UI:${item.event}`, { itemId, label: item.label });
23113
+ if (item.url) downloadItemUrl(item.url, item.label);
22962
23114
  item.onClick?.();
22963
23115
  },
22964
23116
  "aria-disabled": item.disabled || void 0,
@@ -23112,6 +23264,7 @@ var init_Menu = __esm({
23112
23264
  setActiveSubMenu(itemId);
23113
23265
  } else {
23114
23266
  if (item.event) eventBus.emit(`UI:${item.event}`, { itemId, label: item.label });
23267
+ if (item.url) downloadItemUrl(item.url, item.label);
23115
23268
  item.onClick?.();
23116
23269
  setIsOpen(false);
23117
23270
  }
@@ -23147,12 +23300,19 @@ var init_Menu = __esm({
23147
23300
  "bottom-end": "bottom-start"
23148
23301
  };
23149
23302
  const effectivePosition = direction === "rtl" ? rtlMirror[position] ?? position : position;
23150
- const triggerChild = React87__default.isValidElement(trigger) ? trigger : /* @__PURE__ */ jsx(Typography, { variant: "small", as: "span", children: trigger });
23151
- const triggerElement = React87__default.cloneElement(
23152
- triggerChild,
23303
+ const triggerElement = React87__default.isValidElement(trigger) ? React87__default.cloneElement(trigger, {
23304
+ ref: triggerRef,
23305
+ onClick: handleToggle
23306
+ }) : /* @__PURE__ */ jsx(
23307
+ Box,
23153
23308
  {
23154
- ref: triggerRef,
23155
- onClick: handleToggle
23309
+ as: "span",
23310
+ ref: (el) => {
23311
+ triggerRef.current = el;
23312
+ },
23313
+ onClick: handleToggle,
23314
+ className: "inline-flex",
23315
+ children: typeof trigger === "string" || typeof trigger === "number" ? /* @__PURE__ */ jsx(Typography, { variant: "small", as: "span", children: trigger }) : trigger
23156
23316
  }
23157
23317
  );
23158
23318
  const renderMenuItems = (menuItems) => menuItems.map((item, index) => {
@@ -38994,28 +39154,6 @@ var init_DetailPanel = __esm({
38994
39154
  DetailPanel.displayName = "DetailPanel";
38995
39155
  }
38996
39156
  });
38997
- function DrawGroup(props) {
38998
- const register = useContext(DrawableRegistryContext);
38999
- if (register) register({ ...props, type: "draw-group" });
39000
- return null;
39001
- }
39002
- var init_DrawGroup = __esm({
39003
- "components/game/atoms/DrawGroup.tsx"() {
39004
- "use client";
39005
- init_registry();
39006
- }
39007
- });
39008
- function DrawMesh(props) {
39009
- const register = useContext(DrawableRegistryContext);
39010
- if (register) register({ ...props, type: "draw-mesh" });
39011
- return null;
39012
- }
39013
- var init_DrawMesh = __esm({
39014
- "components/game/atoms/DrawMesh.tsx"() {
39015
- "use client";
39016
- init_registry();
39017
- }
39018
- });
39019
39157
  function extractTitle(children) {
39020
39158
  if (!React87__default.isValidElement(children)) return void 0;
39021
39159
  const props = children.props;