Pokesurvivor Script

10/19/2026 14:14 asdfmovie2012#1
Script für Pokesurvivor Tampermonkey

HTML Code:
// ==UserScript==
// @name         Diti´s Trainer
// [MENTION=1005418]namespace[/MENTION]    [url]https://pokesurvivor.com/[/url]
// [MENTION=1805674]Version[/MENTION]      1.20.0
// @match        [url]https://pokesurvivor.com/*[/url]
// @match        [url]https://www.pokesurvivor.com/*[/url]
// [MENTION=341675]run[/MENTION]-at       document-start
// [MENTION=2773606]Sandbox[/MENTION]      raw
// @grant        none
// [MENTION=365995]Nof[/MENTION]rames
// ==/UserScript==

(() => {
  'use strict';

  const PAGE_MARK = 'data-ditis-trainer';

  function trainerMain() {
  'use strict';
  const rootElement = document.documentElement;
  if (rootElement && rootElement.hasAttribute('data-ditis-trainer')) return;
  if (rootElement) rootElement.setAttribute('data-ditis-trainer', '1');

  const HOOK = 'EnemyMgr';
  const SEARCH_RADII = [768, 1536, 3072];
  const UPDATE_MS = 100;
  const HOOK_TIMEOUT_MS = 20_000;
  const MIN_ZOOM = 0.31;
  const ZOOM_STEP = 1.18;
  const RIFT_DISTANCE = 96;
  const RIFT_TTL = 45;
  const FAST_CHEST_RADIUS = 56;
  const BERRY_INTERVAL_MS = 2500;
  const BOMB_INTERVAL_MS = 500;
  const BERRY_MIN_DISTANCE = 72;
  const BERRY_MAX_DISTANCE = 128;
  const BERRY_NEARBY_LIMIT = 4;
  const TIME_JUMP_SECONDS = 29 * 60;
  const BOND_MAX = 12;
  const MOVE_TYPE_LABELS = {
    normal: 'Normal', fire: 'Feuer', water: 'Wasser',
    electric: 'Elektro', grass: 'Pflanze', ice: 'Eis',
    fighting: 'Kampf', poison: 'Gift', ground: 'Boden',
    flying: 'Flug', psychic: 'Psycho', bug: 'Käfer',
    rock: 'Gestein', ghost: 'Geist', dragon: 'Drache',
    dark: 'Unlicht', steel: 'Stahl', fairy: 'Fee',
  };
  const STORAGE_KEY = 'ps-hud-settings-v1';
  const DEFAULT_SETTINGS = {
    rift: true,
    fastChest: false,
    oneHit: false,
    judgmentOneHit: false,
    autoPick: false,
    noTabPause: true,
    zeroXp: false,
    bondMax: true,
    autoBerry: false,
    instaBomb: false,
    noClip: false,
    environmentImmune: false,
    invincible: false,
    superMagnet: false,
    statEditor: false,
    statSpeed: 1,
    statAttack: 1,
    statAttackSpeed: 1,
    statCoins: 1,
    groundXp: 1,
    opacity: 0.9,
  };

  let engine = null;
  let hookActive = false;
  let ui = null;
  let timer = 0;
  let zoom = 1;
  let appliedZoom = 1;
  let zoomCanvas = null;
  let zoomListenersInstalled = false;
  let outerRadiusWrapped = false;
  let noticeTimer = 0;
  let lastBerrySpawnAt = 0;
  let lastBombAt = 0;
  let worldCollideWrapped = false;
  let combatResolveWrapped = false;
  let levelUpWrapped = false;
  let bondMaxPatched = false;
  let legacyMoveTypes = false;
  const playerStatStates = new WeakMap();
  const playerHurtHooks = new WeakMap();
  const ********dateHooks = new WeakMap();
  const playerXpHooks = new WeakMap();
  const legacyTypeRestored = new WeakSet();

  function loadFactor(value, min, max) {
    const number = Number(value);
    return Number.isFinite(number) ? clamp(number, min, max) : 1;
  }

  function loadSettings() {
    try {
      const saved = JSON.parse(window.localStorage.getItem(STORAGE_KEY) || '{}');
      legacyMoveTypes = Boolean(saved.moveTypes && typeof saved.moveTypes === 'object'
        && Object.keys(saved.moveTypes).length);
      return {
        rift: saved.rift !== false,
        fastChest: saved.fastChest === true,
        oneHit: saved.oneHit === true,
        judgmentOneHit: saved.judgmentOneHit === true,
        autoPick: saved.autoPick === true,
        noTabPause: saved.noTabPause !== false,
        zeroXp: saved.zeroXp === true,
        bondMax: saved.bondMax !== false,
        autoBerry: saved.autoBerry === true,
        instaBomb: saved.instaBomb === true,
        noClip: saved.noClip === true,
        environmentImmune: saved.environmentImmune === true,
        invincible: saved.invincible === true,
        superMagnet: saved.superMagnet === true,
        statEditor: saved.statEditor === true,
        statSpeed: loadFactor(saved.statSpeed, 0.1, 20),
        statAttack: loadFactor(saved.statAttack, 0.1, 100),
        statAttackSpeed: loadFactor(saved.statAttackSpeed, 0.1, 20),
        statCoins: loadFactor(saved.statCoins, 0.1, 100),
        groundXp: loadFactor(saved.groundXp, 1, 20),
        opacity: Number.isFinite(saved.opacity)
          ? Math.max(0.25, Math.min(1, saved.opacity))
          : DEFAULT_SETTINGS.opacity,
      };
    } catch (error) {
      return { ...DEFAULT_SETTINGS };
    }
  }

  let settings = loadSettings();

  function saveSettings() {
    try {
      window.localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
    } catch (error) {
      console.warn('[PS-HUD] Einstellungen konnten nicht gespeichert werden.', error);
    }
  }

  const proto = Object.prototype;
  const previousDescriptor = Object.getOwnPropertyDescriptor(proto, HOOK);

  function restoreHook() {
    if (!hookActive) return;
    hookActive = false;
    try {
      if (previousDescriptor) {
        Object.defineProperty(proto, HOOK, previousDescriptor);
      } else {
        delete proto[HOOK];
      }
    } catch (error) {
      console.warn('[PS-Kompass] Zugriffshaken konnte nicht entfernt werden.', error);
    }
  }


  const I18N_HOOK = 'I18n';
  const englishNames = new Map();
  const previousI18nDescriptor = Object.getOwnPropertyDescriptor(proto, I18N_HOOK);
  let i18nHookActive = false;

  function normalizeName(text) {
    return String(text).replace(/\s+/g, ' ').trim();
  }

  function rememberTranslations(dictionary) {
    if (!dictionary || typeof dictionary !== 'object') return;
    for (const [spanish, english] of Object.entries(dictionary)) {
      if (typeof english === 'string' && english) {
        englishNames.set(normalizeName(spanish), english);
      }
    }
  }

  function englishName(name) {
    if (!name) return name;
    const key = normalizeName(name);
    if (englishNames.has(key)) return englishNames.get(key);
    // Ersatzweise die Übersetzung des Spiels, falls es selbst auf Englisch steht.
    const i18n = engine && engine.I18n;
    if (i18n && typeof i18n.t === 'function') {
      try {
        const translated = i18n.t(key);
        if (typeof translated === 'string' && translated) return translated;
      } catch (error) {
        // Übersetzung nicht verfügbar.
      }
    }
    return name;
  }

  function wrapI18n(i18n) {
    if (!i18n || typeof i18n.add !== 'function' || i18n.add.trainerWrapped) return;
    const originalAdd = i18n.add;
    const wrappedAdd = function trainerI18nAdd(dictionary, ...rest) {
      rememberTranslations(dictionary);
      return originalAdd.call(this, dictionary, ...rest);
    };
    wrappedAdd.trainerWrapped = true;
    i18n.add = wrappedAdd;
  }

  function restoreI18nHook() {
    if (!i18nHookActive) return;
    i18nHookActive = false;
    try {
      if (previousI18nDescriptor) {
        Object.defineProperty(proto, I18N_HOOK, previousI18nDescriptor);
      } else {
        delete proto[I18N_HOOK];
      }
    } catch (error) {
      console.warn('[PS-HUD] I18n-Haken konnte nicht entfernt werden.', error);
    }
  }

  function installI18nHook() {
    if (previousI18nDescriptor && previousI18nDescriptor.configurable === false) return;
    Object.defineProperty(proto, I18N_HOOK, {
      configurable: true,
      enumerable: false,
      get() {
        return undefined;
      },
      set(value) {
        Object.defineProperty(this, I18N_HOOK, {
          configurable: true,
          enumerable: true,
          writable: true,
          value,
        });
        const looksLikeI18n = value && typeof value.add === 'function'
          && typeof value.t === 'function';
        if (looksLikeI18n) {
          wrapI18n(value);
          restoreI18nHook();
        }
      },
    });
    i18nHookActive = true;
    window.setTimeout(restoreI18nHook, HOOK_TIMEOUT_MS);
  }

  function installHook() {
    if (previousDescriptor && previousDescriptor.configurable === false) {
      console.warn('[PS-Kompass] EnemyMgr kann nicht beobachtet werden.');
      return;
    }

    Object.defineProperty(proto, HOOK, {
      configurable: true,
      enumerable: false,
      get() {
        return undefined;
      },
      set(value) {
        Object.defineProperty(this, HOOK, {
          configurable: true,
          enumerable: true,
          writable: true,
          value,
        });

        const looksLikeGameManager = value
          && typeof value.all === 'function'
          && typeof value.add === 'function'
          && typeof value.bossAlive === 'function';

        if (looksLikeGameManager) {
          engine = this;
          restoreHook();
          start();
        }
      },
    });

    hookActive = true;
    window.setTimeout(() => {
      if (!engine) {
        restoreHook();
        console.warn('[PS-Kompass] Spiel-API nicht gefunden; vermutlich wurde app.min.js geändert.');
      }
    }, HOOK_TIMEOUT_MS);
  }

  function createUI() {
    if (ui || !document.documentElement) return Boolean(ui);

    const host = document.createElement('div');
    host.id = 'ps-compass-host';
    host.setAttribute('aria-label', 'Diti´s Trainer');
    const shadow = host.attachShadow({ mode: 'closed' });

    const style = document.createElement('style');
    style.textContent = `
      :host {
        all: initial;
        --hud-opacity: .9;
        --red: #8f1824;
        --red-dark: #3a090e;
        --gray-1: #17181b;
        --gray-2: #292b30;
        --gray-3: #42454c;
        --text: #e7e8eb;
        position: fixed;
        inset: 0;
        z-index: 2147483646;
        pointer-events: none;
        user-select: none;
      }
      .stack {
        position: absolute;
        inset: 0;
      }
      .indicator {
        position: absolute;
        left: -100px;
        top: -100px;
        box-sizing: border-box;
        width: 46px;
        height: 46px;
        border: 2px solid #9c9fa6;
        border-radius: 50%;
        background: linear-gradient(145deg, var(--gray-3), var(--gray-1));
        box-shadow: 0 2px 8px rgba(0,0,0,.72), inset 0 0 0 3px var(--red-dark);
        opacity: var(--hud-opacity);
        transform: translate(-50%, -50%);
        transition: left .1s linear, top .1s linear, opacity .1s linear;
        will-change: left, top;
      }
      .indicator.hidden {
        visibility: hidden;
        opacity: 0;
      }
      .needle {
        position: absolute;
        inset: 3px;
        transform: rotate(0deg);
        transition: transform .1s linear;
        will-change: transform;
      }
      .needle::before {
        content: '';
        position: absolute;
        left: 50%;
        top: -2px;
        width: 0;
        height: 0;
        transform: translateX(-50%);
        border-left: 5px solid transparent;
        border-right: 5px solid transparent;
        border-bottom: 10px solid #d83846;
        filter: drop-shadow(0 1px 1px #000);
      }
      .icon {
        position: absolute;
        left: 50%;
        top: 50%;
        transform: translate(-50%, -46%);
      }
      .smallchest-icon,
      .bigchest-icon {
        box-sizing: border-box;
        width: 21px;
        height: 14px;
        border: 2px solid #3b2108;
        border-radius: 2px;
        background: linear-gradient(#d58a32 0 42%, #f2c443 43% 60%, #a9581f 61%);
        box-shadow: inset 0 2px #f0ae55;
      }
      .smallchest-icon::before,
      .bigchest-icon::before {
        content: '';
        position: absolute;
        left: -2px;
        top: -6px;
        box-sizing: border-box;
        width: 21px;
        height: 7px;
        border: 2px solid #3b2108;
        border-radius: 7px 7px 1px 1px;
        background: #bd6d27;
      }
      .smallchest-icon::after,
      .bigchest-icon::after {
        content: '';
        position: absolute;
        left: 7px;
        top: 3px;
        width: 4px;
        height: 6px;
        border: 1px solid #3b2108;
        background: #ffe06a;
      }
      .bigchest-icon {
        width: 25px;
        height: 17px;
        background: linear-gradient(#e3a33a 0 38%, #fff08a 39% 60%, #bd6d27 61%);
        box-shadow: inset 0 2px #ffd46a, 0 0 5px rgba(255,210,63,.85);
      }
      .bigchest-icon::before {
        width: 25px;
        height: 8px;
        top: -7px;
        background: #d88b2f;
      }
      .bigchest-icon::after {
        left: 9px;
        top: 4px;
      }
      .boss-icon {
        color: #e24a56;
        font: 22px/1 "Segoe UI Symbol", sans-serif;
        text-shadow: 0 1px 2px #000, 0 0 5px rgba(255,95,109,.8);
      }
      .distance {
        position: absolute;
        left: 50%;
        bottom: 1px;
        min-width: 26px;
        transform: translateX(-50%);
        color: #fff;
        font: 700 9px/1 "Segoe UI", sans-serif;
        text-align: center;
        text-shadow: 0 1px 2px #000, 1px 0 2px #000, -1px 0 2px #000;
        white-space: nowrap;
      }
      .zoom-readout {
        position: fixed;
        left: 50%;
        bottom: max(10px, env(safe-area-inset-bottom));
        transform: translateX(-50%);
        box-sizing: border-box;
        padding: 5px 9px;
        border: 1px solid #777b83;
        border-radius: 8px;
        color: var(--text);
        background: linear-gradient(180deg, var(--gray-2), var(--gray-1));
        box-shadow: 0 2px 7px rgba(0,0,0,.6);
        opacity: var(--hud-opacity);
        font: 700 12px/1.15 "Segoe UI", sans-serif;
        text-shadow: 0 1px 2px #000;
        white-space: nowrap;
      }
      .zoom-readout.hidden { display: none; }
      .notice {
        position: fixed;
        left: 50%;
        top: max(14px, env(safe-area-inset-top));
        transform: translateX(-50%);
        box-sizing: border-box;
        max-width: min(88vw, 460px);
        padding: 7px 11px;
        border: 1px solid #a92a35;
        border-radius: 8px;
        color: var(--text);
        background: linear-gradient(180deg, #481016, var(--gray-1));
        box-shadow: 0 2px 8px rgba(0,0,0,.65);
        opacity: var(--hud-opacity);
        font: 700 12px/1.2 "Segoe UI", sans-serif;
        text-align: center;
        text-shadow: 0 1px 2px #000;
        white-space: nowrap;
      }
      .notice.hidden { display: none; }
      .hud-panel {
        pointer-events: auto;
        opacity: var(--hud-opacity);
        color: var(--text);
        font-family: "Segoe UI", sans-serif;
        position: fixed;
        right: max(10px, env(safe-area-inset-right));
        top: max(10px, env(safe-area-inset-top));
        box-sizing: border-box;
        width: min(280px, calc(100vw - 20px));
        padding: 0 12px 12px;
        border: 1px solid #686b72;
        border-top: 3px solid var(--red);
        border-radius: 7px;
        background: linear-gradient(150deg, #2c2e33, #141518 72%);
        box-shadow: 0 8px 26px rgba(0,0,0,.78);
      }
      .hud-panel.collapsed {
        width: 112px;
        padding-bottom: 0;
      }
      .hud-panel.collapsed .panel-body { display: none; }
      .panel-head {
        display: flex;
        align-items: center;
        justify-content: space-between;
        margin: 0 -12px 8px;
        padding: 9px 10px 8px 12px;
        background: linear-gradient(90deg, #5d0e16, #2a1619 68%, transparent);
      }
      .hud-panel.collapsed .panel-head { margin-bottom: 0; }
      .panel-title { font-size: 14px; font-weight: 800; letter-spacing: .3px; }
      .panel-close {
        width: 25px;
        height: 25px;
        border: 1px solid #777b83;
        border-radius: 4px;
        color: #ddd;
        background: #25272b;
        font: 700 16px/1 sans-serif;
        cursor: pointer;
      }
      .panel-body {
        max-height: calc(100vh - 72px);
        padding-top: 2px;
        overflow-y: auto;
        scrollbar-width: thin;
      }
      .control-row {
        display: flex;
        align-items: center;
        min-height: 30px;
        gap: 8px;
        border-bottom: 1px solid rgba(125,128,135,.2);
        font-size: 12px;
      }
      .control-row label {
        display: flex;
        flex: 1;
        align-items: center;
        gap: 8px;
        cursor: pointer;
      }
      .control-row input[type="checkbox"] { accent-color: var(--red); }
      .stat-editor {
        margin: 3px 0 7px;
        padding: 5px 7px;
        border: 1px solid rgba(125,128,135,.28);
        border-radius: 5px;
        background: rgba(10,10,12,.28);
      }
      .stat-editor.inactive { opacity: .48; }
      .stat-row {
        display: grid;
        grid-template-columns: 1fr 70px;
        align-items: center;
        gap: 8px;
        min-height: 27px;
        color: #d7d8dc;
        font-size: 11px;
      }
      .stat-row input {
        box-sizing: border-box;
        width: 70px;
        padding: 3px 5px;
        border: 1px solid #64676e;
        border-radius: 4px;
        color: #fff;
        background: #1a1b1f;
        font: 700 11px/1 "Segoe UI", sans-serif;
        text-align: right;
      }
      .stat-row input:focus { border-color: var(--red); outline: none; }
      .xp-multiplier-row input {
        flex: 1;
        min-width: 70px;
        accent-color: var(--red);
      }
      .xp-multiplier-value {
        width: 32px;
        color: #ffd95e;
        font-weight: 800;
        text-align: right;
      }
      .move-editor {
        display: grid;
        grid-template-columns: 1fr 82px;
        gap: 6px;
        margin: 7px 0;
        padding: 7px;
        border: 1px solid rgba(143,24,36,.6);
        border-radius: 5px;
        background: rgba(38,8,12,.34);
      }
      .move-editor-title {
        grid-column: 1 / -1;
        color: #d7d8dc;
        font: 800 11px/1.2 "Segoe UI", sans-serif;
      }
      .move-editor select,
      .move-editor button {
        box-sizing: border-box;
        min-width: 0;
        height: 28px;
        border: 1px solid #64676e;
        border-radius: 4px;
        color: #f0f0f2;
        background: #1a1b1f;
        font: 700 11px/1 "Segoe UI", sans-serif;
      }
      .move-editor select { padding: 3px 5px; }
      .move-editor button { border-color: #8f3039; cursor: pointer; }
      .move-editor button:hover:not(:disabled) { background: #521018; }
      .move-editor button:disabled,
      .move-editor select:disabled { cursor: default; opacity: .45; }
      .move-tabs {
        grid-column: 1 / -1;
        display: flex;
        flex-wrap: wrap;
        gap: 4px;
      }
      .move-editor .move-tab {
        flex: 1 1 calc(50% - 4px);
        height: auto;
        min-height: 30px;
        padding: 4px 6px;
        border-color: #64676e;
        text-align: left;
        line-height: 1.2;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
      }
      .move-editor .move-tab small {
        display: block;
        color: #b9bbc1;
        font-weight: 600;
        font-size: 10px;
      }
      .move-editor .move-tab.active {
        border-color: #d83846;
        background: linear-gradient(180deg, #5d0e16, #3a090e);
        color: #fff;
      }
      .move-editor .move-tab.active small { color: #ffd95e; }
      .move-empty {
        grid-column: 1 / -1;
        color: #a9abb1;
        font-size: 11px;
      }
      .move-detail {
        grid-column: 1 / -1;
        display: grid;
        grid-template-columns: 1fr 1fr;
        gap: 6px;
        padding: 7px;
        border: 1px solid rgba(216,56,70,.45);
        border-radius: 5px;
        background: rgba(10,10,12,.35);
      }
      .move-detail.hidden { display: none; }
      .move-info {
        grid-column: 1 / -1;
        color: #d7d8dc;
        font-size: 11px;
        line-height: 1.4;
      }
      .move-info b { color: #fff; font-size: 12px; }
      .move-level-bar {
        grid-column: 1 / -1;
        height: 5px;
        border-radius: 3px;
        background: #2a2c31;
        overflow: hidden;
      }
      .move-level-fill {
        height: 100%;
        width: 0;
        background: linear-gradient(90deg, #8f1824, #ffd95e);
      }
      .move-section-label {
        grid-column: 1 / -1;
        margin-top: 3px;
        color: #a9abb1;
        font: 700 10px/1.2 "Segoe UI", sans-serif;
        text-transform: uppercase;
        letter-spacing: .4px;
      }
      .move-replacement { grid-column: 1 / -1; }
      .move-replace { grid-column: 1 / -1; }
      .time-jump {
        width: 100%;
        min-height: 29px;
        margin: 6px 0 2px;
        border: 1px solid #8f3039;
        border-radius: 4px;
        color: #f2f2f4;
        background: linear-gradient(180deg, #501018, #2d0b10);
        font: 800 11px/1 "Segoe UI", sans-serif;
        cursor: pointer;
      }
      .time-jump:hover { background: linear-gradient(180deg, #68151f, #390d13); }
      .opacity-row input { flex: 1; accent-color: var(--red); }
      .opacity-value { width: 38px; color: #d7d8dc; text-align: right; }
      @media (max-width: 600px) {
        .indicator { width: 42px; height: 42px; }
        .zoom-readout { font-size: 10px; bottom: max(6px, env(safe-area-inset-bottom)); }
      }
    `;

    const stack = document.createElement('div');
    stack.className = 'stack';

    function makeIndicator(kind, label) {
      const indicator = document.createElement('div');
      indicator.className = `indicator hidden ${kind}`;
      indicator.setAttribute('role', 'img');
      indicator.setAttribute('aria-label', label);
      indicator.title = label;

      const needle = document.createElement('div');
      needle.className = 'needle';

      const icon = document.createElement('div');
      icon.className = `icon ${kind}-icon`;
      if (kind === 'boss') icon.textContent = '☠';

      const distance = document.createElement('span');
      distance.className = 'distance';

      indicator.append(needle, icon, distance);
      stack.append(indicator);
      return { indicator, needle, distance, angle: null };
    }

    const smallChest = makeIndicator('smallchest', 'Nächste kleine Truhe');
    const bigChest = makeIndicator('bigchest', 'Nächste große Truhe');
    const boss = makeIndicator('boss', 'Nächster lebender Boss');
    const zoomReadout = document.createElement('div');
    zoomReadout.className = 'zoom-readout hidden';
    const notice = document.createElement('div');
    notice.className = 'notice hidden';

    const panel = document.createElement('section');
    panel.className = 'hud-panel';

    const panelHead = document.createElement('div');
    panelHead.className = 'panel-head';
    const panelTitle = document.createElement('span');
    panelTitle.className = 'panel-title';
    panelTitle.textContent = 'Diti´s Trainer';
    const panelClose = document.createElement('button');
    panelClose.className = 'panel-close';
    panelClose.type = 'button';
    panelClose.textContent = '+';
    panelHead.append(panelTitle, panelClose);
    panel.append(panelHead);

    const panelBody = document.createElement('div');
    panelBody.className = 'panel-body';

    function makeCheckRow(text) {
      const row = document.createElement('div');
      row.className = 'control-row';
      const label = document.createElement('label');
      const check = document.createElement('input');
      check.type = 'checkbox';
      const span = document.createElement('span');
      span.textContent = text;
      label.append(check, span);
      row.append(label);
      panelBody.append(row);
      return check;
    }

    const riftCheck = makeCheckRow('Rift öffnen (R)');
    const fastChestCheck = makeCheckRow('Schnelle Truhenöffnung');
    const oneHitCheck = makeCheckRow('One-Hit-Kills');
    const judgmentOneHitCheck = makeCheckRow('Judgment-OHK (Arceus-Phase)');
    const autoPickCheck = makeCheckRow('Auto-Pick Fähigkeit');
    const noTabPauseCheck = makeCheckRow('Keine Pause bei Tabwechsel');
    const autoBerryCheck = makeCheckRow('Auto-Sitrusbeeren');
    const instaBombCheck = makeCheckRow('Insta-Bombe (0,5 s)');
    const noClipCheck = makeCheckRow('Durch Wände laufen');
    const environmentImmuneCheck = makeCheckRow('Lava/Gift/Wasser immun');
    const invincibleCheck = makeCheckRow('Unverwundbar');
    const superMagnetCheck = makeCheckRow('Super-Magnet');

    const zeroXpCheck = makeCheckRow('0 EXP bekommen');
    const bondMaxCheck = makeCheckRow('Bond bis 12 Attacken (statt 4)');

    const xpMultiplierRow = document.createElement('div');
    xpMultiplierRow.className = 'control-row xp-multiplier-row';
    const xpMultiplierLabel = document.createElement('span');
    xpMultiplierLabel.textContent = 'EXP ×';
    const xpMultiplierSlider = document.createElement('input');
    xpMultiplierSlider.type = 'range';
    xpMultiplierSlider.min = '1';
    xpMultiplierSlider.max = '20';
    xpMultiplierSlider.step = '1';
    const xpMultiplierValue = document.createElement('span');
    xpMultiplierValue.className = 'xp-multiplier-value';
    xpMultiplierRow.append(xpMultiplierLabel, xpMultiplierSlider, xpMultiplierValue);
    panelBody.append(xpMultiplierRow);

    const statEditorCheck = makeCheckRow('Statuswerte ändern');

    const statEditor = document.createElement('div');
    statEditor.className = 'stat-editor';
    function makeStatField(label, min, max, step) {
      const row = document.createElement('label');
      row.className = 'stat-row';
      const text = document.createElement('span');
      text.textContent = label;
      const input = document.createElement('input');
      input.type = 'number';
      input.min = String(min);
      input.max = String(max);
      input.step = String(step);
      row.append(text, input);
      statEditor.append(row);
      return input;
    }
    const statSpeedInput = makeStatField('Tempo ×', 0.1, 20, 0.1);
    const statAttackInput = makeStatField('Angriff ×', 0.1, 100, 0.1);
    const statAttackSpeedInput = makeStatField('Angriffstempo ×', 0.1, 20, 0.1);
    const statCoinsInput = makeStatField('Münzen ×', 0.1, 100, 0.1);
    panelBody.append(statEditor);

    const moveEditor = document.createElement('div');
    moveEditor.className = 'move-editor';
    const moveEditorTitle = document.createElement('span');
    moveEditorTitle.className = 'move-editor-title';
    moveEditorTitle.textContent = 'Attacken bearbeiten';
    // Ein Tab pro Attacke; der gewählte Tab öffnet darunter seine Bearbeitung.
    const moveTabs = document.createElement('div');
    moveTabs.className = 'move-tabs';
    moveTabs.setAttribute('role', 'tablist');
    const moveDetail = document.createElement('div');
    moveDetail.className = 'move-detail hidden';
    moveDetail.setAttribute('role', 'tabpanel');
    const moveInfo = document.createElement('div');
    moveInfo.className = 'move-info';
    const moveLevelBar = document.createElement('div');
    moveLevelBar.className = 'move-level-bar';
    const moveLevelFill = document.createElement('div');
    moveLevelFill.className = 'move-level-fill';
    moveLevelBar.append(moveLevelFill);
    const moveLevelLabel = document.createElement('span');
    moveLevelLabel.className = 'move-section-label';
    moveLevelLabel.textContent = 'Stufe';
    const moveUpButton = document.createElement('button');
    moveUpButton.type = 'button';
    moveUpButton.textContent = 'Stufe +1';
    const moveMaxButton = document.createElement('button');
    moveMaxButton.type = 'button';
    moveMaxButton.textContent = 'Max. Stufe';
    const moveReplaceLabel = document.createElement('span');
    moveReplaceLabel.className = 'move-section-label';
    moveReplaceLabel.textContent = 'Tauschen gegen';
    const moveReplacementSelect = document.createElement('select');
    moveReplacementSelect.className = 'move-replacement';
    moveReplacementSelect.setAttribute('aria-label', 'Neue Attacke auswählen');
    const moveReplaceButton = document.createElement('button');
    moveReplaceButton.className = 'move-replace';
    moveReplaceButton.type = 'button';
    moveReplaceButton.textContent = 'Attacke tauschen';
    moveDetail.append(moveInfo, moveLevelBar, moveLevelLabel, moveUpButton, moveMaxButton,
      moveReplaceLabel, moveReplacementSelect, moveReplaceButton);
    moveEditor.append(moveEditorTitle, moveTabs, moveDetail);
    panelBody.append(moveEditor);

    const timeJumpButton = document.createElement('button');
    timeJumpButton.className = 'time-jump';
    timeJumpButton.type = 'button';
    timeJumpButton.textContent = 'Zu 29:00 springen';
    panelBody.append(timeJumpButton);

    const opacityRow = document.createElement('div');
    opacityRow.className = 'control-row opacity-row';
    const opacityLabel = document.createElement('span');
    opacityLabel.textContent = 'Transparenz';
    const opacitySlider = document.createElement('input');
    opacitySlider.type = 'range';
    opacitySlider.min = '25';
    opacitySlider.max = '100';
    opacitySlider.step = '5';
    const opacityValue = document.createElement('span');
    opacityValue.className = 'opacity-value';
    opacityRow.append(opacityLabel, opacitySlider, opacityValue);
    panelBody.append(opacityRow);
    panel.append(panelBody);

    shadow.append(style, stack, zoomReadout, notice, panel);
    document.documentElement.append(host);

    ui = {
      host,
      smallChest,
      bigChest,
      boss,
      zoomReadout,
      notice,
      panel,
      panelClose,
      riftCheck,
      fastChestCheck,
      oneHitCheck,
      judgmentOneHitCheck,
      autoPickCheck,
      noTabPauseCheck,
      autoBerryCheck,
      instaBombCheck,
      noClipCheck,
      environmentImmuneCheck,
      invincibleCheck,
      superMagnetCheck,
      zeroXpCheck,
      bondMaxCheck,
      xpMultiplierSlider,
      xpMultiplierValue,
      statEditor,
      statEditorCheck,
      statSpeedInput,
      statAttackInput,
      statAttackSpeedInput,
      statCoinsInput,
      moveEditor,
      moveTabs,
      moveDetail,
      moveInfo,
      moveLevelFill,
      moveUpButton,
      moveMaxButton,
      selectedMoveId: '',
      moveSignature: null,
      moveReplacementSelect,
      moveReplaceButton,
      moveReplacementSignature: null,
      timeJumpButton,
      opacitySlider,
      opacityValue,
    };
    installSettingsControls();
    refreshSettingsUI();
    return true;
  }

  function togglePanel(force) {
    if (!ui) return;
    const expanded = typeof force === 'boolean'
      ? force
      : ui.panel.classList.contains('collapsed');
    ui.panel.classList.toggle('collapsed', !expanded);
    refreshSettingsUI();
  }

  function refreshSettingsUI() {
    if (!ui) return;
    ui.riftCheck.checked = settings.rift;
    ui.fastChestCheck.checked = settings.fastChest;
    ui.oneHitCheck.checked = settings.oneHit;
    ui.judgmentOneHitCheck.checked = settings.judgmentOneHit;
    ui.autoPickCheck.checked = settings.autoPick;
    ui.noTabPauseCheck.checked = settings.noTabPause;
    ui.autoBerryCheck.checked = settings.autoBerry;
    ui.instaBombCheck.checked = settings.instaBomb;
    ui.noClipCheck.checked = settings.noClip;
    ui.environmentImmuneCheck.checked = settings.environmentImmune;
    ui.invincibleCheck.checked = settings.invincible;
    ui.superMagnetCheck.checked = settings.superMagnet;
    ui.zeroXpCheck.checked = settings.zeroXp;
    ui.bondMaxCheck.checked = settings.bondMax;
    ui.xpMultiplierSlider.disabled = settings.zeroXp;
    ui.statEditorCheck.checked = settings.statEditor;
    ui.statSpeedInput.value = String(settings.statSpeed);
    ui.statAttackInput.value = String(settings.statAttack);
    ui.statAttackSpeedInput.value = String(settings.statAttackSpeed);
    ui.statCoinsInput.value = String(settings.statCoins);
    ui.xpMultiplierSlider.value = String(settings.groundXp);
    ui.xpMultiplierValue.textContent = `${settings.groundXp}×`;
    ui.statEditor.classList.toggle('inactive', !settings.statEditor);
    for (const input of [ui.statSpeedInput, ui.statAttackInput,
      ui.statAttackSpeedInput, ui.statCoinsInput]) {
      input.disabled = !settings.statEditor;
    }
    const opacityPercent = Math.round(settings.opacity * 100);
    ui.opacitySlider.value = String(opacityPercent);
    ui.opacityValue.textContent = `${opacityPercent}%`;
    ui.host.style.setProperty('--hud-opacity', String(settings.opacity));
    const collapsed = ui.panel.classList.contains('collapsed');
    ui.panelClose.textContent = collapsed ? '+' : '−';
    ui.panelClose.title = collapsed ? 'HUD erweitern' : 'HUD minimieren';
  }

  function isCoopGuest() {
    return Boolean(engine && engine.Coop && engine.Coop.active && !engine.Coop.isHost);
  }

  function installSettingsControls() {
    ui.panelClose.addEventListener('click', () => togglePanel());

    function bindCheck(check, key, onChange) {
      check.addEventListener('change', () => {
        settings[key] = check.checked;
        saveSettings();
        refreshSettingsUI();
        if (onChange) onChange(settings[key]);
      });
    }
    function hostOnlyNotice(text) {
      return enabled => {
        if (enabled && isCoopGuest()) showNotice(text);
      };
    }

    bindCheck(ui.riftCheck, 'rift');
    bindCheck(ui.fastChestCheck, 'fastChest');
    bindCheck(ui.oneHitCheck, 'oneHit',
      hostOnlyNotice('One-Hit funktioniert im Koop nur beim Host'));
    bindCheck(ui.judgmentOneHitCheck, 'judgmentOneHit',
      hostOnlyNotice('Judgment-OHK funktioniert im Koop nur beim Host'));
    bindCheck(ui.autoPickCheck, 'autoPick');
    bindCheck(ui.noTabPauseCheck, 'noTabPause', moveFramesToTimers);
    bindCheck(ui.autoBerryCheck, 'autoBerry', enabled => {
      lastBerrySpawnAt = 0;
      if (enabled && isCoopGuest()) {
        showNotice('Auto-Sitrusbeeren funktionieren im Koop nur beim Host');
      }
    });
    bindCheck(ui.instaBombCheck, 'instaBomb', enabled => {
      lastBombAt = 0;
      if (enabled && isCoopGuest()) {
        showNotice('Insta-Bombe funktioniert im Koop nur beim Host');
      }
    });
    bindCheck(ui.noClipCheck, 'noClip');
    bindCheck(ui.environmentImmuneCheck, 'environmentImmune');
    bindCheck(ui.invincibleCheck, 'invincible', enabled => {
      if (enabled && engine && engine.Game) {
        applyInvincibility(engine.Game.player, true);
      }
    });
    bindCheck(ui.superMagnetCheck, 'superMagnet',
      hostOnlyNotice('Super-Magnet funktioniert im Koop nur beim Host'));
    bindCheck(ui.zeroXpCheck, 'zeroXp');
    bindCheck(ui.bondMaxCheck, 'bondMax');
    bindCheck(ui.statEditorCheck, 'statEditor');

    function bindStatInput(input, key, min, max) {
      input.addEventListener('change', () => {
        settings[key] = loadFactor(input.value, min, max);
        saveSettings();
        refreshSettingsUI();
      });
    }
    bindStatInput(ui.statSpeedInput, 'statSpeed', 0.1, 20);
    bindStatInput(ui.statAttackInput, 'statAttack', 0.1, 100);
    bindStatInput(ui.statAttackSpeedInput, 'statAttackSpeed', 0.1, 20);
    bindStatInput(ui.statCoinsInput, 'statCoins', 0.1, 100);
    ui.xpMultiplierSlider.addEventListener('input', () => {
      settings.groundXp = loadFactor(ui.xpMultiplierSlider.value, 1, 20);
      saveSettings();
      refreshSettingsUI();
    });
    ui.moveTabs.addEventListener('click', event => {
      const tab = event.target.closest('.move-tab');
      if (!tab) return;
      ui.selectedMoveId = tab.dataset.moveId || '';
      ui.moveSignature = null;
      ui.moveReplacementSignature = null;
      refreshMoveEditor(engine && engine.Game ? engine.Game.player : null);
    });
    ui.moveUpButton.addEventListener('click', () => {
      const player = engine && engine.Game ? engine.Game.player : null;
      const move = selectedPlayerMove(player);
      if (!move || !engine.Moves || typeof engine.Moves.levelUp !== 'function') return;
      if (Number(move.lvl) < moveMaxLevel(move)) engine.Moves.levelUp(move);
      refreshMoveEditor(player);
      showNotice(`${moveDisplayName(move)}: Stufe ${move.lvl}/${moveMaxLevel(move)}`);
    });
    ui.moveMaxButton.addEventListener('click', () => {
      const player = engine && engine.Game ? engine.Game.player : null;
      const move = selectedPlayerMove(player);
      if (!move) return;
      const maximum = moveMaxLevel(move);
      const oldName = moveDisplayName(move);
      let guard = 0;
      while (Number(move.lvl) < maximum && guard++ < 100
        && engine.Moves && typeof engine.Moves.levelUp === 'function') {
        if (!engine.Moves.levelUp(move)) break;
      }
      const evolved = evolveMove(player, move);
      refreshMoveEditor(player);
      showNotice(evolved
        ? `${oldName} → ${moveDisplayName(move)} (Stufe ${move.lvl}/${maximum}, entwickelt)`
        : `${moveDisplayName(move)}: Stufe ${move.lvl}/${maximum}`);
    });
    ui.moveReplaceButton.addEventListener('click', () => {
      const player = engine && engine.Game ? engine.Game.player : null;
      const move = selectedPlayerMove(player);
      const replacementId = ui.moveReplacementSelect.value;
      if (!move || !replacementId) return;
      const oldName = moveDisplayName(move);
      const replacement = replacePlayerMove(player, move, replacementId);
      if (!replacement) {
        showNotice('Attacke konnte nicht getauscht werden');
        return;
      }
      refreshMoveEditor(player);
      showNotice(`${oldName} → ${moveDisplayName(replacement)} (St. ${replacement.lvl})`);
    });
    ui.timeJumpButton.addEventListener('click', jumpTo29Minutes);

    ui.opacitySlider.addEventListener('input', () => {
      settings.opacity = clamp(Number(ui.opacitySlider.value) / 100, 0.25, 1);
      saveSettings();
      refreshSettingsUI();
    });
  }

  function squaredDistance(a, b) {
    const dx = a.x - b.x;
    const dy = a.y - b.y;
    return dx * dx + dy * dy;
  }

  function closestTo(player, candidates) {
    let closest = null;
    let best = Infinity;
    for (const candidate of candidates) {
      if (!candidate || !Number.isFinite(candidate.x) || !Number.isFinite(candidate.y)) continue;
      const distance = squaredDistance(player, candidate);
      if (distance < best) {
        best = distance;
        closest = candidate;
      }
    }
    return closest;
  }

  function findNearestChests(player) {
    const world = engine.World;
    if (!world
      || typeof world.propsInCircle !== 'function'
      || typeof world.specialsNear !== 'function') return { small: null, big: null };

    let small = null;
    let big = null;
    for (const radius of SEARCH_RADII) {
      if (!small) {
        small = closestTo(player, world.propsInCircle(player.x, player.y, radius)
          .filter(prop => prop.type === 'chest' && prop.hp > 0));
      }
      if (!big) {
        big = closestTo(player, world.specialsNear(player.x, player.y, radius)
          .filter(prop => prop.type === 'bigchest'));
      }
      if (small && big) break;
    }
    return { small, big };
  }

  function fastOpenNearbyChests(player, chests) {
    const opened = { small: false, big: false };
    if (!settings.fastChest || !engine.World
      || typeof engine.World.breakProp !== 'function') return opened;

    const maxDistance = FAST_CHEST_RADIUS * FAST_CHEST_RADIUS;
    for (const key of ['small', 'big']) {
      const chest = chests[key];
      if (chest && squaredDistance(player, chest) <= maxDistance) {
        engine.World.breakProp(chest);
        opened[key] = true;
      }
    }
    return opened;
  }

  function findNearestBoss(player) {
    if (!engine.EnemyMgr || typeof engine.EnemyMgr.all !== 'function') return null;
    return closestTo(player, engine.EnemyMgr.all().filter(enemy => (
      enemy && enemy.boss === true && enemy.dead !== true && enemy.hp > 0
    )));
  }

  function applyOneHitMode() {
    if (!engine.EnemyMgr || typeof engine.EnemyMgr.all !== 'function') return;
    if (isCoopGuest()) return;
    const inJudgment = Boolean(engine.Judgment && engine.Judgment.active);
    // Im Arceus-Kampf gilt nur die eigene Judgment-Checkbox, sonst die normale.
    if (inJudgment ? !settings.judgmentOneHit : !settings.oneHit) return;

    for (const enemy of engine.EnemyMgr.all()) {
      if (!enemy || enemy.dead === true || enemy.remote === true
        || !Number.isFinite(enemy.hp) || enemy.hp <= 1) continue;
      enemy.hp = 1;
    }
  }

  function updateAutoBerry(player) {
    if (!settings.autoBerry || !engine.Pickups
      || typeof engine.Pickups.drop !== 'function'
      || typeof engine.Pickups.all !== 'function') return;
    if (isCoopGuest()) return;

    const now = Date.now();
    if (now - lastBerrySpawnAt < BERRY_INTERVAL_MS) return;

    const nearbyRadius = BERRY_MAX_DISTANCE * 2;
    const nearbyLimitDistance = nearbyRadius * nearbyRadius;
    const nearbyBerries = engine.Pickups.all().filter(pickup => (
      pickup && pickup.kind === 'heal'
      && squaredDistance(player, pickup) <= nearbyLimitDistance
    ));
    if (nearbyBerries.length >= BERRY_NEARBY_LIMIT) return;

    let position = null;
    for (let attempt = 0; attempt < 12; attempt++) {
      const angle = Math.random() * Math.PI * 2;
      const distance = BERRY_MIN_DISTANCE
        + Math.random() * (BERRY_MAX_DISTANCE - BERRY_MIN_DISTANCE);
      const x = Math.round(player.x + Math.cos(angle) * distance);
      const y = Math.round(player.y + Math.sin(angle) * distance);
      if (!engine.World || typeof engine.World.isFree !== 'function'
        || engine.World.isFree(x, y, 8)) {
        position = { x, y };
        break;
      }
    }
    if (!position) return;

    engine.Pickups.drop('heal', position.x, position.y, 0);
    lastBerrySpawnAt = now;
  }

  function updateInstaBomb(player) {
    if (!settings.instaBomb || !player || !engine.Game
      || engine.Game.state !== 'playing' || !engine.EnemyMgr
      || typeof engine.EnemyMgr.all !== 'function') return;
    if (isCoopGuest()) return;
    const now = Date.now();
    if (now - lastBombAt < BOMB_INTERVAL_MS) return;
    const radius = engine.Camera && typeof engine.Camera.outerRadius === 'function'
      ? engine.Camera.outerRadius() : 520;
    const damage = 60 + 14 * (Number(player.level) || 1);
    if (engine.Camera && typeof engine.Camera.kick === 'function') engine.Camera.kick(0.8);
    if (engine.FX && typeof engine.FX.ring === 'function') {
      engine.FX.ring(player.x, player.y, 20, radius, '#ff7b3d', 0.5, 6);
    }
    if (engine.Audio && typeof engine.Audio.sfx === 'function') engine.Audio.sfx('break');
    for (const enemy of engine.EnemyMgr.all()) {
      if (!enemy || enemy.dead === true || typeof enemy.hurt !== 'function') continue;
      const visible = engine.Camera && typeof engine.Camera.sees === 'function'
        ? engine.Camera.sees(enemy.x, enemy.y, 0)
        : squaredDistance(player, enemy) <= radius * radius;
      if (visible) {
        enemy.hurt(damage, '#ff7b3d', enemy.x - player.x,
          enemy.y - player.y, 160);
      }
    }
    if (engine.Coop && engine.Coop.active && engine.Coop.isHost
      && typeof engine.Coop.broadcast === 'function') {
      engine.Coop.broadcast(['bomb', Math.round(player.x), Math.round(player.y)]);
    }
    lastBombAt = now;
  }

  function applySuperMagnet() {
    if (!settings.superMagnet || !engine.Pickups
      || typeof engine.Pickups.pullAll !== 'function') return;
    if (isCoopGuest()) return;
    engine.Pickups.pullAll();
  }

  function applyPlayerStatEditor(player, active) {
    if (!player || typeof player !== 'object') return;
    let state = playerStatStates.get(player);
    if (!state) {
      state = { spdMul: 1, atkMul: 1, cdMul: 1, xpMul: 1, coinMul: 1 };
      playerStatStates.set(player, state);
    }
    const factors = {
      spdMul: active ? settings.statSpeed : 1,
      atkMul: active ? settings.statAttack : 1,
      cdMul: active ? 1 / settings.statAttackSpeed : 1,
      xpMul: settings.groundXp,
      coinMul: active ? settings.statCoins : 1,
    };
    for (const field of Object.keys(factors)) {
      const current = Number(player[field]);
      const previousFactor = state[field];
      const nextFactor = factors[field];
      if (!Number.isFinite(current) || !Number.isFinite(previousFactor)
        || previousFactor <= 0 || !Number.isFinite(nextFactor) || nextFactor <= 0) continue;
      player[field] = current / previousFactor * nextFactor;
      state[field] = nextFactor;
    }
  }

  function playerMoves(player) {
    return player && Array.isArray(player.moves)
      ? player.moves.filter(move => move && typeof move === 'object' && move.id)
      : [];
  }

  function moveDefinition(move) {
    return move && (move.def || (engine && engine.Moves && engine.Moves.BY_ID
      ? engine.Moves.BY_ID[move.id] : null));
  }

  function moveMaxLevel(move) {
    const definition = moveDefinition(move) || move;
    const maximum = Number(definition && definition.maxLvl);
    return Number.isFinite(maximum) && maximum >= 1
      ? Math.floor(maximum)
      : Math.max(1, Math.floor(Number(move && move.lvl) || 1));
  }

  function moveDisplayName(move) {
    const definition = moveDefinition(move);
    return englishName(String((move && move.name) || (definition && definition.name)
      || (move && move.id) || 'Attacke'));
  }

  // Moves.evoInfo liefert nur auf Maximalstufe etwas, daher wird eine Kopie auf
  // Maximalstufe gefragt. Die Werte-Bedingung des Spiels („needs“) wird übersprungen.
  function moveEvolutionName(move) {
    if (!move || move.evolved || !engine || !engine.Moves
      || typeof engine.Moves.evoInfo !== 'function') return null;
    try {
      const info = engine.Moves.evoInfo({ ...move, lvl: moveMaxLevel(move) }, {});
      return info && info.name ? englishName(String(info.name)) : null;
    } catch (error) {
      return null;
    }
  }

  // Entwickelt die Attacke wie die Entwicklungskarte des Spiels (inkl. Effekt).
  function evolveMove(player, move) {
    if (!moveEvolutionName(move) || Number(move.lvl) < moveMaxLevel(move)
      || typeof engine.Moves.evolve !== 'function' || !engine.Moves.evolve(move)) return false;
    if (player && engine.FX) {
      if (typeof engine.FX.ring === 'function') {
        engine.FX.ring(player.x, player.y, 8, 120, '#ffd23f', 0.8, 6);
      }
      if (typeof engine.FX.burst === 'function') {
        engine.FX.burst(player.x, player.y - 0.5 * (Number(player.bodyH) || 24),
          '#ffe14d', 40, 240);
      }
    }
    if (engine.Audio && typeof engine.Audio.sfx === 'function') engine.Audio.sfx('legend');
    if (engine.Projectiles && typeof engine.Projectiles.clearOrbs === 'function') {
      engine.Projectiles.clearOrbs(player);
    }
    ui.moveSignature = null;
    return true;
  }

  function moveTypeLabel(type) {
    return MOVE_TYPE_LABELS[type] || type || '?';
  }

  // Ältere Versionen haben move.type überschrieben – einmalig auf den Originaltyp zurücksetzen.
  function restoreLegacyMoveTypes(player) {
    if (!legacyMoveTypes || !player || legacyTypeRestored.has(player)) return;
    for (const move of playerMoves(player)) {
      const definition = moveDefinition(move);
      if (definition && definition.type) move.type = definition.type;
    }
    legacyTypeRestored.add(player);
  }

  function allMoveDefinitions() {
    if (!engine || !engine.Moves) return [];
    const definitions = Array.isArray(engine.Moves.ALL)
      ? engine.Moves.ALL
      : engine.Moves.BY_ID && typeof engine.Moves.BY_ID === 'object'
        ? Object.values(engine.Moves.BY_ID) : [];
    return definitions.filter(definition => definition && definition.id
      && engine.Moves.BY_ID && engine.Moves.BY_ID[definition.id])
      .slice().sort((a, b) => moveDisplayName(a).localeCompare(moveDisplayName(b), 'en'));
  }

  function playerPoolIds(player) {
    // Wie im Level-up-Menü des Spiels: Moves.poolFor(player.mon).
    const mon = player && player.mon && Array.isArray(player.mon.types) ? player.mon
      : player && Array.isArray(player.types) ? player : null;
    if (!mon || !engine.Moves || typeof engine.Moves.poolFor !== 'function') return new Set();
    try {
      return new Set(engine.Moves.poolFor(mon));
    } catch (error) {
      return new Set();
    }
  }

  function selectedPlayerMove(player) {
    const moves = playerMoves(player);
    return moves.find(move => move.id === ui.selectedMoveId) || moves[0] || null;
  }

  // Tauscht die Attacke wie das Spiel selbst (Player.addMove): neue Instanz über
  // Moves.instance, gleiche Stufe, gleicher Slot/Bond, Tastenbelegung (bind) und
  // Entwicklung werden übernommen.
  function replacePlayerMove(player, currentMove, replacementId) {
    if (!player || !currentMove || !engine.Moves
      || typeof engine.Moves.instance !== 'function') return null;
    const definition = engine.Moves.BY_ID && engine.Moves.BY_ID[replacementId];
    if (!definition || replacementId === currentMove.id) return null;
    if (playerMoves(player).some(move => move.id === replacementId)) return null;
    const moveIndex = player.moves.indexOf(currentMove);
    if (moveIndex < 0) return null;

    const targetLevel = clamp(Math.floor(Number(currentMove.lvl) || 1), 1,
      moveMaxLevel(definition));
    const replacement = engine.Moves.instance(replacementId, targetLevel);
    if (!replacement) return null;

    const gameTime = engine.Game && engine.Game.st && Number.isFinite(engine.Game.st.time)
      ? engine.Game.st.time : 0;
    replacement.since = Number.isFinite(currentMove.since) ? currentMove.since : gameTime;
    replacement.t = 0;
    if (currentMove.evolved && typeof engine.Moves.evolve === 'function'
      && replacement.lvl >= moveMaxLevel(replacement)) {
      engine.Moves.evolve(replacement);
    }

    player.moves[moveIndex] = replacement;
    if (Array.isArray(player.slots)) {
      for (const group of player.slots) {
        if (!Array.isArray(group)) continue;
        for (let index = 0; index < group.length; index++) {
          if (group[index] === currentMove) group[index] = replacement;
        }
      }
    }
    if (player.bind && typeof player.bind === 'object' && player.bind[currentMove.id] != null) {
      player.bind[replacementId] = player.bind[currentMove.id];
      delete player.bind[currentMove.id];
    }

    if (engine.Projectiles && typeof engine.Projectiles.clearOrbs === 'function') {
      engine.Projectiles.clearOrbs(player);
    }
    if (typeof player.arrange === 'function') player.arrange();
    if (engine.Audio && typeof engine.Audio.sfx === 'function') engine.Audio.sfx('heal');

    ui.selectedMoveId = replacement.id;
    ui.moveSignature = null;
    ui.moveReplacementSignature = null;
    return replacement;
  }

  function refreshMoveReplacement(player, currentMove) {
    const ownedIds = new Set(playerMoves(player).map(move => move.id));
    const candidates = currentMove ? allMoveDefinitions().filter(definition => (
      !ownedIds.has(definition.id)
    )) : [];
    const pool = playerPoolIds(player);
    const matching = candidates.filter(definition => pool.has(definition.id));
    const others = candidates.filter(definition => !pool.has(definition.id));
    const signature = `${currentMove ? currentMove.id : ''}|${matching
      .map(definition => definition.id).join(',')}|${others
      .map(definition => definition.id).join(',')}`;
    const previous = ui.moveReplacementSelect.value;
    if (signature !== ui.moveReplacementSignature) {
      ui.moveReplacementSelect.textContent = '';
      if (!candidates.length) {
        const option = document.createElement('option');
        option.value = '';
        option.textContent = currentMove ? 'Keine weitere Attacke' : 'Erst Attacke auswählen';
        ui.moveReplacementSelect.append(option);
      } else {
        const addGroup = (label, list) => {
          if (!list.length) return;
          const group = document.createElement('optgroup');
          group.label = label;
          for (const definition of list) {
            const option = document.createElement('option');
            option.value = definition.id;
            option.textContent = `${moveDisplayName(definition)} · ${moveTypeLabel(definition.type)}`;
            group.append(option);
          }
          ui.moveReplacementSelect.append(group);
        };
        addGroup('Passend zum Pokémon', matching);
        addGroup(matching.length ? 'Alle anderen' : 'Alle Attacken', others);
      }
      ui.moveReplacementSelect.value = candidates.some(definition => definition.id === previous)
        ? previous
        : (matching[0] || others[0] || { id: '' }).id;
      ui.moveReplacementSignature = signature;
    }
    ui.moveReplacementSelect.disabled = !candidates.length;
    ui.moveReplaceButton.disabled = !candidates.length;
  }

  function refreshMoveEditor(player) {
    if (!ui) return;
    const moves = playerMoves(player);
    const signature = moves.map(move => [move.id, move.lvl, moveMaxLevel(move),
      moveDisplayName(move)].join(':')).join('|');
    if (!moves.some(move => move.id === ui.selectedMoveId)) {
      ui.selectedMoveId = moves[0] ? moves[0].id : '';
    }
    if (signature + '#' + ui.selectedMoveId !== ui.moveSignature) {
      ui.moveTabs.textContent = '';
      if (!moves.length) {
        const empty = document.createElement('span');
        empty.className = 'move-empty';
        empty.textContent = 'Keine Attacke – erst eine Runde starten.';
        ui.moveTabs.append(empty);
      }
      moves.forEach((move, index) => {
        const tab = document.createElement('button');
        tab.type = 'button';
        tab.className = 'move-tab';
        tab.dataset.moveId = move.id;
        tab.setAttribute('role', 'tab');
        const active = move.id === ui.selectedMoveId;
        tab.classList.toggle('active', active);
        tab.setAttribute('aria-selected', String(active));
        tab.title = moveDisplayName(move);
        tab.textContent = `${index + 1}. ${moveDisplayName(move)}`;
        const level = document.createElement('small');
        level.textContent = `St. ${move.lvl}/${moveMaxLevel(move)}${move.evolved ? ' · entwickelt' : ''}`;
        tab.append(level);
        ui.moveTabs.append(tab);
      });
      ui.moveSignature = signature + '#' + ui.selectedMoveId;
    }

    const move = selectedPlayerMove(player);
    ui.moveDetail.classList.toggle('hidden', !move);
    if (move) {
      const maximum = moveMaxLevel(move);
      const level = Math.max(1, Number(move.lvl) || 1);
      const definition = moveDefinition(move);
      const type = move.type || (definition && definition.type);
      ui.moveInfo.textContent = '';
      const name = document.createElement('b');
      name.textContent = moveDisplayName(move);
      const evolutionName = moveEvolutionName(move);
      ui.moveInfo.append(name, document.createElement('br'),
        `Typ: ${moveTypeLabel(type)} · Stufe ${level}/${maximum}`
        + (move.evolved ? ' · entwickelt' : ''));
      if (evolutionName) {
        ui.moveInfo.append(document.createElement('br'), `Entwicklung: ${evolutionName}`);
      }
      ui.moveLevelFill.style.width = `${Math.round(Math.min(1, level / maximum) * 100)}%`;
      ui.moveUpButton.disabled = level >= maximum;
      ui.moveMaxButton.disabled = level >= maximum && !evolutionName;
      ui.moveMaxButton.textContent = evolutionName
        ? (level >= maximum ? 'Entwickeln' : 'Max. + Entwickeln')
        : 'Max. Stufe';
    }
    refreshMoveReplacement(player, move);
  }

  function installPlayerHurtHook(player) {
    if (!player || typeof player.hurt !== 'function' || playerHurtHooks.has(player)) return;
    const originalHurt = player.hurt;
    player.hurt = function trainerPlayerHurt(...args) {
      const localPlayer = engine && engine.Game && engine.Game.player === this;
      if (localPlayer && this.dead !== true
        && (settings.invincible || (settings.environmentImmune && args[1] === 'poison'))) {
        return undefined;
      }
      return originalHurt.apply(this, args);
    };
    playerHurtHooks.set(player, originalHurt);
  }

  // Player.gainXp(menge) addiert EXP und gibt die Zahl der Level-ups zurück.
  function installPlayerXpHook(player) {
    if (!player || typeof player.gainXp !== 'function' || playerXpHooks.has(player)) return;
    const originalGainXp = player.gainXp;
    player.gainXp = function trainerGainXp(...args) {
      if (settings.zeroXp && engine && engine.Game && engine.Game.player === this) return 0;
      return originalGainXp.apply(this, args);
    };
    playerXpHooks.set(player, originalGainXp);
  }

  // Die Spieler-Klasse hat static get BOND_MAX() { return 4; } – so viele Attacken
  // passen in eine Bond-Gruppe. Die Klasse wird nicht exportiert, steckt aber in
  // player.constructor.
  function applyBondMax(player) {
    if (bondMaxPatched || !player) return;
    let playerClass = player.constructor;
    while (playerClass && playerClass !== Function.prototype
      && !Object.prototype.hasOwnProperty.call(playerClass, 'BOND_MAX')) {
      playerClass = Object.getPrototypeOf(playerClass);
    }
    if (!playerClass || playerClass === Function.prototype) return;
    const descriptor = Object.getOwnPropertyDescriptor(playerClass, 'BOND_MAX');
    const readOriginal = descriptor && typeof descriptor.get === 'function'
      ? () => descriptor.get.call(playerClass)
      : () => descriptor && descriptor.value;
    try {
      Object.defineProperty(playerClass, 'BOND_MAX', {
        configurable: true,
        get() {
          return settings.bondMax ? BOND_MAX : readOriginal();
        },
      });
      bondMaxPatched = true;
    } catch (error) {
      console.warn('[PS-HUD] Bond-Limit konnte nicht geändert werden.', error);
    }
  }

  function installPlayerEnvironmentHook(player) {
    if (!player || typeof player.update !== 'function' || ********dateHooks.has(player)) return;
    const originalUpdate = player.update;
    player.update = function trainer********date(...args) {
      const world = engine && engine.World;
      const originalLiquidEffect = world && typeof world.liquidEffect === 'function'
        ? world.liquidEffect : null;
      if (!settings.environmentImmune || !engine.Game
        || engine.Game.player !== this || !originalLiquidEffect) {
        return originalUpdate.apply(this, args);
      }
      world.liquidEffect = function trainerNeutralLiquidEffect() {
        return {};
      };
      try {
        return originalUpdate.apply(this, args);
      } finally {
        world.liquidEffect = originalLiquidEffect;
      }
    };
    ********dateHooks.set(player, originalUpdate);
  }

  function applyInvincibility(player, active) {
    installPlayerHurtHook(player);
    if (!active || !player || player.dead === true) return;
    if (Number.isFinite(player.maxHp) && player.maxHp > 0) player.hp = player.maxHp;
    if (Number.isFinite(player.invuln)) player.invuln = 0;
    if (Number.isFinite(player.flash)) player.flash = 0;
    if (Number.isFinite(player.slowT)) player.slowT = 0;
  }

  function installCombatInvincibilityHook() {
    if (combatResolveWrapped) return true;
    if (!engine.Combat || typeof engine.Combat.resolve !== 'function') return false;
    const originalResolve = engine.Combat.resolve;
    engine.Combat.resolve = function trainerCombatResolve(...args) {
      const player = args[1];
      if (!settings.invincible || !player || !engine.Game
        || player !== engine.Game.player || !engine.Projectiles
        || typeof engine.Projectiles.all !== 'function') {
        return originalResolve.apply(this, args);
      }
      const radius = Number.isFinite(player.r) && player.r > 0 ? player.r : 12;
      const changed = [];
      for (const projectile of engine.Projectiles.all()) {
        if (!projectile || projectile.friendly || !Number.isFinite(projectile.r)) continue;
        changed.push([projectile, projectile.r]);
        projectile.r = -radius;
      }
      const world = engine.World;
      const originalBlock = world && typeof world.projectileBlock === 'function'
        ? world.projectileBlock : null;
      if (originalBlock) {
        world.projectileBlock = function trainerProjectileBlock(x, y, projectileRadius, ...rest) {
          return originalBlock.call(this, x, y, Math.abs(projectileRadius), ...rest);
        };
      }
      try {
        return originalResolve.apply(this, args);
      } finally {
        if (originalBlock) world.projectileBlock = originalBlock;
        for (const [projectile, originalRadius] of changed) projectile.r = originalRadius;
      }
    };
    combatResolveWrapped = true;
    return true;
  }

  function randomEntry(list) {
    return list.length ? list[Math.floor(Math.random() * list.length)] : null;
  }

  // Bester Kandidat nach Wert; bei Gleichstand zufällig.
  function highestBy(list, valueOf) {
    let best = -Infinity;
    let top = [];
    for (const entry of list) {
      const value = valueOf(entry);
      if (!Number.isFinite(value)) continue;
      if (value > best) {
        best = value;
        top = [entry];
      } else if (value === best) {
        top.push(entry);
      }
    }
    return randomEntry(top);
  }

  // Wahl-Reihenfolge: Entwicklung einer gelevelten Attacke → Level-up der
  // Attacke mit der höchsten Stufe → schon genommene Werte-Verbesserung mit den
  // meisten Stufen → sonst Zufall.
  function chooseAutoPick(player, offers) {
    const valid = offers.filter(offer => offer && typeof offer.apply === 'function');
    if (!valid.length) return null;
    const keyId = offer => String(offer.key || '').slice(String(offer.type || '').length + 1);

    const evo = valid.filter(offer => offer.type === 'evo');
    if (evo.length) return randomEntry(evo);

    const movesById = new Map(playerMoves(player).map(move => [move.id, move]));
    const ups = valid.filter(offer => offer.type === 'up' && movesById.has(keyId(offer)));
    const bestUp = highestBy(ups, offer => Number(movesById.get(keyId(offer)).lvl) || 0);
    if (bestUp) return bestUp;

    const taken = player.statsTaken && typeof player.statsTaken === 'object'
      ? player.statsTaken : {};
    const stats = valid.filter(offer => offer.type === 'stat'
      && Number(taken[keyId(offer)]) > 0);
    const bestStat = highestBy(stats, offer => Number(taken[keyId(offer)]));
    if (bestStat) return bestStat;

    return randomEntry(valid);
  }

  function installAutoPickHook() {
    if (levelUpWrapped) return true;
    const runUI = engine.RunUI;
    if (!runUI || typeof runUI.showLevelUp !== 'function') return false;

    const originalShowLevelUp = runUI.showLevelUp;
    runUI.showLevelUp = function trainerShowLevelUp(level, offers, onPick, waitForOthers, ...rest) {
      const player = engine.Game && engine.Game.player;
      const choice = settings.autoPick && player && Array.isArray(offers)
        && typeof onPick === 'function' ? chooseAutoPick(player, offers) : null;
      if (!choice) {
        return originalShowLevelUp.call(this, level, offers, onPick, waitForOthers, ...rest);
      }

      let picked = false;
      const pickOnce = offer => {
        if (picked) return undefined;
        picked = true;
        return onPick(offer);
      };
      const title = englishName(String(choice.title || choice.key || 'Karte'));
      if (waitForOthers) {
        // Koop: Wartebildschirm muss sichtbar bleiben, Klick auf Karten ist danach wirkungslos.
        const result = originalShowLevelUp.call(this, level, offers, pickOnce,
          waitForOthers, ...rest);
        pickOnce(choice);
        showNotice(`Auto-Pick: ${title}`);
        return result;
      }
      pickOnce(choice);
      showNotice(`Auto-Pick: ${title}`);
      return undefined;
    };
    levelUpWrapped = true;
    return true;
  }

  function installWorldCollisionHook() {
    if (worldCollideWrapped) return true;
    if (!engine.World || typeof engine.World.collide !== 'function') return false;

    const originalCollide = engine.World.collide;
    engine.World.collide = function hudWorldCollide(entity) {
      if (settings.noClip && engine.Game && entity === engine.Game.player) return false;
      return originalCollide.call(this, entity);
    };
    worldCollideWrapped = true;
    return true;
  }

  function clamp(value, min, max) {
    return Math.max(min, Math.min(max, value));
  }

  function pointIndicator(view, player, target, label, laneOffset = 0) {
    if (!target) {
      view.indicator.classList.add('hidden');
      return;
    }

    const camera = engine.Camera;
    const canvas = zoomCanvas || document.getElementById('game');
    if (!camera || !canvas) {
      view.indicator.classList.add('hidden');
      return;
    }

    const rect = canvas.getBoundingClientRect();
    const dx = target.x - player.x;
    const dy = target.y - player.y;
    let angle = Math.atan2(dx, -dy) * 180 / Math.PI;
    if (Number.isFinite(view.angle)) {
      while (angle - view.angle > 180) angle -= 360;
      while (angle - view.angle < -180) angle += 360;
    }
    view.angle = angle;
    const distance = Math.ceil(Math.hypot(dx, dy) / 32);
    let vx = dx * camera.scale;
    let vy = dy * camera.scale;
    if (Math.abs(vx) + Math.abs(vy) < 0.001) vy = -1;
    const length = Math.hypot(vx, vy) || 1;

    const sideInset = Math.min(34, rect.width * 0.08);
    const topInset = Math.min(92, rect.height * 0.2);
    const bottomInset = Math.min(76, rect.height * 0.17);
    const left = rect.left + sideInset;
    const right = rect.right - sideInset;
    const top = rect.top + topInset;
    const bottom = rect.bottom - bottomInset;
    const originX = clamp(rect.left + (player.x - camera.left()) * camera.scale, left, right);
    const originY = clamp(rect.top + (player.y - camera.top()) * camera.scale, top, bottom);

    const tx = vx > 0 ? (right - originX) / vx : vx < 0 ? (left - originX) / vx : Infinity;
    const ty = vy > 0 ? (bottom - originY) / vy : vy < 0 ? (top - originY) / vy : Infinity;
    const travel = Math.max(0, Math.min(tx, ty));
    const tangentX = -vy / length;
    const tangentY = vx / length;
    const markerX = clamp(originX + vx * travel + tangentX * laneOffset, left, right);
    const markerY = clamp(originY + vy * travel + tangentY * laneOffset, top, bottom);

    view.indicator.style.left = `${markerX.toFixed(1)}px`;
    view.indicator.style.top = `${markerY.toFixed(1)}px`;
    view.needle.style.transform = `rotate(${angle.toFixed(2)}deg)`;
    view.distance.textContent = `${distance}m`;
    view.indicator.title = `${label} · ${distance} m`;
    view.indicator.setAttribute('aria-label', `${label}, Entfernung ${distance} Meter`);
    view.indicator.classList.remove('hidden');
  }

  function hideAll() {
    if (!ui) return;
    ui.smallChest.indicator.classList.add('hidden');
    ui.bigChest.indicator.classList.add('hidden');
    ui.boss.indicator.classList.add('hidden');
    ui.zoomReadout.classList.add('hidden');
    ui.notice.classList.add('hidden');
  }

  function showNotice(message) {
    if (!ui) return;
    window.clearTimeout(noticeTimer);
    ui.notice.textContent = message;
    ui.notice.classList.remove('hidden');
    noticeTimer = window.setTimeout(() => {
      if (ui) ui.notice.classList.add('hidden');
    }, 2400);
  }

  function setCameraZoom(factor) {
    const camera = engine && engine.Camera;
    const canvas = zoomCanvas || document.getElementById('game');
    if (!camera || !canvas || !canvas.clientWidth || !canvas.clientHeight) return;

    zoomCanvas = canvas;
    const normalScale = Math.min(
      canvas.clientWidth / camera.VIEW_W,
      canvas.clientHeight / camera.MIN_VIEW_H,
    );
    if (!Number.isFinite(normalScale) || normalScale <= 0) return;

    const safeFactor = Math.max(MIN_ZOOM, Math.min(1, factor));
    appliedZoom = safeFactor;
    camera.scale = normalScale * safeFactor;
    camera.w = canvas.clientWidth / camera.scale;
    camera.h = canvas.clientHeight / camera.scale;
  }

  function applyZoom() {
    zoom = Math.max(MIN_ZOOM, Math.min(1, zoom));
    setCameraZoom(zoom);
  }

  function changeZoom(direction) {
    zoom = direction > 0
      ? Math.min(1, zoom * ZOOM_STEP)
      : Math.max(MIN_ZOOM, zoom / ZOOM_STEP);
    applyZoom();
  }

  function jumpTo29Minutes() {
    const game = engine && engine.Game;
    const player = game && game.player;
    if (!game || typeof game.debugTime !== 'function') {
      showNotice('Zeitfunktion in dieser Spielversion nicht verfügbar');
      return;
    }
    if (!player || player.dead === true || game.state !== 'playing') {
      showNotice('Zeitsprung nur während einer laufenden Runde');
      return;
    }
    if (engine.Rift && engine.Rift.inArena) {
      showNotice('Zeitsprung nicht innerhalb einer Rift-Arena');
      return;
    }
    if (isCoopGuest()) {
      showNotice('Im Koop kann nur der Host die Zeit ändern');
      return;
    }
    game.debugTime(TIME_JUMP_SECONDS);
    showNotice('Rundenzeit auf 29:00 gesetzt');
  }

  function spawnRiftNearPlayer() {
    const game = engine && engine.Game;
    const player = game && game.player;
    const rift = engine && engine.Rift;
    const coop = engine && engine.Coop;
    const world = engine && engine.World;

    if (!settings.rift) {
      showNotice('Rift-Funktion ist im HUD deaktiviert');
      return;
    }
    if (!player || player.dead === true || game.state !== 'playing') {
      showNotice('Rift nur während einer laufenden Runde');
      return;
    }
    if (!rift || typeof rift.onEvent !== 'function') {
      showNotice('Rift-Funktion in dieser Spielversion nicht verfügbar');
      return;
    }
    if (rift.inArena) {
      showNotice('Du bist bereits in einer Rift-Arena');
      return;
    }
    if (rift.rift) {
      showNotice('Es ist bereits ein Rift geöffnet');
      return;
    }
    if (engine.Judgment && engine.Judgment.active) {
      showNotice('Während der Arceus-Phase sind Rifts gesperrt');
      return;
    }
    if (isCoopGuest()) {
      showNotice('Im Koop kann nur der Host einen Rift erzeugen');
      return;
    }
    if (!world || typeof world.isFree !== 'function') {
      showNotice('Kein freier Rift-Platz gefunden');
      return;
    }

    const aim = Number.isFinite(player.aim) ? player.aim : 0;
    const angleOffsets = [0, Math.PI / 4, -Math.PI / 4, Math.PI / 2,
      -Math.PI / 2, 3 * Math.PI / 4, -3 * Math.PI / 4, Math.PI];
    const distances = [RIFT_DISTANCE, 112, 128];
    let position = null;

    for (const distance of distances) {
      for (const offset of angleOffsets) {
        const angle = aim + offset;
        const x = Math.round(player.x + Math.cos(angle) * distance);
        const y = Math.round(player.y + Math.sin(angle) * distance);
        if (world.isFree(x, y, 22)) {
          position = { x, y };
          break;
        }
      }
      if (position) break;
    }

    if (!position) {
      showNotice('Neben dir ist kein freier Platz für einen Rift');
      return;
    }

    const eventData = ['rift', position.x, position.y, RIFT_TTL];
    rift.onEvent(eventData);
    if (coop && coop.active && coop.isHost && typeof coop.broadcast === 'function') {
      coop.broadcast(eventData);
    }
    showNotice(coop && coop.active
      ? 'Rift erzeugt und an die Gruppe übertragen'
      : 'Rift neben dir erzeugt');
  }

  function updateZoomReadout() {
    if (!ui || !engine || !engine.Camera) return;
    const viewFactor = 1 / zoom;
    const factorText = viewFactor < 10 ? viewFactor.toFixed(1) : viewFactor.toFixed(0);
    const widthMeters = Math.round(engine.Camera.w / 32);
    const heightMeters = Math.round(engine.Camera.h / 32);
    ui.zoomReadout.textContent = `Zoom ${Math.round(zoom * 100)}% · Sicht ${factorText}× · ${widthMeters}×${heightMeters} m`;
    ui.zoomReadout.classList.remove('hidden');
  }

  function installZoomControls() {
    if (zoomListenersInstalled) return true;
    const canvas = document.getElementById('game');
    if (!canvas) return false;

    zoomCanvas = canvas;

    if (!outerRadiusWrapped && engine.Camera
      && typeof engine.Camera.outerRadius === 'function') {
      engine.Camera.outerRadius = function compassSafeOuterRadius() {
        return Math.hypot(this.w * appliedZoom, this.h * appliedZoom) / 2 + 40;
      };
      outerRadiusWrapped = true;
    }

    canvas.addEventListener('wheel', event => {
      if (!engine || !engine.Game || !engine.Game.player
        || engine.Game.state === 'ui' || engine.Game.state === 'over') return;
      event.preventDefault();
      changeZoom(event.deltaY < 0 ? 1 : -1);
    }, { passive: false });

    window.addEventListener('keydown', event => {
      const active = document.activeElement;
      if (active && /^(INPUT|TEXTAREA|SELECT)$/.test(active.tagName)) return;

      if (event.code === 'Digit0' || event.code === 'Numpad0') {
        event.preventDefault();
        zoom = 1;
        applyZoom();
        return;
      }
      if (settings.rift && event.code === 'KeyR' && !event.repeat) {
        event.preventDefault();
        spawnRiftNearPlayer();
      }
    });

    zoomListenersInstalled = true;
    return true;
  }

  function update() {
    try {
      if (!createUI() || !engine.Game) return;

      installZoomControls();
      installWorldCollisionHook();
      installCombatInvincibilityHook();
      installAutoPickHook();

      const player = engine.Game.player;
      const state = engine.Game.state;
      const inRun = player
        && player.dead !== true
        && state !== 'ui'
        && state !== 'over';

      applyPlayerStatEditor(player, Boolean(inRun && settings.statEditor));
      installPlayerEnvironmentHook(player);
      installPlayerXpHook(player);
      applyBondMax(player);
      applyInvincibility(player, Boolean(inRun && settings.invincible));
      refreshMoveEditor(inRun ? player : null);

      if (!inRun) {
        setCameraZoom(1);
        hideAll();
        return;
      }

      restoreLegacyMoveTypes(player);
      applyZoom();
      updateZoomReadout();
      applyOneHitMode();
      updateAutoBerry(player);
      updateInstaBomb(player);
      applySuperMagnet();
      const chests = findNearestChests(player);
      const opened = fastOpenNearbyChests(player, chests);
      pointIndicator(ui.smallChest, player,
        opened.small ? null : chests.small, 'Nächste kleine Truhe', -24);
      pointIndicator(ui.bigChest, player,
        opened.big ? null : chests.big, 'Nächste große Truhe', 0);
      pointIndicator(ui.boss, player, findNearestBoss(player), 'Nächster Boss', 24);
    } catch (error) {
      hideAll();
      window.clearInterval(timer);
      timer = 0;
      console.warn('[PS-Kompass] Anzeige wegen inkompatibler Spielversion angehalten.', error);
    }
  }

  // ---- Weiterlaufen im Hintergrund-Tab ----
  // Das Spiel pausiert bei 'visibilitychange' (document.hidden) und seine Schleife
  // hängt an requestAnimationFrame, das der Browser in Hintergrund-Tabs anhält.
  // Beides wird hier abgefangen; die Frames laufen dann über setTimeout weiter.
  const BACKGROUND_FRAME_MS = 16;
  const pendingFrames = new Map();
  const timerFrames = new Map();
  let nativeRequestFrame = null;
  let nativeCancelFrame = null;
  let realHidden = () => document.visibilityState === 'hidden';
  let nextTimerFrameId = 1_000_000_000;

  function shouldRunInBackground() {
    return settings.noTabPause && realHidden();
  }

  function scheduleTimerFrame(callback) {
    const id = ++nextTimerFrameId;
    timerFrames.set(id, window.setTimeout(() => {
      timerFrames.delete(id);
      callback(performance.now());
    }, BACKGROUND_FRAME_MS));
    return id;
  }

  // Beim Wechsel in den Hintergrund hängen bereits angeforderte native Frames
  // fest; sie werden abgebrochen und als Timer neu eingeplant.
  function moveFramesToTimers() {
    if (!shouldRunInBackground() || !nativeCancelFrame) return;
    for (const [id, callback] of pendingFrames) {
      nativeCancelFrame(id);
      scheduleTimerFrame(callback);
    }
    pendingFrames.clear();
  }

  function installBackgroundRunning() {
    const docProto = Document.prototype;
    const hiddenDescriptor = Object.getOwnPropertyDescriptor(docProto, 'hidden');
    const stateDescriptor = Object.getOwnPropertyDescriptor(docProto, 'visibilityState');
    if (stateDescriptor && typeof stateDescriptor.get === 'function') {
      const readState = stateDescriptor.get;
      realHidden = () => readState.call(document) === 'hidden';
      try {
        Object.defineProperty(docProto, 'visibilityState', {
          ...stateDescriptor,
          get() {
            return settings.noTabPause ? 'visible' : readState.call(this);
          },
        });
        if (hiddenDescriptor && typeof hiddenDescriptor.get === 'function') {
          const readHidden = hiddenDescriptor.get;
          Object.defineProperty(docProto, 'hidden', {
            ...hiddenDescriptor,
            get() {
              return settings.noTabPause ? false : readHidden.call(this);
            },
          });
        }
      } catch (error) {
        console.warn('[PS-HUD] Sichtbarkeit konnte nicht überschrieben werden.', error);
      }
    }

    // Läuft vor allen Listenern des Spiels (document-start, Capture-Phase).
    document.addEventListener('visibilitychange', event => {
      if (!settings.noTabPause) return;
      event.stopImmediatePropagation();
      moveFramesToTimers();
    }, true);

    if (typeof window.requestAnimationFrame !== 'function') return;
    nativeRequestFrame = window.requestAnimationFrame.bind(window);
    nativeCancelFrame = window.cancelAnimationFrame.bind(window);
    window.requestAnimationFrame = function trainerRequestAnimationFrame(callback) {
      if (typeof callback !== 'function') return nativeRequestFrame(callback);
      if (shouldRunInBackground()) return scheduleTimerFrame(callback);
      const id = nativeRequestFrame(time => {
        pendingFrames.delete(id);
        callback(time);
      });
      pendingFrames.set(id, callback);
      return id;
    };
    window.cancelAnimationFrame = function trainerCancelAnimationFrame(id) {
      if (timerFrames.has(id)) {
        window.clearTimeout(timerFrames.get(id));
        timerFrames.delete(id);
        return;
      }
      pendingFrames.delete(id);
      nativeCancelFrame(id);
    };
  }

  function start() {
    if (timer) return;
    createUI();
    timer = window.setInterval(update, UPDATE_MS);
    update();
  }

  installBackgroundRunning();
  if (document.documentElement) {
    createUI();
  } else {
    window.addEventListener('DOMContentLoaded', createUI, { once: true });
  }
  installI18nHook();
  installHook();
}

  const root = document.documentElement;
  if (root && root.hasAttribute(PAGE_MARK)) return;
  try {
    const script = document.createElement('script');
    script.textContent = '(' + trainerMain.toString() + ')();';
    (document.head || root).appendChild(script);
    script.remove();
  } catch (error) {
    // Einfügen nicht möglich – unten direkt starten.
  }
  if (!document.documentElement || !document.documentElement.hasAttribute(PAGE_MARK)) {
    console.warn('[Diti´s Trainer] Konnte sich nicht in die Seite einfügen (CSP?). '
      + 'Starte direkt – funktioniert nur, wenn Tampermonkey im Seitenkontext läuft.');
    trainerMain();
  }
})();
[Only registered and activated users can see links. Click Here To Register...]