PokeLike Mod Menu

Floating mod menu for PokeLike

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

Advertisement:

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

Advertisement:

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         PokeLike Mod Menu
// @namespace    pokelike-mod-menu
// @version      3.0.1
// @description  Floating mod menu for PokeLike
// @match        https://pokelike.xyz/*
// @grant        none
// @run-at       document-idle
// @license      MIT 
// ==/UserScript==

(() => {
  'use strict';

  const MENU_ID = 'plmm-menu';
  const STYLE_ID = 'plmm-style';

  const STAT_FIELDS = [
    ['hp', 'HP'],
    ['atk', 'Attack'],
    ['def', 'Defense'],
    ['special', 'Special Attack'],
    ['spdef', 'Special Defense'],
    ['speed', 'Speed']
  ];

  const RUN_SETTING_FIELDS = [
    ['usedBallCatch', 'Used Ball Catch'],
    ['usedPokecenter', 'Used Pokecenter'],
    ['usedTM', 'Used TM'],
    ['pickedUpItem', 'Picked Up Item'],
    ['anyFainted', 'Any Fainted']
  ];

  let selectedUid = '';
  let lastSyncSnapshot = '';
  const lockedRunSettings = {};

  function getState() {
    try {
      return Function('return typeof state !== "undefined" ? state : null;')();
    } catch {
      return null;
    }
  }

  function getItemPool() {
    try {
      const pools = Function(`
        const regularItems = typeof ITEM_POOL !== "undefined" && Array.isArray(ITEM_POOL) ? ITEM_POOL : [];
        const usableItems = typeof USABLE_ITEM_POOL !== "undefined" && Array.isArray(USABLE_ITEM_POOL) ? USABLE_ITEM_POOL : [];
        return [regularItems, usableItems];
      `)();

      const seen = new Set();
      const merged = [];

      for (const item of pools.flat()) {
        const key = item?.id || item?.name || JSON.stringify(item);
        if (seen.has(key)) continue;

        seen.add(key);
        merged.push(item);
      }

      return merged;
    } catch {
      return [];
    }
  }

  function getPokedexEntries() {
    try {
      const pokedex = Function('return typeof __POKEDEX__ !== "undefined" ? __POKEDEX__ : null;')();
      if (!pokedex || typeof pokedex !== 'object') return [];

      return Object.entries(pokedex)
        .map(([speciesId, info]) => ({
          speciesId: String(speciesId),
          name: info?.name || `Pokemon #${speciesId}`
        }))
        .sort((a, b) => Number(a.speciesId) - Number(b.speciesId));
    } catch {
      return [];
    }
  }

  function getTeam() {
    const s = getState();
    return s && Array.isArray(s.team) ? s.team : [];
  }

  function getSelectedMon() {
    const team = getTeam();
    if (!team.length) return null;

    const select = document.querySelector('#plmm-team-select');
    const uid = select?.value ?? selectedUid;
    if (!uid) return null;

    return team.find(p => String(p._uid) === String(uid)) ?? null;
  }

  function clampNumber(value, fallback, min, max) {
    const n = Number(value);
    if (!Number.isFinite(n)) return fallback;
    return Math.min(max, Math.max(min, n));
  }

  function displayName(mon) {
    return mon.name || `Pokemon #${mon._uid ?? '?'}`;
  }

  function itemDisplayName(item) {
    return item?.name || item?.id || 'Unknown Item';
  }

  function pokedexDisplayName(entry) {
    return `${entry.name} (#${entry.speciesId})`;
  }

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

  function canSelectedSpeciesEvolve(mon) {
    if (!mon || mon.speciesId == null) return false;

    try {
      return !!Function('speciesId', 'return typeof canEvolve === "function" ? canEvolve(speciesId) : false;')(mon.speciesId);
    } catch {
      return false;
    }
  }

  function applyEvolutionAtIndex(index) {
    try {
      return Function('n', `
        if (typeof state === "undefined" || !state.team || !state.team[n]) return false;
        if (typeof applyEvolution !== "function") return false;
        applyEvolution(state.team[n]);
        return true;
      `)(index);
    } catch {
      return false;
    }
  }

  function notify(message) {
    const el = document.querySelector('#plmm-status');
    if (el) el.textContent = message;
  }

  function triggerGameUpdate() {
    window.dispatchEvent(new Event('focus'));
    document.dispatchEvent(new Event('visibilitychange'));
    window.dispatchEvent(new CustomEvent('pokelike-state-edited', {
      detail: { time: Date.now() }
    }));
  }

  function renderTeam() {
    const s = getState();
    if (!s || !Array.isArray(s.team)) return;

    try {
      Function('team', 'if (typeof renderTeamBar === "function") renderTeamBar(team);')(s.team);
    } catch {
      // The game may not expose renderTeamBar in every screen/state.
    }
  }

  function renderItems() {
    const s = getState();
    if (!s || !Array.isArray(s.items)) return;

    try {
      Function('items', 'if (typeof renderItemBadges === "function") renderItemBadges(items);')(s.items);
    } catch {
      // The game may not expose renderItemBadges in every screen/state.
    }
  }

  function injectStyle() {
    if (document.getElementById(STYLE_ID)) return;

    const style = document.createElement('style');
    style.id = STYLE_ID;
    style.textContent = `
      #${MENU_ID} {
        position: fixed;
        bottom: 12px;
        right: 12px;
        width: 360px;
        max-height: calc(100vh - 24px);
        overflow-y: auto;
        z-index: 2147483647;
        background: rgba(18, 20, 28, 0.97);
        color: #f5f5f5;
        border: 1px solid rgba(255, 255, 255, 0.18);
        border-radius: 14px;
        box-shadow: 0 12px 30px rgba(0, 0, 0, 0.45);
        font-family: Arial, sans-serif;
        font-size: 13px;
      }

      #${MENU_ID} * {
        box-sizing: border-box;
      }

      #${MENU_ID} .plmm-header,
      #${MENU_ID} .plmm-section-header {
        display: flex;
        justify-content: space-between;
        align-items: center;
        gap: 8px;
      }

      #${MENU_ID} .plmm-header {
        padding: 10px 12px;
        background: rgba(255,255,255,0.08);
        border-bottom: 1px solid rgba(255,255,255,0.12);
      }

      #${MENU_ID} .plmm-title,
      #${MENU_ID} .plmm-card-title {
        font-weight: 700;
      }

      #${MENU_ID} .plmm-title {
        font-size: 14px;
      }

      #${MENU_ID} .plmm-body {
        padding: 12px;
        display: grid;
        gap: 12px;
      }

      #${MENU_ID} .plmm-card {
        border: 1px solid rgba(255,255,255,0.13);
        border-radius: 10px;
        padding: 10px;
        background: rgba(255,255,255,0.055);
        display: grid;
        gap: 10px;
      }

      #${MENU_ID} .plmm-section-body {
        display: grid;
        gap: 10px;
      }

      #${MENU_ID} .plmm-card-title {
        opacity: 0.95;
      }

      #${MENU_ID} .plmm-subsection-title {
        margin-top: 2px;
        padding-top: 10px;
        border-top: 1px solid rgba(255,255,255,0.12);
        font-size: 12px;
        font-weight: 700;
        opacity: 0.82;
      }

      #${MENU_ID} .plmm-section-body > .plmm-subsection-title:first-child {
        margin-top: 0;
        padding-top: 0;
        border-top: 0;
      }

      #${MENU_ID} .plmm-selected-name {
        font-size: 16px;
        font-weight: 700;
      }

      #${MENU_ID} .plmm-subtext {
        font-size: 12px;
        opacity: 0.72;
      }

      #${MENU_ID} .plmm-grid-2 {
        display: grid;
        grid-template-columns: 1fr 1fr;
        gap: 10px;
      }

      #${MENU_ID} .plmm-grid-3 {
        display: grid;
        grid-template-columns: 1fr 1fr 1fr;
        gap: 10px;
      }

      #${MENU_ID} label {
        display: grid;
        gap: 5px;
        font-size: 12px;
        opacity: 0.95;
      }

      #${MENU_ID} input,
      #${MENU_ID} select,
      #${MENU_ID} button {
        width: 100%;
        border: 1px solid rgba(255,255,255,0.24);
        border-radius: 8px;
        padding: 7px 8px;
        font: inherit;
      }

      #${MENU_ID} input,
      #${MENU_ID} select {
        background: rgba(255,255,255,0.10);
        color: #fff;
      }

      #${MENU_ID} select option {
        background: #ffffff;
        color: #111111;
      }

      #${MENU_ID} input[type="checkbox"] {
        width: auto;
        transform: scale(1.1);
      }

      #${MENU_ID} button {
        cursor: pointer;
        background: rgba(255,255,255,0.14);
        color: #fff;
      }

      #${MENU_ID} button:hover {
        background: rgba(255,255,255,0.23);
      }

      #${MENU_ID} .plmm-primary {
        background: rgba(80, 140, 255, 0.32);
      }

      #${MENU_ID} .plmm-mini {
        width: auto;
        min-width: 32px;
        padding: 4px 8px;
      }

      #${MENU_ID}.plmm-collapsed .plmm-body,
      #${MENU_ID} .plmm-card.plmm-collapsed .plmm-section-body,
      #${MENU_ID} .plmm-hidden {
        display: none;
      }

      #${MENU_ID}.plmm-collapsed {
        width: auto;
        min-width: 150px;
        max-height: none;
        overflow: hidden;
      }

      #${MENU_ID}.plmm-collapsed .plmm-header {
        padding: 7px 8px;
        border-bottom: 0;
      }

      #${MENU_ID}.plmm-collapsed .plmm-title {
        font-size: 12px;
      }

      #${MENU_ID} .plmm-status {
        font-size: 11px;
        opacity: 0.75;
        min-height: 16px;
      }

      #${MENU_ID} .plmm-check-row {
        display: flex;
        align-items: center;
        gap: 10px;
        min-height: 34px;
      }

      #${MENU_ID} .plmm-inline-label {
        font-size: 12px;
        font-weight: 700;
        opacity: 0.82;
      }

      #${MENU_ID} .plmm-setting-row {
        display: grid;
        grid-template-columns: 1fr auto auto;
        align-items: center;
        gap: 10px;
        min-height: 34px;
        border-top: 1px solid rgba(255,255,255,0.08);
      }

      #${MENU_ID} .plmm-setting-row:first-child {
        border-top: 0;
      }

      #${MENU_ID} .plmm-lock-label {
        display: flex;
        align-items: center;
        gap: 6px;
        white-space: nowrap;
      }

      #${MENU_ID} .plmm-action-with-icon {
        display: grid;
        grid-template-columns: 1fr auto;
        gap: 10px;
      }

      #${MENU_ID} .plmm-button-row {
        display: grid;
        gap: 10px;
      }

      #${MENU_ID} .plmm-button-row:has(.plmm-hidden) {
        display: none;
      }
    `;

    document.documentElement.appendChild(style);
  }

  function buildMenu() {
    if (document.getElementById(MENU_ID)) return;

    injectStyle();

    const menu = document.createElement('div');
    menu.id = MENU_ID;

    menu.innerHTML = `
      <div class="plmm-header">
        <div class="plmm-title">PokeLike Editor</div>
        <div style="display:flex; gap:6px;">
          <button id="plmm-collapse" class="plmm-mini" title="Collapse">-</button>
        </div>
      </div>

      <div class="plmm-body">
        <div id="plmm-team-actions" class="plmm-card plmm-collapsed">
          <div class="plmm-section-header">
            <div class="plmm-card-title">Team</div>
            <button id="plmm-team-actions-toggle" class="plmm-mini" title="Expand team">+</button>
          </div>

          <div class="plmm-section-body">
            <div class="plmm-subsection-title">Add Pokemon</div>
            <label>
              Pokemon
              <input id="plmm-add-pokemon-search" list="plmm-pokedex-list" type="search" placeholder="Search Pokedex by name or species ID">
            </label>
            <datalist id="plmm-pokedex-list"></datalist>
            <button id="plmm-catch-pokemon" class="plmm-primary">Catch</button>

            <div class="plmm-subsection-title">Edit a Pokemon</div>
            <label>
              Pokemon
              <select id="plmm-team-select"></select>
            </label>

            <div id="plmm-edit-fields" class="plmm-hidden">
              <div class="plmm-grid-3">
                <label>
                  Level
                  <input id="plmm-level" type="number" min="1" max="999">
                </label>

                <label>
                  Current HP
                  <input id="plmm-current-hp" type="number" min="0" max="99999">
                </label>

                <label>
                  Max HP
                  <input id="plmm-max-hp" type="number" min="1" max="99999">
                </label>
              </div>

              <label>
                Move Tier
                <select id="plmm-move-tier">
                  <option value="0">Tier 1</option>
                  <option value="1">Tier 2</option>
                  <option value="2">Tier 3</option>
                </select>
              </label>

              <div class="plmm-check-row">
                <input id="plmm-shiny" type="checkbox">
                <label for="plmm-shiny" style="display:block;">Shiny</label>
              </div>

              <div class="plmm-button-row">
                <button id="plmm-evolve-selected" class="plmm-hidden">Evolve</button>
              </div>

              <div class="plmm-subsection-title">Base Stats</div>
              <div class="plmm-grid-2">
                ${STAT_FIELDS.map(([key, label]) => `
                  <label>
                    ${label}
                    <input id="plmm-stat-${key}" type="number" min="1" max="9999">
                  </label>
                `).join('')}
              </div>

              <div class="plmm-action-with-icon">
                <button id="plmm-apply-edits" class="plmm-primary">Apply Stat Changes</button>
                <button id="plmm-reset-stats" class="plmm-mini" title="Reset base stats">↺</button>
              </div>
              <div class="plmm-button-row">
                <button id="plmm-remove-selected">Remove From Team</button>
              </div>
            </div>

            <div class="plmm-subsection-title">Team Actions</div>
            <div class="plmm-grid-2">
              <button id="plmm-heal-all">Heal All</button>
              <button id="plmm-all-shiny">All Shiny</button>
            </div>

            <div class="plmm-inline-label">Increase level of all by:</div>
            <div class="plmm-grid-3">
              <button id="plmm-level-all-1">+1</button>
              <button id="plmm-level-all-5">+5</button>
              <button id="plmm-level-all-10">+10</button>
            </div>
          </div>
        </div>

        <div id="plmm-run-settings" class="plmm-card plmm-collapsed">
          <div class="plmm-section-header">
            <div>
              <div class="plmm-card-title">Run Settings</div>
              <div class="plmm-subtext">for various Achievements</div>
            </div>
            <button id="plmm-run-settings-toggle" class="plmm-mini" title="Expand run settings">+</button>
          </div>

          <div class="plmm-section-body">
            <button id="plmm-always-roll-shiny">Always Roll Shiny</button>
            <div id="plmm-always-roll-shiny-note" class="plmm-subtext plmm-hidden">Requires browser refresh to turn off</div>

            ${RUN_SETTING_FIELDS.map(([key, label]) => `
              <div class="plmm-setting-row">
                <span>${label}</span>
                <input id="plmm-setting-${key}" data-setting-key="${key}" class="plmm-run-setting-toggle" type="checkbox">
                <label class="plmm-lock-label">
                  <input id="plmm-lock-${key}" data-setting-key="${key}" class="plmm-run-setting-lock" type="checkbox">
                  Lock
                </label>
              </div>
            `).join('')}

            <label>
              Max Team Size
              <input id="plmm-max-team-size" type="number" min="1" max="999">
            </label>
          </div>
        </div>

        <div id="plmm-spawn-actions" class="plmm-card plmm-collapsed">
          <div class="plmm-section-header">
            <div class="plmm-card-title">Items</div>
            <button id="plmm-spawn-toggle" class="plmm-mini" title="Expand items">+</button>
          </div>

          <div class="plmm-section-body">
            <label>
              Spawn Item
              <input id="plmm-spawn-item-search" list="plmm-item-pool-list" type="search" placeholder="Search item pools by name or id">
            </label>
            <datalist id="plmm-item-pool-list"></datalist>
            <button id="plmm-spawn-item" class="plmm-primary">Spawn Item</button>

            <div class="plmm-subsection-title">Current Items</div>
            <label>
              Item
              <select id="plmm-current-item-select"></select>
            </label>
            <button id="plmm-remove-item">Remove Selected Item</button>

          </div>
        </div>

        <div id="plmm-status" class="plmm-status">Ready.</div>
      </div>
    `;

    document.documentElement.appendChild(menu);
    bindEvents();
    refreshItemPoolList();
    refreshPokedexList();
    refreshCurrentItemSelect();
    refreshTeamSelect();
    refreshSelectedCard();
    refreshRunSettings();
  }

  function bindEvents() {
    document.querySelector('#plmm-collapse').addEventListener('click', () => {
      const menu = document.getElementById(MENU_ID);
      const btn = document.querySelector('#plmm-collapse');

      menu.classList.toggle('plmm-collapsed');
      btn.textContent = menu.classList.contains('plmm-collapsed') ? '+' : '-';
    });

    bindSectionToggle('#plmm-team-actions', '#plmm-team-actions-toggle');
    bindSectionToggle('#plmm-run-settings', '#plmm-run-settings-toggle');
    bindSectionToggle('#plmm-spawn-actions', '#plmm-spawn-toggle');

    document.querySelector('#plmm-team-select').addEventListener('change', e => {
      selectedUid = e.target.value;
      refreshSelectedCard();
    });

    document.querySelector('#plmm-apply-edits').addEventListener('click', applySelectedEdits);
    document.querySelector('#plmm-reset-stats').addEventListener('click', resetBaseStatInputs);
    document.querySelector('#plmm-evolve-selected').addEventListener('click', evolveSelectedPokemon);
    document.querySelector('#plmm-remove-selected').addEventListener('click', removeSelectedPokemon);
    document.querySelector('#plmm-catch-pokemon').addEventListener('click', catchSelectedPokemon);
    document.querySelector('#plmm-spawn-item').addEventListener('click', spawnSelectedItem);
    document.querySelector('#plmm-remove-item').addEventListener('click', removeSelectedItem);
    document.querySelector('#plmm-always-roll-shiny').addEventListener('click', enableAlwaysRollShiny);
    document.querySelector('#plmm-max-team-size').addEventListener('change', updateMaxTeamSize);

    for (const input of document.querySelectorAll('.plmm-run-setting-toggle')) {
      input.addEventListener('change', updateRunSetting);
    }

    for (const input of document.querySelectorAll('.plmm-run-setting-lock')) {
      input.addEventListener('change', updateRunSettingLock);
    }

    document.querySelector('#plmm-heal-all').addEventListener('click', () => {
      const team = getTeam();
      if (!team.length) return notify('No team found.');

      for (const mon of team) {
        mon.currentHp = Number(mon.maxHp) || 1;
      }

      renderTeam();
      triggerGameUpdate();
      refreshTeamSelect();
      refreshSelectedCard();
      markSynced();
      notify('Healed all team Pokemon.');
    });

    document.querySelector('#plmm-all-shiny').addEventListener('click', () => {
      const team = getTeam();
      if (!team.length) return notify('No team found.');

      for (const mon of team) {
        mon.isShiny = true;
      }

      renderTeam();
      triggerGameUpdate();
      refreshTeamSelect();
      refreshSelectedCard();
      markSynced();
      notify('Set all team Pokemon to shiny.');
    });

    document.querySelector('#plmm-level-all-1').addEventListener('click', () => increaseAllLevels(1));
    document.querySelector('#plmm-level-all-5').addEventListener('click', () => increaseAllLevels(5));
    document.querySelector('#plmm-level-all-10').addEventListener('click', () => increaseAllLevels(10));
  }

  function bindSectionToggle(cardSelector, buttonSelector) {
    const card = document.querySelector(cardSelector);
    const button = document.querySelector(buttonSelector);
    if (!card || !button) return;

    button.addEventListener('click', () => {
      card.classList.toggle('plmm-collapsed');
      button.textContent = card.classList.contains('plmm-collapsed') ? '+' : '-';
    });
  }

  function enableAlwaysRollShiny() {
    const button = document.querySelector('#plmm-always-roll-shiny');
    const note = document.querySelector('#plmm-always-roll-shiny-note');

    try {
      Function('rollFn', 'rollShiny = rollFn;')(() => true);
    } catch {
      return notify('Could not replace browser function: rollShiny.');
    }

    if (button) {
      button.disabled = true;
      button.textContent = 'Always Roll Shiny: On';
    }

    note?.classList.remove('plmm-hidden');
    notify('Always Roll Shiny enabled. Refresh the browser to turn it off.');
  }

  function updateRunSetting(event) {
    const key = event.target.dataset.settingKey;
    const s = getState();
    if (!s || !key) return notify('Could not find global variable: state.');

    s[key] = event.target.checked;
    if (Object.prototype.hasOwnProperty.call(lockedRunSettings, key)) {
      lockedRunSettings[key] = s[key];
    }

    triggerGameUpdate();
    refreshRunSettings();
    markSynced();
    notify(`Set ${key} to ${s[key]}.`);
  }

  function updateRunSettingLock(event) {
    const key = event.target.dataset.settingKey;
    const s = getState();
    if (!s || !key) return notify('Could not find global variable: state.');

    if (event.target.checked) {
      lockedRunSettings[key] = !!s[key];
      notify(`Locked ${key} to ${lockedRunSettings[key]}.`);
    } else {
      delete lockedRunSettings[key];
      notify(`Unlocked ${key}.`);
    }
  }

  function updateMaxTeamSize(event) {
    const s = getState();
    if (!s) return notify('Could not find global variable: state.');

    s.maxTeamSize = clampNumber(event.target.value, s.maxTeamSize || 1, 1, 999);
    event.target.value = s.maxTeamSize;
    triggerGameUpdate();
    markSynced();
    notify(`Set maxTeamSize to ${s.maxTeamSize}.`);
  }

  function applyLockedRunSettings() {
    const s = getState();
    if (!s) return false;

    let changed = false;
    for (const [key, lockedValue] of Object.entries(lockedRunSettings)) {
      if (s[key] === lockedValue) continue;

      s[key] = lockedValue;
      changed = true;
    }

    if (changed) triggerGameUpdate();
    return changed;
  }

  function refreshRunSettings() {
    const s = getState();
    if (!s) return;

    for (const [key] of RUN_SETTING_FIELDS) {
      const toggle = document.querySelector(`#plmm-setting-${key}`);
      const lock = document.querySelector(`#plmm-lock-${key}`);

      if (toggle) toggle.checked = !!s[key];
      if (lock) lock.checked = Object.prototype.hasOwnProperty.call(lockedRunSettings, key);
    }

    const maxTeamSize = document.querySelector('#plmm-max-team-size');
    if (maxTeamSize) maxTeamSize.value = s.maxTeamSize ?? '';
  }

  function refreshMenuFromState() {
    refreshItemPoolList();
    refreshPokedexList();
    refreshCurrentItemSelect();
    refreshTeamSelect();
    refreshSelectedCard();
    refreshRunSettings();
  }

  function getSyncSnapshot() {
    const s = getState();
    if (!s) return '';

    try {
      return JSON.stringify(s);
    } catch {
      return JSON.stringify({
        team: s.team || [],
        items: s.items || [],
        runSettings: Object.fromEntries(RUN_SETTING_FIELDS.map(([key]) => [key, s[key]])),
        maxTeamSize: s.maxTeamSize
      });
    }
  }

  function syncFromExternalState() {
    const hadLockedChanges = applyLockedRunSettings();
    const snapshot = getSyncSnapshot();
    if (!snapshot) return;

    if (hadLockedChanges || snapshot !== lastSyncSnapshot) {
      lastSyncSnapshot = getSyncSnapshot();
      refreshMenuFromState();
    }
  }

  function markSynced() {
    lastSyncSnapshot = getSyncSnapshot();
  }

  function refreshTeamSelect() {
    const select = document.querySelector('#plmm-team-select');
    if (!select) return;

    const team = getTeam();
    const previous = selectedUid ?? select.value;

    select.innerHTML = '';

    const noneOpt = document.createElement('option');
    noneOpt.value = '';
    noneOpt.textContent = 'None';
    select.appendChild(noneOpt);

    if (!team.length) {
      notify('Could not find global variable: state.team');
      selectedUid = '';
      select.value = '';
      return;
    }

    for (const mon of team) {
      const opt = document.createElement('option');
      opt.value = String(mon._uid);
      opt.textContent = `${mon.isShiny ? '★ ' : ''}${displayName(mon)} | Lv ${mon.level} | HP ${mon.currentHp}/${mon.maxHp}`;
      select.appendChild(opt);
    }

    if (previous && [...select.options].some(o => o.value === String(previous))) {
      select.value = String(previous);
      selectedUid = String(previous);
    } else {
      selectedUid = '';
      select.value = '';
    }
  }

  function refreshItemPoolList() {
    const list = document.querySelector('#plmm-item-pool-list');
    if (!list) return;

    const pool = getItemPool();
    list.innerHTML = '';

    for (const item of pool) {
      const opt = document.createElement('option');
      opt.value = itemDisplayName(item);
      list.appendChild(opt);
    }
  }

  function refreshPokedexList() {
    const list = document.querySelector('#plmm-pokedex-list');
    if (!list) return;

    const entries = getPokedexEntries();
    list.innerHTML = '';

    for (const entry of entries) {
      const opt = document.createElement('option');
      opt.value = pokedexDisplayName(entry);
      list.appendChild(opt);
    }
  }

  function getSelectedPokedexSpeciesId() {
    const input = document.querySelector('#plmm-add-pokemon-search');
    const value = input?.value.trim();
    if (!value) return '';

    const idMatch = value.match(/#(\d+)/);
    if (idMatch) return idMatch[1];

    const entries = getPokedexEntries();
    const lowerValue = value.toLowerCase();
    const match = entries.find(entry =>
      entry.speciesId === value ||
      entry.name.toLowerCase() === lowerValue ||
      pokedexDisplayName(entry).toLowerCase() === lowerValue
    );

    return match?.speciesId || '';
  }

  function refreshCurrentItemSelect() {
    const select = document.querySelector('#plmm-current-item-select');
    if (!select) return;

    const s = getState();
    const items = s && Array.isArray(s.items) ? s.items : [];
    const previous = select.value;

    select.innerHTML = '';

    const noneOpt = document.createElement('option');
    noneOpt.value = '';
    noneOpt.textContent = items.length ? 'Select item' : 'No items';
    select.appendChild(noneOpt);

    for (const [index, item] of items.entries()) {
      const opt = document.createElement('option');
      opt.value = String(index);
      opt.textContent = itemDisplayName(item);
      select.appendChild(opt);
    }

    if (previous && [...select.options].some(o => o.value === previous)) {
      select.value = previous;
    }
  }

  function refreshSelectedCard() {
    const mon = getSelectedMon();
    const editFields = document.querySelector('#plmm-edit-fields');
    const evolveButton = document.querySelector('#plmm-evolve-selected');

    if (!mon) {
      editFields?.classList.add('plmm-hidden');
      evolveButton?.classList.add('plmm-hidden');
      clearInputs();
      return;
    }

    editFields?.classList.remove('plmm-hidden');
    evolveButton?.classList.toggle('plmm-hidden', !canSelectedSpeciesEvolve(mon));

    document.querySelector('#plmm-level').value = mon.level ?? 1;
    document.querySelector('#plmm-current-hp').value = mon.currentHp ?? 0;
    document.querySelector('#plmm-max-hp').value = mon.maxHp ?? 1;
    document.querySelector('#plmm-move-tier').value = mon.moveTier ?? 0;
    document.querySelector('#plmm-shiny').checked = !!mon.isShiny;

    const baseStats = mon.baseStats || {};

    for (const [key] of STAT_FIELDS) {
      document.querySelector(`#plmm-stat-${key}`).value = baseStats[key] ?? 1;
    }
  }

  function clearInputs() {
    for (const input of document.querySelectorAll('#plmm-edit-fields input')) {
      if (input.type === 'checkbox') {
        input.checked = false;
      } else {
        input.value = '';
      }
    }

    const moveTier = document.querySelector('#plmm-move-tier');
    if (moveTier) moveTier.value = '0';
  }

  function applySelectedEdits() {
    const mon = getSelectedMon();
    if (!mon) return notify('No selected Pokemon found.');

    mon.level = clampNumber(document.querySelector('#plmm-level').value, mon.level || 1, 1, 999);

    mon.maxHp = clampNumber(document.querySelector('#plmm-max-hp').value, mon.maxHp || 1, 1, 99999);
    mon.currentHp = clampNumber(
      document.querySelector('#plmm-current-hp').value,
      mon.currentHp || mon.maxHp,
      0,
      99999
    );

    mon.moveTier = clampNumber(document.querySelector('#plmm-move-tier').value, mon.moveTier || 0, 0, 2);
    mon.isShiny = document.querySelector('#plmm-shiny').checked;

    const nextBaseStats = {};

    for (const [key] of STAT_FIELDS) {
      nextBaseStats[key] = clampNumber(
        document.querySelector(`#plmm-stat-${key}`).value,
        mon.baseStats?.[key] || 1,
        1,
        9999
      );
    }

    mon.baseStats = nextBaseStats;

    renderTeam();
    triggerGameUpdate();
    refreshTeamSelect();
    refreshSelectedCard();
    markSynced();
    notify(`Applied edits to ${displayName(mon)}.`);
  }

  async function resetBaseStatInputs() {
    const mon = getSelectedMon();
    if (!mon || mon.speciesId == null) return notify('No selected Pokemon found.');

    const button = document.querySelector('#plmm-reset-stats');
    if (button) button.disabled = true;

    try {
      const basePokemon = await Function('speciesId', `
        return (async () => {
          if (typeof fetchPokemonById !== "function") throw new Error("fetchPokemonById is not available");
          return fetchPokemonById(speciesId);
        })();
      `)(mon.speciesId);

      const baseStats = basePokemon?.baseStats;
      if (!baseStats) return notify(`No baseStats found for ${displayName(mon)}.`);

      for (const [key] of STAT_FIELDS) {
        document.querySelector(`#plmm-stat-${key}`).value = baseStats[key] ?? 1;
      }

      notify(`Reset stat inputs for ${displayName(mon)}. Click Apply Stat Changes to save.`);
    } catch (error) {
      notify(error?.message || 'Could not reset base stats.');
    } finally {
      if (button) button.disabled = false;
    }
  }

  function evolveSelectedPokemon() {
    const s = getState();
    const mon = getSelectedMon();
    if (!s || !Array.isArray(s.team) || !mon) return notify('No selected Pokemon found.');
    if (!canSelectedSpeciesEvolve(mon)) return notify(`${displayName(mon)} cannot evolve.`);

    const index = s.team.findIndex(candidate => String(candidate._uid) === String(mon._uid));
    if (index < 0) return notify('Selected Pokemon is no longer in state.team.');

    const oldName = displayName(mon);
    const evolved = applyEvolutionAtIndex(index);
    if (!evolved) return notify('applyEvolution(state.team[n]) was not available.');

    renderTeam();
    triggerGameUpdate();
    refreshTeamSelect();
    refreshSelectedCard();
    markSynced();
    notify(`Evolved ${oldName}.`);
  }

  function removeSelectedPokemon() {
    const s = getState();
    const mon = getSelectedMon();
    if (!s || !Array.isArray(s.team) || !mon) return notify('No selected Pokemon found.');

    const index = s.team.findIndex(candidate => String(candidate._uid) === String(mon._uid));
    if (index < 0) return notify('Selected Pokemon is no longer in state.team.');

    if (!window.confirm(`Remove ${displayName(mon)} from your team?`)) return;

    const removed = s.team.splice(index, 1)[0];
    selectedUid = '';

    renderTeam();
    triggerGameUpdate();
    refreshTeamSelect();
    refreshSelectedCard();
    markSynced();
    notify(`Removed ${displayName(removed)} from the team.`);
  }

  async function catchSelectedPokemon() {
    const s = getState();
    if (!s) return notify('Could not find global variable: state.');

    const speciesId = getSelectedPokedexSpeciesId();
    if (!speciesId) return notify('Select a Pokemon from the Pokedex list first.');

    const node = s.map?.nodes?.n0_0;
    if (!node) return notify('Could not find state.map.nodes.n0_0.');

    const button = document.querySelector('#plmm-catch-pokemon');
    const input = document.querySelector('#plmm-add-pokemon-search');
    if (button) button.disabled = true;

    try {
      const caughtName = await Function('speciesId', 'node', `
        return (async () => {
          if (typeof fetchPokemonById !== "function") throw new Error("fetchPokemonById is not available");
          if (typeof createInstance !== "function") throw new Error("createInstance is not available");
          if (typeof catchPokemon !== "function") throw new Error("catchPokemon is not available");
          if (typeof getLevelForNode !== "function") throw new Error("getLevelForNode is not available");

          const pkm = await fetchPokemonById(speciesId);
          const level = getLevelForNode(node);
          const pkmInstance = createInstance(pkm, level, false, 0x0);
          const previousCurrentNode = state?.map?.currentNode;
          catchPokemon(pkmInstance, node);
          if (state?.map) state.map.currentNode = previousCurrentNode;
          return pkmInstance?.name || pkm?.name || "Pokemon";
        })();
      `)(speciesId, node);

      if (input) input.value = '';
      renderTeam();
      triggerGameUpdate();
      refreshTeamSelect();
      refreshSelectedCard();
      markSynced();
      notify(`Caught ${caughtName}.`);
    } catch (error) {
      notify(error?.message || 'Could not catch selected Pokemon.');
    } finally {
      if (button) button.disabled = false;
    }
  }

  function spawnSelectedItem() {
    const s = getState();
    if (!s) return notify('Could not find global variable: state.');

    const input = document.querySelector('#plmm-spawn-item-search');
    const value = input?.value.trim();
    if (!value) return notify('Choose an item from ITEM_POOL or USABLE_ITEM_POOL first.');

    const pool = getItemPool();
    const item = pool.find(candidate =>
      String(candidate?.name || '').toLowerCase() === value.toLowerCase() ||
      String(candidate?.id || '').toLowerCase() === value.toLowerCase()
    );

    if (!item) return notify(`No item pool entry matched "${value}".`);
    if (!Array.isArray(s.items)) s.items = [];

    const spawnedItem = cloneItem(item);
    s.items.push(spawnedItem);
    input.value = '';

    renderItems();
    triggerGameUpdate();
    refreshCurrentItemSelect();
    markSynced();
    notify(`Spawned item: ${itemDisplayName(spawnedItem)}.`);
  }

  function removeSelectedItem() {
    const s = getState();
    if (!s || !Array.isArray(s.items)) return notify('No state.items found.');

    const select = document.querySelector('#plmm-current-item-select');
    const index = Number(select?.value);
    if (!Number.isInteger(index) || index < 0 || index >= s.items.length) {
      return notify('Select a current item to remove.');
    }

    const removed = s.items.splice(index, 1)[0];

    renderItems();
    triggerGameUpdate();
    refreshCurrentItemSelect();
    markSynced();
    notify(`Removed item: ${itemDisplayName(removed)}.`);
  }

  function increaseAllLevels(amount) {
    const team = getTeam();
    if (!team.length) return notify('No team found.');

    for (const mon of team) {
      mon.level = Math.min(999, Math.max(1, Number(mon.level || 1) + amount));
    }

    renderTeam();
    triggerGameUpdate();
    refreshTeamSelect();
    refreshSelectedCard();
    markSynced();
    notify(`Increased all team levels by ${amount}.`);
  }

  function init() {
    buildMenu();
    markSynced();
    setInterval(syncFromExternalState, 500);

    setTimeout(() => {
      refreshItemPoolList();
      refreshPokedexList();
      refreshCurrentItemSelect();
      refreshTeamSelect();
      refreshSelectedCard();
      refreshRunSettings();
      markSynced();
    }, 1000);

    setTimeout(() => {
      refreshItemPoolList();
      refreshPokedexList();
      refreshCurrentItemSelect();
      refreshTeamSelect();
      refreshSelectedCard();
      refreshRunSettings();
      markSynced();
    }, 3000);
  }

  init();
})();