PokeRogue Cheat Menu

Cheat menu for PokeRogue

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         PokeRogue Cheat Menu
// @namespace    https://github.com/Eli-Zac/PokeRogue-Cheat-Menu
// @version      1.11
// @description  Cheat menu for PokeRogue
// @author       Eli_Zac
// @match        *://pokerogue.net/*
// @match        *://www.pokerogue.net/*
// @match        *://play.pokerogue.net/*
// @match        *://playpokerogue.com/*
// @match        *://www.playpokerogue.com/*
// @grant        unsafeWindow
// @grant        GM_download
// ==/UserScript==

(function () {
  'use strict';

  const uw = (typeof window !== 'undefined' && window.wrappedJSObject)
    ? window.wrappedJSObject
    : unsafeWindow;

  const TOGGLE_KEY = 'F2';
  let guiVisible = false;
  let activeSection = null;

  function getScriptVersion() {
    return globalThis.GM_info?.script?.version || 'unknown';
  }

  // ─── PHASER ACCESS ───────────────────────────────────────────────────────

  let _game = null;

  function isPhaserGameCandidate(v) {
    return Boolean(v?.scene && Array.isArray(v.scene.scenes) && v.renderer);
  }

  function hasGameDataScene(v) {
    return Boolean(v?.scene?.scenes?.some(sc => sc?.gameData));
  }

  function hookPhaserConstructor() {
    if (!uw.Phaser?.Game || uw.Phaser.Game.__prHooked) return;
    const Orig = uw.Phaser.Game;
    uw.Phaser.Game = function (...args) {
      const inst = new Orig(...args);
      _game = inst;
      return inst;
    };
    uw.Phaser.Game.prototype = Orig.prototype;
    uw.Phaser.Game.__prHooked = true;
    Object.keys(Orig).forEach(k => { try { uw.Phaser.Game[k] = Orig[k]; } catch (_) {} });
  }

  function getPhaserGame(forceRescan = false) {
    if (!forceRescan && isPhaserGameCandidate(_game)) return _game;

    let keys = [];
    try { keys = Object.getOwnPropertyNames(uw); } catch (_) {
      try { keys = Object.keys(uw); } catch (_) {}
    }

    let fallback = null;
    for (const k of keys) {
      try {
        const v = uw[k];
        if (!isPhaserGameCandidate(v)) continue;
        if (hasGameDataScene(v)) {
          _game = v;
          return _game;
        }
        if (!fallback && v.scene.scenes.length > 0) fallback = v;
      } catch (_) {}
    }

    if (fallback) {
      _game = fallback;
      return _game;
    }

    return null;
  }

  function findGameScene() {
    const cachedScene = _game?.scene?.scenes?.find(sc => sc?.gameData);
    if (cachedScene) return cachedScene;
    return getPhaserGame(Boolean(_game))?.scene?.scenes?.find(sc => sc?.gameData) ?? null;
  }

  function findGachaHandler() {
    const scene = findGameScene();
    if (!scene?.ui) return null;
    for (const k of Object.keys(scene.ui)) {
      try {
        const v = scene.ui[k];
        if (!v) continue;
        if (typeof v.updateVoucherCounts === 'function') return v;
        if (Array.isArray(v)) {
          for (const h of v) {
            if (h && typeof h.updateVoucherCounts === 'function') return h;
          }
        }
      } catch (_) {}
    }
    return null;
  }

  function findGameData() {
    if (uw.globalScene?.gameData) return uw.globalScene.gameData;
    return findGameScene()?.gameData ?? null;
  }

  if (uw.Phaser) hookPhaserConstructor();
  else {
    let attempts = 0;
    const iv = setInterval(() => {
      if (uw.Phaser) { hookPhaserConstructor(); clearInterval(iv); }
      if (++attempts > 100) clearInterval(iv);
    }, 100);
  }

  // ─── VOUCHER HACKS ───────────────────────────────────────────────────────

  function getVoucherCounts() {
    const gd = findGameData();
    if (!gd?.voucherCounts) return null;
    return [0, 1, 2, 3].map(i => gd.voucherCounts[i] ?? 0);
  }

  function setVoucherCounts(values) {
    const gd = findGameData();
    if (!gd?.voucherCounts) return false;
    const vals = Array.isArray(values) ? values : [values, values, values, values];
    for (let i = 0; i < 4; i++) gd.voucherCounts[i] = Math.max(0, Math.floor(vals[i]));
    const handler = findGachaHandler();
    if (handler && typeof handler.updateVoucherCounts === 'function') handler.updateVoucherCounts();
    return true;
  }

  // ─── GOLD / CURRENCY HACKS ─────────────────────────────────────────────

  const GOLD_KEY_CANDIDATES = ['gold', 'money', 'coins', 'coin', 'currency', 'cash'];
  const GOLD_GETTER_METHODS = ['getGold', 'getMoney', 'getCoins', 'getCurrency', 'getCash'];
  const GOLD_SETTER_METHODS = ['setGold', 'setMoney', 'setCoins', 'setCurrency', 'setCash', 'updateMoney'];
  const GOLD_ADDER_METHODS = ['addGold', 'addMoney', 'addCoins', 'addCurrency', 'addCash', 'gainMoney'];
  const CURRENCY_KEY_REGEX = /(gold|money|coin|currency|cash|fund)/i;

  function isNumberLike(v) {
    return typeof v === 'number' || typeof v === 'bigint';
  }

  function findCurrencyOnObject(obj) {
    if (!obj || typeof obj !== 'object') return null;

    for (const key of GOLD_KEY_CANDIDATES) {
      try {
        if (isNumberLike(obj[key])) return { holder: obj, key };
      } catch (_) {}
    }

    const keys = Object.keys(obj);
    for (const key of keys) {
      if (!CURRENCY_KEY_REGEX.test(key)) continue;
      try {
        const v = obj[key];
        if (isNumberLike(v)) return { holder: obj, key };
        if (v && typeof v === 'object') {
          if (isNumberLike(v.value)) return { holder: v, key: 'value' };
          if (isNumberLike(v.amount)) return { holder: v, key: 'amount' };
          if (isNumberLike(v.current)) return { holder: v, key: 'current' };
        }
      } catch (_) {}
    }

    return null;
  }

  function findCurrencyDeep(root, maxDepth = 4) {
    if (!root || typeof root !== 'object') return null;

    const visited = new WeakSet();
    const queue = [{ node: root, depth: 0 }];

    while (queue.length > 0) {
      const current = queue.shift();
      const node = current.node;
      const depth = current.depth;

      if (!node || typeof node !== 'object') continue;
      if (visited.has(node)) continue;
      visited.add(node);

      const direct = findCurrencyOnObject(node);
      if (direct) return direct;

      if (depth >= maxDepth) continue;

      let keys = [];
      try {
        keys = Object.keys(node);
      } catch (_) {
        continue;
      }

      for (const key of keys) {
        let child;
        try {
          child = node[key];
        } catch (_) {
          continue;
        }
        if (!child || typeof child !== 'object') continue;
        if (Array.isArray(child) && child.length > 100) continue;
        queue.push({ node: child, depth: depth + 1 });
      }
    }

    return null;
  }

  function callFirstNumberGetter(target) {
    if (!target) return null;
    for (const name of GOLD_GETTER_METHODS) {
      const fn = target[name];
      if (typeof fn !== 'function') continue;
      try {
        const v = fn.call(target);
        if (typeof v === 'number') return v;
        if (typeof v === 'bigint') return Number(v);
      } catch (_) {}
    }
    return null;
  }

  function callFirstSetter(target, next) {
    if (!target) return false;
    for (const name of GOLD_SETTER_METHODS) {
      const fn = target[name];
      if (typeof fn !== 'function') continue;
      try {
        fn.call(target, next);
        return true;
      } catch (_) {}
    }
    return false;
  }

  function callFirstAdder(target, delta) {
    if (!target) return false;
    for (const name of GOLD_ADDER_METHODS) {
      const fn = target[name];
      if (typeof fn !== 'function') continue;
      try {
        fn.call(target, delta);
        return true;
      } catch (_) {}
    }
    return false;
  }

  function findCurrencyField() {
    const gd = findGameData();
    const scene = findGameScene();

    return findCurrencyOnObject(gd)
      || findCurrencyOnObject(gd?.sessionData)
      || findCurrencyOnObject(scene)
      || findCurrencyDeep(gd)
      || findCurrencyDeep(scene)
      || null;
  }

  function getGoldAmount() {
    const ref = findCurrencyField();
    if (!ref) return null;
    try {
      const raw = ref.holder[ref.key];
      if (typeof raw === 'bigint') return Number(raw);
      if (typeof raw === 'number') return raw;
    } catch (_) {}

    const gd = findGameData();
    const scene = findGameScene();
    return callFirstNumberGetter(gd) ?? callFirstNumberGetter(scene) ?? null;

  }

  function setGoldAmount(amount) {
    const next = Math.max(0, Math.floor(Number(amount) || 0));
    const gd = findGameData();
    const scene = findGameScene();

    // Try game setter methods first — these notify the UI
    if (callFirstSetter(gd, next)) return true;
    if (callFirstSetter(scene, next)) return true;

    // Try adder with delta — also notifies the UI
    const current = getGoldAmount();
    if (current !== null) {
      const delta = next - current;
      if (callFirstAdder(gd, delta)) return true;
      if (callFirstAdder(scene, delta)) return true;
    }

    // Last resort: raw field write (data only, no UI refresh)
    const ref = findCurrencyField();
    if (ref) {
      try {
        ref.holder[ref.key] = typeof ref.holder[ref.key] === 'bigint' ? BigInt(next) : next;
        return true;
      } catch (_) {}
    }

    return false;
  }

  function addGoldAmount(delta) {
    const amount = Math.floor(Number(delta) || 0);
    const gd = findGameData();
    const scene = findGameScene();

    // Try adder methods first — these notify the UI
    if (callFirstAdder(gd, amount)) return true;
    if (callFirstAdder(scene, amount)) return true;

    // Try setter with new value
    const current = getGoldAmount();
    if (current !== null) {
      const next = Math.max(0, current + amount);
      if (callFirstSetter(gd, next)) return true;
      if (callFirstSetter(scene, next)) return true;

      // Last resort: raw field write
      const ref = findCurrencyField();
      if (ref) {
        try {
          ref.holder[ref.key] = typeof ref.holder[ref.key] === 'bigint' ? BigInt(next) : next;
          return true;
        } catch (_) {}
      }
    }

    return false;
  }

  // ─── EGG LIMIT HACK ──────────────────────────────────────────────────────
  // Patches processInput on the gacha handler to temporarily swap a fake
  // eggs array (length=0) onto gameData for the duration of the synchronous
  // cap check only. Restored in finally{} before anything else runs.
  // The egg list screen uses a separate code path and is never affected.

  let _eggLimitRemoved = false;
  let _eggPatchInstalled = false;
  let _origProcessInput = null;
  let _patchedHandler = null;

  function installProcessInputPatch() {
    if (_eggPatchInstalled) return true;
    const handler = findGachaHandler();
    if (!handler || typeof handler.processInput !== 'function') return false;

    _origProcessInput = handler.processInput;
    _patchedHandler = handler;

    handler.processInput = function (button) {
      if (!_eggLimitRemoved) return _origProcessInput.call(this, button);

      const gd = findGameData();
      if (!gd) return _origProcessInput.call(this, button);

      const real = gd.eggs;
      const fake = new Proxy(real, {
        get(t, p) {
          if (p === 'length') return 0;
          const v = t[p];
          return typeof v === 'function' ? v.bind(t) : v;
        },
        set(t, p, v) { t[p] = v; return true; }
      });

      try {
        Object.defineProperty(gd, 'eggs', {
          get: () => fake, set: (v) => { gd._eggsReal = v; },
          configurable: true, enumerable: true,
        });
      } catch (_) { gd.eggs = fake; }

      let result;
      try {
        result = _origProcessInput.call(this, button);
      } finally {
        try {
          Object.defineProperty(gd, 'eggs', {
            value: real, writable: true, configurable: true, enumerable: true,
          });
        } catch (_) { gd.eggs = real; }
      }
      return result;
    };

    _eggPatchInstalled = true;
    return true;
  }

  function setEggLimitRemoved(enabled) {
    if (enabled && !_eggPatchInstalled) {
      if (!installProcessInputPatch()) return false;
    }
    _eggLimitRemoved = enabled;
    return true;
  }

  function getEggCount() {
    const gd = findGameData();
    return gd?.eggs?.length ?? null;
  }

  function isManaphyEggEntry(egg) {
    if (!egg || typeof egg !== 'object') return false;
    if (typeof egg.isManaphyEgg === 'function') {
      try {
        return !!egg.isManaphyEgg();
      } catch (_) {}
    }

    const species = egg.species;
    const hasLegacySpecies = species === 0 || species === null || species === undefined;
    return Number.isFinite(egg.id) && egg.id % 204 === 0 && (!Number.isFinite(egg.tier) || egg.tier === 0) && hasLegacySpecies;
  }

  function getEggTypeFromEntry(egg) {
    if (!egg || typeof egg !== 'object') return null;
    if (isManaphyEggEntry(egg)) return 4;
    const keys = ['gachaType', 'eggType', 'sourceType', 'type', 'tier'];
    for (const key of keys) {
      const v = egg[key];
      if (Number.isFinite(v)) return v;
    }
    return null;
  }

  const EGG_TYPE_OPTIONS = [
    { value: 0, label: 'Common' },
    { value: 1, label: 'Rare' },
    { value: 2, label: 'Epic' },
    { value: 3, label: 'Legendary' },
    { value: 4, label: 'Manaphy' },
  ];

  const EGG_SEED_LIMIT = 1073741824;
  let _manaphyEggIdCursor = null;

  function getEggTargetTier(typeId) {
    return typeId === 4 ? 0 : Math.max(0, Math.min(3, Number(typeId) || 0));
  }

  function getEggSourceType(typeId) {
    return typeId === 3 ? 1 : 0;
  }

  function getEggTargetHatchWaves(typeId) {
    switch (typeId) {
      case 1: return 25;
      case 2: return 50;
      case 3: return 100;
      case 4: return 50;
      default: return 10;
    }
  }

  function getEggFieldValueForType(key, typeId) {
    if (key === 'tier') return getEggTargetTier(typeId);
    if (key === 'sourceType') return getEggSourceType(typeId);

    if (key === 'gachaType') return getEggSourceType(typeId);
    if (key === 'eggType' || key === 'type') return getEggTargetTier(typeId);

    return typeId;
  }

  function makeManaphyEggId() {
    const maxId = EGG_SEED_LIMIT - 204;
    if (!Number.isFinite(_manaphyEggIdCursor) || _manaphyEggIdCursor < 204 || _manaphyEggIdCursor > maxId) {
      const seed = 204 + (Date.now() % maxId);
      _manaphyEggIdCursor = Math.min(maxId, Math.ceil(seed / 204) * 204);
    } else {
      _manaphyEggIdCursor += 204;
      if (_manaphyEggIdCursor > maxId) _manaphyEggIdCursor = 204;
    }
    return _manaphyEggIdCursor;
  }

  function normalizeAddedEggType(egg, typeId) {
    if (!egg || typeof egg !== 'object') return;

    ['gachaType', 'eggType', 'sourceType', 'type', 'tier'].forEach(key => {
      if (Object.prototype.hasOwnProperty.call(egg, key)) {
        egg[key] = getEggFieldValueForType(key, typeId);
      }
    });

    if (typeId === 4) {
      if (Object.prototype.hasOwnProperty.call(egg, 'id')) egg.id = makeManaphyEggId();
      if (Object.prototype.hasOwnProperty.call(egg, 'species')) egg.species = 0;
      if (Object.prototype.hasOwnProperty.call(egg, 'hatchWaves')) egg.hatchWaves = getEggTargetHatchWaves(typeId);
    }
  }

  function normalizeNewEggsInRange(gd, fromIndex, typeId) {
    const eggs = gd?.eggs;
    if (!Array.isArray(eggs)) return;
    const start = Math.max(0, Number(fromIndex) || 0);
    for (let i = start; i < eggs.length; i++) {
      normalizeAddedEggType(eggs[i], typeId);
    }
  }

  function snapshotEggState(gd) {
    return {
      eggPity: Array.isArray(gd?.eggPity) ? [...gd.eggPity] : null,
      unlockPity: Array.isArray(gd?.unlockPity) ? [...gd.unlockPity] : null,
      gameStats: gd?.gameStats ? {
        eggsPulled: gd.gameStats.eggsPulled,
        rareEggsPulled: gd.gameStats.rareEggsPulled,
        epicEggsPulled: gd.gameStats.epicEggsPulled,
        legendaryEggsPulled: gd.gameStats.legendaryEggsPulled,
        manaphyEggsPulled: gd.gameStats.manaphyEggsPulled,
      } : null,
    };
  }

  function restoreEggState(gd, snapshot) {
    if (!gd || !snapshot) return;
    if (snapshot.eggPity && Array.isArray(gd.eggPity)) gd.eggPity.splice(0, gd.eggPity.length, ...snapshot.eggPity);
    if (snapshot.unlockPity && Array.isArray(gd.unlockPity)) gd.unlockPity.splice(0, gd.unlockPity.length, ...snapshot.unlockPity);
    if (snapshot.gameStats && gd.gameStats) Object.assign(gd.gameStats, snapshot.gameStats);
  }

  function removeEggsByIds(gd, ids) {
    const eggs = gd?.eggs;
    if (!Array.isArray(eggs) || !ids?.size) return;
    for (let i = eggs.length - 1; i >= 0; i--) {
      if (ids.has(eggs[i]?.id)) eggs.splice(i, 1);
    }
  }

  function incrementManualEggStats(gd, typeId) {
    const stats = gd?.gameStats;
    if (!stats) return;
    if (typeof stats.eggsPulled === 'number') stats.eggsPulled += 1;
    if (typeId === 4) {
      if (typeof stats.manaphyEggsPulled === 'number') stats.manaphyEggsPulled += 1;
      return;
    }
    if (typeId === 1 && typeof stats.rareEggsPulled === 'number') stats.rareEggsPulled += 1;
    if (typeId === 2 && typeof stats.epicEggsPulled === 'number') stats.epicEggsPulled += 1;
    if (typeId === 3 && typeof stats.legendaryEggsPulled === 'number') stats.legendaryEggsPulled += 1;
  }

  function getEggConstructor(gd) {
    const existingEgg = gd?.eggs?.find(egg => egg?.constructor && egg.constructor !== Object);
    if (existingEgg?.constructor) return existingEgg.constructor;

    const handler = findGachaHandler();
    if (!handler || typeof handler.pullEggs !== 'function') return null;

    const snapshot = snapshotEggState(gd);
    const originalCursor = handler.gachaCursor;
    const originalGuaranteed = handler.getGuaranteedEggTierFromPullCount;
    let createdEggs = [];

    try {
      handler.gachaCursor = 0;
      if (typeof originalGuaranteed === 'function') handler.getGuaranteedEggTierFromPullCount = () => 1;
      createdEggs = handler.pullEggs(1) || [];
    } catch (_) {
      createdEggs = [];
    } finally {
      try { handler.gachaCursor = originalCursor; } catch (_) {}
      if (typeof originalGuaranteed === 'function') {
        try { handler.getGuaranteedEggTierFromPullCount = originalGuaranteed; } catch (_) {}
      }
    }

    const EggCtor = createdEggs[0]?.constructor && createdEggs[0].constructor !== Object
      ? createdEggs[0].constructor
      : null;

    const createdIds = new Set(createdEggs.map(egg => egg?.id).filter(id => id !== undefined));
    removeEggsByIds(gd, createdIds);
    restoreEggState(gd, snapshot);

    return EggCtor;
  }

  function tryAddEggWithConstructor(gd, typeId) {
    const EggCtor = getEggConstructor(gd);
    if (!EggCtor) return false;

    if (typeId === 4) {
      const options = {
        sourceType: getEggSourceType(typeId),
        tier: getEggTargetTier(typeId),
        hatchWaves: getEggTargetHatchWaves(typeId),
        id: makeManaphyEggId(),
      };

      let egg = null;
      try {
        egg = new EggCtor(options);
      } catch (_) {
        try {
          egg = new EggCtor({ scene: findGameScene(), ...options });
        } catch (_) {
          egg = null;
        }
      }

      if (!egg) return false;
      if (!Array.isArray(gd?.eggs)) return false;

      gd.eggs.push(egg);
      incrementManualEggStats(gd, typeId);
      return true;
    }

    const options = {
      pulled: true,
      sourceType: getEggSourceType(typeId),
      tier: getEggTargetTier(typeId),
    };

    if (typeId === 4) options.id = makeManaphyEggId();

    const before = gd?.eggs?.length ?? 0;
    try {
      new EggCtor(options);
    } catch (_) {
      try {
        new EggCtor({ scene: findGameScene(), ...options });
      } catch (_) {
        return false;
      }
    }

    const after = gd?.eggs?.length ?? 0;
    if (after > before) {
      normalizeNewEggsInRange(gd, before, typeId);
      return true;
    }

    return false;
  }

  function cloneEggTemplate(egg) {
    if (typeof structuredClone === 'function') return structuredClone(egg);
    return JSON.parse(JSON.stringify(egg));
  }

  function pushEggClone(gd, typeId) {
    const eggs = gd?.eggs;
    if (!Array.isArray(eggs) || eggs.length === 0) return false;

    const template = eggs.find(e => getEggTypeFromEntry(e) === typeId) ?? eggs[0];
    if (!template) return false;

    const clone = cloneEggTemplate(template);
    ['gachaType', 'eggType', 'sourceType', 'type', 'tier'].forEach(key => {
      if (Object.prototype.hasOwnProperty.call(clone, key)) {
        clone[key] = getEggFieldValueForType(key, typeId);
      }
    });

    if (Object.prototype.hasOwnProperty.call(clone, 'id')) {
      clone.id = typeId === 4 ? makeManaphyEggId() : Date.now() + '-' + Math.random().toString(36).slice(2);
    }

    if (typeId === 4) {
      clone.species = 0;
      clone.hatchWaves = getEggTargetHatchWaves(typeId);
    }

    eggs.push(clone);
    return true;
  }

  function tryAddEggWithGameMethod(gd, typeId) {
    const methodNames = [
      'addEgg',
      'addEggs',
      'createEgg',
      'createEggs',
      'generateEgg',
      'generateEggs',
      'giveEgg',
      'giveEggs',
    ];

    for (const name of methodNames) {
      const fn = gd?.[name];
      if (typeof fn !== 'function') continue;

      const argSets = [
        [typeId],
        [typeId, 1],
        [1, typeId],
        [{ type: typeId }],
        [{ gachaType: typeId }],
      ];

      for (const args of argSets) {
        const before = gd?.eggs?.length ?? 0;
        try {
          fn.apply(gd, args);
        } catch (_) {
          continue;
        }
        const after = gd?.eggs?.length ?? 0;
        if (after > before) {
          normalizeNewEggsInRange(gd, before, typeId);
          return true;
        }
      }
    }

    return false;
  }

  async function deleteAllEggs(onProgress) {
    const gd = findGameData();
    if (!gd || !Array.isArray(gd.eggs)) return { ok: false, deleted: 0, reason: 'connect' };

    const total = gd.eggs.length;
    if (total === 0) {
      await triggerAutosaveAfterEggChange();
      return { ok: true, deleted: 0 };
    }

    if (typeof onProgress === 'function') onProgress(0, total, 0);

    const batchSize = 50;
    let deleted = 0;

    while (gd.eggs.length > 0) {
      const remove = Math.min(batchSize, gd.eggs.length);
      gd.eggs.splice(gd.eggs.length - remove, remove);
      deleted += remove;

      if (typeof onProgress === 'function') {
        onProgress(deleted, total, Math.round((deleted / total) * 100));
      }

      if (gd.eggs.length > 0) await waitTick();
    }

    await triggerAutosaveAfterEggChange();
    return { ok: true, deleted };
  }

  function waitTick() {
    return new Promise(resolve => setTimeout(resolve, 0));
  }

  async function addEggsByType(typeId, amount, onProgress) {
    const gd = findGameData();
    if (!gd || !Array.isArray(gd.eggs)) return { ok: false, added: 0, reason: 'connect' };

    const target = Math.max(1, Math.floor(Number(amount) || 0));
    let added = 0;
    const batchSize = 10;

    if (typeof onProgress === 'function') onProgress(0, target, 0);

    for (let i = 0; i < target; i++) {
      if (tryAddEggWithConstructor(gd, typeId) || (typeId !== 4 && tryAddEggWithGameMethod(gd, typeId)) || pushEggClone(gd, typeId)) {
        added++;
      } else {
        break;
      }

      if (typeof onProgress === 'function') {
        const pct = Math.round((added / target) * 100);
        onProgress(added, target, pct);
      }

      if ((i + 1) % batchSize === 0) {
        await waitTick();
      }
    }

    return { ok: added === target, added };
  }

  async function triggerAutosaveAfterEggChange() {
    const scene = findGameScene();
    const gd = findGameData();

    const roots = [
      gd,
      scene,
      scene?.gameData,
      scene?.session,
      scene?.ui,
      uw.globalScene,
    ].filter(Boolean);

    const methodNames = [
      'saveSystem',
      'saveData',
      'saveGameData',
      'queueSave',
      'queueSaveData',
      'queueSystemSave',
      'requestSave',
      'updateSystem',
      'updateSystemData',
      'syncSystemData',
    ];

    const seen = new Set();
    let invoked = false;

    for (const root of roots) {
      for (const name of methodNames) {
        const fn = root?.[name];
        if (typeof fn !== 'function') continue;
        if (seen.has(fn)) continue;
        seen.add(fn);

        const argSets = [
          [],
          [true],
          [{ source: 'cheat-menu', reason: 'manual-eggs' }],
        ];

        for (const args of argSets) {
          try {
            const result = fn.apply(root, args);
            invoked = true;
            if (result && typeof result.then === 'function') {
              await Promise.race([
                result,
                new Promise(resolve => setTimeout(resolve, 350)),
              ]);
            }
            break;
          } catch (_) {}
        }
      }
    }

    return invoked;
  }

  // ─── POKEDEX HACKS ─────────────────────────────────────────────────────────

  // Bitmask covering NON_SHINY(1) | SHINY(2) | MALE(4) | FEMALE(8) |
  // DEFAULT_VARIANT(16) | VARIANT_2(32) | VARIANT_3(64) | DEFAULT_FORM(128)
  // plus generous headroom for extended form bits.
  const FULL_DEX_ATTR = (1n << 12n) - 1n; // 4095n
  // All 25 natures unlocked (bits 0-24)
  const FULL_NATURE_ATTR = (1 << 25) - 1;
  let _defaultDexSnapshot = null;

  function cloneScalarForSnapshot(value) {
    if (typeof value === 'bigint') return BigInt(value.toString());
    return value;
  }

  function captureDefaultDexSnapshot() {
    if (_defaultDexSnapshot) return true;
    const gd = findGameData();
    if (!gd?.dexData) return false;

    const snapshot = {};
    for (const key of Object.keys(gd.dexData)) {
      const entry = gd.dexData[key];
      if (!entry) continue;
      snapshot[key] = {
        caughtAttr: cloneScalarForSnapshot(entry.caughtAttr),
        seenAttr: cloneScalarForSnapshot(entry.seenAttr),
        caughtCount: cloneScalarForSnapshot(entry.caughtCount),
        seenCount: cloneScalarForSnapshot(entry.seenCount),
        natureAttr: cloneScalarForSnapshot(entry.natureAttr),
      };
    }

    _defaultDexSnapshot = snapshot;
    return true;
  }

  async function restoreDefaultPokemon(onProgress) {
    const gd = findGameData();
    if (!gd?.dexData) return { ok: false, updated: 0, reason: 'connect' };
    if (!_defaultDexSnapshot && !captureDefaultDexSnapshot()) {
      return { ok: false, updated: 0, reason: 'snapshot' };
    }

    const keys = Object.keys(gd.dexData);
    const total = keys.length;
    let updated = 0;

    if (typeof onProgress === 'function') onProgress(0, total, 0);

    for (let i = 0; i < keys.length; i++) {
      const key = keys[i];
      const entry = gd.dexData[key];
      const snap = _defaultDexSnapshot?.[key];
      if (!entry || !snap) continue;

      entry.caughtAttr = cloneScalarForSnapshot(snap.caughtAttr);
      entry.seenAttr = cloneScalarForSnapshot(snap.seenAttr);
      entry.caughtCount = cloneScalarForSnapshot(snap.caughtCount);
      entry.seenCount = cloneScalarForSnapshot(snap.seenCount);
      entry.natureAttr = cloneScalarForSnapshot(snap.natureAttr);
      updated++;

      if (typeof onProgress === 'function' && i % 50 === 0) {
        onProgress(updated, total, Math.round((updated / total) * 100));
        await waitTick();
      }
    }

    await triggerAutosaveAfterEggChange();
    return { ok: true, updated };
  }

  function getDexStats() {
    const gd = findGameData();
    if (!gd?.dexData) return null;
    const keys = Object.keys(gd.dexData);
    let caught = 0;
    for (const k of keys) {
      if (gd.dexData[k]?.caughtAttr) caught++;
    }
    return { total: keys.length, caught };
  }

  async function catchAllPokemon(onProgress) {
    const gd = findGameData();
    if (!gd?.dexData) return { ok: false, updated: 0, reason: 'connect' };

    const keys = Object.keys(gd.dexData);
    const total = keys.length;
    let updated = 0;

    if (typeof onProgress === 'function') onProgress(0, total, 0);

    for (let i = 0; i < keys.length; i++) {
      const entry = gd.dexData[keys[i]];
      if (!entry) continue;

      // Mark as fully caught & seen
      if (typeof entry.caughtAttr === 'bigint' || entry.caughtAttr === undefined) {
        entry.caughtAttr = FULL_DEX_ATTR;
      }
      if (typeof entry.seenAttr === 'bigint' || entry.seenAttr === undefined) {
        entry.seenAttr = FULL_DEX_ATTR;
      }

      // Bump counts
      if (typeof entry.caughtCount === 'number') entry.caughtCount = Math.max(1, entry.caughtCount);
      else entry.caughtCount = 1;
      if (typeof entry.seenCount === 'number') entry.seenCount = Math.max(1, entry.seenCount);
      else entry.seenCount = 1;

      // Unlock all natures
      if (typeof entry.natureAttr === 'number' || entry.natureAttr === undefined) {
        entry.natureAttr = FULL_NATURE_ATTR;
      }

      updated++;

      if (typeof onProgress === 'function' && i % 50 === 0) {
        onProgress(updated, total, Math.round((updated / total) * 100));
        await waitTick();
      }
    }

    // Try to persist via game save methods
    await triggerAutosaveAfterEggChange();

    return { ok: true, updated };
  }

  async function resetCaughtPokemon(onProgress) {
    const gd = findGameData();
    if (!gd?.dexData) return { ok: false, updated: 0, reason: 'connect' };

    const keys = Object.keys(gd.dexData);
    const total = keys.length;
    let updated = 0;

    if (typeof onProgress === 'function') onProgress(0, total, 0);

    for (let i = 0; i < keys.length; i++) {
      const entry = gd.dexData[keys[i]];
      if (!entry) continue;

      if (typeof entry.caughtAttr === 'bigint' || entry.caughtAttr === undefined) {
        entry.caughtAttr = 0n;
      } else {
        entry.caughtAttr = 0;
      }

      if (typeof entry.caughtCount === 'number') entry.caughtCount = 0;
      else entry.caughtCount = 0;

      updated++;

      if (typeof onProgress === 'function' && i % 50 === 0) {
        onProgress(updated, total, Math.round((updated / total) * 100));
        await waitTick();
      }
    }

    await triggerAutosaveAfterEggChange();

    return { ok: true, updated };
  }

  // ─── GAME STATS ──────────────────────────────────────────────────────────

  const STAT_LABELS = {
    playTime:                  'Play Time',
    battles:                   'Battles',
    classicSessionsPlayed:     'Classic Sessions Played',
    endlessSessionsPlayed:     'Endless Sessions Played',
    dailyRunSessionsPlayed:    'Daily Run Sessions Played',
    pokemonSeen:               'Pokémon Seen',
    pokemonCaught:             'Pokémon Caught',
    pokemonHatched:            'Pokémon Hatched',
    pokemonFused:              'Pokémon Fused',
    pokemonDefeated:           'Pokémon Defeated',
    shinyCaught:               'Shinies Caught',
    shinyHatched:              'Shinies Hatched',
    trainersDefeated:          'Trainers Defeated',
    ribbonsEarned:             'Ribbons Earned',
    eggsPulled:                'Eggs Pulled',
    rareEggsPulled:            'Rare Eggs Pulled',
    epicEggsPulled:            'Epic Eggs Pulled',
    legendaryEggsPulled:       'Legendary Eggs Pulled',
    manaphyEggsPulled:         'Manaphy Eggs Pulled',
    subLegendaryEggsPulled:    'Sub-Legendary Eggs Pulled',
    gamesBeat:                 'Games Beat',
  };

  function formatStatKey(key) {
    if (STAT_LABELS[key]) return STAT_LABELS[key];
    return key.replace(/([A-Z])/g, ' $1').replace(/^./, s => s.toUpperCase());
  }

  function getGameStats() {
    const gd = findGameData();
    if (!gd?.gameStats) return null;
    const stats = {};
    for (const [k, v] of Object.entries(gd.gameStats)) {
      if (typeof v === 'number') stats[k] = v;
    }
    return Object.keys(stats).length ? stats : null;
  }

  function formatPlayTime(seconds) {
    const total = Math.max(0, Math.floor(Number(seconds) || 0));
    const d = Math.floor(total / 86400);
    const h = Math.floor((total % 86400) / 3600);
    const m = Math.floor((total % 3600) / 60);
    const s = total % 60;
    return d + 'd ' + h + 'h ' + m + 'm ' + s + 's';
  }

  function formatGameStatValue(key, value) {
    if (key === 'playTime') return formatPlayTime(value);
    return value.toLocaleString();
  }

  // ─── SAVE DATA ───────────────────────────────────────────────────────────

  // Keys that represent persistent game progress on gameData
  const SAVE_DATA_KEYS = [
    'trainerId', 'secretId', 'gender', 'playTime', 'gameVersion', 'timestamp',
    'dexData', 'starterData', 'eggs', 'eggPity', 'unlockPity',
    'voucherCounts', 'vouchersUnlocked', 'achvUnlocks', 'unlocks',
    'gameStats', 'sessionData',
  ];

  const DOWNLOAD_BRIDGE_REQUEST = 'PRCM_EXPORT_DOWNLOAD_REQUEST';
  const DOWNLOAD_BRIDGE_RESPONSE = 'PRCM_EXPORT_DOWNLOAD_RESPONSE';

  function safeCloneForSave(value, seen = new WeakSet()) {
    if (value === null) return null;

    const t = typeof value;
    if (t === 'string' || t === 'number' || t === 'boolean') return value;
    if (t === 'bigint') return { __type: 'BigInt', __value: value.toString() };
    if (t === 'undefined' || t === 'function' || t === 'symbol') return undefined;

    if (seen.has(value)) return undefined;
    seen.add(value);

    if (Array.isArray(value)) {
      const out = [];
      for (let i = 0; i < value.length; i++) {
        let entry;
        try {
          entry = value[i];
        } catch (_) {
          out.push(null);
          continue;
        }
        const cloned = safeCloneForSave(entry, seen);
        out.push(cloned === undefined ? null : cloned);
      }
      return out;
    }

    const out = {};
    for (const key of Object.keys(value)) {
      let entry;
      try {
        entry = value[key];
      } catch (_) {
        continue;
      }
      const cloned = safeCloneForSave(entry, seen);
      if (cloned !== undefined) out[key] = cloned;
    }
    return out;
  }

  function buildExportPayload(gd) {
    const payload = {};
    for (const key of SAVE_DATA_KEYS) {
      let raw;
      try {
        raw = gd[key];
      } catch (_) {
        continue;
      }
      const cloned = safeCloneForSave(raw);
      if (cloned !== undefined) payload[key] = cloned;
    }
    payload.__meta = {
      source: 'PokeRogue Cheat Menu',
      version: getScriptVersion(),
      exportedAt: new Date().toISOString(),
    };
    return payload;
  }

  function isTrustedBridgeOrigin(origin) {
    if (!origin || origin === 'null') return false;
    try {
      const host = new URL(origin).hostname.toLowerCase();
      return /(^|\.)pokerogue\.net$/.test(host) || /(^|\.)playpokerogue\.com$/.test(host);
    } catch (_) {
      return false;
    }
  }

  function resolveDownloadFilename(rawName) {
    if (typeof rawName !== 'string' || rawName.trim() === '') {
      const ts = new Date().toISOString().replace(/[T:]/g, '-').replace(/\..+/, '');
      return 'pokerogue-save-' + ts + '.json';
    }
    return rawName;
  }

  function downloadWithAnchor(blob, filename) {
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = filename;
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    setTimeout(() => URL.revokeObjectURL(url), 2000);
  }

  async function downloadWithFilePicker(jsonText, filename) {
    if (typeof window.showSaveFilePicker !== 'function') {
      throw new Error('showSaveFilePicker unavailable');
    }

    const handle = await window.showSaveFilePicker({
      suggestedName: filename,
      types: [{
        description: 'JSON Files',
        accept: { 'application/json': ['.json'] },
      }],
    });
    const writable = await handle.createWritable();
    try {
      await writable.write(jsonText);
    } finally {
      await writable.close();
    }
  }

  function downloadWithGM(blob, filename) {
    return new Promise((resolve, reject) => {
      if (typeof GM_download !== 'function') {
        reject(new Error('GM_download unavailable'));
        return;
      }

      const url = URL.createObjectURL(blob);
      const cleanup = () => setTimeout(() => URL.revokeObjectURL(url), 2000);

      try {
        GM_download({
          url,
          name: filename,
          saveAs: true,
          onload: () => {
            cleanup();
            resolve(true);
          },
          onerror: (err) => {
            cleanup();
            reject(new Error(err?.error || err?.details || 'GM_download failed'));
          },
          ontimeout: () => {
            cleanup();
            reject(new Error('GM_download timed out'));
          },
        });
      } catch (e) {
        cleanup();
        reject(e);
      }
    });
  }

  function requestParentDownload(jsonText, filename) {
    return new Promise((resolve) => {
      if (window.top === window.self) {
        resolve(false);
        return;
      }

      const requestId = 'prcm-' + Date.now() + '-' + Math.random().toString(36).slice(2);
      let finished = false;

      const onMessage = (event) => {
        const data = event?.data;
        if (!data || data.type !== DOWNLOAD_BRIDGE_RESPONSE || data.requestId !== requestId) return;
        if (!isTrustedBridgeOrigin(event.origin)) return;
        finished = true;
        window.removeEventListener('message', onMessage);
        resolve(Boolean(data.ok));
      };

      window.addEventListener('message', onMessage);

      try {
        window.top.postMessage({
          type: DOWNLOAD_BRIDGE_REQUEST,
          requestId,
          filename,
          payload: jsonText,
        }, '*');
      } catch (_) {
        window.removeEventListener('message', onMessage);
        resolve(false);
        return;
      }

      setTimeout(() => {
        if (finished) return;
        window.removeEventListener('message', onMessage);
        resolve(false);
      }, 2000);
    });
  }

  async function triggerSaveDownload(jsonText, filename) {
    const safeName = resolveDownloadFilename(filename);
    const blob = new Blob([jsonText], { type: 'application/json' });

    // Keep this first and as close to button click as possible to preserve user activation.
    try {
      await downloadWithFilePicker(jsonText, safeName);
      return { ok: true, mode: 'picker' };
    } catch (_) {}

    try {
      downloadWithAnchor(blob, safeName);
      return { ok: true, mode: 'anchor' };
    } catch (_) {}

    // Embedded contexts can block direct blob downloads; ask the top frame first.
    if (window.top !== window.self) {
      try {
        const bridged = await requestParentDownload(jsonText, safeName);
        if (bridged) return { ok: true, mode: 'bridge' };
      } catch (_) {}
    }

    try {
      await downloadWithGM(blob, safeName);
      return { ok: true, mode: 'gm' };
    } catch (_) {}

    try {
      if (navigator.clipboard?.writeText) {
        await navigator.clipboard.writeText(jsonText);
        return { ok: true, mode: 'clipboard' };
      }
    } catch (_) {}

    throw new Error('All export download methods failed');
  }

  window.addEventListener('message', (event) => {
    const data = event?.data;
    if (!data || data.type !== DOWNLOAD_BRIDGE_REQUEST) return;
    if (!isTrustedBridgeOrigin(event.origin)) return;
    if (window.top !== window.self) return;

    const requestId = data.requestId;
    const payload = data.payload;
    const filename = resolveDownloadFilename(data.filename);
    let ok = false;

    try {
      if (typeof payload !== 'string' || payload.length === 0) throw new Error('empty payload');
      downloadWithAnchor(new Blob([payload], { type: 'application/json' }), filename);
      ok = true;
    } catch (_) {
      ok = false;
    }

    try {
      event.source?.postMessage({
        type: DOWNLOAD_BRIDGE_RESPONSE,
        requestId,
        ok,
      }, event.origin || '*');
    } catch (_) {}
  });

  async function exportSaveData(onProgress) {
    const gd = findGameData();
    if (!gd) return { ok: false, reason: 'connect' };

    if (typeof onProgress === 'function') onProgress('Collecting data\u2026', 10);

    let json;
    try {
      if (typeof onProgress === 'function') onProgress('Serializing\u2026', 35);
      const payload = buildExportPayload(gd);
      json = JSON.stringify(payload, null, 2);
    } catch (e) {
      return { ok: false, reason: 'serialize', error: e.message };
    }

    try {
      if (typeof onProgress === 'function') onProgress('Packaging file\u2026', 70);
      const kb = Math.round(json.length / 1024);
      const ts = new Date().toISOString().replace(/[T:]/g, '-').replace(/\..+/, '');
      const filename = 'pokerogue-save-' + ts + '.json';
      if (typeof onProgress === 'function') onProgress('Downloading\u2026', 90);
      const dl = await triggerSaveDownload(json, filename);
      if (typeof onProgress === 'function') onProgress('Done!', 100);
      return { ok: true, kb, mode: dl.mode };
    } catch (e) {
      return { ok: false, reason: 'download', error: e.message };
    }
  }

  async function importSaveData(jsonText, onProgress) {
    const gd = findGameData();
    if (!gd) return { ok: false, reason: 'connect' };

    let data;
    try {
      data = JSON.parse(jsonText, function (key, value) {
        if (value && typeof value === 'object' && value.__type === 'BigInt' && value.__value !== undefined) {
          return BigInt(value.__value);
        }
        return value;
      });
    } catch (e) {
      return { ok: false, reason: 'syntax', error: e.message };
    }

    if (typeof data !== 'object' || data === null) {
      return { ok: false, reason: 'parse', error: 'Invalid save file structure' };
    }

    // Sanity-check: require at least one known PokeRogue key
    const VALIDATION_KEYS = ['dexData', 'trainerId', 'starterData', 'gameStats', 'voucherCounts'];
    if (!VALIDATION_KEYS.some(k => k in data)) {
      return { ok: false, reason: 'invalid', error: 'File does not appear to be a PokeRogue save (missing expected fields)' };
    }

    let restored = 0;

    // Priority: restore known persistent keys first, then any remaining data keys
    const importKeys = [...new Set([
      ...SAVE_DATA_KEYS.filter(k => k in data),
      ...Object.keys(data),
    ])];
    const total = importKeys.length;

    for (let i = 0; i < importKeys.length; i++) {
      try {
        const val = data[importKeys[i]];
        if (typeof val !== 'function') {
          gd[importKeys[i]] = val;
          restored++;
        }
      } catch (_) {}

      if (typeof onProgress === 'function' && i % 5 === 0) {
        onProgress(restored, total, Math.round(((i + 1) / total) * 100));
        await waitTick();
      }
    }

    if (typeof onProgress === 'function') onProgress(restored, total, 100);
    await triggerAutosaveAfterEggChange();
    return { ok: true, restored };
  }

  // ─── TOAST ───────────────────────────────────────────────────────────────

  function showToast(message, isError = false) {
    const ex = document.getElementById('pr-toast');
    if (ex) ex.remove();
    const t = document.createElement('div');
    t.id = 'pr-toast';
    t.textContent = message;
    Object.assign(t.style, {
      position: 'fixed', bottom: '80px', right: '24px',
      background: isError ? '#3d0f0f' : '#0d2e1a',
      color: isError ? '#fca5a5' : '#86efac',
      border: '1px solid ' + (isError ? '#dc2626' : '#22c55e'),
      borderRadius: '8px', padding: '11px 16px',
      fontFamily: "'Share Tech Mono', monospace", fontSize: '13px',
      zIndex: '999999', opacity: '0', transition: 'opacity 0.2s',
      pointerEvents: 'none', maxWidth: '320px', lineHeight: '1.4',
    });
    document.body.appendChild(t);
    requestAnimationFrame(() => { t.style.opacity = '1'; });
    setTimeout(() => { t.style.opacity = '0'; setTimeout(() => t.remove(), 300); }, 3000);
  }

  // ─── SECTIONS ────────────────────────────────────────────────────────────

  const SECTIONS = [
    {
      id: 'eggs',
      icon: '🥚',
      label: 'Eggs',
      description: 'Vouchers, pull limits & egg storage',
      buildContent(container) {
        container.innerHTML = `
          <div class="pr-card">
            <div class="pr-card-header">
              <div class="pr-card-title">Voucher Counts</div>
              <div class="pr-card-desc">Set how many of each voucher type you have</div>
            </div>
            <div class="pr-voucher-grid">
              <div class="pr-voucher-card">
                <span class="pr-voucher-tier">Regular</span>
                <span class="pr-voucher-val" id="pv-cur-0">—</span>
                <input class="pr-input pr-voucher-input" id="pv-in-0" type="number" min="0" max="9999" placeholder="new value">
              </div>
              <div class="pr-voucher-card">
                <span class="pr-voucher-tier">Plus</span>
                <span class="pr-voucher-val" id="pv-cur-1">—</span>
                <input class="pr-input pr-voucher-input" id="pv-in-1" type="number" min="0" max="9999" placeholder="new value">
              </div>
              <div class="pr-voucher-card">
                <span class="pr-voucher-tier">Premium</span>
                <span class="pr-voucher-val" id="pv-cur-2">—</span>
                <input class="pr-input pr-voucher-input" id="pv-in-2" type="number" min="0" max="9999" placeholder="new value">
              </div>
              <div class="pr-voucher-card">
                <span class="pr-voucher-tier gold">✦ Golden</span>
                <span class="pr-voucher-val gold" id="pv-cur-3">—</span>
                <input class="pr-input pr-voucher-input" id="pv-in-3" type="number" min="0" max="9999" placeholder="new value">
              </div>
            </div>
            <div class="pr-btn-row">
              <button class="pr-btn green" id="pr-apply">✓ Apply</button>
              <button class="pr-btn purple" id="pr-max">▲ Set 999</button>
              <button class="pr-btn red" id="pr-zero">▼ Set 0</button>
            </div>
          </div>

          <div class="pr-card">
            <div class="pr-toggle-row" id="pr-egg-limit-row">
              <div class="pr-toggle-info">
                <div class="pr-toggle-name">Remove Egg Limit</div>
                <div class="pr-toggle-status" id="pr-egg-count-display">—</div>
              </div>
              <label class="pr-toggle">
                <input type="checkbox" id="pr-egg-limit-toggle">
                <span class="pr-slider"></span>
              </label>
            </div>
          </div>

          <div class="pr-card">
            <div class="pr-card-header">
              <div class="pr-card-title">Give Eggs by Type</div>
              <div class="pr-card-desc">Choose from fixed egg types, add up to 500 at once, or clear your current egg storage</div>
            </div>
            <div class="pr-egg-tools-grid">
              <select class="pr-input" id="pr-egg-type-select"></select>
              <input class="pr-input" id="pr-egg-amount" type="number" min="1" max="500" value="1" placeholder="amount">
            </div>
            <div class="pr-egg-btn-row pr-egg-btn-row-double">
              <button class="pr-btn green" id="pr-egg-add">+ Add Eggs</button>
              <button class="pr-btn red" id="pr-egg-delete-all">🗑 Delete All Eggs</button>
            </div>
          </div>
        `;

        const applyVouchers = () => {
          const counts = getVoucherCounts();
          if (!counts) { showToast('⚠️ Open the Gacha menu first', true); return; }
          const vals = [0, 1, 2, 3].map(i => {
            const raw = container.querySelector('#pv-in-' + i).value;
            return raw === '' ? counts[i] : Math.max(0, parseInt(raw, 10) || 0);
          });
          if (setVoucherCounts(vals)) {
            showToast('✅ Vouchers updated!');
            [0, 1, 2, 3].forEach(i => { container.querySelector('#pv-in-' + i).value = ''; });
          }
        };

        container.querySelector('#pr-apply').addEventListener('click', applyVouchers);
        container.querySelector('#pr-max').addEventListener('click', () => {
          if (setVoucherCounts(999)) showToast('✅ All vouchers set to 999');
          else showToast('⚠️ Open the Gacha menu first', true);
        });
        container.querySelector('#pr-zero').addEventListener('click', () => {
          if (setVoucherCounts(0)) showToast('✅ All vouchers set to 0');
          else showToast('⚠️ Open the Gacha menu first', true);
        });

        container.querySelectorAll('.pr-voucher-input').forEach(inp => {
          inp.addEventListener('keydown', e => {
            if (e.key === 'Enter') applyVouchers();
            e.stopPropagation();
          });
        });

        const limitToggle = container.querySelector('#pr-egg-limit-toggle');
        const limitRow = container.querySelector('#pr-egg-limit-row');
        limitToggle.checked = _eggLimitRemoved;
        if (_eggLimitRemoved) limitRow.classList.add('active');

        limitToggle.addEventListener('change', () => {
          const ok = setEggLimitRemoved(limitToggle.checked);
          if (ok) {
            limitRow.classList.toggle('active', limitToggle.checked);
            showToast(limitToggle.checked ? '✅ Egg limit removed!' : '🔒 Egg limit restored');
          } else {
            limitToggle.checked = false;
            showToast('⚠️ Open the Gacha menu first', true);
          }
        });

        const eggTypeSelect = container.querySelector('#pr-egg-type-select');
        const eggAmountInput = container.querySelector('#pr-egg-amount');
        const eggAddBtn = container.querySelector('#pr-egg-add');
        const eggDeleteAllBtn = container.querySelector('#pr-egg-delete-all');
        let eggAddBusy = false;
        let eggDeleteBusy = false;
        eggTypeSelect.innerHTML = EGG_TYPE_OPTIONS
          .map(o => '<option value="' + o.value + '">' + o.label + '</option>')
          .join('');

        async function applyEggAdd() {
          if (eggAddBusy || eggDeleteBusy) return;
          const typeId = Number(eggTypeSelect.value);
          const amount = Math.min(500, Math.max(1, parseInt(eggAmountInput.value, 10) || 1));
          eggAmountInput.value = String(amount);

          if (!Number.isFinite(typeId)) {
            showToast('⚠️ Select a valid egg type', true);
            return;
          }

          eggAddBusy = true;
          const originalText = eggAddBtn.textContent;
          eggAddBtn.disabled = true;
          eggDeleteAllBtn.disabled = true;
          eggAddBtn.classList.add('pr-loading');
          eggTypeSelect.disabled = true;
          eggAmountInput.disabled = true;

          try {
            const result = await addEggsByType(typeId, amount, (_done, _total, pct) => {
              eggAddBtn.textContent = 'Adding… ' + pct + '%';
            });

            if (result.added > 0) {
              const partial = result.ok ? '' : ' (partial)';
              const typeLabel = EGG_TYPE_OPTIONS.find(o => o.value === typeId)?.label ?? ('Type ' + typeId);
              showToast('✅ Added ' + result.added + ' ' + typeLabel + ' egg(s)' + partial);
              await triggerAutosaveAfterEggChange();
            } else {
              showToast('⚠️ Could not add eggs.', true);
            }
          } finally {
            eggAddBtn.textContent = originalText;
            eggAddBtn.disabled = false;
            eggDeleteAllBtn.disabled = false;
            eggAddBtn.classList.remove('pr-loading');
            eggTypeSelect.disabled = false;
            eggAmountInput.disabled = false;
            eggAddBusy = false;
          }
        }

        async function applyDeleteAllEggs() {
          if (eggAddBusy || eggDeleteBusy) return;

          eggDeleteBusy = true;
          const originalText = eggDeleteAllBtn.textContent;
          eggDeleteAllBtn.disabled = true;
          eggAddBtn.disabled = true;
          eggDeleteAllBtn.classList.add('pr-loading');
          eggTypeSelect.disabled = true;
          eggAmountInput.disabled = true;

          try {
            const result = await deleteAllEggs((_done, _total, pct) => {
              eggDeleteAllBtn.textContent = 'Deleting… ' + pct + '%';
            });
            if (result.ok) {
              showToast('✅ Deleted ' + result.deleted + ' egg(s)');
            } else {
              showToast('⚠️ Could not delete eggs.', true);
            }
          } finally {
            eggDeleteAllBtn.textContent = originalText;
            eggDeleteAllBtn.disabled = false;
            eggAddBtn.disabled = false;
            eggDeleteAllBtn.classList.remove('pr-loading');
            eggTypeSelect.disabled = false;
            eggAmountInput.disabled = false;
            eggDeleteBusy = false;
          }
        }

        eggAddBtn.addEventListener('click', applyEggAdd);
        eggDeleteAllBtn.addEventListener('click', applyDeleteAllEggs);
        eggAmountInput.addEventListener('input', () => {
          const raw = eggAmountInput.value;
          if (raw === '') return;
          const n = parseInt(raw, 10);
          if (!Number.isFinite(n)) {
            eggAmountInput.value = '1';
            return;
          }
          eggAmountInput.value = String(Math.min(500, Math.max(1, n)));
        });
        eggAmountInput.addEventListener('keydown', e => {
          if (e.key === 'Enter') applyEggAdd();
          e.stopPropagation();
        });

        const curEls = [0, 1, 2, 3].map(i => container.querySelector('#pv-cur-' + i));
        const eggCountEl = container.querySelector('#pr-egg-count-display');

        return function tick() {
          const counts = getVoucherCounts();
          curEls.forEach((el, i) => { el.textContent = counts ? counts[i] : '—'; });
          const ec = getEggCount();
          if (ec !== null) {
            eggCountEl.textContent = ec + ' egg' + (ec !== 1 ? 's' : '') + ' stored' +
              (_eggLimitRemoved ? '  •  limit off' : '  •  99 max');
          } else {
            eggCountEl.textContent = 'waiting for game connection';
          }
        };
      }
    },
    // add more sections here
    {
      id: 'pokemon',
      icon: '⚡',
      label: 'Pokémon',
      description: 'Pokédex completion & catch tools',
      buildContent(container) {
        container.innerHTML = `
          <div class="pr-card">
            <div class="pr-card-header">
              <div class="pr-card-title">Pokédex</div>
              <div class="pr-card-desc">Mark every Pokémon as caught, or restore to default Pokédex state with confirmation</div>
            </div>
            <div class="pr-dex-stats" id="pr-dex-stats">—</div>
            <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px">
              <button class="pr-btn green" id="pr-catch-all">✓ Catch All Pokémon</button>
              <button class="pr-btn red" id="pr-reset-caught">↩ Restore Default Pokémon</button>
            </div>
          </div>
        `;

        const statsEl  = container.querySelector('#pr-dex-stats');
        const catchBtn = container.querySelector('#pr-catch-all');
        const resetBtn = container.querySelector('#pr-reset-caught');
        let busy = false;
        let dexAnimRaf = null;
        let dexAnimating = false;
        let displayedCaught = null;
        let displayedTotal = null;

        function renderDexText(caught, total) {
          displayedCaught = caught;
          displayedTotal = total;
          statsEl.textContent = caught + ' / ' + total + ' caught';
        }

        function setDexButtonProgress(btn, label, pct) {
          const progress = Math.max(0, Math.min(100, Number(pct) || 0));
          btn.textContent = label + ' ' + progress + '%';
        }

        async function animateDexCounts(fromCaught, fromTotal, targetCaught, targetTotal, onProgress) {
          const startCaught = Number.isFinite(fromCaught) ? fromCaught : (displayedCaught ?? targetCaught);
          const startTotal = Number.isFinite(fromTotal) ? fromTotal : (displayedTotal ?? targetTotal);
          if (typeof onProgress === 'function') onProgress(0);

          if (startCaught === targetCaught && startTotal === targetTotal) {
            renderDexText(targetCaught, targetTotal);
            if (typeof onProgress === 'function') onProgress(100);
            return;
          }

          if (dexAnimRaf) {
            cancelAnimationFrame(dexAnimRaf);
            dexAnimRaf = null;
          }

          renderDexText(startCaught, startTotal);

          const delta = Math.max(
            Math.abs(targetCaught - startCaught),
            Math.abs(targetTotal - startTotal),
          );
          const duration = Math.min(900, Math.max(280, 180 + delta * 2));
          const start = performance.now();
          dexAnimating = true;

          await new Promise(resolve => {
            const step = (now) => {
              const t = Math.min(1, (now - start) / duration);
              const eased = 1 - Math.pow(1 - t, 3);

              const currentCaught = Math.round(startCaught + (targetCaught - startCaught) * eased);
              const currentTotal = Math.round(startTotal + (targetTotal - startTotal) * eased);
              renderDexText(currentCaught, currentTotal);
              if (typeof onProgress === 'function') onProgress(Math.round(t * 100));

              if (t >= 1) {
                dexAnimating = false;
                dexAnimRaf = null;
                renderDexText(targetCaught, targetTotal);
                if (typeof onProgress === 'function') onProgress(100);
                resolve();
                return;
              }
              dexAnimRaf = requestAnimationFrame(step);
            };
            dexAnimRaf = requestAnimationFrame(step);
          });
        }

        function setBusyState(nextBusy) {
          busy = nextBusy;
          catchBtn.disabled = nextBusy;
          resetBtn.disabled = nextBusy;
        }

        catchBtn.addEventListener('click', async () => {
          if (busy) return;
          const gd = findGameData();
          if (!gd?.dexData) { showToast('⚠️ No game data found', true); return; }
          const startStats = getDexStats();

          setBusyState(true);
          catchBtn.classList.add('pr-loading');
          const origText = catchBtn.textContent;
          catchBtn.textContent = 'Working…';

          try {
            const result = await catchAllPokemon();
            if (result.ok) {
              const endStats = getDexStats();
              if (endStats) {
                await animateDexCounts(startStats?.caught, startStats?.total, endStats.caught, endStats.total, (pct) => {
                  setDexButtonProgress(catchBtn, 'Working…', pct);
                });
              } else {
                setDexButtonProgress(catchBtn, 'Working…', 100);
              }
              showToast('✅ Caught ' + result.updated + ' Pokémon!');
            } else {
              showToast('⚠️ Open a game save first', true);
            }
          } finally {
            catchBtn.textContent = origText;
            catchBtn.classList.remove('pr-loading');
            setBusyState(false);
          }
        });

        resetBtn.addEventListener('click', async () => {
          if (busy) return;
          const gd = findGameData();
          if (!gd?.dexData) { showToast('⚠️ No game data found', true); return; }

          const confirmed = globalThis.confirm('⚠️ Restore Pokémon progress to the default captured state from when this page first connected?');
          if (!confirmed) return;
          const startStats = getDexStats();

          setBusyState(true);
          resetBtn.classList.add('pr-loading');
          const origText = resetBtn.textContent;
          resetBtn.textContent = 'Restoring…';

          try {
            const result = await restoreDefaultPokemon();
            if (result.ok) {
              const endStats = getDexStats();
              if (endStats) {
                await animateDexCounts(startStats?.caught, startStats?.total, endStats.caught, endStats.total, (pct) => {
                  setDexButtonProgress(resetBtn, 'Restoring…', pct);
                });
              } else {
                setDexButtonProgress(resetBtn, 'Restoring…', 100);
              }
              showToast('✅ Restored default Pokémon state for ' + result.updated + ' entries');
            } else if (result.reason === 'snapshot') {
              showToast('⚠️ Default snapshot not available yet. Wait a moment and try again.', true);
            } else {
              showToast('⚠️ Open a game save first', true);
            }
          } finally {
            resetBtn.textContent = origText;
            resetBtn.classList.remove('pr-loading');
            setBusyState(false);
          }
        });

        return function tick() {
          captureDefaultDexSnapshot();
          if (busy) return;
          const stats = getDexStats();
          if (stats) {
            if (!dexAnimating && (displayedCaught !== stats.caught || displayedTotal !== stats.total)) {
              renderDexText(stats.caught, stats.total);
            }
          } else {
            if (dexAnimRaf) {
              cancelAnimationFrame(dexAnimRaf);
              dexAnimRaf = null;
            }
            dexAnimating = false;
            displayedCaught = null;
            displayedTotal = null;
            statsEl.textContent = 'waiting for game connection';
          }
        };
      }
    },
    {
      id: 'gold',
      icon: '💰',
      label: 'Gold',
      description: 'View, set, or add currency',
      buildContent(container) {
        container.innerHTML = `
          <div class="pr-card">
            <div class="pr-card-header">
              <div class="pr-card-title">Gold / Currency</div>
              <div class="pr-card-desc">Set exact value or add more to your current amount</div>
            </div>
            <div class="pr-dex-stats" id="pr-gold-current">—</div>
            <input class="pr-input" id="pr-gold-input" type="number" min="0" step="1" placeholder="Amount">
            <div class="pr-btn-row">
              <button class="pr-btn green" id="pr-gold-set">✓ Set</button>
              <button class="pr-btn purple" id="pr-gold-add">+ Add</button>
              <button class="pr-btn" id="pr-gold-add-10k">+10,000</button>
            </div>
          </div>
        `;

        const currentEl = container.querySelector('#pr-gold-current');
        const inputEl = container.querySelector('#pr-gold-input');
        const setBtn = container.querySelector('#pr-gold-set');
        const addBtn = container.querySelector('#pr-gold-add');
        const add10kBtn = container.querySelector('#pr-gold-add-10k');
        let busy = false;

        function parseInputAmount() {
          const raw = inputEl.value.trim();
          if (raw === '') return null;
          const val = Math.floor(Number(raw));
          if (!Number.isFinite(val) || val < 0) return null;
          return val;
        }

        async function afterCurrencyChange() {
          await triggerAutosaveAfterEggChange();
        }

        setBtn.addEventListener('click', async () => {
          if (busy) return;
          const val = parseInputAmount();
          if (val === null) {
            showToast('⚠️ Enter a valid non-negative amount', true);
            return;
          }

          busy = true;
          setBtn.disabled = true;
          addBtn.disabled = true;
          add10kBtn.disabled = true;

          try {
            if (!setGoldAmount(val)) {
              showToast('⚠️ Could not find currency field in game data', true);
              return;
            }
            await afterCurrencyChange();
            showToast('✅ Currency set to ' + val.toLocaleString());
          } finally {
            setBtn.disabled = false;
            addBtn.disabled = false;
            add10kBtn.disabled = false;
            busy = false;
          }
        });

        addBtn.addEventListener('click', async () => {
          if (busy) return;
          const val = parseInputAmount();
          if (val === null) {
            showToast('⚠️ Enter a valid non-negative amount', true);
            return;
          }

          busy = true;
          setBtn.disabled = true;
          addBtn.disabled = true;
          add10kBtn.disabled = true;

          try {
            if (!addGoldAmount(val)) {
              showToast('⚠️ Could not find currency field in game data', true);
              return;
            }
            await afterCurrencyChange();
            showToast('✅ Added ' + val.toLocaleString() + ' currency');
          } finally {
            setBtn.disabled = false;
            addBtn.disabled = false;
            add10kBtn.disabled = false;
            busy = false;
          }
        });

        add10kBtn.addEventListener('click', async () => {
          if (busy) return;

          busy = true;
          setBtn.disabled = true;
          addBtn.disabled = true;
          add10kBtn.disabled = true;

          try {
            if (!addGoldAmount(10000)) {
              showToast('⚠️ Could not find currency field in game data', true);
              return;
            }
            await afterCurrencyChange();
            showToast('✅ Added 10,000 currency');
          } finally {
            setBtn.disabled = false;
            addBtn.disabled = false;
            add10kBtn.disabled = false;
            busy = false;
          }
        });

        inputEl.addEventListener('keydown', e => {
          if (e.key === 'Enter') {
            e.preventDefault();
            setBtn.click();
          }
          e.stopPropagation();
        });

        return function tick() {
          const cur = getGoldAmount();
          if (cur === null) {
            currentEl.textContent = 'waiting for game connection';
          } else {
            currentEl.textContent = Math.max(0, Math.floor(cur)).toLocaleString() + ' currency';
          }
        };
      }
    },
    {
      id: 'savedata',
      icon: '💾',
      label: 'Save Data',
      description: 'Export & import your full game progress',
      buildContent(container) {
        container.innerHTML = `
          <div class="pr-card pr-save-warn-card">
            <div style="display:flex;align-items:center;gap:10px">
              <span style="font-size:22px;flex-shrink:0;line-height:1">⚠️</span>
              <div style="font-size:15px;color:#f0b429;line-height:1.55">
                Always <strong>export a backup</strong> before importing. Importing overwrites your current in-memory save data immediately.
              </div>
            </div>
          </div>

          <div class="pr-card">
            <div class="pr-card-header">
              <div class="pr-card-title">Export Save Data</div>
              <div class="pr-card-desc">Downloads your complete game progress as a JSON file — Pokédex, starter data, eggs, vouchers, achievements, game stats, and all other persistent fields.</div>
            </div>
            <div class="pr-save-stats" id="pr-save-stats">waiting for game connection…</div>
            <div class="pr-save-progress-wrap" id="pr-export-progress-wrap" style="display:none">
              <div class="pr-save-progress-bar" id="pr-export-bar"></div>
            </div>
            <button class="pr-btn green" id="pr-export-save" style="width:100%">⬇ Export to File</button>
          </div>

          <div class="pr-card">
            <div class="pr-card-header">
              <div class="pr-card-title">Import Save Data</div>
              <div class="pr-card-desc">Load a previously exported JSON file to restore all persistent game progress, then auto-save to the server.</div>
            </div>
            <input type="file" id="pr-import-file" accept=".json" style="display:none">
            <div style="display:grid;grid-template-columns:1fr auto;gap:8px;align-items:stretch">
              <div class="pr-input pr-import-filename" id="pr-import-filename" style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#4e6080;display:flex;align-items:center;border-radius:8px">No file selected…</div>
              <button class="pr-btn" id="pr-import-choose">Browse</button>
            </div>
            <div class="pr-save-fileinfo" id="pr-import-fileinfo" style="display:none"></div>
            <div class="pr-save-progress-wrap" id="pr-import-progress-wrap" style="display:none">
              <div class="pr-save-progress-bar" id="pr-import-bar"></div>
            </div>
            <button class="pr-btn green" id="pr-import-save" disabled style="width:100%">⬆ Import from File</button>
          </div>
        `;

        const exportBtn        = container.querySelector('#pr-export-save');
        const exportProgressWrap = container.querySelector('#pr-export-progress-wrap');
        const exportBar        = container.querySelector('#pr-export-bar');
        const statsEl          = container.querySelector('#pr-save-stats');
        const fileInput        = container.querySelector('#pr-import-file');
        const filenameEl       = container.querySelector('#pr-import-filename');
        const fileinfoEl       = container.querySelector('#pr-import-fileinfo');
        const chooseBtn        = container.querySelector('#pr-import-choose');
        const importBtn        = container.querySelector('#pr-import-save');
        const importProgressWrap = container.querySelector('#pr-import-progress-wrap');
        const importBar        = container.querySelector('#pr-import-bar');
        let selectedFile       = null;
        let busy               = false;
        const exportProgressQueue = [];
        let exportProgressRunning = false;
        let exportProgressLastPaint = 0;

        function setExportProgress(label, pct) {
          exportBtn.textContent = label;
          exportProgressWrap.style.display = '';
          exportBar.style.width = pct + '%';
        }

        function setImportProgress(label, pct) {
          importBtn.textContent = label;
          importProgressWrap.style.display = '';
          importBar.style.width = pct + '%';
        }

        function sleep(ms) {
          return new Promise(resolve => setTimeout(resolve, ms));
        }

        function queueExportProgress(label, pct) {
          exportProgressQueue.push({ label, pct: Math.max(0, Math.min(100, Number(pct) || 0)) });
          if (!exportProgressRunning) void runExportProgressQueue();
        }

        async function runExportProgressQueue() {
          exportProgressRunning = true;
          const MIN_STAGE_MS = 220;

          while (exportProgressQueue.length) {
            const next = exportProgressQueue.shift();
            const elapsed = Date.now() - exportProgressLastPaint;
            if (exportProgressLastPaint && elapsed < MIN_STAGE_MS) {
              await sleep(MIN_STAGE_MS - elapsed);
            }
            setExportProgress(next.label, next.pct);
            exportProgressLastPaint = Date.now();
          }

          exportProgressRunning = false;
        }

        async function flushExportProgressQueue() {
          while (exportProgressRunning || exportProgressQueue.length) {
            await sleep(20);
          }
        }

        function resetExportProgressQueue() {
          exportProgressQueue.length = 0;
          exportProgressRunning = false;
          exportProgressLastPaint = 0;
        }

        exportBtn.addEventListener('click', async () => {
          if (busy) return;
          busy = true;
          resetExportProgressQueue();
          exportBtn.disabled = true;
          importBtn.disabled = true;
          exportBtn.classList.add('pr-loading');
          const origText = exportBtn.textContent;

          try {
            const result = await exportSaveData((label, pct) => queueExportProgress(label, pct));
            await flushExportProgressQueue();
            if (result.ok) {
              setExportProgress('Done! (' + result.kb + ' KB)', 100);
              exportBtn.classList.remove('pr-loading');
              if (result.mode === 'clipboard') {
                showToast('✅ Export copied to clipboard (' + result.kb + ' KB)');
              } else {
                showToast('✅ Save data exported — ' + result.kb + ' KB');
              }
              setTimeout(() => {
                exportProgressWrap.style.display = 'none';
                exportBar.style.width = '0%';
                exportBtn.textContent = origText;
              }, 2000);
            } else if (result.reason === 'connect') {
              showToast('⚠️ No game data found — open a save first', true);
              exportBtn.textContent = origText;
              exportProgressWrap.style.display = 'none';
              resetExportProgressQueue();
              exportBtn.classList.remove('pr-loading');
            } else {
              showToast('⚠️ Export failed: ' + (result.error || result.reason), true);
              exportBtn.textContent = origText;
              exportProgressWrap.style.display = 'none';
              resetExportProgressQueue();
              exportBtn.classList.remove('pr-loading');
            }
          } catch (e) {
            showToast('⚠️ Export failed: ' + e.message, true);
            exportBtn.textContent = origText;
            exportProgressWrap.style.display = 'none';
            resetExportProgressQueue();
            exportBtn.classList.remove('pr-loading');
          } finally {
            busy = false;
            exportBtn.disabled = false;
            importBtn.disabled = !selectedFile;
          }
        });

        chooseBtn.addEventListener('click', () => { if (!busy) fileInput.click(); });

        fileInput.addEventListener('change', () => {
          const file = fileInput.files?.[0];
          if (file) {
            selectedFile = file;
            filenameEl.textContent = file.name;
            filenameEl.style.color = '#e0e8ff';
            const kb = Math.round(file.size / 1024);
            const modified = new Date(file.lastModified).toLocaleDateString(undefined, { month: 'numeric', day: 'numeric', year: 'numeric' });
            fileinfoEl.textContent = kb + ' KB  ·  modified ' + modified;
            fileinfoEl.style.display = '';
            importBtn.disabled = false;
          }
        });

        importBtn.addEventListener('click', async () => {
          if (!selectedFile || busy) return;

          const confirmed = globalThis.confirm(
            '⚠️ This will overwrite your current in-memory save data with the contents of:\n\n' +
            selectedFile.name + '\n\nThis cannot be undone. Continue?'
          );
          if (!confirmed) return;

          busy = true;
          importBtn.disabled = true;
          exportBtn.disabled = true;
          chooseBtn.disabled = true;
          importBtn.classList.add('pr-loading');
          const origText = importBtn.textContent;

          try {
            setImportProgress('Reading file… 0%', 0);
            await waitTick();
            const text = await selectedFile.text();
            setImportProgress('Parsing… 20%', 20);
            await waitTick();

            const result = await importSaveData(text, (_done, _total, pct) => {
              const clamped = 20 + Math.round(pct * 0.7);
              setImportProgress('Restoring… ' + pct + '%', clamped);
            });

            if (result.ok) {
              setImportProgress('Saving… 95%', 95);
              await waitTick();
              setImportProgress('Done!', 100);
              importBtn.classList.remove('pr-loading');
              showToast('✅ Imported — ' + result.restored + ' fields restored');
              setTimeout(() => {
                importProgressWrap.style.display = 'none';
                importBar.style.width = '0%';
                importBtn.textContent = origText;
              }, 2000);
            } else if (result.reason === 'connect') {
              showToast('⚠️ No game data found — open a save first', true);
              importBtn.textContent = origText;
              importProgressWrap.style.display = 'none';
              importBtn.classList.remove('pr-loading');
            } else if (result.reason === 'syntax') {
              showToast('⚠️ JSON syntax error — file may be corrupted or incomplete', true);
              importBtn.textContent = origText;
              importProgressWrap.style.display = 'none';
              importBtn.classList.remove('pr-loading');
            } else if (result.reason === 'parse') {
              showToast('⚠️ Invalid save file: ' + (result.error || 'parse error'), true);
              importBtn.textContent = origText;
              importProgressWrap.style.display = 'none';
              importBtn.classList.remove('pr-loading');
            } else if (result.reason === 'invalid') {
              showToast('⚠️ ' + (result.error || 'Not a valid PokeRogue save file'), true);
              importBtn.textContent = origText;
              importProgressWrap.style.display = 'none';
              importBtn.classList.remove('pr-loading');
            } else {
              showToast('⚠️ Import failed: ' + (result.error || result.reason), true);
              importBtn.textContent = origText;
              importProgressWrap.style.display = 'none';
              importBtn.classList.remove('pr-loading');
            }
          } catch (e) {
            showToast('⚠️ Import failed: ' + e.message, true);
            importBtn.textContent = origText;
            importProgressWrap.style.display = 'none';
            importBtn.classList.remove('pr-loading');
          } finally {
            busy = false;
            importBtn.disabled = false;
            exportBtn.disabled = false;
            chooseBtn.disabled = false;
          }
        });

        return function tick() {
          const gd = findGameData();
          if (!gd) { statsEl.textContent = 'waiting for game connection…'; return; }
          const parts = [];
          if (gd.dexData) {
            const keys = Object.keys(gd.dexData);
            const caught = keys.filter(k => gd.dexData[k]?.caughtAttr).length;
            parts.push('Pokédex ' + caught + ' / ' + keys.length);
          }
          if (Array.isArray(gd.eggs)) parts.push(gd.eggs.length + ' egg' + (gd.eggs.length !== 1 ? 's' : ''));
          if (gd.achvUnlocks) {
            const count = Object.keys(gd.achvUnlocks).length;
            if (count > 0) parts.push(count + ' achievement' + (count !== 1 ? 's' : ''));
          }
          if (gd.voucherCounts) {
            const total = Object.values(gd.voucherCounts).reduce((s, v) => s + (v || 0), 0);
            if (total > 0) parts.push(total + ' voucher' + (total !== 1 ? 's' : ''));
          }
          statsEl.textContent = parts.length ? parts.join(' · ') : 'No summary data available';
        };
      }
    },
    {
      id: 'gamestats',
      icon: '📊',
      label: 'Game Stats',
      description: 'Live read-out of battle, catch, egg & session counters',
      buildContent(container) {
        container.innerHTML = `
          <div class="pr-card">
            <div class="pr-card-header">
              <div class="pr-card-title">Game Statistics</div>
              <div class="pr-card-desc">Live read-out — updates every second</div>
            </div>
            <div class="pr-stats-grid" id="pr-stats-grid">
              <div class="pr-stats-waiting">waiting for game connection…</div>
            </div>
          </div>
        `;

        const gridEl = container.querySelector('#pr-stats-grid');
        let rendered = false;

        function renderStats(stats) {
          if (!stats) {
            gridEl.innerHTML = '<div class="pr-stats-waiting">waiting for game connection…</div>';
            rendered = false;
            return;
          }
          if (!rendered) {
            rendered = true;
            gridEl.innerHTML = Object.keys(stats).map(k =>
              '<div class="pr-stat-card">' +
                '<div class="pr-stat-label">' + formatStatKey(k) + '</div>' +
                '<div class="pr-stat-value" data-key="' + k + '">' + formatGameStatValue(k, stats[k]) + '</div>' +
              '</div>'
            ).join('');
          } else {
            gridEl.querySelectorAll('.pr-stat-value').forEach(el => {
              const live = stats[el.dataset.key];
              if (live !== undefined) el.textContent = formatGameStatValue(el.dataset.key, live);
            });
          }
        }

        return function tick() {
          renderStats(getGameStats());
        };
      }
    },
    // add more sections here
  ];

  // ─── GUI ─────────────────────────────────────────────────────────────────

  let _sectionTick = null;
  let _statusTimer = null;

  function buildGUI() {
    if (document.getElementById('pr-cheat-gui')) return;
    const overlay = document.createElement('div');
    overlay.id = 'pr-cheat-gui';

    const style = document.createElement('style');

    const css = [
      "@import url('https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Orbitron:wght@700&display=swap');",

      // overlay
      '#pr-cheat-gui{position:fixed;inset:0;z-index:999998;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,0.72);backdrop-filter:blur(12px);animation:pr-in .18s ease}',
      '@keyframes pr-in{from{opacity:0}to{opacity:1}}',
      '@keyframes pr-out{from{opacity:1}to{opacity:0}}',
      '#pr-cheat-gui.pr-closing{animation:pr-out .18s ease forwards}',

      // panel
      '#pr-panel{background:#0f1219;border:1px solid #2e3a52;border-radius:16px;width:560px;max-height:88vh;display:flex;flex-direction:column;box-shadow:0 0 0 1px #1a2235,0 32px 80px rgba(0,0,0,.95);overflow:hidden;font-family:"Share Tech Mono",monospace}',

      // titlebar
      '#pr-bar{background:linear-gradient(160deg,#161d2e 0%,#0f1219 100%);border-bottom:1px solid #2e3a52;padding:14px 16px;display:flex;align-items:center;justify-content:space-between;user-select:none;flex-shrink:0;gap:10px}',
      '#pr-bar-left{display:flex;align-items:center;gap:10px;flex:1;min-width:0}',
      '#pr-back{background:none;border:1px solid #2e3a52;border-radius:6px;color:#7c8fff;cursor:pointer;width:28px;height:28px;display:none;align-items:center;justify-content:center;font-size:20px;line-height:1;transition:all .15s;flex-shrink:0}',
      '#pr-back:hover{border-color:#7c8fff;background:#1a2040}',
      '#pr-back.visible{display:flex}',
      '#pr-bar-title{font-family:"Orbitron",monospace;font-size:16px;font-weight:700;color:#7c8fff;letter-spacing:.14em;text-transform:uppercase}',
      '#pr-bar-sub{font-size:15px;color:#8a9ab8;margin-top:3px;letter-spacing:.1em;text-transform:uppercase}',
      '#pr-close{background:none;border:1px solid #2e3a52;border-radius:6px;color:#6b7fa0;cursor:pointer;width:28px;height:28px;display:flex;align-items:center;justify-content:center;font-size:17px;line-height:1;transition:all .15s;flex-shrink:0}',
      '#pr-close:hover{border-color:#f87171;color:#f87171;background:#2d1010}',

      // body
      '#pr-body{flex:1;overflow-y:auto;min-height:0}',
      '#pr-body::-webkit-scrollbar{width:3px}',
      '#pr-body::-webkit-scrollbar-thumb{background:#2e3a52;border-radius:2px}',

      // home
      '#pr-home{padding:14px;display:flex;flex-direction:column;gap:8px}',
      '.pr-home-label{font-size:15px;color:#8a9ab8;letter-spacing:.18em;text-transform:uppercase;padding:2px 4px;margin-bottom:2px}',
      '.pr-category-btn{display:flex;align-items:center;gap:14px;background:#141b28;border:1px solid #2a3650;border-radius:12px;padding:16px 14px;cursor:pointer;width:100%;text-align:left;transition:all .18s}',
      '.pr-category-btn:hover{border-color:#7c8fff;background:#181f30;transform:translateX(2px)}',
      '.pr-category-btn:active{transform:scale(.99)}',
      '.pr-cat-icon{font-size:26px;flex-shrink:0;width:34px;text-align:center;line-height:1}',
      '.pr-cat-info{flex:1;min-width:0}',
      '.pr-cat-label{font-family:"Orbitron",monospace;font-size:15px;color:#c4ccff;letter-spacing:.08em;text-transform:uppercase;margin-bottom:5px}',
      '.pr-cat-desc{font-size:15px;color:#9aaac0;line-height:1.4}',
      '.pr-cat-arrow{color:#4a5e80;font-size:24px;flex-shrink:0;transition:all .18s;line-height:1}',
      '.pr-category-btn:hover .pr-cat-arrow{color:#7c8fff;transform:translateX(3px)}',

      // section
      '#pr-section{padding:14px;display:none;flex-direction:column;gap:10px}',
      '#pr-section.visible{display:flex}',

      // cards
      '.pr-card{background:#141b28;border:1px solid #2a3650;border-radius:12px;padding:14px;display:flex;flex-direction:column;gap:12px}',
      '.pr-card-header{display:flex;flex-direction:column;gap:4px;padding-bottom:12px;border-bottom:1px solid #1e2d40}',
      '.pr-card-title{font-size:18px;color:#d0d8f0;letter-spacing:.03em}',
      '.pr-card-desc{font-size:15px;color:#8a9ab8;line-height:1.5}',
      '.pr-dex-stats{font-size:22px;color:#7c8fff;font-weight:700;letter-spacing:.04em;padding:2px 0}',

      // voucher grid
      '.pr-voucher-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px}',
      '.pr-voucher-card{background:#0f1219;border:1px solid #2a3650;border-radius:10px;padding:12px 14px;display:flex;flex-direction:column;gap:8px;transition:border-color .15s;overflow:hidden}',
      '.pr-voucher-card:focus-within{border-color:#7c8fff}',
      '.pr-voucher-tier{font-size:13px;color:#9aaac0;letter-spacing:.12em;text-transform:uppercase}',
      '.pr-voucher-tier.gold{color:#a07820}',
      '.pr-voucher-val{font-size:24px;color:#7c8fff;font-weight:700;line-height:1.15;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
      '.pr-voucher-val.gold{color:#f0b429}',
      '.pr-input{background:#1a2235;border:1px solid #2a3650;border-radius:6px;color:#e0e8ff;font-family:"Share Tech Mono",monospace;font-size:15px;padding:7px 9px;width:100%;box-sizing:border-box;outline:none;transition:border-color .15s,background .15s}',
      '.pr-input:focus{border-color:#7c8fff;background:#1e2840}',
      '.pr-input::placeholder{color:#4e6080}',
      '.pr-input::-webkit-outer-spin-button,.pr-input::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}',
      '.pr-input[type=number]{-moz-appearance:textfield}',
      '.pr-egg-tools-grid{display:grid;grid-template-columns:minmax(0,1fr) 110px;gap:8px}',
      '#pr-egg-type-select{min-width:0}',
      '#pr-egg-amount{text-align:center}',
      '.pr-egg-btn-row{display:grid;grid-template-columns:1fr;gap:8px}',
      '.pr-egg-btn-row-double{grid-template-columns:1fr 1fr}',

      // buttons
      '.pr-btn-row{display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px}',
      '.pr-btn{background:#1a2235;border:1px solid #2a3650;border-radius:8px;padding:11px 10px;cursor:pointer;font-family:"Share Tech Mono",monospace;font-size:16px;color:#7a8aaa;transition:all .15s;text-align:center;white-space:nowrap}',
      '.pr-btn:hover{background:#1e2840;color:#e0e8ff;border-color:#4a5a80}',
      '.pr-btn:active{transform:scale(.97)}',
      '.pr-btn:disabled{cursor:not-allowed;opacity:.75;pointer-events:none;transform:none}',
      '.pr-btn.green{color:#4ade80;border-color:#1a4030;background:#0e1e14}',
      '.pr-btn.green:hover{background:#142a1e;border-color:#22c55e;color:#86efac}',
      '.pr-btn.green.pr-loading{color:#fde68a;border-color:#b45309;background:#2a1708}',
      '.pr-btn.green.pr-loading:hover{color:#fde68a;border-color:#b45309;background:#2a1708}',
      '.pr-btn.purple{color:#c4b5fd;border-color:#3730a3;background:#12102e}',
      '.pr-btn.purple:hover{background:#181450;border-color:#818cf8;color:#ddd6fe}',
      '.pr-btn.red{color:#f87171;border-color:#7f1d1d;background:#1e0e0e}',
      '.pr-btn.red:hover{background:#2a1010;border-color:#dc2626;color:#fca5a5}',
      '.pr-btn.red.pr-loading,.pr-btn.red.pr-loading:hover{color:#fecaca;border-color:#dc2626;background:#3b0d0d}',

      // toggle row (for hacks)
      '.pr-toggle-row{display:flex;align-items:center;justify-content:space-between;gap:16px}',
      '.pr-toggle-info{flex:1;min-width:0}',
      '.pr-toggle-name{font-size:17px;color:#d0d8f0;margin-bottom:5px}',
      '.pr-toggle-status{font-size:15px;color:#9aaac0;line-height:1.4}',
      '.pr-toggle-row.active .pr-toggle-name{color:#a5b4fc}',
      '.pr-toggle-row.active .pr-toggle-status{color:#7c8fff}',

      // toggle switch
      '.pr-toggle{position:relative;width:44px;height:24px;flex-shrink:0}',
      '.pr-toggle input{opacity:0;width:0;height:0}',
      '.pr-slider{position:absolute;inset:0;background:#1a2235;border-radius:24px;cursor:pointer;border:1px solid #2a3650;transition:all .2s}',
      '.pr-slider::before{content:"";position:absolute;height:16px;width:16px;left:3px;top:50%;transform:translateY(-50%);background:#3a4a68;border-radius:50%;transition:transform .2s,background .2s}',
      '.pr-toggle input:checked + .pr-slider{background:#1a1f4a;border-color:#7c8fff}',
      '.pr-toggle input:checked + .pr-slider::before{transform:translateX(20px) translateY(-50%);background:#a5b4fc}',

      // status bar
      '#pr-statusbar{flex-shrink:0;border-top:1px solid #1e2a40;padding:9px 16px;display:flex;align-items:center;gap:8px;background:#0b0f18}',
      '.pr-dot{width:7px;height:7px;border-radius:50%;flex-shrink:0;background:#2e3a52;transition:background .3s}',
      '.pr-dot.on{background:#22c55e;box-shadow:0 0 8px rgba(34,197,94,.5)}',
      '.pr-dot.blink{background:#f59e0b;animation:pr-blink 1s infinite}',
      '@keyframes pr-blink{0%,100%{opacity:1}50%{opacity:.25}}',
      '#pr-status-text{font-size:15px;color:#9aaac0;flex:1}',

      // footer
      '#pr-footer{flex-shrink:0;padding:8px 16px 12px;font-size:15px;color:#8a9ab8;text-align:center;letter-spacing:.05em}',
      '#pr-footer kbd{background:#1e2840;border:1px solid #3a4e6a;border-radius:4px;padding:2px 7px;font-family:inherit;color:#a0b0d0}',

      // save data warning card
      '.pr-save-warn-card{background:#1e1408;border-color:#7a4c00}',

      // save data progress bar
      '.pr-save-progress-wrap{height:4px;background:#1a2235;border-radius:2px;overflow:hidden;margin-bottom:2px}',
      '.pr-save-progress-bar{height:100%;width:0%;background:linear-gradient(90deg,#22c55e,#4ade80);border-radius:2px;transition:width .3s ease}',

      // save data stats & file info
      '.pr-save-stats{font-size:14px;color:#7c8fff;letter-spacing:.04em;padding:2px 0;min-height:18px}',
      '.pr-save-fileinfo{font-size:13px;color:#6a7a9a;letter-spacing:.04em;padding:1px 0}',

      // game stats grid
      '.pr-stats-grid{display:grid;grid-template-columns:1fr 1fr;gap:6px}',
      '.pr-stat-card{background:#0f1219;border:1px solid #2a3650;border-radius:8px;padding:8px 10px;display:flex;flex-direction:column;gap:4px}',
      '.pr-stat-label{font-size:10px;color:#6a7a9a;letter-spacing:.1em;text-transform:uppercase;line-height:1.3}',
      '.pr-stat-value{font-size:17px;color:#a5b4fc;font-weight:700;letter-spacing:.02em;line-height:1.2}',
      '.pr-stats-waiting{font-size:14px;color:#5a6a88;grid-column:1/-1;text-align:center;padding:12px 0}',
    ].join('\n');

    style.textContent = css;
    document.head.appendChild(style);

    const homeHTML = SECTIONS.map(s =>
      '<button class="pr-category-btn" data-section="' + s.id + '">' +
        '<span class="pr-cat-icon">' + s.icon + '</span>' +
        '<span class="pr-cat-info">' +
          '<div class="pr-cat-label">' + s.label + '</div>' +
          '<div class="pr-cat-desc">' + s.description + '</div>' +
        '</span>' +
        '<span class="pr-cat-arrow">›</span>' +
      '</button>'
    ).join('');

    overlay.innerHTML =
      '<div id="pr-panel">' +
        '<div id="pr-bar">' +
          '<div id="pr-bar-left">' +
            '<button id="pr-back">‹</button>' +
            '<div>' +
              '<div id="pr-bar-title">PokeRogue</div>' +
              '<div id="pr-bar-sub">CHEAT MENU</div>' +
            '</div>' +
          '</div>' +
          '<button id="pr-close">✕</button>' +
        '</div>' +
        '<div id="pr-body">' +
          '<div id="pr-home">' +
            '<div class="pr-home-label">Categories</div>' +
            homeHTML +
          '</div>' +
          '<div id="pr-section"></div>' +
        '</div>' +
        '<div id="pr-statusbar">' +
          '<span class="pr-dot blink" id="pr-dot"></span>' +
          '<span id="pr-status-text">Searching for game…</span>' +
        '</div>' +
        '<div id="pr-footer">v' + getScriptVersion() + ' · Press <kbd>' + TOGGLE_KEY + '</kbd> to open / close</div>' +
      '</div>';

    document.body.appendChild(overlay);

    // ── navigation ──
    const panel     = overlay.querySelector('#pr-panel');
    const homeEl    = overlay.querySelector('#pr-home');
    const sectionEl = overlay.querySelector('#pr-section');
    const backBtn   = overlay.querySelector('#pr-back');
    const barSub    = overlay.querySelector('#pr-bar-sub');

    function animatePanelHeight(changeFn) {
      // 1. pin current height (no transition yet)
      const fromH = panel.offsetHeight;
      panel.style.transition = 'none';
      panel.style.height = fromH + 'px';

      // 2. apply the content change
      changeFn();

      // 3. measure the natural target height without painting (still in same JS task)
      panel.style.height = '';
      const toH = panel.offsetHeight; // forces sync reflow → captures new natural height
      panel.style.height = fromH + 'px'; // snap back to start
      void panel.offsetHeight; // commit the snap

      // 4. animate to target
      panel.style.transition = 'height .28s cubic-bezier(0.4,0,0.2,1)';
      panel.style.height = toH + 'px';

      // 5. clean up once done so CSS max-height takes over again
      panel.addEventListener('transitionend', e => {
        if (e.propertyName === 'height') {
          panel.style.height = '';
          panel.style.transition = '';
        }
      }, { once: true });
    }

    function goHome() {
      animatePanelHeight(() => {
        activeSection = null;
        if (_sectionTick) { clearInterval(_sectionTick); _sectionTick = null; }
        homeEl.style.display = '';
        sectionEl.className = 'pr-section';
        sectionEl.innerHTML = '';
        backBtn.classList.remove('visible');
        barSub.textContent = 'CHEAT MENU';
      });
    }
    overlay._goHome = goHome;

    function goSection(id) {
      const sec = SECTIONS.find(s => s.id === id);
      if (!sec) return;
      animatePanelHeight(() => {
        activeSection = id;
        homeEl.style.display = 'none';
        sectionEl.className = 'pr-section visible';
        sectionEl.innerHTML = '';
        backBtn.classList.add('visible');
        barSub.textContent = sec.label.toUpperCase();
        const tick = sec.buildContent(sectionEl);
        if (typeof tick === 'function') {
          tick();
          _sectionTick = setInterval(tick, 800);
        }
      });
    }

    overlay.querySelectorAll('.pr-category-btn').forEach(btn => {
      btn.addEventListener('click', () => goSection(btn.dataset.section));
    });
    backBtn.addEventListener('click', goHome);

    // ── status bar ──
    const dot        = overlay.querySelector('#pr-dot');
    const statusText = overlay.querySelector('#pr-status-text');

    function startStatusTimer() {
      if (_statusTimer) clearInterval(_statusTimer);
      _statusTimer = setInterval(() => {
        const gd = findGameData();
        if (gd) {
          captureDefaultDexSnapshot();
          dot.className = 'pr-dot on';
          statusText.textContent = 'Connected to game';
        } else {
          dot.className = 'pr-dot blink';
          statusText.textContent = 'Waiting for game connection…';
        }
      }, 1000);
    }
    overlay._startStatusTimer = startStatusTimer;
    startStatusTimer();

    // ── close / backdrop ──
    overlay.querySelector('#pr-close').addEventListener('click', hideGUI);
    overlay.addEventListener('click', e => { if (e.target === overlay) hideGUI(); });

    overlay._cleanup = () => {
      if (_sectionTick) { clearInterval(_sectionTick); _sectionTick = null; }
      if (_statusTimer) { clearInterval(_statusTimer); _statusTimer = null; }
    };
  }

  function showGUI() {
    const existing = document.getElementById('pr-cheat-gui');
    if (existing) {
      existing._hideToken = (existing._hideToken || 0) + 1;
      existing.classList.remove('pr-closing');
      existing.style.display = '';
      // restart the open animation
      existing.style.animation = 'none';
      void existing.offsetHeight; // force reflow
      existing.style.animation = '';
      existing._startStatusTimer?.();
      guiVisible = true;
    } else {
      buildGUI();
      guiVisible = true;
    }
  }
  function hideGUI() {
    const el = document.getElementById('pr-cheat-gui');
    if (el) {
      const hideToken = (el._hideToken || 0) + 1;
      el._hideToken = hideToken;
      el._cleanup?.();
      el._goHome?.();
      el.classList.add('pr-closing');
      el.addEventListener('animationend', () => {
        if (el._hideToken !== hideToken || guiVisible) return;
        el.classList.remove('pr-closing');
        el.style.display = 'none';
      }, { once: true });
    }
    activeSection = null;
    guiVisible = false;
  }
  function toggleGUI() { guiVisible ? hideGUI() : showGUI(); }

  document.addEventListener('keydown', e => {
    if (e.key === TOGGLE_KEY) { e.preventDefault(); toggleGUI(); return; }
    if (guiVisible) {
      if (e.key === 'Escape') { e.preventDefault(); e.stopImmediatePropagation(); hideGUI(); return; }
      // Block all other keypresses from reaching the game while the menu is open
      e.stopImmediatePropagation();
    }
  }, true);

  // corner pill
  const pill = document.createElement('div');
  Object.assign(pill.style, {
    position: 'fixed', bottom: '12px', right: '12px',
    background: '#141b28', border: '1px solid #2a3650',
    borderRadius: '6px', padding: '5px 10px',
    fontFamily: "'Share Tech Mono', monospace", fontSize: '11px',
    color: '#5a6a88', zIndex: '999997', cursor: 'pointer',
    userSelect: 'none', letterSpacing: '0.1em',
  });
  pill.textContent = '[' + TOGGLE_KEY + '] CHEATS';
  pill.addEventListener('click', toggleGUI);
  document.body.appendChild(pill);

})();