Script für Pokesurvivor Tampermonkey
[Only registered and activated users can see links. Click Here To Register...]
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();
}
})();