@almadar/ui 5.46.0 → 5.48.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.
@@ -7367,6 +7367,7 @@ var init_Sprite = __esm({
7367
7367
  }
7368
7368
  });
7369
7369
  function StateIndicator({
7370
+ assetUrl = DEFAULT_ASSET_URL,
7370
7371
  state = "idle",
7371
7372
  label,
7372
7373
  size = "md",
@@ -7389,18 +7390,29 @@ function StateIndicator({
7389
7390
  className
7390
7391
  ),
7391
7392
  children: [
7392
- /* @__PURE__ */ jsx(Box, { as: "span", children: typeof config.icon === "string" ? /^[a-zA-Z0-9-]+$/.test(config.icon) ? /* @__PURE__ */ jsx(Icon, { name: config.icon }) : config.icon : /* @__PURE__ */ jsx(Icon, { icon: config.icon }) }),
7393
+ /* @__PURE__ */ jsx(Box, { as: "span", children: assetUrl ? /* @__PURE__ */ jsx(
7394
+ "img",
7395
+ {
7396
+ src: assetUrl,
7397
+ alt: displayLabel,
7398
+ width: 16,
7399
+ height: 16,
7400
+ style: { imageRendering: "pixelated", objectFit: "contain" },
7401
+ className: "flex-shrink-0"
7402
+ }
7403
+ ) : typeof config.icon === "string" ? /^[a-zA-Z0-9-]+$/.test(config.icon) ? /* @__PURE__ */ jsx(Icon, { name: config.icon }) : config.icon : /* @__PURE__ */ jsx(Icon, { icon: config.icon }) }),
7393
7404
  /* @__PURE__ */ jsx(Box, { as: "span", children: displayLabel })
7394
7405
  ]
7395
7406
  }
7396
7407
  );
7397
7408
  }
7398
- var DEFAULT_STATE_STYLES, DEFAULT_STYLE, SIZE_CLASSES;
7409
+ var DEFAULT_ASSET_URL, DEFAULT_STATE_STYLES, DEFAULT_STYLE, SIZE_CLASSES;
7399
7410
  var init_StateIndicator = __esm({
7400
7411
  "components/game/atoms/StateIndicator.tsx"() {
7401
7412
  init_Box();
7402
7413
  init_Icon();
7403
7414
  init_cn();
7415
+ DEFAULT_ASSET_URL = "https://almadar-kflow-assets.web.app/shared/isometric-dungeon/Isometric/chestClosed_E.png";
7404
7416
  DEFAULT_STATE_STYLES = {
7405
7417
  idle: { icon: "\u23F8", bgClass: "bg-muted" },
7406
7418
  active: { icon: "\u25B6", bgClass: "bg-success" },
@@ -7475,6 +7487,7 @@ var init_TimerDisplay = __esm({
7475
7487
  }
7476
7488
  });
7477
7489
  function ResourceCounter({
7490
+ assetUrl = DEFAULT_ASSET_URL2,
7478
7491
  icon,
7479
7492
  label = "Gold",
7480
7493
  value = 250,
@@ -7494,7 +7507,17 @@ function ResourceCounter({
7494
7507
  className
7495
7508
  ),
7496
7509
  children: [
7497
- icon && /* @__PURE__ */ jsx("span", { className: cn("flex-shrink-0", sizes.icon), children: typeof icon === "string" ? /* @__PURE__ */ jsx(Icon, { name: icon }) : /* @__PURE__ */ jsx(Icon, { icon }) }),
7510
+ assetUrl ? /* @__PURE__ */ jsx(
7511
+ "img",
7512
+ {
7513
+ src: assetUrl,
7514
+ alt: label,
7515
+ width: sizes.img,
7516
+ height: sizes.img,
7517
+ style: { imageRendering: "pixelated", objectFit: "contain" },
7518
+ className: "flex-shrink-0"
7519
+ }
7520
+ ) : icon ? /* @__PURE__ */ jsx("span", { className: cn("flex-shrink-0", sizes.icon), children: typeof icon === "string" ? /* @__PURE__ */ jsx(Icon, { name: icon }) : /* @__PURE__ */ jsx(Icon, { icon }) }) : null,
7498
7521
  /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: label }),
7499
7522
  /* @__PURE__ */ jsxs("span", { className: cn("font-bold tabular-nums", color && (color in colorTokenClasses2 ? colorTokenClasses2[color] : color)), children: [
7500
7523
  value,
@@ -7507,7 +7530,7 @@ function ResourceCounter({
7507
7530
  }
7508
7531
  );
7509
7532
  }
7510
- var colorTokenClasses2, sizeMap5;
7533
+ var colorTokenClasses2, DEFAULT_ASSET_URL2, sizeMap5;
7511
7534
  var init_ResourceCounter = __esm({
7512
7535
  "components/game/atoms/ResourceCounter.tsx"() {
7513
7536
  init_cn();
@@ -7520,16 +7543,17 @@ var init_ResourceCounter = __esm({
7520
7543
  error: "text-error",
7521
7544
  muted: "text-muted-foreground"
7522
7545
  };
7546
+ DEFAULT_ASSET_URL2 = "https://almadar-kflow-assets.web.app/shared/world-map/gold_mine.png";
7523
7547
  sizeMap5 = {
7524
- sm: { wrapper: "text-xs gap-1 px-1.5 py-0.5", icon: "text-sm" },
7525
- md: { wrapper: "text-sm gap-1.5 px-2 py-1", icon: "text-base" },
7526
- lg: { wrapper: "text-base gap-2 px-3 py-1.5", icon: "text-lg" }
7548
+ sm: { wrapper: "text-xs gap-1 px-1.5 py-0.5", icon: "text-sm", img: 16 },
7549
+ md: { wrapper: "text-sm gap-1.5 px-2 py-1", icon: "text-base", img: 20 },
7550
+ lg: { wrapper: "text-base gap-2 px-3 py-1.5", icon: "text-lg", img: 28 }
7527
7551
  };
7528
7552
  ResourceCounter.displayName = "ResourceCounter";
7529
7553
  }
7530
7554
  });
7531
7555
  function ItemSlot({
7532
- assetUrl = DEFAULT_ASSET_URL,
7556
+ assetUrl = DEFAULT_ASSET_URL3,
7533
7557
  icon = "sword",
7534
7558
  label = "Iron Sword",
7535
7559
  quantity,
@@ -7588,7 +7612,7 @@ function ItemSlot({
7588
7612
  }
7589
7613
  );
7590
7614
  }
7591
- var sizeMap6, rarityBorderMap, rarityGlowMap, DEFAULT_ASSET_URL, assetSizeMap;
7615
+ var sizeMap6, rarityBorderMap, rarityGlowMap, DEFAULT_ASSET_URL3, assetSizeMap;
7592
7616
  var init_ItemSlot = __esm({
7593
7617
  "components/game/atoms/ItemSlot.tsx"() {
7594
7618
  "use client";
@@ -7613,7 +7637,7 @@ var init_ItemSlot = __esm({
7613
7637
  epic: "shadow-lg",
7614
7638
  legendary: "shadow-lg"
7615
7639
  };
7616
- DEFAULT_ASSET_URL = "https://almadar-kflow-assets.web.app/shared/isometric-dungeon/Isometric/chestClosed_E.png";
7640
+ DEFAULT_ASSET_URL3 = "https://almadar-kflow-assets.web.app/shared/isometric-dungeon/Isometric/chestClosed_E.png";
7617
7641
  assetSizeMap = {
7618
7642
  sm: 28,
7619
7643
  md: 40,
@@ -7686,6 +7710,7 @@ function getComboScale(combo) {
7686
7710
  return "";
7687
7711
  }
7688
7712
  function ComboCounter({
7713
+ assetUrl = DEFAULT_ASSET_URL4,
7689
7714
  combo = 5,
7690
7715
  multiplier,
7691
7716
  streak,
@@ -7705,6 +7730,17 @@ function ComboCounter({
7705
7730
  className
7706
7731
  ),
7707
7732
  children: [
7733
+ assetUrl && /* @__PURE__ */ jsx(
7734
+ "img",
7735
+ {
7736
+ src: assetUrl,
7737
+ alt: "combo",
7738
+ width: 24,
7739
+ height: 24,
7740
+ style: { imageRendering: "pixelated", objectFit: "contain" },
7741
+ className: "flex-shrink-0 mb-0.5"
7742
+ }
7743
+ ),
7708
7744
  /* @__PURE__ */ jsx("span", { className: cn("font-black tabular-nums leading-none", sizes.combo, getComboIntensity(combo)), children: combo }),
7709
7745
  /* @__PURE__ */ jsx("span", { className: cn("font-bold uppercase tracking-wider text-muted-foreground", sizes.label), children: "combo" }),
7710
7746
  multiplier != null && multiplier > 1 && /* @__PURE__ */ jsxs("span", { className: cn("font-bold text-warning tabular-nums", sizes.multiplier), children: [
@@ -7719,10 +7755,11 @@ function ComboCounter({
7719
7755
  }
7720
7756
  );
7721
7757
  }
7722
- var sizeMap8;
7758
+ var DEFAULT_ASSET_URL4, sizeMap8;
7723
7759
  var init_ComboCounter = __esm({
7724
7760
  "components/game/atoms/ComboCounter.tsx"() {
7725
7761
  init_cn();
7762
+ DEFAULT_ASSET_URL4 = "https://almadar-kflow-assets.web.app/shared/effects/flash/flash00.png";
7726
7763
  sizeMap8 = {
7727
7764
  sm: { combo: "text-lg", label: "text-xs", multiplier: "text-xs" },
7728
7765
  md: { combo: "text-2xl", label: "text-xs", multiplier: "text-sm" },
@@ -7816,6 +7853,7 @@ var init_XPBar = __esm({
7816
7853
  }
7817
7854
  });
7818
7855
  function WaypointMarker({
7856
+ assetUrl = DEFAULT_ASSET_URL5,
7819
7857
  label,
7820
7858
  icon,
7821
7859
  active = true,
@@ -7854,7 +7892,17 @@ function WaypointMarker({
7854
7892
  active && !completed && "bg-info text-foreground",
7855
7893
  !active && !completed && "bg-muted"
7856
7894
  ),
7857
- children: completed ? checkIcon : icon && (typeof icon === "string" ? /* @__PURE__ */ jsx(Icon, { name: icon }) : /* @__PURE__ */ jsx(Icon, { icon }))
7895
+ children: completed ? checkIcon : assetUrl ? /* @__PURE__ */ jsx(
7896
+ "img",
7897
+ {
7898
+ src: assetUrl,
7899
+ alt: label,
7900
+ width: sizes.img,
7901
+ height: sizes.img,
7902
+ style: { imageRendering: "pixelated", objectFit: "contain" },
7903
+ className: "flex-shrink-0"
7904
+ }
7905
+ ) : icon ? typeof icon === "string" ? /* @__PURE__ */ jsx(Icon, { name: icon }) : /* @__PURE__ */ jsx(Icon, { icon }) : null
7858
7906
  }
7859
7907
  )
7860
7908
  ] }),
@@ -7871,15 +7919,16 @@ function WaypointMarker({
7871
7919
  )
7872
7920
  ] });
7873
7921
  }
7874
- var sizeMap10, checkIcon;
7922
+ var DEFAULT_ASSET_URL5, sizeMap10, checkIcon;
7875
7923
  var init_WaypointMarker = __esm({
7876
7924
  "components/game/atoms/WaypointMarker.tsx"() {
7877
7925
  init_cn();
7878
7926
  init_Icon();
7927
+ DEFAULT_ASSET_URL5 = "https://almadar-kflow-assets.web.app/shared/world-map/battle_marker.png";
7879
7928
  sizeMap10 = {
7880
- sm: { dot: "w-4 h-4", ring: "w-6 h-6", label: "text-xs mt-1" },
7881
- md: { dot: "w-6 h-6", ring: "w-8 h-8", label: "text-sm mt-1.5" },
7882
- lg: { dot: "w-8 h-8", ring: "w-10 h-10", label: "text-base mt-2" }
7929
+ sm: { dot: "w-4 h-4", ring: "w-6 h-6", label: "text-xs mt-1", img: 12 },
7930
+ md: { dot: "w-6 h-6", ring: "w-8 h-8", label: "text-sm mt-1.5", img: 18 },
7931
+ lg: { dot: "w-8 h-8", ring: "w-10 h-10", label: "text-base mt-2", img: 24 }
7883
7932
  };
7884
7933
  checkIcon = /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 3, className: "w-3/5 h-3/5", children: /* @__PURE__ */ jsx("path", { d: "M5 13l4 4L19 7", strokeLinecap: "round", strokeLinejoin: "round" }) });
7885
7934
  WaypointMarker.displayName = "WaypointMarker";
@@ -7892,6 +7941,7 @@ function formatDuration(seconds) {
7892
7941
  return `${Math.round(seconds)}s`;
7893
7942
  }
7894
7943
  function StatusEffect({
7944
+ assetUrl = DEFAULT_ASSET_URL6,
7895
7945
  icon = "shield",
7896
7946
  label = "Shield",
7897
7947
  duration = 30,
@@ -7912,7 +7962,17 @@ function StatusEffect({
7912
7962
  ),
7913
7963
  title: label,
7914
7964
  children: [
7915
- /* @__PURE__ */ jsx("span", { className: cn("flex items-center justify-center", sizes.icon), children: typeof icon === "string" ? /* @__PURE__ */ jsx(Icon, { name: icon, size: "sm" }) : /* @__PURE__ */ jsx(Icon, { icon, size: "sm" }) }),
7965
+ /* @__PURE__ */ jsx("span", { className: cn("flex items-center justify-center", sizes.icon), children: assetUrl ? /* @__PURE__ */ jsx(
7966
+ "img",
7967
+ {
7968
+ src: assetUrl,
7969
+ alt: label,
7970
+ width: sizes.img,
7971
+ height: sizes.img,
7972
+ style: { imageRendering: "pixelated", objectFit: "contain" },
7973
+ className: "flex-shrink-0"
7974
+ }
7975
+ ) : typeof icon === "string" ? /* @__PURE__ */ jsx(Icon, { name: icon, size: "sm" }) : /* @__PURE__ */ jsx(Icon, { icon, size: "sm" }) }),
7916
7976
  duration !== void 0 && /* @__PURE__ */ jsx(
7917
7977
  "span",
7918
7978
  {
@@ -7939,15 +7999,16 @@ function StatusEffect({
7939
7999
  label && /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground mt-0.5 text-center whitespace-nowrap", children: label })
7940
8000
  ] });
7941
8001
  }
7942
- var sizeMap11, variantStyles7;
8002
+ var DEFAULT_ASSET_URL6, sizeMap11, variantStyles7;
7943
8003
  var init_StatusEffect = __esm({
7944
8004
  "components/game/atoms/StatusEffect.tsx"() {
7945
8005
  init_cn();
7946
8006
  init_Icon();
8007
+ DEFAULT_ASSET_URL6 = "https://almadar-kflow-assets.web.app/shared/effects/particles/flame_01.png";
7947
8008
  sizeMap11 = {
7948
- sm: { container: "w-8 h-8", icon: "text-sm", badge: "text-xs -top-1 -right-1 w-4 h-4", timer: "text-[9px]" },
7949
- md: { container: "w-10 h-10", icon: "text-base", badge: "text-xs -top-1 -right-1 w-5 h-5", timer: "text-xs" },
7950
- 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" }
8009
+ 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 },
8010
+ 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 },
8011
+ 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 }
7951
8012
  };
7952
8013
  variantStyles7 = {
7953
8014
  buff: "border-success bg-success/20",
@@ -11860,15 +11921,15 @@ function BattleBoard({
11860
11921
  const board = boardEntity(entity) ?? {};
11861
11922
  const tiles = propTiles ?? (Array.isArray(board.tiles) ? board.tiles : []);
11862
11923
  const features = propFeatures ?? (Array.isArray(board.features) ? board.features : []);
11863
- const boardWidth = num(board.boardWidth, 8);
11864
- const boardHeight = num(board.boardHeight, 6);
11924
+ const boardWidth = num(board.gridWidth ?? board.boardWidth, 8);
11925
+ const boardHeight = num(board.gridHeight ?? board.boardHeight, 6);
11865
11926
  const assetManifest = propAssetManifest ?? board.assetManifest;
11866
11927
  const backgroundImage = board.backgroundImage;
11867
11928
  const units = rows(board.units);
11868
11929
  const selectedUnitId = board.selectedUnitId ?? null;
11869
11930
  const currentPhase = str(board.phase) || "observation";
11870
11931
  const currentTurn = num(board.turn, 1);
11871
- const gameResult = board.gameResult ?? null;
11932
+ const gameResult = board.result ?? null;
11872
11933
  const eventBus = useEventBus();
11873
11934
  const { t } = useTranslate();
11874
11935
  const [hoveredTile, setHoveredTile] = useState(null);
@@ -11889,7 +11950,7 @@ function BattleBoard({
11889
11950
  const validMoves = useMemo(() => {
11890
11951
  if (!selectedUnit || currentPhase !== "movement") return [];
11891
11952
  const moves = [];
11892
- const range = num(selectedUnit.movement);
11953
+ const range = num(board.movementRange, 2);
11893
11954
  const origin = unitPosition(selectedUnit);
11894
11955
  for (let dy = -range; dy <= range; dy++) {
11895
11956
  for (let dx = -range; dx <= range; dx++) {
@@ -20174,15 +20235,24 @@ function CastleBoard({
20174
20235
  featureClickEvent,
20175
20236
  unitClickEvent,
20176
20237
  tileClickEvent,
20238
+ playAgainEvent,
20177
20239
  className
20178
20240
  }) {
20179
20241
  const eventBus = useEventBus();
20242
+ const { t } = useTranslate();
20180
20243
  const resolved = boardEntity(entity);
20181
20244
  const tiles = propTiles ?? (Array.isArray(resolved?.tiles) ? resolved.tiles : []);
20182
20245
  const features = propFeatures ?? (Array.isArray(resolved?.features) ? resolved.features : []);
20183
20246
  const units = propUnits ?? (Array.isArray(resolved?.units) ? resolved.units : []);
20184
20247
  const assetManifest = propAssetManifest ?? resolved?.assetManifest;
20185
20248
  const backgroundImage = resolved?.backgroundImage;
20249
+ const gold = num(resolved?.gold);
20250
+ const health = num(resolved?.health);
20251
+ const maxHealth = num(resolved?.maxHealth);
20252
+ const wave = num(resolved?.wave);
20253
+ const tickCount = num(resolved?.tickCount);
20254
+ const buildings = rows(resolved?.buildings);
20255
+ const result = str(resolved?.result) || "none";
20186
20256
  const [hoveredTile, setHoveredTile] = useState(null);
20187
20257
  const [selectedFeature, setSelectedFeature] = useState(null);
20188
20258
  const hoveredFeature = useMemo(() => {
@@ -20195,7 +20265,7 @@ function CastleBoard({
20195
20265
  (u) => u.position?.x === hoveredTile.x && u.position?.y === hoveredTile.y
20196
20266
  ) ?? null;
20197
20267
  }, [hoveredTile, units]);
20198
- const maxY = Math.max(...tiles.map((t) => t.y), 0);
20268
+ const maxY = Math.max(...tiles.map((t2) => t2.y), 0);
20199
20269
  const baseOffsetX = (maxY + 1) * (TILE_WIDTH * scale / 2);
20200
20270
  const tileToScreen = useCallback(
20201
20271
  (tx, ty) => isoToScreen(tx, ty, scale, baseOffsetX),
@@ -20230,6 +20300,11 @@ function CastleBoard({
20230
20300
  }
20231
20301
  }, [units, onUnitClick, unitClickEvent, eventBus]);
20232
20302
  const clearSelection = useCallback(() => setSelectedFeature(null), []);
20303
+ const handlePlayAgain = useCallback(() => {
20304
+ if (playAgainEvent) {
20305
+ eventBus.emit(`UI:${playAgainEvent}`, {});
20306
+ }
20307
+ }, [playAgainEvent, eventBus]);
20233
20308
  const ctx = useMemo(
20234
20309
  () => ({
20235
20310
  hoveredTile,
@@ -20238,11 +20313,18 @@ function CastleBoard({
20238
20313
  selectedFeature,
20239
20314
  clearSelection,
20240
20315
  tileToScreen,
20241
- scale
20316
+ scale,
20317
+ gold,
20318
+ health,
20319
+ maxHealth,
20320
+ wave,
20321
+ tickCount,
20322
+ buildings,
20323
+ result
20242
20324
  }),
20243
- [hoveredTile, hoveredFeature, hoveredUnit, selectedFeature, clearSelection, tileToScreen, scale]
20325
+ [hoveredTile, hoveredFeature, hoveredUnit, selectedFeature, clearSelection, tileToScreen, scale, gold, health, maxHealth, wave, tickCount, buildings, result]
20244
20326
  );
20245
- return /* @__PURE__ */ jsxs("div", { className: cn("castle-board min-h-screen flex flex-col bg-background", className), children: [
20327
+ return /* @__PURE__ */ jsxs("div", { className: cn("castle-board relative min-h-screen flex flex-col bg-background", className), children: [
20246
20328
  header && header(ctx),
20247
20329
  /* @__PURE__ */ jsxs("div", { className: "flex flex-1 overflow-hidden", children: [
20248
20330
  /* @__PURE__ */ jsxs("div", { className: "flex-1 overflow-auto p-4 relative", children: [
@@ -20267,7 +20349,29 @@ function CastleBoard({
20267
20349
  ] }),
20268
20350
  sidePanel && /* @__PURE__ */ jsx("div", { className: "w-96 shrink-0 border-l border-border bg-surface overflow-y-auto", children: sidePanel(ctx) })
20269
20351
  ] }),
20270
- footer && footer(ctx)
20352
+ footer && footer(ctx),
20353
+ result !== "none" && /* @__PURE__ */ jsx(Box, { className: "absolute inset-0 z-50 flex items-center justify-center bg-background/70 backdrop-blur-sm rounded-container", children: /* @__PURE__ */ jsxs(VStack, { className: "text-center p-8", gap: "lg", children: [
20354
+ /* @__PURE__ */ jsx(
20355
+ Typography,
20356
+ {
20357
+ variant: "h2",
20358
+ className: cn(
20359
+ "text-4xl font-black tracking-widest uppercase",
20360
+ result === "victory" ? "text-warning" : "text-error"
20361
+ ),
20362
+ children: result === "victory" ? t("battle.victory") : t("battle.defeat")
20363
+ }
20364
+ ),
20365
+ /* @__PURE__ */ jsx(
20366
+ Button,
20367
+ {
20368
+ variant: "primary",
20369
+ className: "px-8 py-3 font-semibold",
20370
+ onClick: handlePlayAgain,
20371
+ children: t("battle.playAgain")
20372
+ }
20373
+ )
20374
+ ] }) })
20271
20375
  ] });
20272
20376
  }
20273
20377
  var init_CastleBoard = __esm({
@@ -20275,6 +20379,10 @@ var init_CastleBoard = __esm({
20275
20379
  "use client";
20276
20380
  init_cn();
20277
20381
  init_useEventBus();
20382
+ init_Box();
20383
+ init_Button();
20384
+ init_Typography();
20385
+ init_Stack();
20278
20386
  init_IsometricCanvas();
20279
20387
  init_boardEntity();
20280
20388
  init_isometric();
@@ -21151,22 +21259,16 @@ function ClassifierBoard({
21151
21259
  const { emit } = useEventBus();
21152
21260
  const { t } = useTranslate();
21153
21261
  const resolved = boardEntity(entity);
21154
- const [localAssignments, setLocalAssignments] = useState({});
21155
21262
  const [headerError, setHeaderError] = useState(false);
21156
- const [localSubmitted, setLocalSubmitted] = useState(false);
21157
- const [localAttempts, setLocalAttempts] = useState(0);
21158
- const [showHint, setShowHint] = useState(false);
21159
21263
  const items = Array.isArray(resolved?.items) ? resolved.items : [];
21160
21264
  const categories = Array.isArray(resolved?.categories) ? resolved.categories : [];
21161
- const entityResult = str(resolved?.result);
21162
- const entityDrivesResult = entityResult.length > 0;
21163
- const entityHasAssignments = items.some((item) => item.assignedCategory != null);
21164
- const assignments = entityHasAssignments ? items.reduce((acc, item) => {
21165
- if (item.assignedCategory != null) acc[item.id] = item.assignedCategory;
21265
+ const result = str(resolved?.result);
21266
+ const submitted = result === "win";
21267
+ const attempts = num(resolved?.attempts);
21268
+ const assignments = items.reduce((acc, item) => {
21269
+ if (item.assignedCategory != null && item.assignedCategory !== "") acc[item.id] = item.assignedCategory;
21166
21270
  return acc;
21167
- }, {}) : localAssignments;
21168
- const attempts = entityDrivesResult ? num(resolved?.attempts) : localAttempts;
21169
- const submitted = entityDrivesResult || localSubmitted;
21271
+ }, {});
21170
21272
  const unassignedItems = items.filter((item) => !assignments[item.id]);
21171
21273
  const allAssigned = items.length > 0 && Object.keys(assignments).length === items.length;
21172
21274
  const results = submitted ? items.map((item) => ({
@@ -21174,54 +21276,25 @@ function ClassifierBoard({
21174
21276
  assigned: assignments[item.id],
21175
21277
  correct: assignments[item.id] === item.correctCategory
21176
21278
  })) : [];
21177
- const allCorrect = entityDrivesResult ? entityResult === "success" : results.length > 0 && results.every((r) => r.correct);
21279
+ const allCorrect = result === "win";
21178
21280
  const correctCount = results.filter((r) => r.correct).length;
21281
+ const showHint = attempts >= 2 && Boolean(str(resolved?.hint));
21179
21282
  const handleAssign = (itemId, categoryId) => {
21180
21283
  if (submitted) return;
21181
- if (assignEvent) {
21182
- emit(`UI:${assignEvent}`, { itemId, categoryId });
21183
- }
21184
- if (!entityHasAssignments) {
21185
- setLocalAssignments((prev) => ({ ...prev, [itemId]: categoryId }));
21186
- }
21284
+ if (assignEvent) emit(`UI:${assignEvent}`, { itemId, categoryId });
21187
21285
  };
21188
21286
  const handleUnassign = (itemId) => {
21189
21287
  if (submitted) return;
21190
- if (!entityHasAssignments) {
21191
- setLocalAssignments((prev) => {
21192
- const next = { ...prev };
21193
- delete next[itemId];
21194
- return next;
21195
- });
21196
- }
21288
+ if (assignEvent) emit(`UI:${assignEvent}`, { itemId, categoryId: "" });
21197
21289
  };
21198
- const handleSubmit = useCallback(() => {
21199
- if (checkEvent) {
21200
- emit(`UI:${checkEvent}`, {});
21201
- }
21202
- if (!entityDrivesResult) {
21203
- setLocalSubmitted(true);
21204
- setLocalAttempts((a) => a + 1);
21205
- const correct = items.every((item) => assignments[item.id] === item.correctCategory);
21206
- if (correct) {
21207
- emit(`UI:${completeEvent}`, { success: true, attempts: attempts + 1 });
21208
- }
21209
- }
21210
- }, [checkEvent, entityDrivesResult, items, assignments, attempts, completeEvent, emit]);
21211
- const handleReset = () => {
21212
- if (!entityDrivesResult) setLocalSubmitted(false);
21213
- if (attempts >= 2 && str(resolved?.hint)) {
21214
- setShowHint(true);
21290
+ const handleSubmit = () => {
21291
+ if (checkEvent) emit(`UI:${checkEvent}`, {});
21292
+ if (allAssigned && items.every((item) => assignments[item.id] === item.correctCategory)) {
21293
+ emit(`UI:${completeEvent}`, { success: true, attempts: attempts + 1 });
21215
21294
  }
21216
21295
  };
21217
21296
  const handleFullReset = () => {
21218
- if (playAgainEvent) {
21219
- emit(`UI:${playAgainEvent}`, {});
21220
- }
21221
- setLocalAssignments({});
21222
- setLocalSubmitted(false);
21223
- setLocalAttempts(0);
21224
- setShowHint(false);
21297
+ if (playAgainEvent) emit(`UI:${playAgainEvent}`, {});
21225
21298
  };
21226
21299
  if (!resolved) return null;
21227
21300
  const theme = resolved.theme ?? void 0;
@@ -21260,17 +21333,17 @@ function ClassifierBoard({
21260
21333
  /* @__PURE__ */ jsx(Badge, { size: "sm", children: catItems.length })
21261
21334
  ] }),
21262
21335
  /* @__PURE__ */ jsx(HStack, { gap: "xs", className: "flex-wrap min-h-[2rem]", children: catItems.map((item) => {
21263
- const result = results.find((r) => r.item.id === item.id);
21336
+ const result2 = results.find((r) => r.item.id === item.id);
21264
21337
  return /* @__PURE__ */ jsxs(
21265
21338
  Badge,
21266
21339
  {
21267
21340
  size: "sm",
21268
- className: `cursor-pointer ${result ? result.correct ? "border-success bg-success/10" : "border-error bg-error/10" : ""}`,
21341
+ className: `cursor-pointer ${result2 ? result2.correct ? "border-success bg-success/10" : "border-error bg-error/10" : ""}`,
21269
21342
  onClick: () => handleUnassign(item.id),
21270
21343
  children: [
21271
21344
  item.iconUrl && /* @__PURE__ */ jsx("img", { src: item.iconUrl, alt: "", className: "w-3 h-3 object-contain inline-block" }),
21272
21345
  item.label,
21273
- result && /* @__PURE__ */ jsx(Icon, { icon: result.correct ? CheckCircle : XCircle, size: "xs", className: result.correct ? "text-success" : "text-error" })
21346
+ result2 && /* @__PURE__ */ jsx(Icon, { icon: result2.correct ? CheckCircle : XCircle, size: "xs", className: result2.correct ? "text-success" : "text-error" })
21274
21347
  ]
21275
21348
  },
21276
21349
  item.id
@@ -21299,10 +21372,10 @@ function ClassifierBoard({
21299
21372
  ] }) }),
21300
21373
  showHint && hint && /* @__PURE__ */ jsx(Card, { className: "p-4 border-l-4 border-l-warning", children: /* @__PURE__ */ jsx(Typography, { variant: "body", children: hint }) }),
21301
21374
  /* @__PURE__ */ jsxs(HStack, { gap: "sm", justify: "center", children: [
21302
- !submitted ? /* @__PURE__ */ jsxs(Button, { variant: "primary", onClick: handleSubmit, disabled: !allAssigned, children: [
21375
+ !submitted && /* @__PURE__ */ jsxs(Button, { variant: "primary", onClick: handleSubmit, disabled: !allAssigned, children: [
21303
21376
  /* @__PURE__ */ jsx(Icon, { icon: Send, size: "sm" }),
21304
21377
  t("classifier.check")
21305
- ] }) : !allCorrect ? /* @__PURE__ */ jsx(Button, { variant: "primary", onClick: handleReset, children: t("classifier.tryAgain") }) : null,
21378
+ ] }),
21306
21379
  /* @__PURE__ */ jsxs(Button, { variant: "secondary", onClick: handleFullReset, children: [
21307
21380
  /* @__PURE__ */ jsx(Icon, { icon: RotateCcw, size: "sm" }),
21308
21381
  t("classifier.reset")
@@ -29331,7 +29404,7 @@ var init_MapView = __esm({
29331
29404
  shadowSize: [41, 41]
29332
29405
  });
29333
29406
  L.Marker.prototype.options.icon = defaultIcon;
29334
- const { useEffect: useEffect73, useRef: useRef68, useCallback: useCallback112, useState: useState104 } = React83__default;
29407
+ const { useEffect: useEffect73, useRef: useRef68, useCallback: useCallback111, useState: useState104 } = React83__default;
29335
29408
  const { Typography: Typography2 } = await Promise.resolve().then(() => (init_Typography(), Typography_exports));
29336
29409
  const { useEventBus: useEventBus2 } = await Promise.resolve().then(() => (init_useEventBus(), useEventBus_exports));
29337
29410
  function MapUpdater({ centerLat, centerLng, zoom }) {
@@ -29377,7 +29450,7 @@ var init_MapView = __esm({
29377
29450
  }) {
29378
29451
  const eventBus = useEventBus2();
29379
29452
  const [clickedPosition, setClickedPosition] = useState104(null);
29380
- const handleMapClick = useCallback112((lat, lng) => {
29453
+ const handleMapClick = useCallback111((lat, lng) => {
29381
29454
  if (showClickedPin) {
29382
29455
  setClickedPosition({ lat, lng });
29383
29456
  }
@@ -29386,7 +29459,7 @@ var init_MapView = __esm({
29386
29459
  eventBus.emit(`UI:${mapClickEvent}`, { latitude: lat, longitude: lng });
29387
29460
  }
29388
29461
  }, [onMapClick, mapClickEvent, eventBus, showClickedPin]);
29389
- const handleMarkerClick = useCallback112((marker) => {
29462
+ const handleMarkerClick = useCallback111((marker) => {
29390
29463
  onMarkerClick?.(marker);
29391
29464
  if (markerClickEvent) {
29392
29465
  eventBus.emit(`UI:${markerClickEvent}`, { ...marker });
@@ -38617,44 +38690,27 @@ function DebuggerBoard({
38617
38690
  const { t } = useTranslate();
38618
38691
  const resolved = boardEntity(entity);
38619
38692
  const [headerError, setHeaderError] = useState(false);
38620
- const [showHint, setShowHint] = useState(false);
38621
38693
  const lines = Array.isArray(resolved?.lines) ? resolved.lines : [];
38622
- const result = resolved?.result ?? null;
38694
+ const result = str(resolved?.result) || "none";
38623
38695
  const attempts = num(resolved?.attempts);
38624
- const submitted = result != null;
38696
+ const submitted = result === "win";
38697
+ const showHint = !submitted && attempts >= 2 && Boolean(str(resolved?.hint));
38625
38698
  const bugLines = lines.filter((l) => l.isBug);
38626
38699
  const flaggedLines = lines.filter((l) => l.isFlagged);
38627
38700
  const correctFlags = lines.filter((l) => l.isBug && l.isFlagged);
38628
38701
  const falseFlags = lines.filter((l) => !l.isBug && l.isFlagged);
38629
- const allCorrect = result === "win";
38702
+ const allCorrect = submitted;
38630
38703
  const toggleLine = (lineId) => {
38631
38704
  if (submitted) return;
38632
- if (toggleFlagEvent) {
38633
- emit(`UI:${toggleFlagEvent}`, { lineId });
38634
- }
38705
+ if (toggleFlagEvent) emit(`UI:${toggleFlagEvent}`, { lineId });
38635
38706
  };
38636
38707
  const handleSubmit = useCallback(() => {
38637
- if (checkEvent) {
38638
- emit(`UI:${checkEvent}`, {});
38639
- }
38708
+ if (checkEvent) emit(`UI:${checkEvent}`, {});
38640
38709
  const correct = correctFlags.length === bugLines.length && falseFlags.length === 0;
38641
- if (correct) {
38642
- emit(`UI:${completeEvent}`, { success: true, attempts: attempts + 1 });
38643
- }
38710
+ if (correct) emit(`UI:${completeEvent}`, { success: true, attempts: attempts + 1 });
38644
38711
  }, [checkEvent, correctFlags.length, bugLines.length, falseFlags.length, attempts, completeEvent, emit]);
38645
38712
  const handleReset = () => {
38646
- if (playAgainEvent) {
38647
- emit(`UI:${playAgainEvent}`, {});
38648
- }
38649
- if (attempts >= 2 && str(resolved?.hint)) {
38650
- setShowHint(true);
38651
- }
38652
- };
38653
- const handleFullReset = () => {
38654
- if (playAgainEvent) {
38655
- emit(`UI:${playAgainEvent}`, {});
38656
- }
38657
- setShowHint(false);
38713
+ if (playAgainEvent) emit(`UI:${playAgainEvent}`, {});
38658
38714
  };
38659
38715
  if (!resolved) return null;
38660
38716
  const theme = resolved.theme ?? void 0;
@@ -38678,7 +38734,7 @@ function DebuggerBoard({
38678
38734
  /* @__PURE__ */ jsx(Typography, { variant: "h4", weight: "bold", children: str(resolved.title) })
38679
38735
  ] }),
38680
38736
  /* @__PURE__ */ jsx(Typography, { variant: "body", children: str(resolved.description) }),
38681
- /* @__PURE__ */ jsx(Typography, { variant: "caption", className: "text-muted-foreground", children: t("debugger.findBugs", { count: String(num(resolved.bugCount)) }) })
38737
+ /* @__PURE__ */ jsx(Typography, { variant: "caption", className: "text-muted-foreground", children: t("debugger.findBugs", { count: String(lines.filter((l) => l.isBug).length) }) })
38682
38738
  ] }) }),
38683
38739
  /* @__PURE__ */ jsx(Card, { className: "p-0 overflow-hidden", children: /* @__PURE__ */ jsx(VStack, { gap: "none", children: lines.map((line, i) => {
38684
38740
  const isFlagged = !!line.isFlagged;
@@ -38728,11 +38784,11 @@ function DebuggerBoard({
38728
38784
  ] }) }),
38729
38785
  showHint && hint && /* @__PURE__ */ jsx(Card, { className: "p-4 border-l-4 border-l-warning", children: /* @__PURE__ */ jsx(Typography, { variant: "body", children: hint }) }),
38730
38786
  /* @__PURE__ */ jsxs(HStack, { gap: "sm", justify: "center", children: [
38731
- !submitted ? /* @__PURE__ */ jsxs(Button, { variant: "primary", onClick: handleSubmit, disabled: flaggedLines.length === 0, children: [
38787
+ !submitted && /* @__PURE__ */ jsxs(Button, { variant: "primary", onClick: handleSubmit, disabled: flaggedLines.length === 0, children: [
38732
38788
  /* @__PURE__ */ jsx(Icon, { icon: Send, size: "sm" }),
38733
38789
  t("debugger.submit")
38734
- ] }) : !allCorrect ? /* @__PURE__ */ jsx(Button, { variant: "primary", onClick: handleReset, children: t("debugger.tryAgain") }) : null,
38735
- /* @__PURE__ */ jsxs(Button, { variant: "secondary", onClick: handleFullReset, children: [
38790
+ ] }),
38791
+ /* @__PURE__ */ jsxs(Button, { variant: "secondary", onClick: handleReset, children: [
38736
38792
  /* @__PURE__ */ jsx(Icon, { icon: RotateCcw, size: "sm" }),
38737
38793
  t("debugger.reset")
38738
38794
  ] })
@@ -39733,39 +39789,40 @@ function EventHandlerBoard({
39733
39789
  stepDurationMs = 800,
39734
39790
  playEvent,
39735
39791
  completeEvent,
39792
+ editRuleEvent,
39793
+ playAgainEvent,
39736
39794
  className
39737
39795
  }) {
39738
39796
  const { emit } = useEventBus();
39739
39797
  const { t } = useTranslate();
39740
39798
  const resolved = boardEntity(entity);
39741
- const entityObjects = rows(resolved?.objects);
39742
- const [objects, setObjects] = useState(() => [...entityObjects]);
39799
+ const objects = rows(resolved?.objects);
39800
+ const entityResult = str(resolved?.result) || "none";
39801
+ const isSuccess = entityResult === "win";
39802
+ const attempts = typeof resolved?.attempts === "number" ? resolved.attempts : 0;
39743
39803
  const [selectedObjectId, setSelectedObjectId] = useState(
39744
- entityObjects[0] ? objId(entityObjects[0]) : null
39804
+ objects[0] ? objId(objects[0]) : null
39745
39805
  );
39746
39806
  const [headerError, setHeaderError] = useState(false);
39747
- const [playState, setPlayState] = useState("editing");
39807
+ const [isPlaying, setIsPlaying] = useState(false);
39748
39808
  const [eventLog, setEventLog] = useState([]);
39749
- const [attempts, setAttempts] = useState(0);
39750
39809
  const timerRef = useRef(null);
39751
39810
  const logIdCounter = useRef(0);
39752
39811
  useEffect(() => () => {
39753
39812
  if (timerRef.current) clearTimeout(timerRef.current);
39754
39813
  }, []);
39755
- const selectedObject = objects.find((o) => objId(o) === selectedObjectId) || null;
39814
+ const selectedObject = objects.find((o) => objId(o) === selectedObjectId) ?? null;
39756
39815
  const handleRulesChange = useCallback((objectId, rules) => {
39757
- setObjects((prev) => prev.map(
39758
- (o) => objId(o) === objectId ? { ...o, rules } : o
39759
- ));
39760
- }, []);
39816
+ if (editRuleEvent) emit(`UI:${editRuleEvent}`, { objectId, rules });
39817
+ }, [editRuleEvent, emit]);
39761
39818
  const addLogEntry = useCallback((icon, message, status = "done") => {
39762
39819
  const id = `log-${logIdCounter.current++}`;
39763
39820
  setEventLog((prev) => [...prev, { id, timestamp: Date.now(), icon, message, status }]);
39764
39821
  }, []);
39765
39822
  const handlePlay = useCallback(() => {
39766
- if (playState !== "editing") return;
39823
+ if (isPlaying || isSuccess) return;
39767
39824
  if (playEvent) emit(`UI:${playEvent}`, {});
39768
- setPlayState("playing");
39825
+ setIsPlaying(true);
39769
39826
  setEventLog([]);
39770
39827
  const allRules = [];
39771
39828
  objects.forEach((obj) => {
@@ -39781,15 +39838,8 @@ function EventHandlerBoard({
39781
39838
  let goalReached = false;
39782
39839
  const processNext = () => {
39783
39840
  if (eventQueue.length === 0 || stepIdx > 20) {
39784
- if (goalReached) {
39785
- setPlayState("success");
39786
- if (completeEvent) {
39787
- emit(`UI:${completeEvent}`, { success: true });
39788
- }
39789
- } else {
39790
- setAttempts((prev) => prev + 1);
39791
- setPlayState("fail");
39792
- }
39841
+ setIsPlaying(false);
39842
+ if (goalReached && completeEvent) emit(`UI:${completeEvent}`, { success: true });
39793
39843
  return;
39794
39844
  }
39795
39845
  const currentEvent = eventQueue.shift();
@@ -39820,21 +39870,19 @@ function EventHandlerBoard({
39820
39870
  addLogEntry("\u{1F3AC}", t("eventHandler.simulationStarted", { events: triggers.join(", ") }), "active");
39821
39871
  }
39822
39872
  timerRef.current = setTimeout(processNext, stepDurationMs);
39823
- }, [playState, objects, resolved, stepDurationMs, playEvent, completeEvent, emit, addLogEntry, t]);
39873
+ }, [isPlaying, isSuccess, objects, resolved, stepDurationMs, playEvent, completeEvent, emit, addLogEntry, t]);
39824
39874
  const handleTryAgain = useCallback(() => {
39825
39875
  if (timerRef.current) clearTimeout(timerRef.current);
39826
- setPlayState("editing");
39876
+ setIsPlaying(false);
39827
39877
  setEventLog([]);
39828
39878
  }, []);
39829
39879
  const handleReset = useCallback(() => {
39830
39880
  if (timerRef.current) clearTimeout(timerRef.current);
39831
- const resetObjects = rows(resolved?.objects);
39832
- setObjects([...resetObjects]);
39833
- setPlayState("editing");
39881
+ setIsPlaying(false);
39834
39882
  setEventLog([]);
39835
- setSelectedObjectId(resetObjects[0] ? objId(resetObjects[0]) : null);
39836
- setAttempts(0);
39837
- }, [resolved?.objects]);
39883
+ setSelectedObjectId(objects[0] ? objId(objects[0]) : null);
39884
+ if (playAgainEvent) emit(`UI:${playAgainEvent}`, {});
39885
+ }, [objects, playAgainEvent, emit]);
39838
39886
  if (!resolved) return null;
39839
39887
  const objectViewers = objects.map((obj) => {
39840
39888
  const states = objStates(obj);
@@ -39903,12 +39951,12 @@ function EventHandlerBoard({
39903
39951
  {
39904
39952
  object: selectedObject,
39905
39953
  onRulesChange: handleRulesChange,
39906
- disabled: playState !== "editing"
39954
+ disabled: isPlaying
39907
39955
  }
39908
39956
  ),
39909
39957
  eventLog.length > 0 && /* @__PURE__ */ jsx(EventLog, { entries: eventLog }),
39910
- playState === "success" && /* @__PURE__ */ jsx(Box, { className: "p-4 rounded-container bg-success/20 border border-success text-center", children: /* @__PURE__ */ jsx(Typography, { variant: "h5", className: "text-success", children: str(resolved.successMessage) || t("eventHandler.chainComplete") }) }),
39911
- playState === "fail" && /* @__PURE__ */ jsxs(VStack, { gap: "sm", children: [
39958
+ isSuccess && /* @__PURE__ */ jsx(Box, { className: "p-4 rounded-container bg-success/20 border border-success text-center", children: /* @__PURE__ */ jsx(Typography, { variant: "h5", className: "text-success", children: str(resolved.successMessage) || t("eventHandler.chainComplete") }) }),
39959
+ !isPlaying && !isSuccess && attempts > 0 && /* @__PURE__ */ jsxs(VStack, { gap: "sm", children: [
39912
39960
  /* @__PURE__ */ jsx(Box, { className: "p-4 rounded-container bg-warning/10 border border-warning/30 text-center", children: /* @__PURE__ */ jsx(Typography, { variant: "body1", className: "text-foreground font-medium", children: t(encourageKey) }) }),
39913
39961
  showHint && /* @__PURE__ */ jsx(Box, { className: "p-3 rounded-container bg-accent/10 border border-accent/30", children: /* @__PURE__ */ jsxs(HStack, { className: "items-start", gap: "xs", children: [
39914
39962
  /* @__PURE__ */ jsx(Typography, { variant: "body2", className: "text-accent font-bold shrink-0", children: "\u{1F4A1} " + t("game.hint") + ":" }),
@@ -39916,12 +39964,12 @@ function EventHandlerBoard({
39916
39964
  ] }) })
39917
39965
  ] }),
39918
39966
  /* @__PURE__ */ jsxs(HStack, { gap: "sm", children: [
39919
- playState === "fail" ? /* @__PURE__ */ jsx(Button, { variant: "primary", onClick: handleTryAgain, children: "\u{1F504} " + t("puzzle.tryAgainButton") }) : /* @__PURE__ */ jsx(
39967
+ !isPlaying && !isSuccess && attempts > 0 ? /* @__PURE__ */ jsx(Button, { variant: "primary", onClick: handleTryAgain, children: "\u{1F504} " + t("puzzle.tryAgainButton") }) : /* @__PURE__ */ jsx(
39920
39968
  Button,
39921
39969
  {
39922
39970
  variant: "primary",
39923
39971
  onClick: handlePlay,
39924
- disabled: playState !== "editing",
39972
+ disabled: isPlaying || isSuccess,
39925
39973
  children: "\u25B6 " + t("game.play")
39926
39974
  }
39927
39975
  ),
@@ -42474,21 +42522,6 @@ var init_ModalSlot = __esm({
42474
42522
  ModalSlot.displayName = "ModalSlot";
42475
42523
  }
42476
42524
  });
42477
- function getOpponentAction(strategy, actions, history) {
42478
- const actionIds = actions.map((a) => a.id);
42479
- switch (strategy) {
42480
- case "always-cooperate":
42481
- return actionIds[0];
42482
- case "always-defect":
42483
- return actionIds[actionIds.length - 1];
42484
- case "tit-for-tat":
42485
- if (history.length === 0) return actionIds[0];
42486
- return history[history.length - 1].playerAction;
42487
- case "random":
42488
- default:
42489
- return actionIds[Math.floor(Math.random() * actionIds.length)];
42490
- }
42491
- }
42492
42525
  function NegotiatorBoard({
42493
42526
  entity,
42494
42527
  completeEvent = "PUZZLE_COMPLETE",
@@ -42510,28 +42543,35 @@ function NegotiatorBoard({
42510
42543
  const playerTotal = num(resolved?.score);
42511
42544
  const isComplete = result !== "none" || totalRounds > 0 && currentRound >= totalRounds;
42512
42545
  const won = result === "win";
42513
- const opponentTotal = history.reduce((s, r) => s + r.opponentPayoff, 0);
42546
+ const lastPlayerAction = str(resolved?.lastPlayerAction);
42547
+ const lastOpponentAction = str(resolved?.lastOpponentAction);
42548
+ const lastPayoff = num(resolved?.lastPayoff);
42514
42549
  const actions = Array.isArray(resolved?.actions) ? resolved.actions : [];
42515
42550
  const payoffMatrix = Array.isArray(resolved?.payoffMatrix) ? resolved.payoffMatrix : [];
42551
+ const prevRoundRef = useRef(currentRound);
42552
+ useEffect(() => {
42553
+ if (currentRound > prevRoundRef.current && lastPlayerAction) {
42554
+ const opponentPayoffEntry = payoffMatrix.find(
42555
+ (p2) => p2.playerAction === lastPlayerAction && p2.opponentAction === lastOpponentAction
42556
+ );
42557
+ setHistory((prev) => [
42558
+ ...prev,
42559
+ {
42560
+ round: currentRound,
42561
+ playerAction: lastPlayerAction,
42562
+ opponentAction: lastOpponentAction,
42563
+ playerPayoff: lastPayoff,
42564
+ opponentPayoff: opponentPayoffEntry?.opponentPayoff ?? 0
42565
+ }
42566
+ ]);
42567
+ }
42568
+ prevRoundRef.current = currentRound;
42569
+ }, [currentRound, lastPlayerAction, lastOpponentAction, lastPayoff, payoffMatrix]);
42570
+ const opponentTotal = useMemo(() => history.reduce((s, r) => s + r.opponentPayoff, 0), [history]);
42516
42571
  const handleAction = useCallback((actionId) => {
42517
42572
  if (isComplete) return;
42518
- const opponentAction = getOpponentAction(str(resolved?.opponentStrategy) || "random", actions, history);
42519
- const payoff = payoffMatrix.find(
42520
- (p2) => p2.playerAction === actionId && p2.opponentAction === opponentAction
42521
- );
42522
- const playerPayoff = payoff?.playerPayoff ?? 0;
42523
- setHistory((prev) => [
42524
- ...prev,
42525
- {
42526
- round: prev.length + 1,
42527
- playerAction: actionId,
42528
- opponentAction,
42529
- playerPayoff,
42530
- opponentPayoff: payoff?.opponentPayoff ?? 0
42531
- }
42532
- ]);
42533
42573
  if (playRoundEvent) {
42534
- emit(`UI:${playRoundEvent}`, { playerAction: actionId, payoff: playerPayoff });
42574
+ emit(`UI:${playRoundEvent}`, { playerAction: actionId, payoff: 0 });
42535
42575
  }
42536
42576
  if (totalRounds > 0 && currentRound + 1 >= totalRounds) {
42537
42577
  if (finishEvent) {
@@ -42541,10 +42581,11 @@ function NegotiatorBoard({
42541
42581
  setShowHint(true);
42542
42582
  }
42543
42583
  }
42544
- }, [isComplete, resolved, totalRounds, currentRound, actions, payoffMatrix, history, playRoundEvent, finishEvent, emit]);
42584
+ }, [isComplete, resolved, totalRounds, currentRound, playRoundEvent, finishEvent, emit]);
42545
42585
  const handleReset = useCallback(() => {
42546
42586
  setHistory([]);
42547
42587
  setShowHint(false);
42588
+ prevRoundRef.current = 0;
42548
42589
  if (playAgainEvent) {
42549
42590
  emit(`UI:${playAgainEvent}`, {});
42550
42591
  }
@@ -44880,17 +44921,24 @@ function SequencerBoard({
44880
44921
  const { emit } = useEventBus();
44881
44922
  const { t } = useTranslate();
44882
44923
  const resolved = boardEntity(entity);
44883
- const maxSlots = num(resolved?.maxSlots);
44924
+ const maxSlots = num(resolved?.maxSlots) || 3;
44884
44925
  const solutions = Array.isArray(resolved?.solutions) ? resolved.solutions : [];
44885
44926
  const availableActions = Array.isArray(resolved?.availableActions) ? resolved.availableActions : [];
44886
44927
  const allowDuplicates = resolved?.allowDuplicates !== false;
44928
+ const entitySlots = Array.isArray(resolved?.slots) ? resolved.slots : [];
44929
+ const entityResult = str(resolved?.result);
44930
+ const entityAttempts = num(resolved?.attempts);
44931
+ const entityCurrentStep = typeof resolved?.currentStep === "number" ? resolved.currentStep : -1;
44932
+ const slots = Array.from({ length: maxSlots }, (_, i) => {
44933
+ const entitySlot = entitySlots.find((s) => s.index === i);
44934
+ if (!entitySlot?.placedActionId) return void 0;
44935
+ return availableActions.find((a) => a.id === entitySlot.placedActionId);
44936
+ });
44937
+ const isSuccess = entityResult === "win";
44938
+ const attempts = entityAttempts;
44939
+ const isPlayingBack = entityCurrentStep >= 0 && !isSuccess;
44940
+ const currentStep = entityCurrentStep;
44887
44941
  const [headerError, setHeaderError] = useState(false);
44888
- const [slots, setSlots] = useState(
44889
- () => Array.from({ length: maxSlots }, () => void 0)
44890
- );
44891
- const [playState, setPlayState] = useState("idle");
44892
- const [currentStep, setCurrentStep] = useState(-1);
44893
- const [attempts, setAttempts] = useState(0);
44894
44942
  const [slotFeedback, setSlotFeedback] = useState(
44895
44943
  () => Array.from({ length: maxSlots }, () => null)
44896
44944
  );
@@ -44899,11 +44947,6 @@ function SequencerBoard({
44899
44947
  if (timerRef.current) clearTimeout(timerRef.current);
44900
44948
  }, []);
44901
44949
  const handleSlotDrop = useCallback((index, item) => {
44902
- setSlots((prev) => {
44903
- const next = [...prev];
44904
- next[index] = item;
44905
- return next;
44906
- });
44907
44950
  setSlotFeedback((prev) => {
44908
44951
  const next = [...prev];
44909
44952
  next[index] = null;
@@ -44913,11 +44956,6 @@ function SequencerBoard({
44913
44956
  if (placeEvent) emit(`UI:${placeEvent}`, { slotIndex: index, actionId: item.id });
44914
44957
  }, [emit, placeEvent]);
44915
44958
  const handleSlotRemove = useCallback((index) => {
44916
- setSlots((prev) => {
44917
- const next = [...prev];
44918
- next[index] = void 0;
44919
- return next;
44920
- });
44921
44959
  setSlotFeedback((prev) => {
44922
44960
  const next = [...prev];
44923
44961
  next[index] = null;
@@ -44928,48 +44966,34 @@ function SequencerBoard({
44928
44966
  }, [emit, removeEvent]);
44929
44967
  const handleReset = useCallback(() => {
44930
44968
  if (timerRef.current) clearTimeout(timerRef.current);
44931
- setSlots(Array.from({ length: maxSlots }, () => void 0));
44932
- setPlayState("idle");
44933
- setCurrentStep(-1);
44934
- setAttempts(0);
44935
44969
  setSlotFeedback(Array.from({ length: maxSlots }, () => null));
44936
44970
  if (playAgainEvent) emit(`UI:${playAgainEvent}`, {});
44937
44971
  }, [maxSlots, playAgainEvent, emit]);
44938
44972
  const filledSlots = slots.filter((s) => !!s);
44939
- const canPlay = filledSlots.length > 0 && playState === "idle";
44973
+ const canPlay = filledSlots.length > 0 && !isPlayingBack && !isSuccess;
44940
44974
  const handlePlay = useCallback(() => {
44941
44975
  if (!canPlay) return;
44942
44976
  setSlotFeedback(Array.from({ length: maxSlots }, () => null));
44943
44977
  emit("UI:PLAY_SOUND", { key: "confirm" });
44944
44978
  const sequence = slots.map((s) => s?.id || "");
44945
- if (playEvent) {
44946
- emit(`UI:${playEvent}`, { sequence });
44947
- }
44948
- setPlayState("playing");
44949
- setCurrentStep(0);
44979
+ const playerIds = slots.filter(Boolean).map((s) => s?.id || "");
44980
+ if (playEvent) emit(`UI:${playEvent}`, { sequence });
44950
44981
  let step = 0;
44951
44982
  const advance = () => {
44952
44983
  step++;
44984
+ emit("UI:STEP", { step });
44953
44985
  if (step >= maxSlots) {
44986
+ if (checkEvent) emit(`UI:${checkEvent}`, { sequence: playerIds });
44954
44987
  const playerSeq = slots.map((s) => s?.id);
44955
- const playerIds = slots.filter(Boolean).map((s) => s?.id || "");
44956
44988
  const success = solutions.some(
44957
44989
  (sol) => sol.length === playerIds.length && sol.every((id, i) => id === playerIds[i])
44958
44990
  );
44959
- if (checkEvent) emit(`UI:${checkEvent}`, { sequence: playerIds });
44960
44991
  if (success) {
44961
- setPlayState("success");
44962
- setCurrentStep(-1);
44963
44992
  emit("UI:PLAY_SOUND", { key: "levelComplete" });
44964
- if (completeEvent) {
44965
- emit(`UI:${completeEvent}`, { success: true, sequence: playerIds });
44966
- }
44993
+ if (completeEvent) emit(`UI:${completeEvent}`, { success: true, sequence: playerIds });
44967
44994
  } else {
44968
- setAttempts((prev) => prev + 1);
44969
44995
  const feedback = computeSlotFeedback(playerSeq, solutions);
44970
44996
  setSlotFeedback(feedback);
44971
- setPlayState("idle");
44972
- setCurrentStep(-1);
44973
44997
  emit("UI:PLAY_SOUND", { key: "fail" });
44974
44998
  const correctCount2 = feedback.filter((f3) => f3 === "correct").length;
44975
44999
  for (let ci = 0; ci < correctCount2; ci++) {
@@ -44979,7 +45003,6 @@ function SequencerBoard({
44979
45003
  }
44980
45004
  }
44981
45005
  } else {
44982
- setCurrentStep(step);
44983
45006
  timerRef.current = setTimeout(advance, stepDurationMs);
44984
45007
  }
44985
45008
  };
@@ -45029,7 +45052,7 @@ function SequencerBoard({
45029
45052
  /* @__PURE__ */ jsxs(VStack, { gap: "xs", children: [
45030
45053
  /* @__PURE__ */ jsxs(HStack, { className: "items-center justify-between", children: [
45031
45054
  /* @__PURE__ */ jsx(Typography, { variant: "body2", className: "text-muted-foreground font-medium", children: t("sequencer.yourSequence") + ":" }),
45032
- hasFeedback && playState === "idle" && /* @__PURE__ */ jsxs(Typography, { variant: "caption", className: "text-muted-foreground", children: [
45055
+ hasFeedback && !isPlayingBack && !isSuccess && /* @__PURE__ */ jsxs(Typography, { variant: "caption", className: "text-muted-foreground", children: [
45033
45056
  `${correctCount}/${maxSlots} `,
45034
45057
  "\u2705"
45035
45058
  ] })
@@ -45041,7 +45064,7 @@ function SequencerBoard({
45041
45064
  maxSlots,
45042
45065
  onSlotDrop: handleSlotDrop,
45043
45066
  onSlotRemove: handleSlotRemove,
45044
- playing: playState === "playing",
45067
+ playing: isPlayingBack,
45045
45068
  currentStep,
45046
45069
  categoryColors,
45047
45070
  slotFeedback,
@@ -45049,7 +45072,7 @@ function SequencerBoard({
45049
45072
  }
45050
45073
  )
45051
45074
  ] }),
45052
- playState !== "playing" && /* @__PURE__ */ jsx(
45075
+ !isPlayingBack && /* @__PURE__ */ jsx(
45053
45076
  ActionPalette,
45054
45077
  {
45055
45078
  actions: availableActions,
@@ -45059,8 +45082,8 @@ function SequencerBoard({
45059
45082
  label: t("sequencer.dragActions")
45060
45083
  }
45061
45084
  ),
45062
- hasFeedback && playState === "idle" && attempts > 0 && /* @__PURE__ */ jsx(Box, { className: "p-3 rounded-container bg-warning/10 border border-warning/30 text-center", children: /* @__PURE__ */ jsx(Typography, { variant: "body2", className: "text-foreground", children: t(encourageKey) }) }),
45063
- playState === "success" && /* @__PURE__ */ jsx(Box, { className: "p-4 rounded-container bg-success/20 border border-success text-center", children: /* @__PURE__ */ jsx(Typography, { variant: "h5", className: "text-success", children: str(resolved.successMessage) || t("sequencer.levelComplete") }) }),
45085
+ hasFeedback && !isPlayingBack && !isSuccess && attempts > 0 && /* @__PURE__ */ jsx(Box, { className: "p-3 rounded-container bg-warning/10 border border-warning/30 text-center", children: /* @__PURE__ */ jsx(Typography, { variant: "body2", className: "text-foreground", children: t(encourageKey) }) }),
45086
+ isSuccess && /* @__PURE__ */ jsx(Box, { className: "p-4 rounded-container bg-success/20 border border-success text-center", children: /* @__PURE__ */ jsx(Typography, { variant: "h5", className: "text-success", children: str(resolved.successMessage) || t("sequencer.levelComplete") }) }),
45064
45087
  /* @__PURE__ */ jsxs(HStack, { gap: "sm", children: [
45065
45088
  /* @__PURE__ */ jsx(
45066
45089
  Button,
@@ -46110,6 +46133,9 @@ function StateArchitectBoard({
46110
46133
  stepDurationMs = 600,
46111
46134
  testEvent,
46112
46135
  completeEvent,
46136
+ addTransitionEvent,
46137
+ removeTransitionEvent,
46138
+ playAgainEvent,
46113
46139
  className
46114
46140
  }) {
46115
46141
  const { emit } = useEventBus();
@@ -46122,14 +46148,16 @@ function StateArchitectBoard({
46122
46148
  const testCases = Array.isArray(resolved?.testCases) ? resolved.testCases : [];
46123
46149
  const entityTransitions = Array.isArray(resolved?.transitions) ? resolved.transitions : [];
46124
46150
  const entityVariables = rows(resolved?.variables);
46125
- const [transitions, setTransitions] = useState(entityTransitions);
46151
+ const transitions = entityTransitions;
46152
+ const attempts = typeof resolved?.attempts === "number" ? resolved.attempts : 0;
46153
+ const entityResult = str(resolved?.result) || "none";
46154
+ const isSuccess = entityResult === "win";
46155
+ const [isTesting, setIsTesting] = useState(false);
46126
46156
  const [headerError, setHeaderError] = useState(false);
46127
- const [playState, setPlayState] = useState("editing");
46128
46157
  const [currentState, setCurrentState] = useState(initialState);
46129
46158
  const [selectedState, setSelectedState] = useState(null);
46130
46159
  const [testResults, setTestResults] = useState([]);
46131
46160
  const [variables, setVariables] = useState(() => [...entityVariables]);
46132
- const [attempts, setAttempts] = useState(0);
46133
46161
  const timerRef = useRef(null);
46134
46162
  const [addingFrom, setAddingFrom] = useState(null);
46135
46163
  useEffect(() => () => {
@@ -46139,7 +46167,7 @@ function StateArchitectBoard({
46139
46167
  const GRAPH_H = 400;
46140
46168
  const positions = useMemo(() => layoutStates(entityStates, GRAPH_W, GRAPH_H), [entityStates]);
46141
46169
  const handleStateClick = useCallback((state) => {
46142
- if (playState !== "editing") return;
46170
+ if (isTesting) return;
46143
46171
  if (addingFrom) {
46144
46172
  if (addingFrom !== state) {
46145
46173
  const event = availableEvents[0] || "EVENT";
@@ -46149,20 +46177,20 @@ function StateArchitectBoard({
46149
46177
  to: state,
46150
46178
  event
46151
46179
  };
46152
- setTransitions((prev) => [...prev, newTrans]);
46180
+ if (addTransitionEvent) emit(`UI:${addTransitionEvent}`, { id: newTrans.id, from: newTrans.from, to: newTrans.to, event: newTrans.event });
46153
46181
  }
46154
46182
  setAddingFrom(null);
46155
46183
  } else {
46156
46184
  setSelectedState(state);
46157
46185
  }
46158
- }, [playState, addingFrom, availableEvents]);
46186
+ }, [isTesting, addingFrom, availableEvents, addTransitionEvent, emit]);
46159
46187
  const handleStartAddTransition = useCallback(() => {
46160
46188
  if (!selectedState) return;
46161
46189
  setAddingFrom(selectedState);
46162
46190
  }, [selectedState]);
46163
46191
  const handleRemoveTransition = useCallback((transId) => {
46164
- setTransitions((prev) => prev.filter((t2) => t2.id !== transId));
46165
- }, []);
46192
+ if (removeTransitionEvent) emit(`UI:${removeTransitionEvent}`, { id: transId });
46193
+ }, [removeTransitionEvent, emit]);
46166
46194
  const machine = useMemo(() => ({
46167
46195
  name: entityName,
46168
46196
  description: str(resolved?.description),
@@ -46176,16 +46204,16 @@ function StateArchitectBoard({
46176
46204
  }))
46177
46205
  }), [entityName, resolved, entityStates, currentState, transitions]);
46178
46206
  const handleTest = useCallback(() => {
46179
- if (playState !== "editing") return;
46207
+ if (isTesting) return;
46180
46208
  if (testEvent) emit(`UI:${testEvent}`, {});
46181
- setPlayState("testing");
46209
+ setIsTesting(true);
46182
46210
  setTestResults([]);
46183
46211
  const results = [];
46184
46212
  let testIdx = 0;
46185
46213
  const runNextTest = () => {
46186
46214
  if (testIdx >= testCases.length) {
46187
46215
  const allPassed = results.every((r) => r.passed);
46188
- setPlayState(allPassed ? "success" : "fail");
46216
+ setIsTesting(false);
46189
46217
  setTestResults(results);
46190
46218
  if (allPassed && completeEvent) {
46191
46219
  emit(`UI:${completeEvent}`, {
@@ -46193,9 +46221,6 @@ function StateArchitectBoard({
46193
46221
  passedTests: results.filter((r) => r.passed).length
46194
46222
  });
46195
46223
  }
46196
- if (!allPassed) {
46197
- setAttempts((prev) => prev + 1);
46198
- }
46199
46224
  return;
46200
46225
  }
46201
46226
  const testCase = testCases[testIdx];
@@ -46218,24 +46243,23 @@ function StateArchitectBoard({
46218
46243
  timerRef.current = setTimeout(runNextTest, stepDurationMs);
46219
46244
  };
46220
46245
  timerRef.current = setTimeout(runNextTest, stepDurationMs);
46221
- }, [playState, transitions, testCases, initialState, stepDurationMs, testEvent, completeEvent, emit]);
46246
+ }, [isTesting, transitions, testCases, initialState, stepDurationMs, testEvent, completeEvent, emit]);
46222
46247
  const handleTryAgain = useCallback(() => {
46223
46248
  if (timerRef.current) clearTimeout(timerRef.current);
46224
- setPlayState("editing");
46249
+ setIsTesting(false);
46225
46250
  setCurrentState(initialState);
46226
46251
  setTestResults([]);
46227
46252
  }, [initialState]);
46228
46253
  const handleReset = useCallback(() => {
46229
46254
  if (timerRef.current) clearTimeout(timerRef.current);
46230
- setTransitions(entityTransitions);
46231
- setPlayState("editing");
46255
+ setIsTesting(false);
46232
46256
  setCurrentState(initialState);
46233
46257
  setTestResults([]);
46234
46258
  setVariables([...entityVariables]);
46235
46259
  setSelectedState(null);
46236
46260
  setAddingFrom(null);
46237
- setAttempts(0);
46238
- }, [entityTransitions, initialState, entityVariables]);
46261
+ if (playAgainEvent) emit(`UI:${playAgainEvent}`, {});
46262
+ }, [initialState, entityVariables, playAgainEvent, emit]);
46239
46263
  const codeData = useMemo(() => ({
46240
46264
  name: entityName,
46241
46265
  states: entityStates,
@@ -46331,7 +46355,7 @@ function StateArchitectBoard({
46331
46355
  ]
46332
46356
  }
46333
46357
  ),
46334
- playState === "editing" && /* @__PURE__ */ jsx(HStack, { gap: "sm", children: /* @__PURE__ */ jsx(
46358
+ !isTesting && /* @__PURE__ */ jsx(HStack, { gap: "sm", children: /* @__PURE__ */ jsx(
46335
46359
  Button,
46336
46360
  {
46337
46361
  variant: "ghost",
@@ -46353,7 +46377,7 @@ function StateArchitectBoard({
46353
46377
  t2.guardHint,
46354
46378
  ")"
46355
46379
  ] }),
46356
- playState === "editing" && /* @__PURE__ */ jsx(
46380
+ !isTesting && /* @__PURE__ */ jsx(
46357
46381
  Button,
46358
46382
  {
46359
46383
  variant: "ghost",
@@ -46385,21 +46409,21 @@ function StateArchitectBoard({
46385
46409
  resolved.showCodeView !== false && /* @__PURE__ */ jsx(StateJsonView, { data: codeData, label: "View Code" })
46386
46410
  ] })
46387
46411
  ] }),
46388
- playState === "success" && /* @__PURE__ */ jsx(Box, { className: "p-4 rounded-container bg-success/20 border border-success text-center", children: /* @__PURE__ */ jsx(Typography, { variant: "h5", className: "text-success", children: str(resolved.successMessage) || t("stateArchitect.allPassed") }) }),
46389
- playState === "fail" && /* @__PURE__ */ jsxs(VStack, { gap: "sm", children: [
46412
+ isSuccess && /* @__PURE__ */ jsx(Box, { className: "p-4 rounded-container bg-success/20 border border-success text-center", children: /* @__PURE__ */ jsx(Typography, { variant: "h5", className: "text-success", children: str(resolved.successMessage) || t("stateArchitect.allPassed") }) }),
46413
+ !isTesting && !isSuccess && testResults.some((r) => !r.passed) && /* @__PURE__ */ jsxs(VStack, { gap: "sm", children: [
46390
46414
  /* @__PURE__ */ jsx(Box, { className: "p-4 rounded-container bg-warning/10 border border-warning/30 text-center", children: /* @__PURE__ */ jsx(Typography, { variant: "body1", className: "text-foreground font-medium", children: t(ENCOURAGEMENT_KEYS3[Math.min(attempts - 1, ENCOURAGEMENT_KEYS3.length - 1)] ?? ENCOURAGEMENT_KEYS3[0]) }) }),
46391
- attempts >= 3 && hint && /* @__PURE__ */ jsx(Box, { className: "p-3 rounded-container bg-accent/10 border border-accent/30", children: /* @__PURE__ */ jsxs(HStack, { className: "items-start", gap: "xs", children: [
46415
+ !isSuccess && attempts >= 2 && Boolean(str(resolved?.hint)) && /* @__PURE__ */ jsx(Box, { className: "p-3 rounded-container bg-accent/10 border border-accent/30", children: /* @__PURE__ */ jsxs(HStack, { className: "items-start", gap: "xs", children: [
46392
46416
  /* @__PURE__ */ jsx(Typography, { variant: "body2", className: "text-accent font-bold shrink-0", children: "\u{1F4A1} " + t("game.hint") + ":" }),
46393
46417
  /* @__PURE__ */ jsx(Typography, { variant: "body2", className: "text-foreground", children: hint })
46394
46418
  ] }) })
46395
46419
  ] }),
46396
46420
  /* @__PURE__ */ jsxs(HStack, { gap: "sm", children: [
46397
- playState === "fail" ? /* @__PURE__ */ jsx(Button, { variant: "primary", onClick: handleTryAgain, children: "\u{1F504} " + t("puzzle.tryAgainButton") }) : /* @__PURE__ */ jsx(
46421
+ !isTesting && !isSuccess && testResults.some((r) => !r.passed) ? /* @__PURE__ */ jsx(Button, { variant: "primary", onClick: handleTryAgain, children: "\u{1F504} " + t("puzzle.tryAgainButton") }) : /* @__PURE__ */ jsx(
46398
46422
  Button,
46399
46423
  {
46400
46424
  variant: "primary",
46401
46425
  onClick: handleTest,
46402
- disabled: playState !== "editing",
46426
+ disabled: isTesting,
46403
46427
  children: "\u25B6 " + t("game.runTests")
46404
46428
  }
46405
46429
  ),
@@ -47103,17 +47127,12 @@ var init_UncontrolledBattleBoard = __esm({
47103
47127
  }
47104
47128
  });
47105
47129
  function heroPosition(h) {
47130
+ if ("position" in h && h.position != null) return h.position;
47106
47131
  return vec2(h.position);
47107
47132
  }
47108
- function heroOwner(h) {
47109
- return str(h.owner);
47110
- }
47111
47133
  function heroMovement(h) {
47112
47134
  return num(h.movement);
47113
47135
  }
47114
- function hexPassable(h) {
47115
- return h.passable !== false;
47116
- }
47117
47136
  function defaultIsInRange(from, to, range) {
47118
47137
  return Math.abs(from.x - to.x) + Math.abs(from.y - to.y) <= range;
47119
47138
  }
@@ -47149,38 +47168,41 @@ function WorldMapBoard({
47149
47168
  }) {
47150
47169
  const eventBus = useEventBus();
47151
47170
  const resolved = boardEntity(entity);
47152
- const hexes = rows(resolved?.hexes);
47153
- const heroes = rows(resolved?.heroes);
47171
+ const entityUnits = rows(resolved?.units);
47172
+ const entityTiles = rows(resolved?.tiles);
47154
47173
  const features = propFeatures ?? (Array.isArray(resolved?.features) ? resolved.features : []);
47155
47174
  const selectedHeroId = resolved?.selectedHeroId ?? null;
47156
47175
  const assetManifest = propAssetManifest ?? resolved?.assetManifest;
47157
47176
  const backgroundImage = resolved?.backgroundImage;
47158
47177
  const [hoveredTile, setHoveredTile] = useState(null);
47159
- const selectedHero = useMemo(
47160
- () => heroes.find((h) => str(h.id) === selectedHeroId) ?? null,
47161
- [heroes, selectedHeroId]
47162
- );
47163
47178
  const derivedTiles = useMemo(
47164
- () => hexes.map((hex) => ({
47165
- x: num(hex.x),
47166
- y: num(hex.y),
47167
- terrain: str(hex.terrain),
47168
- terrainSprite: hex.terrainSprite == null ? void 0 : str(hex.terrainSprite)
47179
+ () => entityTiles.map((t) => ({
47180
+ x: num(t.x),
47181
+ y: num(t.y),
47182
+ terrain: str(t.terrain),
47183
+ terrainSprite: t.terrainSprite == null ? void 0 : str(t.terrainSprite),
47184
+ passable: t.passable !== false
47169
47185
  })),
47170
- [hexes]
47186
+ [entityTiles]
47171
47187
  );
47172
47188
  const tiles = propTiles ?? derivedTiles;
47173
47189
  const baseUnits = useMemo(
47174
- () => propUnits ?? heroes.map((hero) => ({
47175
- id: str(hero.id),
47176
- position: heroPosition(hero),
47177
- name: str(hero.name),
47178
- team: heroOwner(hero) === "enemy" ? "enemy" : "player",
47179
- health: 100,
47180
- maxHealth: 100,
47181
- sprite: hero.sprite == null ? void 0 : str(hero.sprite)
47190
+ () => propUnits ?? entityUnits.map((u) => ({
47191
+ id: str(u.id),
47192
+ position: heroPosition(u),
47193
+ name: str(u.name),
47194
+ // lolo uses `team` field (not `owner`)
47195
+ team: str(u.team) === "enemy" ? "enemy" : "player",
47196
+ health: num(u.health) || 100,
47197
+ maxHealth: num(u.maxHealth) || 100,
47198
+ sprite: u.sprite == null ? void 0 : str(u.sprite)
47182
47199
  })),
47183
- [heroes, propUnits]
47200
+ [entityUnits, propUnits]
47201
+ );
47202
+ const gameUnits = baseUnits;
47203
+ const selectedHero = useMemo(
47204
+ () => gameUnits.find((u) => u.id === selectedHeroId) ?? null,
47205
+ [gameUnits, selectedHeroId]
47184
47206
  );
47185
47207
  const MOVE_SPEED_MS_PER_TILE = 300;
47186
47208
  const movementAnimRef = useRef(null);
@@ -47226,51 +47248,50 @@ function WorldMapBoard({
47226
47248
  const validMoves = useMemo(() => {
47227
47249
  if (!selectedHero || heroMovement(selectedHero) <= 0) return [];
47228
47250
  const sp = heroPosition(selectedHero);
47229
- const sOwner = heroOwner(selectedHero);
47251
+ const sTeam = str(selectedHero.team);
47230
47252
  const range = heroMovement(selectedHero);
47231
47253
  const moves = [];
47232
- hexes.forEach((hex) => {
47233
- const hx = num(hex.x);
47234
- const hy = num(hex.y);
47235
- if (!hexPassable(hex)) return;
47236
- if (hx === sp.x && hy === sp.y) return;
47237
- if (!isInRange(sp, { x: hx, y: hy }, range)) return;
47238
- if (heroes.some((h) => {
47239
- const hp = heroPosition(h);
47240
- return hp.x === hx && hp.y === hy && heroOwner(h) === sOwner;
47254
+ tiles.forEach((t) => {
47255
+ const tx = t.x;
47256
+ const ty = t.y;
47257
+ if (t.passable === false) return;
47258
+ if (tx === sp.x && ty === sp.y) return;
47259
+ if (!isInRange(sp, { x: tx, y: ty }, range)) return;
47260
+ if (gameUnits.some((u) => {
47261
+ const up = u.position ?? { x: u.x ?? -1, y: u.y ?? -1 };
47262
+ return up.x === tx && up.y === ty && str(u.team) === sTeam;
47241
47263
  })) return;
47242
- moves.push({ x: hx, y: hy });
47264
+ moves.push({ x: tx, y: ty });
47243
47265
  });
47244
47266
  return moves;
47245
- }, [selectedHero, hexes, heroes, isInRange]);
47267
+ }, [selectedHero, tiles, gameUnits, isInRange]);
47246
47268
  const attackTargets = useMemo(() => {
47247
47269
  if (!selectedHero || heroMovement(selectedHero) <= 0) return [];
47248
47270
  const sp = heroPosition(selectedHero);
47249
- const sOwner = heroOwner(selectedHero);
47271
+ const sTeam = str(selectedHero.team);
47250
47272
  const range = heroMovement(selectedHero);
47251
- return heroes.filter((h) => heroOwner(h) !== sOwner).filter((h) => isInRange(sp, heroPosition(h), range)).map((h) => heroPosition(h));
47252
- }, [selectedHero, heroes, isInRange]);
47253
- const maxY = Math.max(...hexes.map((h) => num(h.y)), 0);
47273
+ 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 });
47274
+ }, [selectedHero, gameUnits, isInRange]);
47275
+ const maxY = Math.max(...tiles.map((t) => t.y), 0);
47254
47276
  const baseOffsetX = (maxY + 1) * (TILE_WIDTH * scale / 2);
47255
47277
  const tileToScreen = useCallback(
47256
47278
  (tx, ty) => isoToScreen(tx, ty, scale, baseOffsetX),
47257
47279
  [scale, baseOffsetX]
47258
47280
  );
47259
47281
  const hoveredHex = useMemo(
47260
- () => hoveredTile ? hexes.find((h) => num(h.x) === hoveredTile.x && num(h.y) === hoveredTile.y) ?? null : null,
47261
- [hoveredTile, hexes]
47282
+ () => hoveredTile ? tiles.find((t) => t.x === hoveredTile.x && t.y === hoveredTile.y) ?? null : null,
47283
+ [hoveredTile, tiles]
47262
47284
  );
47263
47285
  const hoveredHero = useMemo(
47264
- () => hoveredTile ? heroes.find((h) => {
47265
- const hp = heroPosition(h);
47266
- return hp.x === hoveredTile.x && hp.y === hoveredTile.y;
47286
+ () => hoveredTile ? gameUnits.find((u) => {
47287
+ const up = u.position ?? { x: u.x ?? -1, y: u.y ?? -1 };
47288
+ return up.x === hoveredTile.x && up.y === hoveredTile.y;
47267
47289
  }) ?? null : null,
47268
- [hoveredTile, heroes]
47290
+ [hoveredTile, gameUnits]
47269
47291
  );
47270
47292
  const handleTileClick = useCallback((x, y) => {
47271
47293
  if (movementAnimRef.current) return;
47272
- const hex = hexes.find((h) => num(h.x) === x && num(h.y) === y);
47273
- if (!hex) return;
47294
+ const tile = tiles.find((t) => t.x === x && t.y === y);
47274
47295
  if (tileClickEvent) {
47275
47296
  eventBus.emit(`UI:${tileClickEvent}`, { x, y });
47276
47297
  }
@@ -47281,38 +47302,39 @@ function WorldMapBoard({
47281
47302
  if (heroMoveEvent) {
47282
47303
  eventBus.emit(`UI:${heroMoveEvent}`, { heroId, toX: x, toY: y });
47283
47304
  }
47284
- const feature = str(hex.feature);
47305
+ const feature = tile ? str(tile.feature) : "";
47285
47306
  if (feature && feature !== "none") {
47286
- onFeatureEnter?.(heroId, hex);
47307
+ const tileRow = tile;
47308
+ onFeatureEnter?.(heroId, tileRow);
47287
47309
  if (featureEnterEvent) {
47288
- eventBus.emit(`UI:${featureEnterEvent}`, { heroId, feature, hex });
47310
+ eventBus.emit(`UI:${featureEnterEvent}`, { heroId, feature, hex: tileRow });
47289
47311
  }
47290
47312
  }
47291
47313
  });
47292
47314
  return;
47293
47315
  }
47294
- const enemy = heroes.find((h) => {
47295
- const hp = heroPosition(h);
47296
- return hp.x === x && hp.y === y && heroOwner(h) === "enemy";
47316
+ const enemy = gameUnits.find((u) => {
47317
+ const up = u.position ?? { x: u.x ?? -1, y: u.y ?? -1 };
47318
+ return up.x === x && up.y === y && str(u.team) === "enemy";
47297
47319
  });
47298
47320
  if (selectedHero && enemy && attackTargets.some((t) => t.x === x && t.y === y)) {
47299
47321
  const attackerId = str(selectedHero.id);
47300
- const defenderId = str(enemy.id);
47322
+ const defenderId = enemy.id;
47301
47323
  onBattleEncounter?.(attackerId, defenderId);
47302
47324
  if (battleEncounterEvent) {
47303
47325
  eventBus.emit(`UI:${battleEncounterEvent}`, { attackerId, defenderId });
47304
47326
  }
47305
47327
  }
47306
- }, [hexes, heroes, selectedHero, validMoves, attackTargets, startMoveAnimation, onHeroMove, onFeatureEnter, onBattleEncounter, eventBus, tileClickEvent, heroMoveEvent, featureEnterEvent, battleEncounterEvent]);
47328
+ }, [tiles, gameUnits, selectedHero, validMoves, attackTargets, startMoveAnimation, onHeroMove, onFeatureEnter, onBattleEncounter, eventBus, tileClickEvent, heroMoveEvent, featureEnterEvent, battleEncounterEvent]);
47307
47329
  const handleUnitClick = useCallback((unitId) => {
47308
- const hero = heroes.find((h) => str(h.id) === unitId);
47309
- if (hero && (heroOwner(hero) === "player" || allowMoveAllHeroes)) {
47330
+ const unit = gameUnits.find((u) => u.id === unitId);
47331
+ if (unit && (str(unit.team) === "player" || allowMoveAllHeroes)) {
47310
47332
  onHeroSelect?.(unitId);
47311
47333
  if (heroSelectEvent) {
47312
47334
  eventBus.emit(`UI:${heroSelectEvent}`, { heroId: unitId });
47313
47335
  }
47314
47336
  }
47315
- }, [heroes, onHeroSelect, allowMoveAllHeroes, eventBus, heroSelectEvent]);
47337
+ }, [gameUnits, onHeroSelect, allowMoveAllHeroes, eventBus, heroSelectEvent]);
47316
47338
  const selectHero = useCallback((id) => {
47317
47339
  onHeroSelect?.(id);
47318
47340
  if (heroSelectEvent) {