@almadar/ui 2.9.0 → 2.9.1

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.
@@ -1,7 +1,7 @@
1
1
  import { useTheme, useUISlots } from './chunk-DKQN5FVU.js';
2
2
  import { useTranslate, useInfiniteScroll, useQuerySingleton, useLongPress, useSwipeGesture, useDragReorder, usePullToRefresh } from './chunk-WGJIL4YR.js';
3
3
  import { useEventBus } from './chunk-YXZM3WCF.js';
4
- import { cn, debugGroup, debug, debugGroupEnd, updateAssetStatus, bindCanvasCapture, getNestedValue, isDebugEnabled } from './chunk-A5J5CNCU.js';
4
+ import { cn, debugGroup, debug, debugGroupEnd, updateAssetStatus, bindCanvasCapture, getNestedValue, isDebugEnabled } from './chunk-6D5QMEUS.js';
5
5
  import { isPortalSlot } from './chunk-K2D5D3WK.js';
6
6
  import { __publicField } from './chunk-PKBMQBKP.js';
7
7
  import * as LucideIcons from 'lucide-react';
@@ -20,6 +20,7 @@ import dark from 'react-syntax-highlighter/dist/esm/styles/prism/vsc-dark-plus';
20
20
  import { MapContainer, TileLayer, Marker, Popup, useMap } from 'react-leaflet';
21
21
  import L from 'leaflet';
22
22
  import 'leaflet/dist/leaflet.css';
23
+ import { getComponentForPattern as getComponentForPattern$1 } from '@almadar/patterns';
23
24
 
24
25
  var iconAliases = {
25
26
  "close": LucideIcons.X,
@@ -2638,30 +2639,30 @@ var ConfettiEffect = ({
2638
2639
  ),
2639
2640
  "aria-hidden": "true",
2640
2641
  children: [
2641
- particles.map((p) => {
2642
- const rad = p.angle * Math.PI / 180;
2643
- const tx = Math.cos(rad) * p.distance;
2644
- const ty = Math.sin(rad) * p.distance - 20;
2642
+ particles.map((p2) => {
2643
+ const rad = p2.angle * Math.PI / 180;
2644
+ const tx = Math.cos(rad) * p2.distance;
2645
+ const ty = Math.sin(rad) * p2.distance - 20;
2645
2646
  return /* @__PURE__ */ jsx(
2646
2647
  Box,
2647
2648
  {
2648
2649
  className: "absolute rounded-sm",
2649
2650
  style: {
2650
- left: `${p.left}%`,
2651
+ left: `${p2.left}%`,
2651
2652
  top: "50%",
2652
- width: p.size,
2653
- height: p.size,
2654
- backgroundColor: p.color,
2655
- animation: `confetti-burst ${duration - p.delay}ms ease-out ${p.delay}ms forwards`,
2653
+ width: p2.size,
2654
+ height: p2.size,
2655
+ backgroundColor: p2.color,
2656
+ animation: `confetti-burst ${duration - p2.delay}ms ease-out ${p2.delay}ms forwards`,
2656
2657
  opacity: 0,
2657
2658
  // Use CSS custom properties for the animation endpoint
2658
2659
  // @ts-expect-error -- CSS custom properties are not typed in CSSProperties
2659
2660
  "--confetti-tx": `${tx}px`,
2660
2661
  "--confetti-ty": `${ty}px`,
2661
- "--confetti-rotate": `${p.rotation}deg`
2662
+ "--confetti-rotate": `${p2.rotation}deg`
2662
2663
  }
2663
2664
  },
2664
- p.id
2665
+ p2.id
2665
2666
  );
2666
2667
  }),
2667
2668
  /* @__PURE__ */ jsx("style", { children: `
@@ -7013,7 +7014,7 @@ var WizardProgress = ({
7013
7014
  )
7014
7015
  }
7015
7016
  )
7016
- ] }, step.id);
7017
+ ] }, step.id || `step-${index}`);
7017
7018
  }) })
7018
7019
  }
7019
7020
  );
@@ -8084,11 +8085,13 @@ var LineChart = ({
8084
8085
  className
8085
8086
  }) => {
8086
8087
  const gradientId = useId();
8088
+ const safeData = data ?? [];
8087
8089
  const sortedData = useMemo(() => {
8088
- return [...data].sort(
8090
+ if (safeData.length === 0) return [];
8091
+ return [...safeData].sort(
8089
8092
  (a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()
8090
8093
  );
8091
- }, [data]);
8094
+ }, [safeData]);
8092
8095
  const points = useMemo(() => {
8093
8096
  if (sortedData.length === 0) return [];
8094
8097
  const values = sortedData.map((d) => d.value);
@@ -8107,7 +8110,7 @@ var LineChart = ({
8107
8110
  }, [sortedData, width, height]);
8108
8111
  const linePath = useMemo(() => {
8109
8112
  if (points.length === 0) return "";
8110
- return points.map((p, i) => `${i === 0 ? "M" : "L"} ${p.x} ${p.y}`).join(" ");
8113
+ return points.map((p2, i) => `${i === 0 ? "M" : "L"} ${p2.x} ${p2.y}`).join(" ");
8111
8114
  }, [points]);
8112
8115
  const areaPath = useMemo(() => {
8113
8116
  if (points.length === 0 || !showArea) return "";
@@ -8116,7 +8119,7 @@ var LineChart = ({
8116
8119
  const last = points[points.length - 1];
8117
8120
  return `${linePath} L ${last.x} ${bottom} L ${first.x} ${bottom} Z`;
8118
8121
  }, [linePath, points, height, showArea]);
8119
- if (data.length === 0) {
8122
+ if (safeData.length === 0) {
8120
8123
  return /* @__PURE__ */ jsx(Box, { className: cn("flex items-center justify-center text-[var(--color-muted-foreground)]", className), style: { width, height }, children: "No data" });
8121
8124
  }
8122
8125
  return /* @__PURE__ */ jsx(Box, { className: cn(className), children: /* @__PURE__ */ jsxs(
@@ -10308,7 +10311,7 @@ function IsometricCanvas({
10308
10311
  resolveUnitFrame,
10309
10312
  effectSpriteUrls = [],
10310
10313
  onDrawEffects,
10311
- hasActiveEffects = false,
10314
+ hasActiveEffects: hasActiveEffects2 = false,
10312
10315
  // Tuning
10313
10316
  diamondTopY: diamondTopYProp,
10314
10317
  // Remote asset loading
@@ -10365,10 +10368,10 @@ function IsometricCanvas({
10365
10368
  return (gridHeight - 1) * (scaledTileWidth / 2);
10366
10369
  }, [gridHeight, scaledTileWidth]);
10367
10370
  const validMoveSet = useMemo(() => {
10368
- return new Set(validMoves.map((p) => `${p.x},${p.y}`));
10371
+ return new Set(validMoves.map((p2) => `${p2.x},${p2.y}`));
10369
10372
  }, [validMoves]);
10370
10373
  const attackTargetSet = useMemo(() => {
10371
- return new Set(attackTargets.map((p) => `${p.x},${p.y}`));
10374
+ return new Set(attackTargets.map((p2) => `${p2.x},${p2.y}`));
10372
10375
  }, [attackTargets]);
10373
10376
  const resolveManifestUrl = useCallback((relativePath) => {
10374
10377
  if (!relativePath) return void 0;
@@ -10461,10 +10464,10 @@ function IsometricCanvas({
10461
10464
  miniCanvas.height = mH;
10462
10465
  mCtx.clearRect(0, 0, mW, mH);
10463
10466
  const allScreenPos = sortedTiles.map((t2) => isoToScreen(t2.x, t2.y, scale, baseOffsetX));
10464
- const minX = Math.min(...allScreenPos.map((p) => p.x));
10465
- const maxX = Math.max(...allScreenPos.map((p) => p.x + scaledTileWidth));
10466
- const minY = Math.min(...allScreenPos.map((p) => p.y));
10467
- const maxY = Math.max(...allScreenPos.map((p) => p.y + scaledTileHeight));
10467
+ const minX = Math.min(...allScreenPos.map((p2) => p2.x));
10468
+ const maxX = Math.max(...allScreenPos.map((p2) => p2.x + scaledTileWidth));
10469
+ const minY = Math.min(...allScreenPos.map((p2) => p2.y));
10470
+ const maxY = Math.max(...allScreenPos.map((p2) => p2.y + scaledTileHeight));
10468
10471
  const worldW = maxX - minX;
10469
10472
  const worldH = maxY - minY;
10470
10473
  const scaleM = Math.min(mW / worldW, mH / worldH) * 0.9;
@@ -10846,7 +10849,7 @@ function IsometricCanvas({
10846
10849
  };
10847
10850
  }, [selectedUnitId, units, scale, baseOffsetX, scaledTileWidth, scaledDiamondTopY, scaledFloorHeight, viewportSize, targetCameraRef]);
10848
10851
  useEffect(() => {
10849
- const hasAnimations = units.length > 0 || validMoves.length > 0 || attackTargets.length > 0 || selectedUnitId != null || targetCameraRef.current != null || hasActiveEffects;
10852
+ const hasAnimations = units.length > 0 || validMoves.length > 0 || attackTargets.length > 0 || selectedUnitId != null || targetCameraRef.current != null || hasActiveEffects2;
10850
10853
  draw(animTimeRef.current);
10851
10854
  if (!hasAnimations) return;
10852
10855
  let running = true;
@@ -10862,7 +10865,7 @@ function IsometricCanvas({
10862
10865
  running = false;
10863
10866
  cancelAnimationFrame(rafIdRef.current);
10864
10867
  };
10865
- }, [draw, units.length, validMoves.length, attackTargets.length, selectedUnitId, hasActiveEffects, lerpToTarget, targetCameraRef]);
10868
+ }, [draw, units.length, validMoves.length, attackTargets.length, selectedUnitId, hasActiveEffects2, lerpToTarget, targetCameraRef]);
10866
10869
  const handleMouseMoveWithCamera = useCallback((e) => {
10867
10870
  if (enableCamera) {
10868
10871
  const wasPanning = handleMouseMove(e, () => draw(animTimeRef.current));
@@ -12855,19 +12858,20 @@ var Meter = ({
12855
12858
  },
12856
12859
  [eventBus, value]
12857
12860
  );
12861
+ const safeVal = value ?? 0;
12858
12862
  const percentage = useMemo(() => {
12859
12863
  const range = max - min;
12860
12864
  if (range <= 0) return 0;
12861
- return Math.min(Math.max((value - min) / range * 100, 0), 100);
12862
- }, [value, min, max]);
12865
+ return Math.min(Math.max((safeVal - min) / range * 100, 0), 100);
12866
+ }, [safeVal, min, max]);
12863
12867
  const activeColor = useMemo(
12864
- () => getColorForValue(value, max, thresholds),
12865
- [value, max, thresholds]
12868
+ () => getColorForValue(safeVal, max, thresholds),
12869
+ [safeVal, max, thresholds]
12866
12870
  );
12867
12871
  const displayValue = useMemo(() => {
12868
- const formatted = Number.isInteger(value) ? value : value.toFixed(1);
12872
+ const formatted = Number.isInteger(safeVal) ? safeVal : safeVal.toFixed(1);
12869
12873
  return unit ? `${formatted}${unit}` : `${formatted}`;
12870
- }, [value, unit]);
12874
+ }, [safeVal, unit]);
12871
12875
  if (isLoading) {
12872
12876
  return /* @__PURE__ */ jsx(LoadingState, { message: "Loading meter...", className });
12873
12877
  }
@@ -15592,6 +15596,1246 @@ function MasterDetail({
15592
15596
  );
15593
15597
  }
15594
15598
  MasterDetail.displayName = "MasterDetail";
15599
+
15600
+ // components/organisms/game/types/effects.ts
15601
+ var EMPTY_EFFECT_STATE = {
15602
+ particles: [],
15603
+ sequences: [],
15604
+ overlays: []
15605
+ };
15606
+
15607
+ // components/organisms/game/utils/canvasEffects.ts
15608
+ var _offscreen = null;
15609
+ var _offCtx = null;
15610
+ function getOffscreenCtx(w, h) {
15611
+ if (!_offscreen) {
15612
+ if (typeof OffscreenCanvas !== "undefined") {
15613
+ _offscreen = new OffscreenCanvas(w, h);
15614
+ } else {
15615
+ _offscreen = document.createElement("canvas");
15616
+ }
15617
+ }
15618
+ if (_offscreen.width < w) _offscreen.width = w;
15619
+ if (_offscreen.height < h) _offscreen.height = h;
15620
+ if (!_offCtx) {
15621
+ _offCtx = _offscreen.getContext("2d");
15622
+ }
15623
+ return _offCtx;
15624
+ }
15625
+ function drawTintedImage(ctx, img, x, y, w, h, tint, alpha, blendMode = "source-over") {
15626
+ if (w <= 0 || h <= 0) return;
15627
+ const oc = getOffscreenCtx(w, h);
15628
+ oc.clearRect(0, 0, w, h);
15629
+ oc.globalCompositeOperation = "source-over";
15630
+ oc.drawImage(img, 0, 0, w, h);
15631
+ oc.globalCompositeOperation = "source-atop";
15632
+ oc.fillStyle = `rgb(${tint.r}, ${tint.g}, ${tint.b})`;
15633
+ oc.fillRect(0, 0, w, h);
15634
+ const prevAlpha = ctx.globalAlpha;
15635
+ const prevBlend = ctx.globalCompositeOperation;
15636
+ ctx.globalAlpha = alpha;
15637
+ ctx.globalCompositeOperation = blendMode;
15638
+ ctx.drawImage(_offscreen, 0, 0, w, h, x, y, w, h);
15639
+ ctx.globalAlpha = prevAlpha;
15640
+ ctx.globalCompositeOperation = prevBlend;
15641
+ }
15642
+ function randRange(min, max) {
15643
+ return min + Math.random() * (max - min);
15644
+ }
15645
+ function spawnParticles(config, animTime) {
15646
+ const particles = [];
15647
+ for (let i = 0; i < config.count; i++) {
15648
+ const angle = randRange(config.angleMin, config.angleMax);
15649
+ const speed = randRange(config.velocityMin, config.velocityMax);
15650
+ const spriteUrl = config.spriteUrls[Math.floor(Math.random() * config.spriteUrls.length)];
15651
+ particles.push({
15652
+ spriteUrl,
15653
+ x: config.originX + randRange(-config.spread, config.spread),
15654
+ y: config.originY + randRange(-config.spread, config.spread),
15655
+ vx: Math.cos(angle) * speed,
15656
+ vy: Math.sin(angle) * speed,
15657
+ gravity: config.gravity,
15658
+ rotation: Math.random() * Math.PI * 2,
15659
+ rotationSpeed: randRange(config.rotationSpeedMin ?? -2, config.rotationSpeedMax ?? 2),
15660
+ scale: randRange(config.scaleMin, config.scaleMax),
15661
+ scaleSpeed: config.scaleSpeed ?? 0,
15662
+ alpha: config.alpha ?? 1,
15663
+ fadeRate: config.fadeRate ?? -1.5,
15664
+ tint: { ...config.tint },
15665
+ blendMode: config.blendMode ?? "source-over",
15666
+ spawnTime: animTime,
15667
+ lifetime: randRange(config.lifetimeMin, config.lifetimeMax)
15668
+ });
15669
+ }
15670
+ return particles;
15671
+ }
15672
+ function spawnSequence(config, animTime) {
15673
+ return {
15674
+ frameUrls: config.frameUrls,
15675
+ x: config.originX,
15676
+ y: config.originY,
15677
+ frameDuration: config.frameDuration,
15678
+ startTime: animTime,
15679
+ loop: config.loop ?? false,
15680
+ scale: config.scale ?? 1,
15681
+ tint: config.tint ?? null,
15682
+ alpha: config.alpha ?? 1,
15683
+ blendMode: config.blendMode ?? "source-over"
15684
+ };
15685
+ }
15686
+ function spawnOverlay(config, animTime) {
15687
+ return {
15688
+ spriteUrl: config.spriteUrl,
15689
+ x: config.originX,
15690
+ y: config.originY,
15691
+ alpha: config.alpha ?? 0.8,
15692
+ fadeRate: config.fadeRate ?? -0.5,
15693
+ pulseAmplitude: config.pulseAmplitude ?? 0,
15694
+ pulseFrequency: config.pulseFrequency ?? 2,
15695
+ scale: config.scale ?? 1,
15696
+ blendMode: config.blendMode ?? "source-over",
15697
+ spawnTime: animTime,
15698
+ lifetime: config.lifetime ?? 2e3
15699
+ };
15700
+ }
15701
+ function updateEffectState(state, animTime, deltaMs) {
15702
+ const dt = deltaMs / 1e3;
15703
+ const particles = state.particles.map((p2) => ({
15704
+ ...p2,
15705
+ x: p2.x + p2.vx * dt,
15706
+ y: p2.y + p2.vy * dt,
15707
+ vy: p2.vy + p2.gravity * dt,
15708
+ rotation: p2.rotation + p2.rotationSpeed * dt,
15709
+ scale: Math.max(0, p2.scale + p2.scaleSpeed * dt),
15710
+ alpha: Math.max(0, p2.alpha + p2.fadeRate * dt)
15711
+ })).filter((p2) => p2.alpha > 0.01 && animTime - p2.spawnTime < p2.lifetime);
15712
+ const sequences = state.sequences.filter((s) => {
15713
+ const elapsed = animTime - s.startTime;
15714
+ const totalDuration = s.frameUrls.length * s.frameDuration;
15715
+ return s.loop || elapsed < totalDuration;
15716
+ });
15717
+ const overlays = state.overlays.map((o) => ({
15718
+ ...o,
15719
+ alpha: Math.max(0, o.alpha + o.fadeRate * dt)
15720
+ })).filter((o) => o.alpha > 0.01 && animTime - o.spawnTime < o.lifetime);
15721
+ return { particles, sequences, overlays };
15722
+ }
15723
+ function drawEffectState(ctx, state, animTime, getImage) {
15724
+ for (const o of state.overlays) {
15725
+ const img = getImage(o.spriteUrl);
15726
+ if (!img) continue;
15727
+ let alpha = o.alpha;
15728
+ if (o.pulseAmplitude > 0) {
15729
+ const elapsed = (animTime - o.spawnTime) / 1e3;
15730
+ alpha += Math.sin(elapsed * o.pulseFrequency * Math.PI * 2) * o.pulseAmplitude;
15731
+ alpha = Math.max(0, Math.min(1, alpha));
15732
+ }
15733
+ const w = img.naturalWidth * o.scale;
15734
+ const h = img.naturalHeight * o.scale;
15735
+ const prevAlpha = ctx.globalAlpha;
15736
+ const prevBlend = ctx.globalCompositeOperation;
15737
+ ctx.globalAlpha = alpha;
15738
+ ctx.globalCompositeOperation = o.blendMode;
15739
+ ctx.drawImage(img, o.x - w / 2, o.y - h / 2, w, h);
15740
+ ctx.globalAlpha = prevAlpha;
15741
+ ctx.globalCompositeOperation = prevBlend;
15742
+ }
15743
+ for (const s of state.sequences) {
15744
+ const elapsed = animTime - s.startTime;
15745
+ let frameIndex = Math.floor(elapsed / s.frameDuration);
15746
+ if (s.loop) {
15747
+ frameIndex = frameIndex % s.frameUrls.length;
15748
+ } else if (frameIndex >= s.frameUrls.length) {
15749
+ continue;
15750
+ }
15751
+ const img = getImage(s.frameUrls[frameIndex]);
15752
+ if (!img) continue;
15753
+ const w = img.naturalWidth * s.scale;
15754
+ const h = img.naturalHeight * s.scale;
15755
+ if (s.tint) {
15756
+ drawTintedImage(ctx, img, s.x - w / 2, s.y - h / 2, w, h, s.tint, s.alpha, s.blendMode);
15757
+ } else {
15758
+ const prevAlpha = ctx.globalAlpha;
15759
+ const prevBlend = ctx.globalCompositeOperation;
15760
+ ctx.globalAlpha = s.alpha;
15761
+ ctx.globalCompositeOperation = s.blendMode;
15762
+ ctx.drawImage(img, s.x - w / 2, s.y - h / 2, w, h);
15763
+ ctx.globalAlpha = prevAlpha;
15764
+ ctx.globalCompositeOperation = prevBlend;
15765
+ }
15766
+ }
15767
+ for (const p2 of state.particles) {
15768
+ const img = getImage(p2.spriteUrl);
15769
+ if (!img) continue;
15770
+ const w = img.naturalWidth * p2.scale;
15771
+ const h = img.naturalHeight * p2.scale;
15772
+ ctx.save();
15773
+ ctx.translate(p2.x, p2.y);
15774
+ ctx.rotate(p2.rotation);
15775
+ drawTintedImage(ctx, img, -w / 2, -h / 2, w, h, p2.tint, p2.alpha, p2.blendMode);
15776
+ ctx.restore();
15777
+ }
15778
+ }
15779
+ function hasActiveEffects(state) {
15780
+ return state.particles.length > 0 || state.sequences.length > 0 || state.overlays.length > 0;
15781
+ }
15782
+ function getAllEffectSpriteUrls(manifest) {
15783
+ const urls = [];
15784
+ const base = manifest.baseUrl;
15785
+ if (manifest.particles) {
15786
+ for (const value of Object.values(manifest.particles)) {
15787
+ if (Array.isArray(value)) {
15788
+ value.forEach((v) => urls.push(`${base}/${v}`));
15789
+ } else if (typeof value === "string") {
15790
+ urls.push(`${base}/${value}`);
15791
+ }
15792
+ }
15793
+ }
15794
+ if (manifest.animations) {
15795
+ for (const frames of Object.values(manifest.animations)) {
15796
+ if (Array.isArray(frames)) {
15797
+ frames.forEach((f) => urls.push(`${base}/${f}`));
15798
+ }
15799
+ }
15800
+ }
15801
+ return urls;
15802
+ }
15803
+
15804
+ // components/organisms/game/utils/combatPresets.ts
15805
+ var PI = Math.PI;
15806
+ function p(manifest, key) {
15807
+ const particles = manifest.particles;
15808
+ if (!particles) return [];
15809
+ const val = particles[key];
15810
+ if (Array.isArray(val)) return val.map((v) => `${manifest.baseUrl}/${v}`);
15811
+ if (typeof val === "string") return [`${manifest.baseUrl}/${val}`];
15812
+ return [];
15813
+ }
15814
+ function anim(manifest, key) {
15815
+ const animations = manifest.animations;
15816
+ if (!animations) return [];
15817
+ const val = animations[key];
15818
+ if (Array.isArray(val)) return val.map((v) => `${manifest.baseUrl}/${v}`);
15819
+ return [];
15820
+ }
15821
+ function createCombatPresets(manifest) {
15822
+ return {
15823
+ // =====================================================================
15824
+ // MELEE — slash (red) + dirt + scratch + flash sequence
15825
+ // =====================================================================
15826
+ melee: (originX, originY) => {
15827
+ const particles = [
15828
+ {
15829
+ spriteUrls: p(manifest, "slash"),
15830
+ count: 6,
15831
+ originX,
15832
+ originY,
15833
+ spread: 8,
15834
+ velocityMin: 40,
15835
+ velocityMax: 120,
15836
+ angleMin: -PI * 0.8,
15837
+ angleMax: -PI * 0.2,
15838
+ gravity: 0,
15839
+ tint: { r: 255, g: 60, b: 40 },
15840
+ scaleMin: 0.3,
15841
+ scaleMax: 0.6,
15842
+ lifetimeMin: 300,
15843
+ lifetimeMax: 500,
15844
+ fadeRate: -2.5
15845
+ },
15846
+ {
15847
+ spriteUrls: p(manifest, "dirt"),
15848
+ count: 4,
15849
+ originX,
15850
+ originY: originY + 10,
15851
+ spread: 12,
15852
+ velocityMin: 20,
15853
+ velocityMax: 60,
15854
+ angleMin: -PI * 0.9,
15855
+ angleMax: -PI * 0.1,
15856
+ gravity: 120,
15857
+ tint: { r: 180, g: 140, b: 90 },
15858
+ scaleMin: 0.15,
15859
+ scaleMax: 0.3,
15860
+ lifetimeMin: 400,
15861
+ lifetimeMax: 700,
15862
+ fadeRate: -1.8
15863
+ },
15864
+ {
15865
+ spriteUrls: p(manifest, "scratch"),
15866
+ count: 2,
15867
+ originX,
15868
+ originY,
15869
+ spread: 5,
15870
+ velocityMin: 10,
15871
+ velocityMax: 30,
15872
+ angleMin: -PI * 0.7,
15873
+ angleMax: -PI * 0.3,
15874
+ gravity: 0,
15875
+ tint: { r: 255, g: 200, b: 150 },
15876
+ scaleMin: 0.25,
15877
+ scaleMax: 0.4,
15878
+ lifetimeMin: 200,
15879
+ lifetimeMax: 400,
15880
+ fadeRate: -3
15881
+ }
15882
+ ];
15883
+ const sequences = [];
15884
+ const flashFrames = anim(manifest, "flash");
15885
+ if (flashFrames.length > 0) {
15886
+ sequences.push({
15887
+ frameUrls: flashFrames,
15888
+ originX,
15889
+ originY,
15890
+ frameDuration: 35,
15891
+ scale: 0.4
15892
+ });
15893
+ }
15894
+ return {
15895
+ particles: particles.filter((pc) => pc.spriteUrls.length > 0),
15896
+ sequences,
15897
+ overlays: [],
15898
+ screenShake: 4,
15899
+ screenFlash: null
15900
+ };
15901
+ },
15902
+ // =====================================================================
15903
+ // RANGED — muzzle + trace + smoke + explosion sequence
15904
+ // =====================================================================
15905
+ ranged: (originX, originY) => {
15906
+ const particles = [
15907
+ {
15908
+ spriteUrls: p(manifest, "muzzle"),
15909
+ count: 3,
15910
+ originX,
15911
+ originY,
15912
+ spread: 4,
15913
+ velocityMin: 60,
15914
+ velocityMax: 150,
15915
+ angleMin: -PI * 0.6,
15916
+ angleMax: -PI * 0.4,
15917
+ gravity: 0,
15918
+ tint: { r: 255, g: 220, b: 100 },
15919
+ scaleMin: 0.2,
15920
+ scaleMax: 0.4,
15921
+ lifetimeMin: 200,
15922
+ lifetimeMax: 400,
15923
+ fadeRate: -3
15924
+ },
15925
+ {
15926
+ spriteUrls: p(manifest, "trace"),
15927
+ count: 5,
15928
+ originX,
15929
+ originY,
15930
+ spread: 3,
15931
+ velocityMin: 100,
15932
+ velocityMax: 200,
15933
+ angleMin: -PI * 0.55,
15934
+ angleMax: -PI * 0.45,
15935
+ gravity: 0,
15936
+ tint: { r: 255, g: 200, b: 80 },
15937
+ scaleMin: 0.15,
15938
+ scaleMax: 0.3,
15939
+ lifetimeMin: 150,
15940
+ lifetimeMax: 300,
15941
+ fadeRate: -4
15942
+ },
15943
+ {
15944
+ spriteUrls: p(manifest, "smoke").slice(0, 3),
15945
+ count: 3,
15946
+ originX,
15947
+ originY: originY + 5,
15948
+ spread: 6,
15949
+ velocityMin: 10,
15950
+ velocityMax: 30,
15951
+ angleMin: -PI * 0.8,
15952
+ angleMax: -PI * 0.2,
15953
+ gravity: -20,
15954
+ tint: { r: 200, g: 200, b: 200 },
15955
+ scaleMin: 0.2,
15956
+ scaleMax: 0.35,
15957
+ lifetimeMin: 500,
15958
+ lifetimeMax: 800,
15959
+ fadeRate: -1.5
15960
+ }
15961
+ ];
15962
+ const sequences = [];
15963
+ const explosionFrames = anim(manifest, "smokeExplosion");
15964
+ if (explosionFrames.length > 0) {
15965
+ sequences.push({
15966
+ frameUrls: explosionFrames,
15967
+ originX,
15968
+ originY,
15969
+ frameDuration: 50,
15970
+ scale: 0.35
15971
+ });
15972
+ }
15973
+ return {
15974
+ particles: particles.filter((pc) => pc.spriteUrls.length > 0),
15975
+ sequences,
15976
+ overlays: [],
15977
+ screenShake: 2,
15978
+ screenFlash: null
15979
+ };
15980
+ },
15981
+ // =====================================================================
15982
+ // MAGIC — twirl (purple) + spark (purple) + star
15983
+ // =====================================================================
15984
+ magic: (originX, originY) => {
15985
+ const particles = [
15986
+ {
15987
+ spriteUrls: p(manifest, "twirl"),
15988
+ count: 5,
15989
+ originX,
15990
+ originY,
15991
+ spread: 15,
15992
+ velocityMin: 20,
15993
+ velocityMax: 80,
15994
+ angleMin: 0,
15995
+ angleMax: PI * 2,
15996
+ gravity: -30,
15997
+ tint: { r: 180, g: 80, b: 255 },
15998
+ scaleMin: 0.2,
15999
+ scaleMax: 0.5,
16000
+ lifetimeMin: 500,
16001
+ lifetimeMax: 900,
16002
+ fadeRate: -1.2,
16003
+ blendMode: "lighter",
16004
+ rotationSpeedMin: -4,
16005
+ rotationSpeedMax: 4
16006
+ },
16007
+ {
16008
+ spriteUrls: p(manifest, "spark"),
16009
+ count: 8,
16010
+ originX,
16011
+ originY,
16012
+ spread: 20,
16013
+ velocityMin: 30,
16014
+ velocityMax: 100,
16015
+ angleMin: 0,
16016
+ angleMax: PI * 2,
16017
+ gravity: -15,
16018
+ tint: { r: 200, g: 120, b: 255 },
16019
+ scaleMin: 0.1,
16020
+ scaleMax: 0.25,
16021
+ lifetimeMin: 300,
16022
+ lifetimeMax: 600,
16023
+ fadeRate: -2,
16024
+ blendMode: "lighter"
16025
+ },
16026
+ {
16027
+ spriteUrls: p(manifest, "star"),
16028
+ count: 4,
16029
+ originX,
16030
+ originY,
16031
+ spread: 10,
16032
+ velocityMin: 15,
16033
+ velocityMax: 50,
16034
+ angleMin: -PI,
16035
+ angleMax: 0,
16036
+ gravity: -40,
16037
+ tint: { r: 220, g: 180, b: 255 },
16038
+ scaleMin: 0.15,
16039
+ scaleMax: 0.3,
16040
+ lifetimeMin: 600,
16041
+ lifetimeMax: 1e3,
16042
+ fadeRate: -1,
16043
+ blendMode: "lighter"
16044
+ }
16045
+ ];
16046
+ const overlays = [];
16047
+ const circleUrls = p(manifest, "circle");
16048
+ if (circleUrls.length > 0) {
16049
+ overlays.push({
16050
+ spriteUrl: circleUrls[0],
16051
+ originX,
16052
+ originY,
16053
+ alpha: 0.5,
16054
+ fadeRate: -0.6,
16055
+ pulseAmplitude: 0.2,
16056
+ pulseFrequency: 3,
16057
+ scale: 0.5,
16058
+ blendMode: "lighter",
16059
+ lifetime: 1200
16060
+ });
16061
+ }
16062
+ return {
16063
+ particles: particles.filter((pc) => pc.spriteUrls.length > 0),
16064
+ sequences: [],
16065
+ overlays,
16066
+ screenShake: 0,
16067
+ screenFlash: null
16068
+ };
16069
+ },
16070
+ // =====================================================================
16071
+ // HEAL — circle (green) + star (green) + light (green, pulse)
16072
+ // =====================================================================
16073
+ heal: (originX, originY) => {
16074
+ const particles = [
16075
+ {
16076
+ spriteUrls: p(manifest, "circle"),
16077
+ count: 6,
16078
+ originX,
16079
+ originY,
16080
+ spread: 15,
16081
+ velocityMin: 10,
16082
+ velocityMax: 40,
16083
+ angleMin: -PI,
16084
+ angleMax: -PI * 0.3,
16085
+ gravity: -50,
16086
+ tint: { r: 80, g: 255, b: 120 },
16087
+ scaleMin: 0.15,
16088
+ scaleMax: 0.35,
16089
+ lifetimeMin: 600,
16090
+ lifetimeMax: 1e3,
16091
+ fadeRate: -0.8,
16092
+ blendMode: "lighter"
16093
+ },
16094
+ {
16095
+ spriteUrls: p(manifest, "star"),
16096
+ count: 5,
16097
+ originX,
16098
+ originY,
16099
+ spread: 12,
16100
+ velocityMin: 15,
16101
+ velocityMax: 50,
16102
+ angleMin: -PI * 0.9,
16103
+ angleMax: -PI * 0.1,
16104
+ gravity: -60,
16105
+ tint: { r: 100, g: 255, b: 140 },
16106
+ scaleMin: 0.1,
16107
+ scaleMax: 0.2,
16108
+ lifetimeMin: 500,
16109
+ lifetimeMax: 800,
16110
+ fadeRate: -1.2,
16111
+ blendMode: "lighter"
16112
+ }
16113
+ ];
16114
+ const overlays = [];
16115
+ const lightUrls = p(manifest, "light");
16116
+ if (lightUrls.length > 0) {
16117
+ overlays.push({
16118
+ spriteUrl: lightUrls[0],
16119
+ originX,
16120
+ originY,
16121
+ alpha: 0.6,
16122
+ fadeRate: -0.4,
16123
+ pulseAmplitude: 0.25,
16124
+ pulseFrequency: 2.5,
16125
+ scale: 0.6,
16126
+ blendMode: "lighter",
16127
+ lifetime: 1500
16128
+ });
16129
+ }
16130
+ return {
16131
+ particles: particles.filter((pc) => pc.spriteUrls.length > 0),
16132
+ sequences: [],
16133
+ overlays,
16134
+ screenShake: 0,
16135
+ screenFlash: null
16136
+ };
16137
+ },
16138
+ // =====================================================================
16139
+ // DEFEND / SHIELD — star (blue) + circle (blue, pulse)
16140
+ // =====================================================================
16141
+ defend: (originX, originY) => {
16142
+ const particles = [
16143
+ {
16144
+ spriteUrls: p(manifest, "star"),
16145
+ count: 8,
16146
+ originX,
16147
+ originY,
16148
+ spread: 18,
16149
+ velocityMin: 10,
16150
+ velocityMax: 35,
16151
+ angleMin: 0,
16152
+ angleMax: PI * 2,
16153
+ gravity: 0,
16154
+ tint: { r: 80, g: 160, b: 255 },
16155
+ scaleMin: 0.12,
16156
+ scaleMax: 0.25,
16157
+ lifetimeMin: 600,
16158
+ lifetimeMax: 1e3,
16159
+ fadeRate: -0.8,
16160
+ blendMode: "lighter",
16161
+ rotationSpeedMin: -1,
16162
+ rotationSpeedMax: 1
16163
+ }
16164
+ ];
16165
+ const overlays = [];
16166
+ const circleUrls = p(manifest, "circle");
16167
+ if (circleUrls.length > 0) {
16168
+ overlays.push({
16169
+ spriteUrl: circleUrls[0],
16170
+ originX,
16171
+ originY,
16172
+ alpha: 0.6,
16173
+ fadeRate: -0.3,
16174
+ pulseAmplitude: 0.2,
16175
+ pulseFrequency: 2,
16176
+ scale: 0.6,
16177
+ blendMode: "lighter",
16178
+ lifetime: 1500
16179
+ });
16180
+ }
16181
+ return {
16182
+ particles: particles.filter((pc) => pc.spriteUrls.length > 0),
16183
+ sequences: [],
16184
+ overlays,
16185
+ screenShake: 0,
16186
+ screenFlash: null
16187
+ };
16188
+ },
16189
+ // shield aliases to defend
16190
+ shield: (originX, originY) => {
16191
+ const particles = [
16192
+ {
16193
+ spriteUrls: p(manifest, "star"),
16194
+ count: 10,
16195
+ originX,
16196
+ originY,
16197
+ spread: 20,
16198
+ velocityMin: 8,
16199
+ velocityMax: 30,
16200
+ angleMin: 0,
16201
+ angleMax: PI * 2,
16202
+ gravity: 0,
16203
+ tint: { r: 60, g: 180, b: 255 },
16204
+ scaleMin: 0.1,
16205
+ scaleMax: 0.22,
16206
+ lifetimeMin: 700,
16207
+ lifetimeMax: 1200,
16208
+ fadeRate: -0.7,
16209
+ blendMode: "lighter",
16210
+ rotationSpeedMin: -0.8,
16211
+ rotationSpeedMax: 0.8
16212
+ }
16213
+ ];
16214
+ const overlays = [];
16215
+ const circleUrls = p(manifest, "circle");
16216
+ if (circleUrls.length > 0) {
16217
+ overlays.push({
16218
+ spriteUrl: circleUrls[0],
16219
+ originX,
16220
+ originY,
16221
+ alpha: 0.7,
16222
+ fadeRate: -0.25,
16223
+ pulseAmplitude: 0.25,
16224
+ pulseFrequency: 1.8,
16225
+ scale: 0.7,
16226
+ blendMode: "lighter",
16227
+ lifetime: 1800
16228
+ });
16229
+ }
16230
+ return {
16231
+ particles: particles.filter((pc) => pc.spriteUrls.length > 0),
16232
+ sequences: [],
16233
+ overlays,
16234
+ screenShake: 0,
16235
+ screenFlash: null
16236
+ };
16237
+ },
16238
+ // =====================================================================
16239
+ // HIT — spark (orange) + flash (5 frames) + screen shake/flash
16240
+ // =====================================================================
16241
+ hit: (originX, originY) => {
16242
+ const particles = [
16243
+ {
16244
+ spriteUrls: p(manifest, "spark"),
16245
+ count: 10,
16246
+ originX,
16247
+ originY,
16248
+ spread: 8,
16249
+ velocityMin: 50,
16250
+ velocityMax: 150,
16251
+ angleMin: 0,
16252
+ angleMax: PI * 2,
16253
+ gravity: 80,
16254
+ tint: { r: 255, g: 180, b: 50 },
16255
+ scaleMin: 0.08,
16256
+ scaleMax: 0.2,
16257
+ lifetimeMin: 200,
16258
+ lifetimeMax: 500,
16259
+ fadeRate: -2.5
16260
+ }
16261
+ ];
16262
+ const sequences = [];
16263
+ const flashFrames = anim(manifest, "flash");
16264
+ if (flashFrames.length > 0) {
16265
+ sequences.push({
16266
+ frameUrls: flashFrames.slice(0, 5),
16267
+ originX,
16268
+ originY,
16269
+ frameDuration: 40,
16270
+ scale: 0.3
16271
+ });
16272
+ }
16273
+ return {
16274
+ particles: particles.filter((pc) => pc.spriteUrls.length > 0),
16275
+ sequences,
16276
+ overlays: [],
16277
+ screenShake: 3,
16278
+ screenFlash: { r: 255, g: 50, b: 50, duration: 150 }
16279
+ };
16280
+ },
16281
+ // critical aliases to hit with bigger shake
16282
+ critical: (originX, originY) => {
16283
+ const particles = [
16284
+ {
16285
+ spriteUrls: p(manifest, "flame"),
16286
+ count: 8,
16287
+ originX,
16288
+ originY,
16289
+ spread: 12,
16290
+ velocityMin: 60,
16291
+ velocityMax: 180,
16292
+ angleMin: 0,
16293
+ angleMax: PI * 2,
16294
+ gravity: 60,
16295
+ tint: { r: 255, g: 120, b: 30 },
16296
+ scaleMin: 0.15,
16297
+ scaleMax: 0.4,
16298
+ lifetimeMin: 300,
16299
+ lifetimeMax: 600,
16300
+ fadeRate: -2
16301
+ },
16302
+ {
16303
+ spriteUrls: p(manifest, "spark"),
16304
+ count: 12,
16305
+ originX,
16306
+ originY,
16307
+ spread: 10,
16308
+ velocityMin: 80,
16309
+ velocityMax: 200,
16310
+ angleMin: 0,
16311
+ angleMax: PI * 2,
16312
+ gravity: 100,
16313
+ tint: { r: 255, g: 200, b: 60 },
16314
+ scaleMin: 0.06,
16315
+ scaleMax: 0.18,
16316
+ lifetimeMin: 200,
16317
+ lifetimeMax: 400,
16318
+ fadeRate: -3
16319
+ }
16320
+ ];
16321
+ const sequences = [];
16322
+ const flashFrames = anim(manifest, "flash");
16323
+ if (flashFrames.length > 0) {
16324
+ sequences.push({
16325
+ frameUrls: flashFrames,
16326
+ originX,
16327
+ originY,
16328
+ frameDuration: 30,
16329
+ scale: 0.5
16330
+ });
16331
+ }
16332
+ return {
16333
+ particles: particles.filter((pc) => pc.spriteUrls.length > 0),
16334
+ sequences,
16335
+ overlays: [],
16336
+ screenShake: 6,
16337
+ screenFlash: { r: 255, g: 80, b: 0, duration: 200 }
16338
+ };
16339
+ },
16340
+ // =====================================================================
16341
+ // DEATH — dirt (gray) + explosion + black smoke + scorch (ground)
16342
+ // =====================================================================
16343
+ death: (originX, originY) => {
16344
+ const particles = [
16345
+ {
16346
+ spriteUrls: p(manifest, "dirt"),
16347
+ count: 8,
16348
+ originX,
16349
+ originY,
16350
+ spread: 10,
16351
+ velocityMin: 30,
16352
+ velocityMax: 100,
16353
+ angleMin: 0,
16354
+ angleMax: PI * 2,
16355
+ gravity: 100,
16356
+ tint: { r: 140, g: 140, b: 140 },
16357
+ scaleMin: 0.15,
16358
+ scaleMax: 0.35,
16359
+ lifetimeMin: 500,
16360
+ lifetimeMax: 900,
16361
+ fadeRate: -1.2
16362
+ }
16363
+ ];
16364
+ const sequences = [];
16365
+ const explosionFrames = anim(manifest, "explosion");
16366
+ if (explosionFrames.length > 0) {
16367
+ sequences.push({
16368
+ frameUrls: explosionFrames,
16369
+ originX,
16370
+ originY,
16371
+ frameDuration: 60,
16372
+ scale: 0.5
16373
+ });
16374
+ }
16375
+ const blackSmokeFrames = anim(manifest, "blackSmoke");
16376
+ if (blackSmokeFrames.length > 0) {
16377
+ sequences.push({
16378
+ frameUrls: blackSmokeFrames,
16379
+ originX,
16380
+ originY: originY - 10,
16381
+ frameDuration: 50,
16382
+ scale: 0.4,
16383
+ alpha: 0.7
16384
+ });
16385
+ }
16386
+ const overlays = [];
16387
+ const scorchUrls = p(manifest, "scorch");
16388
+ if (scorchUrls.length > 0) {
16389
+ overlays.push({
16390
+ spriteUrl: scorchUrls[0],
16391
+ originX,
16392
+ originY: originY + 10,
16393
+ alpha: 0.6,
16394
+ fadeRate: -0.15,
16395
+ scale: 0.4,
16396
+ lifetime: 4e3
16397
+ });
16398
+ }
16399
+ return {
16400
+ particles: particles.filter((pc) => pc.spriteUrls.length > 0),
16401
+ sequences,
16402
+ overlays,
16403
+ screenShake: 0,
16404
+ screenFlash: null
16405
+ };
16406
+ },
16407
+ // =====================================================================
16408
+ // BUFF — star (gold) + symbol + flare (gold, pulse)
16409
+ // =====================================================================
16410
+ buff: (originX, originY) => {
16411
+ const particles = [
16412
+ {
16413
+ spriteUrls: p(manifest, "star"),
16414
+ count: 6,
16415
+ originX,
16416
+ originY,
16417
+ spread: 15,
16418
+ velocityMin: 15,
16419
+ velocityMax: 50,
16420
+ angleMin: -PI,
16421
+ angleMax: 0,
16422
+ gravity: -30,
16423
+ tint: { r: 255, g: 215, b: 50 },
16424
+ scaleMin: 0.12,
16425
+ scaleMax: 0.25,
16426
+ lifetimeMin: 600,
16427
+ lifetimeMax: 1e3,
16428
+ fadeRate: -0.8,
16429
+ blendMode: "lighter"
16430
+ },
16431
+ {
16432
+ spriteUrls: p(manifest, "symbol"),
16433
+ count: 2,
16434
+ originX,
16435
+ originY: originY - 10,
16436
+ spread: 8,
16437
+ velocityMin: 5,
16438
+ velocityMax: 20,
16439
+ angleMin: -PI * 0.7,
16440
+ angleMax: -PI * 0.3,
16441
+ gravity: -20,
16442
+ tint: { r: 255, g: 230, b: 100 },
16443
+ scaleMin: 0.2,
16444
+ scaleMax: 0.35,
16445
+ lifetimeMin: 800,
16446
+ lifetimeMax: 1200,
16447
+ fadeRate: -0.6,
16448
+ blendMode: "lighter"
16449
+ }
16450
+ ];
16451
+ const overlays = [];
16452
+ const flareUrls = p(manifest, "flare");
16453
+ if (flareUrls.length > 0) {
16454
+ overlays.push({
16455
+ spriteUrl: flareUrls[0],
16456
+ originX,
16457
+ originY,
16458
+ alpha: 0.5,
16459
+ fadeRate: -0.3,
16460
+ pulseAmplitude: 0.3,
16461
+ pulseFrequency: 2,
16462
+ scale: 0.5,
16463
+ blendMode: "lighter",
16464
+ lifetime: 1500
16465
+ });
16466
+ }
16467
+ return {
16468
+ particles: particles.filter((pc) => pc.spriteUrls.length > 0),
16469
+ sequences: [],
16470
+ overlays,
16471
+ screenShake: 0,
16472
+ screenFlash: null
16473
+ };
16474
+ },
16475
+ // =====================================================================
16476
+ // DEBUFF — scorch (dark) + smoke (purple tint)
16477
+ // =====================================================================
16478
+ debuff: (originX, originY) => {
16479
+ const particles = [
16480
+ {
16481
+ spriteUrls: p(manifest, "scorch"),
16482
+ count: 4,
16483
+ originX,
16484
+ originY,
16485
+ spread: 12,
16486
+ velocityMin: 15,
16487
+ velocityMax: 40,
16488
+ angleMin: -PI,
16489
+ angleMax: 0,
16490
+ gravity: -20,
16491
+ tint: { r: 120, g: 40, b: 160 },
16492
+ scaleMin: 0.15,
16493
+ scaleMax: 0.3,
16494
+ lifetimeMin: 500,
16495
+ lifetimeMax: 800,
16496
+ fadeRate: -1
16497
+ },
16498
+ {
16499
+ spriteUrls: p(manifest, "smoke").slice(0, 3),
16500
+ count: 3,
16501
+ originX,
16502
+ originY,
16503
+ spread: 10,
16504
+ velocityMin: 8,
16505
+ velocityMax: 25,
16506
+ angleMin: -PI * 0.8,
16507
+ angleMax: -PI * 0.2,
16508
+ gravity: -15,
16509
+ tint: { r: 100, g: 50, b: 140 },
16510
+ scaleMin: 0.2,
16511
+ scaleMax: 0.35,
16512
+ lifetimeMin: 600,
16513
+ lifetimeMax: 1e3,
16514
+ fadeRate: -0.8
16515
+ }
16516
+ ];
16517
+ const overlays = [];
16518
+ const circleUrls = p(manifest, "circle");
16519
+ if (circleUrls.length > 0) {
16520
+ overlays.push({
16521
+ spriteUrl: circleUrls[0],
16522
+ originX,
16523
+ originY,
16524
+ alpha: 0.4,
16525
+ fadeRate: -0.4,
16526
+ pulseAmplitude: 0.15,
16527
+ pulseFrequency: 2,
16528
+ scale: 0.45,
16529
+ lifetime: 1200
16530
+ });
16531
+ }
16532
+ return {
16533
+ particles: particles.filter((pc) => pc.spriteUrls.length > 0),
16534
+ sequences: [],
16535
+ overlays,
16536
+ screenShake: 0,
16537
+ screenFlash: null
16538
+ };
16539
+ },
16540
+ // =====================================================================
16541
+ // AOE — explosion (large) + flame + spark (radial) + screen shake
16542
+ // =====================================================================
16543
+ aoe: (originX, originY) => {
16544
+ const particles = [
16545
+ {
16546
+ spriteUrls: p(manifest, "flame"),
16547
+ count: 10,
16548
+ originX,
16549
+ originY,
16550
+ spread: 20,
16551
+ velocityMin: 40,
16552
+ velocityMax: 140,
16553
+ angleMin: 0,
16554
+ angleMax: PI * 2,
16555
+ gravity: 40,
16556
+ tint: { r: 255, g: 140, b: 30 },
16557
+ scaleMin: 0.2,
16558
+ scaleMax: 0.5,
16559
+ lifetimeMin: 400,
16560
+ lifetimeMax: 800,
16561
+ fadeRate: -1.5
16562
+ },
16563
+ {
16564
+ spriteUrls: p(manifest, "spark"),
16565
+ count: 15,
16566
+ originX,
16567
+ originY,
16568
+ spread: 15,
16569
+ velocityMin: 60,
16570
+ velocityMax: 200,
16571
+ angleMin: 0,
16572
+ angleMax: PI * 2,
16573
+ gravity: 60,
16574
+ tint: { r: 255, g: 180, b: 60 },
16575
+ scaleMin: 0.06,
16576
+ scaleMax: 0.15,
16577
+ lifetimeMin: 200,
16578
+ lifetimeMax: 500,
16579
+ fadeRate: -2.5
16580
+ }
16581
+ ];
16582
+ const sequences = [];
16583
+ const explosionFrames = anim(manifest, "explosion");
16584
+ if (explosionFrames.length > 0) {
16585
+ sequences.push({
16586
+ frameUrls: explosionFrames,
16587
+ originX,
16588
+ originY,
16589
+ frameDuration: 50,
16590
+ scale: 0.6
16591
+ });
16592
+ }
16593
+ return {
16594
+ particles: particles.filter((pc) => pc.spriteUrls.length > 0),
16595
+ sequences,
16596
+ overlays: [],
16597
+ screenShake: 5,
16598
+ screenFlash: { r: 255, g: 160, b: 0, duration: 180 }
16599
+ };
16600
+ }
16601
+ };
16602
+ }
16603
+ var ACTION_EMOJI = {
16604
+ melee: { emoji: "\u2694\uFE0F", color: "var(--color-error)", label: "Slash" },
16605
+ ranged: { emoji: "\u{1F3F9}", color: "var(--color-warning)", label: "Arrow" },
16606
+ magic: { emoji: "\u2728", color: "var(--color-primary)", label: "Spell" },
16607
+ heal: { emoji: "\u{1F49A}", color: "var(--color-success)", label: "Heal" },
16608
+ buff: { emoji: "\u2B06\uFE0F", color: "var(--color-info)", label: "Buff" },
16609
+ debuff: { emoji: "\u2B07\uFE0F", color: "var(--color-warning)", label: "Debuff" },
16610
+ shield: { emoji: "\u{1F6E1}\uFE0F", color: "var(--color-info)", label: "Shield" },
16611
+ aoe: { emoji: "\u{1F4A5}", color: "var(--color-error)", label: "Explosion" },
16612
+ critical: { emoji: "\u{1F525}", color: "var(--color-error)", label: "Critical" },
16613
+ defend: { emoji: "\u{1F6E1}\uFE0F", color: "var(--color-info)", label: "Defend" },
16614
+ hit: { emoji: "\u{1F4A5}", color: "var(--color-error)", label: "Hit" },
16615
+ death: { emoji: "\u{1F480}", color: "var(--color-error)", label: "Death" }
16616
+ };
16617
+ function CanvasEffectEngine({
16618
+ actionType,
16619
+ x,
16620
+ y,
16621
+ duration = 2e3,
16622
+ intensity = 1,
16623
+ onComplete,
16624
+ className,
16625
+ assetManifest,
16626
+ width = 400,
16627
+ height = 300
16628
+ }) {
16629
+ const canvasRef = useRef(null);
16630
+ const stateRef = useRef({ ...EMPTY_EFFECT_STATE });
16631
+ const lastTimeRef = useRef(0);
16632
+ const rafRef = useRef(0);
16633
+ const imageCacheRef = useRef(/* @__PURE__ */ new Map());
16634
+ const [shakeOffset, setShakeOffset] = useState({ x: 0, y: 0 });
16635
+ const [flash, setFlash] = useState(null);
16636
+ const shakeRef = useRef({ x: 0, y: 0, intensity: 0 });
16637
+ const presets = useMemo(() => createCombatPresets(assetManifest), [assetManifest]);
16638
+ const spriteUrls = useMemo(() => getAllEffectSpriteUrls(assetManifest), [assetManifest]);
16639
+ useEffect(() => {
16640
+ const cache = imageCacheRef.current;
16641
+ for (const url of spriteUrls) {
16642
+ if (!cache.has(url)) {
16643
+ const img = new Image();
16644
+ img.crossOrigin = "anonymous";
16645
+ img.src = url;
16646
+ cache.set(url, img);
16647
+ }
16648
+ }
16649
+ }, [spriteUrls]);
16650
+ const getImage = useCallback((url) => {
16651
+ const img = imageCacheRef.current.get(url);
16652
+ return img?.complete ? img : void 0;
16653
+ }, []);
16654
+ useEffect(() => {
16655
+ const now = performance.now();
16656
+ const effectX = x || width / 2;
16657
+ const effectY = y || height / 2;
16658
+ const preset = presets[actionType](effectX, effectY);
16659
+ const state = stateRef.current;
16660
+ for (const emitter of preset.particles) {
16661
+ const scaledEmitter = { ...emitter, count: Math.round(emitter.count * intensity) };
16662
+ state.particles.push(...spawnParticles(scaledEmitter, now));
16663
+ }
16664
+ for (const seqConfig of preset.sequences) {
16665
+ state.sequences.push(spawnSequence(seqConfig, now));
16666
+ }
16667
+ for (const ovConfig of preset.overlays) {
16668
+ state.overlays.push(spawnOverlay(ovConfig, now));
16669
+ }
16670
+ if (preset.screenShake > 0) {
16671
+ shakeRef.current.intensity = preset.screenShake * intensity;
16672
+ }
16673
+ if (preset.screenFlash) {
16674
+ const { r, g, b, duration: flashDur } = preset.screenFlash;
16675
+ setFlash({ color: `rgb(${r}, ${g}, ${b})`, alpha: 0.3 });
16676
+ setTimeout(() => setFlash(null), flashDur);
16677
+ }
16678
+ const timer = setTimeout(() => {
16679
+ onComplete?.();
16680
+ }, duration);
16681
+ return () => clearTimeout(timer);
16682
+ }, []);
16683
+ useEffect(() => {
16684
+ const canvas = canvasRef.current;
16685
+ if (!canvas) return;
16686
+ const ctx = canvas.getContext("2d");
16687
+ if (!ctx) return;
16688
+ function loop(animTime) {
16689
+ const delta = lastTimeRef.current > 0 ? animTime - lastTimeRef.current : 16;
16690
+ lastTimeRef.current = animTime;
16691
+ stateRef.current = updateEffectState(stateRef.current, animTime, delta);
16692
+ if (shakeRef.current.intensity > 0.2) {
16693
+ const i = shakeRef.current.intensity;
16694
+ shakeRef.current.x = (Math.random() - 0.5) * i * 2;
16695
+ shakeRef.current.y = (Math.random() - 0.5) * i * 2;
16696
+ shakeRef.current.intensity *= 0.85;
16697
+ setShakeOffset({ x: shakeRef.current.x, y: shakeRef.current.y });
16698
+ } else if (shakeRef.current.intensity > 0) {
16699
+ shakeRef.current = { x: 0, y: 0, intensity: 0 };
16700
+ setShakeOffset({ x: 0, y: 0 });
16701
+ }
16702
+ ctx.clearRect(0, 0, width, height);
16703
+ drawEffectState(ctx, stateRef.current, animTime, getImage);
16704
+ if (hasActiveEffects(stateRef.current)) {
16705
+ rafRef.current = requestAnimationFrame(loop);
16706
+ }
16707
+ }
16708
+ rafRef.current = requestAnimationFrame(loop);
16709
+ return () => cancelAnimationFrame(rafRef.current);
16710
+ }, [width, height, getImage]);
16711
+ const shakeStyle = shakeOffset.x !== 0 || shakeOffset.y !== 0 ? { transform: `translate(${shakeOffset.x}px, ${shakeOffset.y}px)` } : {};
16712
+ return /* @__PURE__ */ jsxs(
16713
+ Box,
16714
+ {
16715
+ className: cn("absolute inset-0 pointer-events-none z-10", className),
16716
+ style: shakeStyle,
16717
+ children: [
16718
+ flash && /* @__PURE__ */ jsx(
16719
+ Box,
16720
+ {
16721
+ className: "absolute inset-0 z-20 pointer-events-none rounded-lg",
16722
+ style: { backgroundColor: flash.color, opacity: flash.alpha }
16723
+ }
16724
+ ),
16725
+ /* @__PURE__ */ jsx(
16726
+ "canvas",
16727
+ {
16728
+ ref: canvasRef,
16729
+ width,
16730
+ height,
16731
+ className: "absolute inset-0 w-full h-full",
16732
+ style: { imageRendering: "pixelated" }
16733
+ }
16734
+ )
16735
+ ]
16736
+ }
16737
+ );
16738
+ }
16739
+ function EmojiEffect({
16740
+ actionType,
16741
+ x,
16742
+ y,
16743
+ duration = 800,
16744
+ intensity = 1,
16745
+ onComplete,
16746
+ className,
16747
+ effectSpriteUrl,
16748
+ assetBaseUrl
16749
+ }) {
16750
+ const [visible, setVisible] = useState(true);
16751
+ const [phase, setPhase] = useState("enter");
16752
+ useEffect(() => {
16753
+ const enterTimer = setTimeout(() => setPhase("active"), 100);
16754
+ const exitTimer = setTimeout(() => setPhase("exit"), duration * 0.7);
16755
+ const doneTimer = setTimeout(() => {
16756
+ setVisible(false);
16757
+ onComplete?.();
16758
+ }, duration);
16759
+ return () => {
16760
+ clearTimeout(enterTimer);
16761
+ clearTimeout(exitTimer);
16762
+ clearTimeout(doneTimer);
16763
+ };
16764
+ }, [duration, onComplete]);
16765
+ if (!visible) return null;
16766
+ const config = ACTION_EMOJI[actionType] ?? ACTION_EMOJI.melee;
16767
+ const scaleVal = phase === "enter" ? 0.3 : phase === "active" ? intensity : 0.5;
16768
+ const opacity = phase === "exit" ? 0 : 1;
16769
+ const resolvedSpriteUrl = effectSpriteUrl ? effectSpriteUrl.startsWith("http") || effectSpriteUrl.startsWith("/") ? effectSpriteUrl : assetBaseUrl ? `${assetBaseUrl.replace(/\/$/, "")}/${effectSpriteUrl}` : effectSpriteUrl : void 0;
16770
+ return /* @__PURE__ */ jsxs(
16771
+ Box,
16772
+ {
16773
+ className: cn(
16774
+ "fixed pointer-events-none z-50 flex items-center justify-center",
16775
+ "transition-all ease-out",
16776
+ className
16777
+ ),
16778
+ style: {
16779
+ left: x,
16780
+ top: y,
16781
+ transform: `translate(-50%, -50%) scale(${scaleVal})`,
16782
+ opacity,
16783
+ transitionDuration: phase === "enter" ? "100ms" : "300ms"
16784
+ },
16785
+ children: [
16786
+ /* @__PURE__ */ jsx(
16787
+ Box,
16788
+ {
16789
+ className: "absolute rounded-full animate-ping",
16790
+ style: {
16791
+ width: 48 * intensity,
16792
+ height: 48 * intensity,
16793
+ backgroundColor: config.color,
16794
+ opacity: 0.25
16795
+ }
16796
+ }
16797
+ ),
16798
+ resolvedSpriteUrl ? /* @__PURE__ */ jsx(
16799
+ "img",
16800
+ {
16801
+ src: resolvedSpriteUrl,
16802
+ alt: config.label,
16803
+ className: "relative drop-shadow-lg",
16804
+ style: {
16805
+ width: `${3 * intensity}rem`,
16806
+ height: `${3 * intensity}rem`,
16807
+ objectFit: "contain",
16808
+ imageRendering: "pixelated"
16809
+ }
16810
+ }
16811
+ ) : /* @__PURE__ */ jsx(
16812
+ "span",
16813
+ {
16814
+ className: "relative text-3xl drop-shadow-lg",
16815
+ style: { fontSize: `${2 * intensity}rem` },
16816
+ role: "img",
16817
+ "aria-label": config.label,
16818
+ children: config.emoji
16819
+ }
16820
+ )
16821
+ ]
16822
+ }
16823
+ );
16824
+ }
16825
+ function CanvasEffect(props) {
16826
+ const eventBus = useEventBus();
16827
+ const { completeEvent, onComplete, ...rest } = props;
16828
+ const handleComplete = useCallback(() => {
16829
+ if (completeEvent) eventBus.emit(`UI:${completeEvent}`, {});
16830
+ onComplete?.();
16831
+ }, [completeEvent, eventBus, onComplete]);
16832
+ const enhancedProps = { ...rest, onComplete: handleComplete };
16833
+ if (props.assetManifest) {
16834
+ return /* @__PURE__ */ jsx(CanvasEffectEngine, { ...enhancedProps, assetManifest: props.assetManifest });
16835
+ }
16836
+ return /* @__PURE__ */ jsx(EmojiEffect, { ...enhancedProps });
16837
+ }
16838
+ CanvasEffect.displayName = "CanvasEffect";
15595
16839
  function VStackPattern({
15596
16840
  gap = "md",
15597
16841
  align = "stretch",
@@ -15616,7 +16860,7 @@ function HStackPattern({
15616
16860
  }
15617
16861
  HStackPattern.displayName = "HStackPattern";
15618
16862
  function BoxPattern({
15619
- p,
16863
+ p: p2,
15620
16864
  m,
15621
16865
  bg = "transparent",
15622
16866
  border = false,
@@ -15629,7 +16873,7 @@ function BoxPattern({
15629
16873
  return /* @__PURE__ */ jsx(
15630
16874
  Box,
15631
16875
  {
15632
- padding: p,
16876
+ padding: p2,
15633
16877
  margin: m,
15634
16878
  bg,
15635
16879
  border,
@@ -16542,79 +17786,87 @@ var COMPONENT_REGISTRY = {
16542
17786
  // Map patterns
16543
17787
  MapViewPattern,
16544
17788
  // Custom pattern
16545
- CustomPattern
16546
- };
16547
- var PATTERN_TO_COMPONENT = {
16548
- "page-header": "PageHeader",
16549
- "entity-table": "DataTable",
16550
- "entity-cards": "CardGrid",
16551
- "entity-detail": "DetailPanel",
16552
- "detail-panel": "DetailPanel",
16553
- "entity-list": "List",
16554
- "master-detail": "MasterDetail",
16555
- "search-bar": "SearchInput",
16556
- "empty-state": "EmptyState",
16557
- "loading-state": "LoadingState",
16558
- breadcrumb: "Breadcrumb",
16559
- stats: "StatCard",
16560
- "form-section": "Form",
16561
- form: "Form",
16562
- "form-actions": "ButtonGroup",
16563
- "filter-group": "ButtonGroup",
16564
- "button-group": "ButtonGroup",
16565
- // Layout patterns
16566
- vstack: "VStackPattern",
16567
- hstack: "HStackPattern",
16568
- box: "BoxPattern",
16569
- grid: "GridPattern",
16570
- center: "CenterPattern",
16571
- spacer: "SpacerPattern",
16572
- divider: "DividerPattern",
16573
- // Component patterns - Interactive
16574
- button: "ButtonPattern",
16575
- "icon-button": "IconButtonPattern",
16576
- link: "LinkPattern",
16577
- // Component patterns - Display
16578
- text: "TextPattern",
16579
- heading: "HeadingPattern",
16580
- badge: "BadgePattern",
16581
- avatar: "AvatarPattern",
16582
- icon: "IconPattern",
16583
- image: "ImagePattern",
16584
- card: "CardPattern",
16585
- "progress-bar": "ProgressBarPattern",
16586
- spinner: "SpinnerPattern",
16587
- // Component patterns - Form inputs
16588
- input: "InputPattern",
16589
- textarea: "TextareaPattern",
16590
- select: "SelectPattern",
16591
- checkbox: "CheckboxPattern",
16592
- radio: "RadioPattern",
16593
- label: "LabelPattern",
16594
- // Component patterns - Feedback
16595
- alert: "AlertPattern",
16596
- tooltip: "TooltipPattern",
16597
- popover: "PopoverPattern",
16598
- // Component patterns - Navigation
16599
- menu: "MenuPattern",
16600
- accordion: "AccordionPattern",
16601
- // Component patterns - Layout
16602
- container: "ContainerPattern",
16603
- "simple-grid": "SimpleGridPattern",
16604
- "float-button": "FloatButtonPattern",
16605
- // Custom pattern
16606
- custom: "CustomPattern",
16607
- // Map patterns
16608
- "map-view": "MapViewPattern"
17789
+ CustomPattern,
17790
+ // Direct components (not wrapped in pattern adapters)
17791
+ Stack,
17792
+ VStack: VStackPattern,
17793
+ HStack: HStackPattern,
17794
+ Typography: TextPattern,
17795
+ Tabs,
17796
+ StatDisplay,
17797
+ StatBadge,
17798
+ StatusDot,
17799
+ TrendIndicator,
17800
+ RangeSlider,
17801
+ StarRating,
17802
+ LineChart,
17803
+ DataGrid,
17804
+ DataList,
17805
+ CalendarGrid,
17806
+ Lightbox,
17807
+ UploadDropZone,
17808
+ WizardNavigation,
17809
+ WizardProgress,
17810
+ Meter,
17811
+ ActionButtons,
17812
+ HealthBar,
17813
+ ScoreDisplay,
17814
+ DPad,
17815
+ CanvasEffect,
17816
+ CombatLog,
17817
+ DialogueBox,
17818
+ InventoryPanel,
17819
+ GameHud,
17820
+ GameMenu,
17821
+ FilterGroup: ButtonGroup,
17822
+ ErrorState: EmptyState,
17823
+ Toast: AlertPattern,
17824
+ // Plain component name aliases — component-mapping.json returns these names
17825
+ // (e.g., "button" → "Button") but registry above uses "ButtonPattern" keys.
17826
+ Button: ButtonPattern,
17827
+ IconButton: IconButtonPattern,
17828
+ Link: LinkPattern,
17829
+ Text: TextPattern,
17830
+ Heading: HeadingPattern,
17831
+ Badge: BadgePattern,
17832
+ Avatar: AvatarPattern,
17833
+ Icon: IconPattern,
17834
+ Image: ImagePattern,
17835
+ Card: CardPattern,
17836
+ ProgressBar: ProgressBarPattern,
17837
+ Spinner: SpinnerPattern,
17838
+ Input: InputPattern,
17839
+ Textarea: TextareaPattern,
17840
+ Select: SelectPattern,
17841
+ Checkbox: CheckboxPattern,
17842
+ Radio: RadioPattern,
17843
+ Label: LabelPattern,
17844
+ Alert: AlertPattern,
17845
+ Tooltip: TooltipPattern,
17846
+ Popover: PopoverPattern,
17847
+ Menu: MenuPattern,
17848
+ Accordion: AccordionPattern,
17849
+ Container: ContainerPattern,
17850
+ SimpleGrid: SimpleGridPattern,
17851
+ FloatButton: FloatButtonPattern,
17852
+ MapView: MapViewPattern,
17853
+ Box: BoxPattern,
17854
+ Grid: GridPattern,
17855
+ Center: CenterPattern,
17856
+ Spacer: SpacerPattern,
17857
+ Divider: DividerPattern
16609
17858
  };
16610
17859
  function getComponentForPattern(patternType) {
16611
- const componentName = PATTERN_TO_COMPONENT[patternType];
16612
- if (!componentName) {
17860
+ const mapping = getComponentForPattern$1(patternType);
17861
+ if (!mapping) {
16613
17862
  return null;
16614
17863
  }
16615
- return COMPONENT_REGISTRY[componentName] ?? null;
17864
+ const name = typeof mapping === "string" ? mapping : mapping.component;
17865
+ if (!name) return null;
17866
+ return COMPONENT_REGISTRY[name] ?? null;
16616
17867
  }
16617
17868
  var PATTERNS_WITH_CHILDREN = /* @__PURE__ */ new Set([
17869
+ "stack",
16618
17870
  "vstack",
16619
17871
  "hstack",
16620
17872
  "box",
@@ -16886,7 +18138,7 @@ function SlotContentRenderer({
16886
18138
  className: "slot-content",
16887
18139
  "data-pattern": content.pattern,
16888
18140
  "data-id": content.id,
16889
- children: /* @__PURE__ */ jsx(PatternComponent, { ...restProps, onDismiss, children: renderedChildren })
18141
+ children: /* @__PURE__ */ jsx(PatternComponent, { ...restProps, children: renderedChildren })
16890
18142
  }
16891
18143
  );
16892
18144
  }
@@ -16948,4 +18200,4 @@ function UISlotRenderer({
16948
18200
  }
16949
18201
  UISlotRenderer.displayName = "UISlotRenderer";
16950
18202
 
16951
- export { Accordion, ActionButton, ActionButtons, Alert, AnimatedCounter, Avatar, Badge, Box, Breadcrumb, Button, ButtonGroup, CalendarGrid, Card, Card2, CardBody, CardContent, CardFooter, CardGrid, CardHeader, CardTitle, Carousel, Center, ChartLegend, Checkbox, ChoiceButton, CodeBlock, CombatLog, ComboCounter, ConditionalWrapper, ConfettiEffect, Container, ControlButton, CraftingRecipe, DIAMOND_TOP_Y, DPad, DamageNumber, DataGrid, DataList, DataTable, DateRangeSelector, DayCell, DetailPanel, DialogueBox, DialogueBubble, Divider, Drawer, EmptyState, EnemyPlate, EntityDisplayEvents, ErrorBoundary, ErrorState, FEATURE_COLORS, FLOOR_HEIGHT, FilterGroup, Flex, FlipCard, FlipContainer, FloatingActionButton, Form, FormField, FormSectionHeader, GameCanvas2D, GameHud, GameMenu, GameOverScreen, GraphView, Grid, HStack, Heading, HealthBar, HealthPanel, Icon, InfiniteScrollSentinel, Input, InputGroup, InventoryGrid, InventoryPanel, IsometricCanvas, IsometricCanvas_default, ItemSlot, Label, LawReferenceTooltip, Lightbox, LineChart, LoadingState, MapView, MarkdownContent, MasterDetail, Menu, Meter, MiniMap, Modal, NumberStepper, Overlay, PageHeader, Pagination, PlatformerCanvas, Popover, PowerupSlots, ProgressBar, ProgressDots, PullToRefresh, QuestTracker, QuizBlock, Radio, RangeSlider, RelationSelect, RepeatableFormSection, ResourceBar, ResourceCounter, ScaledDiagram, ScoreBoard, ScoreDisplay, SearchInput, Select, SidePanel, SimpleGrid, Skeleton, SlotContentRenderer, SortableList, Spacer, Spinner, Sprite, Stack, StarRating, StatBadge, StatCard, StatDisplay, StateIndicator, StatusDot, StatusEffect, SuspenseConfigProvider, SwipeableRow, Switch, TILE_HEIGHT, TILE_WIDTH, Tabs, Text, TextHighlight, Textarea, ThemeSelector, ThemeToggle, TimeSlotCell, TimerDisplay, Toast, Tooltip, TrendIndicator, TurnIndicator, TurnPanel, TypewriterText, Typography, UISlotComponent, UISlotRenderer, UnitCommandBar, UploadDropZone, VStack, ViolationAlert, WaypointMarker, WizardNavigation, WizardProgress, XPBar, drawSprite, isoToScreen, screenToIso, useCamera, useImageCache };
18203
+ export { Accordion, ActionButton, ActionButtons, Alert, AnimatedCounter, Avatar, Badge, Box, Breadcrumb, Button, ButtonGroup, CalendarGrid, CanvasEffect, Card, Card2, CardBody, CardContent, CardFooter, CardGrid, CardHeader, CardTitle, Carousel, Center, ChartLegend, Checkbox, ChoiceButton, CodeBlock, CombatLog, ComboCounter, ConditionalWrapper, ConfettiEffect, Container, ControlButton, CraftingRecipe, DIAMOND_TOP_Y, DPad, DamageNumber, DataGrid, DataList, DataTable, DateRangeSelector, DayCell, DetailPanel, DialogueBox, DialogueBubble, Divider, Drawer, EmptyState, EnemyPlate, EntityDisplayEvents, ErrorBoundary, ErrorState, FEATURE_COLORS, FLOOR_HEIGHT, FilterGroup, Flex, FlipCard, FlipContainer, FloatingActionButton, Form, FormField, FormSectionHeader, GameCanvas2D, GameHud, GameMenu, GameOverScreen, GraphView, Grid, HStack, Heading, HealthBar, HealthPanel, Icon, InfiniteScrollSentinel, Input, InputGroup, InventoryGrid, InventoryPanel, IsometricCanvas, IsometricCanvas_default, ItemSlot, Label, LawReferenceTooltip, Lightbox, LineChart, LoadingState, MapView, MarkdownContent, MasterDetail, Menu, Meter, MiniMap, Modal, NumberStepper, Overlay, PageHeader, Pagination, PlatformerCanvas, Popover, PowerupSlots, ProgressBar, ProgressDots, PullToRefresh, QuestTracker, QuizBlock, Radio, RangeSlider, RelationSelect, RepeatableFormSection, ResourceBar, ResourceCounter, ScaledDiagram, ScoreBoard, ScoreDisplay, SearchInput, Select, SidePanel, SimpleGrid, Skeleton, SlotContentRenderer, SortableList, Spacer, Spinner, Sprite, Stack, StarRating, StatBadge, StatCard, StatDisplay, StateIndicator, StatusDot, StatusEffect, SuspenseConfigProvider, SwipeableRow, Switch, TILE_HEIGHT, TILE_WIDTH, Tabs, Text, TextHighlight, Textarea, ThemeSelector, ThemeToggle, TimeSlotCell, TimerDisplay, Toast, Tooltip, TrendIndicator, TurnIndicator, TurnPanel, TypewriterText, Typography, UISlotComponent, UISlotRenderer, UnitCommandBar, UploadDropZone, VStack, ViolationAlert, WaypointMarker, WizardNavigation, WizardProgress, XPBar, drawSprite, isoToScreen, screenToIso, useCamera, useImageCache };