@ntmats/hc-ui 0.3.0 → 0.5.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/index.js CHANGED
@@ -1,3 +1,7 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
+
1
5
  // src/index.tsx
2
6
  import { cloneElement, createContext, forwardRef, Fragment, useCallback, useContext, useEffect, useId, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from "react";
3
7
  import { createPortal } from "react-dom";
@@ -99,7 +103,7 @@ function SegmentedControl({
99
103
  );
100
104
  }
101
105
  var classes = (...names) => names.filter(Boolean).join(" ");
102
- var Button = forwardRef(function Button2({ variant = "secondary", size = "md", icon, iconEnd, fullWidth = false, loading = false, type = "button", className, children, onClick, ...props }, ref) {
106
+ var Button = forwardRef(function Button2({ variant = "secondary", size = "md", icon, iconEnd, fullWidth = false, loading = false, wrap = false, type = "button", className, children, onClick, ...props }, ref) {
103
107
  const handleClick = (event) => {
104
108
  if (loading) event.preventDefault();
105
109
  else onClick?.(event);
@@ -117,6 +121,7 @@ var Button = forwardRef(function Button2({ variant = "secondary", size = "md", i
117
121
  "data-variant": variant,
118
122
  "data-size": size,
119
123
  "data-full-width": fullWidth || void 0,
124
+ "data-wrap": wrap || void 0,
120
125
  children: [
121
126
  loading ? /* @__PURE__ */ jsx("span", { className: "hc-ui-button__spinner", "aria-hidden": "true" }) : icon && /* @__PURE__ */ jsx("span", { className: "hc-ui-button__icon", "aria-hidden": "true", children: icon }),
122
127
  /* @__PURE__ */ jsx("span", { className: "hc-ui-button__label", children }),
@@ -643,10 +648,9 @@ function useRovingRadios(options, value) {
643
648
  });
644
649
  return item;
645
650
  }
646
- function SheetOptions({ label, options, value, onChange, variant = "card", className, ...props }) {
651
+ function SheetOptions({ label, options, value, onChange, className, ...props }) {
647
652
  const item = useRovingRadios(options, value);
648
- const list = variant === "list";
649
- return /* @__PURE__ */ jsx("div", { ...props, className: classes("hc-ui-sheet-options", className), role: "radiogroup", "aria-label": label, "data-variant": list ? "list" : void 0, children: options.map((option, index) => {
653
+ return /* @__PURE__ */ jsx("div", { ...props, className: classes("hc-ui-sheet-options", className), role: "radiogroup", "aria-label": label, children: options.map((option, index) => {
650
654
  const on = option.value === value;
651
655
  return /* @__PURE__ */ jsxs(
652
656
  "button",
@@ -665,7 +669,7 @@ function SheetOptions({ label, options, value, onChange, variant = "card", class
665
669
  /* @__PURE__ */ jsx("span", { className: "hc-ui-sheet-options__label", children: option.label }),
666
670
  option.hint && /* @__PURE__ */ jsx("span", { className: "hc-ui-sheet-options__hint", children: option.hint })
667
671
  ] }),
668
- /* @__PURE__ */ jsx("span", { className: "hc-ui-sheet-options__check", "aria-hidden": "true", children: on && /* @__PURE__ */ jsx("svg", { width: list ? 20 : 18, height: list ? 20 : 18, viewBox: "0 0 24 24", fill: "none", strokeWidth: list ? 1.8 : 2.4, "aria-hidden": "true", focusable: "false", children: /* @__PURE__ */ jsx("path", { d: "M5 13L9 17L19 7", stroke: "currentColor", strokeLinecap: "round", strokeLinejoin: "round" }) }) })
672
+ /* @__PURE__ */ jsx("span", { className: "hc-ui-sheet-options__check", "aria-hidden": "true", children: on && /* @__PURE__ */ jsx("svg", { width: 18, height: 18, viewBox: "0 0 24 24", fill: "none", strokeWidth: 2.4, "aria-hidden": "true", focusable: "false", children: /* @__PURE__ */ jsx("path", { d: "M5 13L9 17L19 7", stroke: "currentColor", strokeLinecap: "round", strokeLinejoin: "round" }) }) })
669
673
  ]
670
674
  },
671
675
  option.value
@@ -1434,16 +1438,1829 @@ var CommandPalette = forwardRef(function CommandPalette2({
1434
1438
  }
1435
1439
  );
1436
1440
  });
1441
+ var cx = (...names) => names.filter(Boolean).join(" ");
1442
+ var flag = (on) => on ? "" : void 0;
1443
+ function useImageChain(sources) {
1444
+ const key = sources.join("\n");
1445
+ const [state, setState] = useState({ key, index: 0 });
1446
+ const index = state.key === key ? state.index : 0;
1447
+ if (state.key !== key) setState({ key, index: 0 });
1448
+ return {
1449
+ src: sources[index],
1450
+ index,
1451
+ /** Call from `onError`: moves to the next address. */
1452
+ next: () => setState({ key, index: index + 1 })
1453
+ };
1454
+ }
1455
+ var MINI_DIAL_STATS = ["speed", "attack", "defense", "damage"];
1456
+ var MiniDialView = forwardRef(function MiniDialView2({ columns, currentClick, label, className }, ref) {
1457
+ if (columns.length === 0) return null;
1458
+ return /* @__PURE__ */ jsx(
1459
+ "span",
1460
+ {
1461
+ ref,
1462
+ className: cx("hc-ui-mini-dial", className),
1463
+ role: "img",
1464
+ "aria-label": label,
1465
+ style: { gridTemplateColumns: `repeat(${columns.length}, var(--hc-mini-dial-cell, 7px))` },
1466
+ children: MINI_DIAL_STATS.map(
1467
+ (stat) => columns.map(({ click, cells }) => {
1468
+ const { color, round, special } = cells[stat];
1469
+ return /* @__PURE__ */ jsx(
1470
+ "i",
1471
+ {
1472
+ className: "hc-ui-mini-dial__cell",
1473
+ "data-round": flag(round),
1474
+ "data-past": flag(currentClick !== void 0 && click < currentClick),
1475
+ "data-now": flag(click === currentClick),
1476
+ "data-special": flag(special),
1477
+ "data-empty": flag(!color),
1478
+ style: color ? { backgroundColor: color } : void 0
1479
+ },
1480
+ `${stat}-${click}`
1481
+ );
1482
+ })
1483
+ )
1484
+ }
1485
+ );
1486
+ });
1487
+ var PowerIconView = forwardRef(function PowerIconView2({ color, glyphSrc, invertGlyph = false, size = 24 }, ref) {
1488
+ return /* @__PURE__ */ jsx("span", { ref, className: "hc-ui-power-icon", style: { width: size, height: size, backgroundColor: color }, children: /* @__PURE__ */ jsx("img", { className: "hc-ui-power-icon__glyph", "data-invert": flag(invertGlyph), src: glyphSrc, alt: "" }) });
1489
+ });
1490
+ var StatGlyphView = forwardRef(function StatGlyphView2({ src, alt, size = 22 }, ref) {
1491
+ return /* @__PURE__ */ jsx("img", { ref, className: "hc-ui-stat-glyph", src, alt, style: { width: size, height: size } });
1492
+ });
1493
+ var SpecialIconView = forwardRef(function SpecialIconView2({ backgroundSrc, glyphSrc, size = 24 }, ref) {
1494
+ return /* @__PURE__ */ jsxs("span", { ref, className: "hc-ui-special-icon", style: { width: size, height: size }, children: [
1495
+ /* @__PURE__ */ jsx("img", { className: "hc-ui-special-icon__bg", src: backgroundSrc, alt: "" }),
1496
+ /* @__PURE__ */ jsx("img", { className: "hc-ui-special-icon__glyph", src: glyphSrc, alt: "" })
1497
+ ] });
1498
+ });
1499
+ function EquipmentIcon({ size = 24 }) {
1500
+ return /* @__PURE__ */ jsx("span", { className: "hc-ui-equipment-icon", style: { width: size, height: size }, children: /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", width: "58%", height: "58%", "aria-hidden": true, children: /* @__PURE__ */ jsx("path", { d: "M12 2.5 L21.5 12 L12 21.5 L2.5 12 Z" }) }) });
1501
+ }
1502
+ var STAT_ICON_PATHS = {
1503
+ speed: "M12 4 L17 9 L13 9 L13 17 L11 17 L11 9 L7 9 Z",
1504
+ attack: "M4 12 L9 7 L9 11 L17 11 L17 13 L9 13 L9 17 Z",
1505
+ defense: "M12 3 L20 6 V12 C20 16 16.5 19.5 12 21 C7.5 19.5 4 16 4 12 V6 Z",
1506
+ damage: "M12 4 L20 12 L12 20 L4 12 Z"
1507
+ };
1508
+ function StatIcon({ type, size = 24, className }) {
1509
+ return /* @__PURE__ */ jsx("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "currentColor", className, "aria-hidden": true, children: /* @__PURE__ */ jsx("path", { d: STAT_ICON_PATHS[type] }) });
1510
+ }
1511
+ var preventDefault = (event) => event.preventDefault();
1512
+ var AvatarView = forwardRef(function AvatarView2({ sources, fallbackSrc, alt, shape = "round", className, onFallback, size }, ref) {
1513
+ const { src, index, next } = useImageChain(sources);
1514
+ const isFallback = src === void 0;
1515
+ return /* @__PURE__ */ jsx(
1516
+ "img",
1517
+ {
1518
+ ref,
1519
+ src: src ?? fallbackSrc,
1520
+ alt,
1521
+ className: cx("hc-ui-avatar", className),
1522
+ "data-shape": shape,
1523
+ "data-fallback": flag(isFallback),
1524
+ "data-sized": flag(size !== void 0),
1525
+ style: size !== void 0 ? { "--hc-avatar-size": `${size}px` } : void 0,
1526
+ loading: "lazy",
1527
+ decoding: "async",
1528
+ referrerPolicy: "no-referrer",
1529
+ draggable: false,
1530
+ onDragStart: preventDefault,
1531
+ onContextMenu: preventDefault,
1532
+ onMouseDown: preventDefault,
1533
+ onTouchStart: preventDefault,
1534
+ onError: () => {
1535
+ if (isFallback) return;
1536
+ next();
1537
+ if (index + 1 >= sources.length) onFallback?.();
1538
+ }
1539
+ }
1540
+ );
1541
+ });
1542
+ var SetIconView = forwardRef(function SetIconView2({ sources, className, title }, ref) {
1543
+ const { src, next } = useImageChain(sources);
1544
+ if (!src) return null;
1545
+ return /* @__PURE__ */ jsx("img", { ref, className, src, alt: "", "aria-hidden": true, title, loading: "lazy", decoding: "async", onError: next }, src);
1546
+ });
1547
+ var COMBAT_DIAL_STATS = ["speed", "attack", "defense", "damage"];
1548
+ var PRINTED_ROW = 12;
1549
+ var CombatDialView = forwardRef(function CombatDialView2({
1550
+ columns,
1551
+ legend,
1552
+ points,
1553
+ selectedPointIndex,
1554
+ focusClick,
1555
+ currentClick,
1556
+ labels,
1557
+ presentation = "default",
1558
+ density = "comfortable",
1559
+ onClickChange,
1560
+ onSelectPoint,
1561
+ tip,
1562
+ onTipChange: setTip,
1563
+ tipId,
1564
+ tipSlot
1565
+ }, ref) {
1566
+ const tracksClick = !!onClickChange;
1567
+ const hasMultiplePoints = points.length > 1;
1568
+ const selectedPointColumnRef = useRef(null);
1569
+ const gridWrapRef = useRef(null);
1570
+ const hideTip = () => setTip(null);
1571
+ useEffect(() => {
1572
+ const wrap = gridWrapRef.current;
1573
+ const column = selectedPointColumnRef.current;
1574
+ if (!wrap || !column) return;
1575
+ const wrapRect = wrap.getBoundingClientRect();
1576
+ const columnRect = column.getBoundingClientRect();
1577
+ const delta = columnRect.left - wrapRect.left - (wrap.clientWidth - column.clientWidth) / 2;
1578
+ wrap.scrollTo({ left: wrap.scrollLeft + delta, behavior: window.matchMedia("(prefers-reduced-motion: reduce)").matches ? "auto" : "smooth" });
1579
+ }, [selectedPointIndex, focusClick, points.length]);
1580
+ if (columns.length === 0) return null;
1581
+ const renderValueCell = (stat, cell, clickNumber) => {
1582
+ const { tip: content } = cell;
1583
+ const hasTip = !!content;
1584
+ const key = `${clickNumber}:${stat}`;
1585
+ const openTip = (trigger, x, y, mode) => {
1586
+ if (content) setTip({ key, click: clickNumber, stat, content, anchor: { x, y, preferAbove: mode === "dialog" }, mode, trigger });
1587
+ };
1588
+ const cellProps = {
1589
+ className: "hc-ui-combat-dial__cell",
1590
+ "data-kind": "value",
1591
+ "data-round": flag(cell.round),
1592
+ "data-outlined": flag(cell.outlined),
1593
+ style: { backgroundColor: cell.background, color: cell.color }
1594
+ };
1595
+ if (!hasTip && !tracksClick) return /* @__PURE__ */ jsx("span", { role: "img", "aria-label": cell.label, ...cellProps, children: cell.value });
1596
+ return /* @__PURE__ */ jsx(
1597
+ "button",
1598
+ {
1599
+ type: "button",
1600
+ "aria-label": cell.label,
1601
+ "aria-haspopup": hasTip ? "dialog" : void 0,
1602
+ "aria-expanded": hasTip ? tip?.key === key && tip.mode === "dialog" : void 0,
1603
+ "aria-describedby": tip?.key === key && tip.mode === "hover" ? tipId : void 0,
1604
+ ...cellProps,
1605
+ "data-tip": flag(hasTip),
1606
+ onPointerEnter: (event) => {
1607
+ if (event.pointerType === "mouse" && hasTip && tip?.mode !== "dialog") openTip(event.currentTarget, event.clientX, event.clientY, "hover");
1608
+ },
1609
+ onPointerMove: (event) => {
1610
+ if (event.pointerType === "mouse") setTip((previous) => previous?.key === key && previous.mode === "hover" ? { ...previous, anchor: { x: event.clientX, y: event.clientY } } : previous);
1611
+ },
1612
+ onPointerLeave: () => setTip((previous) => previous?.mode === "hover" ? null : previous),
1613
+ onClick: (event) => {
1614
+ event.stopPropagation();
1615
+ onClickChange?.(clickNumber);
1616
+ if (hasTip) {
1617
+ const rect = event.currentTarget.getBoundingClientRect();
1618
+ openTip(event.currentTarget, event.detail ? event.clientX : rect.left, event.detail ? event.clientY : rect.bottom, "dialog");
1619
+ } else hideTip();
1620
+ },
1621
+ children: cell.value
1622
+ }
1623
+ );
1624
+ };
1625
+ const legendIcon = (icon) => !icon.glyph ? /* @__PURE__ */ jsx("img", { className: "hc-ui-combat-dial__special-icon hc-ui-stat-glyph", src: icon.src, alt: icon.alt, title: icon.title }) : presentation === "printed" ? /* @__PURE__ */ jsx("img", { className: "hc-ui-stat-glyph", src: icon.src, alt: icon.alt }) : /* @__PURE__ */ jsx(StatGlyphView, { src: icon.src, alt: icon.alt });
1626
+ const rows = presentation === "printed" && columns.length > PRINTED_ROW ? [columns.slice(0, PRINTED_ROW), columns.slice(PRINTED_ROW)] : [columns];
1627
+ const renderGridRow = (rowColumns, withKo, rowIndex) => /* @__PURE__ */ jsxs("div", { className: "hc-ui-combat-dial__grid", children: [
1628
+ /* @__PURE__ */ jsxs("div", { className: "hc-ui-combat-dial__col", "data-kind": "legend", children: [
1629
+ /* @__PURE__ */ jsxs("div", { className: "hc-ui-combat-dial__cell hc-ui-combat-dial__rangebox", "data-kind": "legend", children: [
1630
+ /* @__PURE__ */ jsx("span", { className: "hc-ui-combat-dial__range", children: legend.range }),
1631
+ /* @__PURE__ */ jsx("span", { className: "hc-ui-combat-dial__bolts", children: Array.from({ length: Math.max(legend.targets, 0) }).map((_, i) => /* @__PURE__ */ jsx("img", { className: "hc-ui-combat-dial__bolt", src: legend.boltSrc, alt: "" }, i)) })
1632
+ ] }),
1633
+ COMBAT_DIAL_STATS.map((stat) => /* @__PURE__ */ jsx("div", { className: "hc-ui-combat-dial__cell", "data-kind": "legend", children: legendIcon(legend.icons[stat]) }, stat))
1634
+ ] }),
1635
+ rowColumns.map((column) => {
1636
+ const isCurrent = tracksClick && column.click === currentClick;
1637
+ return /* @__PURE__ */ jsxs(
1638
+ "div",
1639
+ {
1640
+ role: "group",
1641
+ ref: focusClick === column.click ? selectedPointColumnRef : null,
1642
+ className: "hc-ui-combat-dial__col",
1643
+ "data-kind": "click",
1644
+ "data-current": flag(isCurrent),
1645
+ "data-dimmed": flag(column.dimmed),
1646
+ "data-start": flag(!!column.startColor),
1647
+ style: column.startColor ? { "--hc-dial-start-color": column.startColor } : void 0,
1648
+ "aria-label": column.label,
1649
+ children: [
1650
+ onClickChange ? /* @__PURE__ */ jsx("button", { type: "button", className: "hc-ui-combat-dial__cell", "data-kind": "number", onClick: () => onClickChange(column.click), "aria-pressed": isCurrent, "aria-label": column.selectLabel, children: column.click }) : /* @__PURE__ */ jsx("div", { className: "hc-ui-combat-dial__cell", "data-kind": "number", children: column.click }),
1651
+ COMBAT_DIAL_STATS.map((stat) => /* @__PURE__ */ jsx(Fragment, { children: renderValueCell(stat, column.cells[stat], column.click) }, stat))
1652
+ ]
1653
+ },
1654
+ column.click
1655
+ );
1656
+ }),
1657
+ withKo && /* @__PURE__ */ jsxs("div", { className: "hc-ui-combat-dial__col", "data-kind": "ko", children: [
1658
+ /* @__PURE__ */ jsx("div", { className: "hc-ui-combat-dial__cell", "data-kind": "number", children: columns.length + 1 }),
1659
+ COMBAT_DIAL_STATS.map((stat) => /* @__PURE__ */ jsx("div", { className: "hc-ui-combat-dial__cell", "data-kind": "ko", children: labels.koCell }, stat))
1660
+ ] })
1661
+ ] }, rowIndex);
1662
+ return /* @__PURE__ */ jsxs("section", { ref, className: "hc-ui-combat-dial", "data-presentation": presentation, "data-density": density === "compact" ? "compact" : void 0, "data-static": flag(!tracksClick), children: [
1663
+ presentation !== "printed" && /* @__PURE__ */ jsxs("header", { className: "hc-ui-combat-dial__header", children: [
1664
+ /* @__PURE__ */ jsxs("div", { className: "hc-ui-combat-dial__header-top", children: [
1665
+ /* @__PURE__ */ jsx("span", { className: "hc-ui-combat-dial__label", children: labels.heading }),
1666
+ /* @__PURE__ */ jsxs("span", { className: "hc-ui-combat-dial__status", children: [
1667
+ /* @__PURE__ */ jsx("span", { className: "hc-ui-combat-dial__status-ko", children: labels.koMark }),
1668
+ /* @__PURE__ */ jsx("span", { children: labels.koAt })
1669
+ ] })
1670
+ ] }),
1671
+ points.some((point) => point.valueLabel) && /* @__PURE__ */ jsx("div", { className: "hc-ui-combat-dial__points", role: hasMultiplePoints ? "group" : void 0, children: points.map((point) => {
1672
+ const isActive = point.index === selectedPointIndex;
1673
+ return /* @__PURE__ */ jsxs(
1674
+ "button",
1675
+ {
1676
+ type: "button",
1677
+ className: "hc-ui-combat-dial__chip",
1678
+ style: { "--hc-dial-chip-color": point.color },
1679
+ onClick: () => onSelectPoint(point),
1680
+ disabled: !hasMultiplePoints,
1681
+ "aria-pressed": isActive,
1682
+ title: hasMultiplePoints ? point.title : void 0,
1683
+ children: [
1684
+ point.valueLabel && /* @__PURE__ */ jsx("span", { className: "hc-ui-combat-dial__chip-value", children: point.valueLabel }),
1685
+ /* @__PURE__ */ jsx("span", { className: "hc-ui-combat-dial__chip-bar" }),
1686
+ /* @__PURE__ */ jsx("span", { className: "hc-ui-combat-dial__chip-clicks", children: point.clicksLabel })
1687
+ ]
1688
+ },
1689
+ point.index
1690
+ );
1691
+ }) })
1692
+ ] }),
1693
+ /* @__PURE__ */ jsx("div", { className: "hc-ui-combat-dial__grid-wrap", ref: gridWrapRef, children: rows.map((rowColumns, rowIndex) => renderGridRow(rowColumns, rowIndex === rows.length - 1, rowIndex)) }),
1694
+ tip && tipSlot
1695
+ ] });
1696
+ });
1697
+ var PHYSICAL_CARD_WIDTH = 375;
1698
+ var PHYSICAL_CARD_HEIGHT = 525;
1699
+ var PHYSICAL_CARD_BACK_TABLE_HEIGHT = 440;
1700
+ var PHYSICAL_CARD_BYSTANDER_DIAL_STEP = { x: 7, y: 24 };
1701
+ function nextPhysicalCardDistribution(pages, overflowing) {
1702
+ const last = pages.length ? Math.max(...pages) : 0;
1703
+ for (let table = 0; table <= last; table += 1) {
1704
+ if (!overflowing(table)) continue;
1705
+ const items = pages.flatMap((page, index) => page === table ? [index] : []);
1706
+ if (!items.length || table > 0 && items.length < 2) continue;
1707
+ const next = pages.slice();
1708
+ next[items.at(-1)] = table + 1;
1709
+ return next;
1710
+ }
1711
+ return null;
1712
+ }
1713
+ function physicalCardBackCount(pages) {
1714
+ const last = pages.length ? Math.max(...pages) : 0;
1715
+ return Math.max(1, Math.ceil(last / 2));
1716
+ }
1717
+ var MAX_TILT_DEG = 3;
1718
+ var DIAL_DAMPING = 0.15;
1719
+ var CARD_DIAL_SELECTOR = ".hc-ui-physical-card__dial, .hc-ui-physical-card__bystander-dial";
1720
+ var CAN_TILT = "(hover: hover) and (pointer: fine) and (prefers-reduced-motion: no-preference)";
1721
+ var CAN_DEVICE_TILT = "(hover: none) and (pointer: coarse) and (prefers-reduced-motion: no-preference)";
1722
+ var DEVICE_RANGE_DEG = 18;
1723
+ var BASELINE_DRIFT = 0.01;
1724
+ var DEVICE_SMOOTHING = 0.2;
1725
+ function physicalCardTiltAt(x, y, damping = 1) {
1726
+ const offsetX = (x - 0.5) * 2 * damping;
1727
+ const offsetY = (y - 0.5) * 2 * damping;
1728
+ return {
1729
+ rotateX: -offsetY * MAX_TILT_DEG,
1730
+ rotateY: offsetX * MAX_TILT_DEG,
1731
+ parallaxX: offsetX,
1732
+ parallaxY: offsetY,
1733
+ shineX: x * 100,
1734
+ shineY: y * 100
1735
+ };
1736
+ }
1737
+ var PARALLAX_ART_PX = 4;
1738
+ var PARALLAX_HEADER_PX = 1.5;
1739
+ var CARD_ART_SELECTOR = ".hc-ui-physical-card__token, .hc-ui-physical-card__terrain-art, .hc-ui-physical-card__bystander-token";
1740
+ function physicalCardTiltStyles(tilt) {
1741
+ return {
1742
+ card: `perspective(1100px) rotateX(${tilt.rotateX}deg) rotateY(${tilt.rotateY}deg)`,
1743
+ art: `${tilt.parallaxX * PARALLAX_ART_PX}px ${tilt.parallaxY * PARALLAX_ART_PX}px`,
1744
+ header: `${tilt.parallaxX * PARALLAX_HEADER_PX}px ${tilt.parallaxY * PARALLAX_HEADER_PX}px`,
1745
+ shine: `${tilt.shineX}% ${tilt.shineY}%`
1746
+ };
1747
+ }
1748
+ var eachIn = (root, selector, write) => {
1749
+ root.querySelectorAll(selector).forEach(write);
1750
+ };
1751
+ function applyTilt(root, tilt) {
1752
+ const styles = physicalCardTiltStyles(tilt);
1753
+ root.dataset.tilt = "";
1754
+ eachIn(root, ".hc-ui-physical-card-tilt", (element) => {
1755
+ element.style.transform = styles.card;
1756
+ });
1757
+ eachIn(root, CARD_ART_SELECTOR, (element) => {
1758
+ element.style.translate = styles.art;
1759
+ });
1760
+ eachIn(root, ".hc-ui-physical-card__header", (element) => {
1761
+ element.style.translate = styles.header;
1762
+ });
1763
+ eachIn(root, ".hc-ui-physical-card__shine", (element) => {
1764
+ element.style.setProperty("--hc-card-shine-at", styles.shine);
1765
+ });
1766
+ }
1767
+ function clearTilt(root) {
1768
+ delete root.dataset.tilt;
1769
+ eachIn(root, ".hc-ui-physical-card-tilt", (element) => {
1770
+ element.style.transform = "";
1771
+ });
1772
+ eachIn(root, `${CARD_ART_SELECTOR}, .hc-ui-physical-card__header`, (element) => {
1773
+ element.style.translate = "";
1774
+ });
1775
+ }
1776
+ function physicalCardDevicePosition(beta, gamma, baseline, screenAngle = 0) {
1777
+ const clampAxis = (value) => Math.max(-1, Math.min(1, value / DEVICE_RANGE_DEG));
1778
+ const front = clampAxis(beta - baseline.beta);
1779
+ const side = clampAxis(gamma - baseline.gamma);
1780
+ const [horizontal, vertical] = screenAngle === 90 ? [front, -side] : screenAngle === 270 ? [-front, side] : screenAngle === 180 ? [-side, -front] : [side, front];
1781
+ return { x: (horizontal + 1) / 2, y: (vertical + 1) / 2 };
1782
+ }
1783
+ var motionAsked = false;
1784
+ function requestPhysicalCardMotionAccess() {
1785
+ const request = typeof DeviceOrientationEvent === "undefined" ? void 0 : DeviceOrientationEvent.requestPermission;
1786
+ if (motionAsked || !request) return;
1787
+ motionAsked = true;
1788
+ request.call(DeviceOrientationEvent).catch(() => {
1789
+ motionAsked = false;
1790
+ });
1791
+ }
1792
+ function useDeviceTilt(ref, enabled) {
1793
+ useEffect(() => {
1794
+ const element = ref.current;
1795
+ if (!enabled || !element || typeof DeviceOrientationEvent === "undefined" || !window.matchMedia(CAN_DEVICE_TILT).matches) return;
1796
+ let touching = false;
1797
+ let baseline = null;
1798
+ let position = { x: 0.5, y: 0.5 };
1799
+ let frame = 0;
1800
+ const onOrientation = (event) => {
1801
+ if (touching || event.beta == null || event.gamma == null) return;
1802
+ baseline ?? (baseline = { beta: event.beta, gamma: event.gamma });
1803
+ baseline.beta += (event.beta - baseline.beta) * BASELINE_DRIFT;
1804
+ baseline.gamma += (event.gamma - baseline.gamma) * BASELINE_DRIFT;
1805
+ const target = physicalCardDevicePosition(event.beta, event.gamma, baseline, screen.orientation?.angle ?? 0);
1806
+ position = { x: position.x + (target.x - position.x) * DEVICE_SMOOTHING, y: position.y + (target.y - position.y) * DEVICE_SMOOTHING };
1807
+ cancelAnimationFrame(frame);
1808
+ frame = requestAnimationFrame(() => applyTilt(element, physicalCardTiltAt(position.x, position.y)));
1809
+ };
1810
+ const onTouchStart = () => {
1811
+ touching = true;
1812
+ };
1813
+ const onTouchEnd = (event) => {
1814
+ touching = event.touches.length > 0;
1815
+ };
1816
+ const passive = { passive: true, capture: true };
1817
+ window.addEventListener("deviceorientation", onOrientation);
1818
+ window.addEventListener("touchstart", onTouchStart, passive);
1819
+ window.addEventListener("touchend", onTouchEnd, passive);
1820
+ window.addEventListener("touchcancel", onTouchEnd, passive);
1821
+ return () => {
1822
+ window.removeEventListener("deviceorientation", onOrientation);
1823
+ window.removeEventListener("touchstart", onTouchStart, passive);
1824
+ window.removeEventListener("touchend", onTouchEnd, passive);
1825
+ window.removeEventListener("touchcancel", onTouchEnd, passive);
1826
+ cancelAnimationFrame(frame);
1827
+ clearTilt(element);
1828
+ };
1829
+ }, [ref, enabled]);
1830
+ }
1831
+ function useCardTilt() {
1832
+ const frame = useRef(0);
1833
+ useEffect(() => () => cancelAnimationFrame(frame.current), []);
1834
+ const onPointerMove = (event) => {
1835
+ if (event.pointerType !== "mouse" || !window.matchMedia(CAN_TILT).matches) return;
1836
+ const slot = event.currentTarget;
1837
+ const rect = slot.getBoundingClientRect();
1838
+ const overDial = event.target.closest(CARD_DIAL_SELECTOR);
1839
+ const tilt = physicalCardTiltAt((event.clientX - rect.left) / rect.width, (event.clientY - rect.top) / rect.height, overDial ? DIAL_DAMPING : 1);
1840
+ cancelAnimationFrame(frame.current);
1841
+ frame.current = requestAnimationFrame(() => applyTilt(slot, tilt));
1842
+ };
1843
+ const onPointerLeave = (event) => {
1844
+ cancelAnimationFrame(frame.current);
1845
+ clearTilt(event.currentTarget);
1846
+ };
1847
+ return { onPointerMove, onPointerLeave };
1848
+ }
1849
+ var PHYSICAL_CARD_STATS = ["speed", "attack", "defense", "damage"];
1850
+ var CARD_GAP = 16;
1851
+ var MAX_CARD_SCALE = 1.2;
1852
+ var SIDE_BY_SIDE_MIN_SCALE = 0.86;
1853
+ var LONG_PRESS_MS = 450;
1854
+ var LONG_PRESS_SLOP = 10;
1855
+ var FOCUS_CHROME_HEIGHT = 112;
1856
+ function useCardScale(fitHeight, focus, scrollerSelector) {
1857
+ const ref = useRef(null);
1858
+ const [width, setWidth] = useState(0);
1859
+ const [height, setHeight] = useState(0);
1860
+ useLayoutEffect(() => {
1861
+ const element = ref.current;
1862
+ if (!element) return;
1863
+ const scroller = fitHeight && scrollerSelector ? element.closest(scrollerSelector) : null;
1864
+ const measure = () => {
1865
+ setWidth(element.clientWidth);
1866
+ if (focus) setHeight(window.innerHeight - FOCUS_CHROME_HEIGHT);
1867
+ if (!scroller) return;
1868
+ const top = element.getBoundingClientRect().top - scroller.getBoundingClientRect().top + scroller.scrollTop;
1869
+ setHeight(scroller.clientHeight - top - 16);
1870
+ };
1871
+ measure();
1872
+ if (typeof ResizeObserver === "undefined") return;
1873
+ const observer = new ResizeObserver(measure);
1874
+ observer.observe(element);
1875
+ if (scroller) observer.observe(scroller);
1876
+ return () => observer.disconnect();
1877
+ }, [fitHeight, focus, scrollerSelector]);
1878
+ const columns = !focus && width >= PHYSICAL_CARD_WIDTH * SIDE_BY_SIDE_MIN_SCALE * 2 + CARD_GAP ? 2 : 1;
1879
+ const byWidth = width ? Math.min(MAX_CARD_SCALE, (width - CARD_GAP * (columns - 1)) / columns / PHYSICAL_CARD_WIDTH) : 1;
1880
+ const scale = (fitHeight || focus) && height > 0 ? Math.max(0.6, Math.min(byWidth, height / PHYSICAL_CARD_HEIGHT)) : byWidth;
1881
+ return { ref, scale };
1882
+ }
1883
+ function useFitText(text, max, min) {
1884
+ const ref = useRef(null);
1885
+ const [fit, setFit] = useState({ text, size: max });
1886
+ const size = fit.text === text ? fit.size : max;
1887
+ useLayoutEffect(() => {
1888
+ const element = ref.current;
1889
+ if (!element || size <= min || element.scrollWidth <= element.clientWidth + 1) return;
1890
+ setFit({ text, size: size - 1 });
1891
+ });
1892
+ return { ref, size };
1893
+ }
1894
+ function CardRule({ name, text, icon, onCopy }) {
1895
+ return /* @__PURE__ */ jsxs("section", { className: "hc-ui-physical-card__rule", children: [
1896
+ name && /* @__PURE__ */ jsxs("h3", { children: [
1897
+ icon,
1898
+ /* @__PURE__ */ jsx("span", { children: name })
1899
+ ] }),
1900
+ /* @__PURE__ */ jsx("p", { className: "hc-ui-physical-card__copy", onClick: onCopy, children: text })
1901
+ ] });
1902
+ }
1903
+ function CardFrame({ side, label, children, legacySrc, backgroundSrc, away = false }) {
1904
+ const tilt = useCardTilt();
1905
+ return /* @__PURE__ */ jsx("div", { className: "hc-ui-physical-card-slot", "data-side": side, "data-away": flag(away), ...tilt, children: /* @__PURE__ */ jsx("div", { className: "hc-ui-physical-card-tilt", children: /* @__PURE__ */ jsxs("article", { className: "hc-ui-physical-card", "data-side": side, "aria-label": label, children: [
1906
+ /* @__PURE__ */ jsx("img", { className: "hc-ui-physical-card__bg", "data-side": side, src: backgroundSrc, alt: "", "aria-hidden": true, draggable: false }),
1907
+ legacySrc && /* @__PURE__ */ jsx("img", { className: "hc-ui-physical-card__legacy", src: legacySrc, alt: "", "aria-hidden": true, draggable: false }),
1908
+ children,
1909
+ /* @__PURE__ */ jsx("span", { className: "hc-ui-physical-card__shine", "aria-hidden": true })
1910
+ ] }) }) });
1911
+ }
1912
+ function useLongPress(onLongPress, enabled) {
1913
+ const press = useRef(null);
1914
+ const cancel = () => {
1915
+ if (press.current) window.clearTimeout(press.current.timer);
1916
+ press.current = null;
1917
+ };
1918
+ useEffect(() => cancel, []);
1919
+ if (!enabled) return {};
1920
+ return {
1921
+ onPointerDown: (event) => {
1922
+ if (event.pointerType !== "touch" || !event.isPrimary) return;
1923
+ cancel();
1924
+ const timer = window.setTimeout(() => {
1925
+ press.current = null;
1926
+ navigator.vibrate?.(10);
1927
+ onLongPress();
1928
+ }, LONG_PRESS_MS);
1929
+ press.current = { timer, x: event.clientX, y: event.clientY };
1930
+ },
1931
+ onPointerMove: (event) => {
1932
+ const start = press.current;
1933
+ if (start && Math.hypot(event.clientX - start.x, event.clientY - start.y) > LONG_PRESS_SLOP) cancel();
1934
+ },
1935
+ onPointerUp: cancel,
1936
+ onPointerCancel: cancel,
1937
+ // On Android, holding would open the image's menu over the focus view.
1938
+ onContextMenu: (event) => {
1939
+ if (press.current) event.preventDefault();
1940
+ }
1941
+ };
1942
+ }
1943
+ function PhysicalCardFocusView({ label, onClose, children }) {
1944
+ const ref = useRef(null);
1945
+ const released = useRef(false);
1946
+ useLayoutEffect(() => {
1947
+ const dialog = ref.current;
1948
+ if (!dialog) return;
1949
+ dialog.showModal();
1950
+ const release = () => window.setTimeout(() => {
1951
+ released.current = true;
1952
+ }, 0);
1953
+ window.addEventListener("pointerup", release, { once: true });
1954
+ window.addEventListener("pointercancel", release, { once: true });
1955
+ window.addEventListener("touchend", requestPhysicalCardMotionAccess);
1956
+ return () => {
1957
+ window.removeEventListener("touchend", requestPhysicalCardMotionAccess);
1958
+ window.removeEventListener("pointerup", release);
1959
+ window.removeEventListener("pointercancel", release);
1960
+ dialog.close();
1961
+ };
1962
+ }, []);
1963
+ const closeOutside = (event) => {
1964
+ const target = event.target;
1965
+ if (released.current && (target === event.currentTarget || target.classList.contains("hc-ui-physical-cards") && target.hasAttribute("data-focus"))) onClose();
1966
+ };
1967
+ return /* @__PURE__ */ jsx("dialog", { ref, className: "hc-ui-card-focus", "aria-label": label, onClick: closeOutside, onCancel: (event) => {
1968
+ event.preventDefault();
1969
+ onClose();
1970
+ }, children });
1971
+ }
1972
+ var PhysicalCardView = forwardRef(function PhysicalCardView2({
1973
+ images,
1974
+ layout,
1975
+ title,
1976
+ gearSrc,
1977
+ headerLines,
1978
+ teamAbilities,
1979
+ teamAbilityIcon,
1980
+ collection,
1981
+ dimensions,
1982
+ token,
1983
+ bystander,
1984
+ keyphrases,
1985
+ relic,
1986
+ rules,
1987
+ contentKey,
1988
+ improved,
1989
+ dial,
1990
+ terrainDial,
1991
+ points,
1992
+ onSelectPoint,
1993
+ backBar,
1994
+ labels,
1995
+ onCopy,
1996
+ fitHeight = false,
1997
+ fitHeightWithin,
1998
+ focus = false,
1999
+ onRequestFocus,
2000
+ focusActions,
2001
+ tip,
2002
+ onTipChange: setTip,
2003
+ tipId,
2004
+ tipSlot
2005
+ }, forwardedRef) {
2006
+ const { ref: wrapRef, scale } = useCardScale(fitHeight, focus, fitHeightWithin);
2007
+ useDeviceTilt(wrapRef, focus);
2008
+ const longPress = useLongPress(() => onRequestFocus?.(), !focus && !!onRequestFocus);
2009
+ const [side, setSide] = useState(0);
2010
+ const name = useFitText(title, 28, 12);
2011
+ const setWrapRef = useCallback((element) => {
2012
+ wrapRef.current = element;
2013
+ if (typeof forwardedRef === "function") forwardedRef(element);
2014
+ else if (forwardedRef) forwardedRef.current = element;
2015
+ }, [wrapRef, forwardedRef]);
2016
+ const tipProps = ({ key, tip: content }) => ({
2017
+ "aria-haspopup": "dialog",
2018
+ "aria-expanded": tip?.key === key && tip.mode === "dialog",
2019
+ "aria-describedby": tip?.key === key && tip.mode === "hover" ? tipId : void 0,
2020
+ onPointerEnter: (event) => {
2021
+ if (event.pointerType === "mouse" && tip?.mode !== "dialog") setTip({ ...content, key, anchor: { x: event.clientX, y: event.clientY }, mode: "hover", trigger: event.currentTarget });
2022
+ },
2023
+ onPointerMove: (event) => {
2024
+ if (event.pointerType === "mouse") setTip((value) => value?.key === key && value.mode === "hover" ? { ...value, anchor: { x: event.clientX, y: event.clientY } } : value);
2025
+ },
2026
+ onPointerLeave: () => setTip((value) => value?.mode === "hover" ? null : value),
2027
+ onBlur: () => setTip((value) => value?.mode === "hover" ? null : value),
2028
+ onClick: (event) => {
2029
+ event.stopPropagation();
2030
+ const rect = event.currentTarget.getBoundingClientRect();
2031
+ setTip({ ...content, key, anchor: { x: event.detail ? event.clientX : rect.left, y: event.detail ? event.clientY : rect.bottom, preferAbove: true }, mode: "dialog", trigger: event.currentTarget });
2032
+ }
2033
+ });
2034
+ const zeros = () => rules.map(() => 0);
2035
+ const [distribution, setDistribution] = useState(() => ({ key: contentKey, pages: zeros() }));
2036
+ const pages = distribution.key === contentKey && distribution.pages.length === rules.length ? distribution.pages : zeros();
2037
+ const tables = useRef([]);
2038
+ useLayoutEffect(() => {
2039
+ const next = nextPhysicalCardDistribution(pages, (table) => {
2040
+ const element = tables.current[table];
2041
+ return !!element && element.scrollHeight > element.clientHeight + 1;
2042
+ });
2043
+ if (next) setDistribution({ key: contentKey, pages: next });
2044
+ });
2045
+ const tableRef = (index) => (element) => {
2046
+ tables.current[index] = element;
2047
+ };
2048
+ const renderRules = (table) => rules.map((rule, index) => pages[index] !== table ? null : /* @__PURE__ */ jsx(
2049
+ CardRule,
2050
+ {
2051
+ name: rule.name,
2052
+ text: rule.text,
2053
+ icon: rule.icon,
2054
+ onCopy: () => onCopy(rule.copyText)
2055
+ },
2056
+ index
2057
+ ));
2058
+ const tokenClass = "hc-ui-physical-card__token";
2059
+ const tokenNode = !token ? null : token.kind === "portrait" ? token.render(tokenClass) : token.kind === "terrain" ? /* @__PURE__ */ jsx("img", { className: "hc-ui-physical-card__terrain-art", src: token.src, alt: token.alt, loading: "lazy" }) : /* @__PURE__ */ jsx("span", { className: tokenClass, "data-object": "", style: token.color ? { "--hc-card-object-color": token.color } : void 0, children: token.src && /* @__PURE__ */ jsx("img", { src: token.src, alt: token.alt, loading: "lazy", onError: (event) => {
2060
+ event.currentTarget.hidden = true;
2061
+ } }) });
2062
+ const objectLines = keyphrases ? /* @__PURE__ */ jsxs(Fragment2, { children: [
2063
+ keyphrases.map((line) => /* @__PURE__ */ jsx("div", { className: "hc-ui-physical-card__keyphrase", children: line.content }, line.key)),
2064
+ relic && /* @__PURE__ */ jsxs("p", { className: "hc-ui-physical-card__relic", children: [
2065
+ /* @__PURE__ */ jsxs("b", { children: [
2066
+ relic.label,
2067
+ ":"
2068
+ ] }),
2069
+ " ",
2070
+ relic.value
2071
+ ] })
2072
+ ] }) : null;
2073
+ const improvedGroups = improved.map((group) => /* @__PURE__ */ jsxs("span", { className: "hc-ui-physical-card__improved-group", children: [
2074
+ /* @__PURE__ */ jsx("img", { className: "hc-ui-physical-card__improved-kind", src: group.kindIconSrc, alt: group.kindLabel }),
2075
+ group.abilities.map((ability) => /* @__PURE__ */ jsx(
2076
+ "button",
2077
+ {
2078
+ type: "button",
2079
+ className: "hc-ui-physical-card__improved",
2080
+ "aria-label": ability.label,
2081
+ ...tipProps(ability),
2082
+ children: /* @__PURE__ */ jsx("img", { src: ability.iconSrc, alt: "" })
2083
+ },
2084
+ ability.key
2085
+ ))
2086
+ ] }, group.kind));
2087
+ const pointValues = points.kind === "none" ? null : points.kind === "object" ? /* @__PURE__ */ jsxs("div", { className: "hc-ui-physical-card__object-points", children: [
2088
+ points.values,
2089
+ " ",
2090
+ points.suffix
2091
+ ] }) : /* @__PURE__ */ jsx("div", { className: "hc-ui-physical-card__points", "data-table": points.kind, role: points.items.length > 1 ? "group" : void 0, "aria-label": points.label, children: points.items.map((point) => /* @__PURE__ */ jsx(
2092
+ "button",
2093
+ {
2094
+ type: "button",
2095
+ className: "hc-ui-physical-card__point",
2096
+ "data-active": flag(point.index === points.selectedIndex),
2097
+ style: { color: point.color },
2098
+ disabled: points.items.length < 2,
2099
+ "aria-pressed": points.items.length > 1 ? point.index === points.selectedIndex : void 0,
2100
+ "aria-label": point.label,
2101
+ onClick: () => onSelectPoint(point),
2102
+ children: point.value
2103
+ },
2104
+ point.index
2105
+ )) });
2106
+ const terrainDialNode = terrainDial && /* @__PURE__ */ jsxs("div", { className: "hc-ui-physical-card__terrain-dial", children: [
2107
+ /* @__PURE__ */ jsxs("span", { children: [
2108
+ /* @__PURE__ */ jsx("img", { src: terrainDial.icons.range.src, alt: terrainDial.icons.range.alt }),
2109
+ " / ",
2110
+ /* @__PURE__ */ jsx("img", { src: terrainDial.icons.giantReach.src, alt: terrainDial.icons.giantReach.alt })
2111
+ ] }),
2112
+ /* @__PURE__ */ jsx("span", { children: /* @__PURE__ */ jsx("img", { src: terrainDial.icons.damage.src, alt: terrainDial.icons.damage.alt }) }),
2113
+ /* @__PURE__ */ jsx("span", { children: /* @__PURE__ */ jsx("img", { src: terrainDial.icons.destroy.src, alt: terrainDial.icons.destroy.alt }) }),
2114
+ /* @__PURE__ */ jsx("b", { children: terrainDial.values[0] }),
2115
+ /* @__PURE__ */ jsx("b", { children: terrainDial.values[1] }),
2116
+ /* @__PURE__ */ jsx("b", { children: terrainDial.values[2] })
2117
+ ] });
2118
+ const teamAbilityButtons = teamAbilities.map((ability) => /* @__PURE__ */ jsxs(
2119
+ "button",
2120
+ {
2121
+ type: "button",
2122
+ className: "hc-ui-physical-card__ability",
2123
+ "aria-label": ability.label,
2124
+ ...tipProps(ability),
2125
+ children: [
2126
+ teamAbilityIcon,
2127
+ /* @__PURE__ */ jsx("img", { src: ability.iconSrc, alt: "", onError: (event) => {
2128
+ event.currentTarget.hidden = true;
2129
+ } })
2130
+ ]
2131
+ },
2132
+ ability.key
2133
+ ));
2134
+ const bystanderToken = bystander && /* @__PURE__ */ jsx("div", { className: "hc-ui-physical-card__bystander", children: /* @__PURE__ */ jsxs("div", { className: "hc-ui-physical-card__bystander-token", children: [
2135
+ /* @__PURE__ */ jsxs("div", { className: "hc-ui-physical-card__bystander-clip", children: [
2136
+ /* @__PURE__ */ jsx("div", { className: "hc-ui-physical-card__bystander-art", children: /* @__PURE__ */ jsx("div", { className: "hc-ui-physical-card__bystander-window", children: bystander.art && /* @__PURE__ */ jsx("img", { src: bystander.art.src, alt: bystander.art.alt, loading: "lazy", onError: (event) => {
2137
+ event.currentTarget.hidden = true;
2138
+ } }) }) }),
2139
+ /* @__PURE__ */ jsx("div", { className: "hc-ui-physical-card__bystander-stripe", "aria-hidden": true }),
2140
+ /* @__PURE__ */ jsxs("div", { className: "hc-ui-physical-card__bystander-symbols", children: [
2141
+ /* @__PURE__ */ jsxs("div", { className: "hc-ui-physical-card__bystander-tab", "data-stat": "range", "aria-label": bystander.rangeLabel, role: "img", children: [
2142
+ bystander.range != null && /* @__PURE__ */ jsx("span", { children: bystander.range }),
2143
+ Array.from({ length: bystander.targets }, (_, index) => /* @__PURE__ */ jsx("img", { className: "hc-ui-physical-card__bystander-bolt", src: bystander.boltSrc, alt: "", style: { left: 18 + index * 4 } }, index))
2144
+ ] }),
2145
+ PHYSICAL_CARD_STATS.map((type) => bystander.symbols[type] && /* @__PURE__ */ jsx("div", { className: "hc-ui-physical-card__bystander-tab", "data-stat": type, children: /* @__PURE__ */ jsx("img", { src: bystander.symbols[type], alt: "" }) }, type))
2146
+ ] })
2147
+ ] }),
2148
+ bystander.improved.length > 0 && /* @__PURE__ */ jsx("div", { className: "hc-ui-physical-card__bystander-improved", children: bystander.improved.map(({ kind, iconSrc, label, ability }, index) => {
2149
+ const angle = 2 * Math.PI / 22 * index;
2150
+ const position = { left: Math.cos(angle) * 90 - 9, top: Math.sin(angle) * 90 - 9 };
2151
+ if (!ability) return /* @__PURE__ */ jsx("img", { src: iconSrc, alt: label, style: position }, `${kind}-kind`);
2152
+ return /* @__PURE__ */ jsx(
2153
+ "button",
2154
+ {
2155
+ type: "button",
2156
+ className: "hc-ui-physical-card__bystander-improved-ability",
2157
+ style: position,
2158
+ "aria-label": ability.label,
2159
+ ...tipProps(ability),
2160
+ children: /* @__PURE__ */ jsx("img", { src: ability.iconSrc, alt: "" })
2161
+ },
2162
+ `${kind}-${ability.key}`
2163
+ );
2164
+ }) }),
2165
+ (bystander.trait || teamAbilityButtons.length > 0) && /* @__PURE__ */ jsxs("div", { className: "hc-ui-physical-card__bystander-abilities", children: [
2166
+ bystander.trait && /* @__PURE__ */ jsx("img", { className: "hc-ui-physical-card__bystander-trait", src: bystander.trait.src, alt: bystander.trait.alt }),
2167
+ teamAbilityButtons
2168
+ ] }),
2169
+ bystander.extra && /* @__PURE__ */ jsxs("div", { className: "hc-ui-physical-card__bystander-extra", "data-kind": bystander.extra.kind, children: [
2170
+ /* @__PURE__ */ jsx("img", { src: bystander.extra.src, alt: bystander.extra.alt }),
2171
+ bystander.extra.kind !== "construct" && /* @__PURE__ */ jsx("span", { children: bystander.extra.text })
2172
+ ] }),
2173
+ /* @__PURE__ */ jsx("div", { className: "hc-ui-physical-card__bystander-name", children: bystander.name }),
2174
+ bystander.dial && /* @__PURE__ */ jsx("div", { className: "hc-ui-physical-card__bystander-dial", role: "group", "aria-label": bystander.dial.label, children: bystander.dial.cells.map((cell, row) => {
2175
+ const position = { left: row * PHYSICAL_CARD_BYSTANDER_DIAL_STEP.x, top: row * PHYSICAL_CARD_BYSTANDER_DIAL_STEP.y };
2176
+ const cellStyle = { ...position, backgroundColor: cell.background, color: cell.color };
2177
+ const cellProps = { className: "hc-ui-physical-card__bystander-value", "data-special": flag(cell.special), style: cellStyle, "aria-label": cell.label };
2178
+ if (!cell.tip) return /* @__PURE__ */ jsx("span", { ...cellProps, role: "img", children: cell.value }, cell.stat);
2179
+ return /* @__PURE__ */ jsx("button", { type: "button", ...cellProps, "data-tip": "", ...tipProps(cell.tip), children: cell.value }, cell.stat);
2180
+ }) })
2181
+ ] }) });
2182
+ const backs = physicalCardBackCount(pages);
2183
+ const shownSide = Math.min(side, backs);
2184
+ const nextSide = (shownSide + 1) % (backs + 1);
2185
+ const style = { "--hc-card-scale": scale, "--hc-card-front-table-height": `${layout.frontTableHeight}px`, "--hc-card-back-table-height": `${PHYSICAL_CARD_BACK_TABLE_HEIGHT}px`, "--hc-card-width": `${PHYSICAL_CARD_WIDTH}px`, "--hc-card-height": `${PHYSICAL_CARD_HEIGHT}px` };
2186
+ return /* @__PURE__ */ jsxs("div", { className: "hc-ui-physical-cards", "data-focus": flag(focus), ref: setWrapRef, style, ...longPress, children: [
2187
+ /* @__PURE__ */ jsxs(CardFrame, { side: "front", backgroundSrc: images.front, legacySrc: images.legacy, label: labels.front, away: focus && shownSide !== 0, children: [
2188
+ layout.teamUp && /* @__PURE__ */ jsx("span", { className: "hc-ui-physical-card__team-up", "aria-hidden": true }),
2189
+ layout.rarityImage && /* @__PURE__ */ jsx("img", { className: "hc-ui-physical-card__rarity", src: layout.rarityImage, alt: "", "aria-hidden": true, draggable: false }),
2190
+ layout.dialImage && /* @__PURE__ */ jsx("img", { className: "hc-ui-physical-card__dial-bg", "data-table": layout.frontTable, src: layout.dialImage, alt: "", "aria-hidden": true, draggable: false }),
2191
+ /* @__PURE__ */ jsxs("header", { className: "hc-ui-physical-card__header", "data-team-up": flag(layout.teamUp), children: [
2192
+ /* @__PURE__ */ jsxs("div", { className: "hc-ui-physical-card__title", ref: name.ref, children: [
2193
+ /* @__PURE__ */ jsx("h2", { style: { fontSize: name.size }, children: title }),
2194
+ gearSrc && /* @__PURE__ */ jsx("img", { className: "hc-ui-physical-card__gear", src: gearSrc, alt: "" }),
2195
+ !bystander && teamAbilityButtons
2196
+ ] }),
2197
+ headerLines.map((line, index) => /* @__PURE__ */ jsx("p", { className: "hc-ui-physical-card__header-line", "data-kind": line.kind, children: line.text }, index))
2198
+ ] }),
2199
+ /* @__PURE__ */ jsxs("div", { className: "hc-ui-physical-card__collection", children: [
2200
+ /* @__PURE__ */ jsx("strong", { children: collection.number }),
2201
+ /* @__PURE__ */ jsx(SetIconView, { sources: collection.setSources, title: collection.setTitle, className: "hc-ui-physical-card__set" })
2202
+ ] }),
2203
+ dimensions && /* @__PURE__ */ jsx("span", { className: "hc-ui-physical-card__dimensions", "data-oval": flag(dimensions.oval), children: dimensions.text }),
2204
+ /* @__PURE__ */ jsxs("div", { className: "hc-ui-physical-card__table", "data-column": "front", "data-table": layout.frontTable, "data-small-token": flag(layout.smallToken), ref: tableRef(0), children: [
2205
+ tokenNode,
2206
+ bystanderToken,
2207
+ objectLines,
2208
+ renderRules(0)
2209
+ ] }),
2210
+ improved.length > 0 && /* @__PURE__ */ jsx("div", { className: "hc-ui-physical-card__improved-row", style: { top: layout.improvedTop }, children: improvedGroups }),
2211
+ dial && /* @__PURE__ */ jsx("div", { className: "hc-ui-physical-card__dial", "data-tables": dial.tables, children: dial.node }),
2212
+ terrainDialNode,
2213
+ pointValues
2214
+ ] }),
2215
+ Array.from({ length: backs }, (_, back) => /* @__PURE__ */ jsxs(CardFrame, { side: "back", backgroundSrc: images.back, legacySrc: images.legacy, label: labels.back(back, backs), away: focus && shownSide !== back + 1, children: [
2216
+ /* @__PURE__ */ jsx("div", { className: "hc-ui-physical-card__table", "data-column": "left", ref: tableRef(back * 2 + 1), children: renderRules(back * 2 + 1) }),
2217
+ /* @__PURE__ */ jsx("div", { className: "hc-ui-physical-card__table", "data-column": "right", ref: tableRef(back * 2 + 2), children: renderRules(back * 2 + 2) }),
2218
+ back === backs - 1 && /* @__PURE__ */ jsxs("footer", { className: "hc-ui-physical-card__back-bar", children: [
2219
+ backBar.realName && /* @__PURE__ */ jsxs("p", { children: [
2220
+ /* @__PURE__ */ jsxs("b", { children: [
2221
+ backBar.realName.label,
2222
+ ":"
2223
+ ] }),
2224
+ " ",
2225
+ backBar.realName.value
2226
+ ] }),
2227
+ backBar.source && /* @__PURE__ */ jsxs("p", { children: [
2228
+ /* @__PURE__ */ jsxs("b", { children: [
2229
+ backBar.source.label,
2230
+ ":"
2231
+ ] }),
2232
+ " ",
2233
+ backBar.source.value
2234
+ ] })
2235
+ ] }),
2236
+ back !== backs - 1 && /* @__PURE__ */ jsx("footer", { className: "hc-ui-physical-card__back-bar", "aria-hidden": true })
2237
+ ] }, back)),
2238
+ focus && /* @__PURE__ */ jsx("div", { className: "hc-ui-card-focus__actions", children: focusActions?.({ nextSide, backs, flip: () => setSide(nextSide) }) }),
2239
+ tip && tipSlot
2240
+ ] });
2241
+ });
2242
+ var MAP_TILE_SIZE = 64;
2243
+ var EDGE_SIZE = MAP_TILE_SIZE / 16;
2244
+ var WALL_SIZE = EDGE_SIZE * 2;
2245
+ var MAX_EDGES = 4;
2246
+ var HINDERING_TILE_COLOR = "#B4ECB4";
2247
+ var BLOCKING_TILE_COLOR = "#c9b8aa";
2248
+ var WATER_TILE_COLOR = "#dee8eb";
2249
+ var OBSCURING_TILE_COLOR = "#E8E8E8";
2250
+ var WINDOW_TILE_COLOR = "#dee8eb";
2251
+ var SPECIAL_TILE_COLOR = "#FFD394";
2252
+ var STARTING_ZONE_TILE_COLOR = "#fff8fa";
2253
+ var MAP_ELEVATION_TILE_COLORS = ["#FFFFFF", "#FB7A7F", "#D0424C", "#B02020", "#902020", "#702020"];
2254
+ var MAP_TERRAIN_TILE_COLORS = {
2255
+ clear: MAP_ELEVATION_TILE_COLORS[0],
2256
+ hindering: HINDERING_TILE_COLOR,
2257
+ blocking: BLOCKING_TILE_COLOR,
2258
+ water: WATER_TILE_COLOR,
2259
+ obscuring: OBSCURING_TILE_COLOR,
2260
+ window: WINDOW_TILE_COLOR,
2261
+ special: SPECIAL_TILE_COLOR,
2262
+ special2: SPECIAL_TILE_COLOR
2263
+ };
2264
+ var MAP_TERRAIN_EDGE_COLORS = {
2265
+ clear: "#FFFFFF",
2266
+ hindering: "#3ACE3A",
2267
+ blocking: "#76491E",
2268
+ water: "#1FA9E1",
2269
+ obscuring: "#BBBBBB",
2270
+ window: "#1FA9E1",
2271
+ special: "#FF7600",
2272
+ special2: "#FF7600"
2273
+ };
2274
+ var STARTING_ZONE_EDGE_COLOR = "#D560A9";
2275
+ var MAP_ELEVATION_EDGE_COLOR = "#FF0000";
2276
+ var MAP_INDOOR_EDGE_COLOR = "#FFFF00";
2277
+ var MAP_WALL_COLORS = {
2278
+ normal: "#000000",
2279
+ glass: "#E59400",
2280
+ glass2: "#33BBFF",
2281
+ door: "#000000",
2282
+ door2: "#DDDDDD",
2283
+ forceField: "#E59400",
2284
+ chainLinkFence: "#555555",
2285
+ levelSeparator: "#E5E500"
2286
+ };
2287
+ var DASHED_WALLS = /* @__PURE__ */ new Set(["glass", "glass2", "door", "door2", "levelSeparator"]);
2288
+ var Tile = class {
2289
+ constructor(x, y, t, hideLabel) {
2290
+ __publicField(this, "x");
2291
+ __publicField(this, "y");
2292
+ __publicField(this, "terrain");
2293
+ __publicField(this, "type");
2294
+ __publicField(this, "elevation");
2295
+ __publicField(this, "isStartingZone");
2296
+ __publicField(this, "isStartingZone4p");
2297
+ __publicField(this, "group");
2298
+ __publicField(this, "label");
2299
+ __publicField(this, "hideLabel");
2300
+ __publicField(this, "edges", new Array(4 * MAX_EDGES).fill(null));
2301
+ __publicField(this, "ramps", [false, false, false, false]);
2302
+ __publicField(this, "fillStyle");
2303
+ this.x = x;
2304
+ this.y = y;
2305
+ this.terrain = t.terrain ?? "clear";
2306
+ this.type = t.type;
2307
+ this.elevation = t.elevation ?? 1;
2308
+ this.isStartingZone = t.isStartingZone;
2309
+ this.isStartingZone4p = t.isStartingZone4p;
2310
+ this.group = t.group ?? 0;
2311
+ this.label = t.label;
2312
+ this.hideLabel = hideLabel;
2313
+ if (this.terrain === "hindering") this.fillStyle = HINDERING_TILE_COLOR;
2314
+ else if (this.terrain === "blocking") this.fillStyle = BLOCKING_TILE_COLOR;
2315
+ else if (this.terrain === "water") this.fillStyle = WATER_TILE_COLOR;
2316
+ else if (this.terrain === "obscuring") this.fillStyle = OBSCURING_TILE_COLOR;
2317
+ else if (this.terrain === "window") this.fillStyle = WINDOW_TILE_COLOR;
2318
+ else if (this.terrain === "special" || this.terrain === "special2") this.fillStyle = SPECIAL_TILE_COLOR;
2319
+ else if (this.isStartingZone || this.isStartingZone4p) this.fillStyle = STARTING_ZONE_TILE_COLOR;
2320
+ else if (typeof this.elevation === "string") this.fillStyle = MAP_ELEVATION_TILE_COLORS[1];
2321
+ else this.fillStyle = MAP_ELEVATION_TILE_COLORS[this.elevation - 1] ?? MAP_ELEVATION_TILE_COLORS[0];
2322
+ }
2323
+ addEdge(edgeType, strokeStyle, dashed) {
2324
+ while (this.edges[edgeType]) edgeType += 4;
2325
+ if (edgeType < this.edges.length) this.edges[edgeType] = { strokeStyle, dashed };
2326
+ }
2327
+ hasEdge(edgeType) {
2328
+ while (edgeType >= 0) {
2329
+ if (this.edges[edgeType]) return true;
2330
+ edgeType -= 4;
2331
+ }
2332
+ return false;
2333
+ }
2334
+ draw(ctx) {
2335
+ ctx.fillStyle = this.fillStyle;
2336
+ ctx.fillRect(this.x * MAP_TILE_SIZE, this.y * MAP_TILE_SIZE, MAP_TILE_SIZE, MAP_TILE_SIZE);
2337
+ }
2338
+ drawEdges(ctx) {
2339
+ const edgeLevels = [-1, -1, -1, -1];
2340
+ for (let i = 0; i < MAX_EDGES; i++) {
2341
+ for (let j = 0; j < 4; j++) {
2342
+ if (this.edges[4 * i + j]) edgeLevels[j] = i;
2343
+ }
2344
+ }
2345
+ for (let i = 0; i < 4; i++) {
2346
+ this.drawEdge(ctx, 0 /* LEFT */, edgeLevels[0 /* LEFT */]--);
2347
+ this.drawEdge(ctx, 1 /* RIGHT */, edgeLevels[1 /* RIGHT */]--);
2348
+ this.drawEdge(ctx, 2 /* TOP */, edgeLevels[2 /* TOP */]--);
2349
+ this.drawEdge(ctx, 3 /* BOTTOM */, edgeLevels[3 /* BOTTOM */]--);
2350
+ }
2351
+ this.drawRamps(ctx);
2352
+ this.drawLabel(ctx);
2353
+ }
2354
+ drawEdge(ctx, edgeType, level) {
2355
+ if (edgeType < 0 || level < 0) return;
2356
+ const edge = this.edges[edgeType + level * 4];
2357
+ if (!edge) return;
2358
+ let x0 = 0;
2359
+ let y0 = 0;
2360
+ let x1 = 0;
2361
+ let y1 = 0;
2362
+ let lineDashOffset = 0;
2363
+ const overhangSize = EDGE_SIZE * (level + 1);
2364
+ const oLeft = this.hasEdge(0 /* LEFT */ + level * 4) ? 0 : overhangSize;
2365
+ const oRight = this.hasEdge(1 /* RIGHT */ + level * 4) ? 0 : overhangSize;
2366
+ const oTop = this.hasEdge(2 /* TOP */ + level * 4) ? 0 : overhangSize;
2367
+ const oBottom = this.hasEdge(3 /* BOTTOM */ + level * 4) ? 0 : overhangSize;
2368
+ switch (edgeType) {
2369
+ case 0 /* LEFT */:
2370
+ x0 = x1 = this.x * MAP_TILE_SIZE + EDGE_SIZE / 2 + EDGE_SIZE * level;
2371
+ y0 = this.y * MAP_TILE_SIZE - oTop;
2372
+ y1 = (this.y + 1) * MAP_TILE_SIZE + oBottom;
2373
+ lineDashOffset = oTop;
2374
+ break;
2375
+ case 1 /* RIGHT */:
2376
+ x0 = x1 = (this.x + 1) * MAP_TILE_SIZE - EDGE_SIZE / 2 - EDGE_SIZE * level;
2377
+ y0 = this.y * MAP_TILE_SIZE - oTop;
2378
+ y1 = (this.y + 1) * MAP_TILE_SIZE + oBottom;
2379
+ lineDashOffset = oTop;
2380
+ break;
2381
+ case 2 /* TOP */:
2382
+ x0 = this.x * MAP_TILE_SIZE - oLeft;
2383
+ y0 = y1 = this.y * MAP_TILE_SIZE + EDGE_SIZE / 2 + EDGE_SIZE * level;
2384
+ x1 = (this.x + 1) * MAP_TILE_SIZE + oRight;
2385
+ lineDashOffset = oLeft;
2386
+ break;
2387
+ case 3 /* BOTTOM */:
2388
+ x0 = this.x * MAP_TILE_SIZE - oLeft;
2389
+ y0 = y1 = (this.y + 1) * MAP_TILE_SIZE - EDGE_SIZE / 2 - EDGE_SIZE * level;
2390
+ x1 = (this.x + 1) * MAP_TILE_SIZE + oRight;
2391
+ lineDashOffset = oLeft;
2392
+ break;
2393
+ }
2394
+ ctx.strokeStyle = edge.strokeStyle;
2395
+ ctx.lineWidth = EDGE_SIZE;
2396
+ if (edge.dashed) {
2397
+ ctx.setLineDash([EDGE_SIZE, EDGE_SIZE]);
2398
+ ctx.lineDashOffset = lineDashOffset;
2399
+ }
2400
+ ctx.beginPath();
2401
+ ctx.moveTo(x0, y0);
2402
+ ctx.lineTo(x1, y1);
2403
+ ctx.stroke();
2404
+ ctx.setLineDash([]);
2405
+ }
2406
+ drawRamps(ctx) {
2407
+ if (this.ramps[1 /* RIGHT */]) this.drawRamp(ctx, Math.PI * 3 / 2);
2408
+ if (this.ramps[0 /* LEFT */]) this.drawRamp(ctx, Math.PI / 2);
2409
+ if (this.ramps[2 /* TOP */]) this.drawRamp(ctx, Math.PI);
2410
+ if (this.ramps[3 /* BOTTOM */]) this.drawRamp(ctx, 0);
2411
+ }
2412
+ drawRamp(ctx, angle) {
2413
+ ctx.save();
2414
+ ctx.translate(this.x * MAP_TILE_SIZE + MAP_TILE_SIZE / 2, this.y * MAP_TILE_SIZE + MAP_TILE_SIZE / 2);
2415
+ ctx.rotate(angle);
2416
+ ctx.beginPath();
2417
+ ctx.moveTo(0, EDGE_SIZE);
2418
+ ctx.lineTo(-(MAP_TILE_SIZE / 2 - EDGE_SIZE * 2), MAP_TILE_SIZE / 2 - EDGE_SIZE);
2419
+ ctx.lineTo(MAP_TILE_SIZE / 2 - EDGE_SIZE * 2, MAP_TILE_SIZE / 2 - EDGE_SIZE);
2420
+ ctx.closePath();
2421
+ ctx.lineWidth = EDGE_SIZE * 2;
2422
+ ctx.strokeStyle = MAP_ELEVATION_EDGE_COLOR;
2423
+ ctx.stroke();
2424
+ ctx.fillStyle = MAP_ELEVATION_TILE_COLORS[1];
2425
+ ctx.fill();
2426
+ ctx.font = "18px Arial";
2427
+ ctx.fillStyle = "#000000";
2428
+ ctx.textAlign = "center";
2429
+ ctx.textBaseline = "middle";
2430
+ ctx.fillText(String(this.elevation), 0, MAP_TILE_SIZE / 4 + 4);
2431
+ ctx.restore();
2432
+ }
2433
+ drawLabel(ctx) {
2434
+ if (this.hideLabel || this.x !== 0 && this.y !== 0 && !this.label) return;
2435
+ let text = "";
2436
+ ctx.save();
2437
+ ctx.translate(this.x * MAP_TILE_SIZE + MAP_TILE_SIZE / 2, this.y * MAP_TILE_SIZE + MAP_TILE_SIZE / 2);
2438
+ if (this.label) {
2439
+ text = this.label.text;
2440
+ ctx.fillStyle = this.label.color ?? MAP_ELEVATION_EDGE_COLOR;
2441
+ ctx.rotate((this.label.rotate ?? 0) * Math.PI / 180);
2442
+ } else {
2443
+ if (this.y === 0) text = String.fromCharCode("A".charCodeAt(0) + this.x);
2444
+ if (this.x === 0) text += this.y + 1;
2445
+ ctx.fillStyle = "#000000";
2446
+ }
2447
+ ctx.font = "30px Arial";
2448
+ ctx.textAlign = "center";
2449
+ ctx.textBaseline = "middle";
2450
+ ctx.fillText(text, 0, 0);
2451
+ ctx.restore();
2452
+ }
2453
+ };
2454
+ var HCMap = class {
2455
+ constructor(json) {
2456
+ __publicField(this, "width");
2457
+ __publicField(this, "height");
2458
+ __publicField(this, "type");
2459
+ __publicField(this, "tiles", []);
2460
+ __publicField(this, "walls");
2461
+ __publicField(this, "ziplines");
2462
+ this.width = json.width;
2463
+ this.height = json.height;
2464
+ this.type = json.type ?? "outdoor";
2465
+ const hideLabels = json.hideLabels ?? false;
2466
+ for (let y = 0; y < this.height; y++) {
2467
+ for (let x = 0; x < this.width; x++) {
2468
+ const jt = { ...json.rows[y]?.tiles[x] ?? {} };
2469
+ if (!jt.terrain && json.defaultTerrain) jt.terrain = json.defaultTerrain;
2470
+ if (!jt.elevation && json.defaultElevation) jt.elevation = json.defaultElevation;
2471
+ if (!jt.type) jt.type = json.defaultType ?? this.type;
2472
+ this.tiles.push(new Tile(x, y, jt, hideLabels));
2473
+ }
2474
+ }
2475
+ for (let y = 0; y < this.height; y++) {
2476
+ for (let x = 0; x < this.width; x++) {
2477
+ const tile = this.getTile(x, y);
2478
+ this.tryAddEdge(tile, this.getTile(x - 1, y), 0 /* LEFT */);
2479
+ this.tryAddEdge(tile, this.getTile(x + 1, y), 1 /* RIGHT */);
2480
+ this.tryAddEdge(tile, this.getTile(x, y - 1), 2 /* TOP */);
2481
+ this.tryAddEdge(tile, this.getTile(x, y + 1), 3 /* BOTTOM */);
2482
+ }
2483
+ }
2484
+ for (const ramp of json.ramps ?? []) {
2485
+ let { x0, y0, x1, y1 } = ramp;
2486
+ if (x0 > x1 || y0 > y1) {
2487
+ [x0, x1] = [x1, x0];
2488
+ [y0, y1] = [y1, y0];
2489
+ }
2490
+ if (x0 < x1) {
2491
+ if (x0 >= 0 && x0 < this.width) this.tiles[y0 * this.width + x0].ramps[1 /* RIGHT */] = true;
2492
+ if (x1 >= 0 && x1 < this.width) this.tiles[y1 * this.width + x1].ramps[0 /* LEFT */] = true;
2493
+ } else {
2494
+ if (y0 >= 0 && y0 < this.height) this.tiles[y0 * this.width + x0].ramps[3 /* BOTTOM */] = true;
2495
+ if (y1 >= 0 && y1 < this.height) this.tiles[y1 * this.width + x1].ramps[2 /* TOP */] = true;
2496
+ }
2497
+ }
2498
+ this.walls = (json.walls ?? []).map((w) => ({ ...w, type: w.type ?? json.defaultWallType ?? "normal" }));
2499
+ this.ziplines = json.ziplines ?? [];
2500
+ }
2501
+ getTile(x, y) {
2502
+ if (x < 0 || y < 0 || x >= this.width || y >= this.height) return null;
2503
+ return this.tiles[y * this.width + x];
2504
+ }
2505
+ tryAddEdge(t0, t1, edgeType) {
2506
+ if (this.type === "indoorOutdoor" && t0.type === "indoor" && (t1 == null || t1.type === "outdoor")) {
2507
+ t0.addEdge(edgeType, MAP_INDOOR_EDGE_COLOR, false);
2508
+ }
2509
+ if (typeof t0.elevation === "number" && t0.elevation > 1 && (t1 == null || t0.elevation !== t1.elevation)) {
2510
+ t0.addEdge(edgeType, MAP_ELEVATION_EDGE_COLOR, false);
2511
+ }
2512
+ if (t0.isStartingZone && (t1 == null || !t1.isStartingZone)) {
2513
+ t0.addEdge(edgeType, STARTING_ZONE_EDGE_COLOR, false);
2514
+ }
2515
+ if (t0.isStartingZone4p && (t1 == null || !t1.isStartingZone4p)) {
2516
+ t0.addEdge(edgeType, STARTING_ZONE_EDGE_COLOR, true);
2517
+ }
2518
+ if (t0.terrain !== "clear" && (t1 == null || t0.terrain !== t1.terrain || t0.type !== t1.type || t0.elevation !== t1.elevation || t0.isStartingZone !== t1.isStartingZone || t0.isStartingZone4p !== t1.isStartingZone4p || t0.group !== t1.group)) {
2519
+ const dashed = t0.terrain === "window" || t0.terrain === "special2";
2520
+ t0.addEdge(edgeType, MAP_TERRAIN_EDGE_COLORS[t0.terrain] ?? "#FFFFFF", dashed);
2521
+ }
2522
+ }
2523
+ draw(ctx) {
2524
+ ctx.save();
2525
+ for (const tile of this.tiles) tile.draw(ctx);
2526
+ ctx.restore();
2527
+ ctx.save();
2528
+ for (const tile of this.tiles) tile.drawEdges(ctx);
2529
+ ctx.restore();
2530
+ ctx.save();
2531
+ ctx.strokeStyle = "#222222";
2532
+ ctx.lineWidth = 1;
2533
+ for (let y = 0; y < this.height; y++) {
2534
+ for (let x = 0; x < this.width; x++) {
2535
+ ctx.strokeRect(x * MAP_TILE_SIZE, y * MAP_TILE_SIZE, MAP_TILE_SIZE, MAP_TILE_SIZE);
2536
+ }
2537
+ }
2538
+ ctx.restore();
2539
+ for (const wall of this.walls) {
2540
+ ctx.save();
2541
+ this.drawWall(ctx, wall);
2542
+ ctx.restore();
2543
+ }
2544
+ for (const z of this.ziplines) {
2545
+ ctx.save();
2546
+ ctx.strokeStyle = MAP_WALL_COLORS.normal;
2547
+ ctx.lineCap = "round";
2548
+ ctx.lineWidth = 4;
2549
+ ctx.beginPath();
2550
+ ctx.moveTo(z.x0 * MAP_TILE_SIZE + MAP_TILE_SIZE / 2, z.y0 * MAP_TILE_SIZE + MAP_TILE_SIZE / 2);
2551
+ ctx.lineTo(z.x1 * MAP_TILE_SIZE + MAP_TILE_SIZE / 2, z.y1 * MAP_TILE_SIZE + MAP_TILE_SIZE / 2);
2552
+ ctx.stroke();
2553
+ ctx.restore();
2554
+ }
2555
+ }
2556
+ drawWall(ctx, wall) {
2557
+ const type = wall.type ?? "normal";
2558
+ ctx.lineWidth = WALL_SIZE;
2559
+ if (type === "levelSeparator") {
2560
+ ctx.strokeStyle = MAP_WALL_COLORS.normal;
2561
+ ctx.lineWidth = WALL_SIZE * 2;
2562
+ ctx.beginPath();
2563
+ ctx.moveTo(wall.x0 * MAP_TILE_SIZE, wall.y0 * MAP_TILE_SIZE);
2564
+ ctx.lineTo(wall.x1 * MAP_TILE_SIZE, wall.y1 * MAP_TILE_SIZE);
2565
+ ctx.stroke();
2566
+ ctx.lineWidth = WALL_SIZE;
2567
+ }
2568
+ ctx.strokeStyle = MAP_WALL_COLORS[type] ?? MAP_WALL_COLORS.normal;
2569
+ if (DASHED_WALLS.has(type)) {
2570
+ ctx.setLineDash([WALL_SIZE, WALL_SIZE]);
2571
+ ctx.lineDashOffset = WALL_SIZE / 2;
2572
+ } else {
2573
+ ctx.lineCap = "round";
2574
+ }
2575
+ ctx.beginPath();
2576
+ ctx.moveTo(wall.x0 * MAP_TILE_SIZE, wall.y0 * MAP_TILE_SIZE);
2577
+ ctx.lineTo(wall.x1 * MAP_TILE_SIZE, wall.y1 * MAP_TILE_SIZE);
2578
+ ctx.stroke();
2579
+ }
2580
+ };
2581
+ function mapSquare(map, x, y) {
2582
+ const tile = map.rows[y]?.tiles[x] || {};
2583
+ const environment = tile.type || map.defaultType || map.type || "outdoor";
2584
+ return {
2585
+ x,
2586
+ y,
2587
+ terrain: tile.terrain || map.defaultTerrain || "clear",
2588
+ elevation: tile.elevation ?? map.defaultElevation ?? 1,
2589
+ environment: environment === "indoor" || environment === "outdoor" ? environment : "unknown",
2590
+ starting: !!(tile.isStartingZone || tile.isStartingZone4p),
2591
+ label: tile.label?.text
2592
+ };
2593
+ }
2594
+ function mapElevationLevel(elevation) {
2595
+ const value = typeof elevation === "number" ? elevation : Number.parseInt(elevation, 10);
2596
+ return Number.isFinite(value) ? Math.max(1, value) : 2;
2597
+ }
2598
+ function mapMaxLevel(map) {
2599
+ let level = 1;
2600
+ for (let y = 0; y < map.height; y++) {
2601
+ for (let x = 0; x < map.width; x++) level = Math.max(level, mapElevationLevel(mapSquare(map, x, y).elevation));
2602
+ }
2603
+ return level;
2604
+ }
2605
+ function distanceToLine(x, y, line) {
2606
+ const dx = line.x1 - line.x0, dy = line.y1 - line.y0;
2607
+ const length2 = dx * dx + dy * dy;
2608
+ const t = length2 ? Math.max(0, Math.min(1, ((x - line.x0) * dx + (y - line.y0) * dy) / length2)) : 0;
2609
+ return Math.hypot(x - line.x0 - t * dx, y - line.y0 - t * dy);
2610
+ }
2611
+ function inspectMap(map, x, y, tolerance = 0.16) {
2612
+ if (x < 0 || y < 0 || x > map.width || y > map.height) return null;
2613
+ const cx2 = Math.min(map.width - 1, Math.floor(x)), cy = Math.min(map.height - 1, Math.floor(y));
2614
+ const square = mapSquare(map, cx2, cy);
2615
+ const wall = (map.walls || []).map((line) => ({ line, distance: distanceToLine(x, y, line) })).filter((item) => item.distance <= tolerance).sort((a, b) => a.distance - b.distance)[0]?.line;
2616
+ if (wall) return { square, wall: wall.type || map.defaultWallType || "normal", line: wall };
2617
+ const vertical = Math.abs(x - Math.round(x)), horizontal = Math.abs(y - Math.round(y));
2618
+ const alongX = vertical <= horizontal;
2619
+ const edge = Math.round(alongX ? x : y);
2620
+ if (Math.min(vertical, horizontal) <= tolerance) {
2621
+ const a = alongX ? { x: edge - 1, y: cy } : { x: cx2, y: edge - 1 };
2622
+ const b = alongX ? { x: edge, y: cy } : { x: cx2, y: edge };
2623
+ const valid = (p) => p.x >= 0 && p.y >= 0 && p.x < map.width && p.y < map.height;
2624
+ if (valid(a) && valid(b)) return {
2625
+ square: mapSquare(map, a.x, a.y),
2626
+ adjacent: mapSquare(map, b.x, b.y),
2627
+ line: alongX ? { x0: edge, y0: cy, x1: edge, y1: cy + 1 } : { x0: cx2, y0: edge, x1: cx2 + 1, y1: edge }
2628
+ };
2629
+ }
2630
+ return { square };
2631
+ }
2632
+ function mapSquareCoordinate(square) {
2633
+ let index = square.x + 1, column = "";
2634
+ while (index > 0) {
2635
+ index--;
2636
+ column = String.fromCharCode(65 + index % 26) + column;
2637
+ index = Math.floor(index / 26);
2638
+ }
2639
+ return `${column}${square.y + 1}`;
2640
+ }
2641
+ var MAP_SCENE_LEVEL_HEIGHT = 0.62;
2642
+ var MAP_SCENE_BASE_Z = -0.34;
2643
+ var WATER_DROP = 0.1;
2644
+ var TERRAIN_VOLUME = { obscuring: 0.42, window: 0.42 };
2645
+ var WALL_HEIGHT = { levelSeparator: 0.12, door: 0.62, door2: 0.62 };
2646
+ var DEFAULT_WALL_HEIGHT = 0.62;
2647
+ function mapSceneCell(scene, x, y) {
2648
+ if (x < 0 || y < 0 || x >= scene.width || y >= scene.height) return null;
2649
+ return scene.cells[y * scene.width + x];
2650
+ }
2651
+ function buildMapScene(map) {
2652
+ const { width, height } = map;
2653
+ const maxLevel = mapMaxLevel(map);
2654
+ const cells = [];
2655
+ for (let y = 0; y < height; y++) {
2656
+ for (let x = 0; x < width; x++) {
2657
+ const square = mapSquare(map, x, y);
2658
+ const level = mapElevationLevel(square.elevation);
2659
+ const floor = (level - 1) * MAP_SCENE_LEVEL_HEIGHT - (square.terrain === "water" ? WATER_DROP : 0);
2660
+ const top = square.terrain !== "blocking" ? floor + (TERRAIN_VOLUME[square.terrain] ?? 0) : square.environment === "indoor" ? maxLevel * MAP_SCENE_LEVEL_HEIGHT : floor + MAP_SCENE_LEVEL_HEIGHT;
2661
+ cells.push({ ...square, level, floor, top });
2662
+ }
2663
+ }
2664
+ const scene = { width, height, cells };
2665
+ const walls = [];
2666
+ for (const wall of map.walls ?? []) {
2667
+ const type = wall.type || map.defaultWallType || "normal";
2668
+ const length = Math.hypot(wall.x1 - wall.x0, wall.y1 - wall.y0);
2669
+ const pieces = Math.max(1, Math.round(length));
2670
+ for (let i = 0; i < pieces; i++) {
2671
+ const x0 = wall.x0 + (wall.x1 - wall.x0) * i / pieces, y0 = wall.y0 + (wall.y1 - wall.y0) * i / pieces;
2672
+ const x1 = wall.x0 + (wall.x1 - wall.x0) * (i + 1) / pieces, y1 = wall.y0 + (wall.y1 - wall.y0) * (i + 1) / pieces;
2673
+ const mx = (x0 + x1) / 2, my = (y0 + y1) / 2;
2674
+ const nx = length ? (y1 - y0) / (length / pieces) : 0, ny = length ? (x0 - x1) / (length / pieces) : 0;
2675
+ const sides = [[mx + nx * 0.25, my + ny * 0.25], [mx - nx * 0.25, my - ny * 0.25]].map(([px, py]) => mapSceneCell(scene, Math.floor(px), Math.floor(py))).filter((cell) => !!cell);
2676
+ const floors = sides.length ? sides.map((cell) => cell.floor) : [0];
2677
+ const z1 = Math.max(...floors) + (WALL_HEIGHT[type] ?? DEFAULT_WALL_HEIGHT);
2678
+ walls.push({ x0, y0, x1, y1, type, z0: Math.min(...floors), z1 });
2679
+ }
2680
+ }
2681
+ const ramps = [];
2682
+ for (const ramp of map.ramps ?? []) {
2683
+ const a = mapSceneCell(scene, ramp.x0, ramp.y0), b = mapSceneCell(scene, ramp.x1, ramp.y1);
2684
+ if (!a || !b || Math.abs(a.x - b.x) + Math.abs(a.y - b.y) !== 1 || a.floor === b.floor) continue;
2685
+ const [low, high] = a.floor < b.floor ? [a, b] : [b, a];
2686
+ ramps.push({ x: low.x, y: low.y, dx: high.x - low.x, dy: high.y - low.y, from: low.floor, to: high.floor });
2687
+ }
2688
+ const ziplines = (map.ziplines ?? []).map((line) => ({
2689
+ x0: line.x0 + 0.5,
2690
+ y0: line.y0 + 0.5,
2691
+ x1: line.x1 + 0.5,
2692
+ y1: line.y1 + 0.5,
2693
+ z: Math.max(mapSceneCell(scene, line.x0, line.y0)?.top ?? 0, mapSceneCell(scene, line.x1, line.y1)?.top ?? 0) + 0.7
2694
+ }));
2695
+ const maxTop = Math.max(...cells.map((cell) => cell.top), ...walls.map((wall) => wall.z1), ...ziplines.map((line) => line.z));
2696
+ return { width, height, cells, walls, ramps, ziplines, maxTop };
2697
+ }
2698
+ var MAP_DEFAULT_CAMERA = { azimuth: -32, tilt: 52, zoom: 1, panX: 0, panY: 0 };
2699
+ var MAP_TILT_RANGE = [6, 90];
2700
+ var MAP_ZOOM_RANGE = [1, 6];
2701
+ var clampMapValue = (value, min, max) => Math.min(max, Math.max(min, value));
2702
+ function makeMapProjection(scene, camera, viewport) {
2703
+ const rad = camera.azimuth * Math.PI / 180, tilt = camera.tilt * Math.PI / 180;
2704
+ const sin = Math.sin(rad), cos = Math.cos(rad), sinT = Math.sin(tilt), cosT = Math.cos(tilt);
2705
+ const cx2 = scene.width / 2, cy = scene.height / 2, midZ = (scene.maxTop + MAP_SCENE_BASE_Z) / 2;
2706
+ const halfWidth = Math.hypot(cx2, cy);
2707
+ const halfHeight = Math.hypot(halfWidth, (scene.maxTop - MAP_SCENE_BASE_Z) / 2);
2708
+ const fit = Math.min(viewport.width / (halfWidth * 2.06), viewport.height / (halfHeight * 2.06));
2709
+ const scale = fit * camera.zoom;
2710
+ const ox = viewport.width / 2 + camera.panX, oy = viewport.height / 2 + camera.panY;
2711
+ return {
2712
+ scale,
2713
+ sin,
2714
+ cos,
2715
+ point(x, y, z) {
2716
+ const dx = x - cx2, dy = y - cy;
2717
+ return [ox + scale * (dx * cos - dy * sin), oy + scale * ((dx * sin + dy * cos) * sinT - (z - midZ) * cosT)];
2718
+ },
2719
+ depth(x, y) {
2720
+ return (x - cx2) * sin + (y - cy) * cos;
2721
+ },
2722
+ ground(px, py, z) {
2723
+ const rx = (px - ox) / scale, ry = ((py - oy) / scale + (z - midZ) * cosT) / sinT;
2724
+ return [cx2 + rx * cos + ry * sin, cy - rx * sin + ry * cos];
2725
+ }
2726
+ };
2727
+ }
2728
+ function clampMapPan(scene, camera, viewport) {
2729
+ const limit = Math.hypot(scene.width, scene.height) / 2 * makeMapProjection(scene, camera, viewport).scale;
2730
+ return { ...camera, panX: clampMapValue(camera.panX, -limit, limit), panY: clampMapValue(camera.panY, -limit, limit) };
2731
+ }
2732
+ function zoomMapCamera(scene, camera, viewport, factor, anchor = { x: 0, y: 0 }) {
2733
+ const zoom = clampMapValue(camera.zoom * factor, MAP_ZOOM_RANGE[0], MAP_ZOOM_RANGE[1]);
2734
+ const applied = zoom / camera.zoom;
2735
+ return clampMapPan(scene, {
2736
+ ...camera,
2737
+ zoom,
2738
+ panX: anchor.x + (camera.panX - anchor.x) * applied,
2739
+ panY: anchor.y + (camera.panY - anchor.y) * applied
2740
+ }, viewport);
2741
+ }
2742
+ var MAP_CELL_SIDES = [
2743
+ { ax: 0, ay: 1, bx: 0, by: 0, dx: -1, dy: 0 },
2744
+ { ax: 1, ay: 0, bx: 1, by: 1, dx: 1, dy: 0 },
2745
+ { ax: 0, ay: 0, bx: 1, by: 0, dx: 0, dy: -1 },
2746
+ { ax: 1, ay: 1, bx: 0, by: 1, dx: 0, dy: 1 }
2747
+ ];
2748
+ var inside = (px, py, polygon2) => {
2749
+ let hit = false;
2750
+ for (let i = 0, j = polygon2.length - 1; i < polygon2.length; j = i++) {
2751
+ const [xi, yi] = polygon2[i], [xj, yj] = polygon2[j];
2752
+ if (yi > py !== yj > py && px < (xj - xi) * (py - yi) / (yj - yi) + xi) hit = !hit;
2753
+ }
2754
+ return hit;
2755
+ };
2756
+ function pickMapScene(scene, view, px, py) {
2757
+ const items = [];
2758
+ for (const wall of scene.walls) {
2759
+ items.push({
2760
+ depth: view.depth((wall.x0 + wall.x1) / 2, (wall.y0 + wall.y1) / 2),
2761
+ test() {
2762
+ const a = view.point(wall.x0, wall.y0, wall.z0), b = view.point(wall.x1, wall.y1, wall.z0);
2763
+ const c = view.point(wall.x1, wall.y1, wall.z1), d = view.point(wall.x0, wall.y0, wall.z1);
2764
+ if (!inside(px, py, [a, b, c, d])) return null;
2765
+ const length2 = (b[0] - a[0]) ** 2 + (b[1] - a[1]) ** 2;
2766
+ const t = length2 ? clampMapValue(((px - a[0]) * (b[0] - a[0]) + (py - a[1]) * (b[1] - a[1])) / length2, 0, 1) : 0.5;
2767
+ return { x: wall.x0 + (wall.x1 - wall.x0) * t, y: wall.y0 + (wall.y1 - wall.y0) * t };
2768
+ }
2769
+ });
2770
+ }
2771
+ for (const cell of scene.cells) {
2772
+ items.push({
2773
+ depth: view.depth(cell.x + 0.5, cell.y + 0.5),
2774
+ test() {
2775
+ const p00 = view.point(cell.x, cell.y, cell.top), p10 = view.point(cell.x + 1, cell.y, cell.top);
2776
+ const p11 = view.point(cell.x + 1, cell.y + 1, cell.top), p01 = view.point(cell.x, cell.y + 1, cell.top);
2777
+ if (inside(px, py, [p00, p10, p11, p01])) {
2778
+ const ax = p10[0] - p00[0], ay = p10[1] - p00[1], bx = p01[0] - p00[0], by = p01[1] - p00[1];
2779
+ const det = ax * by - ay * bx;
2780
+ if (!det) return { x: cell.x + 0.5, y: cell.y + 0.5 };
2781
+ const u = ((px - p00[0]) * by - (py - p00[1]) * bx) / det, v = ((py - p00[1]) * ax - (px - p00[0]) * ay) / det;
2782
+ return { x: cell.x + clampMapValue(u, 0, 1), y: cell.y + clampMapValue(v, 0, 1) };
2783
+ }
2784
+ for (const side of MAP_CELL_SIDES) {
2785
+ if (side.dx * view.sin + side.dy * view.cos <= 0) continue;
2786
+ const neighbor = mapSceneCell(scene, cell.x + side.dx, cell.y + side.dy);
2787
+ const low = neighbor ? neighbor.top : MAP_SCENE_BASE_Z;
2788
+ if (low >= cell.top) continue;
2789
+ const a = view.point(cell.x + side.ax, cell.y + side.ay, low), b = view.point(cell.x + side.bx, cell.y + side.by, low);
2790
+ const c = view.point(cell.x + side.bx, cell.y + side.by, cell.top), d = view.point(cell.x + side.ax, cell.y + side.ay, cell.top);
2791
+ if (inside(px, py, [a, b, c, d])) {
2792
+ return { x: cell.x + 0.5 + side.dx * 0.4, y: cell.y + 0.5 + side.dy * 0.4 };
2793
+ }
2794
+ }
2795
+ return null;
2796
+ }
2797
+ });
2798
+ }
2799
+ items.sort((a, b) => b.depth - a.depth);
2800
+ for (const item of items) {
2801
+ const hit = item.test();
2802
+ if (hit) return hit;
2803
+ }
2804
+ return null;
2805
+ }
2806
+ function mapFootprintOutline({ x, y, width, height, cells }, boardWidth, boardHeight) {
2807
+ const inside2 = (cx2, cy) => cx2 >= x && cy >= y && cx2 < x + width && cy < y + height && cx2 >= 0 && cy >= 0 && cx2 < boardWidth && cy < boardHeight && (!cells || !!cells[cy - y]?.[cx2 - x]);
2808
+ const squares = [];
2809
+ const edges = [];
2810
+ for (let cy = Math.max(0, y); cy < Math.min(boardHeight, y + height); cy++) {
2811
+ for (let cx2 = Math.max(0, x); cx2 < Math.min(boardWidth, x + width); cx2++) {
2812
+ if (!inside2(cx2, cy)) continue;
2813
+ squares.push([cx2, cy]);
2814
+ if (!inside2(cx2, cy - 1)) edges.push([[cx2, cy], [cx2 + 1, cy]]);
2815
+ if (!inside2(cx2 + 1, cy)) edges.push([[cx2 + 1, cy], [cx2 + 1, cy + 1]]);
2816
+ if (!inside2(cx2, cy + 1)) edges.push([[cx2, cy + 1], [cx2 + 1, cy + 1]]);
2817
+ if (!inside2(cx2 - 1, cy)) edges.push([[cx2, cy], [cx2, cy + 1]]);
2818
+ }
2819
+ }
2820
+ const directions = /* @__PURE__ */ new Map();
2821
+ for (const [a, b] of edges) {
2822
+ const direction = a[1] === b[1] ? "h" : "v";
2823
+ for (const point of [a, b]) {
2824
+ const key = point.join(",");
2825
+ directions.set(key, (directions.get(key) ?? /* @__PURE__ */ new Set()).add(direction));
2826
+ }
2827
+ }
2828
+ const corners = [...directions].filter(([, set]) => set.size === 2).map(([key]) => key.split(",").map(Number));
2829
+ return { squares, edges, corners };
2830
+ }
2831
+ var SIDE_COLORS = { indoor: "#7f8797", outdoor: "#a3906f", unknown: "#9a948a" };
2832
+ var MOVE_VALID_COLOR = "#1f7ae0";
2833
+ var MOVE_INVALID_COLOR = "#d0242c";
2834
+ var WALL_THICKNESS = 0.07;
2835
+ var HINDERING_BLOCK = "#5acb5a";
2836
+ var TRANSLUCENT_WALLS = { glass: 0.42, glass2: 0.42, forceField: 0.42, chainLinkFence: 0.55 };
2837
+ var GLASS_COLORS = { glass: "#e59400", glass2: "#33bbff", forceField: "#e59400" };
2838
+ function shade(hex, factor) {
2839
+ const n = Number.parseInt(hex.slice(1), 16);
2840
+ const c = [n >> 16, n >> 8 & 255, n & 255].map((v) => Math.round(Math.min(255, v * factor)));
2841
+ return `rgb(${c[0]},${c[1]},${c[2]})`;
2842
+ }
2843
+ function faceLight(view, nx, ny) {
2844
+ const sx = nx * view.cos - ny * view.sin;
2845
+ return 0.74 - sx * 0.2;
2846
+ }
2847
+ function polygon(ctx, points) {
2848
+ ctx.beginPath();
2849
+ points.forEach(([x, y], index) => index ? ctx.lineTo(x, y) : ctx.moveTo(x, y));
2850
+ ctx.closePath();
2851
+ }
2852
+ function face(ctx, points, color) {
2853
+ polygon(ctx, points);
2854
+ ctx.fillStyle = color;
2855
+ ctx.fill();
2856
+ ctx.strokeStyle = color;
2857
+ ctx.lineWidth = 0.7;
2858
+ ctx.stroke();
2859
+ }
2860
+ function sideColor(cell) {
2861
+ if (cell.terrain === "blocking") return MAP_TERRAIN_EDGE_COLORS.blocking;
2862
+ if (cell.level > 1) return MAP_ELEVATION_TILE_COLORS[Math.min(cell.level, MAP_ELEVATION_TILE_COLORS.length) - 1];
2863
+ if (cell.terrain === "water") return MAP_TERRAIN_EDGE_COLORS.water;
2864
+ if (cell.terrain === "hindering") return MAP_TERRAIN_EDGE_COLORS.hindering;
2865
+ if (cell.terrain === "obscuring") return MAP_TERRAIN_EDGE_COLORS.obscuring;
2866
+ if (cell.terrain === "window") return MAP_TERRAIN_EDGE_COLORS.window;
2867
+ return SIDE_COLORS[cell.environment] ?? SIDE_COLORS.unknown;
2868
+ }
2869
+ function drawCellSides(ctx, scene, view, cell) {
2870
+ const { point } = view;
2871
+ for (const side of MAP_CELL_SIDES) {
2872
+ if (side.dx * view.sin + side.dy * view.cos <= 0) continue;
2873
+ const neighbor = mapSceneCell(scene, cell.x + side.dx, cell.y + side.dy);
2874
+ const low = neighbor ? neighbor.top : MAP_SCENE_BASE_Z;
2875
+ if (low >= cell.top) continue;
2876
+ const ax = cell.x + side.ax, ay = cell.y + side.ay, bx = cell.x + side.bx, by = cell.y + side.by;
2877
+ const light = faceLight(view, side.dx, side.dy);
2878
+ face(ctx, [point(ax, ay, low), point(bx, by, low), point(bx, by, cell.top), point(ax, ay, cell.top)], shade(sideColor(cell), light));
2879
+ ctx.strokeStyle = "rgba(0,0,0,.26)";
2880
+ ctx.lineWidth = 1;
2881
+ for (let z = 0; z < cell.top - 0.01; z += MAP_SCENE_LEVEL_HEIGHT) {
2882
+ if (z <= low + 0.01) continue;
2883
+ const [x0, y0] = point(ax, ay, z), [x1, y1] = point(bx, by, z);
2884
+ ctx.beginPath();
2885
+ ctx.moveTo(x0, y0);
2886
+ ctx.lineTo(x1, y1);
2887
+ ctx.stroke();
2888
+ }
2889
+ }
2890
+ }
2891
+ function drawCellTop(ctx, scene, view, cell, texture, shadows) {
2892
+ const p00 = view.point(cell.x, cell.y, cell.top), p10 = view.point(cell.x + 1, cell.y, cell.top), p01 = view.point(cell.x, cell.y + 1, cell.top);
2893
+ ctx.save();
2894
+ ctx.transform(p10[0] - p00[0], p10[1] - p00[1], p01[0] - p00[0], p01[1] - p00[1], p00[0], p00[1]);
2895
+ ctx.drawImage(texture, cell.x * MAP_TILE_SIZE, cell.y * MAP_TILE_SIZE, MAP_TILE_SIZE, MAP_TILE_SIZE, -0.012, -0.012, 1.024, 1.024);
2896
+ if (cell.environment === "indoor") {
2897
+ ctx.fillStyle = (cell.x + cell.y) % 2 ? "rgba(52,62,92,.13)" : "rgba(52,62,92,.07)";
2898
+ ctx.fillRect(0, 0, 1, 1);
2899
+ }
2900
+ MAP_CELL_SIDES.forEach((side, index) => {
2901
+ const neighbor = mapSceneCell(scene, cell.x + side.dx, cell.y + side.dy);
2902
+ if (!neighbor || neighbor.top <= cell.top + 0.01) return;
2903
+ ctx.fillStyle = shadows[index];
2904
+ ctx.fillRect(0, 0, 1, 1);
2905
+ });
2906
+ ctx.restore();
2907
+ }
2908
+ function hash(x, y, salt) {
2909
+ const value = Math.sin(x * 127.1 + y * 311.7 + salt * 74.7) * 43758.5453;
2910
+ return value - Math.floor(value);
2911
+ }
2912
+ function drawBox(ctx, view, x0, y0, x1, y1, z0, z1, color, topFactor = 1.08) {
2913
+ const { point } = view;
2914
+ const sides = [
2915
+ { ax: x0, ay: y1, bx: x0, by: y0, dx: -1, dy: 0 },
2916
+ { ax: x1, ay: y0, bx: x1, by: y1, dx: 1, dy: 0 },
2917
+ { ax: x0, ay: y0, bx: x1, by: y0, dx: 0, dy: -1 },
2918
+ { ax: x1, ay: y1, bx: x0, by: y1, dx: 0, dy: 1 }
2919
+ ];
2920
+ for (const side of sides) {
2921
+ if (side.dx * view.sin + side.dy * view.cos <= 0) continue;
2922
+ face(ctx, [point(side.ax, side.ay, z0), point(side.bx, side.by, z0), point(side.bx, side.by, z1), point(side.ax, side.ay, z1)], shade(color, faceLight(view, side.dx, side.dy)));
2923
+ }
2924
+ face(ctx, [point(x0, y0, z1), point(x1, y0, z1), point(x1, y1, z1), point(x0, y1, z1)], shade(color, topFactor));
2925
+ }
2926
+ function drawHindering(ctx, view, cell) {
2927
+ const spots = [[0.28, 0.3], [0.7, 0.26], [0.3, 0.72], [0.72, 0.7]].map(([u, v], index) => {
2928
+ const size = 0.13 + hash(cell.x, cell.y, index) * 0.07;
2929
+ const x = cell.x + u + (hash(cell.x, cell.y, index + 4) - 0.5) * 0.1, y = cell.y + v + (hash(cell.x, cell.y, index + 8) - 0.5) * 0.1;
2930
+ return { x, y, size, height: 0.12 + hash(cell.x, cell.y, index + 12) * 0.16 };
2931
+ }).sort((a, b) => view.depth(a.x, a.y) - view.depth(b.x, b.y));
2932
+ for (const spot of spots) {
2933
+ drawBox(ctx, view, spot.x - spot.size, spot.y - spot.size, spot.x + spot.size, spot.y + spot.size, cell.top, cell.top + spot.height, HINDERING_BLOCK, 1.1);
2934
+ }
2935
+ }
2936
+ function drawRamp(ctx, view, ramp) {
2937
+ const { point } = view;
2938
+ const cx2 = ramp.x + 0.5 + ramp.dx * 0.5, cy = ramp.y + 0.5 + ramp.dy * 0.5;
2939
+ const sx = ramp.dy * 0.2, sy = ramp.dx * 0.2, lx = -ramp.dx * 0.38, ly = -ramp.dy * 0.38;
2940
+ const a = [cx2 - sx, cy - sy], b = [cx2 + sx, cy + sy];
2941
+ const c = [cx2 + sx + lx, cy + sy + ly], d = [cx2 - sx + lx, cy - sy + ly];
2942
+ const color = MAP_ELEVATION_TILE_COLORS[1];
2943
+ face(ctx, [point(a[0], a[1], ramp.from), point(d[0], d[1], ramp.from), point(a[0], a[1], ramp.to)], shade(color, 0.66));
2944
+ face(ctx, [point(b[0], b[1], ramp.from), point(c[0], c[1], ramp.from), point(b[0], b[1], ramp.to)], shade(color, 0.66));
2945
+ polygon(ctx, [point(a[0], a[1], ramp.to), point(b[0], b[1], ramp.to), point(c[0], c[1], ramp.from), point(d[0], d[1], ramp.from)]);
2946
+ ctx.fillStyle = color;
2947
+ ctx.fill();
2948
+ ctx.strokeStyle = "#ff0000";
2949
+ ctx.lineWidth = 1.5;
2950
+ ctx.stroke();
2951
+ }
2952
+ function drawWall(ctx, view, wall) {
2953
+ if (wall.x0 !== wall.x1 && wall.y0 !== wall.y1) {
2954
+ face(ctx, [view.point(wall.x0, wall.y0, wall.z0), view.point(wall.x1, wall.y1, wall.z0), view.point(wall.x1, wall.y1, wall.z1), view.point(wall.x0, wall.y0, wall.z1)], "#30343c");
2955
+ return;
2956
+ }
2957
+ const half = WALL_THICKNESS / 2;
2958
+ const ox = wall.y0 === wall.y1 ? 0 : half, oy = wall.y0 === wall.y1 ? half : 0;
2959
+ const x0 = Math.min(wall.x0, wall.x1) - ox, x1 = Math.max(wall.x0, wall.x1) + ox;
2960
+ const y0 = Math.min(wall.y0, wall.y1) - oy, y1 = Math.max(wall.y0, wall.y1) + oy;
2961
+ const alpha = TRANSLUCENT_WALLS[wall.type];
2962
+ const color = GLASS_COLORS[wall.type] ?? (wall.type === "normal" ? "#30343c" : MAP_WALL_COLORS[wall.type] ?? "#30343c");
2963
+ ctx.save();
2964
+ if (alpha) ctx.globalAlpha = alpha;
2965
+ if (wall.type === "door" || wall.type === "door2") {
2966
+ const along = wall.y0 === wall.y1;
2967
+ const post = 0.16;
2968
+ const pieces = along ? [[x0, y0, x0 + post, y1, wall.z0, wall.z1], [x1 - post, y0, x1, y1, wall.z0, wall.z1], [x0, y0, x1, y1, wall.z1 - 0.14, wall.z1]] : [[x0, y0, x1, y0 + post, wall.z0, wall.z1], [x0, y1 - post, x1, y1, wall.z0, wall.z1], [x0, y0, x1, y1, wall.z1 - 0.14, wall.z1]];
2969
+ const doorColor = wall.type === "door2" ? "#dddddd" : "#30343c";
2970
+ pieces.sort((a, b) => view.depth(a[0], a[1]) - view.depth(b[0], b[1]));
2971
+ for (const p of pieces) drawBox(ctx, view, p[0], p[1], p[2], p[3], p[4], p[5], doorColor);
2972
+ } else {
2973
+ drawBox(ctx, view, x0, y0, x1, y1, wall.z0, wall.z1, color, 1.25);
2974
+ }
2975
+ ctx.restore();
2976
+ }
2977
+ function drawIndoorCurb(ctx, scene, view, cell) {
2978
+ ctx.strokeStyle = MAP_INDOOR_EDGE_COLOR;
2979
+ ctx.lineWidth = 2;
2980
+ for (const side of MAP_CELL_SIDES) {
2981
+ const neighbor = mapSceneCell(scene, cell.x + side.dx, cell.y + side.dy);
2982
+ if (neighbor?.environment !== "outdoor") continue;
2983
+ const [x0, y0] = view.point(cell.x + side.ax, cell.y + side.ay, cell.top), [x1, y1] = view.point(cell.x + side.bx, cell.y + side.by, cell.top);
2984
+ ctx.beginPath();
2985
+ ctx.moveTo(x0, y0);
2986
+ ctx.lineTo(x1, y1);
2987
+ ctx.stroke();
2988
+ }
2989
+ }
2990
+ function drawFootprint(ctx, scene, view, footprint) {
2991
+ const { squares, edges, corners } = mapFootprintOutline(footprint, scene.width, scene.height);
2992
+ if (!squares.length) return;
2993
+ const { top, valid } = footprint;
2994
+ const color = valid ? MOVE_VALID_COLOR : MOVE_INVALID_COLOR;
2995
+ ctx.save();
2996
+ ctx.beginPath();
2997
+ for (const [cx2, cy] of squares) {
2998
+ const points = [[cx2, cy], [cx2 + 1, cy], [cx2 + 1, cy + 1], [cx2, cy + 1]].map(([px, py]) => view.point(px, py, 0));
2999
+ ctx.moveTo(...points[0]);
3000
+ for (const point of points.slice(1)) ctx.lineTo(...point);
3001
+ ctx.closePath();
3002
+ }
3003
+ ctx.globalAlpha = 0.28;
3004
+ ctx.fillStyle = color;
3005
+ ctx.fill();
3006
+ ctx.globalAlpha = 1;
3007
+ ctx.strokeStyle = color;
3008
+ ctx.lineWidth = 2;
3009
+ if (!valid) ctx.setLineDash([6, 4]);
3010
+ const strokeEdges = (z) => {
3011
+ ctx.beginPath();
3012
+ for (const [a, b] of edges) {
3013
+ ctx.moveTo(...view.point(a[0], a[1], z));
3014
+ ctx.lineTo(...view.point(b[0], b[1], z));
3015
+ }
3016
+ ctx.stroke();
3017
+ };
3018
+ strokeEdges(0);
3019
+ if (top > 0.01) {
3020
+ strokeEdges(top);
3021
+ ctx.beginPath();
3022
+ for (const [cx2, cy] of corners) {
3023
+ ctx.moveTo(...view.point(cx2, cy, 0));
3024
+ ctx.lineTo(...view.point(cx2, cy, top));
3025
+ }
3026
+ ctx.stroke();
3027
+ }
3028
+ ctx.restore();
3029
+ }
3030
+ function drawHighlight(ctx, scene, view, highlight) {
3031
+ if (highlight.footprint) {
3032
+ drawFootprint(ctx, scene, view, highlight.footprint);
3033
+ return;
3034
+ }
3035
+ if (highlight.line) {
3036
+ const { x0, y0, x1, y1 } = highlight.line;
3037
+ const mx = (x0 + x1) / 2, my = (y0 + y1) / 2;
3038
+ const near = [[Math.floor(mx - 0.01), Math.floor(my - 0.01)], [Math.floor(mx + 0.01), Math.floor(my + 0.01)]].map(([x, y]) => mapSceneCell(scene, x, y)?.top ?? 0);
3039
+ const z = Math.max(...near);
3040
+ const [ax, ay] = view.point(x0, y0, z), [bx, by] = view.point(x1, y1, z);
3041
+ ctx.strokeStyle = "rgba(0,0,0,.6)";
3042
+ ctx.lineWidth = 4;
3043
+ ctx.lineCap = "round";
3044
+ ctx.beginPath();
3045
+ ctx.moveTo(ax, ay);
3046
+ ctx.lineTo(bx, by);
3047
+ ctx.stroke();
3048
+ ctx.lineCap = "butt";
3049
+ return;
3050
+ }
3051
+ const cell = highlight.square && mapSceneCell(scene, highlight.square.x, highlight.square.y);
3052
+ if (!cell) return;
3053
+ polygon(ctx, [view.point(cell.x, cell.y, cell.top), view.point(cell.x + 1, cell.y, cell.top), view.point(cell.x + 1, cell.y + 1, cell.top), view.point(cell.x, cell.y + 1, cell.top)]);
3054
+ ctx.fillStyle = "rgba(0,0,0,.16)";
3055
+ ctx.fill();
3056
+ ctx.strokeStyle = "rgba(0,0,0,.6)";
3057
+ ctx.lineWidth = 2;
3058
+ ctx.stroke();
3059
+ }
3060
+ function drawMapScene(ctx, scene, view, texture, highlight, { tufts = true } = {}) {
3061
+ const pad = 0.35;
3062
+ polygon(ctx, [view.point(-pad, -pad, MAP_SCENE_BASE_Z - 0.16), view.point(scene.width + pad, -pad, MAP_SCENE_BASE_Z - 0.16), view.point(scene.width + pad, scene.height + pad, MAP_SCENE_BASE_Z - 0.16), view.point(-pad, scene.height + pad, MAP_SCENE_BASE_Z - 0.16)]);
3063
+ ctx.fillStyle = "rgba(0,0,0,.14)";
3064
+ ctx.fill();
3065
+ const shadowDepth = 0.24;
3066
+ const shadows = [[0, 0, shadowDepth, 0], [1, 0, 1 - shadowDepth, 0], [0, 0, 0, shadowDepth], [0, 1, 0, 1 - shadowDepth]].map(([x0, y0, x1, y1]) => {
3067
+ const gradient = ctx.createLinearGradient(x0, y0, x1, y1);
3068
+ gradient.addColorStop(0, "rgba(0,0,0,.26)");
3069
+ gradient.addColorStop(1, "rgba(0,0,0,0)");
3070
+ return gradient;
3071
+ });
3072
+ const rampsByCell = /* @__PURE__ */ new Map();
3073
+ for (const ramp of scene.ramps) {
3074
+ const key = ramp.y * scene.width + ramp.x;
3075
+ rampsByCell.set(key, [...rampsByCell.get(key) ?? [], ramp]);
3076
+ }
3077
+ const mixed = scene.cells.some((cell) => cell.environment === "indoor") && scene.cells.some((cell) => cell.environment === "outdoor");
3078
+ const items = [];
3079
+ for (const cell of scene.cells) {
3080
+ items.push({
3081
+ depth: view.depth(cell.x + 0.5, cell.y + 0.5),
3082
+ draw() {
3083
+ drawCellSides(ctx, scene, view, cell);
3084
+ drawCellTop(ctx, scene, view, cell, texture, shadows);
3085
+ if (mixed && cell.environment === "indoor") drawIndoorCurb(ctx, scene, view, cell);
3086
+ for (const ramp of rampsByCell.get(cell.y * scene.width + cell.x) ?? []) drawRamp(ctx, view, ramp);
3087
+ if (tufts && cell.terrain === "hindering") drawHindering(ctx, view, cell);
3088
+ }
3089
+ });
3090
+ }
3091
+ for (const wall of scene.walls) {
3092
+ items.push({ depth: view.depth((wall.x0 + wall.x1) / 2, (wall.y0 + wall.y1) / 2), draw: () => drawWall(ctx, view, wall) });
3093
+ }
3094
+ items.sort((a, b) => a.depth - b.depth);
3095
+ for (const item of items) item.draw();
3096
+ ctx.strokeStyle = "#111";
3097
+ ctx.lineWidth = 2;
3098
+ for (const line of scene.ziplines) {
3099
+ const [ax, ay] = view.point(line.x0, line.y0, line.z), [bx, by] = view.point(line.x1, line.y1, line.z);
3100
+ ctx.beginPath();
3101
+ ctx.moveTo(ax, ay);
3102
+ ctx.lineTo(bx, by);
3103
+ ctx.stroke();
3104
+ }
3105
+ if (highlight) drawHighlight(ctx, scene, view, highlight);
3106
+ }
3107
+ var MAP_THUMBNAIL_HEIGHT = 192;
3108
+ function renderMapThumbnail(json, height = MAP_THUMBNAIL_HEIGHT) {
3109
+ if (!json?.width || !json?.height) return null;
3110
+ const full = document.createElement("canvas");
3111
+ full.width = json.width * MAP_TILE_SIZE;
3112
+ full.height = json.height * MAP_TILE_SIZE;
3113
+ const fctx = full.getContext("2d");
3114
+ if (!fctx) return null;
3115
+ new HCMap(json).draw(fctx);
3116
+ const scale = height / full.height;
3117
+ const thumb = document.createElement("canvas");
3118
+ thumb.width = Math.max(1, Math.round(full.width * scale));
3119
+ thumb.height = height;
3120
+ const tctx = thumb.getContext("2d");
3121
+ if (!tctx) return null;
3122
+ tctx.imageSmoothingQuality = "high";
3123
+ tctx.drawImage(full, 0, 0, thumb.width, thumb.height);
3124
+ return thumb.toDataURL("image/png");
3125
+ }
3126
+ var MapBoardView = forwardRef(function MapBoardView2({ map, canvasLabel, selection, onInspect, onLeave, onDrawError, className, style, ...rest }, ref) {
3127
+ const canvasRef = useRef(null);
3128
+ const failed = useRef(onDrawError);
3129
+ failed.current = onDrawError;
3130
+ useEffect(() => {
3131
+ const canvas = canvasRef.current;
3132
+ if (!canvas) return;
3133
+ const ctx = canvas.getContext("2d");
3134
+ if (!ctx) return;
3135
+ try {
3136
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
3137
+ new HCMap(map).draw(ctx);
3138
+ } catch {
3139
+ failed.current?.();
3140
+ }
3141
+ }, [map]);
3142
+ function pointer(event) {
3143
+ const bounds = event.currentTarget.getBoundingClientRect();
3144
+ onInspect?.((event.clientX - bounds.left) / bounds.width * map.width, (event.clientY - bounds.top) / bounds.height * map.height, event.clientX, event.clientY);
3145
+ }
3146
+ return /* @__PURE__ */ jsxs(
3147
+ "div",
3148
+ {
3149
+ ref,
3150
+ className: cx("hc-ui-map-board", className),
3151
+ style: { aspectRatio: `${map.width} / ${map.height}`, ...style },
3152
+ ...rest,
3153
+ onPointerMove: (event) => {
3154
+ if (event.pointerType !== "touch") pointer(event);
3155
+ },
3156
+ onPointerDown: pointer,
3157
+ onPointerLeave: (event) => {
3158
+ if (event.pointerType !== "touch") onLeave?.();
3159
+ },
3160
+ children: [
3161
+ /* @__PURE__ */ jsx("canvas", { ref: canvasRef, width: map.width * MAP_TILE_SIZE, height: map.height * MAP_TILE_SIZE, role: "img", "aria-label": canvasLabel }),
3162
+ selection && /* @__PURE__ */ jsx("svg", { className: "hc-ui-map-board__selection", viewBox: `0 0 ${map.width} ${map.height}`, "aria-hidden": "true", children: selection.line ? /* @__PURE__ */ jsx("line", { x1: selection.line.x0, y1: selection.line.y0, x2: selection.line.x1, y2: selection.line.y1 }) : /* @__PURE__ */ jsx("rect", { x: selection.square.x, y: selection.square.y, width: "1", height: "1" }) })
3163
+ ]
3164
+ }
3165
+ );
3166
+ });
3167
+ function MapBoardPlaceholder({ label }) {
3168
+ return /* @__PURE__ */ jsx("div", { className: "hc-ui-map-board", "data-loading": "", role: "status", "aria-busy": "true", children: /* @__PURE__ */ jsx("span", { className: "hc-ui-map-board__status", children: label }) });
3169
+ }
3170
+ function MapSwatch({ fill, edge, children, lightText }) {
3171
+ return /* @__PURE__ */ jsx("span", { className: "hc-ui-map-swatch", "data-light-text": flag(lightText), style: { backgroundColor: fill, borderColor: edge }, "aria-hidden": "true", children });
3172
+ }
3173
+ function MapSquareInfo({ square }) {
3174
+ const { coordinate, terrain, elevation, environment, environmentNote, notes = [] } = square;
3175
+ return /* @__PURE__ */ jsxs("div", { className: "hc-ui-map-tooltip__square", children: [
3176
+ coordinate && /* @__PURE__ */ jsx("span", { className: "hc-ui-map-tooltip__coordinate", children: coordinate }),
3177
+ /* @__PURE__ */ jsxs("span", { className: "hc-ui-map-tooltip__row", "data-kind": "terrain", children: [
3178
+ /* @__PURE__ */ jsx(MapSwatch, { fill: terrain.fill, edge: terrain.edge }),
3179
+ terrain.label
3180
+ ] }),
3181
+ /* @__PURE__ */ jsxs("span", { className: "hc-ui-map-tooltip__row", children: [
3182
+ /* @__PURE__ */ jsx(MapSwatch, { fill: elevation.fill, edge: elevation.edge, lightText: elevation.lightText, children: elevation.level }),
3183
+ elevation.label
3184
+ ] }),
3185
+ environmentNote !== void 0 ? /* @__PURE__ */ jsx("span", { className: "hc-ui-map-tooltip__row", children: environmentNote }) : environment,
3186
+ notes.map((note, index) => /* @__PURE__ */ jsx("span", { className: "hc-ui-map-tooltip__note", children: note }, index))
3187
+ ] });
3188
+ }
3189
+ function MapTooltipView({ id, anchor, title, wallColor, squares }) {
3190
+ const ref = useRef(null);
3191
+ const [position, setPosition] = useState({ left: 0, top: 0 });
3192
+ useLayoutEffect(() => {
3193
+ const bounds = ref.current?.getBoundingClientRect();
3194
+ if (!bounds) return;
3195
+ setPosition({ left: Math.max(8, Math.min(anchor.x + 14, window.innerWidth - bounds.width - 8)), top: Math.max(8, anchor.y + 18 + bounds.height > window.innerHeight - 8 ? anchor.y - bounds.height - 14 : anchor.y + 18) });
3196
+ }, [anchor]);
3197
+ return /* @__PURE__ */ jsxs("div", { ref, id, role: "tooltip", className: "hc-ui-map-tooltip", style: position, children: [
3198
+ /* @__PURE__ */ jsxs("strong", { className: "hc-ui-map-tooltip__title", children: [
3199
+ wallColor && /* @__PURE__ */ jsx("span", { className: "hc-ui-map-swatch", "data-wall": "", style: { backgroundColor: wallColor }, "aria-hidden": "true" }),
3200
+ title
3201
+ ] }),
3202
+ /* @__PURE__ */ jsx("div", { className: "hc-ui-map-tooltip__squares", children: squares.map((square, index) => /* @__PURE__ */ jsx(MapSquareInfo, { square }, index)) })
3203
+ ] });
3204
+ }
3205
+ function MapLegendView({ items, children }) {
3206
+ return /* @__PURE__ */ jsxs("div", { className: "hc-ui-map-legend", children: [
3207
+ items.map((item) => /* @__PURE__ */ jsxs("span", { className: "hc-ui-map-legend__key", children: [
3208
+ /* @__PURE__ */ jsx(MapSwatch, { fill: item.fill, edge: item.edge }),
3209
+ item.label
3210
+ ] }, item.label)),
3211
+ children
3212
+ ] });
3213
+ }
3214
+ function mapEnvironments(types) {
3215
+ return [...new Set(types.flatMap((type) => {
3216
+ const value = type.toLowerCase().replace(/[^a-z]/g, "");
3217
+ return value === "indooroutdoor" ? ["indoor", "outdoor"] : value === "indoor" || value === "outdoor" ? [value] : [];
3218
+ }))];
3219
+ }
3220
+ function MapEnvironmentView({ types, icons, labels, className }) {
3221
+ const environments = mapEnvironments(types);
3222
+ if (!environments.length) return null;
3223
+ return /* @__PURE__ */ jsx("span", { className: cx("hc-ui-map-environment", className), children: environments.map((environment) => /* @__PURE__ */ jsxs("span", { className: "hc-ui-map-environment__item", children: [
3224
+ /* @__PURE__ */ jsx("img", { src: icons[environment], alt: "", width: "18", height: "18" }),
3225
+ /* @__PURE__ */ jsx("span", { children: labels[environment] })
3226
+ ] }, environment)) });
3227
+ }
1437
3228
  export {
3229
+ AvatarView,
1438
3230
  Button,
3231
+ COMBAT_DIAL_STATS,
1439
3232
  Checkbox,
1440
3233
  Chip,
3234
+ CombatDialView,
1441
3235
  CommandPalette,
1442
3236
  Dialog,
3237
+ EquipmentIcon,
1443
3238
  Field,
1444
3239
  FilterBar,
3240
+ HCMap,
1445
3241
  IconButton,
1446
3242
  ListRow,
3243
+ MAP_CELL_SIDES,
3244
+ MAP_DEFAULT_CAMERA,
3245
+ MAP_ELEVATION_EDGE_COLOR,
3246
+ MAP_ELEVATION_TILE_COLORS,
3247
+ MAP_INDOOR_EDGE_COLOR,
3248
+ MAP_SCENE_BASE_Z,
3249
+ MAP_SCENE_LEVEL_HEIGHT,
3250
+ MAP_TERRAIN_EDGE_COLORS,
3251
+ MAP_TERRAIN_TILE_COLORS,
3252
+ MAP_THUMBNAIL_HEIGHT,
3253
+ MAP_TILE_SIZE,
3254
+ MAP_TILT_RANGE,
3255
+ MAP_WALL_COLORS,
3256
+ MAP_ZOOM_RANGE,
3257
+ MINI_DIAL_STATS,
3258
+ MapBoardPlaceholder,
3259
+ MapBoardView,
3260
+ MapEnvironmentView,
3261
+ MapLegendView,
3262
+ MapSwatch,
3263
+ MapTooltipView,
1447
3264
  Menu,
1448
3265
  MenuItem,
1449
3266
  MenuItemCheckbox,
@@ -1452,16 +3269,52 @@ export {
1452
3269
  MenuRadioGroup,
1453
3270
  MenuSeparator,
1454
3271
  Meter,
3272
+ MiniDialView,
3273
+ PHYSICAL_CARD_BACK_TABLE_HEIGHT,
3274
+ PHYSICAL_CARD_BYSTANDER_DIAL_STEP,
3275
+ PHYSICAL_CARD_HEIGHT,
3276
+ PHYSICAL_CARD_STATS,
3277
+ PHYSICAL_CARD_WIDTH,
1455
3278
  PageHeader,
1456
3279
  PanelHeader,
3280
+ PhysicalCardFocusView,
3281
+ PhysicalCardView,
1457
3282
  Popover,
3283
+ PowerIconView,
1458
3284
  SegmentedControl,
3285
+ SetIconView,
1459
3286
  Sheet,
1460
3287
  SheetChips,
1461
3288
  SheetOptions,
1462
3289
  Skeleton,
1463
3290
  SortMenu,
3291
+ SpecialIconView,
3292
+ StatGlyphView,
3293
+ StatIcon,
1464
3294
  Surface,
1465
3295
  Table,
1466
- placePopup
3296
+ buildMapScene,
3297
+ clampMapPan,
3298
+ clampMapValue,
3299
+ drawMapScene,
3300
+ inspectMap,
3301
+ makeMapProjection,
3302
+ mapElevationLevel,
3303
+ mapEnvironments,
3304
+ mapFootprintOutline,
3305
+ mapMaxLevel,
3306
+ mapSceneCell,
3307
+ mapSquare,
3308
+ mapSquareCoordinate,
3309
+ nextPhysicalCardDistribution,
3310
+ physicalCardBackCount,
3311
+ physicalCardDevicePosition,
3312
+ physicalCardTiltAt,
3313
+ physicalCardTiltStyles,
3314
+ pickMapScene,
3315
+ placePopup,
3316
+ renderMapThumbnail,
3317
+ requestPhysicalCardMotionAccess,
3318
+ useImageChain,
3319
+ zoomMapCamera
1467
3320
  };