// ドリームテーゼ - 盤面・キャラクター画面コンポーネント

// CSS アニメーション注入
(function () {
  if (document.getElementById('dt-board-style')) return;
  var s = document.createElement('style');
  s.id = 'dt-board-style';
  s.textContent = [
    // スポットライト用アニメーション可能なカスタムプロパティ定義
    '@property --dt-spot-size { syntax: "<percentage>"; initial-value: 1.5%; inherits: false; }',
    '@property --dt-outer-stop { syntax: "<percentage>"; initial-value: 5.5%; inherits: false; }',
    '@property --dt-spot-x { syntax: "<percentage>"; initial-value: 50%; inherits: false; }',
    '@property --dt-spot-y { syntax: "<percentage>"; initial-value: 50%; inherits: false; }',
    '@property --dt-spot-alpha { syntax: "<number>"; initial-value: 1; inherits: false; }',
    '@keyframes tokenGlowPulse {',
    '  0%,100%{ filter: drop-shadow(0 0 3px rgba(124,58,237,1)) drop-shadow(0 0 6px rgba(124,58,237,0.8)); }',
    '  50%   { filter: drop-shadow(0 0 8px rgba(167,139,250,1)) drop-shadow(0 0 16px rgba(124,58,237,0.8)); }',
    '}',
    '@keyframes costGlowPulse {',
    '  0%,100%{ filter: drop-shadow(0 0 3px rgba(80,20,180,0.8)) drop-shadow(0 0 6px rgba(80,20,180,0.6)); }',
    '  50%   { filter: drop-shadow(0 0 8px rgba(120,60,240,1)) drop-shadow(0 0 16px rgba(100,40,210,0.9)); }',
    '}',
    '@keyframes dimFadeIn { from { opacity: 0; } to { opacity: 1; } }',
    '@keyframes opRueReveal { from { clip-path: inset(0 0 100% 0); } to { clip-path: inset(0 0 0% 0); } }',
    '@keyframes opRueWhispIn { from { opacity: 0; transform: scale(0.88) translateY(24px); } to { opacity: 1; transform: scale(1) translateY(0); } }',
    '@keyframes opRueFadeIn { from { opacity: 0; } to { opacity: 1; } }',
  ].join('');
  document.head.appendChild(s);
}());

/* ==============================================================
   カード画像マッピング
   ============================================================== */

var CARD_IMAGES = {
  '威風':             'assets/card_威風.webp',
  '判断':             'assets/card_判断.webp',
  '宣誓':             'assets/card_宣誓.webp',
  '護法斬り':         'assets/card_護法斬り.webp',
  '闇纏い':           'assets/card_闇纏い.webp',
  '大上段':           'assets/card_大上段.webp',
  '踏み込み':         'assets/card_踏み込み.webp',
  'ステラ・ルプトゥラ': 'assets/card_ステラルプトゥラ.webp',
  '二連撃':           'assets/card_二連撃.webp',
  '自我':             'assets/card_自我.webp',
  'moment_a':        'assets/card_moment_a.webp',
  'moment_b':        'assets/card_moment_b.webp',
};

/* ==============================================================
   トークン配置定義
   ============================================================== */

// TOKEN_W/TOKEN_H はボードの幅/高さに対する%。
// トークン画像は正方形なので、ピクセル上で正方形に見えるよう
// ボードの縦横比 (2167/1588 ≈ 1.364) に合わせて W を調整している。
var TOKEN_W = 4.35;  // ≈ 3.2% × (board_h/board_w) → ピクセル上でほぼ正方形
var TOKEN_H = 3.2;   // ボード高さの 3.2%

// ダイヤモンド配置: [x%, y%, flipped]
//  ^       row0: col1
// ^v^      row1: col0,1,2
// v^v      row2: col0,1,2
//  v       row3: col1
// (row+col) が偶数 → v (flipped=true), 奇数 → ^ (flipped=false)
function genAwareGrid(sx, sy, gx, gy) {
  var layout = [[1], [0, 1, 2], [0, 1, 2], [1]];
  var pos = [];
  for (var row = 0; row < layout.length; row++) {
    for (var c = 0; c < layout[row].length; c++) {
      var col = layout[row][c];
      pos.push([sx + col * gx, sy + row * gy, (row + col) % 2 === 0]);
    }
  }
  return pos;
}

// opponent = 画面上、player = 画面下（ミラー＋向き反転）
var OPPONENT_POS = genAwareGrid(1.5, 1.0, 2.95, 3.5);
var PLAYER_POS   = OPPONENT_POS.map(function (p) {
  return [100 - p[0] - TOKEN_W, 100 - p[1] - TOKEN_H, !p[2]];
});

// 上端中央(opponent) / 下端中央(player) の全黒トークン群
//  v        row0: col1
//  vvv      row1: col1,2,3
// vvvv      row2: col0,1,2,3
// gx=2.95 で4列幅≈13.05% → 中央揃え sx≈43.5
var ALTER_OPPONENT_POS = (function () {
  var layout = [[0, 1, 2, 3], [0, 1, 2], [2]];
  var gx = 5.6, gy = 3.5, sx = 41.2, sy = 1.1, pos = [];
  for (var row = 0; row < layout.length; row++) {
    for (var c = 0; c < layout[row].length; c++) {
      var col = layout[row][c];
      pos.push([sx + col * gx, sy + row * gy]);
    }
  }
  return pos;
}());
var ALTER_PLAYER_POS = ALTER_OPPONENT_POS.map(function (p) {
  return [100 - p[0] - TOKEN_W, 100 - p[1] - TOKEN_H];
});

var SLOTS = {
  opponent: [
    { left: 70.5, top: 28, w: 12.5, h: 16.8 },
    { left: 47.5, top: 28, w: 12.5, h: 16.8 },
    { left: 24.5, top: 29.3, w: 12.5, h: 16.8 },
  ],
  player: [
    { left: 59, top: 55, w: 12.5, h: 16.8 },
    { left: 36, top: 53.7, w: 12.5, h: 16.8 },
    { left: 13, top: 55, w: 12.5, h: 16.8 },
  ],
};

// カードのアスペクト比（幅/高さ）と、ボードの高さ/幅比。
// これを使ってカードの実際の幅（ボード幅%）を計算し、MagicDotsをカード中央に配置する。
// カードは height: sl.h% (ボード高さ基準) でレンダリングされるため、
// 実際の幅(ボード幅%) = CARD_ASPECT × sl.h × BOARD_HW
var CARD_ASPECT = 5 / 7;           // カード幅/高さ比（標準TCG 5:7）
var BOARD_HW = 2167 / 1588;        // ボード高さ/幅比（aspectRatio: 1588/2167 より）

// スロットのカード中心X座標（ボード幅%）を返す
function cardCenterX(sl) {
  return sl.left + CARD_ASPECT * sl.h * BOARD_HW / 2;
}

// top のみ定義。X位置は SLOTS[side][i] から cardCenterX() で自動計算。
// opponent guardian は player の対応スロット上に配置
var GUARDIAN_SLOTS = {
  player:   [
    { top: 50 },
    { top: 50 },
    { top: 50 },
  ],
  opponent: [
    { top: 47 },
    { top: 47 },
    { top: 47 },
  ],
};

// reinforcement: player は自分スロット下部、opponent は自分スロット上部
var REINFORCEMENT_SLOTS = {
  player:   [
    { top: 72.5 },
    { top: 72.5 },
    { top: 72.5 },
  ],
  opponent: [
    { top: 23 },
    { top: 23 },
    { top: 23 },
  ],
};

// 夢遊魔力トークン階段配置パラメータ
//    v^v
//   v^v
//  v^v
// v^v
var DREAM_GX     = TOKEN_W - 1.2;  // 水平ステップ（board幅%）
var DREAM_GY     = TOKEN_H + 0;  // 垂直ステップ（board高さ%）
var DREAM_BASE_X = 4;            // 最左列 left 基準（opponent）
var DREAM_BASE_Y = 11;            // 最上行 top  基準（opponent）

// i 番目（0始まり）のトークン位置と向きを返す
// side='opponent': 左上エリア / side='player': 右下エリア（点対称）
function dreamPos(i, side) {
  var r = Math.floor(i / 3);   // 行（0=底辺, 3=頂辺）
  var c = i % 3;               // 列（0=左, 1=中, 2=右）
  var left, top, rot;
  if (side !== 'player') {
    left = DREAM_BASE_X + (r + c) * DREAM_GX;
    top  = DREAM_BASE_Y + (3 - r) * DREAM_GY;
    rot  = c % 2 === 0 ? 'rotate(180deg)' : undefined;  // v^v
  } else {
    left = 100 - (DREAM_BASE_X + (r + c) * DREAM_GX) - TOKEN_W;
    top  = 100 - (DREAM_BASE_Y + (3 - r) * DREAM_GY)  - TOKEN_H;
    rot  = c % 2 === 0 ? undefined : 'rotate(180deg)';  // 点対称で反転
  }
  return { left: left, top: top, rot: rot };
}

/* ==============================================================
   ヘルパー
   ============================================================== */

function computeGlowFlags(tokens, color, count) {
  var flags = [];
  var matched = 0;
  for (var i = 0; i < tokens.length; i++) {
    if (color && tokens[i] === color && matched < count) {
      flags.push(true); matched++;
    } else {
      flags.push(false);
    }
  }
  return flags;
}

function useMeasuredSize() {
  var elementState = React.useState(null);
  var element = elementState[0];
  var setElement = elementState[1];
  var sizeState = React.useState({ width: 0, height: 0 });
  var size = sizeState[0];
  var setSize = sizeState[1];
  var ref = React.useCallback(function (node) {
    setElement(node);
  }, []);

  React.useLayoutEffect(function () {
    var el = element;
    if (!el) return;

    var update = function () {
      var rect = el.getBoundingClientRect();
      setSize(function (prev) {
        if (Math.abs(prev.width - rect.width) < 0.5 && Math.abs(prev.height - rect.height) < 0.5) {
          return prev;
        }
        return { width: rect.width, height: rect.height };
      });
    };

    update();

    if (window.ResizeObserver) {
      var ro = new ResizeObserver(update);
      ro.observe(el);
      return function () { ro.disconnect(); };
    }

    window.addEventListener('resize', update);
    return function () { window.removeEventListener('resize', update); };
  }, [element]);

  return { ref: ref, size: size };
}

function getBoardCameraTarget(camera, containerSize) {
  var maxNormalHeight = 655;
  var maxNormalWidth = maxNormalHeight * 1588 / 2167;
  var maxTokenViewportWidth = 425;
  var maxTokenBaseWidth = Math.min(maxTokenViewportWidth * 0.85, maxNormalWidth);
  var boardPerspective = 900;
  var boardRotateX = 28 * Math.PI / 180;
  var baseWidthCss = 'min(85cqw, calc(100cqh * 1588 / 2167), calc(655px * 1588 / 2167))';
  var baseWidth = 0;
  var visualWidth = 0;
  var shiftX = 0;
  var shiftY = 0;

  if (containerSize.width > 0 && containerSize.height > 0) {
    baseWidth = Math.min(containerSize.width * 0.85, containerSize.height * 1588 / 2167, maxNormalWidth);
    visualWidth = baseWidth;

    if (camera === 'tokens') {
      visualWidth = Math.min(baseWidth, maxTokenBaseWidth) * 2.3;
      var targetHeight = visualWidth * 2167 / 1588;
      var nearEdgeZ = targetHeight / 2 * Math.sin(boardRotateX);
      var perspectiveScale = boardPerspective / Math.max(1, boardPerspective - nearEdgeZ);
      shiftX = (containerSize.width / perspectiveScale - visualWidth) / (2 * visualWidth) * 100;
      shiftY = containerSize.height / 2 - targetHeight / 2 - 22;
    } else if (camera === 'center') {
      visualWidth = baseWidth * 1.5;
    }

    return {
      width: Math.max(1, visualWidth) + 'px',
      visualWidth: Math.max(1, visualWidth),
      shiftX: shiftX,
      shiftY: shiftY,
      hasSize: true,
    };
  }

  if (camera === 'tokens') {
    var fallbackTokenWidth = maxTokenViewportWidth * 0.85 * 2.3;
    var fallbackTokenHeight = fallbackTokenWidth * 2167 / 1588;
    var fallbackNearEdgeZ = fallbackTokenHeight / 2 * Math.sin(boardRotateX);
    var fallbackPerspectiveScale = boardPerspective / Math.max(1, boardPerspective - fallbackNearEdgeZ);
    shiftX = (maxTokenViewportWidth / fallbackPerspectiveScale - fallbackTokenWidth) / (2 * fallbackTokenWidth) * 100;
  }
  return {
    width: baseWidthCss,
    visualWidth: camera === 'tokens' ? 2.3 : (camera === 'center' ? 1.5 : 1),
    shiftX: shiftX,
    shiftY: shiftY,
    hasSize: false,
  };
}

function getBoardCameraTransform(visualTarget, layoutTarget) {
  var scale = visualTarget.hasSize && layoutTarget.visualWidth > 0
    ? visualTarget.visualWidth / layoutTarget.visualWidth
    : visualTarget.visualWidth;
  return {
    transform: 'translate3d(0,' + visualTarget.shiftY.toFixed(2) + 'px,0) rotateX(28deg) scale(' + scale.toFixed(4) + ') translateX(' + visualTarget.shiftX + '%)',
    rasterScale: Math.max(1, scale),
  };
}

function useBoardCameraLayout(camera, containerSize) {
  var prevCameraRef = React.useRef(camera);
  var rafRef = React.useRef([]);
  var animState = React.useState({ from: camera, to: camera, phase: 'idle' });
  var anim = animState[0];
  var setAnim = animState[1];

  React.useLayoutEffect(function () {
    rafRef.current.forEach(function (id) { cancelAnimationFrame(id); });
    rafRef.current = [];

    if (containerSize.width <= 0 || containerSize.height <= 0) {
      prevCameraRef.current = camera;
      setAnim({ from: camera, to: camera, phase: 'idle' });
      return;
    }

    var prevCamera = prevCameraRef.current;
    if (prevCamera === camera) return;

    prevCameraRef.current = camera;
    setAnim({ from: prevCamera, to: camera, phase: 'start' });

    var raf1 = requestAnimationFrame(function () {
      var raf2 = requestAnimationFrame(function () {
        setAnim({ from: prevCamera, to: camera, phase: 'run' });
      });
      rafRef.current.push(raf2);
    });
    rafRef.current.push(raf1);

    return function () {
      rafRef.current.forEach(function (id) { cancelAnimationFrame(id); });
      rafRef.current = [];
    };
  }, [camera, containerSize.width, containerSize.height]);

  var layoutCamera = anim.phase === 'idle' ? camera : anim.to;
  var visualCamera = anim.phase === 'start' ? anim.from : layoutCamera;
  var layoutTarget = getBoardCameraTarget(layoutCamera, containerSize);
  var visualTarget = getBoardCameraTarget(visualCamera, containerSize);
  var tx = getBoardCameraTransform(visualTarget, layoutTarget);

  return {
    width: layoutTarget.width,
    transform: tx.transform,
    rasterScale: tx.rasterScale,
    transition: anim.phase === 'start' ? 'none' : 'transform 0.8s cubic-bezier(.22,.61,.36,1)',
  };
}

/* ==============================================================
   キャラクター画面（シーン 1〜6）
   ============================================================== */

function CharacterScreen({ character }) {
  var visible     = character.visible;
  var focus       = character.focus;
  var imageState  = character.imageState !== undefined ? character.imageState : 'normal';
  var scale    = character.scale    !== undefined ? character.scale    : 1;
  var shiftX   = character.shiftX   !== undefined ? character.shiftX   : 0;
  var shiftY   = character.shiftY   !== undefined ? character.shiftY   : 0;
  var spotX    = character.spotX    !== undefined ? character.spotX    : 50;
  var spotY    = character.spotY    !== undefined ? character.spotY    : 50;
  var spotSize     = character.spotSize     !== undefined ? character.spotSize     : 28;
  var spotSoftness = character.spotSoftness !== undefined ? character.spotSoftness : 8;

  var wrapStyle = {
    flex: 1, minHeight: 0,
    position: 'relative',
    overflow: 'hidden',
    background: '#05050c',
    display: 'flex', alignItems: 'center', justifyContent: 'center',
  };

  if (!visible) {
    var darkStyle = Object.assign({}, wrapStyle, {
      background: '#000',
    });
    return React.createElement('div', { style: darkStyle });
  }

  var imgStyle = {
    position: 'absolute',
    width: '100%', height: '100%',
    objectFit: 'cover',
    objectPosition: '50% 0%',
    transform: 'scale(' + scale + ') translateX(' + shiftX + '%) translateY(' + shiftY + '%)',
    opacity: 1,
  };

  // spotSoftness が負の場合はハードエッジとして扱う
  var innerStop = Math.max(0, spotSize);
  var outerStop = Math.max(innerStop + 0.1, spotSize + spotSoftness);

  // 'full' モード時のターゲット値（スポットが広がって全体が見える）
  var isMouth   = focus === 'mouth';
  var tSpotX    = isMouth ? spotX     : 50;
  var tSpotY    = isMouth ? spotY     : 60;
  var tInner    = isMouth ? innerStop : 55;
  var tOuter    = isMouth ? outerStop : 100;
  var tAlpha    = isMouth ? 1         : 0.75;

  // @property で定義したカスタムプロパティを使うことで gradient がアニメーション可能になる
  var overlayStyle = {
    position: 'absolute', inset: 0,
    pointerEvents: 'none',
    transition: '--dt-spot-size 0.8s ease, --dt-outer-stop 0.8s ease, --dt-spot-x 0.6s ease, --dt-spot-y 0.6s ease, --dt-spot-alpha 0.8s ease',
    '--dt-spot-size':  tInner + '%',
    '--dt-outer-stop': tOuter + '%',
    '--dt-spot-x':     tSpotX + '%',
    '--dt-spot-y':     tSpotY + '%',
    '--dt-spot-alpha': tAlpha,
    background: 'radial-gradient(circle at var(--dt-spot-x) var(--dt-spot-y), transparent var(--dt-spot-size), rgba(5,5,12,var(--dt-spot-alpha)) var(--dt-outer-stop))',
  };

  return React.createElement('div', { style: wrapStyle },
    React.createElement('div', {
      key: 'rue-' + imageState,
      style: { position: 'absolute', inset: 0, animation: imageState === 'whisp' ? 'opRueWhispIn 0.6s cubic-bezier(0.2,0,0.2,1)' : 'opRueFadeIn 0.6s ease' },
    },
      React.createElement('img', { src: 'assets/op_rue_' + imageState + '.webp', alt: '', style: imgStyle }),
    ),
    React.createElement('div', { style: overlayStyle }),
  );
}

/* ==============================================================
   テーゼトークン
   ============================================================== */

function Token({ face, glowing, renderScale }) {
  var dispState      = React.useState(face);
  var displayFace    = dispState[0];
  var setDisplayFace = dispState[1];
  var flipState      = React.useState(false);
  var flipping       = flipState[0];
  var setFlipping    = flipState[1];
  var isFirst        = React.useRef(true);

  React.useEffect(function() {
    if (isFirst.current) { isFirst.current = false; return; }
    setFlipping(true);
    var mid = setTimeout(function() { setDisplayFace(face); }, 270);
    var end = setTimeout(function() { setFlipping(false); }, 600);
    return function() { clearTimeout(mid); clearTimeout(end); };
  }, [face]);

  var src = displayFace === 'white'
    ? 'uploads/token_white.png'
    : 'uploads/token_black.png';
  var rasterScale = Math.max(1, renderScale || 1);
  var wrapStyle = {
    width: '100%',
    height: '100%',
    display: 'block',
    overflow: 'visible',
    transformOrigin: '50% 50%',
  };
  var s = {
    width: (100 * rasterScale) + '%',
    height: (100 * rasterScale) + '%',
    objectFit: 'contain', display: 'block',
    transform: 'scale(' + (1 / rasterScale) + ')',
    transformOrigin: '0 0',
    filter: 'drop-shadow(0 1px 3px rgba(0,0,0,0.7))',
  };
  if (glowing)       s.animation = 'tokenGlowPulse 1.5s ease-in-out infinite';
  else if (flipping) wrapStyle.animation = 'tokenFlip 0.6s ease-in-out';
  return React.createElement('span', { style: wrapStyle },
    React.createElement('img', { src: src, style: s })
  );
}

/* ==============================================================
   移動中トークン（魔力へのフライアニメーション）
   ============================================================== */

function MovingToken(props) {
  var st = React.useState(false);
  var moved = st[0];
  var setMoved = st[1];

  var ms = React.useState(false);
  var moving = ms[0];
  var setMoving = ms[1];

  React.useEffect(function () {
    var id1 = requestAnimationFrame(function () {
      requestAnimationFrame(function () { setMoved(true); setMoving(true); });
    });
    return function () { cancelAnimationFrame(id1); };
  }, []);

  React.useEffect(function () {
    if (!moving) return;
    var tid = setTimeout(function () { setMoving(false); }, 1000);
    return function () { clearTimeout(tid); };
  }, [moving]);

  var left = (moved ? props.endLeft : props.startLeft) + '%';
  var top  = (moved ? props.endTop  : props.startTop)  + '%';
  var rasterScale = Math.max(1, props.renderScale || 1);
  return React.createElement('div', {
    style: {
      position: 'absolute',
      left: left, top: top,
      width: TOKEN_W + '%', height: TOKEN_H + '%',
      zIndex: 15, pointerEvents: 'none',
      transition: 'left 1.0s cubic-bezier(0.4,0,0.2,1), top 1.0s cubic-bezier(0.4,0,0.2,1)',
      transform: props.rotated ? 'rotate(180deg)' : undefined,
    },
  },
    React.createElement('span', {
      style: {
        width: '100%', height: '100%',
        display: 'block',
        overflow: 'visible',
        animation: moving ? 'tokenFlip 0.3s ease-in-out infinite' : 'none',
        transformOrigin: '50% 50%',
      },
    },
      React.createElement('img', {
        src: 'uploads/token_black.png',
        style: {
          width: (100 * rasterScale) + '%',
          height: (100 * rasterScale) + '%',
          objectFit: 'contain',
          display: 'block',
          transform: 'scale(' + (1 / rasterScale) + ')',
          transformOrigin: '0 0',
        filter: 'drop-shadow(0 0 6px rgba(167,139,250,0.9)) drop-shadow(0 0 12px rgba(124,58,237,0.7))',
        },
      })
    ),
  );
}

/* ==============================================================
   ボード上のカード
   ============================================================== */

function BoardCard({ name }) {
  var imgSrc = CARD_IMAGES[name] || null;

  var s = {
    height: '100%',
    width: 'auto',
    display: 'block',
    borderRadius: 3,
    border: '1px solid rgba(130,120,180,0.3)',
    flexShrink: 0,
  };

  if (imgSrc) {
    return React.createElement('img', { src: imgSrc, alt: name, style: s });
  }

  // 画像なし → 標準的なカード縦横比（5:7）でテキスト表示
  return React.createElement('div', {
    style: Object.assign({}, s, {
      aspectRatio: '5 / 7',
      background: 'linear-gradient(180deg,rgba(22,22,40,.92),rgba(14,14,28,.95))',
      display: 'flex', alignItems: 'center', justifyContent: 'center',
    })
  },
    React.createElement('span', {
      style: {
        color: '#d0d0e0', fontSize: 9, fontWeight: 500,
        textAlign: 'center', lineHeight: 1.2,
        fontFamily: "'Shippori Mincho',serif",
        padding: '0 2px',
      }
    }, name),
  );
}


/* ==============================================================
   フローティングカード（選択肢演出）
   ============================================================== */

// cardWidth: CSS width 値（'30cqw' などの文字列）。高さは画像のアスペクト比に自動追従。
function FloatCard({ name, glowing, cardWidth, costImg }) {
  var imgSrc = CARD_IMAGES[name] || null;
  var w = cardWidth || '30cqw';

  var s = {
    width: w,
    height: 'auto',
    display: 'block',
    borderRadius: 5,
    border: '1px solid rgba(140,130,180,0.3)',
    animation: 'cardSlideIn 0.5s ease-out',
    transition: 'all 0.4s ease',
    flexShrink: 0,
  };

  if (imgSrc) {
    var shadowFilter = 'drop-shadow(0 0 8px rgba(124,58,237,.7)) drop-shadow(0 0 18px rgba(124,58,237,.4))';
    var rawImageStyle = {
      width: '100%',
      height: 'auto',
      display: 'block',
    };
    return React.createElement('span', {
      style: {
        position: 'relative',
        display: 'block',
        width: w,
        height: 'auto',
        lineHeight: 0,
        animation: 'cardSlideIn 0.5s ease-out',
        transition: 'all 0.4s ease',
        flexShrink: 0,
      },
    },
      glowing && React.createElement('img', {
        src: imgSrc,
        alt: '',
        'aria-hidden': true,
        style: {
          position: 'absolute',
          top: 0,
          left: 0,
          width: rawImageStyle.width,
          height: rawImageStyle.height,
          display: rawImageStyle.display,
          filter: shadowFilter,
          pointerEvents: 'none',
        },
      }),
      React.createElement('img', {
        src: imgSrc,
        alt: name,
        style: Object.assign({}, rawImageStyle, {
          position: 'relative',
          zIndex: 1,
        }),
      }),
      costImg && React.createElement('img', {
        src: costImg,
        alt: '',
        'aria-hidden': true,
        style: {
          position: 'absolute',
          top: 0,
          left: 0,
          width: '100%',
          height: 'auto',
          display: 'block',
          zIndex: 2,
          pointerEvents: 'none',
          animation: 'costGlowPulse 1.5s ease-in-out infinite',
        },
      }),
    );
  }

  if (glowing) {
    s.borderColor = 'rgba(124,58,237,.6)';
  }

  // 画像なし → 5:7 縦横比でテキスト表示
  return React.createElement('div', {
    style: Object.assign({}, s, {
      aspectRatio: '5 / 7',
      background: 'linear-gradient(180deg,#1a1a2c,#121220)',
      display: 'flex', alignItems: 'center', justifyContent: 'center',
      padding: 4,
      boxShadow: glowing ? '0 0 12px 4px rgba(124,58,237,.5)' : undefined,
    })
  },
    React.createElement('span', {
      style: {
        color: '#d0d0e0', fontSize: 11, fontWeight: 500,
        textAlign: 'center', lineHeight: 1.3,
        fontFamily: "'Shippori Mincho',serif",
      }
    }, name),
  );
}

function FloatingCardOverlay({ card, dimmed, focusCost }) {
  if (!card) return null;
  // tilt: floatCards 由来（右10°傾き・大きめ表示）
  // flipped: floatCard 由来（葛藤で180°回転）
  var isTilt   = !!card.tilt;
  var flipped  = card.flipped;
  var baseRot  = isTilt ? 10 : 0;
  var rotation = flipped ? baseRot + 180 : baseRot;
  var wrap = {
    position: 'absolute', top: 0, left: 0, right: 0, bottom: 0,
    zIndex: 20, pointerEvents: 'none',
    filter: dimmed ? 'brightness(0.2)' : 'none',
    transition: 'filter 0.5s ease',
  };
  var cardWrap = isTilt ? {
    position: 'absolute', top: '50%', left: '43%',
    transform: 'translate(-50%, -50%)',
  } : {
    position: 'absolute', top: '8%', left: '3%',
  };
  return React.createElement('div', { style: wrap },
    React.createElement('div', { style: cardWrap },
      React.createElement('div', {
        style: { animation: 'floatIn 0.6s ease-out' }
      },
        React.createElement('div', {
          style: {
            transform: 'rotate(' + rotation + 'deg)',
            transition: 'transform 0.8s ease-in-out',
          }
        },
          React.createElement(FloatCard, {
            name: card.name,
            cardWidth: isTilt ? '65cqw' : '52cqw',
            glowing: true,
            costImg: focusCost ? ('assets/card_' + card.name + '_cost.webp') : undefined,
          }),
        ),
      ),
    ),
  );
}

/* ==============================================================
   メインボードコンポーネント
   ============================================================== */

function Board({ state }) {
  var measured = useMeasuredSize();
  var boardWrapRef = measured.ref;
  var boardWrapSize = measured.size;
  var screen       = state.screen;
  var character    = state.character;
  var boardVisible = state.boardVisible;
  var camera       = state.camera;
  var whiteOut     = state.whiteOut;
  var opponent     = state.opponent;
  var player       = state.player;
  var floatingCard = state.floatingCard;
  var glows        = state.glows;
  var resolvedCards= state.resolvedCards;
  var fadeOut      = state.fadeOut;
  var tokenMotion  = state.tokenMotion;
  var dreamTokens  = state.dreamTokens  || { player: [], opponent: [] };
  var dreamMotion  = state.dreamMotion  || null;
  var momentCard   = state.momentCard   || null;
  var offering     = state.offering     || false;
  var boardImage   = state.boardImage   || 'uploads/board_default.jpg';
  var cameraLayout = useBoardCameraLayout(camera, boardWrapSize);
  var tokenRenderScale = cameraLayout.rasterScale;

  // ---- キャラクター画面（シーン 1〜6）----
  if (screen === 'dark' || screen === 'character') {
    return React.createElement(CharacterScreen, { character: character });
  }

  // ---- ボード画面（シーン 7〜）----
  if (!boardVisible) {
    return React.createElement('div', {
      style: { flex: 1, minHeight: 0, background: '#08080f' }
    });
  }

  var glowW = 0, glowB = 0, glowP = 0, glowPlayerTokenArea = false, glowFocusCardCost = false;
  if (glows) {
    (glows.targets || []).forEach(function (t) {
      if (t.indexOf('whiteTokens:')  === 0) glowW = parseInt(t.split(':')[1]);
      if (t.indexOf('blackTokens:')  === 0) glowB = parseInt(t.split(':')[1]);
      if (t.indexOf('playerTokens:') === 0) glowP = parseInt(t.split(':')[1]);
      if (t === 'playerTokenArea')           glowPlayerTokenArea = true;
      if (t === 'focusCardcost1' || t === 'focusCardcost2') glowFocusCardCost = true;
    });
  }

  var kids = [];

  if (boardImage !== 'uploads/board_default.jpg') {
    kids.push(React.createElement('div', {
      key: 'board-overlay',
      style: {
        position: 'absolute', inset: 0, zIndex: 0,
        backgroundImage: 'url(' + boardImage + ')',
        backgroundSize: '100% 100%',
        backgroundRepeat: 'no-repeat',
        borderRadius: 2,
        animation: 'boardImageSwap 0.4s ease forwards',
      },
    }));
  }

  // 自分トークン
  if (player.tokens.length > 0) {
    var pFlags;
    if (tokenMotion && tokenMotion.side === 'player') {
      pFlags = player.tokens.map(function (_, i) { return tokenMotion.indices.indexOf(i) >= 0; });
    } else if (glowP > 0) {
      pFlags = player.tokens.map(function (_, i) { return i < glowP; });
    } else {
      var pColor = glowW > 0 ? 'white' : (glowB > 0 ? 'black' : null);
      pFlags = computeGlowFlags(player.tokens, pColor, glowW || glowB);
    }
    player.tokens.forEach(function (face, i) {
      if (!face) return; // null = 魔力に送られたトークン
      var pos = PLAYER_POS[i]; if (!pos) return;
      kids.push(React.createElement('div', { key: 'pt' + i, style: {
        position: 'absolute',
        left: pos[0] + '%', top: pos[1] + '%',
        width: TOKEN_W + '%', height: TOKEN_H + '%',
        transform: pos[2] ? 'rotate(180deg)' : undefined,
        zIndex: glowPlayerTokenArea ? 7 : 3,
      }}, React.createElement(Token, { face: face, glowing: pFlags[i], renderScale: tokenRenderScale })));
    });
  }

  // 相手トークン
  if (opponent.tokens.length > 0) {
    var oFlags;
    if (tokenMotion && tokenMotion.side === 'opponent') {
      oFlags = opponent.tokens.map(function (_, i) { return tokenMotion.indices.indexOf(i) >= 0; });
    } else {
      var oColor = glowB > 0 ? 'black' : null;
      oFlags = computeGlowFlags(opponent.tokens, oColor, glowB);
    }
    opponent.tokens.forEach(function (face, i) {
      if (!face) return; // null = 魔力に送られたトークン
      var pos = OPPONENT_POS[i]; if (!pos) return;
      kids.push(React.createElement('div', { key: 'ot' + i, style: {
        position: 'absolute',
        left: pos[0] + '%', top: pos[1] + '%',
        width: TOKEN_W + '%', height: TOKEN_H + '%',
        transform: pos[2] ? 'rotate(180deg)' : undefined,
        zIndex: 3, transition: 'all 0.3s ease',
      }}, React.createElement(Token, { face: face, glowing: oFlags[i], renderScale: tokenRenderScale })));
    });
  }

  // 中央クラスター（initTokens 後に表示、常に黒・全て rotate(180deg)）
  if (player.tokens.length > 0) {
    ALTER_OPPONENT_POS.forEach(function (pos, i) {
      kids.push(React.createElement('div', { key: 'ao' + i, style: {
        position: 'absolute',
        left: pos[0] + '%', top: pos[1] + '%',
        width: TOKEN_W + '%', height: TOKEN_H + '%',
        zIndex: 3,
      }}, React.createElement(Token, { face: 'black', glowing: false, renderScale: tokenRenderScale })));
    });
    ALTER_PLAYER_POS.forEach(function (pos, i) {
      var p = pos;
      if (offering) {
        if (i === 0) p = [46.4, 82.9];
        if (i === 4) p = [51.5, 82.9];
      }
      kids.push(React.createElement('div', { key: 'ap' + i, style: {
        position: 'absolute',
        left: p[0] + '%', top: p[1] + '%',
        width: TOKEN_W + '%', height: TOKEN_H + '%',
        transform: 'rotate(180deg)', zIndex: 3,
        transition: 'left 0.6s ease, top 0.6s ease',
      }}, React.createElement(Token, { face: 'black', glowing: false, renderScale: tokenRenderScale })));
    });

    // cardback（offering 後: rotate(-90deg) で alter 右に配置）
    if (offering) {
      kids.push(React.createElement('div', {
        key: 'offeringCard',
        style: {
          position: 'absolute',
          left: '57%', top: '84.5%', height: '16.8%',
          zIndex: 3,
          animation: 'offeringCardSlideIn 0.55s cubic-bezier(0.15,0,0,1) forwards',
        }
      },
        React.createElement('img', {
          src: 'assets/cardback.webp', alt: '',
          style: { height: '100%', width: 'auto', display: 'block', borderRadius: 3, border: '1px solid rgba(130,120,180,0.3)' },
        })
      ));
    }
  }

  // 相手カード
  var CARD_ANIM = 'cardArriveFlash 0.85s ease-out 0.35s both';
  opponent.cards.forEach(function (card, i) {
    var sl = SLOTS.opponent[i]; if (!sl) return;
    var res = resolvedCards && (resolvedCards['opponent:' + card.name] || resolvedCards[card.name]);
    var oAnim = res
      ? (res === 'non-attack' ? 'resolveCardNonAttack 0.6s ease' : ('resolveCardOpponent' + (res === 'hit' ? 'Hit' : 'Blocked') + ' 0.7s forwards'))
      : 'cardSlideInOpponent 0.55s cubic-bezier(0.15,0,0,1), ' + CARD_ANIM;
    kids.push(React.createElement('div', { key: 'o' + i, style: {
      position: 'absolute',
      left: sl.left + '%', top: sl.top + '%',
      height: sl.h + '%',
      animation: oAnim,
      zIndex: 2,
    }},
      React.createElement('div', { style: {
        height: '100%',
        transform: card.flipped ? 'none' : 'rotate(180deg)',
      }},
        React.createElement(BoardCard, { name: card.name }),
      ),
    ));
  });

  // 自分カード
  player.cards.forEach(function (card, i) {
    var sl = SLOTS.player[i]; if (!sl) return;
    var res = resolvedCards && (resolvedCards['player:' + card.name] || resolvedCards[card.name]);
    var pAnim = res
      ? (res === 'non-attack' ? 'resolveCardNonAttack 0.6s ease' : ('resolveCardPlayer' + (res === 'hit' ? 'Hit' : 'Blocked') + ' 0.7s forwards'))
      : 'cardSlideInPlayer 0.55s cubic-bezier(0.15,0,0,1), ' + CARD_ANIM;
    kids.push(React.createElement('div', { key: 'p' + i, style: {
      position: 'absolute',
      left: sl.left + '%', top: sl.top + '%',
      height: sl.h + '%',
      animation: pAnim,
      zIndex: 2,
    }},
      React.createElement('div', { style: {
        height: '100%',
        transform: card.flipped ? 'rotate(180deg)' : undefined,
      }},
        React.createElement(BoardCard, { name: card.name }),
      ),
    ));
  });

  // モーメントカード（相手スロット左・3D フリップ）
  if (momentCard) {
    var mFaceUp = momentCard.faceUp !== false;
    var mCardStyle = { height: '100%', width: 'auto', display: 'block', borderRadius: 3, border: '1px solid rgba(130,120,180,0.3)' };
    var mFallStyle = { height: '100%', aspectRatio: '5 / 7', background: 'linear-gradient(180deg,rgba(22,22,40,.92),rgba(14,14,28,.95))', display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: 3, border: '1px solid rgba(130,120,180,0.3)' };
    var mLblStyle  = { color: '#d0d0e0', fontSize: 9, fontWeight: 500, textAlign: 'center', lineHeight: 1.2, fontFamily: "'Shippori Mincho',serif", padding: '0 2px' };
    var mImgStyle = { height: '100%', width: 'auto', display: 'block', borderRadius: 3, border: '1px solid rgba(130,120,180,0.3)' };
    var mFace = function (name, abs) {
      var src = CARD_IMAGES[name];
      var base = { height: '100%', backfaceVisibility: 'hidden', WebkitBackfaceVisibility: 'hidden' };
      if (abs) { base.position = 'absolute'; base.top = 0; base.left = 0; base.transform = 'rotateY(180deg)'; }
      return React.createElement('div', { style: base },
        src
          ? React.createElement('img', { src: src, alt: name, style: mImgStyle })
          : React.createElement('div', { style: mFallStyle }, React.createElement('span', { style: mLblStyle }, name))
      );
    };
    kids.push(React.createElement('div', {
      key: 'momentCard',
      style: { position: 'absolute', left: '1.5%', top: '28%', height: '16.8%', zIndex: 2, perspective: '600px', transform: 'rotate(180deg)' },
    },
      React.createElement('div', {
        style: { height: '100%', transformStyle: 'preserve-3d', transition: 'transform 0.7s ease', transform: mFaceUp ? 'rotateY(0deg)' : 'rotateY(180deg)' },
      },
        mFace('moment_a', false),
        mFace('moment_b', true)
      )
    ));
  }

  // 魔力トークン（zone slot 位置に独立配置、tokenMotion中は非表示）
  // guardian X: 相手側スロットのカード中心 / reinforcement X: 自分側スロットのカード中心
  ['opponent', 'player'].forEach(function (side) {
    var sideCards = side === 'player' ? player.cards : opponent.cards;
    var sideMagic = side === 'player' ? player : opponent;
    var otherSide = side === 'player' ? 'opponent' : 'player';
    sideCards.forEach(function (card, i) {
      var sl = SLOTS[side][i];
      var otherSl = SLOTS[otherSide][i];
      var guardianCx = otherSl ? cardCenterX(otherSl) : (sl ? cardCenterX(sl) : 0);
      var reinforceCx = sl ? cardCenterX(sl) : 0;
      var gPos = GUARDIAN_SLOTS[side][i];
      var gInMotion = tokenMotion && tokenMotion.side === side && tokenMotion.zone === 'guardian' && tokenMotion.targetSlot === i;
      var gC = gInMotion ? 0 : ((sideMagic.guardianTokens && sideMagic.guardianTokens[card.name]) || 0);
      var magicRot = side === 'opponent' ? 'rotate(180deg)' : undefined;
      var reinforcementRot = side === 'player' ? 'rotate(180deg)' : undefined;
      if (gC > 0 && gPos && otherSl) {
        for (var gi = 0; gi < gC; gi++) {
          var gOff = (gi - (gC - 1) / 2) * (TOKEN_W + 0.5);
          kids.push(React.createElement('div', {
            key: side + 'gm' + i + '_' + gi,
            style: {
              position: 'absolute',
              left: (guardianCx - TOKEN_W / 2 + gOff) + '%',
              top: gPos.top + '%',
              width: TOKEN_W + '%', height: TOKEN_H + '%',
              transform: magicRot,
              zIndex: 4, pointerEvents: 'none',
            },
          }, React.createElement(Token, { face: 'black', glowing: false, renderScale: tokenRenderScale })));
        }
      }
      var rPos = REINFORCEMENT_SLOTS[side][i];
      var rInMotion = tokenMotion && tokenMotion.side === side && tokenMotion.zone === 'reinforcement' && tokenMotion.targetSlot === i;
      var rC = rInMotion ? 0 : ((sideMagic.reinforcementTokens && sideMagic.reinforcementTokens[card.name]) || 0);
      if (rC > 0 && rPos && sl) {
        for (var ri = 0; ri < rC; ri++) {
          var rOff = (ri - (rC - 1) / 2) * (TOKEN_W + 0.5);
          kids.push(React.createElement('div', {
            key: side + 'rm' + i + '_' + ri,
            style: {
              position: 'absolute',
              left: (reinforceCx - TOKEN_W / 2 + rOff) + '%',
              top: rPos.top + '%',
              width: TOKEN_W + '%', height: TOKEN_H + '%',
              transform: reinforcementRot,
              zIndex: 4, pointerEvents: 'none',
            },
          }, React.createElement(Token, { face: 'black', glowing: false, renderScale: tokenRenderScale })));
        }
      }
    });
  });

  if (!dreamMotion) {
    ['opponent', 'player'].forEach(function (side) {
      var dtArr = side === 'player' ? dreamTokens.player : dreamTokens.opponent;
      if (dtArr.length <= 0) return;
      for (var di = 0; di < dtArr.length; di++) {
        var dp = dreamPos(di, side);
        kids.push(React.createElement('div', {
          key: side + 'dt' + di,
          style: {
            position: 'absolute',
            left: dp.left + '%',
            top:  dp.top  + '%',
            width: TOKEN_W + '%', height: TOKEN_H + '%',
            transform: dp.rot,
            zIndex: 4, pointerEvents: 'none',
          },
        }, React.createElement(Token, { face: dtArr[di], glowing: false, renderScale: tokenRenderScale })));
      }
    });
  }

  // 移動中トークン（黒トークンがカードスロットへフライ）
  if (tokenMotion && tokenMotion.indices && tokenMotion.indices.length > 0) {
    var motSrcPos = tokenMotion.side === 'player' ? PLAYER_POS : OPPONENT_POS;
    var zoneSlots = tokenMotion.zone === 'guardian' ? GUARDIAN_SLOTS : REINFORCEMENT_SLOTS;
    var zonePos = zoneSlots[tokenMotion.side] && zoneSlots[tokenMotion.side][tokenMotion.targetSlot];
    // guardian: 相手側スロットのX / reinforcement: 自分側スロットのX
    var motXSide = tokenMotion.zone === 'guardian'
      ? (tokenMotion.side === 'player' ? 'opponent' : 'player')
      : tokenMotion.side;
    var motSl = SLOTS[motXSide] && SLOTS[motXSide][tokenMotion.targetSlot];
    if (zonePos && motSl) {
      var motEndLeft = cardCenterX(motSl) - TOKEN_W / 2;
      var motN = tokenMotion.indices.length;
      tokenMotion.indices.forEach(function (idx, k) {
        var sp = motSrcPos[idx];
        if (!sp) return;
        var endOffset = (k - (motN - 1) / 2) * (TOKEN_W + 0.5);
        kids.push(React.createElement(MovingToken, {
          key: 'mv' + tokenMotion.key + ':' + idx,
          startLeft: sp[0], startTop: sp[1],
          endLeft: motEndLeft + endOffset,
          endTop: zonePos.top,
          rotated: tokenMotion.zone === 'reinforcement'
            ? tokenMotion.side === 'player'
            : tokenMotion.side === 'opponent',
          renderScale: tokenRenderScale,
        }));
      });
    }
  }

  if (dreamMotion) {
    ['opponent', 'player'].forEach(function (side) {
      var dm = dreamMotion[side];
      if (!dm) return;
      var otherSide = side === 'player' ? 'opponent' : 'player';
      var allSrc = [];

      dm.guardian.forEach(function (entry) {
        var sl      = SLOTS[side][entry.slotIndex];
        var otherSl = SLOTS[otherSide][entry.slotIndex];
        var gCx = otherSl ? cardCenterX(otherSl) : (sl ? cardCenterX(sl) : 0);
        var gTop = GUARDIAN_SLOTS[side][entry.slotIndex]
          ? GUARDIAN_SLOTS[side][entry.slotIndex].top : 50;
        for (var gi = 0; gi < entry.count; gi++) {
          var gOff = (gi - (entry.count - 1) / 2) * (TOKEN_W + 0.5);
          allSrc.push({ startLeft: gCx - TOKEN_W / 2 + gOff, startTop: gTop });
        }
      });

      dm.reinforcement.forEach(function (entry) {
        var sl   = SLOTS[side][entry.slotIndex];
        var rCx  = sl ? cardCenterX(sl) : 0;
        var rTop = REINFORCEMENT_SLOTS[side][entry.slotIndex]
          ? REINFORCEMENT_SLOTS[side][entry.slotIndex].top : 50;
        for (var ri = 0; ri < entry.count; ri++) {
          var rOff = (ri - (entry.count - 1) / 2) * (TOKEN_W + 0.5);
          allSrc.push({ startLeft: rCx - TOKEN_W / 2 + rOff, startTop: rTop });
        }
      });

      var total = allSrc.length;
      var priorCount = (side === 'player' ? dreamTokens.player.length : dreamTokens.opponent.length) - total;
      allSrc.forEach(function (src, idx) {
        var dp = dreamPos(priorCount + idx, side);
        kids.push(React.createElement(MovingToken, {
          key: 'dmt' + dreamMotion.key + ':' + side + ':' + idx,
          startLeft: src.startLeft,
          startTop:  src.startTop,
          endLeft:   dp.left,
          endTop:    dp.top,
          rotated:   dp.rot === 'rotate(180deg)',
          renderScale: tokenRenderScale,
        }));
      });
    });
  }

  // playerTokenArea グロー：ボード全体を暗くし、aware.webp とプレイヤートークンだけを浮かび上がらせる
  if (glowPlayerTokenArea) {
    kids.push(React.createElement('div', { key: 'pta-dim', style: {
      position: 'absolute', inset: 0,
      background: 'rgba(0,0,0,0.65)',
      zIndex: 5, pointerEvents: 'none',
      animation: 'dimFadeIn 0.5s ease',
    }}));
    kids.push(React.createElement('img', { key: 'pta-aware', src: 'assets/aware.webp', alt: '', style: {
      position: 'absolute', inset: 0,
      width: '100%', height: '100%',
      objectFit: 'fill',
      zIndex: 6, pointerEvents: 'none',
      filter: 'drop-shadow(0 0 5px rgba(167,139,250,0.9)) drop-shadow(0 0 14px rgba(124,58,237,0.8))',
    }}));
  }

  return React.createElement('div', {
    ref: boardWrapRef,
    style: {
      flex: 1, minHeight: 0,
      display: 'flex',
      alignItems: 'center',
      justifyContent: 'center',
      perspective: 900, perspectiveOrigin: '50% 55%',
      position: 'relative', overflow: 'hidden',
      containerType: 'size',
      opacity: fadeOut ? 0 : 1,
      transition: 'opacity 2s ease',
    }
  },
    React.createElement('div', {
      style: {
        width: cameraLayout.width,
        flexShrink: 0,
        aspectRatio: '1588 / 2167',
        backgroundImage: 'url(uploads/board_default.jpg)',
        backgroundSize: '100% 100%',
        backgroundRepeat: 'no-repeat',
        borderRadius: 2,
        boxShadow: '0 8px 40px rgba(0,0,0,.6), 0 0 60px rgba(60,30,100,.06)',
        transform: cameraLayout.transform,
        transition: cameraLayout.transition,
        willChange: 'transform',
        position: 'relative',
        WebkitBackfaceVisibility: 'hidden',
        backfaceVisibility: 'hidden',
        transformOrigin: '50% 50%',
      }
    }, kids),
    React.createElement(FloatingCardOverlay, { card: floatingCard, dimmed: glowPlayerTokenArea, focusCost: glowFocusCardCost }),
  );
}

window.Board = Board;
window.CARD_IMAGES = CARD_IMAGES;
