@almadar/ui 5.47.0 → 5.49.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.
Files changed (45) hide show
  1. package/dist/avl/index.cjs +2998 -443
  2. package/dist/avl/index.css +504 -0
  3. package/dist/avl/index.js +2998 -444
  4. package/dist/components/core/templates/index.d.ts +3 -0
  5. package/dist/components/game/atoms/ActionButton.d.ts +4 -1
  6. package/dist/components/game/atoms/ComboCounter.d.ts +4 -1
  7. package/dist/components/game/atoms/ControlButton.d.ts +4 -2
  8. package/dist/components/game/atoms/DamageNumber.d.ts +4 -1
  9. package/dist/components/game/atoms/ResourceCounter.d.ts +4 -1
  10. package/dist/components/game/atoms/ScoreDisplay.d.ts +4 -1
  11. package/dist/components/game/atoms/StateIndicator.d.ts +4 -7
  12. package/dist/components/game/atoms/StatusEffect.d.ts +4 -1
  13. package/dist/components/game/atoms/TurnIndicator.d.ts +4 -1
  14. package/dist/components/game/atoms/WaypointMarker.d.ts +4 -1
  15. package/dist/components/game/molecules/EnemyPlate.d.ts +4 -1
  16. package/dist/components/game/molecules/StatBadge.d.ts +4 -1
  17. package/dist/components/game/molecules/three/index.cjs +1428 -346
  18. package/dist/components/game/molecules/three/index.css +0 -5
  19. package/dist/components/game/molecules/three/index.js +1429 -347
  20. package/dist/components/game/organisms/CastleBoard.d.ts +10 -1
  21. package/dist/components/game/organisms/GameBoard3D.d.ts +62 -0
  22. package/dist/components/game/organisms/PlatformerBoard.d.ts +51 -0
  23. package/dist/components/game/organisms/RoguelikeBoard.d.ts +59 -0
  24. package/dist/components/game/organisms/TowerDefenseBoard.d.ts +64 -0
  25. package/dist/components/game/organisms/index.d.ts +4 -0
  26. package/dist/components/game/organisms/puzzles/event-handler/EventHandlerBoard.d.ts +9 -1
  27. package/dist/components/game/organisms/puzzles/state-architect/StateArchitectBoard.d.ts +14 -1
  28. package/dist/components/game/templates/GameCanvas3DBattleTemplate.d.ts +22 -35
  29. package/dist/components/game/templates/GameCanvas3DCastleTemplate.d.ts +23 -23
  30. package/dist/components/game/templates/GameCanvas3DWorldMapTemplate.d.ts +27 -29
  31. package/dist/components/game/templates/PlatformerTemplate.d.ts +26 -0
  32. package/dist/components/game/templates/RoguelikeTemplate.d.ts +23 -0
  33. package/dist/components/game/templates/TowerDefenseTemplate.d.ts +43 -0
  34. package/dist/components/index.cjs +2991 -422
  35. package/dist/components/index.css +504 -0
  36. package/dist/components/index.js +2985 -424
  37. package/dist/docs/index.css +504 -0
  38. package/dist/providers/index.cjs +2981 -426
  39. package/dist/providers/index.css +504 -0
  40. package/dist/providers/index.js +2981 -427
  41. package/dist/runtime/index.cjs +2982 -427
  42. package/dist/runtime/index.css +504 -0
  43. package/dist/runtime/index.js +2982 -428
  44. package/package.json +1 -1
  45. package/tailwind-preset.cjs +1 -0
@@ -41,6 +41,12 @@ var core$1 = require('@dnd-kit/core');
41
41
  var sortable = require('@dnd-kit/sortable');
42
42
  var utilities = require('@dnd-kit/utilities');
43
43
  var react = require('@xyflow/react');
44
+ var THREE = require('three');
45
+ var GLTFLoader_js = require('three/examples/jsm/loaders/GLTFLoader.js');
46
+ var OBJLoader_js = require('three/examples/jsm/loaders/OBJLoader.js');
47
+ var GLTFLoader = require('three/examples/jsm/loaders/GLTFLoader');
48
+ var fiber = require('@react-three/fiber');
49
+ var drei = require('@react-three/drei');
44
50
  var patterns = require('@almadar/patterns');
45
51
  var runtime = require('@almadar/runtime');
46
52
  var OrbitalServerRuntime = require('@almadar/runtime/OrbitalServerRuntime');
@@ -90,6 +96,7 @@ var ReactMarkdown__default = /*#__PURE__*/_interopDefault(ReactMarkdown);
90
96
  var remarkGfm__default = /*#__PURE__*/_interopDefault(remarkGfm);
91
97
  var remarkMath__default = /*#__PURE__*/_interopDefault(remarkMath);
92
98
  var rehypeKatex__default = /*#__PURE__*/_interopDefault(rehypeKatex);
99
+ var THREE__namespace = /*#__PURE__*/_interopNamespace(THREE);
93
100
 
94
101
  var __defProp = Object.defineProperty;
95
102
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -455,7 +462,7 @@ var init_Box = __esm({
455
462
  position,
456
463
  className,
457
464
  children,
458
- as: Component = "div",
465
+ as: Component2 = "div",
459
466
  action,
460
467
  actionPayload,
461
468
  hoverEvent,
@@ -487,7 +494,7 @@ var init_Box = __esm({
487
494
  }, [hoverEvent, eventBus, onMouseLeave]);
488
495
  const isClickable = action || onClick;
489
496
  return React82__namespace.default.createElement(
490
- Component,
497
+ Component2,
491
498
  {
492
499
  ref,
493
500
  className: cn(
@@ -625,9 +632,9 @@ function resolvePhosphor(name, weight, family) {
625
632
  (lib) => {
626
633
  const PhosphorComp = lib[target];
627
634
  if (!PhosphorComp || typeof PhosphorComp !== "object") return null;
628
- const Component = PhosphorComp;
635
+ const Component2 = PhosphorComp;
629
636
  const Adapter = (props) => /* @__PURE__ */ jsxRuntime.jsx(
630
- Component,
637
+ Component2,
631
638
  {
632
639
  weight,
633
640
  className: props.className,
@@ -651,9 +658,9 @@ function resolveTabler(name, family) {
651
658
  (lib) => {
652
659
  const TablerComp = lib[target];
653
660
  if (!TablerComp || typeof TablerComp !== "object") return null;
654
- const Component = TablerComp;
661
+ const Component2 = TablerComp;
655
662
  const Adapter = (props) => /* @__PURE__ */ jsxRuntime.jsx(
656
- Component,
663
+ Component2,
657
664
  {
658
665
  stroke: props.strokeWidth ?? 1.5,
659
666
  className: props.className,
@@ -677,9 +684,9 @@ function resolveFa(name, family) {
677
684
  (lib) => {
678
685
  const FaComp = lib[target];
679
686
  if (!FaComp || typeof FaComp !== "function") return null;
680
- const Component = FaComp;
687
+ const Component2 = FaComp;
681
688
  const Adapter = (props) => /* @__PURE__ */ jsxRuntime.jsx(
682
- Component,
689
+ Component2,
683
690
  {
684
691
  className: props.className,
685
692
  style: props.style,
@@ -1634,8 +1641,8 @@ var init_Typography = __esm({
1634
1641
  children
1635
1642
  }) => {
1636
1643
  const variant = variantProp ?? (level ? `h${level}` : "body1");
1637
- const Component = as || defaultElements[variant];
1638
- const Comp = Component;
1644
+ const Component2 = as || defaultElements[variant];
1645
+ const Comp = Component2;
1639
1646
  return /* @__PURE__ */ jsxRuntime.jsx(
1640
1647
  Comp,
1641
1648
  {
@@ -4190,10 +4197,10 @@ var init_Center = __esm({
4190
4197
  className,
4191
4198
  style,
4192
4199
  children,
4193
- as: Component = "div"
4200
+ as: Component2 = "div"
4194
4201
  }) => {
4195
4202
  const mergedStyle = minHeight ? { minHeight, ...style } : style;
4196
- const Comp = Component;
4203
+ const Comp = Component2;
4197
4204
  return /* @__PURE__ */ jsxRuntime.jsx(
4198
4205
  Comp,
4199
4206
  {
@@ -4824,7 +4831,7 @@ var init_Stack = __esm({
4824
4831
  className,
4825
4832
  style,
4826
4833
  children,
4827
- as: Component = "div",
4834
+ as: Component2 = "div",
4828
4835
  onClick,
4829
4836
  onKeyDown,
4830
4837
  role,
@@ -4842,7 +4849,7 @@ var init_Stack = __esm({
4842
4849
  };
4843
4850
  const isHorizontal = direction === "horizontal";
4844
4851
  const directionClass = responsive && isHorizontal ? reverse ? "flex-col-reverse md:flex-row-reverse" : "flex-col md:flex-row" : isHorizontal ? reverse ? "flex-row-reverse" : "flex-row" : reverse ? "flex-col-reverse" : "flex-col";
4845
- const Comp = Component;
4852
+ const Comp = Component2;
4846
4853
  return /* @__PURE__ */ jsxRuntime.jsx(
4847
4854
  Comp,
4848
4855
  {
@@ -7073,6 +7080,7 @@ var init_HealthBar = __esm({
7073
7080
  }
7074
7081
  });
7075
7082
  function ScoreDisplay({
7083
+ assetUrl = DEFAULT_ASSET_URL,
7076
7084
  value,
7077
7085
  label,
7078
7086
  icon,
@@ -7119,14 +7127,24 @@ function ScoreDisplay({
7119
7127
  className
7120
7128
  ),
7121
7129
  children: [
7122
- icon && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-shrink-0", children: typeof icon === "string" ? /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: icon }) : /* @__PURE__ */ jsxRuntime.jsx(Icon, { icon }) }),
7130
+ assetUrl ? /* @__PURE__ */ jsxRuntime.jsx(
7131
+ "img",
7132
+ {
7133
+ src: assetUrl,
7134
+ alt: "",
7135
+ width: 20,
7136
+ height: 20,
7137
+ style: { imageRendering: "pixelated", objectFit: "contain" },
7138
+ className: "flex-shrink-0"
7139
+ }
7140
+ ) : icon ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-shrink-0", children: typeof icon === "string" ? /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: icon }) : /* @__PURE__ */ jsxRuntime.jsx(Icon, { icon }) }) : null,
7123
7141
  label && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: label }),
7124
7142
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "tabular-nums", children: formattedValue })
7125
7143
  ]
7126
7144
  }
7127
7145
  );
7128
7146
  }
7129
- var sizeMap2;
7147
+ var sizeMap2, DEFAULT_ASSET_URL;
7130
7148
  var init_ScoreDisplay = __esm({
7131
7149
  "components/game/atoms/ScoreDisplay.tsx"() {
7132
7150
  "use client";
@@ -7138,10 +7156,12 @@ var init_ScoreDisplay = __esm({
7138
7156
  lg: "text-2xl",
7139
7157
  xl: "text-4xl"
7140
7158
  };
7159
+ DEFAULT_ASSET_URL = "https://almadar-kflow-assets.web.app/shared/effects/particles/star_01.png";
7141
7160
  ScoreDisplay.displayName = "ScoreDisplay";
7142
7161
  }
7143
7162
  });
7144
7163
  function ControlButton({
7164
+ assetUrl = DEFAULT_ASSET_URL2,
7145
7165
  label,
7146
7166
  icon,
7147
7167
  size = "md",
@@ -7188,7 +7208,7 @@ function ControlButton({
7188
7208
  },
7189
7209
  [isPressed, releaseEvent, eventBus, onRelease]
7190
7210
  );
7191
- return /* @__PURE__ */ jsxRuntime.jsxs(
7211
+ return /* @__PURE__ */ jsxRuntime.jsx(
7192
7212
  "button",
7193
7213
  {
7194
7214
  type: "button",
@@ -7209,20 +7229,27 @@ function ControlButton({
7209
7229
  disabled && "opacity-50 cursor-not-allowed",
7210
7230
  className
7211
7231
  ),
7212
- children: [
7213
- icon && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-2xl", children: typeof icon === "string" ? /^[a-zA-Z0-9-]+$/.test(icon) ? (() => {
7214
- const I = resolveIcon(icon);
7215
- return I ? /* @__PURE__ */ jsxRuntime.jsx(I, { className: "w-6 h-6" }) : null;
7216
- })() : icon : /* @__PURE__ */ (() => {
7217
- const I = icon;
7218
- return /* @__PURE__ */ jsxRuntime.jsx(I, { className: "w-6 h-6" });
7219
- })() }),
7220
- label && !icon && /* @__PURE__ */ jsxRuntime.jsx("span", { children: label })
7221
- ]
7232
+ children: assetUrl ? /* @__PURE__ */ jsxRuntime.jsx(
7233
+ "img",
7234
+ {
7235
+ src: assetUrl,
7236
+ alt: "",
7237
+ width: 24,
7238
+ height: 24,
7239
+ style: { imageRendering: "pixelated", objectFit: "contain" },
7240
+ className: "flex-shrink-0"
7241
+ }
7242
+ ) : icon ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-2xl", children: typeof icon === "string" ? /^[a-zA-Z0-9-]+$/.test(icon) ? (() => {
7243
+ const I = resolveIcon(icon);
7244
+ return I ? /* @__PURE__ */ jsxRuntime.jsx(I, { className: "w-6 h-6" }) : null;
7245
+ })() : icon : /* @__PURE__ */ (() => {
7246
+ const I = icon;
7247
+ return /* @__PURE__ */ jsxRuntime.jsx(I, { className: "w-6 h-6" });
7248
+ })() }) : label ? /* @__PURE__ */ jsxRuntime.jsx("span", { children: label }) : null
7222
7249
  }
7223
7250
  );
7224
7251
  }
7225
- var sizeMap3, shapeMap, variantMap;
7252
+ var sizeMap3, shapeMap, variantMap, DEFAULT_ASSET_URL2;
7226
7253
  var init_ControlButton = __esm({
7227
7254
  "components/game/atoms/ControlButton.tsx"() {
7228
7255
  "use client";
@@ -7245,6 +7272,7 @@ var init_ControlButton = __esm({
7245
7272
  secondary: "bg-secondary text-secondary-foreground border-border hover:bg-secondary-hover",
7246
7273
  ghost: "bg-transparent text-foreground border-border hover:bg-muted"
7247
7274
  };
7275
+ DEFAULT_ASSET_URL2 = "https://almadar-kflow-assets.web.app/shared/effects/particles/circle_01.png";
7248
7276
  ControlButton.displayName = "ControlButton";
7249
7277
  }
7250
7278
  });
@@ -7324,6 +7352,7 @@ var init_Sprite = __esm({
7324
7352
  }
7325
7353
  });
7326
7354
  function StateIndicator({
7355
+ assetUrl = DEFAULT_ASSET_URL3,
7327
7356
  state = "idle",
7328
7357
  label,
7329
7358
  size = "md",
@@ -7346,18 +7375,29 @@ function StateIndicator({
7346
7375
  className
7347
7376
  ),
7348
7377
  children: [
7349
- /* @__PURE__ */ jsxRuntime.jsx(Box, { as: "span", children: typeof config.icon === "string" ? /^[a-zA-Z0-9-]+$/.test(config.icon) ? /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: config.icon }) : config.icon : /* @__PURE__ */ jsxRuntime.jsx(Icon, { icon: config.icon }) }),
7378
+ /* @__PURE__ */ jsxRuntime.jsx(Box, { as: "span", children: assetUrl ? /* @__PURE__ */ jsxRuntime.jsx(
7379
+ "img",
7380
+ {
7381
+ src: assetUrl,
7382
+ alt: displayLabel,
7383
+ width: 16,
7384
+ height: 16,
7385
+ style: { imageRendering: "pixelated", objectFit: "contain" },
7386
+ className: "flex-shrink-0"
7387
+ }
7388
+ ) : typeof config.icon === "string" ? /^[a-zA-Z0-9-]+$/.test(config.icon) ? /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: config.icon }) : config.icon : /* @__PURE__ */ jsxRuntime.jsx(Icon, { icon: config.icon }) }),
7350
7389
  /* @__PURE__ */ jsxRuntime.jsx(Box, { as: "span", children: displayLabel })
7351
7390
  ]
7352
7391
  }
7353
7392
  );
7354
7393
  }
7355
- var DEFAULT_STATE_STYLES, DEFAULT_STYLE, SIZE_CLASSES;
7394
+ var DEFAULT_ASSET_URL3, DEFAULT_STATE_STYLES, DEFAULT_STYLE, SIZE_CLASSES;
7356
7395
  var init_StateIndicator = __esm({
7357
7396
  "components/game/atoms/StateIndicator.tsx"() {
7358
7397
  init_Box();
7359
7398
  init_Icon();
7360
7399
  init_cn();
7400
+ DEFAULT_ASSET_URL3 = "https://almadar-kflow-assets.web.app/shared/isometric-dungeon/Isometric/chestClosed_E.png";
7361
7401
  DEFAULT_STATE_STYLES = {
7362
7402
  idle: { icon: "\u23F8", bgClass: "bg-muted" },
7363
7403
  active: { icon: "\u25B6", bgClass: "bg-success" },
@@ -7432,6 +7472,7 @@ var init_TimerDisplay = __esm({
7432
7472
  }
7433
7473
  });
7434
7474
  function ResourceCounter({
7475
+ assetUrl = DEFAULT_ASSET_URL4,
7435
7476
  icon,
7436
7477
  label = "Gold",
7437
7478
  value = 250,
@@ -7451,7 +7492,17 @@ function ResourceCounter({
7451
7492
  className
7452
7493
  ),
7453
7494
  children: [
7454
- icon && /* @__PURE__ */ jsxRuntime.jsx("span", { className: cn("flex-shrink-0", sizes.icon), children: typeof icon === "string" ? /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: icon }) : /* @__PURE__ */ jsxRuntime.jsx(Icon, { icon }) }),
7495
+ assetUrl ? /* @__PURE__ */ jsxRuntime.jsx(
7496
+ "img",
7497
+ {
7498
+ src: assetUrl,
7499
+ alt: label,
7500
+ width: sizes.img,
7501
+ height: sizes.img,
7502
+ style: { imageRendering: "pixelated", objectFit: "contain" },
7503
+ className: "flex-shrink-0"
7504
+ }
7505
+ ) : icon ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: cn("flex-shrink-0", sizes.icon), children: typeof icon === "string" ? /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: icon }) : /* @__PURE__ */ jsxRuntime.jsx(Icon, { icon }) }) : null,
7455
7506
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: label }),
7456
7507
  /* @__PURE__ */ jsxRuntime.jsxs("span", { className: cn("font-bold tabular-nums", color && (color in colorTokenClasses2 ? colorTokenClasses2[color] : color)), children: [
7457
7508
  value,
@@ -7464,7 +7515,7 @@ function ResourceCounter({
7464
7515
  }
7465
7516
  );
7466
7517
  }
7467
- var colorTokenClasses2, sizeMap5;
7518
+ var colorTokenClasses2, DEFAULT_ASSET_URL4, sizeMap5;
7468
7519
  var init_ResourceCounter = __esm({
7469
7520
  "components/game/atoms/ResourceCounter.tsx"() {
7470
7521
  init_cn();
@@ -7477,16 +7528,17 @@ var init_ResourceCounter = __esm({
7477
7528
  error: "text-error",
7478
7529
  muted: "text-muted-foreground"
7479
7530
  };
7531
+ DEFAULT_ASSET_URL4 = "https://almadar-kflow-assets.web.app/shared/world-map/gold_mine.png";
7480
7532
  sizeMap5 = {
7481
- sm: { wrapper: "text-xs gap-1 px-1.5 py-0.5", icon: "text-sm" },
7482
- md: { wrapper: "text-sm gap-1.5 px-2 py-1", icon: "text-base" },
7483
- lg: { wrapper: "text-base gap-2 px-3 py-1.5", icon: "text-lg" }
7533
+ sm: { wrapper: "text-xs gap-1 px-1.5 py-0.5", icon: "text-sm", img: 16 },
7534
+ md: { wrapper: "text-sm gap-1.5 px-2 py-1", icon: "text-base", img: 20 },
7535
+ lg: { wrapper: "text-base gap-2 px-3 py-1.5", icon: "text-lg", img: 28 }
7484
7536
  };
7485
7537
  ResourceCounter.displayName = "ResourceCounter";
7486
7538
  }
7487
7539
  });
7488
7540
  function ItemSlot({
7489
- assetUrl = DEFAULT_ASSET_URL,
7541
+ assetUrl = DEFAULT_ASSET_URL5,
7490
7542
  icon = "sword",
7491
7543
  label = "Iron Sword",
7492
7544
  quantity,
@@ -7545,7 +7597,7 @@ function ItemSlot({
7545
7597
  }
7546
7598
  );
7547
7599
  }
7548
- var sizeMap6, rarityBorderMap, rarityGlowMap, DEFAULT_ASSET_URL, assetSizeMap;
7600
+ var sizeMap6, rarityBorderMap, rarityGlowMap, DEFAULT_ASSET_URL5, assetSizeMap;
7549
7601
  var init_ItemSlot = __esm({
7550
7602
  "components/game/atoms/ItemSlot.tsx"() {
7551
7603
  "use client";
@@ -7570,7 +7622,7 @@ var init_ItemSlot = __esm({
7570
7622
  epic: "shadow-lg",
7571
7623
  legendary: "shadow-lg"
7572
7624
  };
7573
- DEFAULT_ASSET_URL = "https://almadar-kflow-assets.web.app/shared/isometric-dungeon/Isometric/chestClosed_E.png";
7625
+ DEFAULT_ASSET_URL5 = "https://almadar-kflow-assets.web.app/shared/isometric-dungeon/Isometric/chestClosed_E.png";
7574
7626
  assetSizeMap = {
7575
7627
  sm: 28,
7576
7628
  md: 40,
@@ -7580,6 +7632,7 @@ var init_ItemSlot = __esm({
7580
7632
  }
7581
7633
  });
7582
7634
  function TurnIndicator({
7635
+ assetUrl = DEFAULT_ASSET_URL6,
7583
7636
  currentTurn = 1,
7584
7637
  maxTurns,
7585
7638
  activeTeam,
@@ -7612,14 +7665,24 @@ function TurnIndicator({
7612
7665
  ] }),
7613
7666
  activeTeam && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
7614
7667
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: "|" }),
7615
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: cn("rounded-full bg-success/20", sizes.dot) }),
7668
+ assetUrl ? /* @__PURE__ */ jsxRuntime.jsx(
7669
+ "img",
7670
+ {
7671
+ src: assetUrl,
7672
+ alt: "",
7673
+ width: 12,
7674
+ height: 12,
7675
+ style: { imageRendering: "pixelated", objectFit: "contain" },
7676
+ className: "flex-shrink-0"
7677
+ }
7678
+ ) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: cn("rounded-full bg-success/20", sizes.dot) }),
7616
7679
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-success", children: activeTeam })
7617
7680
  ] })
7618
7681
  ]
7619
7682
  }
7620
7683
  );
7621
7684
  }
7622
- var sizeMap7;
7685
+ var sizeMap7, DEFAULT_ASSET_URL6;
7623
7686
  var init_TurnIndicator = __esm({
7624
7687
  "components/game/atoms/TurnIndicator.tsx"() {
7625
7688
  init_cn();
@@ -7628,6 +7691,7 @@ var init_TurnIndicator = __esm({
7628
7691
  md: { wrapper: "text-sm gap-2 px-3 py-1", dot: "w-2 h-2" },
7629
7692
  lg: { wrapper: "text-base gap-2.5 px-4 py-1.5", dot: "w-2.5 h-2.5" }
7630
7693
  };
7694
+ DEFAULT_ASSET_URL6 = "https://almadar-kflow-assets.web.app/shared/effects/particles/symbol_01.png";
7631
7695
  TurnIndicator.displayName = "TurnIndicator";
7632
7696
  }
7633
7697
  });
@@ -7643,6 +7707,7 @@ function getComboScale(combo) {
7643
7707
  return "";
7644
7708
  }
7645
7709
  function ComboCounter({
7710
+ assetUrl = DEFAULT_ASSET_URL7,
7646
7711
  combo = 5,
7647
7712
  multiplier,
7648
7713
  streak,
@@ -7662,6 +7727,17 @@ function ComboCounter({
7662
7727
  className
7663
7728
  ),
7664
7729
  children: [
7730
+ assetUrl && /* @__PURE__ */ jsxRuntime.jsx(
7731
+ "img",
7732
+ {
7733
+ src: assetUrl,
7734
+ alt: "combo",
7735
+ width: 24,
7736
+ height: 24,
7737
+ style: { imageRendering: "pixelated", objectFit: "contain" },
7738
+ className: "flex-shrink-0 mb-0.5"
7739
+ }
7740
+ ),
7665
7741
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: cn("font-black tabular-nums leading-none", sizes.combo, getComboIntensity(combo)), children: combo }),
7666
7742
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: cn("font-bold uppercase tracking-wider text-muted-foreground", sizes.label), children: "combo" }),
7667
7743
  multiplier != null && multiplier > 1 && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: cn("font-bold text-warning tabular-nums", sizes.multiplier), children: [
@@ -7676,10 +7752,11 @@ function ComboCounter({
7676
7752
  }
7677
7753
  );
7678
7754
  }
7679
- var sizeMap8;
7755
+ var DEFAULT_ASSET_URL7, sizeMap8;
7680
7756
  var init_ComboCounter = __esm({
7681
7757
  "components/game/atoms/ComboCounter.tsx"() {
7682
7758
  init_cn();
7759
+ DEFAULT_ASSET_URL7 = "https://almadar-kflow-assets.web.app/shared/effects/flash/flash00.png";
7683
7760
  sizeMap8 = {
7684
7761
  sm: { combo: "text-lg", label: "text-xs", multiplier: "text-xs" },
7685
7762
  md: { combo: "text-2xl", label: "text-xs", multiplier: "text-sm" },
@@ -7773,6 +7850,7 @@ var init_XPBar = __esm({
7773
7850
  }
7774
7851
  });
7775
7852
  function WaypointMarker({
7853
+ assetUrl = DEFAULT_ASSET_URL8,
7776
7854
  label,
7777
7855
  icon,
7778
7856
  active = true,
@@ -7811,7 +7889,17 @@ function WaypointMarker({
7811
7889
  active && !completed && "bg-info text-foreground",
7812
7890
  !active && !completed && "bg-muted"
7813
7891
  ),
7814
- children: completed ? checkIcon : icon && (typeof icon === "string" ? /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: icon }) : /* @__PURE__ */ jsxRuntime.jsx(Icon, { icon }))
7892
+ children: completed ? checkIcon : assetUrl ? /* @__PURE__ */ jsxRuntime.jsx(
7893
+ "img",
7894
+ {
7895
+ src: assetUrl,
7896
+ alt: label,
7897
+ width: sizes.img,
7898
+ height: sizes.img,
7899
+ style: { imageRendering: "pixelated", objectFit: "contain" },
7900
+ className: "flex-shrink-0"
7901
+ }
7902
+ ) : icon ? typeof icon === "string" ? /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: icon }) : /* @__PURE__ */ jsxRuntime.jsx(Icon, { icon }) : null
7815
7903
  }
7816
7904
  )
7817
7905
  ] }),
@@ -7828,15 +7916,16 @@ function WaypointMarker({
7828
7916
  )
7829
7917
  ] });
7830
7918
  }
7831
- var sizeMap10, checkIcon;
7919
+ var DEFAULT_ASSET_URL8, sizeMap10, checkIcon;
7832
7920
  var init_WaypointMarker = __esm({
7833
7921
  "components/game/atoms/WaypointMarker.tsx"() {
7834
7922
  init_cn();
7835
7923
  init_Icon();
7924
+ DEFAULT_ASSET_URL8 = "https://almadar-kflow-assets.web.app/shared/world-map/battle_marker.png";
7836
7925
  sizeMap10 = {
7837
- sm: { dot: "w-4 h-4", ring: "w-6 h-6", label: "text-xs mt-1" },
7838
- md: { dot: "w-6 h-6", ring: "w-8 h-8", label: "text-sm mt-1.5" },
7839
- lg: { dot: "w-8 h-8", ring: "w-10 h-10", label: "text-base mt-2" }
7926
+ sm: { dot: "w-4 h-4", ring: "w-6 h-6", label: "text-xs mt-1", img: 12 },
7927
+ md: { dot: "w-6 h-6", ring: "w-8 h-8", label: "text-sm mt-1.5", img: 18 },
7928
+ lg: { dot: "w-8 h-8", ring: "w-10 h-10", label: "text-base mt-2", img: 24 }
7840
7929
  };
7841
7930
  checkIcon = /* @__PURE__ */ jsxRuntime.jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 3, className: "w-3/5 h-3/5", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M5 13l4 4L19 7", strokeLinecap: "round", strokeLinejoin: "round" }) });
7842
7931
  WaypointMarker.displayName = "WaypointMarker";
@@ -7849,6 +7938,7 @@ function formatDuration(seconds) {
7849
7938
  return `${Math.round(seconds)}s`;
7850
7939
  }
7851
7940
  function StatusEffect({
7941
+ assetUrl = DEFAULT_ASSET_URL9,
7852
7942
  icon = "shield",
7853
7943
  label = "Shield",
7854
7944
  duration = 30,
@@ -7869,7 +7959,17 @@ function StatusEffect({
7869
7959
  ),
7870
7960
  title: label,
7871
7961
  children: [
7872
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: cn("flex items-center justify-center", sizes.icon), children: typeof icon === "string" ? /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: icon, size: "sm" }) : /* @__PURE__ */ jsxRuntime.jsx(Icon, { icon, size: "sm" }) }),
7962
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: cn("flex items-center justify-center", sizes.icon), children: assetUrl ? /* @__PURE__ */ jsxRuntime.jsx(
7963
+ "img",
7964
+ {
7965
+ src: assetUrl,
7966
+ alt: label,
7967
+ width: sizes.img,
7968
+ height: sizes.img,
7969
+ style: { imageRendering: "pixelated", objectFit: "contain" },
7970
+ className: "flex-shrink-0"
7971
+ }
7972
+ ) : typeof icon === "string" ? /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: icon, size: "sm" }) : /* @__PURE__ */ jsxRuntime.jsx(Icon, { icon, size: "sm" }) }),
7873
7973
  duration !== void 0 && /* @__PURE__ */ jsxRuntime.jsx(
7874
7974
  "span",
7875
7975
  {
@@ -7896,15 +7996,16 @@ function StatusEffect({
7896
7996
  label && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-muted-foreground mt-0.5 text-center whitespace-nowrap", children: label })
7897
7997
  ] });
7898
7998
  }
7899
- var sizeMap11, variantStyles7;
7999
+ var DEFAULT_ASSET_URL9, sizeMap11, variantStyles7;
7900
8000
  var init_StatusEffect = __esm({
7901
8001
  "components/game/atoms/StatusEffect.tsx"() {
7902
8002
  init_cn();
7903
8003
  init_Icon();
8004
+ DEFAULT_ASSET_URL9 = "https://almadar-kflow-assets.web.app/shared/effects/particles/flame_01.png";
7904
8005
  sizeMap11 = {
7905
- sm: { container: "w-8 h-8", icon: "text-sm", badge: "text-xs -top-1 -right-1 w-4 h-4", timer: "text-[9px]" },
7906
- md: { container: "w-10 h-10", icon: "text-base", badge: "text-xs -top-1 -right-1 w-5 h-5", timer: "text-xs" },
7907
- lg: { container: "w-12 h-12", icon: "text-lg", badge: "text-sm -top-1.5 -right-1.5 w-6 h-6", timer: "text-xs" }
8006
+ sm: { container: "w-8 h-8", icon: "text-sm", badge: "text-xs -top-1 -right-1 w-4 h-4", timer: "text-[9px]", img: 20 },
8007
+ md: { container: "w-10 h-10", icon: "text-base", badge: "text-xs -top-1 -right-1 w-5 h-5", timer: "text-xs", img: 28 },
8008
+ lg: { container: "w-12 h-12", icon: "text-lg", badge: "text-sm -top-1.5 -right-1.5 w-6 h-6", timer: "text-xs", img: 36 }
7908
8009
  };
7909
8010
  variantStyles7 = {
7910
8011
  buff: "border-success bg-success/20",
@@ -7915,6 +8016,7 @@ var init_StatusEffect = __esm({
7915
8016
  }
7916
8017
  });
7917
8018
  function DamageNumber({
8019
+ assetUrl = DEFAULT_ASSET_URL10,
7918
8020
  value = 42,
7919
8021
  type = "damage",
7920
8022
  size = "md",
@@ -7923,22 +8025,35 @@ function DamageNumber({
7923
8025
  const displayText = type === "miss" ? "MISS" : type === "heal" ? `+${value}` : `${value}`;
7924
8026
  return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
7925
8027
  /* @__PURE__ */ jsxRuntime.jsx("style", { children: floatKeyframes }),
7926
- /* @__PURE__ */ jsxRuntime.jsx(
8028
+ /* @__PURE__ */ jsxRuntime.jsxs(
7927
8029
  "span",
7928
8030
  {
7929
8031
  className: cn(
7930
- "inline-block font-mono select-none pointer-events-none",
8032
+ "inline-flex items-center gap-0.5 font-mono select-none pointer-events-none",
7931
8033
  sizeMap12[size],
7932
8034
  typeStyles[type],
7933
8035
  className
7934
8036
  ),
7935
8037
  style: { animation: "damageFloat 1s ease-out forwards" },
7936
- children: displayText
8038
+ children: [
8039
+ assetUrl && /* @__PURE__ */ jsxRuntime.jsx(
8040
+ "img",
8041
+ {
8042
+ src: assetUrl,
8043
+ alt: "",
8044
+ width: 14,
8045
+ height: 14,
8046
+ style: { imageRendering: "pixelated", objectFit: "contain" },
8047
+ className: "flex-shrink-0"
8048
+ }
8049
+ ),
8050
+ displayText
8051
+ ]
7937
8052
  }
7938
8053
  )
7939
8054
  ] });
7940
8055
  }
7941
- var sizeMap12, typeStyles, floatKeyframes;
8056
+ var sizeMap12, typeStyles, floatKeyframes, DEFAULT_ASSET_URL10;
7942
8057
  var init_DamageNumber = __esm({
7943
8058
  "components/game/atoms/DamageNumber.tsx"() {
7944
8059
  init_cn();
@@ -7960,6 +8075,7 @@ var init_DamageNumber = __esm({
7960
8075
  100% { opacity: 0; transform: translateY(-32px) scale(0.8); }
7961
8076
  }
7962
8077
  `;
8078
+ DEFAULT_ASSET_URL10 = "https://almadar-kflow-assets.web.app/shared/effects/particles/spark_01.png";
7963
8079
  DamageNumber.displayName = "DamageNumber";
7964
8080
  }
7965
8081
  });
@@ -8048,6 +8164,7 @@ var init_ChoiceButton = __esm({
8048
8164
  }
8049
8165
  });
8050
8166
  function ActionButton({
8167
+ assetUrl = DEFAULT_ASSET_URL11,
8051
8168
  label = "Attack",
8052
8169
  icon,
8053
8170
  cooldown = 0,
@@ -8087,13 +8204,23 @@ function ActionButton({
8087
8204
  }
8088
8205
  }
8089
8206
  ),
8090
- icon && /* @__PURE__ */ jsxRuntime.jsx("span", { className: cn("flex-shrink-0", sizes.icon), children: typeof icon === "string" ? (() => {
8207
+ assetUrl ? /* @__PURE__ */ jsxRuntime.jsx(
8208
+ "img",
8209
+ {
8210
+ src: assetUrl,
8211
+ alt: "",
8212
+ width: 16,
8213
+ height: 16,
8214
+ style: { imageRendering: "pixelated", objectFit: "contain" },
8215
+ className: cn("flex-shrink-0", sizes.icon)
8216
+ }
8217
+ ) : icon ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: cn("flex-shrink-0", sizes.icon), children: typeof icon === "string" ? (() => {
8091
8218
  const I = resolveIcon(icon);
8092
8219
  return I ? /* @__PURE__ */ jsxRuntime.jsx(I, { className: "w-4 h-4" }) : null;
8093
8220
  })() : /* @__PURE__ */ (() => {
8094
8221
  const I = icon;
8095
8222
  return /* @__PURE__ */ jsxRuntime.jsx(I, { className: "w-4 h-4" });
8096
- })() }),
8223
+ })() }) : null,
8097
8224
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "relative z-10", children: label }),
8098
8225
  hotkey && /* @__PURE__ */ jsxRuntime.jsx(
8099
8226
  "span",
@@ -8109,7 +8236,7 @@ function ActionButton({
8109
8236
  }
8110
8237
  );
8111
8238
  }
8112
- var sizeMap13, variantStyles8;
8239
+ var sizeMap13, variantStyles8, DEFAULT_ASSET_URL11;
8113
8240
  var init_ActionButton = __esm({
8114
8241
  "components/game/atoms/ActionButton.tsx"() {
8115
8242
  init_cn();
@@ -8124,6 +8251,7 @@ var init_ActionButton = __esm({
8124
8251
  secondary: "bg-secondary text-secondary-foreground hover:bg-secondary-hover border-border",
8125
8252
  danger: "bg-error text-error-foreground hover:bg-error/90 border-error"
8126
8253
  };
8254
+ DEFAULT_ASSET_URL11 = "https://almadar-kflow-assets.web.app/shared/effects/particles/slash_01.png";
8127
8255
  ActionButton.displayName = "ActionButton";
8128
8256
  }
8129
8257
  });
@@ -8636,10 +8764,10 @@ var init_Container = __esm({
8636
8764
  center = true,
8637
8765
  className,
8638
8766
  children,
8639
- as: Component = "div"
8767
+ as: Component2 = "div"
8640
8768
  }) => {
8641
8769
  const resolvedSize = maxWidth ?? size ?? "lg";
8642
- const Comp = Component;
8770
+ const Comp = Component2;
8643
8771
  return /* @__PURE__ */ jsxRuntime.jsx(
8644
8772
  Comp,
8645
8773
  {
@@ -11473,15 +11601,15 @@ function BattleBoard({
11473
11601
  const board = boardEntity(entity) ?? {};
11474
11602
  const tiles = propTiles ?? (Array.isArray(board.tiles) ? board.tiles : []);
11475
11603
  const features = propFeatures ?? (Array.isArray(board.features) ? board.features : []);
11476
- const boardWidth = num(board.boardWidth, 8);
11477
- const boardHeight = num(board.boardHeight, 6);
11604
+ const boardWidth = num(board.gridWidth ?? board.boardWidth, 8);
11605
+ const boardHeight = num(board.gridHeight ?? board.boardHeight, 6);
11478
11606
  const assetManifest = propAssetManifest ?? board.assetManifest;
11479
11607
  const backgroundImage = board.backgroundImage;
11480
11608
  const units = rows(board.units);
11481
11609
  const selectedUnitId = board.selectedUnitId ?? null;
11482
11610
  const currentPhase = str(board.phase) || "observation";
11483
11611
  const currentTurn = num(board.turn, 1);
11484
- const gameResult = board.gameResult ?? null;
11612
+ const gameResult = board.result ?? null;
11485
11613
  const eventBus = useEventBus();
11486
11614
  const { t } = hooks.useTranslate();
11487
11615
  const [hoveredTile, setHoveredTile] = React82.useState(null);
@@ -11502,7 +11630,7 @@ function BattleBoard({
11502
11630
  const validMoves = React82.useMemo(() => {
11503
11631
  if (!selectedUnit || currentPhase !== "movement") return [];
11504
11632
  const moves = [];
11505
- const range = num(selectedUnit.movement);
11633
+ const range = num(board.movementRange, 2);
11506
11634
  const origin = unitPosition(selectedUnit);
11507
11635
  for (let dy = -range; dy <= range; dy++) {
11508
11636
  for (let dx = -range; dx <= range; dx++) {
@@ -19787,15 +19915,24 @@ function CastleBoard({
19787
19915
  featureClickEvent,
19788
19916
  unitClickEvent,
19789
19917
  tileClickEvent,
19918
+ playAgainEvent,
19790
19919
  className
19791
19920
  }) {
19792
19921
  const eventBus = useEventBus();
19922
+ const { t } = hooks.useTranslate();
19793
19923
  const resolved = boardEntity(entity);
19794
19924
  const tiles = propTiles ?? (Array.isArray(resolved?.tiles) ? resolved.tiles : []);
19795
19925
  const features = propFeatures ?? (Array.isArray(resolved?.features) ? resolved.features : []);
19796
19926
  const units = propUnits ?? (Array.isArray(resolved?.units) ? resolved.units : []);
19797
19927
  const assetManifest = propAssetManifest ?? resolved?.assetManifest;
19798
19928
  const backgroundImage = resolved?.backgroundImage;
19929
+ const gold = num(resolved?.gold);
19930
+ const health = num(resolved?.health);
19931
+ const maxHealth = num(resolved?.maxHealth);
19932
+ const wave = num(resolved?.wave);
19933
+ const tickCount = num(resolved?.tickCount);
19934
+ const buildings = rows(resolved?.buildings);
19935
+ const result = str(resolved?.result) || "none";
19799
19936
  const [hoveredTile, setHoveredTile] = React82.useState(null);
19800
19937
  const [selectedFeature, setSelectedFeature] = React82.useState(null);
19801
19938
  const hoveredFeature = React82.useMemo(() => {
@@ -19808,7 +19945,7 @@ function CastleBoard({
19808
19945
  (u) => u.position?.x === hoveredTile.x && u.position?.y === hoveredTile.y
19809
19946
  ) ?? null;
19810
19947
  }, [hoveredTile, units]);
19811
- const maxY = Math.max(...tiles.map((t) => t.y), 0);
19948
+ const maxY = Math.max(...tiles.map((t2) => t2.y), 0);
19812
19949
  const baseOffsetX = (maxY + 1) * (TILE_WIDTH * scale / 2);
19813
19950
  const tileToScreen = React82.useCallback(
19814
19951
  (tx, ty) => isoToScreen(tx, ty, scale, baseOffsetX),
@@ -19843,6 +19980,11 @@ function CastleBoard({
19843
19980
  }
19844
19981
  }, [units, onUnitClick, unitClickEvent, eventBus]);
19845
19982
  const clearSelection = React82.useCallback(() => setSelectedFeature(null), []);
19983
+ const handlePlayAgain = React82.useCallback(() => {
19984
+ if (playAgainEvent) {
19985
+ eventBus.emit(`UI:${playAgainEvent}`, {});
19986
+ }
19987
+ }, [playAgainEvent, eventBus]);
19846
19988
  const ctx = React82.useMemo(
19847
19989
  () => ({
19848
19990
  hoveredTile,
@@ -19851,11 +19993,18 @@ function CastleBoard({
19851
19993
  selectedFeature,
19852
19994
  clearSelection,
19853
19995
  tileToScreen,
19854
- scale
19996
+ scale,
19997
+ gold,
19998
+ health,
19999
+ maxHealth,
20000
+ wave,
20001
+ tickCount,
20002
+ buildings,
20003
+ result
19855
20004
  }),
19856
- [hoveredTile, hoveredFeature, hoveredUnit, selectedFeature, clearSelection, tileToScreen, scale]
20005
+ [hoveredTile, hoveredFeature, hoveredUnit, selectedFeature, clearSelection, tileToScreen, scale, gold, health, maxHealth, wave, tickCount, buildings, result]
19857
20006
  );
19858
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("castle-board min-h-screen flex flex-col bg-background", className), children: [
20007
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("castle-board relative min-h-screen flex flex-col bg-background", className), children: [
19859
20008
  header && header(ctx),
19860
20009
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-1 overflow-hidden", children: [
19861
20010
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 overflow-auto p-4 relative", children: [
@@ -19880,7 +20029,29 @@ function CastleBoard({
19880
20029
  ] }),
19881
20030
  sidePanel && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-96 shrink-0 border-l border-border bg-surface overflow-y-auto", children: sidePanel(ctx) })
19882
20031
  ] }),
19883
- footer && footer(ctx)
20032
+ footer && footer(ctx),
20033
+ result !== "none" && /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "absolute inset-0 z-50 flex items-center justify-center bg-background/70 backdrop-blur-sm rounded-container", children: /* @__PURE__ */ jsxRuntime.jsxs(VStack, { className: "text-center p-8", gap: "lg", children: [
20034
+ /* @__PURE__ */ jsxRuntime.jsx(
20035
+ Typography,
20036
+ {
20037
+ variant: "h2",
20038
+ className: cn(
20039
+ "text-4xl font-black tracking-widest uppercase",
20040
+ result === "victory" ? "text-warning" : "text-error"
20041
+ ),
20042
+ children: result === "victory" ? t("battle.victory") : t("battle.defeat")
20043
+ }
20044
+ ),
20045
+ /* @__PURE__ */ jsxRuntime.jsx(
20046
+ Button,
20047
+ {
20048
+ variant: "primary",
20049
+ className: "px-8 py-3 font-semibold",
20050
+ onClick: handlePlayAgain,
20051
+ children: t("battle.playAgain")
20052
+ }
20053
+ )
20054
+ ] }) })
19884
20055
  ] });
19885
20056
  }
19886
20057
  var init_CastleBoard = __esm({
@@ -19888,6 +20059,10 @@ var init_CastleBoard = __esm({
19888
20059
  "use client";
19889
20060
  init_cn();
19890
20061
  init_useEventBus();
20062
+ init_Box();
20063
+ init_Button();
20064
+ init_Typography();
20065
+ init_Stack();
19891
20066
  init_IsometricCanvas();
19892
20067
  init_boardEntity();
19893
20068
  init_isometric();
@@ -20764,22 +20939,16 @@ function ClassifierBoard({
20764
20939
  const { emit } = useEventBus();
20765
20940
  const { t } = hooks.useTranslate();
20766
20941
  const resolved = boardEntity(entity);
20767
- const [localAssignments, setLocalAssignments] = React82.useState({});
20768
20942
  const [headerError, setHeaderError] = React82.useState(false);
20769
- const [localSubmitted, setLocalSubmitted] = React82.useState(false);
20770
- const [localAttempts, setLocalAttempts] = React82.useState(0);
20771
- const [showHint, setShowHint] = React82.useState(false);
20772
20943
  const items = Array.isArray(resolved?.items) ? resolved.items : [];
20773
20944
  const categories = Array.isArray(resolved?.categories) ? resolved.categories : [];
20774
- const entityResult = str(resolved?.result);
20775
- const entityDrivesResult = entityResult.length > 0;
20776
- const entityHasAssignments = items.some((item) => item.assignedCategory != null);
20777
- const assignments = entityHasAssignments ? items.reduce((acc, item) => {
20778
- if (item.assignedCategory != null) acc[item.id] = item.assignedCategory;
20945
+ const result = str(resolved?.result);
20946
+ const submitted = result === "win";
20947
+ const attempts = num(resolved?.attempts);
20948
+ const assignments = items.reduce((acc, item) => {
20949
+ if (item.assignedCategory != null && item.assignedCategory !== "") acc[item.id] = item.assignedCategory;
20779
20950
  return acc;
20780
- }, {}) : localAssignments;
20781
- const attempts = entityDrivesResult ? num(resolved?.attempts) : localAttempts;
20782
- const submitted = entityDrivesResult || localSubmitted;
20951
+ }, {});
20783
20952
  const unassignedItems = items.filter((item) => !assignments[item.id]);
20784
20953
  const allAssigned = items.length > 0 && Object.keys(assignments).length === items.length;
20785
20954
  const results = submitted ? items.map((item) => ({
@@ -20787,54 +20956,25 @@ function ClassifierBoard({
20787
20956
  assigned: assignments[item.id],
20788
20957
  correct: assignments[item.id] === item.correctCategory
20789
20958
  })) : [];
20790
- const allCorrect = entityDrivesResult ? entityResult === "success" : results.length > 0 && results.every((r) => r.correct);
20959
+ const allCorrect = result === "win";
20791
20960
  const correctCount = results.filter((r) => r.correct).length;
20961
+ const showHint = attempts >= 2 && Boolean(str(resolved?.hint));
20792
20962
  const handleAssign = (itemId, categoryId) => {
20793
20963
  if (submitted) return;
20794
- if (assignEvent) {
20795
- emit(`UI:${assignEvent}`, { itemId, categoryId });
20796
- }
20797
- if (!entityHasAssignments) {
20798
- setLocalAssignments((prev) => ({ ...prev, [itemId]: categoryId }));
20799
- }
20964
+ if (assignEvent) emit(`UI:${assignEvent}`, { itemId, categoryId });
20800
20965
  };
20801
20966
  const handleUnassign = (itemId) => {
20802
20967
  if (submitted) return;
20803
- if (!entityHasAssignments) {
20804
- setLocalAssignments((prev) => {
20805
- const next = { ...prev };
20806
- delete next[itemId];
20807
- return next;
20808
- });
20809
- }
20968
+ if (assignEvent) emit(`UI:${assignEvent}`, { itemId, categoryId: "" });
20810
20969
  };
20811
- const handleSubmit = React82.useCallback(() => {
20812
- if (checkEvent) {
20813
- emit(`UI:${checkEvent}`, {});
20814
- }
20815
- if (!entityDrivesResult) {
20816
- setLocalSubmitted(true);
20817
- setLocalAttempts((a) => a + 1);
20818
- const correct = items.every((item) => assignments[item.id] === item.correctCategory);
20819
- if (correct) {
20820
- emit(`UI:${completeEvent}`, { success: true, attempts: attempts + 1 });
20821
- }
20822
- }
20823
- }, [checkEvent, entityDrivesResult, items, assignments, attempts, completeEvent, emit]);
20824
- const handleReset = () => {
20825
- if (!entityDrivesResult) setLocalSubmitted(false);
20826
- if (attempts >= 2 && str(resolved?.hint)) {
20827
- setShowHint(true);
20970
+ const handleSubmit = () => {
20971
+ if (checkEvent) emit(`UI:${checkEvent}`, {});
20972
+ if (allAssigned && items.every((item) => assignments[item.id] === item.correctCategory)) {
20973
+ emit(`UI:${completeEvent}`, { success: true, attempts: attempts + 1 });
20828
20974
  }
20829
20975
  };
20830
20976
  const handleFullReset = () => {
20831
- if (playAgainEvent) {
20832
- emit(`UI:${playAgainEvent}`, {});
20833
- }
20834
- setLocalAssignments({});
20835
- setLocalSubmitted(false);
20836
- setLocalAttempts(0);
20837
- setShowHint(false);
20977
+ if (playAgainEvent) emit(`UI:${playAgainEvent}`, {});
20838
20978
  };
20839
20979
  if (!resolved) return null;
20840
20980
  const theme = resolved.theme ?? void 0;
@@ -20873,17 +21013,17 @@ function ClassifierBoard({
20873
21013
  /* @__PURE__ */ jsxRuntime.jsx(Badge, { size: "sm", children: catItems.length })
20874
21014
  ] }),
20875
21015
  /* @__PURE__ */ jsxRuntime.jsx(HStack, { gap: "xs", className: "flex-wrap min-h-[2rem]", children: catItems.map((item) => {
20876
- const result = results.find((r) => r.item.id === item.id);
21016
+ const result2 = results.find((r) => r.item.id === item.id);
20877
21017
  return /* @__PURE__ */ jsxRuntime.jsxs(
20878
21018
  Badge,
20879
21019
  {
20880
21020
  size: "sm",
20881
- className: `cursor-pointer ${result ? result.correct ? "border-success bg-success/10" : "border-error bg-error/10" : ""}`,
21021
+ className: `cursor-pointer ${result2 ? result2.correct ? "border-success bg-success/10" : "border-error bg-error/10" : ""}`,
20882
21022
  onClick: () => handleUnassign(item.id),
20883
21023
  children: [
20884
21024
  item.iconUrl && /* @__PURE__ */ jsxRuntime.jsx("img", { src: item.iconUrl, alt: "", className: "w-3 h-3 object-contain inline-block" }),
20885
21025
  item.label,
20886
- result && /* @__PURE__ */ jsxRuntime.jsx(Icon, { icon: result.correct ? LucideIcons2.CheckCircle : LucideIcons2.XCircle, size: "xs", className: result.correct ? "text-success" : "text-error" })
21026
+ result2 && /* @__PURE__ */ jsxRuntime.jsx(Icon, { icon: result2.correct ? LucideIcons2.CheckCircle : LucideIcons2.XCircle, size: "xs", className: result2.correct ? "text-success" : "text-error" })
20887
21027
  ]
20888
21028
  },
20889
21029
  item.id
@@ -20912,10 +21052,10 @@ function ClassifierBoard({
20912
21052
  ] }) }),
20913
21053
  showHint && hint && /* @__PURE__ */ jsxRuntime.jsx(Card, { className: "p-4 border-l-4 border-l-warning", children: /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "body", children: hint }) }),
20914
21054
  /* @__PURE__ */ jsxRuntime.jsxs(HStack, { gap: "sm", justify: "center", children: [
20915
- !submitted ? /* @__PURE__ */ jsxRuntime.jsxs(Button, { variant: "primary", onClick: handleSubmit, disabled: !allAssigned, children: [
21055
+ !submitted && /* @__PURE__ */ jsxRuntime.jsxs(Button, { variant: "primary", onClick: handleSubmit, disabled: !allAssigned, children: [
20916
21056
  /* @__PURE__ */ jsxRuntime.jsx(Icon, { icon: LucideIcons2.Send, size: "sm" }),
20917
21057
  t("classifier.check")
20918
- ] }) : !allCorrect ? /* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "primary", onClick: handleReset, children: t("classifier.tryAgain") }) : null,
21058
+ ] }),
20919
21059
  /* @__PURE__ */ jsxRuntime.jsxs(Button, { variant: "secondary", onClick: handleFullReset, children: [
20920
21060
  /* @__PURE__ */ jsxRuntime.jsx(Icon, { icon: LucideIcons2.RotateCcw, size: "sm" }),
20921
21061
  t("classifier.reset")
@@ -24633,7 +24773,7 @@ var init_Flex = __esm({
24633
24773
  basis,
24634
24774
  className,
24635
24775
  children,
24636
- as: Component = "div"
24776
+ as: Component2 = "div"
24637
24777
  }) => {
24638
24778
  const flexStyle = {};
24639
24779
  if (grow !== void 0 || shrink !== void 0 || basis !== void 0) {
@@ -24645,7 +24785,7 @@ var init_Flex = __esm({
24645
24785
  flexStyle.flexBasis = typeof basis === "number" ? `${basis}px` : basis;
24646
24786
  }
24647
24787
  }
24648
- const Comp = Component;
24788
+ const Comp = Component2;
24649
24789
  return /* @__PURE__ */ jsxRuntime.jsx(
24650
24790
  Comp,
24651
24791
  {
@@ -24765,11 +24905,11 @@ var init_Grid = __esm({
24765
24905
  className,
24766
24906
  style,
24767
24907
  children,
24768
- as: Component = "div"
24908
+ as: Component2 = "div"
24769
24909
  }) => {
24770
24910
  const mergedStyle = rows2 ? { gridTemplateRows: `repeat(${rows2}, minmax(0, 1fr))`, ...style } : style;
24771
24911
  return React82__namespace.default.createElement(
24772
- Component,
24912
+ Component2,
24773
24913
  {
24774
24914
  className: cn(
24775
24915
  "grid",
@@ -26595,6 +26735,7 @@ var init_ProgressDots = __esm({
26595
26735
  }
26596
26736
  });
26597
26737
  function StatBadge({
26738
+ assetUrl = DEFAULT_ASSET_URL12,
26598
26739
  label,
26599
26740
  value = 0,
26600
26741
  max,
@@ -26618,7 +26759,17 @@ function StatBadge({
26618
26759
  className
26619
26760
  ),
26620
26761
  children: [
26621
- icon && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-shrink-0 text-lg", children: typeof icon === "string" ? /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: icon, className: "w-4 h-4" }) : /* @__PURE__ */ jsxRuntime.jsx(Icon, { icon, className: "w-4 h-4" }) }),
26762
+ assetUrl ? /* @__PURE__ */ jsxRuntime.jsx(
26763
+ "img",
26764
+ {
26765
+ src: assetUrl,
26766
+ alt: "",
26767
+ width: 16,
26768
+ height: 16,
26769
+ style: { imageRendering: "pixelated", objectFit: "contain" },
26770
+ className: "flex-shrink-0"
26771
+ }
26772
+ ) : icon ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-shrink-0 text-lg", children: typeof icon === "string" ? /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: icon, className: "w-4 h-4" }) : /* @__PURE__ */ jsxRuntime.jsx(Icon, { icon, className: "w-4 h-4" }) }) : null,
26622
26773
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground font-medium", children: label }),
26623
26774
  format === "hearts" && max && /* @__PURE__ */ jsxRuntime.jsx(
26624
26775
  HealthBar,
@@ -26651,7 +26802,7 @@ function StatBadge({
26651
26802
  }
26652
26803
  );
26653
26804
  }
26654
- var sizeMap17, variantMap2;
26805
+ var sizeMap17, variantMap2, DEFAULT_ASSET_URL12;
26655
26806
  var init_StatBadge = __esm({
26656
26807
  "components/game/molecules/StatBadge.tsx"() {
26657
26808
  init_cn();
@@ -26670,6 +26821,7 @@ var init_StatBadge = __esm({
26670
26821
  warning: "bg-warning/15 border-warning/40 text-foreground",
26671
26822
  danger: "bg-error/15 border-error/40 text-foreground"
26672
26823
  };
26824
+ DEFAULT_ASSET_URL12 = "https://almadar-kflow-assets.web.app/shared/effects/particles/light_01.png";
26673
26825
  StatBadge.displayName = "StatBadge";
26674
26826
  }
26675
26827
  });
@@ -27326,6 +27478,7 @@ var init_TurnPanel = __esm({
27326
27478
  }
27327
27479
  });
27328
27480
  function EnemyPlate({
27481
+ assetUrl = DEFAULT_ASSET_URL13,
27329
27482
  name = "Shadow Guard",
27330
27483
  health = 80,
27331
27484
  maxHealth = 100,
@@ -27343,15 +27496,28 @@ function EnemyPlate({
27343
27496
  ),
27344
27497
  children: [
27345
27498
  /* @__PURE__ */ jsxRuntime.jsxs(Box, { className: "flex items-center justify-between gap-2", children: [
27346
- /* @__PURE__ */ jsxRuntime.jsx(
27347
- Typography,
27348
- {
27349
- variant: "caption",
27350
- weight: "bold",
27351
- className: "text-[var(--color-foreground)] truncate",
27352
- children: name
27353
- }
27354
- ),
27499
+ /* @__PURE__ */ jsxRuntime.jsxs(Box, { className: "flex items-center gap-1.5 min-w-0", children: [
27500
+ assetUrl && /* @__PURE__ */ jsxRuntime.jsx(
27501
+ "img",
27502
+ {
27503
+ src: assetUrl,
27504
+ alt: name,
27505
+ width: 24,
27506
+ height: 24,
27507
+ style: { objectFit: "cover", borderRadius: "50%" },
27508
+ className: "flex-shrink-0"
27509
+ }
27510
+ ),
27511
+ /* @__PURE__ */ jsxRuntime.jsx(
27512
+ Typography,
27513
+ {
27514
+ variant: "caption",
27515
+ weight: "bold",
27516
+ className: "text-[var(--color-foreground)] truncate",
27517
+ children: name
27518
+ }
27519
+ )
27520
+ ] }),
27355
27521
  level != null && /* @__PURE__ */ jsxRuntime.jsxs(Badge, { variant: "neutral", size: "sm", children: [
27356
27522
  "Lv.",
27357
27523
  level
@@ -27396,7 +27562,7 @@ function EnemyPlate({
27396
27562
  }
27397
27563
  );
27398
27564
  }
27399
- var effectVariantMap2, DEFAULT_ENEMY_EFFECTS;
27565
+ var effectVariantMap2, DEFAULT_ENEMY_EFFECTS, DEFAULT_ASSET_URL13;
27400
27566
  var init_EnemyPlate = __esm({
27401
27567
  "components/game/molecules/EnemyPlate.tsx"() {
27402
27568
  "use client";
@@ -27413,6 +27579,7 @@ var init_EnemyPlate = __esm({
27413
27579
  DEFAULT_ENEMY_EFFECTS = [
27414
27580
  { icon: "flame", label: "Burn", variant: "debuff" }
27415
27581
  ];
27582
+ DEFAULT_ASSET_URL13 = "https://almadar-kflow-assets.web.app/shared/portraits/shadow-legion.png";
27416
27583
  EnemyPlate.displayName = "EnemyPlate";
27417
27584
  }
27418
27585
  });
@@ -28944,13 +29111,13 @@ var init_MapView = __esm({
28944
29111
  shadowSize: [41, 41]
28945
29112
  });
28946
29113
  L.Marker.prototype.options.icon = defaultIcon;
28947
- const { useEffect: useEffect74, useRef: useRef68, useCallback: useCallback112, useState: useState107 } = React82__namespace.default;
29114
+ const { useEffect: useEffect80, useRef: useRef74, useCallback: useCallback118, useState: useState112 } = React82__namespace.default;
28948
29115
  const { Typography: Typography2 } = await Promise.resolve().then(() => (init_Typography(), Typography_exports));
28949
- const { useEventBus: useEventBus3 } = await Promise.resolve().then(() => (init_useEventBus(), useEventBus_exports));
29116
+ const { useEventBus: useEventBus4 } = await Promise.resolve().then(() => (init_useEventBus(), useEventBus_exports));
28950
29117
  function MapUpdater({ centerLat, centerLng, zoom }) {
28951
29118
  const map = useMap();
28952
- const prevRef = useRef68({ centerLat, centerLng, zoom });
28953
- useEffect74(() => {
29119
+ const prevRef = useRef74({ centerLat, centerLng, zoom });
29120
+ useEffect80(() => {
28954
29121
  const prev = prevRef.current;
28955
29122
  if (prev.centerLat !== centerLat || prev.centerLng !== centerLng || prev.zoom !== zoom) {
28956
29123
  map.setView([centerLat, centerLng], zoom);
@@ -28961,7 +29128,7 @@ var init_MapView = __esm({
28961
29128
  }
28962
29129
  function MapClickHandler({ onMapClick }) {
28963
29130
  const map = useMap();
28964
- useEffect74(() => {
29131
+ useEffect80(() => {
28965
29132
  if (!onMapClick) return;
28966
29133
  const handler = (e) => {
28967
29134
  onMapClick(e.latlng.lat, e.latlng.lng);
@@ -28988,9 +29155,9 @@ var init_MapView = __esm({
28988
29155
  className,
28989
29156
  showAttribution = true
28990
29157
  }) {
28991
- const eventBus = useEventBus3();
28992
- const [clickedPosition, setClickedPosition] = useState107(null);
28993
- const handleMapClick = useCallback112((lat, lng) => {
29158
+ const eventBus = useEventBus4();
29159
+ const [clickedPosition, setClickedPosition] = useState112(null);
29160
+ const handleMapClick = useCallback118((lat, lng) => {
28994
29161
  if (showClickedPin) {
28995
29162
  setClickedPosition({ lat, lng });
28996
29163
  }
@@ -28999,7 +29166,7 @@ var init_MapView = __esm({
28999
29166
  eventBus.emit(`UI:${mapClickEvent}`, { latitude: lat, longitude: lng });
29000
29167
  }
29001
29168
  }, [onMapClick, mapClickEvent, eventBus, showClickedPin]);
29002
- const handleMarkerClick = useCallback112((marker) => {
29169
+ const handleMarkerClick = useCallback118((marker) => {
29003
29170
  onMarkerClick?.(marker);
29004
29171
  if (markerClickEvent) {
29005
29172
  eventBus.emit(`UI:${markerClickEvent}`, { ...marker });
@@ -36126,10 +36293,10 @@ var init_Section = __esm({
36126
36293
  children,
36127
36294
  headerClassName,
36128
36295
  contentClassName,
36129
- as: Component = "section"
36296
+ as: Component2 = "section"
36130
36297
  }) => {
36131
36298
  const hasHeader = title || description || action;
36132
- const Comp = Component;
36299
+ const Comp = Component2;
36133
36300
  return /* @__PURE__ */ jsxRuntime.jsxs(
36134
36301
  Comp,
36135
36302
  {
@@ -38230,44 +38397,27 @@ function DebuggerBoard({
38230
38397
  const { t } = hooks.useTranslate();
38231
38398
  const resolved = boardEntity(entity);
38232
38399
  const [headerError, setHeaderError] = React82.useState(false);
38233
- const [showHint, setShowHint] = React82.useState(false);
38234
38400
  const lines = Array.isArray(resolved?.lines) ? resolved.lines : [];
38235
- const result = resolved?.result ?? null;
38401
+ const result = str(resolved?.result) || "none";
38236
38402
  const attempts = num(resolved?.attempts);
38237
- const submitted = result != null;
38403
+ const submitted = result === "win";
38404
+ const showHint = !submitted && attempts >= 2 && Boolean(str(resolved?.hint));
38238
38405
  const bugLines = lines.filter((l) => l.isBug);
38239
38406
  const flaggedLines = lines.filter((l) => l.isFlagged);
38240
38407
  const correctFlags = lines.filter((l) => l.isBug && l.isFlagged);
38241
38408
  const falseFlags = lines.filter((l) => !l.isBug && l.isFlagged);
38242
- const allCorrect = result === "win";
38409
+ const allCorrect = submitted;
38243
38410
  const toggleLine = (lineId) => {
38244
38411
  if (submitted) return;
38245
- if (toggleFlagEvent) {
38246
- emit(`UI:${toggleFlagEvent}`, { lineId });
38247
- }
38412
+ if (toggleFlagEvent) emit(`UI:${toggleFlagEvent}`, { lineId });
38248
38413
  };
38249
38414
  const handleSubmit = React82.useCallback(() => {
38250
- if (checkEvent) {
38251
- emit(`UI:${checkEvent}`, {});
38252
- }
38415
+ if (checkEvent) emit(`UI:${checkEvent}`, {});
38253
38416
  const correct = correctFlags.length === bugLines.length && falseFlags.length === 0;
38254
- if (correct) {
38255
- emit(`UI:${completeEvent}`, { success: true, attempts: attempts + 1 });
38256
- }
38417
+ if (correct) emit(`UI:${completeEvent}`, { success: true, attempts: attempts + 1 });
38257
38418
  }, [checkEvent, correctFlags.length, bugLines.length, falseFlags.length, attempts, completeEvent, emit]);
38258
38419
  const handleReset = () => {
38259
- if (playAgainEvent) {
38260
- emit(`UI:${playAgainEvent}`, {});
38261
- }
38262
- if (attempts >= 2 && str(resolved?.hint)) {
38263
- setShowHint(true);
38264
- }
38265
- };
38266
- const handleFullReset = () => {
38267
- if (playAgainEvent) {
38268
- emit(`UI:${playAgainEvent}`, {});
38269
- }
38270
- setShowHint(false);
38420
+ if (playAgainEvent) emit(`UI:${playAgainEvent}`, {});
38271
38421
  };
38272
38422
  if (!resolved) return null;
38273
38423
  const theme = resolved.theme ?? void 0;
@@ -38291,7 +38441,7 @@ function DebuggerBoard({
38291
38441
  /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "h4", weight: "bold", children: str(resolved.title) })
38292
38442
  ] }),
38293
38443
  /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "body", children: str(resolved.description) }),
38294
- /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "caption", className: "text-muted-foreground", children: t("debugger.findBugs", { count: String(num(resolved.bugCount)) }) })
38444
+ /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "caption", className: "text-muted-foreground", children: t("debugger.findBugs", { count: String(lines.filter((l) => l.isBug).length) }) })
38295
38445
  ] }) }),
38296
38446
  /* @__PURE__ */ jsxRuntime.jsx(Card, { className: "p-0 overflow-hidden", children: /* @__PURE__ */ jsxRuntime.jsx(VStack, { gap: "none", children: lines.map((line, i) => {
38297
38447
  const isFlagged = !!line.isFlagged;
@@ -38341,11 +38491,11 @@ function DebuggerBoard({
38341
38491
  ] }) }),
38342
38492
  showHint && hint && /* @__PURE__ */ jsxRuntime.jsx(Card, { className: "p-4 border-l-4 border-l-warning", children: /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "body", children: hint }) }),
38343
38493
  /* @__PURE__ */ jsxRuntime.jsxs(HStack, { gap: "sm", justify: "center", children: [
38344
- !submitted ? /* @__PURE__ */ jsxRuntime.jsxs(Button, { variant: "primary", onClick: handleSubmit, disabled: flaggedLines.length === 0, children: [
38494
+ !submitted && /* @__PURE__ */ jsxRuntime.jsxs(Button, { variant: "primary", onClick: handleSubmit, disabled: flaggedLines.length === 0, children: [
38345
38495
  /* @__PURE__ */ jsxRuntime.jsx(Icon, { icon: LucideIcons2.Send, size: "sm" }),
38346
38496
  t("debugger.submit")
38347
- ] }) : !allCorrect ? /* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "primary", onClick: handleReset, children: t("debugger.tryAgain") }) : null,
38348
- /* @__PURE__ */ jsxRuntime.jsxs(Button, { variant: "secondary", onClick: handleFullReset, children: [
38497
+ ] }),
38498
+ /* @__PURE__ */ jsxRuntime.jsxs(Button, { variant: "secondary", onClick: handleReset, children: [
38349
38499
  /* @__PURE__ */ jsxRuntime.jsx(Icon, { icon: LucideIcons2.RotateCcw, size: "sm" }),
38350
38500
  t("debugger.reset")
38351
38501
  ] })
@@ -39346,39 +39496,40 @@ function EventHandlerBoard({
39346
39496
  stepDurationMs = 800,
39347
39497
  playEvent,
39348
39498
  completeEvent,
39499
+ editRuleEvent,
39500
+ playAgainEvent,
39349
39501
  className
39350
39502
  }) {
39351
39503
  const { emit } = useEventBus();
39352
39504
  const { t } = hooks.useTranslate();
39353
39505
  const resolved = boardEntity(entity);
39354
- const entityObjects = rows(resolved?.objects);
39355
- const [objects, setObjects] = React82.useState(() => [...entityObjects]);
39506
+ const objects = rows(resolved?.objects);
39507
+ const entityResult = str(resolved?.result) || "none";
39508
+ const isSuccess = entityResult === "win";
39509
+ const attempts = typeof resolved?.attempts === "number" ? resolved.attempts : 0;
39356
39510
  const [selectedObjectId, setSelectedObjectId] = React82.useState(
39357
- entityObjects[0] ? objId(entityObjects[0]) : null
39511
+ objects[0] ? objId(objects[0]) : null
39358
39512
  );
39359
39513
  const [headerError, setHeaderError] = React82.useState(false);
39360
- const [playState, setPlayState] = React82.useState("editing");
39514
+ const [isPlaying, setIsPlaying] = React82.useState(false);
39361
39515
  const [eventLog, setEventLog] = React82.useState([]);
39362
- const [attempts, setAttempts] = React82.useState(0);
39363
39516
  const timerRef = React82.useRef(null);
39364
39517
  const logIdCounter = React82.useRef(0);
39365
39518
  React82.useEffect(() => () => {
39366
39519
  if (timerRef.current) clearTimeout(timerRef.current);
39367
39520
  }, []);
39368
- const selectedObject = objects.find((o) => objId(o) === selectedObjectId) || null;
39521
+ const selectedObject = objects.find((o) => objId(o) === selectedObjectId) ?? null;
39369
39522
  const handleRulesChange = React82.useCallback((objectId, rules) => {
39370
- setObjects((prev) => prev.map(
39371
- (o) => objId(o) === objectId ? { ...o, rules } : o
39372
- ));
39373
- }, []);
39523
+ if (editRuleEvent) emit(`UI:${editRuleEvent}`, { objectId, rules });
39524
+ }, [editRuleEvent, emit]);
39374
39525
  const addLogEntry = React82.useCallback((icon, message, status = "done") => {
39375
39526
  const id = `log-${logIdCounter.current++}`;
39376
39527
  setEventLog((prev) => [...prev, { id, timestamp: Date.now(), icon, message, status }]);
39377
39528
  }, []);
39378
39529
  const handlePlay = React82.useCallback(() => {
39379
- if (playState !== "editing") return;
39530
+ if (isPlaying || isSuccess) return;
39380
39531
  if (playEvent) emit(`UI:${playEvent}`, {});
39381
- setPlayState("playing");
39532
+ setIsPlaying(true);
39382
39533
  setEventLog([]);
39383
39534
  const allRules = [];
39384
39535
  objects.forEach((obj) => {
@@ -39394,15 +39545,8 @@ function EventHandlerBoard({
39394
39545
  let goalReached = false;
39395
39546
  const processNext = () => {
39396
39547
  if (eventQueue.length === 0 || stepIdx > 20) {
39397
- if (goalReached) {
39398
- setPlayState("success");
39399
- if (completeEvent) {
39400
- emit(`UI:${completeEvent}`, { success: true });
39401
- }
39402
- } else {
39403
- setAttempts((prev) => prev + 1);
39404
- setPlayState("fail");
39405
- }
39548
+ setIsPlaying(false);
39549
+ if (goalReached && completeEvent) emit(`UI:${completeEvent}`, { success: true });
39406
39550
  return;
39407
39551
  }
39408
39552
  const currentEvent = eventQueue.shift();
@@ -39433,21 +39577,19 @@ function EventHandlerBoard({
39433
39577
  addLogEntry("\u{1F3AC}", t("eventHandler.simulationStarted", { events: triggers.join(", ") }), "active");
39434
39578
  }
39435
39579
  timerRef.current = setTimeout(processNext, stepDurationMs);
39436
- }, [playState, objects, resolved, stepDurationMs, playEvent, completeEvent, emit, addLogEntry, t]);
39580
+ }, [isPlaying, isSuccess, objects, resolved, stepDurationMs, playEvent, completeEvent, emit, addLogEntry, t]);
39437
39581
  const handleTryAgain = React82.useCallback(() => {
39438
39582
  if (timerRef.current) clearTimeout(timerRef.current);
39439
- setPlayState("editing");
39583
+ setIsPlaying(false);
39440
39584
  setEventLog([]);
39441
39585
  }, []);
39442
39586
  const handleReset = React82.useCallback(() => {
39443
39587
  if (timerRef.current) clearTimeout(timerRef.current);
39444
- const resetObjects = rows(resolved?.objects);
39445
- setObjects([...resetObjects]);
39446
- setPlayState("editing");
39588
+ setIsPlaying(false);
39447
39589
  setEventLog([]);
39448
- setSelectedObjectId(resetObjects[0] ? objId(resetObjects[0]) : null);
39449
- setAttempts(0);
39450
- }, [resolved?.objects]);
39590
+ setSelectedObjectId(objects[0] ? objId(objects[0]) : null);
39591
+ if (playAgainEvent) emit(`UI:${playAgainEvent}`, {});
39592
+ }, [objects, playAgainEvent, emit]);
39451
39593
  if (!resolved) return null;
39452
39594
  const objectViewers = objects.map((obj) => {
39453
39595
  const states = objStates(obj);
@@ -39516,12 +39658,12 @@ function EventHandlerBoard({
39516
39658
  {
39517
39659
  object: selectedObject,
39518
39660
  onRulesChange: handleRulesChange,
39519
- disabled: playState !== "editing"
39661
+ disabled: isPlaying
39520
39662
  }
39521
39663
  ),
39522
39664
  eventLog.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(EventLog, { entries: eventLog }),
39523
- playState === "success" && /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "p-4 rounded-container bg-success/20 border border-success text-center", children: /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "h5", className: "text-success", children: str(resolved.successMessage) || t("eventHandler.chainComplete") }) }),
39524
- playState === "fail" && /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "sm", children: [
39665
+ isSuccess && /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "p-4 rounded-container bg-success/20 border border-success text-center", children: /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "h5", className: "text-success", children: str(resolved.successMessage) || t("eventHandler.chainComplete") }) }),
39666
+ !isPlaying && !isSuccess && attempts > 0 && /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "sm", children: [
39525
39667
  /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "p-4 rounded-container bg-warning/10 border border-warning/30 text-center", children: /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "body1", className: "text-foreground font-medium", children: t(encourageKey) }) }),
39526
39668
  showHint && /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "p-3 rounded-container bg-accent/10 border border-accent/30", children: /* @__PURE__ */ jsxRuntime.jsxs(HStack, { className: "items-start", gap: "xs", children: [
39527
39669
  /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "body2", className: "text-accent font-bold shrink-0", children: "\u{1F4A1} " + t("game.hint") + ":" }),
@@ -39529,12 +39671,12 @@ function EventHandlerBoard({
39529
39671
  ] }) })
39530
39672
  ] }),
39531
39673
  /* @__PURE__ */ jsxRuntime.jsxs(HStack, { gap: "sm", children: [
39532
- playState === "fail" ? /* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "primary", onClick: handleTryAgain, children: "\u{1F504} " + t("puzzle.tryAgainButton") }) : /* @__PURE__ */ jsxRuntime.jsx(
39674
+ !isPlaying && !isSuccess && attempts > 0 ? /* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "primary", onClick: handleTryAgain, children: "\u{1F504} " + t("puzzle.tryAgainButton") }) : /* @__PURE__ */ jsxRuntime.jsx(
39533
39675
  Button,
39534
39676
  {
39535
39677
  variant: "primary",
39536
39678
  onClick: handlePlay,
39537
- disabled: playState !== "editing",
39679
+ disabled: isPlaying || isSuccess,
39538
39680
  children: "\u25B6 " + t("game.play")
39539
39681
  }
39540
39682
  ),
@@ -40719,6 +40861,1568 @@ var init_GameAudioToggle = __esm({
40719
40861
  GameAudioToggle.displayName = "GameAudioToggle";
40720
40862
  }
40721
40863
  });
40864
+ function detectAssetRoot(modelUrl) {
40865
+ const idx = modelUrl.indexOf("/3d/");
40866
+ if (idx !== -1) {
40867
+ return modelUrl.substring(0, idx + 4);
40868
+ }
40869
+ return modelUrl.substring(0, modelUrl.lastIndexOf("/") + 1);
40870
+ }
40871
+ function createGLTFLoaderForUrl(url) {
40872
+ const loader = new GLTFLoader_js.GLTFLoader();
40873
+ loader.setResourcePath(detectAssetRoot(url));
40874
+ return loader;
40875
+ }
40876
+ var AssetLoader;
40877
+ var init_AssetLoader = __esm({
40878
+ "components/game/molecules/three/loaders/AssetLoader.ts"() {
40879
+ AssetLoader = class {
40880
+ constructor() {
40881
+ __publicField(this, "objLoader");
40882
+ __publicField(this, "textureLoader");
40883
+ __publicField(this, "modelCache");
40884
+ __publicField(this, "textureCache");
40885
+ __publicField(this, "loadingPromises");
40886
+ this.objLoader = new OBJLoader_js.OBJLoader();
40887
+ this.textureLoader = new THREE__namespace.TextureLoader();
40888
+ this.modelCache = /* @__PURE__ */ new Map();
40889
+ this.textureCache = /* @__PURE__ */ new Map();
40890
+ this.loadingPromises = /* @__PURE__ */ new Map();
40891
+ }
40892
+ /**
40893
+ * Load a GLB/GLTF model
40894
+ * @param url - URL to the .glb or .gltf file
40895
+ * @returns Promise with loaded model scene and animations
40896
+ */
40897
+ async loadModel(url) {
40898
+ if (this.modelCache.has(url)) {
40899
+ return this.modelCache.get(url);
40900
+ }
40901
+ if (this.loadingPromises.has(url)) {
40902
+ return this.loadingPromises.get(url);
40903
+ }
40904
+ const loader = createGLTFLoaderForUrl(url);
40905
+ const loadPromise = loader.loadAsync(url).then((gltf) => {
40906
+ const result = {
40907
+ scene: gltf.scene,
40908
+ animations: gltf.animations || []
40909
+ };
40910
+ this.modelCache.set(url, result);
40911
+ this.loadingPromises.delete(url);
40912
+ return result;
40913
+ }).catch((error) => {
40914
+ this.loadingPromises.delete(url);
40915
+ throw new Error(`Failed to load model ${url}: ${error.message}`);
40916
+ });
40917
+ this.loadingPromises.set(url, loadPromise);
40918
+ return loadPromise;
40919
+ }
40920
+ /**
40921
+ * Load an OBJ model (fallback for non-GLB assets)
40922
+ * @param url - URL to the .obj file
40923
+ * @returns Promise with loaded object group
40924
+ */
40925
+ async loadOBJ(url) {
40926
+ if (this.modelCache.has(url)) {
40927
+ return this.modelCache.get(url).scene;
40928
+ }
40929
+ if (this.loadingPromises.has(url)) {
40930
+ const result = await this.loadingPromises.get(url);
40931
+ return result.scene;
40932
+ }
40933
+ const loadPromise = this.objLoader.loadAsync(url).then((group) => {
40934
+ const result = {
40935
+ scene: group,
40936
+ animations: []
40937
+ };
40938
+ this.modelCache.set(url, result);
40939
+ this.loadingPromises.delete(url);
40940
+ return result;
40941
+ }).catch((error) => {
40942
+ this.loadingPromises.delete(url);
40943
+ throw new Error(`Failed to load OBJ ${url}: ${error.message}`);
40944
+ });
40945
+ this.loadingPromises.set(url, loadPromise);
40946
+ return (await loadPromise).scene;
40947
+ }
40948
+ /**
40949
+ * Load a texture
40950
+ * @param url - URL to the texture image
40951
+ * @returns Promise with loaded texture
40952
+ */
40953
+ async loadTexture(url) {
40954
+ if (this.textureCache.has(url)) {
40955
+ return this.textureCache.get(url);
40956
+ }
40957
+ if (this.loadingPromises.has(`texture:${url}`)) {
40958
+ return this.loadingPromises.get(`texture:${url}`);
40959
+ }
40960
+ const loadPromise = this.textureLoader.loadAsync(url).then((texture) => {
40961
+ texture.colorSpace = THREE__namespace.SRGBColorSpace;
40962
+ this.textureCache.set(url, texture);
40963
+ this.loadingPromises.delete(`texture:${url}`);
40964
+ return texture;
40965
+ }).catch((error) => {
40966
+ this.loadingPromises.delete(`texture:${url}`);
40967
+ throw new Error(`Failed to load texture ${url}: ${error.message}`);
40968
+ });
40969
+ this.loadingPromises.set(`texture:${url}`, loadPromise);
40970
+ return loadPromise;
40971
+ }
40972
+ /**
40973
+ * Preload multiple assets
40974
+ * @param urls - Array of asset URLs to preload
40975
+ * @returns Promise that resolves when all assets are loaded
40976
+ */
40977
+ async preload(urls) {
40978
+ const promises = urls.map((url) => {
40979
+ if (url.endsWith(".glb") || url.endsWith(".gltf")) {
40980
+ return this.loadModel(url).catch(() => null);
40981
+ } else if (url.endsWith(".obj")) {
40982
+ return this.loadOBJ(url).catch(() => null);
40983
+ } else if (/\.(png|jpg|jpeg|webp)$/i.test(url)) {
40984
+ return this.loadTexture(url).catch(() => null);
40985
+ }
40986
+ return Promise.resolve(null);
40987
+ });
40988
+ await Promise.all(promises);
40989
+ }
40990
+ /**
40991
+ * Check if a model is cached
40992
+ * @param url - Model URL
40993
+ */
40994
+ hasModel(url) {
40995
+ return this.modelCache.has(url);
40996
+ }
40997
+ /**
40998
+ * Check if a texture is cached
40999
+ * @param url - Texture URL
41000
+ */
41001
+ hasTexture(url) {
41002
+ return this.textureCache.has(url);
41003
+ }
41004
+ /**
41005
+ * Get cached model (throws if not cached)
41006
+ * @param url - Model URL
41007
+ */
41008
+ getModel(url) {
41009
+ const model = this.modelCache.get(url);
41010
+ if (!model) {
41011
+ throw new Error(`Model ${url} not in cache`);
41012
+ }
41013
+ return model;
41014
+ }
41015
+ /**
41016
+ * Get cached texture (throws if not cached)
41017
+ * @param url - Texture URL
41018
+ */
41019
+ getTexture(url) {
41020
+ const texture = this.textureCache.get(url);
41021
+ if (!texture) {
41022
+ throw new Error(`Texture ${url} not in cache`);
41023
+ }
41024
+ return texture;
41025
+ }
41026
+ /**
41027
+ * Clear all caches
41028
+ */
41029
+ clearCache() {
41030
+ this.textureCache.forEach((texture) => {
41031
+ texture.dispose();
41032
+ });
41033
+ this.modelCache.forEach((model) => {
41034
+ model.scene.traverse((child) => {
41035
+ if (child instanceof THREE__namespace.Mesh) {
41036
+ child.geometry.dispose();
41037
+ if (Array.isArray(child.material)) {
41038
+ child.material.forEach((m) => m.dispose());
41039
+ } else {
41040
+ child.material.dispose();
41041
+ }
41042
+ }
41043
+ });
41044
+ });
41045
+ this.modelCache.clear();
41046
+ this.textureCache.clear();
41047
+ this.loadingPromises.clear();
41048
+ }
41049
+ /**
41050
+ * Get cache statistics
41051
+ */
41052
+ getStats() {
41053
+ return {
41054
+ models: this.modelCache.size,
41055
+ textures: this.textureCache.size,
41056
+ loading: this.loadingPromises.size
41057
+ };
41058
+ }
41059
+ };
41060
+ new AssetLoader();
41061
+ }
41062
+ });
41063
+ function useAssetLoader(options = {}) {
41064
+ const { preloadUrls = [], loader: customLoader } = options;
41065
+ const loaderRef = React82.useRef(customLoader || new AssetLoader());
41066
+ const [state, setState] = React82.useState({
41067
+ isLoading: false,
41068
+ progress: 0,
41069
+ loaded: 0,
41070
+ total: 0,
41071
+ errors: []
41072
+ });
41073
+ React82.useEffect(() => {
41074
+ if (preloadUrls.length > 0) {
41075
+ preload(preloadUrls);
41076
+ }
41077
+ }, []);
41078
+ const updateProgress = React82.useCallback((loaded, total) => {
41079
+ setState((prev) => ({
41080
+ ...prev,
41081
+ loaded,
41082
+ total,
41083
+ progress: total > 0 ? Math.round(loaded / total * 100) : 0
41084
+ }));
41085
+ }, []);
41086
+ const loadModel = React82.useCallback(
41087
+ async (url) => {
41088
+ setState((prev) => ({ ...prev, isLoading: true }));
41089
+ try {
41090
+ const model = await loaderRef.current.loadModel(url);
41091
+ setState((prev) => ({
41092
+ ...prev,
41093
+ isLoading: false,
41094
+ loaded: prev.loaded + 1
41095
+ }));
41096
+ return model;
41097
+ } catch (error) {
41098
+ const errorMsg = error instanceof Error ? error.message : String(error);
41099
+ setState((prev) => ({
41100
+ ...prev,
41101
+ isLoading: false,
41102
+ errors: [...prev.errors, errorMsg]
41103
+ }));
41104
+ throw error;
41105
+ }
41106
+ },
41107
+ []
41108
+ );
41109
+ const loadOBJ = React82.useCallback(
41110
+ async (url) => {
41111
+ setState((prev) => ({ ...prev, isLoading: true }));
41112
+ try {
41113
+ const model = await loaderRef.current.loadOBJ(url);
41114
+ setState((prev) => ({
41115
+ ...prev,
41116
+ isLoading: false,
41117
+ loaded: prev.loaded + 1
41118
+ }));
41119
+ return model;
41120
+ } catch (error) {
41121
+ const errorMsg = error instanceof Error ? error.message : String(error);
41122
+ setState((prev) => ({
41123
+ ...prev,
41124
+ isLoading: false,
41125
+ errors: [...prev.errors, errorMsg]
41126
+ }));
41127
+ throw error;
41128
+ }
41129
+ },
41130
+ []
41131
+ );
41132
+ const loadTexture = React82.useCallback(
41133
+ async (url) => {
41134
+ setState((prev) => ({ ...prev, isLoading: true }));
41135
+ try {
41136
+ const texture = await loaderRef.current.loadTexture(url);
41137
+ setState((prev) => ({
41138
+ ...prev,
41139
+ isLoading: false,
41140
+ loaded: prev.loaded + 1
41141
+ }));
41142
+ return texture;
41143
+ } catch (error) {
41144
+ const errorMsg = error instanceof Error ? error.message : String(error);
41145
+ setState((prev) => ({
41146
+ ...prev,
41147
+ isLoading: false,
41148
+ errors: [...prev.errors, errorMsg]
41149
+ }));
41150
+ throw error;
41151
+ }
41152
+ },
41153
+ []
41154
+ );
41155
+ const preload = React82.useCallback(
41156
+ async (urls) => {
41157
+ setState((prev) => ({
41158
+ ...prev,
41159
+ isLoading: true,
41160
+ total: urls.length,
41161
+ loaded: 0,
41162
+ errors: []
41163
+ }));
41164
+ let completed = 0;
41165
+ const errors = [];
41166
+ await Promise.all(
41167
+ urls.map(async (url) => {
41168
+ try {
41169
+ if (url.endsWith(".glb") || url.endsWith(".gltf")) {
41170
+ await loaderRef.current.loadModel(url);
41171
+ } else if (url.endsWith(".obj")) {
41172
+ await loaderRef.current.loadOBJ(url);
41173
+ } else if (/\.(png|jpg|jpeg|webp)$/i.test(url)) {
41174
+ await loaderRef.current.loadTexture(url);
41175
+ }
41176
+ completed++;
41177
+ updateProgress(completed, urls.length);
41178
+ } catch (error) {
41179
+ const errorMsg = error instanceof Error ? error.message : String(error);
41180
+ errors.push(`${url}: ${errorMsg}`);
41181
+ completed++;
41182
+ updateProgress(completed, urls.length);
41183
+ }
41184
+ })
41185
+ );
41186
+ setState((prev) => ({
41187
+ ...prev,
41188
+ isLoading: false,
41189
+ errors
41190
+ }));
41191
+ },
41192
+ [updateProgress]
41193
+ );
41194
+ const hasModel = React82.useCallback((url) => {
41195
+ return loaderRef.current.hasModel(url);
41196
+ }, []);
41197
+ const hasTexture = React82.useCallback((url) => {
41198
+ return loaderRef.current.hasTexture(url);
41199
+ }, []);
41200
+ const getModel = React82.useCallback((url) => {
41201
+ try {
41202
+ return loaderRef.current.getModel(url);
41203
+ } catch {
41204
+ return void 0;
41205
+ }
41206
+ }, []);
41207
+ const getTexture = React82.useCallback((url) => {
41208
+ try {
41209
+ return loaderRef.current.getTexture(url);
41210
+ } catch {
41211
+ return void 0;
41212
+ }
41213
+ }, []);
41214
+ const clearCache = React82.useCallback(() => {
41215
+ loaderRef.current.clearCache();
41216
+ setState({
41217
+ isLoading: false,
41218
+ progress: 0,
41219
+ loaded: 0,
41220
+ total: 0,
41221
+ errors: []
41222
+ });
41223
+ }, []);
41224
+ return {
41225
+ ...state,
41226
+ loadModel,
41227
+ loadOBJ,
41228
+ loadTexture,
41229
+ preload,
41230
+ hasModel,
41231
+ hasTexture,
41232
+ getModel,
41233
+ getTexture,
41234
+ clearCache
41235
+ };
41236
+ }
41237
+ var init_useAssetLoader = __esm({
41238
+ "components/game/molecules/three/hooks/useAssetLoader.ts"() {
41239
+ "use client";
41240
+ init_AssetLoader();
41241
+ }
41242
+ });
41243
+ function useGameCanvas3DEvents(options) {
41244
+ const {
41245
+ tileClickEvent,
41246
+ unitClickEvent,
41247
+ featureClickEvent,
41248
+ canvasClickEvent,
41249
+ tileHoverEvent,
41250
+ tileLeaveEvent,
41251
+ unitAnimationEvent,
41252
+ cameraChangeEvent,
41253
+ onTileClick,
41254
+ onUnitClick,
41255
+ onFeatureClick,
41256
+ onCanvasClick,
41257
+ onTileHover,
41258
+ onUnitAnimation
41259
+ } = options;
41260
+ const emit = useEmitEvent();
41261
+ const optionsRef = React82.useRef(options);
41262
+ optionsRef.current = options;
41263
+ const handleTileClick = React82.useCallback(
41264
+ (tile, event) => {
41265
+ if (tileClickEvent) {
41266
+ emit(tileClickEvent, {
41267
+ tileId: tile.id,
41268
+ x: tile.x,
41269
+ z: tile.z ?? tile.y ?? 0,
41270
+ type: tile.type,
41271
+ terrain: tile.terrain,
41272
+ elevation: tile.elevation
41273
+ });
41274
+ }
41275
+ optionsRef.current.onTileClick?.(tile, event);
41276
+ },
41277
+ [tileClickEvent, emit]
41278
+ );
41279
+ const handleUnitClick = React82.useCallback(
41280
+ (unit, event) => {
41281
+ if (unitClickEvent) {
41282
+ emit(unitClickEvent, {
41283
+ unitId: unit.id,
41284
+ x: unit.x,
41285
+ z: unit.z ?? unit.y ?? 0,
41286
+ unitType: unit.unitType,
41287
+ name: unit.name,
41288
+ team: unit.team,
41289
+ faction: unit.faction,
41290
+ health: unit.health,
41291
+ maxHealth: unit.maxHealth
41292
+ });
41293
+ }
41294
+ optionsRef.current.onUnitClick?.(unit, event);
41295
+ },
41296
+ [unitClickEvent, emit]
41297
+ );
41298
+ const handleFeatureClick = React82.useCallback(
41299
+ (feature, event) => {
41300
+ if (featureClickEvent) {
41301
+ emit(featureClickEvent, {
41302
+ featureId: feature.id,
41303
+ x: feature.x,
41304
+ z: feature.z ?? feature.y ?? 0,
41305
+ type: feature.type,
41306
+ elevation: feature.elevation
41307
+ });
41308
+ }
41309
+ optionsRef.current.onFeatureClick?.(feature, event);
41310
+ },
41311
+ [featureClickEvent, emit]
41312
+ );
41313
+ const handleCanvasClick = React82.useCallback(
41314
+ (event) => {
41315
+ if (canvasClickEvent) {
41316
+ emit(canvasClickEvent, {
41317
+ clientX: event.clientX,
41318
+ clientY: event.clientY,
41319
+ button: event.button
41320
+ });
41321
+ }
41322
+ optionsRef.current.onCanvasClick?.(event);
41323
+ },
41324
+ [canvasClickEvent, emit]
41325
+ );
41326
+ const handleTileHover = React82.useCallback(
41327
+ (tile, event) => {
41328
+ if (tile) {
41329
+ if (tileHoverEvent) {
41330
+ emit(tileHoverEvent, {
41331
+ tileId: tile.id,
41332
+ x: tile.x,
41333
+ z: tile.z ?? tile.y ?? 0,
41334
+ type: tile.type
41335
+ });
41336
+ }
41337
+ } else {
41338
+ if (tileLeaveEvent) {
41339
+ emit(tileLeaveEvent, {});
41340
+ }
41341
+ }
41342
+ optionsRef.current.onTileHover?.(tile, event);
41343
+ },
41344
+ [tileHoverEvent, tileLeaveEvent, emit]
41345
+ );
41346
+ const handleUnitAnimation = React82.useCallback(
41347
+ (unitId, state) => {
41348
+ if (unitAnimationEvent) {
41349
+ emit(unitAnimationEvent, {
41350
+ unitId,
41351
+ state,
41352
+ timestamp: Date.now()
41353
+ });
41354
+ }
41355
+ optionsRef.current.onUnitAnimation?.(unitId, state);
41356
+ },
41357
+ [unitAnimationEvent, emit]
41358
+ );
41359
+ const handleCameraChange = React82.useCallback(
41360
+ (position) => {
41361
+ if (cameraChangeEvent) {
41362
+ emit(cameraChangeEvent, {
41363
+ position,
41364
+ timestamp: Date.now()
41365
+ });
41366
+ }
41367
+ },
41368
+ [cameraChangeEvent, emit]
41369
+ );
41370
+ return {
41371
+ handleTileClick,
41372
+ handleUnitClick,
41373
+ handleFeatureClick,
41374
+ handleCanvasClick,
41375
+ handleTileHover,
41376
+ handleUnitAnimation,
41377
+ handleCameraChange
41378
+ };
41379
+ }
41380
+ var init_useGameCanvas3DEvents = __esm({
41381
+ "components/game/molecules/three/hooks/useGameCanvas3DEvents.ts"() {
41382
+ "use client";
41383
+ init_useEventBus();
41384
+ }
41385
+ });
41386
+
41387
+ // components/game/molecules/three/components/Canvas3DLoadingState.css
41388
+ var init_Canvas3DLoadingState = __esm({
41389
+ "components/game/molecules/three/components/Canvas3DLoadingState.css"() {
41390
+ }
41391
+ });
41392
+ function Canvas3DLoadingState({
41393
+ progress = 0,
41394
+ loaded = 0,
41395
+ total = 0,
41396
+ message = "Loading 3D Scene...",
41397
+ details,
41398
+ showSpinner = true,
41399
+ className
41400
+ }) {
41401
+ const clampedProgress = Math.max(0, Math.min(100, progress));
41402
+ const hasProgress = total > 0;
41403
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `canvas-3d-loading ${className || ""}`, children: [
41404
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "canvas-3d-loading__content", children: [
41405
+ showSpinner && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "canvas-3d-loading__spinner", children: [
41406
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "spinner__ring" }),
41407
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "spinner__ring spinner__ring--secondary" })
41408
+ ] }),
41409
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "canvas-3d-loading__message", children: message }),
41410
+ details && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "canvas-3d-loading__details", children: details }),
41411
+ hasProgress && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "canvas-3d-loading__progress", children: [
41412
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "progress__bar", children: /* @__PURE__ */ jsxRuntime.jsx(
41413
+ "div",
41414
+ {
41415
+ className: "progress__fill",
41416
+ style: { width: `${clampedProgress}%` }
41417
+ }
41418
+ ) }),
41419
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "progress__text", children: [
41420
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "progress__percentage", children: [
41421
+ clampedProgress,
41422
+ "%"
41423
+ ] }),
41424
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "progress__count", children: [
41425
+ "(",
41426
+ loaded,
41427
+ "/",
41428
+ total,
41429
+ ")"
41430
+ ] })
41431
+ ] })
41432
+ ] })
41433
+ ] }),
41434
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "canvas-3d-loading__background", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bg__grid" }) })
41435
+ ] });
41436
+ }
41437
+ var init_Canvas3DLoadingState2 = __esm({
41438
+ "components/game/molecules/three/components/Canvas3DLoadingState.tsx"() {
41439
+ init_Canvas3DLoadingState();
41440
+ }
41441
+ });
41442
+
41443
+ // components/game/molecules/three/components/Canvas3DErrorBoundary.css
41444
+ var init_Canvas3DErrorBoundary = __esm({
41445
+ "components/game/molecules/three/components/Canvas3DErrorBoundary.css"() {
41446
+ }
41447
+ });
41448
+ var log10, Canvas3DErrorBoundary;
41449
+ var init_Canvas3DErrorBoundary2 = __esm({
41450
+ "components/game/molecules/three/components/Canvas3DErrorBoundary.tsx"() {
41451
+ init_Canvas3DErrorBoundary();
41452
+ log10 = logger.createLogger("almadar:ui:game:canvas3d:error-boundary");
41453
+ Canvas3DErrorBoundary = class extends React82.Component {
41454
+ constructor(props) {
41455
+ super(props);
41456
+ __publicField(this, "handleReset", () => {
41457
+ this.setState({
41458
+ hasError: false,
41459
+ error: null,
41460
+ errorInfo: null
41461
+ });
41462
+ this.props.onReset?.();
41463
+ });
41464
+ this.state = {
41465
+ hasError: false,
41466
+ error: null,
41467
+ errorInfo: null
41468
+ };
41469
+ }
41470
+ static getDerivedStateFromError(error) {
41471
+ return {
41472
+ hasError: true,
41473
+ error,
41474
+ errorInfo: null
41475
+ };
41476
+ }
41477
+ componentDidCatch(error, errorInfo) {
41478
+ this.setState({ errorInfo });
41479
+ this.props.onError?.(error, errorInfo);
41480
+ log10.error("Error caught", { error });
41481
+ log10.error("Component stack", { componentStack: errorInfo.componentStack ?? "<none>" });
41482
+ }
41483
+ render() {
41484
+ if (this.state.hasError) {
41485
+ if (this.props.fallback) {
41486
+ return this.props.fallback;
41487
+ }
41488
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "canvas-3d-error", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "canvas-3d-error__content", children: [
41489
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "canvas-3d-error__icon", children: "\u26A0\uFE0F" }),
41490
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "canvas-3d-error__title", children: "3D Scene Error" }),
41491
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "canvas-3d-error__message", children: "Something went wrong while rendering the 3D scene." }),
41492
+ this.state.error && /* @__PURE__ */ jsxRuntime.jsxs("details", { className: "canvas-3d-error__details", children: [
41493
+ /* @__PURE__ */ jsxRuntime.jsx("summary", { children: "Error Details" }),
41494
+ /* @__PURE__ */ jsxRuntime.jsxs("pre", { className: "error__stack", children: [
41495
+ this.state.error.message,
41496
+ "\n",
41497
+ this.state.error.stack
41498
+ ] }),
41499
+ this.state.errorInfo && /* @__PURE__ */ jsxRuntime.jsx("pre", { className: "error__component-stack", children: this.state.errorInfo.componentStack })
41500
+ ] }),
41501
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "canvas-3d-error__actions", children: [
41502
+ /* @__PURE__ */ jsxRuntime.jsx(
41503
+ "button",
41504
+ {
41505
+ className: "error__button error__button--primary",
41506
+ onClick: this.handleReset,
41507
+ children: "Try Again"
41508
+ }
41509
+ ),
41510
+ /* @__PURE__ */ jsxRuntime.jsx(
41511
+ "button",
41512
+ {
41513
+ className: "error__button error__button--secondary",
41514
+ onClick: () => window.location.reload(),
41515
+ children: "Reload Page"
41516
+ }
41517
+ )
41518
+ ] })
41519
+ ] }) });
41520
+ }
41521
+ return this.props.children;
41522
+ }
41523
+ };
41524
+ }
41525
+ });
41526
+ function detectAssetRoot2(modelUrl) {
41527
+ const idx = modelUrl.indexOf("/3d/");
41528
+ if (idx !== -1) {
41529
+ return modelUrl.substring(0, idx + 4);
41530
+ }
41531
+ return modelUrl.substring(0, modelUrl.lastIndexOf("/") + 1);
41532
+ }
41533
+ function useGLTFModel(url, resourceBasePath) {
41534
+ const [state, setState] = React82.useState({
41535
+ model: null,
41536
+ isLoading: false,
41537
+ error: null
41538
+ });
41539
+ React82.useEffect(() => {
41540
+ if (!url) {
41541
+ setState({ model: null, isLoading: false, error: null });
41542
+ return;
41543
+ }
41544
+ log11.debug("Loading", { url });
41545
+ setState((prev) => ({ ...prev, isLoading: true, error: null }));
41546
+ const assetRoot = resourceBasePath || detectAssetRoot2(url);
41547
+ const loader = new GLTFLoader.GLTFLoader();
41548
+ loader.setResourcePath(assetRoot);
41549
+ loader.load(
41550
+ url,
41551
+ (gltf) => {
41552
+ log11.debug("Loaded", { url });
41553
+ setState({
41554
+ model: gltf.scene,
41555
+ isLoading: false,
41556
+ error: null
41557
+ });
41558
+ },
41559
+ void 0,
41560
+ (err) => {
41561
+ log11.warn("Failed", { url, error: err instanceof Error ? err : String(err) });
41562
+ setState({
41563
+ model: null,
41564
+ isLoading: false,
41565
+ error: err instanceof Error ? err : new Error(String(err))
41566
+ });
41567
+ }
41568
+ );
41569
+ }, [url, resourceBasePath]);
41570
+ return state;
41571
+ }
41572
+ function ModelLoader({
41573
+ url,
41574
+ position = [0, 0, 0],
41575
+ scale = 1,
41576
+ rotation = [0, 0, 0],
41577
+ isSelected = false,
41578
+ isHovered = false,
41579
+ onClick,
41580
+ onHover,
41581
+ fallbackGeometry = "box",
41582
+ castShadow = true,
41583
+ receiveShadow = true,
41584
+ resourceBasePath
41585
+ }) {
41586
+ const { model: loadedModel, isLoading, error } = useGLTFModel(url, resourceBasePath);
41587
+ const model = React82.useMemo(() => {
41588
+ if (!loadedModel) return null;
41589
+ const cloned = loadedModel.clone();
41590
+ cloned.traverse((child) => {
41591
+ if (child instanceof THREE__namespace.Mesh) {
41592
+ child.castShadow = castShadow;
41593
+ child.receiveShadow = receiveShadow;
41594
+ }
41595
+ });
41596
+ return cloned;
41597
+ }, [loadedModel, castShadow, receiveShadow]);
41598
+ const scaleArray = React82.useMemo(() => {
41599
+ if (typeof scale === "number") {
41600
+ return [scale, scale, scale];
41601
+ }
41602
+ return scale;
41603
+ }, [scale]);
41604
+ const rotationRad = React82.useMemo(() => {
41605
+ return [
41606
+ rotation[0] * Math.PI / 180,
41607
+ rotation[1] * Math.PI / 180,
41608
+ rotation[2] * Math.PI / 180
41609
+ ];
41610
+ }, [rotation]);
41611
+ if (isLoading) {
41612
+ return /* @__PURE__ */ jsxRuntime.jsx("group", { position, children: /* @__PURE__ */ jsxRuntime.jsxs("mesh", { rotation: [Math.PI / 2, 0, 0], children: [
41613
+ /* @__PURE__ */ jsxRuntime.jsx("ringGeometry", { args: [0.3, 0.35, 16] }),
41614
+ /* @__PURE__ */ jsxRuntime.jsx("meshBasicMaterial", { color: "#4a90d9", transparent: true, opacity: 0.8 })
41615
+ ] }) });
41616
+ }
41617
+ if (error || !model) {
41618
+ if (fallbackGeometry === "none") {
41619
+ return /* @__PURE__ */ jsxRuntime.jsx("group", { position });
41620
+ }
41621
+ const fallbackProps = {
41622
+ onClick,
41623
+ onPointerOver: () => onHover?.(true),
41624
+ onPointerOut: () => onHover?.(false)
41625
+ };
41626
+ return /* @__PURE__ */ jsxRuntime.jsxs("group", { position, children: [
41627
+ (isSelected || isHovered) && /* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, 0.02, 0], rotation: [-Math.PI / 2, 0, 0], children: [
41628
+ /* @__PURE__ */ jsxRuntime.jsx("ringGeometry", { args: [0.6, 0.7, 32] }),
41629
+ /* @__PURE__ */ jsxRuntime.jsx(
41630
+ "meshBasicMaterial",
41631
+ {
41632
+ color: isSelected ? 16755200 : 16777215,
41633
+ transparent: true,
41634
+ opacity: 0.5
41635
+ }
41636
+ )
41637
+ ] }),
41638
+ fallbackGeometry === "box" && /* @__PURE__ */ jsxRuntime.jsxs("mesh", { ...fallbackProps, position: [0, 0.5, 0], children: [
41639
+ /* @__PURE__ */ jsxRuntime.jsx("boxGeometry", { args: [0.8, 0.8, 0.8] }),
41640
+ /* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color: error ? 16729156 : 8947848 })
41641
+ ] }),
41642
+ fallbackGeometry === "sphere" && /* @__PURE__ */ jsxRuntime.jsxs("mesh", { ...fallbackProps, position: [0, 0.5, 0], children: [
41643
+ /* @__PURE__ */ jsxRuntime.jsx("sphereGeometry", { args: [0.4, 16, 16] }),
41644
+ /* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color: error ? 16729156 : 8947848 })
41645
+ ] }),
41646
+ fallbackGeometry === "cylinder" && /* @__PURE__ */ jsxRuntime.jsxs("mesh", { ...fallbackProps, position: [0, 0.5, 0], children: [
41647
+ /* @__PURE__ */ jsxRuntime.jsx("cylinderGeometry", { args: [0.3, 0.3, 0.8, 16] }),
41648
+ /* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color: error ? 16729156 : 8947848 })
41649
+ ] })
41650
+ ] });
41651
+ }
41652
+ return /* @__PURE__ */ jsxRuntime.jsxs(
41653
+ "group",
41654
+ {
41655
+ position,
41656
+ rotation: rotationRad,
41657
+ onClick,
41658
+ onPointerOver: () => onHover?.(true),
41659
+ onPointerOut: () => onHover?.(false),
41660
+ children: [
41661
+ (isSelected || isHovered) && /* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, 0.02, 0], rotation: [-Math.PI / 2, 0, 0], children: [
41662
+ /* @__PURE__ */ jsxRuntime.jsx("ringGeometry", { args: [0.6, 0.7, 32] }),
41663
+ /* @__PURE__ */ jsxRuntime.jsx(
41664
+ "meshBasicMaterial",
41665
+ {
41666
+ color: isSelected ? 16755200 : 16777215,
41667
+ transparent: true,
41668
+ opacity: 0.5
41669
+ }
41670
+ )
41671
+ ] }),
41672
+ /* @__PURE__ */ jsxRuntime.jsx("primitive", { object: model, scale: scaleArray })
41673
+ ]
41674
+ }
41675
+ );
41676
+ }
41677
+ var log11;
41678
+ var init_ModelLoader = __esm({
41679
+ "components/game/molecules/three/components/ModelLoader.tsx"() {
41680
+ "use client";
41681
+ log11 = logger.createLogger("almadar:ui:game:model-loader");
41682
+ }
41683
+ });
41684
+
41685
+ // components/game/molecules/GameCanvas3D.css
41686
+ var init_GameCanvas3D = __esm({
41687
+ "components/game/molecules/GameCanvas3D.css"() {
41688
+ }
41689
+ });
41690
+ function CameraController({
41691
+ onCameraChange
41692
+ }) {
41693
+ const { camera } = fiber.useThree();
41694
+ React82.useEffect(() => {
41695
+ if (onCameraChange) {
41696
+ onCameraChange({
41697
+ x: camera.position.x,
41698
+ y: camera.position.y,
41699
+ z: camera.position.z
41700
+ });
41701
+ }
41702
+ }, [camera.position, onCameraChange]);
41703
+ return null;
41704
+ }
41705
+ var DEFAULT_GRID_CONFIG, GameCanvas3D;
41706
+ var init_GameCanvas3D2 = __esm({
41707
+ "components/game/molecules/GameCanvas3D.tsx"() {
41708
+ "use client";
41709
+ init_AssetLoader();
41710
+ init_useAssetLoader();
41711
+ init_useGameCanvas3DEvents();
41712
+ init_Canvas3DLoadingState2();
41713
+ init_Canvas3DErrorBoundary2();
41714
+ init_ModelLoader();
41715
+ init_cn();
41716
+ init_GameCanvas3D();
41717
+ DEFAULT_GRID_CONFIG = {
41718
+ cellSize: 1,
41719
+ offsetX: 0,
41720
+ offsetZ: 0
41721
+ };
41722
+ GameCanvas3D = React82.forwardRef(
41723
+ ({
41724
+ tiles = [],
41725
+ units = [],
41726
+ features = [],
41727
+ events: events2 = [],
41728
+ orientation = "standard",
41729
+ cameraMode = "isometric",
41730
+ showGrid = true,
41731
+ showCoordinates = false,
41732
+ showTileInfo = false,
41733
+ overlay = "default",
41734
+ shadows = true,
41735
+ backgroundColor = "#1a1a2e",
41736
+ onTileClick,
41737
+ onUnitClick,
41738
+ onFeatureClick,
41739
+ onCanvasClick,
41740
+ onTileHover,
41741
+ onUnitAnimation,
41742
+ assetLoader: customAssetLoader,
41743
+ tileRenderer: CustomTileRenderer,
41744
+ unitRenderer: CustomUnitRenderer,
41745
+ featureRenderer: CustomFeatureRenderer,
41746
+ className,
41747
+ isLoading: externalLoading,
41748
+ error: externalError,
41749
+ entity,
41750
+ preloadAssets = [],
41751
+ tileClickEvent,
41752
+ unitClickEvent,
41753
+ featureClickEvent,
41754
+ canvasClickEvent,
41755
+ tileHoverEvent,
41756
+ tileLeaveEvent,
41757
+ unitAnimationEvent,
41758
+ cameraChangeEvent,
41759
+ loadingMessage = "Loading 3D Scene...",
41760
+ useInstancing = true,
41761
+ validMoves = [],
41762
+ attackTargets = [],
41763
+ selectedTileIds = [],
41764
+ selectedUnitId = null,
41765
+ children
41766
+ }, ref) => {
41767
+ const containerRef = React82.useRef(null);
41768
+ const controlsRef = React82.useRef(null);
41769
+ const [hoveredTile, setHoveredTile] = React82.useState(null);
41770
+ const [internalError, setInternalError] = React82.useState(null);
41771
+ const { isLoading: assetsLoading, progress, loaded, total } = useAssetLoader({
41772
+ preloadUrls: preloadAssets,
41773
+ loader: customAssetLoader
41774
+ });
41775
+ const eventHandlers = useGameCanvas3DEvents({
41776
+ tileClickEvent,
41777
+ unitClickEvent,
41778
+ featureClickEvent,
41779
+ canvasClickEvent,
41780
+ tileHoverEvent,
41781
+ tileLeaveEvent,
41782
+ unitAnimationEvent,
41783
+ cameraChangeEvent,
41784
+ onTileClick,
41785
+ onUnitClick,
41786
+ onFeatureClick,
41787
+ onCanvasClick,
41788
+ onTileHover,
41789
+ onUnitAnimation
41790
+ });
41791
+ const gridBounds = React82.useMemo(() => {
41792
+ if (tiles.length === 0) {
41793
+ return { minX: 0, maxX: 10, minZ: 0, maxZ: 10 };
41794
+ }
41795
+ const xs = tiles.map((t) => t.x);
41796
+ const zs = tiles.map((t) => t.z || t.y || 0);
41797
+ return {
41798
+ minX: Math.min(...xs),
41799
+ maxX: Math.max(...xs),
41800
+ minZ: Math.min(...zs),
41801
+ maxZ: Math.max(...zs)
41802
+ };
41803
+ }, [tiles]);
41804
+ const cameraTarget = React82.useMemo(() => {
41805
+ return [
41806
+ (gridBounds.minX + gridBounds.maxX) / 2,
41807
+ 0,
41808
+ (gridBounds.minZ + gridBounds.maxZ) / 2
41809
+ ];
41810
+ }, [gridBounds]);
41811
+ const gridConfig = React82.useMemo(
41812
+ () => ({
41813
+ ...DEFAULT_GRID_CONFIG,
41814
+ offsetX: -(gridBounds.maxX - gridBounds.minX) / 2,
41815
+ offsetZ: -(gridBounds.maxZ - gridBounds.minZ) / 2
41816
+ }),
41817
+ [gridBounds]
41818
+ );
41819
+ const gridToWorld = React82.useCallback(
41820
+ (x, z, y = 0) => {
41821
+ const worldX = (x - gridBounds.minX) * gridConfig.cellSize;
41822
+ const worldZ = (z - gridBounds.minZ) * gridConfig.cellSize;
41823
+ return [worldX, y * gridConfig.cellSize, worldZ];
41824
+ },
41825
+ [gridBounds, gridConfig]
41826
+ );
41827
+ React82.useImperativeHandle(ref, () => ({
41828
+ getCameraPosition: () => {
41829
+ if (controlsRef.current) {
41830
+ const pos = controlsRef.current.object.position;
41831
+ return new THREE__namespace.Vector3(pos.x, pos.y, pos.z);
41832
+ }
41833
+ return null;
41834
+ },
41835
+ setCameraPosition: (x, y, z) => {
41836
+ if (controlsRef.current) {
41837
+ controlsRef.current.object.position.set(x, y, z);
41838
+ controlsRef.current.update();
41839
+ }
41840
+ },
41841
+ lookAt: (x, y, z) => {
41842
+ if (controlsRef.current) {
41843
+ controlsRef.current.target.set(x, y, z);
41844
+ controlsRef.current.update();
41845
+ }
41846
+ },
41847
+ resetCamera: () => {
41848
+ if (controlsRef.current) {
41849
+ controlsRef.current.reset();
41850
+ }
41851
+ },
41852
+ screenshot: () => {
41853
+ const canvas = containerRef.current?.querySelector("canvas");
41854
+ if (canvas) {
41855
+ return canvas.toDataURL("image/png");
41856
+ }
41857
+ return null;
41858
+ },
41859
+ export: () => ({
41860
+ tiles,
41861
+ units,
41862
+ features
41863
+ })
41864
+ }));
41865
+ const handleTileClick = React82.useCallback(
41866
+ (tile, event) => {
41867
+ eventHandlers.handleTileClick(tile, event);
41868
+ },
41869
+ [eventHandlers]
41870
+ );
41871
+ const handleUnitClick = React82.useCallback(
41872
+ (unit, event) => {
41873
+ eventHandlers.handleUnitClick(unit, event);
41874
+ },
41875
+ [eventHandlers]
41876
+ );
41877
+ const handleFeatureClick = React82.useCallback(
41878
+ (feature, event) => {
41879
+ if (event) {
41880
+ eventHandlers.handleFeatureClick(feature, event);
41881
+ }
41882
+ },
41883
+ [eventHandlers]
41884
+ );
41885
+ const handleTileHover = React82.useCallback(
41886
+ (tile, event) => {
41887
+ setHoveredTile(tile);
41888
+ if (event) {
41889
+ eventHandlers.handleTileHover(tile, event);
41890
+ }
41891
+ },
41892
+ [eventHandlers]
41893
+ );
41894
+ const cameraConfig = React82.useMemo(() => {
41895
+ const size = Math.max(
41896
+ gridBounds.maxX - gridBounds.minX,
41897
+ gridBounds.maxZ - gridBounds.minZ
41898
+ );
41899
+ const distance = size * 1.5;
41900
+ switch (cameraMode) {
41901
+ case "isometric":
41902
+ return {
41903
+ position: [distance, distance * 0.8, distance],
41904
+ fov: 45
41905
+ };
41906
+ case "top-down":
41907
+ return {
41908
+ position: [0, distance * 2, 0],
41909
+ fov: 45
41910
+ };
41911
+ case "perspective":
41912
+ default:
41913
+ return {
41914
+ position: [distance, distance, distance],
41915
+ fov: 45
41916
+ };
41917
+ }
41918
+ }, [cameraMode, gridBounds]);
41919
+ const DefaultTileRenderer = React82.useCallback(
41920
+ ({ tile, position }) => {
41921
+ const isSelected = tile.id ? selectedTileIds.includes(tile.id) : false;
41922
+ const isHovered = hoveredTile?.id === tile.id;
41923
+ const isValidMove = validMoves.some(
41924
+ (m) => m.x === tile.x && m.z === (tile.z ?? tile.y ?? 0)
41925
+ );
41926
+ const isAttackTarget = attackTargets.some(
41927
+ (m) => m.x === tile.x && m.z === (tile.z ?? tile.y ?? 0)
41928
+ );
41929
+ let color = 8421504;
41930
+ if (tile.type === "water") color = 4491468;
41931
+ else if (tile.type === "grass") color = 4500036;
41932
+ else if (tile.type === "sand") color = 14535816;
41933
+ else if (tile.type === "rock") color = 8947848;
41934
+ else if (tile.type === "snow") color = 15658734;
41935
+ let emissive = 0;
41936
+ if (isSelected) emissive = 4473924;
41937
+ else if (isAttackTarget) emissive = 4456448;
41938
+ else if (isValidMove) emissive = 17408;
41939
+ else if (isHovered) emissive = 2236962;
41940
+ if (tile.modelUrl) {
41941
+ return /* @__PURE__ */ jsxRuntime.jsx(
41942
+ "group",
41943
+ {
41944
+ position,
41945
+ onClick: (e) => handleTileClick(tile, e),
41946
+ onPointerEnter: (e) => handleTileHover(tile, e),
41947
+ onPointerLeave: (e) => handleTileHover(null, e),
41948
+ userData: { type: "tile", tileId: tile.id, gridX: tile.x, gridZ: tile.z ?? tile.y },
41949
+ children: /* @__PURE__ */ jsxRuntime.jsx(
41950
+ ModelLoader,
41951
+ {
41952
+ url: tile.modelUrl,
41953
+ scale: 0.95,
41954
+ fallbackGeometry: "box",
41955
+ castShadow: true,
41956
+ receiveShadow: true
41957
+ }
41958
+ )
41959
+ }
41960
+ );
41961
+ }
41962
+ return /* @__PURE__ */ jsxRuntime.jsxs(
41963
+ "mesh",
41964
+ {
41965
+ position,
41966
+ onClick: (e) => handleTileClick(tile, e),
41967
+ onPointerEnter: (e) => handleTileHover(tile, e),
41968
+ onPointerLeave: (e) => handleTileHover(null, e),
41969
+ userData: { type: "tile", tileId: tile.id, gridX: tile.x, gridZ: tile.z ?? tile.y },
41970
+ children: [
41971
+ /* @__PURE__ */ jsxRuntime.jsx("boxGeometry", { args: [0.95, 0.2, 0.95] }),
41972
+ /* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color, emissive })
41973
+ ]
41974
+ }
41975
+ );
41976
+ },
41977
+ [selectedTileIds, hoveredTile, validMoves, attackTargets, handleTileClick, handleTileHover]
41978
+ );
41979
+ const DefaultUnitRenderer = React82.useCallback(
41980
+ ({ unit, position }) => {
41981
+ const isSelected = selectedUnitId === unit.id;
41982
+ const color = unit.faction === "player" ? 4491519 : unit.faction === "enemy" ? 16729156 : 16777028;
41983
+ return /* @__PURE__ */ jsxRuntime.jsxs(
41984
+ "group",
41985
+ {
41986
+ position,
41987
+ onClick: (e) => handleUnitClick(unit, e),
41988
+ userData: { type: "unit", unitId: unit.id },
41989
+ children: [
41990
+ isSelected && /* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, 0.05, 0], rotation: [-Math.PI / 2, 0, 0], children: [
41991
+ /* @__PURE__ */ jsxRuntime.jsx("ringGeometry", { args: [0.4, 0.5, 32] }),
41992
+ /* @__PURE__ */ jsxRuntime.jsx("meshBasicMaterial", { color: "#ffff00", transparent: true, opacity: 0.8 })
41993
+ ] }),
41994
+ unit.modelUrl ? (
41995
+ /* GLB unit model (box fallback while loading / on error) */
41996
+ /* @__PURE__ */ jsxRuntime.jsx(
41997
+ ModelLoader,
41998
+ {
41999
+ url: unit.modelUrl,
42000
+ scale: 0.5,
42001
+ fallbackGeometry: "box",
42002
+ castShadow: true
42003
+ }
42004
+ )
42005
+ ) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
42006
+ /* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, 0.3, 0], children: [
42007
+ /* @__PURE__ */ jsxRuntime.jsx("cylinderGeometry", { args: [0.3, 0.3, 0.1, 8] }),
42008
+ /* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color })
42009
+ ] }),
42010
+ /* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, 0.6, 0], children: [
42011
+ /* @__PURE__ */ jsxRuntime.jsx("capsuleGeometry", { args: [0.2, 0.4, 4, 8] }),
42012
+ /* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color })
42013
+ ] }),
42014
+ /* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, 0.9, 0], children: [
42015
+ /* @__PURE__ */ jsxRuntime.jsx("sphereGeometry", { args: [0.12, 8, 8] }),
42016
+ /* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color })
42017
+ ] })
42018
+ ] }),
42019
+ unit.health !== void 0 && unit.maxHealth !== void 0 && /* @__PURE__ */ jsxRuntime.jsxs("group", { position: [0, 1.2, 0], children: [
42020
+ /* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [-0.25, 0, 0], children: [
42021
+ /* @__PURE__ */ jsxRuntime.jsx("planeGeometry", { args: [0.5, 0.05] }),
42022
+ /* @__PURE__ */ jsxRuntime.jsx("meshBasicMaterial", { color: 3355443 })
42023
+ ] }),
42024
+ /* @__PURE__ */ jsxRuntime.jsxs(
42025
+ "mesh",
42026
+ {
42027
+ position: [
42028
+ -0.25 + 0.5 * (unit.health / unit.maxHealth) / 2,
42029
+ 0,
42030
+ 0.01
42031
+ ],
42032
+ children: [
42033
+ /* @__PURE__ */ jsxRuntime.jsx("planeGeometry", { args: [0.5 * (unit.health / unit.maxHealth), 0.05] }),
42034
+ /* @__PURE__ */ jsxRuntime.jsx(
42035
+ "meshBasicMaterial",
42036
+ {
42037
+ color: unit.health / unit.maxHealth > 0.5 ? 4500036 : unit.health / unit.maxHealth > 0.25 ? 11184708 : 16729156
42038
+ }
42039
+ )
42040
+ ]
42041
+ }
42042
+ )
42043
+ ] })
42044
+ ]
42045
+ }
42046
+ );
42047
+ },
42048
+ [selectedUnitId, handleUnitClick]
42049
+ );
42050
+ const DefaultFeatureRenderer = React82.useCallback(
42051
+ ({
42052
+ feature,
42053
+ position
42054
+ }) => {
42055
+ if (feature.assetUrl) {
42056
+ return /* @__PURE__ */ jsxRuntime.jsx(
42057
+ ModelLoader,
42058
+ {
42059
+ url: feature.assetUrl,
42060
+ position,
42061
+ scale: 0.5,
42062
+ rotation: [0, feature.rotation ?? 0, 0],
42063
+ onClick: () => handleFeatureClick(feature, null),
42064
+ fallbackGeometry: "box"
42065
+ },
42066
+ feature.id
42067
+ );
42068
+ }
42069
+ if (feature.type === "tree") {
42070
+ return /* @__PURE__ */ jsxRuntime.jsxs(
42071
+ "group",
42072
+ {
42073
+ position,
42074
+ onClick: (e) => handleFeatureClick(feature, e),
42075
+ userData: { type: "feature", featureId: feature.id },
42076
+ children: [
42077
+ /* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, 0.4, 0], children: [
42078
+ /* @__PURE__ */ jsxRuntime.jsx("cylinderGeometry", { args: [0.1, 0.15, 0.8, 6] }),
42079
+ /* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color: 9127187 })
42080
+ ] }),
42081
+ /* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, 0.9, 0], children: [
42082
+ /* @__PURE__ */ jsxRuntime.jsx("coneGeometry", { args: [0.5, 0.8, 8] }),
42083
+ /* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color: 2263842 })
42084
+ ] })
42085
+ ]
42086
+ }
42087
+ );
42088
+ }
42089
+ if (feature.type === "rock") {
42090
+ return /* @__PURE__ */ jsxRuntime.jsxs(
42091
+ "mesh",
42092
+ {
42093
+ position: [position[0], position[1] + 0.3, position[2]],
42094
+ onClick: (e) => handleFeatureClick(feature, e),
42095
+ userData: { type: "feature", featureId: feature.id },
42096
+ children: [
42097
+ /* @__PURE__ */ jsxRuntime.jsx("dodecahedronGeometry", { args: [0.3, 0] }),
42098
+ /* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color: 8421504 })
42099
+ ]
42100
+ }
42101
+ );
42102
+ }
42103
+ return null;
42104
+ },
42105
+ [handleFeatureClick]
42106
+ );
42107
+ if (externalLoading || assetsLoading && preloadAssets.length > 0) {
42108
+ return /* @__PURE__ */ jsxRuntime.jsx(
42109
+ Canvas3DLoadingState,
42110
+ {
42111
+ progress,
42112
+ loaded,
42113
+ total,
42114
+ message: loadingMessage,
42115
+ className
42116
+ }
42117
+ );
42118
+ }
42119
+ const displayError = externalError || internalError;
42120
+ if (displayError) {
42121
+ return /* @__PURE__ */ jsxRuntime.jsx(Canvas3DErrorBoundary, { children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "game-canvas-3d game-canvas-3d--error", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "game-canvas-3d__error", children: [
42122
+ "Error: ",
42123
+ displayError
42124
+ ] }) }) });
42125
+ }
42126
+ return /* @__PURE__ */ jsxRuntime.jsx(
42127
+ Canvas3DErrorBoundary,
42128
+ {
42129
+ onError: (err) => setInternalError(err.message),
42130
+ onReset: () => setInternalError(null),
42131
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
42132
+ "div",
42133
+ {
42134
+ ref: containerRef,
42135
+ className: cn("game-canvas-3d relative w-full h-full min-h-[85vh] overflow-hidden", className),
42136
+ "data-orientation": orientation,
42137
+ "data-camera-mode": cameraMode,
42138
+ "data-overlay": overlay,
42139
+ children: [
42140
+ /* @__PURE__ */ jsxRuntime.jsxs(
42141
+ fiber.Canvas,
42142
+ {
42143
+ shadows,
42144
+ camera: {
42145
+ position: cameraConfig.position,
42146
+ fov: cameraConfig.fov,
42147
+ near: 0.1,
42148
+ far: 1e3
42149
+ },
42150
+ style: { background: backgroundColor },
42151
+ onClick: (e) => {
42152
+ if (e.target === e.currentTarget) {
42153
+ eventHandlers.handleCanvasClick(e);
42154
+ }
42155
+ },
42156
+ children: [
42157
+ /* @__PURE__ */ jsxRuntime.jsx(CameraController, { onCameraChange: eventHandlers.handleCameraChange }),
42158
+ /* @__PURE__ */ jsxRuntime.jsx("ambientLight", { intensity: 0.6 }),
42159
+ /* @__PURE__ */ jsxRuntime.jsx(
42160
+ "directionalLight",
42161
+ {
42162
+ position: [10, 20, 10],
42163
+ intensity: 0.8,
42164
+ castShadow: shadows,
42165
+ "shadow-mapSize": [2048, 2048]
42166
+ }
42167
+ ),
42168
+ /* @__PURE__ */ jsxRuntime.jsx("hemisphereLight", { intensity: 0.3, color: "#87ceeb", groundColor: "#362d1d" }),
42169
+ showGrid && /* @__PURE__ */ jsxRuntime.jsx(
42170
+ drei.Grid,
42171
+ {
42172
+ args: [
42173
+ Math.max(gridBounds.maxX - gridBounds.minX + 2, 10),
42174
+ Math.max(gridBounds.maxZ - gridBounds.minZ + 2, 10)
42175
+ ],
42176
+ position: [
42177
+ (gridBounds.maxX - gridBounds.minX) / 2 - 0.5,
42178
+ 0,
42179
+ (gridBounds.maxZ - gridBounds.minZ) / 2 - 0.5
42180
+ ],
42181
+ cellSize: 1,
42182
+ cellThickness: 1,
42183
+ cellColor: "#444444",
42184
+ sectionSize: 5,
42185
+ sectionThickness: 1.5,
42186
+ sectionColor: "#666666",
42187
+ fadeDistance: 50,
42188
+ fadeStrength: 1
42189
+ }
42190
+ ),
42191
+ tiles.map((tile, index) => {
42192
+ const position = gridToWorld(
42193
+ tile.x,
42194
+ tile.z ?? tile.y ?? 0,
42195
+ tile.elevation ?? 0
42196
+ );
42197
+ const Renderer = CustomTileRenderer || DefaultTileRenderer;
42198
+ return /* @__PURE__ */ jsxRuntime.jsx(Renderer, { tile, position }, tile.id ?? `tile-${index}`);
42199
+ }),
42200
+ features.map((feature, index) => {
42201
+ const position = gridToWorld(
42202
+ feature.x,
42203
+ feature.z ?? feature.y ?? 0,
42204
+ (feature.elevation ?? 0) + 0.5
42205
+ );
42206
+ const Renderer = CustomFeatureRenderer || DefaultFeatureRenderer;
42207
+ return /* @__PURE__ */ jsxRuntime.jsx(Renderer, { feature, position }, feature.id ?? `feature-${index}`);
42208
+ }),
42209
+ units.map((unit) => {
42210
+ const position = gridToWorld(
42211
+ unit.x ?? 0,
42212
+ unit.z ?? unit.y ?? 0,
42213
+ (unit.elevation ?? 0) + 0.5
42214
+ );
42215
+ const Renderer = CustomUnitRenderer || DefaultUnitRenderer;
42216
+ return /* @__PURE__ */ jsxRuntime.jsx(Renderer, { unit, position }, unit.id);
42217
+ }),
42218
+ children,
42219
+ /* @__PURE__ */ jsxRuntime.jsx(
42220
+ drei.OrbitControls,
42221
+ {
42222
+ ref: controlsRef,
42223
+ target: cameraTarget,
42224
+ enableDamping: true,
42225
+ dampingFactor: 0.05,
42226
+ minDistance: 2,
42227
+ maxDistance: 100,
42228
+ maxPolarAngle: Math.PI / 2 - 0.1
42229
+ }
42230
+ )
42231
+ ]
42232
+ }
42233
+ ),
42234
+ showCoordinates && hoveredTile && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "game-canvas-3d__coordinates", children: [
42235
+ "X: ",
42236
+ hoveredTile.x,
42237
+ ", Z: ",
42238
+ hoveredTile.z ?? hoveredTile.y ?? 0
42239
+ ] }),
42240
+ showTileInfo && hoveredTile && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "game-canvas-3d__tile-info", children: [
42241
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "tile-info__type", children: hoveredTile.type }),
42242
+ hoveredTile.terrain && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "tile-info__terrain", children: hoveredTile.terrain })
42243
+ ] })
42244
+ ]
42245
+ }
42246
+ )
42247
+ }
42248
+ );
42249
+ }
42250
+ );
42251
+ GameCanvas3D.displayName = "GameCanvas3D";
42252
+ }
42253
+ });
42254
+ function GameBoard3D({
42255
+ entity,
42256
+ tiles = [],
42257
+ features = [],
42258
+ cameraMode = "perspective",
42259
+ backgroundColor = "#2a1a1a",
42260
+ tileClickEvent,
42261
+ unitClickEvent,
42262
+ attackEvent,
42263
+ endTurnEvent,
42264
+ cancelEvent,
42265
+ playAgainEvent,
42266
+ gameEndEvent,
42267
+ className
42268
+ }) {
42269
+ const row = boardEntity(entity);
42270
+ const eventBus = useEventBus();
42271
+ const entityUnits = row ? rows(row.units) : [];
42272
+ const units = entityUnits;
42273
+ const selectedUnitId = row ? str(row.selectedUnitId) || null : null;
42274
+ const phase = row ? str(row.phase) : "observation";
42275
+ const result = row ? str(row.result) : "none";
42276
+ const validMoves = row && Array.isArray(row.validMoves) ? row.validMoves : [];
42277
+ const attackTargets = row && Array.isArray(row.attackTargets) ? row.attackTargets : [];
42278
+ const turn = row ? num(row.turn) : 0;
42279
+ const currentTeam = row ? str(row.currentTeam) : "player";
42280
+ const isGameOver = result !== "none";
42281
+ const gameEndEmittedRef = React82.useRef(false);
42282
+ React82.useEffect(() => {
42283
+ if ((result === "victory" || result === "defeat") && gameEndEvent) {
42284
+ if (!gameEndEmittedRef.current) {
42285
+ gameEndEmittedRef.current = true;
42286
+ eventBus.emit(`UI:${gameEndEvent}`, { result });
42287
+ }
42288
+ } else {
42289
+ gameEndEmittedRef.current = false;
42290
+ }
42291
+ }, [result, gameEndEvent, eventBus]);
42292
+ const checkGameEnd = React82.useCallback(() => {
42293
+ const alivePlayer = entityUnits.filter((u) => unitTeam(u) === "player" && unitHealth(u) > 0);
42294
+ const aliveEnemy = entityUnits.filter((u) => unitTeam(u) === "enemy" && unitHealth(u) > 0);
42295
+ if (alivePlayer.length === 0 && gameEndEvent) {
42296
+ eventBus.emit(`UI:${gameEndEvent}`, { result: "defeat" });
42297
+ } else if (aliveEnemy.length === 0 && gameEndEvent) {
42298
+ eventBus.emit(`UI:${gameEndEvent}`, { result: "victory" });
42299
+ }
42300
+ }, [entityUnits, gameEndEvent, eventBus]);
42301
+ const handleUnitClickCallback = React82.useCallback((isoUnit) => {
42302
+ if (phase !== "action" || !selectedUnitId || !attackEvent) return;
42303
+ const attackerRow = entityUnits.find((u) => str(u.id) === selectedUnitId);
42304
+ const targetRow = entityUnits.find((u) => str(u.id) === isoUnit.id);
42305
+ if (!attackerRow || !targetRow) return;
42306
+ if (unitTeam(targetRow) !== "enemy" || unitHealth(targetRow) <= 0) return;
42307
+ const ap = unitPosition(attackerRow);
42308
+ const tp = unitPosition(targetRow);
42309
+ const dx = Math.abs(ap.x - tp.x);
42310
+ const dy = Math.abs(ap.y - tp.y);
42311
+ if (dx <= 1 && dy <= 1 && dx + dy > 0) {
42312
+ const damage = Math.max(1, num(attackerRow.attack) - num(targetRow.defense));
42313
+ eventBus.emit(`UI:${attackEvent}`, {
42314
+ attackerId: str(attackerRow.id),
42315
+ targetId: str(targetRow.id),
42316
+ damage
42317
+ });
42318
+ setTimeout(checkGameEnd, 100);
42319
+ }
42320
+ }, [phase, selectedUnitId, entityUnits, attackEvent, eventBus, checkGameEnd]);
42321
+ const handleEndTurn = React82.useCallback(() => {
42322
+ if (endTurnEvent) eventBus.emit(`UI:${endTurnEvent}`, {});
42323
+ }, [endTurnEvent, eventBus]);
42324
+ const handleCancel = React82.useCallback(() => {
42325
+ if (cancelEvent) eventBus.emit(`UI:${cancelEvent}`, {});
42326
+ }, [cancelEvent, eventBus]);
42327
+ const handlePlayAgain = React82.useCallback(() => {
42328
+ if (playAgainEvent) eventBus.emit(`UI:${playAgainEvent}`, {});
42329
+ }, [playAgainEvent, eventBus]);
42330
+ return /* @__PURE__ */ jsxRuntime.jsxs(VStack, { className: cn("game-board-3d block w-full min-h-[85vh] relative", className), children: [
42331
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "game-board-3d__status absolute top-3 left-3 z-10 flex gap-2 items-center", children: [
42332
+ /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "small", className: "status__phase capitalize", children: phase.replace("_", " ") }),
42333
+ currentTeam && !isGameOver && /* @__PURE__ */ jsxRuntime.jsxs(Typography, { variant: "small", color: "muted", className: "status__team", children: [
42334
+ "\u2014 ",
42335
+ currentTeam === "player" ? "Your Turn" : "Enemy's Turn"
42336
+ ] }),
42337
+ /* @__PURE__ */ jsxRuntime.jsxs(Typography, { variant: "small", color: "muted", className: "status__turn", children: [
42338
+ "Turn ",
42339
+ turn
42340
+ ] })
42341
+ ] }),
42342
+ /* @__PURE__ */ jsxRuntime.jsx(
42343
+ GameCanvas3D,
42344
+ {
42345
+ tiles,
42346
+ units,
42347
+ features,
42348
+ cameraMode,
42349
+ showGrid: true,
42350
+ showCoordinates: false,
42351
+ showTileInfo: false,
42352
+ shadows: true,
42353
+ backgroundColor,
42354
+ tileClickEvent,
42355
+ unitClickEvent,
42356
+ onUnitClick: handleUnitClickCallback,
42357
+ selectedUnitId,
42358
+ validMoves,
42359
+ attackTargets,
42360
+ className: "game-board-3d__canvas w-full min-h-[85vh]"
42361
+ }
42362
+ ),
42363
+ !isGameOver && /* @__PURE__ */ jsxRuntime.jsxs(HStack, { className: "fixed bottom-6 right-6 z-50", gap: "sm", children: [
42364
+ (phase === "selection" || phase === "movement" || phase === "action") && /* @__PURE__ */ jsxRuntime.jsx(
42365
+ Button,
42366
+ {
42367
+ variant: "secondary",
42368
+ className: "shadow-xl",
42369
+ onClick: handleCancel,
42370
+ children: "Cancel"
42371
+ }
42372
+ ),
42373
+ phase !== "enemy_turn" && /* @__PURE__ */ jsxRuntime.jsx(
42374
+ Button,
42375
+ {
42376
+ variant: "primary",
42377
+ className: "shadow-xl",
42378
+ onClick: handleEndTurn,
42379
+ children: "End Turn"
42380
+ }
42381
+ )
42382
+ ] }),
42383
+ isGameOver && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "game-board-3d__overlay absolute inset-0 z-20 flex items-center justify-center bg-black/60", children: /* @__PURE__ */ jsxRuntime.jsxs(VStack, { align: "center", gap: "md", className: "overlay__card p-8 rounded-xl bg-gray-900/90 shadow-2xl", children: [
42384
+ /* @__PURE__ */ jsxRuntime.jsx(
42385
+ Typography,
42386
+ {
42387
+ variant: "h2",
42388
+ className: cn(
42389
+ "overlay__result",
42390
+ result === "victory" ? "text-yellow-400" : "text-red-500"
42391
+ ),
42392
+ children: result === "victory" ? "Victory!" : "Defeat"
42393
+ }
42394
+ ),
42395
+ /* @__PURE__ */ jsxRuntime.jsxs(Typography, { variant: "body", color: "muted", className: "overlay__turn-count", children: [
42396
+ "Completed in ",
42397
+ turn,
42398
+ " turn",
42399
+ turn !== 1 ? "s" : ""
42400
+ ] }),
42401
+ /* @__PURE__ */ jsxRuntime.jsx(
42402
+ Button,
42403
+ {
42404
+ variant: "primary",
42405
+ className: "px-8 py-3 font-semibold",
42406
+ onClick: handlePlayAgain,
42407
+ children: "Play Again"
42408
+ }
42409
+ )
42410
+ ] }) })
42411
+ ] });
42412
+ }
42413
+ var init_GameBoard3D = __esm({
42414
+ "components/game/organisms/GameBoard3D.tsx"() {
42415
+ "use client";
42416
+ init_cn();
42417
+ init_Button();
42418
+ init_Typography();
42419
+ init_Stack();
42420
+ init_useEventBus();
42421
+ init_GameCanvas3D2();
42422
+ init_boardEntity();
42423
+ GameBoard3D.displayName = "GameBoard3D";
42424
+ }
42425
+ });
40722
42426
  var GameShell;
40723
42427
  var init_GameShell = __esm({
40724
42428
  "components/game/templates/GameShell.tsx"() {
@@ -42087,21 +43791,6 @@ var init_ModalSlot = __esm({
42087
43791
  ModalSlot.displayName = "ModalSlot";
42088
43792
  }
42089
43793
  });
42090
- function getOpponentAction(strategy, actions, history) {
42091
- const actionIds = actions.map((a) => a.id);
42092
- switch (strategy) {
42093
- case "always-cooperate":
42094
- return actionIds[0];
42095
- case "always-defect":
42096
- return actionIds[actionIds.length - 1];
42097
- case "tit-for-tat":
42098
- if (history.length === 0) return actionIds[0];
42099
- return history[history.length - 1].playerAction;
42100
- case "random":
42101
- default:
42102
- return actionIds[Math.floor(Math.random() * actionIds.length)];
42103
- }
42104
- }
42105
43794
  function NegotiatorBoard({
42106
43795
  entity,
42107
43796
  completeEvent = "PUZZLE_COMPLETE",
@@ -42123,28 +43812,35 @@ function NegotiatorBoard({
42123
43812
  const playerTotal = num(resolved?.score);
42124
43813
  const isComplete = result !== "none" || totalRounds > 0 && currentRound >= totalRounds;
42125
43814
  const won = result === "win";
42126
- const opponentTotal = history.reduce((s, r) => s + r.opponentPayoff, 0);
43815
+ const lastPlayerAction = str(resolved?.lastPlayerAction);
43816
+ const lastOpponentAction = str(resolved?.lastOpponentAction);
43817
+ const lastPayoff = num(resolved?.lastPayoff);
42127
43818
  const actions = Array.isArray(resolved?.actions) ? resolved.actions : [];
42128
43819
  const payoffMatrix = Array.isArray(resolved?.payoffMatrix) ? resolved.payoffMatrix : [];
43820
+ const prevRoundRef = React82.useRef(currentRound);
43821
+ React82.useEffect(() => {
43822
+ if (currentRound > prevRoundRef.current && lastPlayerAction) {
43823
+ const opponentPayoffEntry = payoffMatrix.find(
43824
+ (p2) => p2.playerAction === lastPlayerAction && p2.opponentAction === lastOpponentAction
43825
+ );
43826
+ setHistory((prev) => [
43827
+ ...prev,
43828
+ {
43829
+ round: currentRound,
43830
+ playerAction: lastPlayerAction,
43831
+ opponentAction: lastOpponentAction,
43832
+ playerPayoff: lastPayoff,
43833
+ opponentPayoff: opponentPayoffEntry?.opponentPayoff ?? 0
43834
+ }
43835
+ ]);
43836
+ }
43837
+ prevRoundRef.current = currentRound;
43838
+ }, [currentRound, lastPlayerAction, lastOpponentAction, lastPayoff, payoffMatrix]);
43839
+ const opponentTotal = React82.useMemo(() => history.reduce((s, r) => s + r.opponentPayoff, 0), [history]);
42129
43840
  const handleAction = React82.useCallback((actionId) => {
42130
43841
  if (isComplete) return;
42131
- const opponentAction = getOpponentAction(str(resolved?.opponentStrategy) || "random", actions, history);
42132
- const payoff = payoffMatrix.find(
42133
- (p2) => p2.playerAction === actionId && p2.opponentAction === opponentAction
42134
- );
42135
- const playerPayoff = payoff?.playerPayoff ?? 0;
42136
- setHistory((prev) => [
42137
- ...prev,
42138
- {
42139
- round: prev.length + 1,
42140
- playerAction: actionId,
42141
- opponentAction,
42142
- playerPayoff,
42143
- opponentPayoff: payoff?.opponentPayoff ?? 0
42144
- }
42145
- ]);
42146
43842
  if (playRoundEvent) {
42147
- emit(`UI:${playRoundEvent}`, { playerAction: actionId, payoff: playerPayoff });
43843
+ emit(`UI:${playRoundEvent}`, { playerAction: actionId, payoff: 0 });
42148
43844
  }
42149
43845
  if (totalRounds > 0 && currentRound + 1 >= totalRounds) {
42150
43846
  if (finishEvent) {
@@ -42154,10 +43850,11 @@ function NegotiatorBoard({
42154
43850
  setShowHint(true);
42155
43851
  }
42156
43852
  }
42157
- }, [isComplete, resolved, totalRounds, currentRound, actions, payoffMatrix, history, playRoundEvent, finishEvent, emit]);
43853
+ }, [isComplete, resolved, totalRounds, currentRound, playRoundEvent, finishEvent, emit]);
42158
43854
  const handleReset = React82.useCallback(() => {
42159
43855
  setHistory([]);
42160
43856
  setShowHint(false);
43857
+ prevRoundRef.current = 0;
42161
43858
  if (playAgainEvent) {
42162
43859
  emit(`UI:${playAgainEvent}`, {});
42163
43860
  }
@@ -42268,6 +43965,236 @@ var init_NegotiatorBoard = __esm({
42268
43965
  NegotiatorBoard.displayName = "NegotiatorBoard";
42269
43966
  }
42270
43967
  });
43968
+ function resolveEntity(entity) {
43969
+ if (!entity) return {};
43970
+ if (Array.isArray(entity)) return entity[0] ?? {};
43971
+ return entity;
43972
+ }
43973
+ function numField(row, key, fallback) {
43974
+ const v = row[key];
43975
+ return typeof v === "number" ? v : fallback;
43976
+ }
43977
+ function strField(row, key, fallback) {
43978
+ const v = row[key];
43979
+ return typeof v === "string" ? v : fallback;
43980
+ }
43981
+ function PlatformerBoard({
43982
+ entity,
43983
+ player: propPlayer,
43984
+ platforms: propPlatforms,
43985
+ result: propResult,
43986
+ lives: propLives,
43987
+ score: propScore,
43988
+ level: propLevel,
43989
+ worldWidth = 800,
43990
+ worldHeight = 400,
43991
+ canvasWidth = 800,
43992
+ canvasHeight = 400,
43993
+ playerSprite,
43994
+ tileSprites,
43995
+ bgColor,
43996
+ leftEvent = "LEFT",
43997
+ rightEvent = "RIGHT",
43998
+ jumpEvent = "JUMP",
43999
+ stopEvent = "STOP",
44000
+ playAgainEvent = "PLAY_AGAIN",
44001
+ gameEndEvent = "GAME_END",
44002
+ className
44003
+ }) {
44004
+ const row = resolveEntity(entity);
44005
+ const eventBus = useEventBus();
44006
+ const { t } = hooks.useTranslate();
44007
+ const result = propResult ?? strField(row, "result", "none");
44008
+ propLives ?? numField(row, "lives", 3);
44009
+ const score = propScore ?? numField(row, "score", 0);
44010
+ const level = propLevel ?? numField(row, "level", 1);
44011
+ const entityPlayer = row["player"];
44012
+ const player = propPlayer ?? entityPlayer ?? {
44013
+ x: 80,
44014
+ y: 320,
44015
+ width: 32,
44016
+ height: 48,
44017
+ vx: 0,
44018
+ vy: 0,
44019
+ grounded: false,
44020
+ facingRight: true
44021
+ };
44022
+ const entityPlatforms = Array.isArray(row["platforms"]) ? row["platforms"] : void 0;
44023
+ const platforms = propPlatforms ?? entityPlatforms ?? [
44024
+ { x: 0, y: 368, width: 800, height: 32, type: "ground" },
44025
+ { x: 150, y: 280, width: 160, height: 16, type: "platform" },
44026
+ { x: 420, y: 220, width: 160, height: 16, type: "platform" },
44027
+ { x: 580, y: 300, width: 80, height: 16, type: "hazard" },
44028
+ { x: 700, y: 340, width: 64, height: 28, type: "goal" }
44029
+ ];
44030
+ const handleRestart = React82.useCallback(() => {
44031
+ if (playAgainEvent) {
44032
+ eventBus.emit(`UI:${playAgainEvent}`, {});
44033
+ }
44034
+ }, [playAgainEvent, eventBus]);
44035
+ const isGameOver = result === "won" || result === "lost";
44036
+ const gameEndEmittedRef = React82.useRef(false);
44037
+ React82.useEffect(() => {
44038
+ if ((result === "won" || result === "lost") && gameEndEvent) {
44039
+ if (!gameEndEmittedRef.current) {
44040
+ gameEndEmittedRef.current = true;
44041
+ eventBus.emit(`UI:${gameEndEvent}`, { result });
44042
+ }
44043
+ } else {
44044
+ gameEndEmittedRef.current = false;
44045
+ }
44046
+ }, [result, gameEndEvent, eventBus]);
44047
+ return /* @__PURE__ */ jsxRuntime.jsx(
44048
+ VStack,
44049
+ {
44050
+ className: cn("platformer-board relative bg-background", className),
44051
+ gap: "none",
44052
+ children: /* @__PURE__ */ jsxRuntime.jsxs(Box, { className: "relative", children: [
44053
+ /* @__PURE__ */ jsxRuntime.jsx(
44054
+ PlatformerCanvas,
44055
+ {
44056
+ player,
44057
+ platforms,
44058
+ worldWidth,
44059
+ worldHeight,
44060
+ canvasWidth,
44061
+ canvasHeight,
44062
+ followCamera: true,
44063
+ bgColor,
44064
+ playerSprite,
44065
+ tileSprites,
44066
+ leftEvent,
44067
+ rightEvent,
44068
+ jumpEvent,
44069
+ stopEvent
44070
+ }
44071
+ ),
44072
+ isGameOver && /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "absolute inset-0 z-50 flex items-center justify-center bg-background/70 backdrop-blur-sm rounded-container", children: /* @__PURE__ */ jsxRuntime.jsxs(VStack, { className: "text-center p-8", gap: "lg", children: [
44073
+ /* @__PURE__ */ jsxRuntime.jsx(
44074
+ Typography,
44075
+ {
44076
+ variant: "h2",
44077
+ className: cn(
44078
+ "text-4xl font-black tracking-widest uppercase",
44079
+ result === "won" ? "text-warning" : "text-error"
44080
+ ),
44081
+ children: result === "won" ? t("platformer.won") : t("platformer.lost")
44082
+ }
44083
+ ),
44084
+ result === "won" && /* @__PURE__ */ jsxRuntime.jsxs(Typography, { variant: "body1", className: "text-muted-foreground", children: [
44085
+ t("platformer.level"),
44086
+ " ",
44087
+ level,
44088
+ " \u2014 ",
44089
+ t("platformer.score"),
44090
+ ": ",
44091
+ score
44092
+ ] }),
44093
+ result === "lost" && /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "body1", className: "text-muted-foreground", children: t("platformer.noLives") }),
44094
+ /* @__PURE__ */ jsxRuntime.jsx(
44095
+ Button,
44096
+ {
44097
+ variant: "primary",
44098
+ className: "px-8 py-3 font-semibold",
44099
+ onClick: handleRestart,
44100
+ children: result === "won" ? t("platformer.nextLevel") : t("platformer.playAgain")
44101
+ }
44102
+ )
44103
+ ] }) })
44104
+ ] })
44105
+ }
44106
+ );
44107
+ }
44108
+ var init_PlatformerBoard = __esm({
44109
+ "components/game/organisms/PlatformerBoard.tsx"() {
44110
+ "use client";
44111
+ init_cn();
44112
+ init_useEventBus();
44113
+ init_Box();
44114
+ init_Button();
44115
+ init_Typography();
44116
+ init_Stack();
44117
+ init_PlatformerCanvas();
44118
+ PlatformerBoard.displayName = "PlatformerBoard";
44119
+ }
44120
+ });
44121
+ function PlatformerTemplate({
44122
+ entity,
44123
+ platforms = DEFAULT_PLATFORMS,
44124
+ worldWidth = 800,
44125
+ worldHeight = 400,
44126
+ canvasWidth = 800,
44127
+ canvasHeight = 400,
44128
+ playerSprite = `${CDN3}/characters/platformChar_idle.png`,
44129
+ tileSprites = DEFAULT_TILE_SPRITES,
44130
+ bgColor = "#5c94fc",
44131
+ gameEndEvent = "GAME_END",
44132
+ className
44133
+ }) {
44134
+ const row = entity && !Array.isArray(entity) ? entity : void 0;
44135
+ const lives = typeof row?.["lives"] === "number" ? row["lives"] : 3;
44136
+ const score = typeof row?.["score"] === "number" ? row["score"] : 0;
44137
+ const level = typeof row?.["level"] === "number" ? row["level"] : 1;
44138
+ return /* @__PURE__ */ jsxRuntime.jsxs(VStack, { className: cn("platformer-template w-full", className), gap: "none", children: [
44139
+ /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "relative", children: /* @__PURE__ */ jsxRuntime.jsx(
44140
+ GameHud,
44141
+ {
44142
+ position: "top",
44143
+ stats: [
44144
+ { label: "Lives", value: lives, format: "number", variant: "danger" },
44145
+ { label: "Level", value: level, format: "number", variant: "default" }
44146
+ ]
44147
+ }
44148
+ ) }),
44149
+ /* @__PURE__ */ jsxRuntime.jsx(
44150
+ PlatformerBoard,
44151
+ {
44152
+ entity,
44153
+ platforms,
44154
+ worldWidth,
44155
+ worldHeight,
44156
+ canvasWidth,
44157
+ canvasHeight,
44158
+ playerSprite,
44159
+ tileSprites,
44160
+ bgColor,
44161
+ leftEvent: "LEFT",
44162
+ rightEvent: "RIGHT",
44163
+ jumpEvent: "JUMP",
44164
+ stopEvent: "STOP",
44165
+ playAgainEvent: "PLAY_AGAIN",
44166
+ gameEndEvent
44167
+ }
44168
+ ),
44169
+ /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "px-4 py-2", children: /* @__PURE__ */ jsxRuntime.jsx(ScoreBoard, { score, level }) })
44170
+ ] });
44171
+ }
44172
+ var CDN3, DEFAULT_TILE_SPRITES, DEFAULT_PLATFORMS;
44173
+ var init_PlatformerTemplate = __esm({
44174
+ "components/game/templates/PlatformerTemplate.tsx"() {
44175
+ init_cn();
44176
+ init_Box();
44177
+ init_Stack();
44178
+ init_PlatformerBoard();
44179
+ init_GameHud();
44180
+ init_ScoreBoard();
44181
+ CDN3 = "https://almadar-kflow-assets.web.app/shared/platformer";
44182
+ DEFAULT_TILE_SPRITES = {
44183
+ ground: `${CDN3}/tiles/platformPack_tile001.png`,
44184
+ platform: `${CDN3}/tiles/platformPack_tile004.png`,
44185
+ hazard: `${CDN3}/tiles/platformPack_tile017.png`,
44186
+ goal: `${CDN3}/tiles/platformPack_tile007.png`
44187
+ };
44188
+ DEFAULT_PLATFORMS = [
44189
+ { x: 0, y: 368, width: 800, height: 32, type: "ground" },
44190
+ { x: 150, y: 280, width: 160, height: 16, type: "platform" },
44191
+ { x: 420, y: 220, width: 160, height: 16, type: "platform" },
44192
+ { x: 580, y: 300, width: 80, height: 16, type: "hazard" },
44193
+ { x: 700, y: 340, width: 64, height: 28, type: "goal" }
44194
+ ];
44195
+ PlatformerTemplate.displayName = "PlatformerTemplate";
44196
+ }
44197
+ });
42271
44198
  var PricingOrganism;
42272
44199
  var init_PricingOrganism = __esm({
42273
44200
  "components/marketing/organisms/PricingOrganism.tsx"() {
@@ -42390,6 +44317,356 @@ var init_PricingPageTemplate = __esm({
42390
44317
  PricingPageTemplate.displayName = "PricingPageTemplate";
42391
44318
  }
42392
44319
  });
44320
+ function RoguelikeBoard({
44321
+ entity,
44322
+ tiles: propTiles,
44323
+ enemies: propEnemies,
44324
+ items: propItems,
44325
+ player: propPlayer,
44326
+ playerHp: propPlayerHp,
44327
+ playerMaxHp: propPlayerMaxHp,
44328
+ playerAttack: propPlayerAttack,
44329
+ depth: propDepth,
44330
+ result: propResult,
44331
+ phase: propPhase,
44332
+ stairsX: propStairsX,
44333
+ stairsY: propStairsY,
44334
+ assetManifest: propAssetManifest,
44335
+ scale = 0.45,
44336
+ unitScale = 1,
44337
+ moveEvent,
44338
+ playAgainEvent,
44339
+ gameEndEvent,
44340
+ className
44341
+ }) {
44342
+ const board = boardEntity(entity) ?? {};
44343
+ const tiles = propTiles ?? (Array.isArray(board.tiles) ? board.tiles : DEFAULT_TILES2);
44344
+ const enemies = propEnemies ?? (Array.isArray(board.enemies) ? board.enemies : DEFAULT_ENEMIES);
44345
+ const items = propItems ?? (Array.isArray(board.items) ? board.items : DEFAULT_ITEMS);
44346
+ const player = propPlayer ?? board.player ?? { x: 1, y: 1 };
44347
+ const playerHp = propPlayerHp ?? num(board.playerHp, 10);
44348
+ const playerMaxHp = propPlayerMaxHp ?? num(board.playerMaxHp, 10);
44349
+ const playerAttack = propPlayerAttack ?? num(board.playerAttack, 3);
44350
+ const depth = propDepth ?? num(board.depth, 1);
44351
+ const result = propResult ?? (str(board.result) || "none");
44352
+ const phase = propPhase ?? (str(board.phase) || "player_turn");
44353
+ propStairsX ?? num(board.stairsX, 3);
44354
+ propStairsY ?? num(board.stairsY, 3);
44355
+ const eventBus = useEventBus();
44356
+ const { t } = hooks.useTranslate();
44357
+ const [hoveredTile, setHoveredTile] = React82.useState(null);
44358
+ const emitMove = React82.useCallback((dx, dy) => {
44359
+ if (!moveEvent) return;
44360
+ eventBus.emit(`UI:${moveEvent}`, { dx, dy });
44361
+ }, [moveEvent, eventBus]);
44362
+ React82.useEffect(() => {
44363
+ const handler = (e) => {
44364
+ if (result !== "none" || phase !== "player_turn") return;
44365
+ if (e.key === "ArrowUp" || e.key === "w") emitMove(0, -1);
44366
+ if (e.key === "ArrowDown" || e.key === "s") emitMove(0, 1);
44367
+ if (e.key === "ArrowLeft" || e.key === "a") emitMove(-1, 0);
44368
+ if (e.key === "ArrowRight" || e.key === "d") emitMove(1, 0);
44369
+ };
44370
+ window.addEventListener("keydown", handler);
44371
+ return () => window.removeEventListener("keydown", handler);
44372
+ }, [result, phase, emitMove]);
44373
+ React82.useEffect(() => {
44374
+ if ((result === "won" || result === "lost") && gameEndEvent) {
44375
+ eventBus.emit(`UI:${gameEndEvent}`, { result });
44376
+ }
44377
+ }, [result, gameEndEvent, eventBus]);
44378
+ const handleTileClick = React82.useCallback((x, y) => {
44379
+ if (result !== "none" || phase !== "player_turn") return;
44380
+ const dx = x - player.x;
44381
+ const dy = y - player.y;
44382
+ if (Math.abs(dx) + Math.abs(dy) === 1) {
44383
+ emitMove(dx, dy);
44384
+ }
44385
+ }, [result, phase, player, emitMove]);
44386
+ const handleReset = React82.useCallback(() => {
44387
+ if (!playAgainEvent) return;
44388
+ eventBus.emit(`UI:${playAgainEvent}`, {});
44389
+ }, [playAgainEvent, eventBus]);
44390
+ const isoUnits = React82.useMemo(() => {
44391
+ const liveEnemies = enemies.filter((e) => e.hp > 0);
44392
+ return [
44393
+ {
44394
+ id: "player",
44395
+ position: player,
44396
+ name: t("roguelike.player") || "Hero",
44397
+ team: "player",
44398
+ health: playerHp,
44399
+ maxHealth: playerMaxHp,
44400
+ unitType: "player",
44401
+ sprite: `${CDN4}/isometric-dungeon/Characters/Male/Male_0_Idle0.png`
44402
+ },
44403
+ ...liveEnemies.map((e) => ({
44404
+ id: e.id,
44405
+ position: { x: e.x, y: e.y },
44406
+ name: t("roguelike.enemy") || "Enemy",
44407
+ team: "enemy",
44408
+ health: e.hp,
44409
+ maxHealth: 6,
44410
+ unitType: "enemy",
44411
+ sprite: `${CDN4}/sprite-sheets/shadow-legion-sprite-sheet-sw.png`
44412
+ }))
44413
+ ];
44414
+ }, [player, enemies, playerHp, playerMaxHp, t]);
44415
+ const isoFeatures = React82.useMemo(
44416
+ () => items.map((it) => ({
44417
+ id: it.id,
44418
+ x: it.x,
44419
+ y: it.y,
44420
+ type: it.kind,
44421
+ sprite: FEATURE_SPRITE[it.kind] ?? FEATURE_SPRITE.health_potion
44422
+ })),
44423
+ [items]
44424
+ );
44425
+ const validMoves = React82.useMemo(() => {
44426
+ if (result !== "none" || phase !== "player_turn") return [];
44427
+ const dirs = [{ dx: 0, dy: -1 }, { dx: 0, dy: 1 }, { dx: -1, dy: 0 }, { dx: 1, dy: 0 }];
44428
+ return dirs.map(({ dx, dy }) => ({ x: player.x + dx, y: player.y + dy })).filter(({ x, y }) => {
44429
+ const tile = tiles.find((t2) => t2.x === x && t2.y === y);
44430
+ return tile?.passable ?? false;
44431
+ });
44432
+ }, [result, phase, player, tiles]);
44433
+ const isGameOver = result === "won" || result === "lost";
44434
+ return /* @__PURE__ */ jsxRuntime.jsxs(VStack, { className: cn("roguelike-board relative min-h-[600px] bg-background", className), gap: "none", children: [
44435
+ /* @__PURE__ */ jsxRuntime.jsxs(HStack, { className: "items-center justify-between px-4 py-2 border-b border-border", gap: "md", children: [
44436
+ /* @__PURE__ */ jsxRuntime.jsxs(HStack, { gap: "sm", className: "items-center", children: [
44437
+ /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "body2", className: "text-muted-foreground", children: t("roguelike.depth") || "Depth" }),
44438
+ /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "body1", className: "font-bold tabular-nums", children: depth })
44439
+ ] }),
44440
+ /* @__PURE__ */ jsxRuntime.jsxs(HStack, { gap: "sm", className: "items-center", children: [
44441
+ /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "body2", className: "text-muted-foreground", children: t("roguelike.hp") || "HP" }),
44442
+ /* @__PURE__ */ jsxRuntime.jsxs(
44443
+ Typography,
44444
+ {
44445
+ variant: "body1",
44446
+ className: cn(
44447
+ "font-bold tabular-nums",
44448
+ playerHp <= Math.floor(playerMaxHp * 0.25) ? "text-error" : "text-success"
44449
+ ),
44450
+ children: [
44451
+ playerHp,
44452
+ " / ",
44453
+ playerMaxHp
44454
+ ]
44455
+ }
44456
+ )
44457
+ ] }),
44458
+ /* @__PURE__ */ jsxRuntime.jsxs(HStack, { gap: "sm", className: "items-center", children: [
44459
+ /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "body2", className: "text-muted-foreground", children: t("roguelike.atk") || "ATK" }),
44460
+ /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "body1", className: "font-bold tabular-nums text-foreground", children: playerAttack })
44461
+ ] }),
44462
+ /* @__PURE__ */ jsxRuntime.jsxs(HStack, { gap: "sm", className: "items-center", children: [
44463
+ /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "body2", className: "text-muted-foreground", children: t("roguelike.phase") || "Phase" }),
44464
+ /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "body2", className: "capitalize text-foreground", children: phase.replace("_", " ") })
44465
+ ] })
44466
+ ] }),
44467
+ /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "relative flex-1", children: /* @__PURE__ */ jsxRuntime.jsx(
44468
+ IsometricCanvas_default,
44469
+ {
44470
+ tiles,
44471
+ units: isoUnits,
44472
+ features: isoFeatures,
44473
+ selectedUnitId: null,
44474
+ validMoves,
44475
+ attackTargets: [],
44476
+ hoveredTile,
44477
+ onTileClick: handleTileClick,
44478
+ onUnitClick: () => void 0,
44479
+ onTileHover: (x, y) => setHoveredTile({ x, y }),
44480
+ onTileLeave: () => setHoveredTile(null),
44481
+ scale,
44482
+ assetBaseUrl: propAssetManifest?.baseUrl ?? CDN4,
44483
+ assetManifest: propAssetManifest,
44484
+ unitScale
44485
+ }
44486
+ ) }),
44487
+ !isGameOver && /* @__PURE__ */ jsxRuntime.jsx(HStack, { className: "justify-center gap-2 p-3", gap: "none", children: [
44488
+ { label: "\u2191", dx: 0, dy: -1 },
44489
+ { label: "\u2193", dx: 0, dy: 1 },
44490
+ { label: "\u2190", dx: -1, dy: 0 },
44491
+ { label: "\u2192", dx: 1, dy: 0 }
44492
+ ].map(({ label, dx, dy }) => /* @__PURE__ */ jsxRuntime.jsx(
44493
+ Button,
44494
+ {
44495
+ variant: "secondary",
44496
+ className: "w-9 h-9 p-0 text-base font-mono",
44497
+ onClick: () => emitMove(dx, dy),
44498
+ disabled: phase !== "player_turn",
44499
+ children: label
44500
+ },
44501
+ label
44502
+ )) }),
44503
+ isGameOver && /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "absolute inset-0 z-50 flex items-center justify-center bg-background/75 backdrop-blur-sm rounded-container", children: /* @__PURE__ */ jsxRuntime.jsxs(VStack, { className: "text-center p-8", gap: "lg", children: [
44504
+ /* @__PURE__ */ jsxRuntime.jsx(
44505
+ Typography,
44506
+ {
44507
+ variant: "h2",
44508
+ className: cn(
44509
+ "text-4xl font-black tracking-widest uppercase",
44510
+ result === "won" ? "text-warning" : "text-error"
44511
+ ),
44512
+ children: result === "won" ? t("roguelike.victory") || "Victory!" : t("roguelike.defeat") || "Defeated!"
44513
+ }
44514
+ ),
44515
+ result === "won" && /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "body1", className: "text-muted-foreground", children: t("roguelike.cleared") || `Dungeon cleared! Depth reached: ${depth}` }),
44516
+ /* @__PURE__ */ jsxRuntime.jsx(
44517
+ Button,
44518
+ {
44519
+ variant: "primary",
44520
+ className: "px-8 py-3 font-semibold",
44521
+ onClick: handleReset,
44522
+ children: t("roguelike.playAgain") || "Play Again"
44523
+ }
44524
+ )
44525
+ ] }) })
44526
+ ] });
44527
+ }
44528
+ var CDN4, DEFAULT_TILES2, DEFAULT_ENEMIES, DEFAULT_ITEMS, FEATURE_SPRITE;
44529
+ var init_RoguelikeBoard = __esm({
44530
+ "components/game/organisms/RoguelikeBoard.tsx"() {
44531
+ "use client";
44532
+ init_cn();
44533
+ init_useEventBus();
44534
+ init_Box();
44535
+ init_Button();
44536
+ init_Typography();
44537
+ init_Stack();
44538
+ init_IsometricCanvas();
44539
+ init_boardEntity();
44540
+ CDN4 = "https://almadar-kflow-assets.web.app/shared";
44541
+ DEFAULT_TILES2 = [
44542
+ { x: 0, y: 0, terrain: "wall", passable: false, terrainSprite: `${CDN4}/isometric-dungeon/Isometric/stoneInset_E.png` },
44543
+ { x: 1, y: 0, terrain: "wall", passable: false, terrainSprite: `${CDN4}/isometric-dungeon/Isometric/stoneInset_E.png` },
44544
+ { x: 2, y: 0, terrain: "wall", passable: false, terrainSprite: `${CDN4}/isometric-dungeon/Isometric/stoneInset_E.png` },
44545
+ { x: 3, y: 0, terrain: "wall", passable: false, terrainSprite: `${CDN4}/isometric-dungeon/Isometric/stoneInset_E.png` },
44546
+ { x: 4, y: 0, terrain: "wall", passable: false, terrainSprite: `${CDN4}/isometric-dungeon/Isometric/stoneInset_E.png` },
44547
+ { x: 0, y: 1, terrain: "wall", passable: false, terrainSprite: `${CDN4}/isometric-dungeon/Isometric/stoneInset_E.png` },
44548
+ { x: 1, y: 1, terrain: "floor", passable: true, terrainSprite: `${CDN4}/isometric-dungeon/Isometric/dirt_E.png` },
44549
+ { x: 2, y: 1, terrain: "floor", passable: true, terrainSprite: `${CDN4}/isometric-dungeon/Isometric/dirt_E.png` },
44550
+ { x: 3, y: 1, terrain: "floor", passable: true, terrainSprite: `${CDN4}/isometric-dungeon/Isometric/dirt_E.png` },
44551
+ { x: 4, y: 1, terrain: "wall", passable: false, terrainSprite: `${CDN4}/isometric-dungeon/Isometric/stoneInset_E.png` },
44552
+ { x: 0, y: 2, terrain: "wall", passable: false, terrainSprite: `${CDN4}/isometric-dungeon/Isometric/stoneInset_E.png` },
44553
+ { x: 1, y: 2, terrain: "floor", passable: true, terrainSprite: `${CDN4}/isometric-dungeon/Isometric/dirtTiles_E.png` },
44554
+ { x: 2, y: 2, terrain: "floor", passable: true, terrainSprite: `${CDN4}/isometric-dungeon/Isometric/dirtTiles_E.png` },
44555
+ { x: 3, y: 2, terrain: "floor", passable: true, terrainSprite: `${CDN4}/isometric-dungeon/Isometric/dirt_E.png` },
44556
+ { x: 4, y: 2, terrain: "wall", passable: false, terrainSprite: `${CDN4}/isometric-dungeon/Isometric/stoneInset_E.png` },
44557
+ { x: 0, y: 3, terrain: "wall", passable: false, terrainSprite: `${CDN4}/isometric-dungeon/Isometric/stoneInset_E.png` },
44558
+ { x: 1, y: 3, terrain: "floor", passable: true, terrainSprite: `${CDN4}/isometric-dungeon/Isometric/dirt_E.png` },
44559
+ { x: 2, y: 3, terrain: "floor", passable: true, terrainSprite: `${CDN4}/isometric-dungeon/Isometric/dirtTiles_E.png` },
44560
+ { x: 3, y: 3, terrain: "stairs", passable: true, terrainSprite: `${CDN4}/isometric-dungeon/Isometric/stairs_E.png` },
44561
+ { x: 4, y: 3, terrain: "wall", passable: false, terrainSprite: `${CDN4}/isometric-dungeon/Isometric/stoneInset_E.png` },
44562
+ { x: 0, y: 4, terrain: "wall", passable: false, terrainSprite: `${CDN4}/isometric-dungeon/Isometric/stoneInset_E.png` },
44563
+ { x: 1, y: 4, terrain: "wall", passable: false, terrainSprite: `${CDN4}/isometric-dungeon/Isometric/stoneInset_E.png` },
44564
+ { x: 2, y: 4, terrain: "wall", passable: false, terrainSprite: `${CDN4}/isometric-dungeon/Isometric/stoneInset_E.png` },
44565
+ { x: 3, y: 4, terrain: "wall", passable: false, terrainSprite: `${CDN4}/isometric-dungeon/Isometric/stoneInset_E.png` },
44566
+ { x: 4, y: 4, terrain: "wall", passable: false, terrainSprite: `${CDN4}/isometric-dungeon/Isometric/stoneInset_E.png` }
44567
+ ];
44568
+ DEFAULT_ENEMIES = [
44569
+ { id: "e1", x: 3, y: 1, hp: 5, attack: 2 },
44570
+ { id: "e2", x: 1, y: 3, hp: 4, attack: 1 }
44571
+ ];
44572
+ DEFAULT_ITEMS = [
44573
+ { id: "i1", x: 2, y: 2, kind: "health_potion" }
44574
+ ];
44575
+ FEATURE_SPRITE = {
44576
+ health_potion: `${CDN4}/isometric-dungeon/Isometric/chestClosed_E.png`,
44577
+ sword: `${CDN4}/isometric-dungeon/Isometric/barrel_E.png`,
44578
+ shield: `${CDN4}/isometric-dungeon/Isometric/barrel_E.png`
44579
+ };
44580
+ RoguelikeBoard.displayName = "RoguelikeBoard";
44581
+ }
44582
+ });
44583
+ function RoguelikeTemplate({
44584
+ entity,
44585
+ scale = 0.45,
44586
+ unitScale = 1,
44587
+ tiles = DEFAULT_ROGUELIKE_TILES,
44588
+ enemies,
44589
+ items,
44590
+ stairsX,
44591
+ stairsY,
44592
+ assetManifest = DEFAULT_ROGUELIKE_MANIFEST,
44593
+ gameEndEvent,
44594
+ className
44595
+ }) {
44596
+ const resolved = entity && typeof entity === "object" && !Array.isArray(entity) ? entity : void 0;
44597
+ if (!resolved && !tiles && !assetManifest) return null;
44598
+ return /* @__PURE__ */ jsxRuntime.jsx(
44599
+ RoguelikeBoard,
44600
+ {
44601
+ entity: resolved,
44602
+ tiles,
44603
+ enemies,
44604
+ items,
44605
+ stairsX,
44606
+ stairsY,
44607
+ assetManifest,
44608
+ scale,
44609
+ unitScale,
44610
+ moveEvent: "MOVE",
44611
+ playAgainEvent: "PLAY_AGAIN",
44612
+ gameEndEvent: gameEndEvent ?? "GAME_END",
44613
+ className
44614
+ }
44615
+ );
44616
+ }
44617
+ var CDN5, DEFAULT_ROGUELIKE_TILES, DEFAULT_ROGUELIKE_MANIFEST;
44618
+ var init_RoguelikeTemplate = __esm({
44619
+ "components/game/templates/RoguelikeTemplate.tsx"() {
44620
+ init_RoguelikeBoard();
44621
+ CDN5 = "https://almadar-kflow-assets.web.app/shared";
44622
+ DEFAULT_ROGUELIKE_TILES = [
44623
+ { x: 0, y: 0, terrain: "wall", passable: false, terrainSprite: `${CDN5}/isometric-dungeon/Isometric/stoneInset_E.png` },
44624
+ { x: 1, y: 0, terrain: "wall", passable: false, terrainSprite: `${CDN5}/isometric-dungeon/Isometric/stoneInset_E.png` },
44625
+ { x: 2, y: 0, terrain: "wall", passable: false, terrainSprite: `${CDN5}/isometric-dungeon/Isometric/stoneInset_E.png` },
44626
+ { x: 3, y: 0, terrain: "wall", passable: false, terrainSprite: `${CDN5}/isometric-dungeon/Isometric/stoneInset_E.png` },
44627
+ { x: 4, y: 0, terrain: "wall", passable: false, terrainSprite: `${CDN5}/isometric-dungeon/Isometric/stoneInset_E.png` },
44628
+ { x: 0, y: 1, terrain: "wall", passable: false, terrainSprite: `${CDN5}/isometric-dungeon/Isometric/stoneInset_E.png` },
44629
+ { x: 1, y: 1, terrain: "floor", passable: true, terrainSprite: `${CDN5}/isometric-dungeon/Isometric/dirt_E.png` },
44630
+ { x: 2, y: 1, terrain: "floor", passable: true, terrainSprite: `${CDN5}/isometric-dungeon/Isometric/dirt_E.png` },
44631
+ { x: 3, y: 1, terrain: "floor", passable: true, terrainSprite: `${CDN5}/isometric-dungeon/Isometric/dirt_E.png` },
44632
+ { x: 4, y: 1, terrain: "wall", passable: false, terrainSprite: `${CDN5}/isometric-dungeon/Isometric/stoneInset_E.png` },
44633
+ { x: 0, y: 2, terrain: "wall", passable: false, terrainSprite: `${CDN5}/isometric-dungeon/Isometric/stoneInset_E.png` },
44634
+ { x: 1, y: 2, terrain: "floor", passable: true, terrainSprite: `${CDN5}/isometric-dungeon/Isometric/dirtTiles_E.png` },
44635
+ { x: 2, y: 2, terrain: "floor", passable: true, terrainSprite: `${CDN5}/isometric-dungeon/Isometric/dirtTiles_E.png` },
44636
+ { x: 3, y: 2, terrain: "floor", passable: true, terrainSprite: `${CDN5}/isometric-dungeon/Isometric/dirt_E.png` },
44637
+ { x: 4, y: 2, terrain: "wall", passable: false, terrainSprite: `${CDN5}/isometric-dungeon/Isometric/stoneInset_E.png` },
44638
+ { x: 0, y: 3, terrain: "wall", passable: false, terrainSprite: `${CDN5}/isometric-dungeon/Isometric/stoneInset_E.png` },
44639
+ { x: 1, y: 3, terrain: "floor", passable: true, terrainSprite: `${CDN5}/isometric-dungeon/Isometric/dirt_E.png` },
44640
+ { x: 2, y: 3, terrain: "floor", passable: true, terrainSprite: `${CDN5}/isometric-dungeon/Isometric/dirtTiles_E.png` },
44641
+ { x: 3, y: 3, terrain: "stairs", passable: true, terrainSprite: `${CDN5}/isometric-dungeon/Isometric/stairs_E.png` },
44642
+ { x: 4, y: 3, terrain: "wall", passable: false, terrainSprite: `${CDN5}/isometric-dungeon/Isometric/stoneInset_E.png` },
44643
+ { x: 0, y: 4, terrain: "wall", passable: false, terrainSprite: `${CDN5}/isometric-dungeon/Isometric/stoneInset_E.png` },
44644
+ { x: 1, y: 4, terrain: "wall", passable: false, terrainSprite: `${CDN5}/isometric-dungeon/Isometric/stoneInset_E.png` },
44645
+ { x: 2, y: 4, terrain: "wall", passable: false, terrainSprite: `${CDN5}/isometric-dungeon/Isometric/stoneInset_E.png` },
44646
+ { x: 3, y: 4, terrain: "wall", passable: false, terrainSprite: `${CDN5}/isometric-dungeon/Isometric/stoneInset_E.png` },
44647
+ { x: 4, y: 4, terrain: "wall", passable: false, terrainSprite: `${CDN5}/isometric-dungeon/Isometric/stoneInset_E.png` }
44648
+ ];
44649
+ DEFAULT_ROGUELIKE_MANIFEST = {
44650
+ baseUrl: CDN5,
44651
+ terrains: {
44652
+ floor: "/isometric-dungeon/Isometric/dirt_E.png",
44653
+ wall: "/isometric-dungeon/Isometric/stoneInset_E.png",
44654
+ stairs: "/isometric-dungeon/Isometric/stairs_E.png",
44655
+ planks: "/isometric-dungeon/Isometric/planks_E.png"
44656
+ },
44657
+ units: {
44658
+ player: "/isometric-dungeon/Characters/Male/Male_0_Idle0.png",
44659
+ enemy: "/sprite-sheets/shadow-legion-sprite-sheet-sw.png"
44660
+ },
44661
+ features: {
44662
+ health_potion: "/isometric-dungeon/Isometric/chestClosed_E.png",
44663
+ sword: "/isometric-dungeon/Isometric/barrel_E.png",
44664
+ shield: "/isometric-dungeon/Isometric/barrel_E.png"
44665
+ }
44666
+ };
44667
+ RoguelikeTemplate.displayName = "RoguelikeTemplate";
44668
+ }
44669
+ });
42393
44670
 
42394
44671
  // lib/traitRegistry.ts
42395
44672
  function notifyListeners2() {
@@ -43643,7 +45920,7 @@ function getAllEvents(traits2) {
43643
45920
  function EventDispatcherTab({ traits: traits2, schema }) {
43644
45921
  const eventBus = useEventBus();
43645
45922
  const { t } = hooks.useTranslate();
43646
- const [log12, setLog] = React82__namespace.useState([]);
45923
+ const [log14, setLog] = React82__namespace.useState([]);
43647
45924
  const prevStatesRef = React82__namespace.useRef(/* @__PURE__ */ new Map());
43648
45925
  React82__namespace.useEffect(() => {
43649
45926
  for (const trait of traits2) {
@@ -43707,9 +45984,9 @@ function EventDispatcherTab({ traits: traits2, schema }) {
43707
45984
  /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "small", weight: "medium", className: "text-gray-500 mb-1", children: t("debug.otherEvents") }),
43708
45985
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-1", children: unavailableEvents.map((event) => /* @__PURE__ */ jsxRuntime.jsx(Badge, { variant: "default", size: "sm", className: "opacity-50", children: event }, event)) })
43709
45986
  ] }),
43710
- log12.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
45987
+ log14.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
43711
45988
  /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "small", weight: "medium", className: "text-gray-500 mb-1", children: t("debug.recentTransitions") }),
43712
- /* @__PURE__ */ jsxRuntime.jsx(Stack, { gap: "xs", children: log12.map((entry, i) => /* @__PURE__ */ jsxRuntime.jsxs(Typography, { variant: "small", className: "font-mono text-xs", children: [
45989
+ /* @__PURE__ */ jsxRuntime.jsx(Stack, { gap: "xs", children: log14.map((entry, i) => /* @__PURE__ */ jsxRuntime.jsxs(Typography, { variant: "small", className: "font-mono text-xs", children: [
43713
45990
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-purple-400", children: entry.traitName }),
43714
45991
  " ",
43715
45992
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-gray-500", children: entry.from }),
@@ -44512,17 +46789,24 @@ function SequencerBoard({
44512
46789
  const { emit } = useEventBus();
44513
46790
  const { t } = hooks.useTranslate();
44514
46791
  const resolved = boardEntity(entity);
44515
- const maxSlots = num(resolved?.maxSlots);
46792
+ const maxSlots = num(resolved?.maxSlots) || 3;
44516
46793
  const solutions = Array.isArray(resolved?.solutions) ? resolved.solutions : [];
44517
46794
  const availableActions = Array.isArray(resolved?.availableActions) ? resolved.availableActions : [];
44518
46795
  const allowDuplicates = resolved?.allowDuplicates !== false;
46796
+ const entitySlots = Array.isArray(resolved?.slots) ? resolved.slots : [];
46797
+ const entityResult = str(resolved?.result);
46798
+ const entityAttempts = num(resolved?.attempts);
46799
+ const entityCurrentStep = typeof resolved?.currentStep === "number" ? resolved.currentStep : -1;
46800
+ const slots = Array.from({ length: maxSlots }, (_, i) => {
46801
+ const entitySlot = entitySlots.find((s) => s.index === i);
46802
+ if (!entitySlot?.placedActionId) return void 0;
46803
+ return availableActions.find((a) => a.id === entitySlot.placedActionId);
46804
+ });
46805
+ const isSuccess = entityResult === "win";
46806
+ const attempts = entityAttempts;
46807
+ const isPlayingBack = entityCurrentStep >= 0 && !isSuccess;
46808
+ const currentStep = entityCurrentStep;
44519
46809
  const [headerError, setHeaderError] = React82.useState(false);
44520
- const [slots, setSlots] = React82.useState(
44521
- () => Array.from({ length: maxSlots }, () => void 0)
44522
- );
44523
- const [playState, setPlayState] = React82.useState("idle");
44524
- const [currentStep, setCurrentStep] = React82.useState(-1);
44525
- const [attempts, setAttempts] = React82.useState(0);
44526
46810
  const [slotFeedback, setSlotFeedback] = React82.useState(
44527
46811
  () => Array.from({ length: maxSlots }, () => null)
44528
46812
  );
@@ -44531,11 +46815,6 @@ function SequencerBoard({
44531
46815
  if (timerRef.current) clearTimeout(timerRef.current);
44532
46816
  }, []);
44533
46817
  const handleSlotDrop = React82.useCallback((index, item) => {
44534
- setSlots((prev) => {
44535
- const next = [...prev];
44536
- next[index] = item;
44537
- return next;
44538
- });
44539
46818
  setSlotFeedback((prev) => {
44540
46819
  const next = [...prev];
44541
46820
  next[index] = null;
@@ -44545,11 +46824,6 @@ function SequencerBoard({
44545
46824
  if (placeEvent) emit(`UI:${placeEvent}`, { slotIndex: index, actionId: item.id });
44546
46825
  }, [emit, placeEvent]);
44547
46826
  const handleSlotRemove = React82.useCallback((index) => {
44548
- setSlots((prev) => {
44549
- const next = [...prev];
44550
- next[index] = void 0;
44551
- return next;
44552
- });
44553
46827
  setSlotFeedback((prev) => {
44554
46828
  const next = [...prev];
44555
46829
  next[index] = null;
@@ -44560,48 +46834,34 @@ function SequencerBoard({
44560
46834
  }, [emit, removeEvent]);
44561
46835
  const handleReset = React82.useCallback(() => {
44562
46836
  if (timerRef.current) clearTimeout(timerRef.current);
44563
- setSlots(Array.from({ length: maxSlots }, () => void 0));
44564
- setPlayState("idle");
44565
- setCurrentStep(-1);
44566
- setAttempts(0);
44567
46837
  setSlotFeedback(Array.from({ length: maxSlots }, () => null));
44568
46838
  if (playAgainEvent) emit(`UI:${playAgainEvent}`, {});
44569
46839
  }, [maxSlots, playAgainEvent, emit]);
44570
46840
  const filledSlots = slots.filter((s) => !!s);
44571
- const canPlay = filledSlots.length > 0 && playState === "idle";
46841
+ const canPlay = filledSlots.length > 0 && !isPlayingBack && !isSuccess;
44572
46842
  const handlePlay = React82.useCallback(() => {
44573
46843
  if (!canPlay) return;
44574
46844
  setSlotFeedback(Array.from({ length: maxSlots }, () => null));
44575
46845
  emit("UI:PLAY_SOUND", { key: "confirm" });
44576
46846
  const sequence = slots.map((s) => s?.id || "");
44577
- if (playEvent) {
44578
- emit(`UI:${playEvent}`, { sequence });
44579
- }
44580
- setPlayState("playing");
44581
- setCurrentStep(0);
46847
+ const playerIds = slots.filter(Boolean).map((s) => s?.id || "");
46848
+ if (playEvent) emit(`UI:${playEvent}`, { sequence });
44582
46849
  let step = 0;
44583
46850
  const advance = () => {
44584
46851
  step++;
46852
+ emit("UI:STEP", { step });
44585
46853
  if (step >= maxSlots) {
46854
+ if (checkEvent) emit(`UI:${checkEvent}`, { sequence: playerIds });
44586
46855
  const playerSeq = slots.map((s) => s?.id);
44587
- const playerIds = slots.filter(Boolean).map((s) => s?.id || "");
44588
46856
  const success = solutions.some(
44589
46857
  (sol) => sol.length === playerIds.length && sol.every((id, i) => id === playerIds[i])
44590
46858
  );
44591
- if (checkEvent) emit(`UI:${checkEvent}`, { sequence: playerIds });
44592
46859
  if (success) {
44593
- setPlayState("success");
44594
- setCurrentStep(-1);
44595
46860
  emit("UI:PLAY_SOUND", { key: "levelComplete" });
44596
- if (completeEvent) {
44597
- emit(`UI:${completeEvent}`, { success: true, sequence: playerIds });
44598
- }
46861
+ if (completeEvent) emit(`UI:${completeEvent}`, { success: true, sequence: playerIds });
44599
46862
  } else {
44600
- setAttempts((prev) => prev + 1);
44601
46863
  const feedback = computeSlotFeedback(playerSeq, solutions);
44602
46864
  setSlotFeedback(feedback);
44603
- setPlayState("idle");
44604
- setCurrentStep(-1);
44605
46865
  emit("UI:PLAY_SOUND", { key: "fail" });
44606
46866
  const correctCount2 = feedback.filter((f3) => f3 === "correct").length;
44607
46867
  for (let ci = 0; ci < correctCount2; ci++) {
@@ -44611,7 +46871,6 @@ function SequencerBoard({
44611
46871
  }
44612
46872
  }
44613
46873
  } else {
44614
- setCurrentStep(step);
44615
46874
  timerRef.current = setTimeout(advance, stepDurationMs);
44616
46875
  }
44617
46876
  };
@@ -44661,7 +46920,7 @@ function SequencerBoard({
44661
46920
  /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "xs", children: [
44662
46921
  /* @__PURE__ */ jsxRuntime.jsxs(HStack, { className: "items-center justify-between", children: [
44663
46922
  /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "body2", className: "text-muted-foreground font-medium", children: t("sequencer.yourSequence") + ":" }),
44664
- hasFeedback && playState === "idle" && /* @__PURE__ */ jsxRuntime.jsxs(Typography, { variant: "caption", className: "text-muted-foreground", children: [
46923
+ hasFeedback && !isPlayingBack && !isSuccess && /* @__PURE__ */ jsxRuntime.jsxs(Typography, { variant: "caption", className: "text-muted-foreground", children: [
44665
46924
  `${correctCount}/${maxSlots} `,
44666
46925
  "\u2705"
44667
46926
  ] })
@@ -44673,7 +46932,7 @@ function SequencerBoard({
44673
46932
  maxSlots,
44674
46933
  onSlotDrop: handleSlotDrop,
44675
46934
  onSlotRemove: handleSlotRemove,
44676
- playing: playState === "playing",
46935
+ playing: isPlayingBack,
44677
46936
  currentStep,
44678
46937
  categoryColors,
44679
46938
  slotFeedback,
@@ -44681,7 +46940,7 @@ function SequencerBoard({
44681
46940
  }
44682
46941
  )
44683
46942
  ] }),
44684
- playState !== "playing" && /* @__PURE__ */ jsxRuntime.jsx(
46943
+ !isPlayingBack && /* @__PURE__ */ jsxRuntime.jsx(
44685
46944
  ActionPalette,
44686
46945
  {
44687
46946
  actions: availableActions,
@@ -44691,8 +46950,8 @@ function SequencerBoard({
44691
46950
  label: t("sequencer.dragActions")
44692
46951
  }
44693
46952
  ),
44694
- hasFeedback && playState === "idle" && attempts > 0 && /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "p-3 rounded-container bg-warning/10 border border-warning/30 text-center", children: /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "body2", className: "text-foreground", children: t(encourageKey) }) }),
44695
- playState === "success" && /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "p-4 rounded-container bg-success/20 border border-success text-center", children: /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "h5", className: "text-success", children: str(resolved.successMessage) || t("sequencer.levelComplete") }) }),
46953
+ hasFeedback && !isPlayingBack && !isSuccess && attempts > 0 && /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "p-3 rounded-container bg-warning/10 border border-warning/30 text-center", children: /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "body2", className: "text-foreground", children: t(encourageKey) }) }),
46954
+ isSuccess && /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "p-4 rounded-container bg-success/20 border border-success text-center", children: /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "h5", className: "text-success", children: str(resolved.successMessage) || t("sequencer.levelComplete") }) }),
44696
46955
  /* @__PURE__ */ jsxRuntime.jsxs(HStack, { gap: "sm", children: [
44697
46956
  /* @__PURE__ */ jsxRuntime.jsx(
44698
46957
  Button,
@@ -45648,9 +47907,9 @@ function VariablePanel({
45648
47907
  /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "body2", className: "text-muted-foreground font-medium", children: t("stateArchitect.variables", { name: entityName }) }),
45649
47908
  variables.map((v) => {
45650
47909
  const name = v.name == null ? "" : String(v.name);
45651
- const value = numField(v.value);
45652
- const max = numField(v.max, 100);
45653
- const min = numField(v.min, 0);
47910
+ const value = numField2(v.value);
47911
+ const max = numField2(v.max, 100);
47912
+ const min = numField2(v.min, 0);
45654
47913
  const unit = v.unit == null ? "" : String(v.unit);
45655
47914
  const pct = Math.round((value - min) / (max - min) * 100);
45656
47915
  const isHigh = pct > 80;
@@ -45680,12 +47939,12 @@ function VariablePanel({
45680
47939
  })
45681
47940
  ] });
45682
47941
  }
45683
- var numField;
47942
+ var numField2;
45684
47943
  var init_VariablePanel = __esm({
45685
47944
  "components/game/organisms/puzzles/state-architect/VariablePanel.tsx"() {
45686
47945
  init_atoms2();
45687
47946
  init_cn();
45688
- numField = (v, fallback = 0) => {
47947
+ numField2 = (v, fallback = 0) => {
45689
47948
  const n = Number(v);
45690
47949
  return Number.isFinite(n) ? n : fallback;
45691
47950
  };
@@ -45742,6 +48001,9 @@ function StateArchitectBoard({
45742
48001
  stepDurationMs = 600,
45743
48002
  testEvent,
45744
48003
  completeEvent,
48004
+ addTransitionEvent,
48005
+ removeTransitionEvent,
48006
+ playAgainEvent,
45745
48007
  className
45746
48008
  }) {
45747
48009
  const { emit } = useEventBus();
@@ -45754,14 +48016,16 @@ function StateArchitectBoard({
45754
48016
  const testCases = Array.isArray(resolved?.testCases) ? resolved.testCases : [];
45755
48017
  const entityTransitions = Array.isArray(resolved?.transitions) ? resolved.transitions : [];
45756
48018
  const entityVariables = rows(resolved?.variables);
45757
- const [transitions, setTransitions] = React82.useState(entityTransitions);
48019
+ const transitions = entityTransitions;
48020
+ const attempts = typeof resolved?.attempts === "number" ? resolved.attempts : 0;
48021
+ const entityResult = str(resolved?.result) || "none";
48022
+ const isSuccess = entityResult === "win";
48023
+ const [isTesting, setIsTesting] = React82.useState(false);
45758
48024
  const [headerError, setHeaderError] = React82.useState(false);
45759
- const [playState, setPlayState] = React82.useState("editing");
45760
48025
  const [currentState, setCurrentState] = React82.useState(initialState);
45761
48026
  const [selectedState, setSelectedState] = React82.useState(null);
45762
48027
  const [testResults, setTestResults] = React82.useState([]);
45763
48028
  const [variables, setVariables] = React82.useState(() => [...entityVariables]);
45764
- const [attempts, setAttempts] = React82.useState(0);
45765
48029
  const timerRef = React82.useRef(null);
45766
48030
  const [addingFrom, setAddingFrom] = React82.useState(null);
45767
48031
  React82.useEffect(() => () => {
@@ -45771,7 +48035,7 @@ function StateArchitectBoard({
45771
48035
  const GRAPH_H = 400;
45772
48036
  const positions = React82.useMemo(() => layoutStates(entityStates, GRAPH_W, GRAPH_H), [entityStates]);
45773
48037
  const handleStateClick = React82.useCallback((state) => {
45774
- if (playState !== "editing") return;
48038
+ if (isTesting) return;
45775
48039
  if (addingFrom) {
45776
48040
  if (addingFrom !== state) {
45777
48041
  const event = availableEvents[0] || "EVENT";
@@ -45781,20 +48045,20 @@ function StateArchitectBoard({
45781
48045
  to: state,
45782
48046
  event
45783
48047
  };
45784
- setTransitions((prev) => [...prev, newTrans]);
48048
+ if (addTransitionEvent) emit(`UI:${addTransitionEvent}`, { id: newTrans.id, from: newTrans.from, to: newTrans.to, event: newTrans.event });
45785
48049
  }
45786
48050
  setAddingFrom(null);
45787
48051
  } else {
45788
48052
  setSelectedState(state);
45789
48053
  }
45790
- }, [playState, addingFrom, availableEvents]);
48054
+ }, [isTesting, addingFrom, availableEvents, addTransitionEvent, emit]);
45791
48055
  const handleStartAddTransition = React82.useCallback(() => {
45792
48056
  if (!selectedState) return;
45793
48057
  setAddingFrom(selectedState);
45794
48058
  }, [selectedState]);
45795
48059
  const handleRemoveTransition = React82.useCallback((transId) => {
45796
- setTransitions((prev) => prev.filter((t2) => t2.id !== transId));
45797
- }, []);
48060
+ if (removeTransitionEvent) emit(`UI:${removeTransitionEvent}`, { id: transId });
48061
+ }, [removeTransitionEvent, emit]);
45798
48062
  const machine = React82.useMemo(() => ({
45799
48063
  name: entityName,
45800
48064
  description: str(resolved?.description),
@@ -45808,16 +48072,16 @@ function StateArchitectBoard({
45808
48072
  }))
45809
48073
  }), [entityName, resolved, entityStates, currentState, transitions]);
45810
48074
  const handleTest = React82.useCallback(() => {
45811
- if (playState !== "editing") return;
48075
+ if (isTesting) return;
45812
48076
  if (testEvent) emit(`UI:${testEvent}`, {});
45813
- setPlayState("testing");
48077
+ setIsTesting(true);
45814
48078
  setTestResults([]);
45815
48079
  const results = [];
45816
48080
  let testIdx = 0;
45817
48081
  const runNextTest = () => {
45818
48082
  if (testIdx >= testCases.length) {
45819
48083
  const allPassed = results.every((r) => r.passed);
45820
- setPlayState(allPassed ? "success" : "fail");
48084
+ setIsTesting(false);
45821
48085
  setTestResults(results);
45822
48086
  if (allPassed && completeEvent) {
45823
48087
  emit(`UI:${completeEvent}`, {
@@ -45825,9 +48089,6 @@ function StateArchitectBoard({
45825
48089
  passedTests: results.filter((r) => r.passed).length
45826
48090
  });
45827
48091
  }
45828
- if (!allPassed) {
45829
- setAttempts((prev) => prev + 1);
45830
- }
45831
48092
  return;
45832
48093
  }
45833
48094
  const testCase = testCases[testIdx];
@@ -45850,24 +48111,23 @@ function StateArchitectBoard({
45850
48111
  timerRef.current = setTimeout(runNextTest, stepDurationMs);
45851
48112
  };
45852
48113
  timerRef.current = setTimeout(runNextTest, stepDurationMs);
45853
- }, [playState, transitions, testCases, initialState, stepDurationMs, testEvent, completeEvent, emit]);
48114
+ }, [isTesting, transitions, testCases, initialState, stepDurationMs, testEvent, completeEvent, emit]);
45854
48115
  const handleTryAgain = React82.useCallback(() => {
45855
48116
  if (timerRef.current) clearTimeout(timerRef.current);
45856
- setPlayState("editing");
48117
+ setIsTesting(false);
45857
48118
  setCurrentState(initialState);
45858
48119
  setTestResults([]);
45859
48120
  }, [initialState]);
45860
48121
  const handleReset = React82.useCallback(() => {
45861
48122
  if (timerRef.current) clearTimeout(timerRef.current);
45862
- setTransitions(entityTransitions);
45863
- setPlayState("editing");
48123
+ setIsTesting(false);
45864
48124
  setCurrentState(initialState);
45865
48125
  setTestResults([]);
45866
48126
  setVariables([...entityVariables]);
45867
48127
  setSelectedState(null);
45868
48128
  setAddingFrom(null);
45869
- setAttempts(0);
45870
- }, [entityTransitions, initialState, entityVariables]);
48129
+ if (playAgainEvent) emit(`UI:${playAgainEvent}`, {});
48130
+ }, [initialState, entityVariables, playAgainEvent, emit]);
45871
48131
  const codeData = React82.useMemo(() => ({
45872
48132
  name: entityName,
45873
48133
  states: entityStates,
@@ -45963,7 +48223,7 @@ function StateArchitectBoard({
45963
48223
  ]
45964
48224
  }
45965
48225
  ),
45966
- playState === "editing" && /* @__PURE__ */ jsxRuntime.jsx(HStack, { gap: "sm", children: /* @__PURE__ */ jsxRuntime.jsx(
48226
+ !isTesting && /* @__PURE__ */ jsxRuntime.jsx(HStack, { gap: "sm", children: /* @__PURE__ */ jsxRuntime.jsx(
45967
48227
  Button,
45968
48228
  {
45969
48229
  variant: "ghost",
@@ -45985,7 +48245,7 @@ function StateArchitectBoard({
45985
48245
  t2.guardHint,
45986
48246
  ")"
45987
48247
  ] }),
45988
- playState === "editing" && /* @__PURE__ */ jsxRuntime.jsx(
48248
+ !isTesting && /* @__PURE__ */ jsxRuntime.jsx(
45989
48249
  Button,
45990
48250
  {
45991
48251
  variant: "ghost",
@@ -46017,21 +48277,21 @@ function StateArchitectBoard({
46017
48277
  resolved.showCodeView !== false && /* @__PURE__ */ jsxRuntime.jsx(StateJsonView, { data: codeData, label: "View Code" })
46018
48278
  ] })
46019
48279
  ] }),
46020
- playState === "success" && /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "p-4 rounded-container bg-success/20 border border-success text-center", children: /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "h5", className: "text-success", children: str(resolved.successMessage) || t("stateArchitect.allPassed") }) }),
46021
- playState === "fail" && /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "sm", children: [
48280
+ isSuccess && /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "p-4 rounded-container bg-success/20 border border-success text-center", children: /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "h5", className: "text-success", children: str(resolved.successMessage) || t("stateArchitect.allPassed") }) }),
48281
+ !isTesting && !isSuccess && testResults.some((r) => !r.passed) && /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "sm", children: [
46022
48282
  /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "p-4 rounded-container bg-warning/10 border border-warning/30 text-center", children: /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "body1", className: "text-foreground font-medium", children: t(ENCOURAGEMENT_KEYS3[Math.min(attempts - 1, ENCOURAGEMENT_KEYS3.length - 1)] ?? ENCOURAGEMENT_KEYS3[0]) }) }),
46023
- attempts >= 3 && hint && /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "p-3 rounded-container bg-accent/10 border border-accent/30", children: /* @__PURE__ */ jsxRuntime.jsxs(HStack, { className: "items-start", gap: "xs", children: [
48283
+ !isSuccess && attempts >= 2 && Boolean(str(resolved?.hint)) && /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "p-3 rounded-container bg-accent/10 border border-accent/30", children: /* @__PURE__ */ jsxRuntime.jsxs(HStack, { className: "items-start", gap: "xs", children: [
46024
48284
  /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "body2", className: "text-accent font-bold shrink-0", children: "\u{1F4A1} " + t("game.hint") + ":" }),
46025
48285
  /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "body2", className: "text-foreground", children: hint })
46026
48286
  ] }) })
46027
48287
  ] }),
46028
48288
  /* @__PURE__ */ jsxRuntime.jsxs(HStack, { gap: "sm", children: [
46029
- playState === "fail" ? /* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "primary", onClick: handleTryAgain, children: "\u{1F504} " + t("puzzle.tryAgainButton") }) : /* @__PURE__ */ jsxRuntime.jsx(
48289
+ !isTesting && !isSuccess && testResults.some((r) => !r.passed) ? /* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "primary", onClick: handleTryAgain, children: "\u{1F504} " + t("puzzle.tryAgainButton") }) : /* @__PURE__ */ jsxRuntime.jsx(
46030
48290
  Button,
46031
48291
  {
46032
48292
  variant: "primary",
46033
48293
  onClick: handleTest,
46034
- disabled: playState !== "editing",
48294
+ disabled: isTesting,
46035
48295
  children: "\u25B6 " + t("game.runTests")
46036
48296
  }
46037
48297
  ),
@@ -46537,6 +48797,289 @@ var init_ToastSlot = __esm({
46537
48797
  ToastSlot.displayName = "ToastSlot";
46538
48798
  }
46539
48799
  });
48800
+ function tilesToIso(tiles) {
48801
+ return tiles.map((t) => ({
48802
+ x: t.x,
48803
+ y: t.y,
48804
+ terrain: t.terrain,
48805
+ terrainSprite: t.terrainSprite ?? TERRAIN_SPRITES[t.terrain ?? "ground"],
48806
+ passable: t.passable
48807
+ }));
48808
+ }
48809
+ function towersToUnits(towers) {
48810
+ return towers.map((t) => ({
48811
+ id: t.id,
48812
+ position: { x: t.x, y: t.y },
48813
+ name: "Tower",
48814
+ team: "player",
48815
+ sprite: TOWER_SPRITE,
48816
+ unitType: "guardian",
48817
+ health: 1,
48818
+ maxHealth: 1
48819
+ }));
48820
+ }
48821
+ function creepsToUnits(creeps) {
48822
+ return creeps.map((c) => ({
48823
+ id: c.id,
48824
+ position: { x: c.x, y: c.y },
48825
+ name: "Creep",
48826
+ team: "enemy",
48827
+ sprite: CREEP_SPRITE,
48828
+ unitType: "scrapper",
48829
+ health: c.hp,
48830
+ maxHealth: c.maxHp
48831
+ }));
48832
+ }
48833
+ function pathToFeatures(path) {
48834
+ return path.map((p2, i) => ({
48835
+ id: `path-${i}`,
48836
+ x: p2.x,
48837
+ y: p2.y,
48838
+ type: "path-marker",
48839
+ sprite: `${CDN6}world-map/road_straight.png`
48840
+ }));
48841
+ }
48842
+ function TowerDefenseBoard({
48843
+ entity,
48844
+ tiles: propTiles,
48845
+ path: propPath,
48846
+ towers: propTowers,
48847
+ creeps: propCreeps,
48848
+ gold: propGold,
48849
+ lives: propLives,
48850
+ wave: propWave,
48851
+ maxWaves: propMaxWaves,
48852
+ result: propResult,
48853
+ waveActive: propWaveActive,
48854
+ towerCost = 25,
48855
+ scale = 0.45,
48856
+ placeTowerEvent,
48857
+ startWaveEvent,
48858
+ playAgainEvent,
48859
+ gameEndEvent,
48860
+ className
48861
+ }) {
48862
+ const board = boardEntity(entity) ?? {};
48863
+ const eventBus = useEventBus();
48864
+ const { t } = hooks.useTranslate();
48865
+ const tiles = propTiles ?? rows(board.tiles);
48866
+ const path = propPath ?? rows(board.path);
48867
+ const towers = propTowers ?? rows(board.towers);
48868
+ const creeps = propCreeps ?? rows(board.creeps);
48869
+ const gold = propGold ?? num(board.gold, 100);
48870
+ const lives = propLives ?? num(board.lives, 20);
48871
+ const wave = propWave ?? num(board.wave, 1);
48872
+ const maxWaves = propMaxWaves ?? num(board.maxWaves, 5);
48873
+ const result = propResult ?? (str(board.result) || "none");
48874
+ const waveActive = propWaveActive ?? Boolean(board.waveActive);
48875
+ const [hoveredTile, setHoveredTile] = React82.useState(null);
48876
+ const emittedGameEnd = React82.useRef(false);
48877
+ if (result !== "none" && !emittedGameEnd.current) {
48878
+ emittedGameEnd.current = true;
48879
+ if (gameEndEvent) {
48880
+ eventBus.emit(`UI:${gameEndEvent}`, { result });
48881
+ }
48882
+ }
48883
+ if (result === "none") {
48884
+ emittedGameEnd.current = false;
48885
+ }
48886
+ const towerPositions = React82.useMemo(() => new Set(towers.map((t2) => `${t2.x},${t2.y}`)), [towers]);
48887
+ const pathPositions = React82.useMemo(() => new Set(path.map((p2) => `${p2.x},${p2.y}`)), [path]);
48888
+ const validMoves = React82.useMemo(() => {
48889
+ if (result !== "none" || gold < towerCost) return [];
48890
+ return tiles.filter(
48891
+ (t2) => t2.passable !== false && !towerPositions.has(`${t2.x},${t2.y}`) && !pathPositions.has(`${t2.x},${t2.y}`)
48892
+ ).map((t2) => ({ x: t2.x, y: t2.y }));
48893
+ }, [tiles, towerPositions, pathPositions, result, gold, towerCost]);
48894
+ const isoTiles = React82.useMemo(() => tilesToIso(tiles), [tiles]);
48895
+ const towerUnits = React82.useMemo(() => towersToUnits(towers), [towers]);
48896
+ const creepUnits = React82.useMemo(() => creepsToUnits(creeps), [creeps]);
48897
+ const isoUnits = React82.useMemo(() => [...towerUnits, ...creepUnits], [towerUnits, creepUnits]);
48898
+ const pathFeatures = React82.useMemo(() => pathToFeatures(path), [path]);
48899
+ const handleTileClick = React82.useCallback((x, y) => {
48900
+ if (result !== "none") return;
48901
+ if (pathPositions.has(`${x},${y}`)) return;
48902
+ if (towerPositions.has(`${x},${y}`)) return;
48903
+ if (gold < towerCost) return;
48904
+ if (placeTowerEvent) {
48905
+ eventBus.emit(`UI:${placeTowerEvent}`, { x, y });
48906
+ }
48907
+ }, [result, pathPositions, towerPositions, gold, towerCost, placeTowerEvent, eventBus]);
48908
+ const handleStartWave = React82.useCallback(() => {
48909
+ if (startWaveEvent) {
48910
+ eventBus.emit(`UI:${startWaveEvent}`, { wave });
48911
+ }
48912
+ }, [startWaveEvent, wave, eventBus]);
48913
+ const handlePlayAgain = React82.useCallback(() => {
48914
+ if (playAgainEvent) {
48915
+ eventBus.emit(`UI:${playAgainEvent}`, {});
48916
+ }
48917
+ }, [playAgainEvent, eventBus]);
48918
+ const canPlaceTower = gold >= towerCost && result === "none";
48919
+ const canStartWave = !waveActive && result === "none";
48920
+ return /* @__PURE__ */ jsxRuntime.jsxs(VStack, { className: cn("tower-defense-board relative min-h-[600px] bg-background", className), gap: "none", children: [
48921
+ /* @__PURE__ */ jsxRuntime.jsxs(
48922
+ HStack,
48923
+ {
48924
+ className: "px-4 py-2 border-b border-border bg-surface",
48925
+ gap: "lg",
48926
+ align: "center",
48927
+ children: [
48928
+ /* @__PURE__ */ jsxRuntime.jsxs(HStack, { gap: "xs", align: "center", children: [
48929
+ /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "small", color: "muted", children: t("td.gold") ?? "Gold" }),
48930
+ /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "body2", weight: "bold", className: "text-warning", children: gold })
48931
+ ] }),
48932
+ /* @__PURE__ */ jsxRuntime.jsxs(HStack, { gap: "xs", align: "center", children: [
48933
+ /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "small", color: "muted", children: t("td.lives") ?? "Lives" }),
48934
+ /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "body2", weight: "bold", className: lives <= 5 ? "text-error" : "text-success", children: lives })
48935
+ ] }),
48936
+ /* @__PURE__ */ jsxRuntime.jsxs(HStack, { gap: "xs", align: "center", children: [
48937
+ /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "small", color: "muted", children: t("td.wave") ?? "Wave" }),
48938
+ /* @__PURE__ */ jsxRuntime.jsxs(Typography, { variant: "body2", weight: "bold", children: [
48939
+ wave,
48940
+ " / ",
48941
+ maxWaves
48942
+ ] })
48943
+ ] }),
48944
+ /* @__PURE__ */ jsxRuntime.jsxs(Box, { className: "ml-auto", children: [
48945
+ canStartWave && /* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "primary", size: "sm", onClick: handleStartWave, children: t("td.startWave") ?? "Start Wave" }),
48946
+ waveActive && /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "small", className: "text-warning animate-pulse", children: t("td.waveInProgress") ?? "Wave in progress\u2026" })
48947
+ ] })
48948
+ ]
48949
+ }
48950
+ ),
48951
+ /* @__PURE__ */ jsxRuntime.jsxs(Box, { className: "relative flex-1", children: [
48952
+ /* @__PURE__ */ jsxRuntime.jsx(
48953
+ IsometricCanvas_default,
48954
+ {
48955
+ tiles: isoTiles,
48956
+ units: isoUnits,
48957
+ features: pathFeatures,
48958
+ validMoves,
48959
+ hoveredTile,
48960
+ onTileClick: handleTileClick,
48961
+ onTileHover: (x, y) => setHoveredTile({ x, y }),
48962
+ onTileLeave: () => setHoveredTile(null),
48963
+ scale
48964
+ }
48965
+ ),
48966
+ hoveredTile && canPlaceTower && validMoves.some((m) => m.x === hoveredTile.x && m.y === hoveredTile.y) && /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "absolute bottom-4 left-1/2 -translate-x-1/2 z-10 bg-background/80 rounded px-3 py-1 backdrop-blur-sm pointer-events-none", children: /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "small", children: t("td.placeTower") ?? `Place tower (${towerCost} gold)` }) })
48967
+ ] }),
48968
+ result !== "none" && /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "absolute inset-0 z-50 flex items-center justify-center bg-background/70 backdrop-blur-sm", children: /* @__PURE__ */ jsxRuntime.jsxs(VStack, { className: "text-center p-8", gap: "lg", children: [
48969
+ /* @__PURE__ */ jsxRuntime.jsx(
48970
+ Typography,
48971
+ {
48972
+ variant: "h2",
48973
+ className: cn(
48974
+ "text-4xl font-black tracking-widest uppercase",
48975
+ result === "won" ? "text-warning" : "text-error"
48976
+ ),
48977
+ children: result === "won" ? t("td.victory") ?? "Victory!" : t("td.defeat") ?? "Defeat!"
48978
+ }
48979
+ ),
48980
+ /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "body1", className: "text-muted-foreground", children: result === "won" ? t("td.survivedAllWaves") ?? `Survived all ${maxWaves} waves!` : t("td.livesReachedZero") ?? "Your base was overrun." }),
48981
+ /* @__PURE__ */ jsxRuntime.jsx(
48982
+ Button,
48983
+ {
48984
+ variant: "primary",
48985
+ className: "px-8 py-3 font-semibold",
48986
+ onClick: handlePlayAgain,
48987
+ children: t("td.playAgain") ?? "Play Again"
48988
+ }
48989
+ )
48990
+ ] }) })
48991
+ ] });
48992
+ }
48993
+ var CDN6, TERRAIN_SPRITES, TOWER_SPRITE, CREEP_SPRITE;
48994
+ var init_TowerDefenseBoard = __esm({
48995
+ "components/game/organisms/TowerDefenseBoard.tsx"() {
48996
+ "use client";
48997
+ init_cn();
48998
+ init_useEventBus();
48999
+ init_Box();
49000
+ init_Button();
49001
+ init_Typography();
49002
+ init_Stack();
49003
+ init_IsometricCanvas();
49004
+ init_boardEntity();
49005
+ CDN6 = "https://almadar-kflow-assets.web.app/shared/";
49006
+ TERRAIN_SPRITES = {
49007
+ ground: `${CDN6}isometric-blocks/PNG/Platformer tiles/platformerTile_01.png`,
49008
+ path: `${CDN6}isometric-blocks/PNG/Platformer tiles/platformerTile_09.png`,
49009
+ wall: `${CDN6}isometric-blocks/PNG/Platformer tiles/platformerTile_05.png`
49010
+ };
49011
+ TOWER_SPRITE = `${CDN6}units/guardian.png`;
49012
+ CREEP_SPRITE = `${CDN6}units/scrapper.png`;
49013
+ TowerDefenseBoard.displayName = "TowerDefenseBoard";
49014
+ }
49015
+ });
49016
+ function TowerDefenseTemplate({
49017
+ entity,
49018
+ title = "Tower Defense",
49019
+ tiles,
49020
+ path,
49021
+ towers,
49022
+ gold,
49023
+ lives,
49024
+ maxWaves,
49025
+ towerCost,
49026
+ scale,
49027
+ placeTowerEvent,
49028
+ startWaveEvent,
49029
+ playAgainEvent,
49030
+ gameEndEvent,
49031
+ className
49032
+ }) {
49033
+ const resolved = entity && typeof entity === "object" && !Array.isArray(entity) ? entity : void 0;
49034
+ return /* @__PURE__ */ jsxRuntime.jsxs(
49035
+ Box,
49036
+ {
49037
+ display: "flex",
49038
+ fullHeight: true,
49039
+ className: cn("tower-defense-template flex-col", className),
49040
+ children: [
49041
+ /* @__PURE__ */ jsxRuntime.jsx(
49042
+ HStack,
49043
+ {
49044
+ gap: "sm",
49045
+ align: "center",
49046
+ className: "px-4 py-3 border-b-2 border-border bg-surface shrink-0",
49047
+ children: /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "h4", children: title })
49048
+ }
49049
+ ),
49050
+ /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "flex-1 relative overflow-hidden", children: /* @__PURE__ */ jsxRuntime.jsx(
49051
+ TowerDefenseBoard,
49052
+ {
49053
+ entity: resolved,
49054
+ tiles,
49055
+ path,
49056
+ towers,
49057
+ gold,
49058
+ lives,
49059
+ maxWaves,
49060
+ towerCost,
49061
+ scale,
49062
+ placeTowerEvent,
49063
+ startWaveEvent,
49064
+ playAgainEvent,
49065
+ gameEndEvent,
49066
+ className: "h-full"
49067
+ }
49068
+ ) })
49069
+ ]
49070
+ }
49071
+ );
49072
+ }
49073
+ var init_TowerDefenseTemplate = __esm({
49074
+ "components/game/templates/TowerDefenseTemplate.tsx"() {
49075
+ init_cn();
49076
+ init_Box();
49077
+ init_Stack();
49078
+ init_Typography();
49079
+ init_TowerDefenseBoard();
49080
+ TowerDefenseTemplate.displayName = "TowerDefenseTemplate";
49081
+ }
49082
+ });
46540
49083
  function useBattleState(initialUnits, eventConfig = {}, callbacks = {}) {
46541
49084
  const eventBus = useEventBus();
46542
49085
  const {
@@ -46735,17 +49278,12 @@ var init_UncontrolledBattleBoard = __esm({
46735
49278
  }
46736
49279
  });
46737
49280
  function heroPosition(h) {
49281
+ if ("position" in h && h.position != null) return h.position;
46738
49282
  return vec2(h.position);
46739
49283
  }
46740
- function heroOwner(h) {
46741
- return str(h.owner);
46742
- }
46743
49284
  function heroMovement(h) {
46744
49285
  return num(h.movement);
46745
49286
  }
46746
- function hexPassable(h) {
46747
- return h.passable !== false;
46748
- }
46749
49287
  function defaultIsInRange(from, to, range) {
46750
49288
  return Math.abs(from.x - to.x) + Math.abs(from.y - to.y) <= range;
46751
49289
  }
@@ -46781,38 +49319,41 @@ function WorldMapBoard({
46781
49319
  }) {
46782
49320
  const eventBus = useEventBus();
46783
49321
  const resolved = boardEntity(entity);
46784
- const hexes = rows(resolved?.hexes);
46785
- const heroes = rows(resolved?.heroes);
49322
+ const entityUnits = rows(resolved?.units);
49323
+ const entityTiles = rows(resolved?.tiles);
46786
49324
  const features = propFeatures ?? (Array.isArray(resolved?.features) ? resolved.features : []);
46787
49325
  const selectedHeroId = resolved?.selectedHeroId ?? null;
46788
49326
  const assetManifest = propAssetManifest ?? resolved?.assetManifest;
46789
49327
  const backgroundImage = resolved?.backgroundImage;
46790
49328
  const [hoveredTile, setHoveredTile] = React82.useState(null);
46791
- const selectedHero = React82.useMemo(
46792
- () => heroes.find((h) => str(h.id) === selectedHeroId) ?? null,
46793
- [heroes, selectedHeroId]
46794
- );
46795
49329
  const derivedTiles = React82.useMemo(
46796
- () => hexes.map((hex) => ({
46797
- x: num(hex.x),
46798
- y: num(hex.y),
46799
- terrain: str(hex.terrain),
46800
- terrainSprite: hex.terrainSprite == null ? void 0 : str(hex.terrainSprite)
49330
+ () => entityTiles.map((t) => ({
49331
+ x: num(t.x),
49332
+ y: num(t.y),
49333
+ terrain: str(t.terrain),
49334
+ terrainSprite: t.terrainSprite == null ? void 0 : str(t.terrainSprite),
49335
+ passable: t.passable !== false
46801
49336
  })),
46802
- [hexes]
49337
+ [entityTiles]
46803
49338
  );
46804
49339
  const tiles = propTiles ?? derivedTiles;
46805
49340
  const baseUnits = React82.useMemo(
46806
- () => propUnits ?? heroes.map((hero) => ({
46807
- id: str(hero.id),
46808
- position: heroPosition(hero),
46809
- name: str(hero.name),
46810
- team: heroOwner(hero) === "enemy" ? "enemy" : "player",
46811
- health: 100,
46812
- maxHealth: 100,
46813
- sprite: hero.sprite == null ? void 0 : str(hero.sprite)
49341
+ () => propUnits ?? entityUnits.map((u) => ({
49342
+ id: str(u.id),
49343
+ position: heroPosition(u),
49344
+ name: str(u.name),
49345
+ // lolo uses `team` field (not `owner`)
49346
+ team: str(u.team) === "enemy" ? "enemy" : "player",
49347
+ health: num(u.health) || 100,
49348
+ maxHealth: num(u.maxHealth) || 100,
49349
+ sprite: u.sprite == null ? void 0 : str(u.sprite)
46814
49350
  })),
46815
- [heroes, propUnits]
49351
+ [entityUnits, propUnits]
49352
+ );
49353
+ const gameUnits = baseUnits;
49354
+ const selectedHero = React82.useMemo(
49355
+ () => gameUnits.find((u) => u.id === selectedHeroId) ?? null,
49356
+ [gameUnits, selectedHeroId]
46816
49357
  );
46817
49358
  const MOVE_SPEED_MS_PER_TILE = 300;
46818
49359
  const movementAnimRef = React82.useRef(null);
@@ -46858,51 +49399,50 @@ function WorldMapBoard({
46858
49399
  const validMoves = React82.useMemo(() => {
46859
49400
  if (!selectedHero || heroMovement(selectedHero) <= 0) return [];
46860
49401
  const sp = heroPosition(selectedHero);
46861
- const sOwner = heroOwner(selectedHero);
49402
+ const sTeam = str(selectedHero.team);
46862
49403
  const range = heroMovement(selectedHero);
46863
49404
  const moves = [];
46864
- hexes.forEach((hex) => {
46865
- const hx = num(hex.x);
46866
- const hy = num(hex.y);
46867
- if (!hexPassable(hex)) return;
46868
- if (hx === sp.x && hy === sp.y) return;
46869
- if (!isInRange(sp, { x: hx, y: hy }, range)) return;
46870
- if (heroes.some((h) => {
46871
- const hp = heroPosition(h);
46872
- return hp.x === hx && hp.y === hy && heroOwner(h) === sOwner;
49405
+ tiles.forEach((t) => {
49406
+ const tx = t.x;
49407
+ const ty = t.y;
49408
+ if (t.passable === false) return;
49409
+ if (tx === sp.x && ty === sp.y) return;
49410
+ if (!isInRange(sp, { x: tx, y: ty }, range)) return;
49411
+ if (gameUnits.some((u) => {
49412
+ const up = u.position ?? { x: u.x ?? -1, y: u.y ?? -1 };
49413
+ return up.x === tx && up.y === ty && str(u.team) === sTeam;
46873
49414
  })) return;
46874
- moves.push({ x: hx, y: hy });
49415
+ moves.push({ x: tx, y: ty });
46875
49416
  });
46876
49417
  return moves;
46877
- }, [selectedHero, hexes, heroes, isInRange]);
49418
+ }, [selectedHero, tiles, gameUnits, isInRange]);
46878
49419
  const attackTargets = React82.useMemo(() => {
46879
49420
  if (!selectedHero || heroMovement(selectedHero) <= 0) return [];
46880
49421
  const sp = heroPosition(selectedHero);
46881
- const sOwner = heroOwner(selectedHero);
49422
+ const sTeam = str(selectedHero.team);
46882
49423
  const range = heroMovement(selectedHero);
46883
- return heroes.filter((h) => heroOwner(h) !== sOwner).filter((h) => isInRange(sp, heroPosition(h), range)).map((h) => heroPosition(h));
46884
- }, [selectedHero, heroes, isInRange]);
46885
- const maxY = Math.max(...hexes.map((h) => num(h.y)), 0);
49424
+ return gameUnits.filter((u) => str(u.team) !== sTeam).filter((u) => isInRange(sp, u.position ?? { x: u.x ?? -1, y: u.y ?? -1 }, range)).map((u) => u.position ?? { x: u.x ?? -1, y: u.y ?? -1 });
49425
+ }, [selectedHero, gameUnits, isInRange]);
49426
+ const maxY = Math.max(...tiles.map((t) => t.y), 0);
46886
49427
  const baseOffsetX = (maxY + 1) * (TILE_WIDTH * scale / 2);
46887
49428
  const tileToScreen = React82.useCallback(
46888
49429
  (tx, ty) => isoToScreen(tx, ty, scale, baseOffsetX),
46889
49430
  [scale, baseOffsetX]
46890
49431
  );
46891
49432
  const hoveredHex = React82.useMemo(
46892
- () => hoveredTile ? hexes.find((h) => num(h.x) === hoveredTile.x && num(h.y) === hoveredTile.y) ?? null : null,
46893
- [hoveredTile, hexes]
49433
+ () => hoveredTile ? tiles.find((t) => t.x === hoveredTile.x && t.y === hoveredTile.y) ?? null : null,
49434
+ [hoveredTile, tiles]
46894
49435
  );
46895
49436
  const hoveredHero = React82.useMemo(
46896
- () => hoveredTile ? heroes.find((h) => {
46897
- const hp = heroPosition(h);
46898
- return hp.x === hoveredTile.x && hp.y === hoveredTile.y;
49437
+ () => hoveredTile ? gameUnits.find((u) => {
49438
+ const up = u.position ?? { x: u.x ?? -1, y: u.y ?? -1 };
49439
+ return up.x === hoveredTile.x && up.y === hoveredTile.y;
46899
49440
  }) ?? null : null,
46900
- [hoveredTile, heroes]
49441
+ [hoveredTile, gameUnits]
46901
49442
  );
46902
49443
  const handleTileClick = React82.useCallback((x, y) => {
46903
49444
  if (movementAnimRef.current) return;
46904
- const hex = hexes.find((h) => num(h.x) === x && num(h.y) === y);
46905
- if (!hex) return;
49445
+ const tile = tiles.find((t) => t.x === x && t.y === y);
46906
49446
  if (tileClickEvent) {
46907
49447
  eventBus.emit(`UI:${tileClickEvent}`, { x, y });
46908
49448
  }
@@ -46913,38 +49453,39 @@ function WorldMapBoard({
46913
49453
  if (heroMoveEvent) {
46914
49454
  eventBus.emit(`UI:${heroMoveEvent}`, { heroId, toX: x, toY: y });
46915
49455
  }
46916
- const feature = str(hex.feature);
49456
+ const feature = tile ? str(tile.feature) : "";
46917
49457
  if (feature && feature !== "none") {
46918
- onFeatureEnter?.(heroId, hex);
49458
+ const tileRow = tile;
49459
+ onFeatureEnter?.(heroId, tileRow);
46919
49460
  if (featureEnterEvent) {
46920
- eventBus.emit(`UI:${featureEnterEvent}`, { heroId, feature, hex });
49461
+ eventBus.emit(`UI:${featureEnterEvent}`, { heroId, feature, hex: tileRow });
46921
49462
  }
46922
49463
  }
46923
49464
  });
46924
49465
  return;
46925
49466
  }
46926
- const enemy = heroes.find((h) => {
46927
- const hp = heroPosition(h);
46928
- return hp.x === x && hp.y === y && heroOwner(h) === "enemy";
49467
+ const enemy = gameUnits.find((u) => {
49468
+ const up = u.position ?? { x: u.x ?? -1, y: u.y ?? -1 };
49469
+ return up.x === x && up.y === y && str(u.team) === "enemy";
46929
49470
  });
46930
49471
  if (selectedHero && enemy && attackTargets.some((t) => t.x === x && t.y === y)) {
46931
49472
  const attackerId = str(selectedHero.id);
46932
- const defenderId = str(enemy.id);
49473
+ const defenderId = enemy.id;
46933
49474
  onBattleEncounter?.(attackerId, defenderId);
46934
49475
  if (battleEncounterEvent) {
46935
49476
  eventBus.emit(`UI:${battleEncounterEvent}`, { attackerId, defenderId });
46936
49477
  }
46937
49478
  }
46938
- }, [hexes, heroes, selectedHero, validMoves, attackTargets, startMoveAnimation, onHeroMove, onFeatureEnter, onBattleEncounter, eventBus, tileClickEvent, heroMoveEvent, featureEnterEvent, battleEncounterEvent]);
49479
+ }, [tiles, gameUnits, selectedHero, validMoves, attackTargets, startMoveAnimation, onHeroMove, onFeatureEnter, onBattleEncounter, eventBus, tileClickEvent, heroMoveEvent, featureEnterEvent, battleEncounterEvent]);
46939
49480
  const handleUnitClick = React82.useCallback((unitId) => {
46940
- const hero = heroes.find((h) => str(h.id) === unitId);
46941
- if (hero && (heroOwner(hero) === "player" || allowMoveAllHeroes)) {
49481
+ const unit = gameUnits.find((u) => u.id === unitId);
49482
+ if (unit && (str(unit.team) === "player" || allowMoveAllHeroes)) {
46942
49483
  onHeroSelect?.(unitId);
46943
49484
  if (heroSelectEvent) {
46944
49485
  eventBus.emit(`UI:${heroSelectEvent}`, { heroId: unitId });
46945
49486
  }
46946
49487
  }
46947
- }, [heroes, onHeroSelect, allowMoveAllHeroes, eventBus, heroSelectEvent]);
49488
+ }, [gameUnits, onHeroSelect, allowMoveAllHeroes, eventBus, heroSelectEvent]);
46948
49489
  const selectHero = React82.useCallback((id) => {
46949
49490
  onHeroSelect?.(id);
46950
49491
  if (heroSelectEvent) {
@@ -47048,48 +49589,48 @@ function WorldMapTemplate({
47048
49589
  }
47049
49590
  );
47050
49591
  }
47051
- var CDN3, DEFAULT_WORLDMAP_TILES, DEFAULT_WORLDMAP_UNITS, DEFAULT_WORLDMAP_FEATURES, DEFAULT_WORLDMAP_MANIFEST;
49592
+ var CDN7, DEFAULT_WORLDMAP_TILES, DEFAULT_WORLDMAP_UNITS, DEFAULT_WORLDMAP_FEATURES, DEFAULT_WORLDMAP_MANIFEST;
47052
49593
  var init_WorldMapTemplate = __esm({
47053
49594
  "components/game/templates/WorldMapTemplate.tsx"() {
47054
49595
  init_WorldMapBoard();
47055
- CDN3 = "https://almadar-kflow-assets.web.app/shared";
49596
+ CDN7 = "https://almadar-kflow-assets.web.app/shared";
47056
49597
  DEFAULT_WORLDMAP_TILES = [
47057
- { x: 0, y: 0, terrain: "mountain", passable: false, terrainSprite: `${CDN3}/isometric-blocks/PNG/Platformer tiles/platformerTile_13.png` },
47058
- { x: 1, y: 0, terrain: "mountain", passable: false, terrainSprite: `${CDN3}/isometric-blocks/PNG/Platformer tiles/platformerTile_13.png` },
47059
- { x: 2, y: 0, terrain: "water", passable: false, terrainSprite: `${CDN3}/isometric-blocks/PNG/Platformer tiles/platformerTile_11.png` },
47060
- { x: 3, y: 0, terrain: "mountain", passable: false, terrainSprite: `${CDN3}/isometric-blocks/PNG/Platformer tiles/platformerTile_13.png` },
47061
- { x: 4, y: 0, terrain: "mountain", passable: false, terrainSprite: `${CDN3}/isometric-blocks/PNG/Platformer tiles/platformerTile_13.png` },
47062
- { x: 0, y: 1, terrain: "mountain", passable: false, terrainSprite: `${CDN3}/isometric-blocks/PNG/Platformer tiles/platformerTile_13.png` },
47063
- { x: 1, y: 1, terrain: "grass", passable: true, terrainSprite: `${CDN3}/isometric-blocks/PNG/Platformer tiles/platformerTile_01.png` },
47064
- { x: 2, y: 1, terrain: "grass", passable: true, terrainSprite: `${CDN3}/isometric-blocks/PNG/Platformer tiles/platformerTile_01.png` },
47065
- { x: 3, y: 1, terrain: "grass", passable: true, terrainSprite: `${CDN3}/isometric-blocks/PNG/Platformer tiles/platformerTile_01.png` },
47066
- { x: 4, y: 1, terrain: "water", passable: false, terrainSprite: `${CDN3}/isometric-blocks/PNG/Platformer tiles/platformerTile_11.png` },
47067
- { x: 0, y: 2, terrain: "water", passable: false, terrainSprite: `${CDN3}/isometric-blocks/PNG/Platformer tiles/platformerTile_11.png` },
47068
- { x: 1, y: 2, terrain: "grass", passable: true, terrainSprite: `${CDN3}/isometric-blocks/PNG/Platformer tiles/platformerTile_01.png` },
47069
- { x: 2, y: 2, terrain: "grass", passable: true, terrainSprite: `${CDN3}/isometric-blocks/PNG/Platformer tiles/platformerTile_03.png` },
47070
- { x: 3, y: 2, terrain: "grass", passable: true, terrainSprite: `${CDN3}/isometric-blocks/PNG/Platformer tiles/platformerTile_01.png` },
47071
- { x: 4, y: 2, terrain: "mountain", passable: false, terrainSprite: `${CDN3}/isometric-blocks/PNG/Platformer tiles/platformerTile_13.png` },
47072
- { x: 0, y: 3, terrain: "mountain", passable: false, terrainSprite: `${CDN3}/isometric-blocks/PNG/Platformer tiles/platformerTile_13.png` },
47073
- { x: 1, y: 3, terrain: "grass", passable: true, terrainSprite: `${CDN3}/isometric-blocks/PNG/Platformer tiles/platformerTile_01.png` },
47074
- { x: 2, y: 3, terrain: "grass", passable: true, terrainSprite: `${CDN3}/isometric-blocks/PNG/Platformer tiles/platformerTile_01.png` },
47075
- { x: 3, y: 3, terrain: "grass", passable: true, terrainSprite: `${CDN3}/isometric-blocks/PNG/Platformer tiles/platformerTile_03.png` },
47076
- { x: 4, y: 3, terrain: "mountain", passable: false, terrainSprite: `${CDN3}/isometric-blocks/PNG/Platformer tiles/platformerTile_13.png` },
47077
- { x: 0, y: 4, terrain: "mountain", passable: false, terrainSprite: `${CDN3}/isometric-blocks/PNG/Platformer tiles/platformerTile_13.png` },
47078
- { x: 1, y: 4, terrain: "water", passable: false, terrainSprite: `${CDN3}/isometric-blocks/PNG/Platformer tiles/platformerTile_11.png` },
47079
- { x: 2, y: 4, terrain: "grass", passable: true, terrainSprite: `${CDN3}/isometric-blocks/PNG/Platformer tiles/platformerTile_01.png` },
47080
- { x: 3, y: 4, terrain: "mountain", passable: false, terrainSprite: `${CDN3}/isometric-blocks/PNG/Platformer tiles/platformerTile_13.png` },
47081
- { x: 4, y: 4, terrain: "mountain", passable: false, terrainSprite: `${CDN3}/isometric-blocks/PNG/Platformer tiles/platformerTile_13.png` }
49598
+ { x: 0, y: 0, terrain: "mountain", passable: false, terrainSprite: `${CDN7}/isometric-blocks/PNG/Platformer tiles/platformerTile_13.png` },
49599
+ { x: 1, y: 0, terrain: "mountain", passable: false, terrainSprite: `${CDN7}/isometric-blocks/PNG/Platformer tiles/platformerTile_13.png` },
49600
+ { x: 2, y: 0, terrain: "water", passable: false, terrainSprite: `${CDN7}/isometric-blocks/PNG/Platformer tiles/platformerTile_11.png` },
49601
+ { x: 3, y: 0, terrain: "mountain", passable: false, terrainSprite: `${CDN7}/isometric-blocks/PNG/Platformer tiles/platformerTile_13.png` },
49602
+ { x: 4, y: 0, terrain: "mountain", passable: false, terrainSprite: `${CDN7}/isometric-blocks/PNG/Platformer tiles/platformerTile_13.png` },
49603
+ { x: 0, y: 1, terrain: "mountain", passable: false, terrainSprite: `${CDN7}/isometric-blocks/PNG/Platformer tiles/platformerTile_13.png` },
49604
+ { x: 1, y: 1, terrain: "grass", passable: true, terrainSprite: `${CDN7}/isometric-blocks/PNG/Platformer tiles/platformerTile_01.png` },
49605
+ { x: 2, y: 1, terrain: "grass", passable: true, terrainSprite: `${CDN7}/isometric-blocks/PNG/Platformer tiles/platformerTile_01.png` },
49606
+ { x: 3, y: 1, terrain: "grass", passable: true, terrainSprite: `${CDN7}/isometric-blocks/PNG/Platformer tiles/platformerTile_01.png` },
49607
+ { x: 4, y: 1, terrain: "water", passable: false, terrainSprite: `${CDN7}/isometric-blocks/PNG/Platformer tiles/platformerTile_11.png` },
49608
+ { x: 0, y: 2, terrain: "water", passable: false, terrainSprite: `${CDN7}/isometric-blocks/PNG/Platformer tiles/platformerTile_11.png` },
49609
+ { x: 1, y: 2, terrain: "grass", passable: true, terrainSprite: `${CDN7}/isometric-blocks/PNG/Platformer tiles/platformerTile_01.png` },
49610
+ { x: 2, y: 2, terrain: "grass", passable: true, terrainSprite: `${CDN7}/isometric-blocks/PNG/Platformer tiles/platformerTile_03.png` },
49611
+ { x: 3, y: 2, terrain: "grass", passable: true, terrainSprite: `${CDN7}/isometric-blocks/PNG/Platformer tiles/platformerTile_01.png` },
49612
+ { x: 4, y: 2, terrain: "mountain", passable: false, terrainSprite: `${CDN7}/isometric-blocks/PNG/Platformer tiles/platformerTile_13.png` },
49613
+ { x: 0, y: 3, terrain: "mountain", passable: false, terrainSprite: `${CDN7}/isometric-blocks/PNG/Platformer tiles/platformerTile_13.png` },
49614
+ { x: 1, y: 3, terrain: "grass", passable: true, terrainSprite: `${CDN7}/isometric-blocks/PNG/Platformer tiles/platformerTile_01.png` },
49615
+ { x: 2, y: 3, terrain: "grass", passable: true, terrainSprite: `${CDN7}/isometric-blocks/PNG/Platformer tiles/platformerTile_01.png` },
49616
+ { x: 3, y: 3, terrain: "grass", passable: true, terrainSprite: `${CDN7}/isometric-blocks/PNG/Platformer tiles/platformerTile_03.png` },
49617
+ { x: 4, y: 3, terrain: "mountain", passable: false, terrainSprite: `${CDN7}/isometric-blocks/PNG/Platformer tiles/platformerTile_13.png` },
49618
+ { x: 0, y: 4, terrain: "mountain", passable: false, terrainSprite: `${CDN7}/isometric-blocks/PNG/Platformer tiles/platformerTile_13.png` },
49619
+ { x: 1, y: 4, terrain: "water", passable: false, terrainSprite: `${CDN7}/isometric-blocks/PNG/Platformer tiles/platformerTile_11.png` },
49620
+ { x: 2, y: 4, terrain: "grass", passable: true, terrainSprite: `${CDN7}/isometric-blocks/PNG/Platformer tiles/platformerTile_01.png` },
49621
+ { x: 3, y: 4, terrain: "mountain", passable: false, terrainSprite: `${CDN7}/isometric-blocks/PNG/Platformer tiles/platformerTile_13.png` },
49622
+ { x: 4, y: 4, terrain: "mountain", passable: false, terrainSprite: `${CDN7}/isometric-blocks/PNG/Platformer tiles/platformerTile_13.png` }
47082
49623
  ];
47083
49624
  DEFAULT_WORLDMAP_UNITS = [
47084
- { id: "h1", position: { x: 1, y: 1 }, unitType: "hero", name: "Amir", team: "player", health: 10, maxHealth: 10, sprite: `${CDN3}/sprite-sheets/amir-sprite-sheet-se.png` },
47085
- { id: "h2", position: { x: 3, y: 3 }, unitType: "scout", name: "Archivist", team: "player", health: 10, maxHealth: 10, sprite: `${CDN3}/sprite-sheets/archivist-sprite-sheet-se.png` }
49625
+ { id: "h1", position: { x: 1, y: 1 }, unitType: "hero", name: "Amir", team: "player", health: 10, maxHealth: 10, sprite: `${CDN7}/sprite-sheets/amir-sprite-sheet-se.png` },
49626
+ { id: "h2", position: { x: 3, y: 3 }, unitType: "scout", name: "Archivist", team: "player", health: 10, maxHealth: 10, sprite: `${CDN7}/sprite-sheets/archivist-sprite-sheet-se.png` }
47086
49627
  ];
47087
49628
  DEFAULT_WORLDMAP_FEATURES = [
47088
- { id: "f1", x: 2, y: 2, type: "capital", sprite: `${CDN3}/scenes/world/capital.png` },
47089
- { id: "f2", x: 4, y: 2, type: "power_node", sprite: `${CDN3}/world-map/power_node.png` }
49629
+ { id: "f1", x: 2, y: 2, type: "capital", sprite: `${CDN7}/scenes/world/capital.png` },
49630
+ { id: "f2", x: 4, y: 2, type: "power_node", sprite: `${CDN7}/world-map/power_node.png` }
47090
49631
  ];
47091
49632
  DEFAULT_WORLDMAP_MANIFEST = {
47092
- baseUrl: CDN3,
49633
+ baseUrl: CDN7,
47093
49634
  terrains: {
47094
49635
  grass: "/isometric-blocks/PNG/Platformer tiles/platformerTile_01.png",
47095
49636
  water: "/isometric-blocks/PNG/Platformer tiles/platformerTile_11.png",
@@ -47134,7 +49675,7 @@ function lazyThree(name, loader) {
47134
49675
  ThreeWrapper.displayName = `Lazy(${name})`;
47135
49676
  return ThreeWrapper;
47136
49677
  }
47137
- var ThreeBoundary, FeatureRenderer, GameCanvas3D, GameCanvas3DBattleTemplate, GameCanvas3DCastleTemplate, GameCanvas3DWorldMapTemplate, COMPONENT_REGISTRY;
49678
+ var ThreeBoundary, FeatureRenderer, GameCanvas3D2, GameCanvas3DBattleTemplate, GameCanvas3DCastleTemplate, GameCanvas3DWorldMapTemplate, COMPONENT_REGISTRY;
47138
49679
  var init_component_registry_generated = __esm({
47139
49680
  "components/core/organisms/component-registry.generated.ts"() {
47140
49681
  init_AboutPageTemplate();
@@ -47244,6 +49785,7 @@ var init_component_registry_generated = __esm({
47244
49785
  init_FormSectionHeader();
47245
49786
  init_GameAudioProvider();
47246
49787
  init_GameAudioToggle();
49788
+ init_GameBoard3D();
47247
49789
  init_GameCanvas2D();
47248
49790
  init_GameHud();
47249
49791
  init_GameMenu();
@@ -47301,7 +49843,9 @@ var init_component_registry_generated = __esm({
47301
49843
  init_PageHeader();
47302
49844
  init_Pagination();
47303
49845
  init_PatternTile();
49846
+ init_PlatformerBoard();
47304
49847
  init_PlatformerCanvas();
49848
+ init_PlatformerTemplate();
47305
49849
  init_Popover();
47306
49850
  init_PositionedCanvas();
47307
49851
  init_PowerupSlots();
@@ -47325,6 +49869,8 @@ var init_component_registry_generated = __esm({
47325
49869
  init_ResourceBar();
47326
49870
  init_ResourceCounter();
47327
49871
  init_RichBlockEditor();
49872
+ init_RoguelikeBoard();
49873
+ init_RoguelikeTemplate();
47328
49874
  init_RuntimeDebugger2();
47329
49875
  init_ScaledDiagram();
47330
49876
  init_ScoreBoard();
@@ -47397,6 +49943,8 @@ var init_component_registry_generated = __esm({
47397
49943
  init_Toast();
47398
49944
  init_ToastSlot();
47399
49945
  init_Tooltip();
49946
+ init_TowerDefenseBoard();
49947
+ init_TowerDefenseTemplate();
47400
49948
  init_TraitFrame();
47401
49949
  init_TraitSlot();
47402
49950
  init_TrendIndicator();
@@ -47441,7 +49989,7 @@ var init_component_registry_generated = __esm({
47441
49989
  }
47442
49990
  };
47443
49991
  FeatureRenderer = lazyThree("FeatureRenderer", () => import('@almadar/ui/components/molecules/game/three'));
47444
- GameCanvas3D = lazyThree("GameCanvas3D", () => import('@almadar/ui/components/molecules/game/three'));
49992
+ GameCanvas3D2 = lazyThree("GameCanvas3D", () => import('@almadar/ui/components/molecules/game/three'));
47445
49993
  GameCanvas3DBattleTemplate = lazyThree("GameCanvas3DBattleTemplate", () => import('@almadar/ui/components/molecules/game/three'));
47446
49994
  GameCanvas3DCastleTemplate = lazyThree("GameCanvas3DCastleTemplate", () => import('@almadar/ui/components/molecules/game/three'));
47447
49995
  GameCanvas3DWorldMapTemplate = lazyThree("GameCanvas3DWorldMapTemplate", () => import('@almadar/ui/components/molecules/game/three'));
@@ -47560,8 +50108,9 @@ var init_component_registry_generated = __esm({
47560
50108
  "FormSectionHeader": FormSectionHeader,
47561
50109
  "GameAudioProvider": GameAudioProvider,
47562
50110
  "GameAudioToggle": GameAudioToggle,
50111
+ "GameBoard3D": GameBoard3D,
47563
50112
  "GameCanvas2D": GameCanvas2D,
47564
- "GameCanvas3D": GameCanvas3D,
50113
+ "GameCanvas3D": GameCanvas3D2,
47565
50114
  "GameCanvas3DBattleTemplate": GameCanvas3DBattleTemplate,
47566
50115
  "GameCanvas3DCastleTemplate": GameCanvas3DCastleTemplate,
47567
50116
  "GameCanvas3DWorldMapTemplate": GameCanvas3DWorldMapTemplate,
@@ -47623,7 +50172,9 @@ var init_component_registry_generated = __esm({
47623
50172
  "PageHeader": PageHeader,
47624
50173
  "Pagination": Pagination,
47625
50174
  "PatternTile": PatternTile,
50175
+ "PlatformerBoard": PlatformerBoard,
47626
50176
  "PlatformerCanvas": PlatformerCanvas,
50177
+ "PlatformerTemplate": PlatformerTemplate,
47627
50178
  "Popover": Popover,
47628
50179
  "PositionedCanvas": PositionedCanvas,
47629
50180
  "PowerupSlots": PowerupSlots,
@@ -47647,6 +50198,8 @@ var init_component_registry_generated = __esm({
47647
50198
  "ResourceBar": ResourceBar,
47648
50199
  "ResourceCounter": ResourceCounter,
47649
50200
  "RichBlockEditor": RichBlockEditor,
50201
+ "RoguelikeBoard": RoguelikeBoard,
50202
+ "RoguelikeTemplate": RoguelikeTemplate,
47650
50203
  "RuntimeDebugger": RuntimeDebugger,
47651
50204
  "ScaledDiagram": ScaledDiagram,
47652
50205
  "ScoreBoard": ScoreBoard,
@@ -47722,6 +50275,8 @@ var init_component_registry_generated = __esm({
47722
50275
  "Toast": Toast,
47723
50276
  "ToastSlot": ToastSlot,
47724
50277
  "Tooltip": Tooltip,
50278
+ "TowerDefenseBoard": TowerDefenseBoard,
50279
+ "TowerDefenseTemplate": TowerDefenseTemplate,
47725
50280
  "TraitFrame": TraitFrame,
47726
50281
  "TraitSlot": TraitSlot,
47727
50282
  "TrendIndicator": TrendIndicator,
@@ -49618,7 +52173,7 @@ function clearSchemaCache() {
49618
52173
 
49619
52174
  // runtime/index.ts
49620
52175
  init_EntitySchemaContext();
49621
- var log10 = logger.createLogger("almadar:ui:trait-provider");
52176
+ var log12 = logger.createLogger("almadar:ui:trait-provider");
49622
52177
  var TraitContext = React82.createContext(null);
49623
52178
  function TraitProvider({
49624
52179
  traits: traitBindings,
@@ -49635,7 +52190,7 @@ function TraitProvider({
49635
52190
  currentState: stateName,
49636
52191
  availableEvents: trait.transitions.filter((t) => t.from === stateName).map((t) => t.event),
49637
52192
  dispatch: (eventKey, payload) => {
49638
- log10.debug("Dispatch", () => ({
52193
+ log12.debug("Dispatch", () => ({
49639
52194
  trait: trait.name,
49640
52195
  event: eventKey,
49641
52196
  payloadKeys: payload ? Object.keys(payload) : []
@@ -50219,7 +52774,7 @@ OrbitalThemeProvider.displayName = "OrbitalThemeProvider";
50219
52774
  init_navigation();
50220
52775
  init_verificationRegistry();
50221
52776
  var PERF_NAMESPACE = "almadar:perf:canvas";
50222
- var log11 = logger.createLogger(PERF_NAMESPACE);
52777
+ var log13 = logger.createLogger(PERF_NAMESPACE);
50223
52778
  var RING_SIZE = 50;
50224
52779
  var ring = [];
50225
52780
  var writeIdx = 0;
@@ -50274,7 +52829,7 @@ function perfEnd(name, startToken, detail) {
50274
52829
  }
50275
52830
  }
50276
52831
  push({ name, durationMs, ts: endTs, detail });
50277
- log11.debug(name, () => ({ durationMs, ...detail ?? {} }));
52832
+ log13.debug(name, () => ({ durationMs, ...detail ?? {} }));
50278
52833
  }
50279
52834
  function perfTime(name, fn, detail) {
50280
52835
  const t = perfStart(name);
@@ -50292,7 +52847,7 @@ var profilerOnRender = (id, phase, actualDuration, baseDuration, _startTime, com
50292
52847
  ts: commitTime,
50293
52848
  detail: { baseDuration }
50294
52849
  });
50295
- log11.debug(`profiler:${id}:${phase}`, () => ({ actualDuration, baseDuration }));
52850
+ log13.debug(`profiler:${id}:${phase}`, () => ({ actualDuration, baseDuration }));
50296
52851
  };
50297
52852
  function getPerfSnapshot() {
50298
52853
  if (ring.length < RING_SIZE) return ring.slice();