@almadar/ui 5.104.0 → 5.105.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/avl/index.js CHANGED
@@ -11203,7 +11203,7 @@ function screenToIso(screenX, screenY, scale, baseOffsetX, layout = "isometric")
11203
11203
  const tileY = (screenY / (scaledFloorHeight / 2) - adjustedX / (scaledTileWidth / 2)) / 2;
11204
11204
  return { x: Math.round(tileX), y: Math.round(tileY) };
11205
11205
  }
11206
- var TILE_WIDTH, TILE_HEIGHT, FLOOR_HEIGHT, DIAMOND_TOP_Y, FEATURE_COLORS;
11206
+ var TILE_WIDTH, TILE_HEIGHT, FLOOR_HEIGHT, DIAMOND_TOP_Y, FEATURE_COLORS, TERRAIN_COLORS, TEAM_COLORS, TEAM_SHADOW_COLORS, HIGHLIGHT_COLORS, SELECTION_RING_COLOR, GRID_STROKE_COLOR, BACKGROUND_FALLBACK_COLOR, MINIMAP_TERRAIN_COLORS, MINIMAP_UNIT_COLORS, UNIT_LABEL_BG_COLORS, SIDE_PLATFORM_COLORS, SIDE_PLAYER_COLOR, SIDE_PLAYER_EYE_COLOR, SIDE_SKY_GRADIENT_TOP, SIDE_SKY_GRADIENT_BOTTOM, SIDE_GRID_COLOR, SIDE_FX_COLOR, SIDE_PLATFORM_BEVEL_COLOR, SIDE_PLATFORM_SHADOW_COLOR, SIDE_HAZARD_STRIPE_COLOR, SIDE_GOAL_GLOW_COLOR, DEFAULT_BG_COLOR;
11207
11207
  var init_isometric = __esm({
11208
11208
  "components/game/shared/isometric.ts"() {
11209
11209
  TILE_WIDTH = 256;
@@ -11220,14 +11220,79 @@ var init_isometric = __esm({
11220
11220
  mountain: "#78716c",
11221
11221
  default: "#9ca3af"
11222
11222
  };
11223
+ TERRAIN_COLORS = {
11224
+ water: "#3b82f6",
11225
+ mountain: "#78716c",
11226
+ stone: "#9ca3af",
11227
+ default: "#4ade80"
11228
+ };
11229
+ TEAM_COLORS = {
11230
+ player: "#3b82f6",
11231
+ enemy: "#ef4444",
11232
+ neutral: "#6b7280",
11233
+ default: "#6b7280"
11234
+ };
11235
+ TEAM_SHADOW_COLORS = {
11236
+ player: "rgba(0,150,255,0.6)",
11237
+ default: "rgba(255,50,50,0.6)"
11238
+ };
11239
+ HIGHLIGHT_COLORS = {
11240
+ hover: "rgba(255,255,255,0.15)",
11241
+ validMove: "rgba(74,222,128,0.25)",
11242
+ attack: "rgba(239,68,68,0.35)"
11243
+ };
11244
+ SELECTION_RING_COLOR = "rgba(0,200,255,0.8)";
11245
+ GRID_STROKE_COLOR = "rgba(0,0,0,0.2)";
11246
+ BACKGROUND_FALLBACK_COLOR = "#1a1a2e";
11247
+ MINIMAP_TERRAIN_COLORS = {
11248
+ water: "#3b82f6",
11249
+ mountain: "#78716c",
11250
+ default: "#4ade80"
11251
+ };
11252
+ MINIMAP_UNIT_COLORS = {
11253
+ player: "#60a5fa",
11254
+ enemy: "#f87171",
11255
+ neutral: "#9ca3af",
11256
+ default: "#9ca3af"
11257
+ };
11258
+ UNIT_LABEL_BG_COLORS = {
11259
+ player: "rgba(59,130,246,0.9)",
11260
+ enemy: "rgba(239,68,68,0.9)",
11261
+ neutral: "rgba(107,114,128,0.9)",
11262
+ default: "rgba(107,114,128,0.9)"
11263
+ };
11264
+ SIDE_PLATFORM_COLORS = {
11265
+ ground: "#4a7c59",
11266
+ platform: "#7c6b4a",
11267
+ hazard: "#c0392b",
11268
+ goal: "#f1c40f"
11269
+ };
11270
+ SIDE_PLAYER_COLOR = "#3498db";
11271
+ SIDE_PLAYER_EYE_COLOR = "#ffffff";
11272
+ SIDE_SKY_GRADIENT_TOP = "#1a1a2e";
11273
+ SIDE_SKY_GRADIENT_BOTTOM = "#16213e";
11274
+ SIDE_GRID_COLOR = "rgba(255, 255, 255, 0.03)";
11275
+ SIDE_FX_COLOR = "#ffe066";
11276
+ SIDE_PLATFORM_BEVEL_COLOR = "rgba(255,255,255,0.15)";
11277
+ SIDE_PLATFORM_SHADOW_COLOR = "rgba(0,0,0,0.3)";
11278
+ SIDE_HAZARD_STRIPE_COLOR = "#e74c3c";
11279
+ SIDE_GOAL_GLOW_COLOR = "rgba(241, 196, 15, 0.5)";
11280
+ DEFAULT_BG_COLOR = "#5c94fc";
11223
11281
  }
11224
11282
  });
11225
11283
 
11226
11284
  // components/game/shared/spriteSheetConstants.ts
11227
- var SHEET_COLUMNS;
11285
+ var SHEET_COLUMNS, SPRITE_SHEET_LAYOUT;
11228
11286
  var init_spriteSheetConstants = __esm({
11229
11287
  "components/game/shared/spriteSheetConstants.ts"() {
11230
11288
  SHEET_COLUMNS = 8;
11289
+ SPRITE_SHEET_LAYOUT = {
11290
+ idle: { row: 0, frames: 4, frameRate: 6, loop: true },
11291
+ walk: { row: 1, frames: 8, frameRate: 10, loop: true },
11292
+ attack: { row: 2, frames: 6, frameRate: 12, loop: false },
11293
+ hit: { row: 3, frames: 3, frameRate: 8, loop: false },
11294
+ death: { row: 4, frames: 6, frameRate: 8, loop: false }
11295
+ };
11231
11296
  }
11232
11297
  });
11233
11298
 
@@ -13960,7 +14025,7 @@ var init_useUnitSpriteAtlas = __esm({
13960
14025
  function normalizeBackdrop(bg) {
13961
14026
  return typeof bg === "string" ? { url: bg, role: "decoration", category: "background" } : bg;
13962
14027
  }
13963
- function SideView({
14028
+ function SideCanvas2D({
13964
14029
  player,
13965
14030
  platforms,
13966
14031
  worldWidth,
@@ -13981,24 +14046,18 @@ function SideView({
13981
14046
  const canvasRef = useRef(null);
13982
14047
  const eventBus = useEventBus();
13983
14048
  const keysRef = useRef(/* @__PURE__ */ new Set());
13984
- const imageCache2 = useRef(/* @__PURE__ */ new Map());
13985
- const [loadedImages, setLoadedImages] = useState(/* @__PURE__ */ new Set());
13986
- const loadImage = useCallback((url) => {
13987
- if (!url) return null;
13988
- const cached = imageCache2.current.get(url);
13989
- if (cached?.complete && cached.naturalWidth > 0) {
13990
- if (!loadedImages.has(url)) setLoadedImages((prev) => new Set(prev).add(url));
13991
- return cached;
13992
- }
13993
- if (!cached) {
13994
- const img = new Image();
13995
- img.crossOrigin = "anonymous";
13996
- img.src = url;
13997
- img.onload = () => setLoadedImages((prev) => new Set(prev).add(url));
13998
- imageCache2.current.set(url, img);
14049
+ const spriteUrls = useMemo(() => {
14050
+ const urls = [];
14051
+ if (backgroundImage?.url) urls.push(backgroundImage.url);
14052
+ if (playerSprite?.url) urls.push(playerSprite.url);
14053
+ if (tileSprites) {
14054
+ for (const asset of Object.values(tileSprites)) {
14055
+ if (asset?.url) urls.push(asset.url);
14056
+ }
13999
14057
  }
14000
- return null;
14001
- }, [loadedImages]);
14058
+ return urls;
14059
+ }, [backgroundImage, playerSprite, tileSprites]);
14060
+ const { getImage } = useImageCache(spriteUrls);
14002
14061
  useEffect(() => {
14003
14062
  if (typeof window === "undefined") return;
14004
14063
  const canvas = canvasRef.current;
@@ -14111,7 +14170,7 @@ function SideView({
14111
14170
  camX = Math.max(0, Math.min(px - cw / 2, ww - cw));
14112
14171
  camY = Math.max(0, Math.min(py - ch / 2 - 50, wh - ch));
14113
14172
  }
14114
- const bgImage = bgImg ? loadImage(bgImg.url) : null;
14173
+ const bgImage = bgImg ? getImage(bgImg.url) : void 0;
14115
14174
  const bgSrc = bgImage ? resolveAssetSource(bgImage, bgImg, NOOP) : null;
14116
14175
  if (bgSrc) {
14117
14176
  blit(ctx, bgSrc, 0, 0, cw, ch);
@@ -14122,12 +14181,12 @@ function SideView({
14122
14181
  ctx.fillRect(0, 0, cw, ch);
14123
14182
  } else {
14124
14183
  const grad = ctx.createLinearGradient(0, 0, 0, ch);
14125
- grad.addColorStop(0, SKY_GRADIENT_TOP);
14126
- grad.addColorStop(1, SKY_GRADIENT_BOTTOM);
14184
+ grad.addColorStop(0, SIDE_SKY_GRADIENT_TOP);
14185
+ grad.addColorStop(1, SIDE_SKY_GRADIENT_BOTTOM);
14127
14186
  ctx.fillStyle = grad;
14128
14187
  ctx.fillRect(0, 0, cw, ch);
14129
14188
  }
14130
- ctx.strokeStyle = GRID_COLOR;
14189
+ ctx.strokeStyle = SIDE_GRID_COLOR;
14131
14190
  ctx.lineWidth = 1;
14132
14191
  const gridSize = 32;
14133
14192
  for (let gx = -camX % gridSize; gx < cw; gx += gridSize) {
@@ -14147,7 +14206,7 @@ function SideView({
14147
14206
  const platY = plat.y - camY;
14148
14207
  const platType = plat.type ?? "ground";
14149
14208
  const spriteAsset = tSprites?.[platType];
14150
- const tileImg = spriteAsset ? loadImage(spriteAsset.url) : null;
14209
+ const tileImg = spriteAsset ? getImage(spriteAsset.url) : void 0;
14151
14210
  const tileSrc = tileImg ? resolveAssetSource(tileImg, spriteAsset, NOOP) : null;
14152
14211
  if (tileSrc) {
14153
14212
  const originX = tileSrc.rect ? tileSrc.rect.sx : 0;
@@ -14162,15 +14221,15 @@ function SideView({
14162
14221
  ctx.drawImage(tileImg, originX, originY, srcW, tileH, platX + tx, platY, drawW, plat.height);
14163
14222
  }
14164
14223
  } else {
14165
- const color = PLATFORM_COLORS[platType] ?? PLATFORM_COLORS.ground;
14224
+ const color = SIDE_PLATFORM_COLORS[platType] ?? SIDE_PLATFORM_COLORS.ground;
14166
14225
  ctx.fillStyle = color;
14167
14226
  ctx.fillRect(platX, platY, plat.width, plat.height);
14168
- ctx.fillStyle = "rgba(255, 255, 255, 0.15)";
14227
+ ctx.fillStyle = SIDE_PLATFORM_BEVEL_COLOR;
14169
14228
  ctx.fillRect(platX, platY, plat.width, 3);
14170
- ctx.fillStyle = "rgba(0, 0, 0, 0.3)";
14229
+ ctx.fillStyle = SIDE_PLATFORM_SHADOW_COLOR;
14171
14230
  ctx.fillRect(platX, platY + plat.height - 2, plat.width, 2);
14172
14231
  if (platType === "hazard") {
14173
- ctx.strokeStyle = "#e74c3c";
14232
+ ctx.strokeStyle = SIDE_HAZARD_STRIPE_COLOR;
14174
14233
  ctx.lineWidth = 2;
14175
14234
  for (let sx = platX; sx < platX + plat.width; sx += 12) {
14176
14235
  ctx.beginPath();
@@ -14180,7 +14239,7 @@ function SideView({
14180
14239
  }
14181
14240
  }
14182
14241
  if (platType === "goal") {
14183
- ctx.fillStyle = "rgba(241, 196, 15, 0.5)";
14242
+ ctx.fillStyle = SIDE_GOAL_GLOW_COLOR;
14184
14243
  ctx.beginPath();
14185
14244
  ctx.arc(platX + plat.width / 2, platY - 10, 8, 0, Math.PI * 2);
14186
14245
  ctx.fill();
@@ -14192,7 +14251,7 @@ function SideView({
14192
14251
  const ppx = px - camX;
14193
14252
  const ppy = py - camY;
14194
14253
  const facingRight = auth.facingRight ?? true;
14195
- const playerImg = pSprite ? loadImage(pSprite.url) : null;
14254
+ const playerImg = pSprite ? getImage(pSprite.url) : void 0;
14196
14255
  const playerSrc = playerImg ? resolveAssetSource(playerImg, pSprite, NOOP) : null;
14197
14256
  if (playerSrc) {
14198
14257
  ctx.save();
@@ -14205,7 +14264,7 @@ function SideView({
14205
14264
  }
14206
14265
  ctx.restore();
14207
14266
  } else {
14208
- ctx.fillStyle = PLAYER_COLOR;
14267
+ ctx.fillStyle = SIDE_PLAYER_COLOR;
14209
14268
  const radius = Math.min(pw, ph) * 0.25;
14210
14269
  ctx.beginPath();
14211
14270
  ctx.moveTo(ppx + radius, ppy);
@@ -14221,7 +14280,7 @@ function SideView({
14221
14280
  const eyeY = ppy + ph * 0.3;
14222
14281
  const eyeSize = 3;
14223
14282
  const eyeOffsetX = facingRight ? pw * 0.55 : pw * 0.2;
14224
- ctx.fillStyle = PLAYER_EYE_COLOR;
14283
+ ctx.fillStyle = SIDE_PLAYER_EYE_COLOR;
14225
14284
  ctx.beginPath();
14226
14285
  ctx.arc(ppx + eyeOffsetX, eyeY, eyeSize, 0, Math.PI * 2);
14227
14286
  ctx.fill();
@@ -14235,7 +14294,7 @@ function SideView({
14235
14294
  const alpha = Math.min(1, fx.ttl / 4);
14236
14295
  const prev = ctx.globalAlpha;
14237
14296
  ctx.globalAlpha = alpha;
14238
- ctx.fillStyle = "#ffe066";
14297
+ ctx.fillStyle = SIDE_FX_COLOR;
14239
14298
  ctx.beginPath();
14240
14299
  ctx.arc(fxScreenX, fxScreenY, 12, 0, Math.PI * 2);
14241
14300
  ctx.fill();
@@ -14243,7 +14302,7 @@ function SideView({
14243
14302
  }
14244
14303
  };
14245
14304
  return interp.startLoop(drawFrame);
14246
- }, [interp.startLoop, loadImage]);
14305
+ }, [interp.startLoop, getImage]);
14247
14306
  return /* @__PURE__ */ jsx(
14248
14307
  "canvas",
14249
14308
  {
@@ -14255,6 +14314,33 @@ function SideView({
14255
14314
  }
14256
14315
  );
14257
14316
  }
14317
+ var NOOP;
14318
+ var init_SideCanvas2D = __esm({
14319
+ "components/game/2d/molecules/SideCanvas2D.tsx"() {
14320
+ "use client";
14321
+ init_cn();
14322
+ init_useEventBus();
14323
+ init_useImageCache();
14324
+ init_atlasSlice();
14325
+ init_useRenderInterpolation();
14326
+ init_verificationRegistry();
14327
+ init_isometric();
14328
+ NOOP = () => {
14329
+ };
14330
+ SideCanvas2D.displayName = "SideCanvas2D";
14331
+ }
14332
+ });
14333
+ function normalizeBackdrop2(bg) {
14334
+ return typeof bg === "string" ? { url: bg, role: "decoration", category: "background" } : bg;
14335
+ }
14336
+ function resolveTileFallbackColor(tile) {
14337
+ const override = tile.color;
14338
+ return override ?? TERRAIN_COLORS[tile.terrain ?? ""] ?? TERRAIN_COLORS.default;
14339
+ }
14340
+ function resolveUnitTeamColor(unit) {
14341
+ const override = unit.tint;
14342
+ return override ?? TEAM_COLORS[unit.team ?? ""] ?? TEAM_COLORS.default;
14343
+ }
14258
14344
  function Canvas2D({
14259
14345
  // Closed-circuit
14260
14346
  className,
@@ -14304,13 +14390,13 @@ function Canvas2D({
14304
14390
  // Side-view asset resolution
14305
14391
  playerSprite,
14306
14392
  tileSprites,
14307
- bgColor = "#5c94fc",
14393
+ bgColor = DEFAULT_BG_COLOR,
14308
14394
  worldWidth = 800,
14309
14395
  worldHeight = 400,
14310
14396
  // Remote asset loading
14311
14397
  assetManifest
14312
14398
  }) {
14313
- const backgroundImage = normalizeBackdrop(backgroundImageRaw);
14399
+ const backgroundImage = normalizeBackdrop2(backgroundImageRaw);
14314
14400
  const isSide = projection === "side";
14315
14401
  const isFree = projection === "free";
14316
14402
  const flatLike = projection === "hex" || projection === "flat" || isFree;
@@ -14494,7 +14580,7 @@ function Canvas2D({
14494
14580
  return sortedTiles.map((t2) => ({
14495
14581
  x: t2.x,
14496
14582
  y: t2.y,
14497
- color: t2.terrain === "water" ? "#3b82f6" : t2.terrain === "mountain" ? "#78716c" : "#4ade80"
14583
+ color: MINIMAP_TERRAIN_COLORS[t2.terrain ?? ""] ?? MINIMAP_TERRAIN_COLORS.default
14498
14584
  }));
14499
14585
  }, [showMinimap, sortedTiles]);
14500
14586
  const miniMapUnits = useMemo(() => {
@@ -14502,7 +14588,7 @@ function Canvas2D({
14502
14588
  return units.filter((u) => u.position).map((u) => ({
14503
14589
  x: u.position.x,
14504
14590
  y: u.position.y,
14505
- color: u.team === "player" ? "#60a5fa" : u.team === "enemy" ? "#f87171" : "#9ca3af",
14591
+ color: MINIMAP_UNIT_COLORS[u.team ?? ""] ?? MINIMAP_UNIT_COLORS.default,
14506
14592
  isPlayer: u.team === "player"
14507
14593
  }));
14508
14594
  }, [showMinimap, units]);
@@ -14539,7 +14625,7 @@ function Canvas2D({
14539
14625
  }
14540
14626
  }
14541
14627
  } else {
14542
- ctx.fillStyle = "#1a1a2e";
14628
+ ctx.fillStyle = BACKGROUND_FALLBACK_COLOR;
14543
14629
  ctx.fillRect(0, 0, viewportSize.width, viewportSize.height);
14544
14630
  }
14545
14631
  if (sortedTiles.length === 0 && units.length === 0 && features.length === 0) return;
@@ -14569,15 +14655,15 @@ function Canvas2D({
14569
14655
  } else if (img && img.naturalWidth === 0) {
14570
14656
  ctx.drawImage(img, pos.x, pos.y, scaledTileWidth, scaledTileHeight);
14571
14657
  } else if (squareGrid) {
14572
- ctx.fillStyle = tile.terrain === "water" ? "#3b82f6" : tile.terrain === "mountain" ? "#78716c" : tile.terrain === "stone" ? "#9ca3af" : "#4ade80";
14658
+ ctx.fillStyle = resolveTileFallbackColor(tile);
14573
14659
  ctx.fillRect(pos.x, pos.y, scaledTileWidth, scaledTileWidth);
14574
- ctx.strokeStyle = "rgba(0,0,0,0.2)";
14660
+ ctx.strokeStyle = GRID_STROKE_COLOR;
14575
14661
  ctx.lineWidth = 1;
14576
14662
  ctx.strokeRect(pos.x, pos.y, scaledTileWidth, scaledTileWidth);
14577
14663
  } else {
14578
14664
  const centerX = pos.x + scaledTileWidth / 2;
14579
14665
  const topY = pos.y + scaledDiamondTopY;
14580
- ctx.fillStyle = tile.terrain === "water" ? "#3b82f6" : tile.terrain === "mountain" ? "#78716c" : tile.terrain === "stone" ? "#9ca3af" : "#4ade80";
14666
+ ctx.fillStyle = resolveTileFallbackColor(tile);
14581
14667
  ctx.beginPath();
14582
14668
  ctx.moveTo(centerX, topY);
14583
14669
  ctx.lineTo(pos.x + scaledTileWidth, topY + scaledFloorHeight / 2);
@@ -14585,7 +14671,7 @@ function Canvas2D({
14585
14671
  ctx.lineTo(pos.x, topY + scaledFloorHeight / 2);
14586
14672
  ctx.closePath();
14587
14673
  ctx.fill();
14588
- ctx.strokeStyle = "rgba(0,0,0,0.2)";
14674
+ ctx.strokeStyle = GRID_STROKE_COLOR;
14589
14675
  ctx.lineWidth = 1;
14590
14676
  ctx.stroke();
14591
14677
  }
@@ -14606,11 +14692,11 @@ function Canvas2D({
14606
14692
  ctx.fill();
14607
14693
  };
14608
14694
  if (hoveredTile && hoveredTile.x === tile.x && hoveredTile.y === tile.y) {
14609
- drawHighlight("rgba(255, 255, 255, 0.15)");
14695
+ drawHighlight(HIGHLIGHT_COLORS.hover);
14610
14696
  }
14611
14697
  const tileKey = `${tile.x},${tile.y}`;
14612
- if (validMoveSet.has(tileKey)) drawHighlight("rgba(74, 222, 128, 0.25)");
14613
- if (attackTargetSet.has(tileKey)) drawHighlight("rgba(239, 68, 68, 0.35)");
14698
+ if (validMoveSet.has(tileKey)) drawHighlight(HIGHLIGHT_COLORS.validMove);
14699
+ if (attackTargetSet.has(tileKey)) drawHighlight(HIGHLIGHT_COLORS.attack);
14614
14700
  if (debug2) {
14615
14701
  const centerX = pos.x + scaledTileWidth / 2;
14616
14702
  const centerY = squareGrid ? pos.y + scaledTileWidth / 2 : pos.y + scaledFloorHeight / 2 + scaledDiamondTopY;
@@ -14684,9 +14770,9 @@ function Canvas2D({
14684
14770
  const maxUnitW = scaledTileWidth * spriteMaxWidthRatio * unitScale;
14685
14771
  const unitIsSheet = unit.spriteSheet?.url !== void 0;
14686
14772
  const unitSrc = img && !unitIsSheet ? resolveAssetSource(img, unitAsset, bumpAtlas) : null;
14687
- const SHEET_ROWS = 5;
14688
14773
  const sheetFrameW = img ? img.naturalWidth / SHEET_COLUMNS : 0;
14689
- const sheetFrameH = img ? img.naturalHeight / SHEET_ROWS : 0;
14774
+ const sheetFrameH = img ? img.naturalHeight / LEGACY_SHEET_ROWS : 0;
14775
+ const legacyFrame = frameRect(0, 0, SHEET_COLUMNS, sheetFrameW, sheetFrameH);
14690
14776
  const frameW = unitIsSheet ? sheetFrameW : img?.naturalWidth ?? 1;
14691
14777
  const frameH = unitIsSheet ? sheetFrameH : img?.naturalHeight ?? 1;
14692
14778
  const ar = unitIsSheet ? sheetFrameW / (sheetFrameH || 1) : unitSrc ? unitSrc.aspect : frameW / (frameH || 1);
@@ -14704,17 +14790,16 @@ function Canvas2D({
14704
14790
  ctx.globalAlpha = 0.25;
14705
14791
  if (img) {
14706
14792
  if (unitIsSheet) {
14707
- ctx.drawImage(img, 0, 0, sheetFrameW, sheetFrameH, ghostCenterX - drawW / 2, ghostGroundY - drawH, drawW, drawH);
14793
+ ctx.drawImage(img, legacyFrame.sx, legacyFrame.sy, legacyFrame.sw, legacyFrame.sh, ghostCenterX - drawW / 2, ghostGroundY - drawH, drawW, drawH);
14708
14794
  } else if (unitSrc) {
14709
14795
  blit(ctx, unitSrc, ghostCenterX - drawW / 2, ghostGroundY - drawH, drawW, drawH);
14710
14796
  } else {
14711
14797
  ctx.drawImage(img, ghostCenterX - drawW / 2, ghostGroundY - drawH, drawW, drawH);
14712
14798
  }
14713
14799
  } else {
14714
- const color = unit.team === "player" ? "#3b82f6" : unit.team === "enemy" ? "#ef4444" : "#6b7280";
14715
14800
  ctx.beginPath();
14716
14801
  ctx.arc(ghostCenterX, ghostGroundY - 16 * scale, 20 * scale, 0, Math.PI * 2);
14717
- ctx.fillStyle = color;
14802
+ ctx.fillStyle = resolveUnitTeamColor(unit);
14718
14803
  ctx.fill();
14719
14804
  }
14720
14805
  ctx.restore();
@@ -14722,7 +14807,7 @@ function Canvas2D({
14722
14807
  if (isSelected) {
14723
14808
  ctx.beginPath();
14724
14809
  ctx.ellipse(centerX, groundY, drawW / 2 + 4 * scale, 12 * scale, 0, 0, Math.PI * 2);
14725
- ctx.strokeStyle = "rgba(0, 200, 255, 0.8)";
14810
+ ctx.strokeStyle = SELECTION_RING_COLOR;
14726
14811
  ctx.lineWidth = 3;
14727
14812
  ctx.stroke();
14728
14813
  }
@@ -14739,7 +14824,7 @@ function Canvas2D({
14739
14824
  const spriteY = groundY - fDrawH - (frame.applyBreathing ? breatheOffset : 0);
14740
14825
  ctx.save();
14741
14826
  if (unit.team) {
14742
- ctx.shadowColor = unit.team === "player" ? "rgba(0, 150, 255, 0.6)" : "rgba(255, 50, 50, 0.6)";
14827
+ ctx.shadowColor = TEAM_SHADOW_COLORS[unit.team] ?? TEAM_SHADOW_COLORS.default;
14743
14828
  ctx.shadowBlur = 12 * scale;
14744
14829
  }
14745
14830
  if (frame.flipX) {
@@ -14754,7 +14839,7 @@ function Canvas2D({
14754
14839
  const spriteY = groundY - drawH - breatheOffset;
14755
14840
  const drawUnit = (x) => {
14756
14841
  if (unitIsSheet) {
14757
- ctx.drawImage(img, 0, 0, sheetFrameW, sheetFrameH, x, spriteY, drawW, drawH);
14842
+ ctx.drawImage(img, legacyFrame.sx, legacyFrame.sy, legacyFrame.sw, legacyFrame.sh, x, spriteY, drawW, drawH);
14758
14843
  } else if (unitSrc) {
14759
14844
  blit(ctx, unitSrc, x, spriteY, drawW, drawH);
14760
14845
  } else {
@@ -14763,7 +14848,7 @@ function Canvas2D({
14763
14848
  };
14764
14849
  if (unit.team) {
14765
14850
  ctx.save();
14766
- ctx.shadowColor = unit.team === "player" ? "rgba(0, 150, 255, 0.6)" : "rgba(255, 50, 50, 0.6)";
14851
+ ctx.shadowColor = TEAM_SHADOW_COLORS[unit.team] ?? TEAM_SHADOW_COLORS.default;
14767
14852
  ctx.shadowBlur = 12 * scale;
14768
14853
  drawUnit(centerX - drawW / 2);
14769
14854
  ctx.restore();
@@ -14771,10 +14856,9 @@ function Canvas2D({
14771
14856
  drawUnit(centerX - drawW / 2);
14772
14857
  }
14773
14858
  } else {
14774
- const color = unit.team === "player" ? "#3b82f6" : unit.team === "enemy" ? "#ef4444" : "#6b7280";
14775
14859
  ctx.beginPath();
14776
14860
  ctx.arc(centerX, groundY - 20 * scale - breatheOffset, 20 * scale, 0, Math.PI * 2);
14777
- ctx.fillStyle = color;
14861
+ ctx.fillStyle = resolveUnitTeamColor(unit);
14778
14862
  ctx.fill();
14779
14863
  ctx.strokeStyle = "rgba(255,255,255,0.8)";
14780
14864
  ctx.lineWidth = 2;
@@ -15013,7 +15097,7 @@ function Canvas2D({
15013
15097
  }
15014
15098
  if (isSide) {
15015
15099
  return /* @__PURE__ */ jsx(Box, { ref: containerRef, className: cn("relative overflow-hidden w-full h-full", className), children: /* @__PURE__ */ jsx(
15016
- SideView,
15100
+ SideCanvas2D,
15017
15101
  {
15018
15102
  player,
15019
15103
  platforms: platforms && platforms.length ? platforms : DEFAULT_PLATFORMS,
@@ -15111,7 +15195,7 @@ function Canvas2D({
15111
15195
  {
15112
15196
  as: "span",
15113
15197
  className: "text-white text-xs font-bold px-1.5 py-0.5 rounded mb-0.5 whitespace-nowrap block",
15114
- style: { background: unit.team === "player" ? "rgba(59,130,246,0.9)" : unit.team === "enemy" ? "rgba(239,68,68,0.9)" : "rgba(107,114,128,0.9)" },
15198
+ style: { background: UNIT_LABEL_BG_COLORS[unit.team ?? ""] ?? UNIT_LABEL_BG_COLORS.default },
15115
15199
  children: unit.name
15116
15200
  }
15117
15201
  ),
@@ -15135,7 +15219,7 @@ function Canvas2D({
15135
15219
  }
15136
15220
  );
15137
15221
  }
15138
- var PLATFORM_COLORS, NOOP, PLAYER_COLOR, PLAYER_EYE_COLOR, SKY_GRADIENT_TOP, SKY_GRADIENT_BOTTOM, GRID_COLOR, DEFAULT_PLATFORMS;
15222
+ var LEGACY_SHEET_ROWS, DEFAULT_PLATFORMS;
15139
15223
  var init_Canvas2D = __esm({
15140
15224
  "components/game/2d/molecules/Canvas2D.tsx"() {
15141
15225
  "use client";
@@ -15157,19 +15241,9 @@ var init_Canvas2D = __esm({
15157
15241
  init_verificationRegistry();
15158
15242
  init_isometric();
15159
15243
  init_spriteSheetConstants();
15160
- PLATFORM_COLORS = {
15161
- ground: "#4a7c59",
15162
- platform: "#7c6b4a",
15163
- hazard: "#c0392b",
15164
- goal: "#f1c40f"
15165
- };
15166
- NOOP = () => {
15167
- };
15168
- PLAYER_COLOR = "#3498db";
15169
- PLAYER_EYE_COLOR = "#ffffff";
15170
- SKY_GRADIENT_TOP = "#1a1a2e";
15171
- SKY_GRADIENT_BOTTOM = "#16213e";
15172
- GRID_COLOR = "rgba(255, 255, 255, 0.03)";
15244
+ init_spriteAnimation();
15245
+ init_SideCanvas2D();
15246
+ LEGACY_SHEET_ROWS = Object.keys(SPRITE_SHEET_LAYOUT).length;
15173
15247
  DEFAULT_PLATFORMS = [
15174
15248
  { x: 0, y: 368, width: 800, height: 32, type: "ground" },
15175
15249
  { x: 150, y: 280, width: 160, height: 16, type: "platform" },
@@ -33255,13 +33329,13 @@ var init_MapView = __esm({
33255
33329
  shadowSize: [41, 41]
33256
33330
  });
33257
33331
  L.Marker.prototype.options.icon = defaultIcon;
33258
- const { useEffect: useEffect71, useRef: useRef68, useCallback: useCallback104, useState: useState105 } = React105__default;
33332
+ const { useEffect: useEffect72, useRef: useRef69, useCallback: useCallback105, useState: useState105 } = React105__default;
33259
33333
  const { Typography: Typography2 } = await Promise.resolve().then(() => (init_Typography(), Typography_exports));
33260
33334
  const { useEventBus: useEventBus2 } = await Promise.resolve().then(() => (init_useEventBus(), useEventBus_exports));
33261
33335
  function MapUpdater({ centerLat, centerLng, zoom }) {
33262
33336
  const map = useMap();
33263
- const prevRef = useRef68({ centerLat, centerLng, zoom });
33264
- useEffect71(() => {
33337
+ const prevRef = useRef69({ centerLat, centerLng, zoom });
33338
+ useEffect72(() => {
33265
33339
  const prev = prevRef.current;
33266
33340
  if (prev.centerLat !== centerLat || prev.centerLng !== centerLng || prev.zoom !== zoom) {
33267
33341
  map.setView([centerLat, centerLng], zoom);
@@ -33272,7 +33346,7 @@ var init_MapView = __esm({
33272
33346
  }
33273
33347
  function MapClickHandler({ onMapClick }) {
33274
33348
  const map = useMap();
33275
- useEffect71(() => {
33349
+ useEffect72(() => {
33276
33350
  if (!onMapClick) return;
33277
33351
  const handler = (e) => {
33278
33352
  onMapClick(e.latlng.lat, e.latlng.lng);
@@ -33301,7 +33375,7 @@ var init_MapView = __esm({
33301
33375
  }) {
33302
33376
  const eventBus = useEventBus2();
33303
33377
  const [clickedPosition, setClickedPosition] = useState105(null);
33304
- const handleMapClick = useCallback104((lat, lng) => {
33378
+ const handleMapClick = useCallback105((lat, lng) => {
33305
33379
  if (showClickedPin) {
33306
33380
  setClickedPosition({ lat, lng });
33307
33381
  }
@@ -33310,7 +33384,7 @@ var init_MapView = __esm({
33310
33384
  eventBus.emit(`UI:${mapClickEvent}`, { latitude: lat, longitude: lng });
33311
33385
  }
33312
33386
  }, [onMapClick, mapClickEvent, eventBus, showClickedPin]);
33313
- const handleMarkerClick = useCallback104((marker) => {
33387
+ const handleMarkerClick = useCallback105((marker) => {
33314
33388
  onMarkerClick?.(marker);
33315
33389
  if (markerClickEvent) {
33316
33390
  eventBus.emit(`UI:${markerClickEvent}`, { ...marker });
@@ -126,7 +126,7 @@ export interface Canvas2DProps {
126
126
  /**
127
127
  * Opt-in: smooth-interpolate unit positions between LOLO tick snapshots
128
128
  * via a `requestAnimationFrame` loop (same fixed-timestep technique
129
- * `SideView` already uses for the player), instead of the default
129
+ * `SideCanvas2D` already uses for the player), instead of the default
130
130
  * tick-rate-cadence redraw. DEFAULT OFF — for continuous-movement boards
131
131
  * only; tile-snapped boards see no behavior change.
132
132
  */
@@ -0,0 +1,36 @@
1
+ /**
2
+ * SideCanvas2D
3
+ *
4
+ * Side-view platformer renderer — extracted from Canvas2D's private `SideView` branch.
5
+ * Physics is NOT here (it lives in the LOLO model); this only interpolates+draws the
6
+ * authoritative `player`/`platforms` props and translates keyboard input into the
7
+ * board's semantic events via keyMap/keyUpMap.
8
+ *
9
+ * @packageDocumentation
10
+ */
11
+ import * as React from 'react';
12
+ import type { Asset, AssetUrl } from '@almadar/core';
13
+ import type { ActiveEffect } from '../../shared/isometricTypes';
14
+ import type { Platform, SidePlayer } from './Canvas2D';
15
+ export interface SideCanvas2DProps {
16
+ player?: SidePlayer;
17
+ platforms: Platform[];
18
+ worldWidth: number;
19
+ worldHeight: number;
20
+ canvasWidth: number;
21
+ canvasHeight: number;
22
+ follow: boolean;
23
+ bgColor: string;
24
+ backgroundImage?: AssetUrl | Asset;
25
+ playerSprite?: Asset;
26
+ tileSprites?: Record<string, Asset>;
27
+ effects: ActiveEffect[];
28
+ keyMap?: Record<string, string>;
29
+ keyUpMap?: Record<string, string>;
30
+ className?: string;
31
+ }
32
+ export declare function SideCanvas2D({ player, platforms, worldWidth, worldHeight, canvasWidth, canvasHeight, follow, bgColor, backgroundImage: backgroundImageRaw, playerSprite, tileSprites, effects, keyMap, keyUpMap, className, }: SideCanvas2DProps): React.JSX.Element;
33
+ export declare namespace SideCanvas2D {
34
+ var displayName: string;
35
+ }
36
+ export default SideCanvas2D;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * EventMarker3D
3
+ *
4
+ * Floating combat/feedback marker for one `GameEvent` — billboarded text above
5
+ * the cell. Lifetime is LOLO-owned: boards append events on actions and expire
6
+ * them in their tick (same contract as the 2D effects array).
7
+ *
8
+ * @packageDocumentation
9
+ */
10
+ import React from 'react';
11
+ import type { GameEvent } from '../molecules/GameCanvas3D';
12
+ export interface EventMarker3DProps {
13
+ event: GameEvent;
14
+ position: [number, number, number];
15
+ }
16
+ export declare function EventMarker3D({ event, position }: EventMarker3DProps): React.JSX.Element;
17
+ export default EventMarker3D;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * SpriteBillboard3D
3
+ *
4
+ * Billboarded sprite-sheet plane for a unit. Loads the atlas's PNG sheet as a
5
+ * texture and crops a SINGLE frame via UV `repeat`/`offset`, advancing per
6
+ * animation state. Mirrors the 2D canvas: one frame, never the whole sheet.
7
+ *
8
+ * @packageDocumentation
9
+ */
10
+ import React from 'react';
11
+ import type { ResolvedFrame } from '../../shared/spriteAnimationTypes';
12
+ export interface SpriteBillboard3DProps {
13
+ sheetUrl: string;
14
+ resolveFrame: () => ResolvedFrame | null;
15
+ height?: number;
16
+ }
17
+ export declare function SpriteBillboard3D({ sheetUrl, resolveFrame, height, }: SpriteBillboard3DProps): React.JSX.Element | null;
18
+ export default SpriteBillboard3D;