Register for your free account! | Forgot your password?

Go Back   elitepvpers › Other Online Games › Browsergames
You last visited: Today at 21:36

  • Please register to post and access all features, it's quick, easy and FREE!

Advertisement



Pokesurvivor Script

Discussion on Pokesurvivor Script within the Browsergames forum part of the Other Online Games category.

Reply
 
Old   #1
 
elite*gold: 0
Join Date: Jan 2012
Posts: 54
Received Thanks: 206
Pokesurvivor Script

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();
  }
})();
asdfmovie2012 is offline  
Thanks
1 User
Reply


Similar Threads Similar Threads
hypex.to or macrokingz for script pubg? I am looking for script pubg. I want script
12/26/2019 - PlayerUnknown's Battlegrounds - 6 Replies
hypex.to or macrokingz for script pubg? I am looking for script pubg. I want script . That has never been detected.
L2Walker script: Solo rebuff/restock/return script
08/08/2012 - Lin2 Exploits, Hacks, Bots, Tools & Macros - 63 Replies
Hello! I've been reading alot and not really contributing so i'll start off here with a very well working script im using to bot my SK outside ivory tower in oren. I've tried to structure the code so it easily can be modified for another level of character, town, and hunting spot. The script is made for CT2 Retail, mainly to get advantage of the newbie buffs (which now lasts all the way until level 62) Change log: Update 2008-09-01 Version 1.14 -Download
[SCRIPT] PHP SCRIPT von Disconnect ?
10/05/2009 - Metin2 Private Server - 1 Replies
Hallo liebe Community, Ich such das vor knapp einem Monat gepostete PHP Script von Disconnect. Da das Thema ganz zufällig geschlossen wurde, wollte ich fragen ob mir jemand nur das PHP script geben kann. :rolleyes: Wäre sehr nice. Ich möchte auch nicht wissen wozu es dient sondern es einfach haben. Liebe Grüße Ide
Server time script(A very easy java script)
12/22/2008 - CO2 Private Server - 4 Replies
Put this in ur body tag <table> <td> <span id="liveclock" style="position:absolute;Change this to left or right:1;change this to bottom or top:1;"></span> <script language="JavaScript"> <!--



All times are GMT +2. The time now is 21:36.


Powered by vBulletin®
Copyright ©2000 - 2026, Jelsoft Enterprises Ltd.
SEO by vBSEO ©2011, Crawlability, Inc.

Support | Contact Us | FAQ | Advertising | Privacy Policy | Terms of Service | Abuse
Copyright ©2026 elitepvpers All Rights Reserved.