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