import { useLocalStorage } from '../chunk-VVWQTO4Y.mjs';
import '../chunk-WEEXCPSE.mjs';
import '../chunk-BJTO5JO5.mjs';
import React3, { useRef, useEffect, useState, useCallback, useMemo } from 'react';

// src/mikuFusionGame/constants.ts
var BASE_RADIUS = 22;
var RADIUS_STEP = 4;
var LEVEL_LABEL_PREFIX = "M";
var DEFAULT_MIKU_FUSION_CONFIG = {
  width: 390,
  height: 700,
  gravity: 1450,
  damping: 0.995,
  collisionDamping: 0.92,
  spawnY: 82,
  lossLineY: 132,
  maxLevel: 10,
  maxOrbs: 90,
  maxMergesPerTick: 6,
  gameOverAgeThreshold: 1.1,
  spawnWeights: [0.62, 0.28, 0.1],
  theme: {
    backgroundTop: "#dafaff",
    backgroundBottom: "#86e5ef",
    aimLine: "#14b8a6",
    lossLine: "#ef4444",
    orbColors: [
      "#67e8f9",
      "#22d3ee",
      "#2dd4bf",
      "#5eead4",
      "#34d399",
      "#a7f3d0",
      "#99f6e4",
      "#0ea5e9",
      "#06b6d4",
      "#14b8a6"
    ]
  }
};

// src/mikuFusionGame/engine/collision.ts
function resolveCircleCollisions(orbs, config) {
  const next = orbs.map((orb) => ({ ...orb }));
  for (let i = 0; i < next.length; i += 1) {
    for (let j = i + 1; j < next.length; j += 1) {
      const a = next[i];
      const b = next[j];
      const dx = b.x - a.x;
      const dy = b.y - a.y;
      const distance = Math.sqrt(dx * dx + dy * dy) || 1e-4;
      const minDistance = a.radius + b.radius;
      if (distance >= minDistance) {
        continue;
      }
      const nx = dx / distance;
      const ny = dy / distance;
      const overlap = minDistance - distance;
      a.x -= nx * overlap * 0.5;
      a.y -= ny * overlap * 0.5;
      b.x += nx * overlap * 0.5;
      b.y += ny * overlap * 0.5;
      const rvx = b.vx - a.vx;
      const rvy = b.vy - a.vy;
      const velocityAlongNormal = rvx * nx + rvy * ny;
      if (velocityAlongNormal > 0) {
        continue;
      }
      const restitution = config.collisionDamping;
      const impulse = -(1 + restitution) * velocityAlongNormal / 2;
      const impulseX = impulse * nx;
      const impulseY = impulse * ny;
      a.vx -= impulseX;
      a.vy -= impulseY;
      b.vx += impulseX;
      b.vy += impulseY;
    }
  }
  return next;
}

// src/mikuFusionGame/engine/scoring.ts
function getMergeScore(newLevel, chainIndex) {
  const base = 10 * Math.max(1, newLevel) * Math.max(1, newLevel);
  const chainMultiplier = 1 + Math.max(0, chainIndex - 1) * 0.15;
  return Math.round(base * chainMultiplier);
}

// src/mikuFusionGame/engine/physics.ts
function getRadiusByLevel(level) {
  return BASE_RADIUS + (Math.max(1, level) - 1) * RADIUS_STEP;
}
function stepPhysics(orbs, config, dt) {
  return orbs.map((orb) => {
    const next = { ...orb };
    next.vy += config.gravity * dt;
    next.vx *= config.damping;
    next.vy *= config.damping;
    next.x += next.vx * dt;
    next.y += next.vy * dt;
    next.age += dt;
    if (next.x - next.radius < 0) {
      next.x = next.radius;
      next.vx = Math.abs(next.vx) * config.collisionDamping;
    } else if (next.x + next.radius > config.width) {
      next.x = config.width - next.radius;
      next.vx = -Math.abs(next.vx) * config.collisionDamping;
    }
    if (next.y + next.radius > config.height) {
      next.y = config.height - next.radius;
      next.vy = -Math.abs(next.vy) * config.collisionDamping;
    }
    return next;
  });
}

// src/mikuFusionGame/engine/merge.ts
function canMerge(a, b, config) {
  if (a.level !== b.level) {
    return false;
  }
  if (a.level >= config.maxLevel) {
    return false;
  }
  const dx = b.x - a.x;
  const dy = b.y - a.y;
  const distance = Math.sqrt(dx * dx + dy * dy);
  const threshold = (a.radius + b.radius) * 0.92;
  return distance <= threshold;
}
function mergeSameLevelOrbs(orbs, config) {
  const mergedIds = /* @__PURE__ */ new Set();
  const spawned = [];
  let scoreGain = 0;
  let chainIndex = 0;
  for (let i = 0; i < orbs.length; i += 1) {
    if (mergedIds.has(orbs[i].id)) {
      continue;
    }
    for (let j = i + 1; j < orbs.length; j += 1) {
      if (mergedIds.has(orbs[j].id)) {
        continue;
      }
      const a = orbs[i];
      const b = orbs[j];
      if (!canMerge(a, b, config)) {
        continue;
      }
      mergedIds.add(a.id);
      mergedIds.add(b.id);
      chainIndex += 1;
      const nextLevel = Math.min(config.maxLevel, a.level + 1);
      const mergedOrb = {
        id: `orb-${a.id}-${b.id}-${chainIndex}`,
        x: (a.x + b.x) / 2,
        y: (a.y + b.y) / 2,
        vx: (a.vx + b.vx) / 2 * 0.4,
        vy: (a.vy + b.vy) / 2 * 0.4,
        radius: getRadiusByLevel(nextLevel),
        level: nextLevel,
        age: Math.min(a.age, b.age)
      };
      spawned.push(mergedOrb);
      scoreGain += getMergeScore(nextLevel, chainIndex);
      if (chainIndex >= config.maxMergesPerTick) {
        break;
      }
    }
    if (chainIndex >= config.maxMergesPerTick) {
      break;
    }
  }
  if (mergedIds.size === 0) {
    return {
      orbs,
      scoreGain: 0,
      mergeCount: 0
    };
  }
  const survivors = orbs.filter((orb) => !mergedIds.has(orb.id));
  const mergedOrbs = survivors.concat(spawned);
  return {
    orbs: mergedOrbs,
    scoreGain,
    mergeCount: chainIndex
  };
}

// src/mikuFusionGame/engine/stateMachine.ts
var ALLOWED_TRANSITIONS = {
  ready: ["playing"],
  playing: ["paused", "gameOver", "ready"],
  paused: ["playing", "ready"],
  gameOver: ["ready", "playing"]
};
function canTransition(from, to) {
  return ALLOWED_TRANSITIONS[from].includes(to);
}

// src/mikuFusionGame/hooks/useMikuFusionGame.ts
function clamp(value, min, max) {
  return Math.max(min, Math.min(max, value));
}
function nextOrbLevel(weights) {
  const random = Math.random();
  let cumulative = 0;
  for (let i = 0; i < weights.length; i += 1) {
    cumulative += weights[i] ?? 0;
    if (random <= cumulative) {
      return i + 1;
    }
  }
  return Math.max(1, weights.length);
}
function getOrbColor(level, config) {
  const index = Math.max(0, Math.min(config.theme.orbColors.length - 1, level - 1));
  return config.theme.orbColors[index] || "#22d3ee";
}
function drawScene(context, width, height, config, orbs, aimX, nextLevel, status) {
  const gradient = context.createLinearGradient(0, 0, 0, height);
  gradient.addColorStop(0, config.theme.backgroundTop);
  gradient.addColorStop(1, config.theme.backgroundBottom);
  context.fillStyle = gradient;
  context.fillRect(0, 0, width, height);
  context.lineWidth = 2;
  context.strokeStyle = config.theme.lossLine;
  context.setLineDash([6, 6]);
  context.beginPath();
  context.moveTo(0, config.lossLineY);
  context.lineTo(width, config.lossLineY);
  context.stroke();
  context.setLineDash([]);
  context.strokeStyle = config.theme.aimLine;
  context.lineWidth = 1.5;
  context.beginPath();
  context.moveTo(aimX, 0);
  context.lineTo(aimX, config.spawnY);
  context.stroke();
  const previewRadius = getRadiusByLevel(nextLevel);
  context.fillStyle = getOrbColor(nextLevel, config);
  context.beginPath();
  context.arc(aimX, config.spawnY - previewRadius - 4, previewRadius, 0, Math.PI * 2);
  context.fill();
  orbs.forEach((orb) => {
    context.beginPath();
    context.fillStyle = getOrbColor(orb.level, config);
    context.arc(orb.x, orb.y, orb.radius, 0, Math.PI * 2);
    context.fill();
    context.fillStyle = "#0f172a";
    context.font = "bold 12px system-ui";
    context.textAlign = "center";
    context.textBaseline = "middle";
    context.fillText(`${LEVEL_LABEL_PREFIX}${orb.level}`, orb.x, orb.y);
  });
  if (status === "paused") {
    context.fillStyle = "rgba(15, 23, 42, 0.5)";
    context.fillRect(0, 0, width, height);
    context.fillStyle = "#ffffff";
    context.font = "bold 30px system-ui";
    context.textAlign = "center";
    context.fillText("Paused", width / 2, height / 2);
  }
}
function normalizeConfig(config) {
  const mergedTheme = {
    ...DEFAULT_MIKU_FUSION_CONFIG.theme,
    ...config?.theme ?? {}
  };
  const merged = {
    ...DEFAULT_MIKU_FUSION_CONFIG,
    ...config ?? {},
    theme: mergedTheme
  };
  const totalWeight = merged.spawnWeights.reduce((sum, current) => sum + current, 0);
  if (totalWeight <= 0) {
    merged.spawnWeights = [...DEFAULT_MIKU_FUSION_CONFIG.spawnWeights];
  } else if (Math.abs(totalWeight - 1) > 1e-3) {
    merged.spawnWeights = merged.spawnWeights.map((weight) => weight / totalWeight);
  }
  return merged;
}
function useMikuFusionGame(options = {}) {
  const configRef = useRef(normalizeConfig(options));
  const config = configRef.current;
  const onScoreChangeRef = useRef(options.onScoreChange);
  const onGameOverRef = useRef(options.onGameOver);
  useEffect(() => {
    onScoreChangeRef.current = options.onScoreChange;
    onGameOverRef.current = options.onGameOver;
  }, [options.onGameOver, options.onScoreChange]);
  const [bestScore, setBestScore] = useLocalStorage(
    options.storageKey || "sa2kit:mikuFusionGame:bestScore",
    0
  );
  const canvasRef = useRef(null);
  const rafRef = useRef(null);
  const lastTsRef = useRef(0);
  const idRef = useRef(0);
  const orbsRef = useRef([]);
  const aimXRef = useRef(config.width / 2);
  const scoreRef = useRef(0);
  const [status, setStatus] = useState("ready");
  const [score, setScore] = useState(0);
  const [nextLevel, setNextLevel] = useState(() => nextOrbLevel(config.spawnWeights));
  const draw = useCallback(
    (nextStatus = status) => {
      const canvas = canvasRef.current;
      if (!canvas) {
        return;
      }
      const context = canvas.getContext("2d");
      if (!context) {
        return;
      }
      drawScene(
        context,
        config.width,
        config.height,
        config,
        orbsRef.current,
        aimXRef.current,
        nextLevel,
        nextStatus
      );
    },
    [config, nextLevel, status]
  );
  const transitionStatus = useCallback((nextStatus) => {
    setStatus((previous) => canTransition(previous, nextStatus) ? nextStatus : previous);
  }, []);
  const spawnOrb = useCallback(() => {
    if (status === "paused" || status === "gameOver") {
      return;
    }
    if (status === "ready") {
      transitionStatus("playing");
    }
    idRef.current += 1;
    const radius = getRadiusByLevel(nextLevel);
    const x = clamp(aimXRef.current, radius, config.width - radius);
    const created = {
      id: `${idRef.current}`,
      x,
      y: config.spawnY,
      vx: 0,
      vy: 0,
      radius,
      level: nextLevel,
      age: 0
    };
    const nextOrbs = orbsRef.current.concat(created);
    orbsRef.current = nextOrbs.slice(-config.maxOrbs);
    setNextLevel(nextOrbLevel(config.spawnWeights));
  }, [config, nextLevel, status, transitionStatus]);
  const restart = useCallback(() => {
    orbsRef.current = [];
    scoreRef.current = 0;
    setScore(0);
    setNextLevel(nextOrbLevel(config.spawnWeights));
    transitionStatus("ready");
    draw("ready");
  }, [config.spawnWeights, draw, transitionStatus]);
  const togglePause = useCallback(() => {
    setStatus((previous) => {
      if (previous === "playing" && canTransition(previous, "paused")) {
        return "paused";
      }
      if (previous === "paused" && canTransition(previous, "playing")) {
        return "playing";
      }
      return previous;
    });
  }, []);
  const setAimFromClientX = useCallback((clientX, rect) => {
    const ratio = config.width / rect.width;
    const x = (clientX - rect.left) * ratio;
    aimXRef.current = clamp(x, 0, config.width);
  }, [config.width]);
  const handleDrop = useCallback(
    (clientX, rect) => {
      setAimFromClientX(clientX, rect);
      spawnOrb();
    },
    [setAimFromClientX, spawnOrb]
  );
  useEffect(() => {
    draw(status);
  }, [draw, status]);
  useEffect(() => {
    if (status !== "playing") {
      if (rafRef.current) {
        window.cancelAnimationFrame(rafRef.current);
      }
      lastTsRef.current = 0;
      draw(status);
      return;
    }
    const frame = (timestamp) => {
      if (lastTsRef.current === 0) {
        lastTsRef.current = timestamp;
      }
      const dt = Math.min((timestamp - lastTsRef.current) / 1e3, 0.033);
      lastTsRef.current = timestamp;
      let orbs = stepPhysics(orbsRef.current, config, dt);
      orbs = resolveCircleCollisions(orbs, config);
      const merged = mergeSameLevelOrbs(orbs, config);
      orbs = merged.orbs;
      orbsRef.current = orbs.slice(-config.maxOrbs);
      if (merged.scoreGain > 0) {
        scoreRef.current += merged.scoreGain;
        setScore(scoreRef.current);
        onScoreChangeRef.current?.(scoreRef.current);
      }
      const gameOver = orbs.some(
        (orb) => orb.age > config.gameOverAgeThreshold && orb.y - orb.radius <= config.lossLineY && Math.abs(orb.vy) < 90
      );
      if (gameOver) {
        transitionStatus("gameOver");
        const latestBest = Math.max(bestScore, scoreRef.current);
        if (latestBest !== bestScore) {
          setBestScore(latestBest);
        }
        onGameOverRef.current?.(scoreRef.current, latestBest);
        draw("gameOver");
        return;
      }
      draw("playing");
      rafRef.current = window.requestAnimationFrame(frame);
    };
    rafRef.current = window.requestAnimationFrame(frame);
    return () => {
      if (rafRef.current) {
        window.cancelAnimationFrame(rafRef.current);
      }
    };
  }, [
    bestScore,
    config,
    draw,
    setBestScore,
    status,
    transitionStatus
  ]);
  useEffect(() => {
    if (score > bestScore) {
      setBestScore(score);
    }
  }, [bestScore, score, setBestScore]);
  return {
    canvasRef,
    status,
    score,
    bestScore: Math.max(bestScore, score),
    nextLevel,
    config,
    setAimFromClientX,
    handleDrop,
    togglePause,
    restart
  };
}
function useResponsiveCanvas(logicalWidth, logicalHeight, containerRef) {
  const [size, setSize] = useState({
    displayWidth: logicalWidth,
    displayHeight: logicalHeight,
    scale: 1
  });
  useEffect(() => {
    const updateSize = () => {
      const container2 = containerRef.current;
      if (!container2 || typeof window === "undefined") {
        return;
      }
      const rect = container2.getBoundingClientRect();
      const maxWidth = Math.max(280, rect.width);
      const maxHeight = Math.max(420, Math.floor(window.innerHeight * 0.78));
      const scale = Math.min(maxWidth / logicalWidth, maxHeight / logicalHeight, 1);
      setSize({
        displayWidth: Math.floor(logicalWidth * scale),
        displayHeight: Math.floor(logicalHeight * scale),
        scale
      });
    };
    updateSize();
    const container = containerRef.current;
    const observer = typeof ResizeObserver !== "undefined" ? new ResizeObserver(updateSize) : null;
    if (container && observer) {
      observer.observe(container);
    }
    window.addEventListener("resize", updateSize);
    return () => {
      observer?.disconnect();
      window.removeEventListener("resize", updateSize);
    };
  }, [containerRef, logicalWidth, logicalHeight]);
  return size;
}
function GameCanvas({
  canvasRef,
  width,
  height,
  displayWidth,
  displayHeight,
  onPointerMove,
  onPointerDown,
  onPointerUp
}) {
  return /* @__PURE__ */ React3.createElement(
    "canvas",
    {
      ref: canvasRef,
      width,
      height,
      className: "rounded-xl border border-cyan-200 bg-cyan-50 shadow-sm",
      style: {
        width: `${displayWidth}px`,
        height: `${displayHeight}px`,
        maxWidth: "100%",
        touchAction: "none"
      },
      onPointerMove,
      onPointerDown,
      onPointerUp,
      onContextMenu: (event) => event.preventDefault()
    }
  );
}
function GameControls({ isPaused, isPlaying, onTogglePause, onRestart }) {
  return /* @__PURE__ */ React3.createElement("div", { className: "grid grid-cols-2 gap-2" }, /* @__PURE__ */ React3.createElement(
    "button",
    {
      type: "button",
      className: "rounded-lg bg-cyan-600 px-4 py-3 text-sm font-semibold text-white disabled:cursor-not-allowed disabled:opacity-50",
      onClick: onTogglePause,
      disabled: !isPlaying
    },
    isPaused ? "\u7EE7\u7EED" : "\u6682\u505C"
  ), /* @__PURE__ */ React3.createElement(
    "button",
    {
      type: "button",
      className: "rounded-lg bg-emerald-600 px-4 py-3 text-sm font-semibold text-white",
      onClick: onRestart
    },
    "\u91CD\u5F00"
  ));
}
function GameHUD({ score, bestScore, nextLevel, statusText }) {
  return /* @__PURE__ */ React3.createElement("div", { className: "flex items-center justify-between gap-2 rounded-lg border border-cyan-200 bg-white/80 p-3" }, /* @__PURE__ */ React3.createElement("div", { className: "min-w-0" }, /* @__PURE__ */ React3.createElement("div", { className: "text-xs text-slate-500" }, "\u5F53\u524D\u5206\u6570"), /* @__PURE__ */ React3.createElement("div", { className: "text-xl font-bold text-cyan-700" }, score), /* @__PURE__ */ React3.createElement("div", { className: "text-xs text-slate-500" }, "\u6700\u9AD8\u5206 ", bestScore)), /* @__PURE__ */ React3.createElement("div", { className: "text-right" }, /* @__PURE__ */ React3.createElement("div", { className: "text-xs text-slate-500" }, "\u4E0B\u4E00\u4E2A"), /* @__PURE__ */ React3.createElement("div", { className: "text-base font-semibold text-emerald-700" }, "M", nextLevel), /* @__PURE__ */ React3.createElement("div", { className: "text-xs font-medium text-slate-600" }, statusText)));
}
function GameResultModal({ open, score, bestScore, onRestart }) {
  if (!open) {
    return null;
  }
  return /* @__PURE__ */ React3.createElement("div", { className: "pointer-events-none absolute inset-0 flex items-center justify-center bg-slate-900/50 p-4" }, /* @__PURE__ */ React3.createElement("div", { className: "pointer-events-auto w-full max-w-xs rounded-xl bg-white p-5 shadow-2xl" }, /* @__PURE__ */ React3.createElement("h3", { className: "text-lg font-semibold text-slate-900" }, "\u6E38\u620F\u7ED3\u675F"), /* @__PURE__ */ React3.createElement("p", { className: "mt-2 text-sm text-slate-600" }, "\u5F53\u524D\u5206\u6570\uFF1A", score), /* @__PURE__ */ React3.createElement("p", { className: "text-sm text-slate-600" }, "\u6700\u9AD8\u5206\uFF1A", bestScore), /* @__PURE__ */ React3.createElement(
    "button",
    {
      type: "button",
      className: "mt-4 w-full rounded-lg bg-cyan-600 px-4 py-2 text-sm font-semibold text-white",
      onClick: onRestart
    },
    "\u518D\u6765\u4E00\u5C40"
  )));
}

// src/mikuFusionGame/components/MikuFusionGame.tsx
function MikuFusionGame({ className, storageKey, ...options }) {
  const containerRef = useRef(null);
  const {
    canvasRef,
    status,
    score,
    bestScore,
    nextLevel,
    config,
    setAimFromClientX,
    handleDrop,
    togglePause,
    restart
  } = useMikuFusionGame({ ...options, storageKey });
  const { displayWidth, displayHeight } = useResponsiveCanvas(
    config.width,
    config.height,
    containerRef
  );
  const statusText = useMemo(() => {
    if (status === "ready") {
      return "\u5F85\u5F00\u59CB";
    }
    if (status === "playing") {
      return "\u8FDB\u884C\u4E2D";
    }
    if (status === "paused") {
      return "\u5DF2\u6682\u505C";
    }
    return "\u5DF2\u7ED3\u675F";
  }, [status]);
  const handlePointerMove = (event) => {
    const rect = event.currentTarget.getBoundingClientRect();
    setAimFromClientX(event.clientX, rect);
  };
  const handlePointerDown = (event) => {
    event.currentTarget.setPointerCapture(event.pointerId);
    const rect = event.currentTarget.getBoundingClientRect();
    setAimFromClientX(event.clientX, rect);
  };
  const handlePointerUp = (event) => {
    const rect = event.currentTarget.getBoundingClientRect();
    handleDrop(event.clientX, rect);
    event.currentTarget.releasePointerCapture(event.pointerId);
  };
  const handleKeyDown = (event) => {
    if (event.key.toLowerCase() === "r") {
      restart();
    }
    if (event.key.toLowerCase() === "p") {
      togglePause();
    }
  };
  return /* @__PURE__ */ React3.createElement(
    "div",
    {
      ref: containerRef,
      tabIndex: 0,
      onKeyDown: handleKeyDown,
      className: `relative mx-auto flex w-full max-w-xl flex-col gap-3 p-3 md:p-4 ${className || ""}`,
      style: {
        paddingBottom: "max(0.75rem, env(safe-area-inset-bottom))"
      }
    },
    /* @__PURE__ */ React3.createElement(GameHUD, { score, bestScore, nextLevel, statusText }),
    /* @__PURE__ */ React3.createElement("div", { className: "relative mx-auto" }, /* @__PURE__ */ React3.createElement(
      GameCanvas,
      {
        canvasRef,
        width: config.width,
        height: config.height,
        displayWidth,
        displayHeight,
        onPointerMove: handlePointerMove,
        onPointerDown: handlePointerDown,
        onPointerUp: handlePointerUp
      }
    ), /* @__PURE__ */ React3.createElement(GameResultModal, { open: status === "gameOver", score, bestScore, onRestart: restart })),
    /* @__PURE__ */ React3.createElement(
      GameControls,
      {
        isPaused: status === "paused",
        isPlaying: status === "playing" || status === "paused",
        onTogglePause: togglePause,
        onRestart: restart
      }
    )
  );
}

// src/mikuFusionGame/content/index.ts
var defaultContentRegistry = {
  producers: [],
  songs: [],
  themes: []
};

export { BASE_RADIUS, DEFAULT_MIKU_FUSION_CONFIG, LEVEL_LABEL_PREFIX, MikuFusionGame, RADIUS_STEP, defaultContentRegistry, useMikuFusionGame, useResponsiveCanvas };
//# sourceMappingURL=index.mjs.map
//# sourceMappingURL=index.mjs.map