PokeRogue Cheat Menu

Cheat menu for PokeRogue

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey, Greasemonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

You will need to install an extension such as Tampermonkey to install this script.

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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);

})();