'use strict';

var React = require('react');
var clsx = require('clsx');
var tailwindMerge = require('tailwind-merge');

function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }

var React__default = /*#__PURE__*/_interopDefault(React);

// src/mikuFusionGame/components/MikuFusionGame.tsx
function useStorage(storage, key, defaultValue) {
  const [value, setValue] = React.useState(defaultValue);
  const [loading, setLoading] = React.useState(true);
  React.useEffect(() => {
    const loadValue = async () => {
      try {
        const stored = await storage.getItem(key);
        if (stored !== null) {
          setValue(JSON.parse(stored));
        }
      } catch (error) {
        console.error('Error reading storage key "' + key + '":', error);
      } finally {
        setLoading(false);
      }
    };
    void loadValue();
  }, [storage, key]);
  const updateValue = React.useCallback(
    async (newValue) => {
      try {
        setValue(newValue);
        await storage.setItem(key, JSON.stringify(newValue));
        if ("dispatchChange" in storage && typeof storage.dispatchChange === "function") {
          storage.dispatchChange(key, JSON.stringify(newValue));
        }
      } catch (error) {
        console.error('Error setting storage key "' + key + '":', error);
      }
    },
    [storage, key]
  );
  const removeValue = React.useCallback(async () => {
    try {
      setValue(defaultValue);
      await storage.removeItem(key);
      if ("dispatchChange" in storage && typeof storage.dispatchChange === "function") {
        storage.dispatchChange(key, null);
      }
    } catch (error) {
      console.error('Error removing storage key "' + key + '":', error);
    }
  }, [storage, key, defaultValue]);
  React.useEffect(() => {
    if (!storage.addChangeListener) {
      return;
    }
    const cleanup = storage.addChangeListener((changedKey, newValue) => {
      if (changedKey === key) {
        try {
          if (newValue === null) {
            setValue(defaultValue);
          } else {
            setValue(JSON.parse(newValue));
          }
        } catch (error) {
          console.error("Error parsing storage change event:", error);
        }
      }
    });
    return cleanup;
  }, [storage, key, defaultValue]);
  return [value, updateValue, removeValue, loading];
}

// src/storage/adapters/web-adapter.ts
var isBrowser = typeof window !== "undefined" && typeof window.localStorage !== "undefined";
var WebStorageAdapter = class {
  async getItem(key) {
    if (!isBrowser) {
      return null;
    }
    try {
      return localStorage.getItem(key);
    } catch (error) {
      console.error('[WebStorage] Error getting item "' + key + '":', error);
      return null;
    }
  }
  async setItem(key, value) {
    if (!isBrowser) {
      return;
    }
    try {
      localStorage.setItem(key, value);
    } catch (error) {
      console.error('[WebStorage] Error setting item "' + key + '":', error);
      throw error;
    }
  }
  async removeItem(key) {
    if (!isBrowser) {
      return;
    }
    try {
      localStorage.removeItem(key);
    } catch (error) {
      console.error('[WebStorage] Error removing item "' + key + '":', error);
      throw error;
    }
  }
  async clear() {
    if (!isBrowser) {
      return;
    }
    try {
      localStorage.clear();
    } catch (error) {
      console.error("[WebStorage] Error clearing storage:", error);
      throw error;
    }
  }
  addChangeListener(callback) {
    if (!isBrowser) {
      return () => {
      };
    }
    const handleStorageEvent = (e) => {
      if (e.key) {
        callback(e.key, e.newValue);
      }
    };
    const handleCustomEvent = (e) => {
      const customEvent = e;
      callback(customEvent.detail.key, customEvent.detail.value);
    };
    window.addEventListener("storage", handleStorageEvent);
    window.addEventListener("local-storage-change", handleCustomEvent);
    return () => {
      window.removeEventListener("storage", handleStorageEvent);
      window.removeEventListener("local-storage-change", handleCustomEvent);
    };
  }
  /**
   * 触发自定义事件，通知同标签页的其他组件
   */
  dispatchChange(key, value) {
    if (!isBrowser) {
      return;
    }
    window.dispatchEvent(
      new CustomEvent("local-storage-change", {
        detail: { key, value }
      })
    );
  }
};

// src/storage/hooks/useLocalStorage.ts
var webStorage = new WebStorageAdapter();
function useLocalStorage(key, defaultValue) {
  return useStorage(webStorage, key, defaultValue);
}

// src/logger/console-adapter.ts
var ConsoleLoggerAdapter = class {
  constructor() {
    this.colors = {
      DEBUG: "\x1B[36m",
      // Cyan
      INFO: "\x1B[32m",
      // Green
      WARN: "\x1B[33m",
      // Yellow
      ERROR: "\x1B[31m",
      // Red
      RESET: "\x1B[0m"
    };
  }
  log(entry) {
    const { level, message, timestamp, data, context, error } = entry;
    let logMessage = "";
    if (timestamp) {
      logMessage += "[" + this.formatTimestamp(timestamp) + "] ";
    }
    const levelName = this.getLevelName(level);
    logMessage += levelName + ": ";
    if (context) {
      logMessage += "[" + context + "] ";
    }
    logMessage += message;
    switch (level) {
      case 0:
        console.debug(this.colorize(logMessage, "DEBUG"), data || "");
        break;
      case 1:
        console.info(this.colorize(logMessage, "INFO"), data || "");
        break;
      case 2:
        console.warn(this.colorize(logMessage, "WARN"), data || "");
        break;
      case 3:
        console.error(this.colorize(logMessage, "ERROR"), data || "");
        if (error) {
          console.error(error);
        }
        break;
    }
  }
  formatTimestamp(date) {
    return date.toISOString();
  }
  getLevelName(level) {
    const names = ["DEBUG", "INFO", "WARN", "ERROR", "NONE"];
    return names[level] || "UNKNOWN";
  }
  colorize(message, level) {
    if (typeof process !== "undefined" && process.stdout?.isTTY) {
      return this.colors[level] + message + this.colors.RESET;
    }
    return message;
  }
};

// src/logger/Logger.ts
var Logger = class _Logger {
  constructor(config, context) {
    const isProduction = typeof process !== "undefined" ? process.env.NODE_ENV === "production" : false;
    this.config = {
      minLevel: config?.minLevel ?? (isProduction ? 1 : 0),
      // INFO in prod, DEBUG in dev
      enableTimestamp: config?.enableTimestamp ?? true,
      enableContext: config?.enableContext ?? true,
      environment: config?.environment ?? (isProduction ? "production" : "development"),
      adapter: config?.adapter ?? new ConsoleLoggerAdapter()
    };
    this.adapter = this.config.adapter;
    this.context = context;
  }
  /**
   * 创建带上下文的子 Logger
   */
  createChild(context) {
    return new _Logger(this.config, context);
  }
  /**
   * 调试日志
   */
  debug(message, data) {
    this.log(0, message, data);
  }
  /**
   * 信息日志
   */
  info(message, data) {
    this.log(1, message, data);
  }
  /**
   * 警告日志
   */
  warn(message, data) {
    this.log(2, message, data);
  }
  /**
   * 错误日志
   */
  error(message, error) {
    this.log(
      3,
      // LogLevel.ERROR
      message,
      error instanceof Error ? void 0 : error,
      error instanceof Error ? error : void 0
    );
  }
  /**
   * 核心日志方法
   */
  log(level, message, data, error) {
    if (level < this.config.minLevel) {
      return;
    }
    if (typeof window !== "undefined" && typeof localStorage !== "undefined") {
      const loggerDebug = localStorage.getItem("logger-debug");
      if (loggerDebug === "false" && level < 3) {
        return;
      }
    }
    const entry = {
      level,
      message,
      timestamp: this.config.enableTimestamp ? /* @__PURE__ */ new Date() : void 0,
      data,
      context: this.config.enableContext ? this.context : void 0,
      error
    };
    this.adapter.log(entry);
  }
  /**
   * 设置日志级别
   */
  setLevel(level) {
    this.config.minLevel = level;
  }
  /**
   * 获取当前日志级别
   */
  getLevel() {
    return this.config.minLevel;
  }
};
new Logger();
function cn(...inputs) {
  return tailwindMerge.twMerge(clsx.clsx(inputs));
}

// src/components/ImageMappingPanel.tsx
function ImageMappingPanel({
  title,
  items,
  value,
  onChange,
  className,
  uploadLabel = "\u4E0A\u4F20\u56FE\u7247",
  clearLabel = "\u6E05\u9664"
}) {
  const handleFileChange = (id, file) => {
    if (!file) {
      return;
    }
    const reader = new FileReader();
    reader.onload = () => {
      const base64 = typeof reader.result === "string" ? reader.result : "";
      if (!base64) {
        return;
      }
      onChange({
        ...value,
        [id]: base64
      });
    };
    reader.readAsDataURL(file);
  };
  const handleRemove = (id) => {
    const next = { ...value };
    delete next[id];
    onChange(next);
  };
  return /* @__PURE__ */ React__default.default.createElement("details", { className: cn("rounded-lg border border-slate-200 bg-white/80 p-3", className) }, /* @__PURE__ */ React__default.default.createElement("summary", { className: "cursor-pointer text-sm font-semibold text-slate-700" }, title), /* @__PURE__ */ React__default.default.createElement("div", { className: "mt-3 grid grid-cols-1 gap-2 sm:grid-cols-2" }, items.map((item) => {
    const image = value[item.id];
    return /* @__PURE__ */ React__default.default.createElement(
      "div",
      {
        key: item.id,
        className: "flex items-center gap-2 rounded-md border border-slate-200 bg-white p-2"
      },
      /* @__PURE__ */ React__default.default.createElement("div", { className: "w-16 text-xs font-semibold text-slate-600" }, item.label),
      /* @__PURE__ */ React__default.default.createElement("label", { className: "flex-1 cursor-pointer rounded-md bg-cyan-600 px-2 py-1 text-center text-xs font-medium text-white" }, uploadLabel, /* @__PURE__ */ React__default.default.createElement(
        "input",
        {
          type: "file",
          accept: "image/*",
          className: "hidden",
          onChange: (event) => {
            handleFileChange(item.id, event.target.files?.[0]);
            event.currentTarget.value = "";
          }
        }
      )),
      image ? /* @__PURE__ */ React__default.default.createElement(React__default.default.Fragment, null, /* @__PURE__ */ React__default.default.createElement(
        "img",
        {
          src: image,
          alt: item.label,
          className: "h-8 w-8 rounded-full border border-slate-200 object-cover"
        }
      ), /* @__PURE__ */ React__default.default.createElement(
        "button",
        {
          type: "button",
          className: "rounded-md border border-red-200 px-2 py-1 text-xs text-red-600",
          onClick: () => handleRemove(item.id)
        },
        clearLabel
      )) : null
    );
  })));
}

// src/components/LocalImageMappingPanel.tsx
function LocalImageMappingPanel({
  storageKey,
  defaultValue = {},
  onValueChange,
  ...panelProps
}) {
  const [value, setValue] = useLocalStorage(storageKey, defaultValue);
  return /* @__PURE__ */ React__default.default.createElement(
    ImageMappingPanel,
    {
      ...panelProps,
      value,
      onChange: (next) => {
        setValue(next);
        onValueChange?.(next);
      }
    }
  );
}

// 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.75;
  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, imageCache) {
  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);
  const previewY = config.spawnY - previewRadius - 4;
  const previewImage = imageCache[nextLevel];
  context.beginPath();
  context.arc(aimX, previewY, previewRadius, 0, Math.PI * 2);
  context.closePath();
  if (previewImage && previewImage.complete && previewImage.naturalWidth > 0) {
    context.save();
    context.clip();
    context.drawImage(
      previewImage,
      aimX - previewRadius,
      previewY - previewRadius,
      previewRadius * 2,
      previewRadius * 2
    );
    context.restore();
  } else {
    context.fillStyle = getOrbColor(nextLevel, config);
    context.fill();
  }
  orbs.forEach((orb) => {
    context.beginPath();
    context.arc(orb.x, orb.y, orb.radius, 0, Math.PI * 2);
    context.closePath();
    const orbImage = imageCache[orb.level];
    if (orbImage && orbImage.complete && orbImage.naturalWidth > 0) {
      context.save();
      context.clip();
      context.drawImage(
        orbImage,
        orb.x - orb.radius,
        orb.y - orb.radius,
        orb.radius * 2,
        orb.radius * 2
      );
      context.restore();
      context.beginPath();
      context.strokeStyle = "rgba(15, 23, 42, 0.15)";
      context.lineWidth = 1;
      context.arc(orb.x, orb.y, orb.radius, 0, Math.PI * 2);
      context.stroke();
    } else {
      context.fillStyle = getOrbColor(orb.level, config);
      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 = React.useRef(normalizeConfig(options));
  const config = configRef.current;
  const onScoreChangeRef = React.useRef(options.onScoreChange);
  const onGameOverRef = React.useRef(options.onGameOver);
  const orbImageMappingRef = React.useRef(options.orbImageMapping ?? {});
  const imageCacheRef = React.useRef({});
  React.useEffect(() => {
    onScoreChangeRef.current = options.onScoreChange;
    onGameOverRef.current = options.onGameOver;
  }, [options.onGameOver, options.onScoreChange]);
  React.useEffect(() => {
    orbImageMappingRef.current = options.orbImageMapping ?? {};
    const nextCache = {};
    Object.entries(orbImageMappingRef.current).forEach(([levelText, src]) => {
      const level = Number(levelText);
      if (!src || Number.isNaN(level)) {
        return;
      }
      const image = new window.Image();
      image.src = src;
      nextCache[level] = image;
    });
    imageCacheRef.current = nextCache;
  }, [options.orbImageMapping]);
  const [bestScore, setBestScore] = useLocalStorage(
    options.storageKey || "sa2kit:mikuFusionGame:bestScore",
    0
  );
  const canvasRef = React.useRef(null);
  const rafRef = React.useRef(null);
  const lastTsRef = React.useRef(0);
  const idRef = React.useRef(0);
  const orbsRef = React.useRef([]);
  const aimXRef = React.useRef(config.width / 2);
  const scoreRef = React.useRef(0);
  const [status, setStatus] = React.useState("ready");
  const [score, setScore] = React.useState(0);
  const [nextLevel, setNextLevel] = React.useState(() => nextOrbLevel(config.spawnWeights));
  const draw = React.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,
        imageCacheRef.current
      );
    },
    [config, nextLevel, status]
  );
  const transitionStatus = React.useCallback((nextStatus) => {
    setStatus((previous) => canTransition(previous, nextStatus) ? nextStatus : previous);
  }, []);
  const spawnOrb = React.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 = React.useCallback(() => {
    orbsRef.current = [];
    scoreRef.current = 0;
    setScore(0);
    setNextLevel(nextOrbLevel(config.spawnWeights));
    transitionStatus("ready");
    draw("ready");
  }, [config.spawnWeights, draw, transitionStatus]);
  const togglePause = React.useCallback(() => {
    setStatus((previous) => {
      if (previous === "playing" && canTransition(previous, "paused")) {
        return "paused";
      }
      if (previous === "paused" && canTransition(previous, "playing")) {
        return "playing";
      }
      return previous;
    });
  }, []);
  const setAimFromClientX = React.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 = React.useCallback(
    (clientX, rect) => {
      setAimFromClientX(clientX, rect);
      spawnOrb();
    },
    [setAimFromClientX, spawnOrb]
  );
  React.useEffect(() => {
    draw(status);
  }, [draw, status]);
  React.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
  ]);
  React.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] = React.useState({
    displayWidth: logicalWidth,
    displayHeight: logicalHeight,
    scale: 1
  });
  React.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__ */ React__default.default.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__ */ React__default.default.createElement("div", { className: "grid grid-cols-2 gap-2" }, /* @__PURE__ */ React__default.default.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__ */ React__default.default.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__ */ React__default.default.createElement("div", { className: "flex items-center justify-between gap-2 rounded-lg border border-cyan-200 bg-white/80 p-3" }, /* @__PURE__ */ React__default.default.createElement("div", { className: "min-w-0" }, /* @__PURE__ */ React__default.default.createElement("div", { className: "text-xs text-slate-500" }, "\u5F53\u524D\u5206\u6570"), /* @__PURE__ */ React__default.default.createElement("div", { className: "text-xl font-bold text-cyan-700" }, score), /* @__PURE__ */ React__default.default.createElement("div", { className: "text-xs text-slate-500" }, "\u6700\u9AD8\u5206 ", bestScore)), /* @__PURE__ */ React__default.default.createElement("div", { className: "text-right" }, /* @__PURE__ */ React__default.default.createElement("div", { className: "text-xs text-slate-500" }, "\u4E0B\u4E00\u4E2A"), /* @__PURE__ */ React__default.default.createElement("div", { className: "text-base font-semibold text-emerald-700" }, "M", nextLevel), /* @__PURE__ */ React__default.default.createElement("div", { className: "text-xs font-medium text-slate-600" }, statusText)));
}
function GameResultModal({ open, score, bestScore, onRestart }) {
  if (!open) {
    return null;
  }
  return /* @__PURE__ */ React__default.default.createElement("div", { className: "pointer-events-none absolute inset-0 flex items-center justify-center bg-slate-900/50 p-4" }, /* @__PURE__ */ React__default.default.createElement("div", { className: "pointer-events-auto w-full max-w-xs rounded-xl bg-white p-5 shadow-2xl" }, /* @__PURE__ */ React__default.default.createElement("h3", { className: "text-lg font-semibold text-slate-900" }, "\u6E38\u620F\u7ED3\u675F"), /* @__PURE__ */ React__default.default.createElement("p", { className: "mt-2 text-sm text-slate-600" }, "\u5F53\u524D\u5206\u6570\uFF1A", score), /* @__PURE__ */ React__default.default.createElement("p", { className: "text-sm text-slate-600" }, "\u6700\u9AD8\u5206\uFF1A", bestScore), /* @__PURE__ */ React__default.default.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,
  orbImageStorageKey = "sa2kit:mikuFusionGame:orbImages",
  enableImageConfigPanel = true,
  presetOrbImageMapping = {},
  ...options
}) {
  const containerRef = React.useRef(null);
  const [orbImageMapping, setOrbImageMapping] = React.useState({});
  const [isUserDefineMode] = React.useState(() => {
    if (typeof window === "undefined") {
      return false;
    }
    const value = new URLSearchParams(window.location.search).get("userDefine");
    return value === "true";
  });
  const activeOrbImageMapping = isUserDefineMode ? orbImageMapping : presetOrbImageMapping;
  const {
    canvasRef,
    status,
    score,
    bestScore,
    nextLevel,
    config,
    setAimFromClientX,
    handleDrop,
    togglePause,
    restart
  } = useMikuFusionGame({ ...options, storageKey, orbImageMapping: activeOrbImageMapping });
  const panelItems = React.useMemo(
    () => Array.from({ length: config.maxLevel }, (_, index) => ({
      id: String(index + 1),
      label: `M${index + 1}`
    })),
    [config.maxLevel]
  );
  const panelValue = React.useMemo(
    () => Object.entries(orbImageMapping).reduce((acc, [key, value]) => {
      if (value) {
        acc[String(key)] = value;
      }
      return acc;
    }, {}),
    [orbImageMapping]
  );
  React.useEffect(() => {
    if (typeof window === "undefined") {
      return;
    }
    try {
      const raw = window.localStorage.getItem(orbImageStorageKey);
      if (!raw) {
        setOrbImageMapping({});
        return;
      }
      const parsed = JSON.parse(raw);
      const normalized = {};
      Object.entries(parsed).forEach(([key, value]) => {
        const level = Number(key);
        if (!Number.isNaN(level) && typeof value === "string" && value) {
          normalized[level] = value;
        }
      });
      setOrbImageMapping(normalized);
    } catch {
      setOrbImageMapping({});
    }
  }, [orbImageStorageKey]);
  const { displayWidth, displayHeight } = useResponsiveCanvas(
    config.width,
    config.height,
    containerRef
  );
  const statusText = React.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();
    }
  };
  const handlePanelValueChange = (next) => {
    const normalized = {};
    Object.entries(next).forEach(([key, value]) => {
      const level = Number(key);
      if (!Number.isNaN(level) && value) {
        normalized[level] = value;
      }
    });
    setOrbImageMapping(normalized);
  };
  return /* @__PURE__ */ React__default.default.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__ */ React__default.default.createElement(GameHUD, { score, bestScore, nextLevel, statusText }),
    /* @__PURE__ */ React__default.default.createElement("div", { className: "relative mx-auto" }, /* @__PURE__ */ React__default.default.createElement(
      GameCanvas,
      {
        canvasRef,
        width: config.width,
        height: config.height,
        displayWidth,
        displayHeight,
        onPointerMove: handlePointerMove,
        onPointerDown: handlePointerDown,
        onPointerUp: handlePointerUp
      }
    ), /* @__PURE__ */ React__default.default.createElement(GameResultModal, { open: status === "gameOver", score, bestScore, onRestart: restart })),
    /* @__PURE__ */ React__default.default.createElement(
      GameControls,
      {
        isPaused: status === "paused",
        isPlaying: status === "playing" || status === "paused",
        onTogglePause: togglePause,
        onRestart: restart
      }
    ),
    enableImageConfigPanel && isUserDefineMode ? /* @__PURE__ */ React__default.default.createElement(
      LocalImageMappingPanel,
      {
        storageKey: orbImageStorageKey,
        title: "\u7403\u4F53\u56FE\u7247\u914D\u7F6E\uFF08\u4FDD\u5B58\u5230\u672C\u5730\uFF09",
        items: panelItems,
        defaultValue: panelValue,
        onValueChange: handlePanelValueChange
      }
    ) : null
  );
}

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

exports.BASE_RADIUS = BASE_RADIUS;
exports.DEFAULT_MIKU_FUSION_CONFIG = DEFAULT_MIKU_FUSION_CONFIG;
exports.LEVEL_LABEL_PREFIX = LEVEL_LABEL_PREFIX;
exports.MikuFusionGame = MikuFusionGame;
exports.RADIUS_STEP = RADIUS_STEP;
exports.defaultContentRegistry = defaultContentRegistry;
exports.useMikuFusionGame = useMikuFusionGame;
exports.useResponsiveCanvas = useResponsiveCanvas;
//# sourceMappingURL=index.js.map
//# sourceMappingURL=index.js.map