Chain Planer (Torn)

Chain-Hit-Planer für Torn – mit API-Sync, einklappbarer Member-Verwaltung, verschiebbarem & größenveränderbarem Fenster

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

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

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

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

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

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

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

Advertisement:

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

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

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

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

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

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

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

Advertisement:

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         Chain Planer (Torn)
// @namespace    https://edelzentrale.de/
// @version      1.3
// @description  Chain-Hit-Planer für Torn – mit API-Sync, einklappbarer Member-Verwaltung, verschiebbarem & größenveränderbarem Fenster
// @author       xShaYaKaZ
// @match        https://www.torn.com/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_xmlhttpRequest
// @connect      api.torn.com
// @run-at       document-end
// ==/UserScript==

(function () {
  'use strict';

  /* ============================================================
     STORAGE
  ============================================================ */
  const STORAGE_KEY = 'cp_chainPlaner_v1';

  function loadState() {
    try {
      const raw = GM_getValue(STORAGE_KEY, null);
      if (raw) {
        const p = JSON.parse(raw);
        if (!p.members) p.members = [];
        if (!p.planner) p.planner = { date: '', members: [], sequence: [], completed: {} };
        if (!p.planner.completed) p.planner.completed = {};
        return p;
      }
    } catch (e) { console.error('[ChainPlaner] Ladefehler:', e); }
    return { members: [], planner: { date: '', members: [], sequence: [], completed: {} } };
  }
  function saveState() {
    try { GM_setValue(STORAGE_KEY, JSON.stringify(state)); }
    catch (e) { console.error('[ChainPlaner] Speicherfehler:', e); }
  }
  let state = loadState();

  const API_KEY_STORE = 'cp_api_key_v1';
  function loadApiKey() {
    try { return GM_getValue(API_KEY_STORE, ''); } catch (e) { return ''; }
  }
  function saveApiKey(key) {
    try { GM_setValue(API_KEY_STORE, key); } catch (e) {}
  }

  /* ============================================================
     STYLES (alles scoped unter #cp-root / #cp-fab, keine globalen
     :root Variablen um Konflikte mit Torn zu vermeiden)
  ============================================================ */
  const style = document.createElement('style');
  style.textContent = `
#cp-fab{
  position:fixed;left:0;top:0;z-index:2147483000;
  width:30px;height:30px;border-radius:50%;border:none;cursor:grab;
  background:#ff8c42;color:#1a1208;font-family:'Oswald',sans-serif;
  font-weight:700;font-size:8.5px;letter-spacing:.2px;
  display:flex;align-items:center;justify-content:center;text-align:center;
  box-shadow:0 3px 10px rgba(0,0,0,.45);touch-action:none;user-select:none;
  padding:0;line-height:1;
}
#cp-fab:active{cursor:grabbing;transform:scale(.95);}

#cp-root{
  --bg:#12151a;--panel:#1a1e25;--panel-alt:#20252d;--panel-hover:#262c35;--border:#2b313b;
  --text:#eceae5;--text-dim:#8b919c;--accent:#ff9a52;--accent-dim:#c06a2e;
  --accent-2:#3ee2b0;--danger:#ff6b6b;
  --font-display:'Oswald',sans-serif;--font-body:'Inter',system-ui,sans-serif;
  --font-mono:'Roboto Mono',monospace;
  --r-lg:10px;--r-md:7px;--r-sm:4px;

  position:fixed;z-index:2147482999;
  background:var(--bg);color:var(--text);font-family:var(--font-body);
  box-sizing:border-box;
  border:1px solid var(--border);border-radius:var(--r-lg);
  box-shadow:0 20px 50px -12px rgba(0,0,0,.6),0 0 0 1px rgba(255,255,255,.02);
  min-width:300px;min-height:260px;
  width:min(420px,92vw);height:min(600px,85vh);
  display:none;flex-direction:column;
  overflow:hidden;
}
#cp-root *{box-sizing:border-box;}
#cp-root.cp-open{display:flex;}

#cp-root ::-webkit-scrollbar{width:9px;height:9px;}
#cp-root ::-webkit-scrollbar-track{background:transparent;}
#cp-root ::-webkit-scrollbar-thumb{background:var(--border);border-radius:5px;border:2px solid var(--bg);}
#cp-root ::-webkit-scrollbar-thumb:hover{background:var(--accent-dim);}

#cp-root .cp-app{
  container-type:inline-size;container-name:cpapp;
  flex:1;min-height:0;overflow-y:auto;-webkit-overflow-scrolling:touch;
  padding:0 12px 20px;
}

#cp-root header.cp-top{
  background:linear-gradient(180deg,var(--panel-alt),var(--panel));
  display:flex;align-items:center;gap:10px;
  padding:11px 12px;border-bottom:1px solid var(--border);
  cursor:move;touch-action:none;user-select:none;
  flex-shrink:0;position:relative;
}
#cp-root header.cp-top::after{
  content:"";position:absolute;left:0;right:0;bottom:-1px;height:1px;
  background:linear-gradient(90deg,transparent,var(--accent-dim),transparent);opacity:.5;
}
#cp-resize-handle{
  position:absolute;right:2px;bottom:2px;width:16px;height:16px;
  cursor:nwse-resize;touch-action:none;z-index:6;opacity:.5;
  background:
    radial-gradient(circle,var(--text-dim) 1px,transparent 1.2px) 12px 12px/4px 4px no-repeat,
    radial-gradient(circle,var(--text-dim) 1px,transparent 1.2px) 12px 4px/4px 4px no-repeat,
    radial-gradient(circle,var(--text-dim) 1px,transparent 1.2px) 4px 12px/4px 4px no-repeat;
}
#cp-resize-handle:hover{opacity:.9;}
#cp-root header.cp-top h1{
  font-family:var(--font-display);font-weight:600;font-size:18px;
  letter-spacing:.4px;margin:0;color:var(--text);text-transform:uppercase;flex:1;
}
#cp-root header.cp-top h1 span{color:var(--accent);}
#cp-root header.cp-top .cp-sub{
  font-family:var(--font-mono);font-size:9.5px;color:var(--text-dim);
  letter-spacing:1px;text-transform:uppercase;display:none;
}
#cp-close{
  background:transparent;border:1px solid var(--border);color:var(--text-dim);
  width:28px;height:28px;border-radius:var(--r-sm);font-size:13px;cursor:pointer;flex-shrink:0;
  display:flex;align-items:center;justify-content:center;transition:border-color .15s,color .15s;
}
#cp-close:active{border-color:var(--danger);color:var(--danger);}

#cp-root .cp-field{display:flex;flex-direction:column;gap:5px;}
#cp-root .cp-field label{
  font-size:10px;text-transform:uppercase;letter-spacing:.8px;
  color:var(--text-dim);font-family:var(--font-mono);
}
#cp-root input,#cp-root select{
  background:var(--panel-alt);border:1px solid var(--border);color:var(--text);
  font-family:var(--font-mono);font-size:16px;padding:9px 10px;border-radius:var(--r-sm);
  outline:none;transition:border-color .15s,background .15s;-webkit-appearance:none;
}
#cp-root input:focus,#cp-root select:focus{border-color:var(--accent);background:var(--panel-hover);}
#cp-root input[type="checkbox"]{
  width:18px;height:18px;padding:0;flex-shrink:0;accent-color:var(--accent-2);cursor:pointer;
}
#cp-root button.cp-btn{
  font-family:var(--font-display);font-size:12px;letter-spacing:.7px;text-transform:uppercase;
  background:linear-gradient(180deg,var(--accent),var(--accent-dim));color:#1a1208;border:none;
  padding:11px 16px;border-radius:var(--r-sm);cursor:pointer;min-height:40px;
  transition:filter .15s,transform .1s;box-shadow:0 2px 6px rgba(0,0,0,.25);
}
#cp-root button.cp-btn:hover{filter:brightness(1.08);}
#cp-root button.cp-btn:active{transform:scale(.97);filter:brightness(.97);}
#cp-root button.cp-btn:disabled{opacity:.35;cursor:not-allowed;filter:none;box-shadow:none;}
#cp-root button.cp-btn.cp-secondary{background:transparent;color:var(--text);border:1px solid var(--border);box-shadow:none;}
#cp-root button.cp-btn.cp-secondary:hover{border-color:var(--accent);color:var(--accent);}
#cp-root button.cp-btn.cp-secondary:active{border-color:var(--accent);color:var(--accent);}
#cp-root button.cp-btn.cp-small{padding:8px 12px;font-size:10.5px;min-height:34px;}
#cp-root .cp-empty{color:var(--text-dim);font-family:var(--font-mono);font-size:12px;padding:18px 0;text-align:center;letter-spacing:.2px;}

#cp-toast{
  position:fixed;bottom:80px;left:50%;transform:translateX(-50%) translateY(10px);
  background:var(--panel,#1a1e25);border:1px solid var(--accent,#ff9a52);color:var(--text,#eceae5);
  font-family:var(--font-mono,monospace);font-size:12px;padding:11px 18px;border-radius:var(--r-md,6px);
  opacity:0;transition:opacity .2s,transform .2s;pointer-events:none;z-index:2147483001;
  max-width:90vw;text-align:center;box-shadow:0 6px 20px rgba(0,0,0,.4);
}
#cp-toast.cp-show{opacity:1;transform:translateX(-50%) translateY(0);}

#cp-root .cp-planer-topbar{display:flex;gap:10px;align-items:flex-end;flex-wrap:wrap;margin-bottom:14px;}
#cp-root .cp-planer-grid{display:flex;flex-direction:column;gap:14px;}
@container cpapp (min-width:760px){
  #cp-root .cp-planer-grid{display:grid;grid-template-columns:repeat(3,1fr);}
}
@media(min-width:600px){
  #cp-root header.cp-top .cp-sub{display:inline;}
}

#cp-root .cp-card2{
  background:var(--panel);border:1px solid var(--border);border-radius:var(--r-md);
  display:flex;flex-direction:column;overflow:hidden;min-height:220px;
}
#cp-root .cp-card2-head{
  font-family:var(--font-display);font-size:11.5px;text-transform:uppercase;letter-spacing:1px;
  padding:10px 12px;border-bottom:1px solid var(--border);color:var(--text);
  background:var(--panel-alt);display:flex;align-items:center;gap:6px;flex-wrap:wrap;
}
#cp-root .cp-card2-head::before{content:"";width:6px;height:6px;border-radius:50%;background:var(--accent);flex-shrink:0;box-shadow:0 0 6px var(--accent);}
#cp-root .cp-add-row{
  display:flex;gap:8px;align-items:stretch;flex-wrap:wrap;
  padding:10px 12px;border-bottom:1px solid var(--border);
}
#cp-root .cp-add-row select{flex:1 1 100px;min-width:90px;}
#cp-root .cp-add-row input[type=number]{width:64px;flex-shrink:0;}
#cp-root .cp-mode-toggle{display:flex;border:1px solid var(--border);border-radius:var(--r-sm);overflow:hidden;flex-shrink:0;}
#cp-root .cp-pm-btn{
  font-family:var(--font-mono);font-size:10.5px;text-transform:uppercase;letter-spacing:.5px;
  background:var(--panel-alt);border:none;color:var(--text-dim);padding:8px 10px;cursor:pointer;min-height:38px;
  transition:background .15s,color .15s;
}
#cp-root .cp-pm-btn.cp-active{background:var(--accent);color:#1a1208;}

#cp-root .cp-member-list2,#cp-root .cp-seq-list{
  list-style:none;margin:0;padding:9px;
  display:flex;flex-direction:column;gap:6px;flex:1;
  max-height:60vh;overflow-y:auto;
}
@media(min-width:900px){
  #cp-root .cp-member-list2,#cp-root .cp-seq-list{max-height:340px;}
}

#cp-root .cp-member-item2{
  display:flex;align-items:center;gap:8px;flex-wrap:wrap;background:var(--panel-alt);
  border:1px solid var(--border);border-radius:var(--r-sm);padding:8px 10px;
  transition:background .15s,border-color .15s;
}
#cp-root .cp-member-item2:hover{background:var(--panel-hover);}
#cp-root .cp-member-item2 .cp-name{flex:1 1 100px;font-weight:600;font-size:13px;}
#cp-root .cp-member-item2 .cp-mode-badge{
  font-family:var(--font-mono);font-size:9.5px;text-transform:uppercase;letter-spacing:.5px;
  padding:2px 6px;border-radius:var(--r-sm);
}
#cp-root .cp-mode-badge.cp-normal{color:var(--text-dim);border:1px solid var(--border);}
#cp-root .cp-mode-badge.cp-schnell{color:var(--accent);border:1px solid var(--accent-dim);}
#cp-root .cp-member-item2 input[type=number]{width:60px;font-size:14px;padding:6px;}
#cp-root .cp-del-btn{
  background:transparent;border:none;color:var(--danger);cursor:pointer;font-size:16px;
  padding:4px 6px;flex-shrink:0;min-width:30px;opacity:.75;transition:opacity .15s;
}
#cp-root .cp-del-btn:hover{opacity:1;}

#cp-root .cp-seq-item{
  display:flex;align-items:center;gap:6px;flex-wrap:wrap;background:var(--panel-alt);
  border:1px solid var(--border);border-radius:var(--r-sm);padding:7px 8px;
  transition:background .15s;
}
#cp-root .cp-seq-item:hover{background:var(--panel-hover);}
#cp-root .cp-reorder{display:flex;flex-direction:column;gap:2px;flex-shrink:0;}
#cp-root .cp-move{
  background:var(--panel);border:1px solid var(--border);color:var(--text-dim);
  cursor:pointer;font-size:9px;line-height:1;padding:4px 6px;border-radius:3px;min-width:24px;
  transition:border-color .15s,color .15s;
}
#cp-root .cp-move:disabled{opacity:.25;cursor:not-allowed;}
#cp-root .cp-move:hover:not(:disabled){border-color:var(--accent);color:var(--accent);}
#cp-root .cp-seq-item .cp-name{flex:1 1 90px;font-weight:600;font-size:12.5px;}
#cp-root .cp-seq-item input[type=number]{width:54px;font-size:14px;padding:6px 5px;}
#cp-root .cp-seq-item .cp-del-btn{font-size:14px;}
#cp-root .cp-split-btn{
  background:transparent;border:1px solid var(--border);color:var(--text-dim);
  font-family:var(--font-mono);font-size:9.5px;padding:5px 8px;border-radius:3px;cursor:pointer;
  letter-spacing:.3px;flex-shrink:0;transition:border-color .15s,color .15s;
}
#cp-root .cp-split-btn:hover{border-color:var(--accent);color:var(--accent);}
#cp-root .cp-alloc{
  font-family:var(--font-mono);font-size:9.5px;flex-shrink:0;padding:3px 6px;border-radius:3px;
}
#cp-root .cp-alloc.cp-ok{color:var(--accent-2);border:1px solid rgba(62,226,176,.3);background:rgba(62,226,176,.06);}
#cp-root .cp-alloc.cp-over{color:var(--danger);border:1px solid rgba(255,107,107,.3);background:rgba(255,107,107,.06);}
#cp-root .cp-alloc.cp-partial{color:var(--accent);border:1px solid rgba(255,154,82,.3);background:rgba(255,154,82,.06);}

#cp-root .cp-output{padding:12px 14px;flex:1;overflow-y:auto;font-family:var(--font-mono);font-size:13px;line-height:1.85;max-height:60vh;}
#cp-root .cp-output-line{display:flex;align-items:center;gap:8px;margin-bottom:1px;border-radius:var(--r-sm);transition:background .15s;}
#cp-root .cp-output-line:hover{background:var(--panel-alt);}
#cp-root .cp-output-line label{display:flex;align-items:center;gap:8px;cursor:pointer;flex:1;padding:4px 6px;}
#cp-root .cp-output-line .cp-text{color:var(--text);}
#cp-root .cp-output-line.cp-done .cp-text{color:var(--text-dim);text-decoration:line-through;}
#cp-root .cp-output-date{
  font-family:var(--font-display);font-size:12.5px;color:var(--accent);
  letter-spacing:.5px;margin-bottom:8px;text-transform:uppercase;
  padding-bottom:6px;border-bottom:1px solid var(--border);
}

#cp-root .cp-stamm{background:var(--panel);border:1px solid var(--border);border-radius:var(--r-md);margin:14px 0 18px;overflow:hidden;}
#cp-root .cp-stamm-head{
  display:flex;align-items:center;gap:8px;cursor:pointer;user-select:none;
  padding:12px 14px;background:var(--panel-alt);transition:background .15s;
}
#cp-root .cp-stamm-head:hover{background:var(--panel-hover);}
#cp-root .cp-stamm-head h2{
  font-family:var(--font-display);font-size:13px;text-transform:uppercase;
  letter-spacing:1px;margin:0;color:var(--text);display:flex;align-items:center;gap:8px;flex:1;
}
#cp-root .cp-stamm-head h2::before{content:"";width:8px;height:8px;border-radius:50%;background:var(--accent);display:inline-block;box-shadow:0 0 6px var(--accent);}
#cp-root .cp-stamm-count{
  font-family:var(--font-mono);font-size:10px;color:var(--text-dim);
  background:var(--panel);border:1px solid var(--border);border-radius:20px;padding:2px 9px;
}
#cp-root .cp-stamm-chevron{
  font-size:11px;color:var(--text-dim);transition:transform .2s;flex-shrink:0;
}
#cp-root .cp-stamm.cp-collapsed .cp-stamm-chevron{transform:rotate(-90deg);}
#cp-root .cp-stamm-body{padding:14px;}
#cp-root .cp-stamm.cp-collapsed .cp-stamm-body{display:none;}
#cp-root .cp-member-add{display:flex;gap:8px;margin-bottom:12px;flex-wrap:wrap;}
#cp-root .cp-member-add input{flex:1 1 160px;}
#cp-root .cp-member-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:6px;}
#cp-root .cp-member-list li{
  display:flex;justify-content:space-between;align-items:center;
  background:var(--panel-alt);border:1px solid var(--border);
  padding:8px 12px;border-radius:var(--r-sm);transition:background .15s;
}
#cp-root .cp-member-list li:hover{background:var(--panel-hover);}
#cp-root .cp-member-list li .cp-nm{font-weight:600;}
#cp-root .cp-member-list li button{
  background:transparent;border:none;color:var(--danger);cursor:pointer;font-size:11.5px;
  font-family:var(--font-mono);text-transform:uppercase;letter-spacing:.5px;padding:6px;opacity:.8;
}
#cp-root .cp-member-list li button:hover{opacity:1;}
`;
  document.head.appendChild(style);

  /* ============================================================
     DOM AUFBAU
  ============================================================ */
  const fab = document.createElement('button');
  fab.id = 'cp-fab';
  fab.textContent = 'CP';
  document.body.appendChild(fab);

  const root = document.createElement('div');
  root.id = 'cp-root';
  root.innerHTML = `
<header class="cp-top">
  <h1>Chain<span>Planer</span></h1>
  <span class="cp-sub">Torn · Hit-Planung</span>
  <button id="cp-close" title="Schließen">✕</button>
</header>
<div class="cp-app">

  <div class="cp-stamm cp-collapsed" id="cp-stamm">
    <div class="cp-stamm-head" id="cp-stamm-head">
      <h2>Member verwalten</h2>
      <span class="cp-stamm-count" id="cp-stamm-count">0</span>
      <span class="cp-stamm-chevron">▾</span>
    </div>
    <div class="cp-stamm-body">
      <div class="cp-member-add">
        <input type="password" id="cp-api-key" placeholder="Torn API-Key (Fraktion)">
        <button class="cp-btn cp-secondary" id="cp-btn-sync">🔄 Sync Fraktion</button>
      </div>
      <div id="cp-sync-status" style="font-family:var(--font-mono);font-size:10.5px;color:var(--text-dim);margin-bottom:10px;"></div>
      <div class="cp-member-add">
        <input type="text" id="cp-inp-new-member" placeholder="Neuer Member-Name (manuell)">
        <button class="cp-btn" id="cp-btn-add-member">Hinzufügen</button>
      </div>
      <ul class="cp-member-list" id="cp-member-list">
        <div class="cp-empty">Noch keine Member. Füge oben Member hinzu oder synce die Fraktion.</div>
      </ul>
    </div>
  </div>

  <div class="cp-planer-topbar">
    <div class="cp-field" style="min-width:150px;">
      <label>Chain-Datum</label>
      <input type="date" id="cp-planer-date">
    </div>
    <button class="cp-btn cp-secondary cp-small" id="cp-btn-planer-reset">Planer zurücksetzen</button>
  </div>

  <div class="cp-planer-grid">

    <div class="cp-card2">
      <div class="cp-card2-head">Fenster 1 – Mitglieder &amp; Gesamt-Hits</div>
      <div class="cp-add-row">
        <select id="cp-planer-member-select"></select>
        <input type="number" id="cp-planer-hits-input" min="1" max="999" value="10" title="Gesamt-Hits">
        <div class="cp-mode-toggle">
          <button class="cp-pm-btn cp-active" data-mode="normal">Normal</button>
          <button class="cp-pm-btn" data-mode="schnell">⚡ Schnell</button>
        </div>
        <button class="cp-btn cp-small" id="cp-btn-planer-add">+</button>
      </div>
      <ul id="cp-planer-member-list" class="cp-member-list2"></ul>
    </div>

    <div class="cp-card2">
      <div class="cp-card2-head">
        Fenster 2 – Reihenfolge
        <span style="font-size:10px;color:var(--text-dim);font-family:var(--font-mono);">(▲▼ · ÷ Split)</span>
      </div>
      <div id="cp-planer-seq-info" style="font-family:var(--font-mono);font-size:11px;color:var(--text-dim);padding:6px 10px 0;"></div>
      <ul id="cp-planer-seq-list" class="cp-seq-list"></ul>
    </div>

    <div class="cp-card2">
      <div class="cp-card2-head" style="justify-content:space-between;">
        <span>Fenster 3 – Chain-Ausgabe</span>
        <button class="cp-btn cp-secondary cp-small" id="cp-btn-planer-copy">Kopieren</button>
      </div>
      <div id="cp-planer-output" class="cp-output"></div>
    </div>

  </div>
</div>
<div id="cp-resize-handle" title="Größe ändern"></div>
`;
  document.body.appendChild(root);

  const toast = document.createElement('div');
  toast.id = 'cp-toast';
  document.body.appendChild(toast);

  /* ============================================================
     STAMMDATEN EIN-/AUSKLAPPEN
  ============================================================ */
  const STAMM_COLLAPSED_KEY = 'cp_stamm_collapsed_v1';
  const stammEl = root.querySelector('#cp-stamm');
  (function initStammCollapse() {
    let collapsed = true;
    try {
      const raw = GM_getValue(STAMM_COLLAPSED_KEY, null);
      if (raw !== null) collapsed = raw === 'true' || raw === true;
    } catch (e) {}
    stammEl.classList.toggle('cp-collapsed', collapsed);
  })();
  root.querySelector('#cp-stamm-head').addEventListener('click', () => {
    const collapsed = stammEl.classList.toggle('cp-collapsed');
    try { GM_setValue(STAMM_COLLAPSED_KEY, collapsed); } catch (e) {}
  });

  /* ============================================================
     POSITION / GRÖSSE - LADEN & SPEICHERN
  ============================================================ */
  const RECT_KEY = 'cp_win_rect_v1';
  const FAB_KEY = 'cp_fab_pos_v1';

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

  function loadWinRect() {
    try {
      const raw = GM_getValue(RECT_KEY, null);
      if (raw) return JSON.parse(raw);
    } catch (e) {}
    const w = Math.min(420, window.innerWidth * 0.92);
    const h = Math.min(600, window.innerHeight * 0.85);
    return { x: Math.round(window.innerWidth - w - 24), y: 70, w: Math.round(w), h: Math.round(h) };
  }
  function saveWinRect(rect) {
    try { GM_setValue(RECT_KEY, JSON.stringify(rect)); } catch (e) {}
  }
  function applyWinRect(rect) {
    root.style.left = rect.x + 'px';
    root.style.top = rect.y + 'px';
    root.style.width = rect.w + 'px';
    root.style.height = rect.h + 'px';
  }
  function currentWinRect() {
    return { x: root.offsetLeft, y: root.offsetTop, w: root.offsetWidth, h: root.offsetHeight };
  }

  function loadFabPos() {
    try {
      const raw = GM_getValue(FAB_KEY, null);
      if (raw) return JSON.parse(raw);
    } catch (e) {}
    return { x: window.innerWidth - 46, y: window.innerHeight - 80 };
  }
  function saveFabPos(pos) {
    try { GM_setValue(FAB_KEY, JSON.stringify(pos)); } catch (e) {}
  }
  function applyFabPos(pos) {
    fab.style.left = pos.x + 'px';
    fab.style.top = pos.y + 'px';
  }

  applyWinRect(loadWinRect());
  applyFabPos(loadFabPos());

  /* ============================================================
     OPEN / CLOSE
  ============================================================ */
  function openPanel() {
    const rect = currentWinRect();
    const w = rect.w || 420, h = rect.h || 600;
    root.style.left = clamp(rect.x || 0, 0, Math.max(0, window.innerWidth - w)) + 'px';
    root.style.top = clamp(rect.y || 0, 0, Math.max(0, window.innerHeight - h)) + 'px';
    root.classList.add('cp-open');
  }
  function closePanel() {
    root.classList.remove('cp-open');
  }
  fab.addEventListener('click', () => {
    if (fab._didDrag) { fab._didDrag = false; return; }
    if (root.classList.contains('cp-open')) closePanel(); else openPanel();
  });
  root.querySelector('#cp-close').addEventListener('click', closePanel);

  /* ============================================================
     FENSTER VERSCHIEBEN (Titelleiste)
  ============================================================ */
  (function enableWindowDrag() {
    const header = root.querySelector('header.cp-top');
    let drag = null;
    header.addEventListener('pointerdown', e => {
      if (e.target.closest('#cp-close')) return;
      drag = { startX: e.clientX, startY: e.clientY, startLeft: root.offsetLeft, startTop: root.offsetTop };
      header.setPointerCapture(e.pointerId);
      e.preventDefault();
    });
    header.addEventListener('pointermove', e => {
      if (!drag) return;
      const dx = e.clientX - drag.startX, dy = e.clientY - drag.startY;
      const nl = clamp(drag.startLeft + dx, 0, window.innerWidth - root.offsetWidth);
      const nt = clamp(drag.startTop + dy, 0, window.innerHeight - root.offsetHeight);
      root.style.left = nl + 'px';
      root.style.top = nt + 'px';
    });
    function endDrag() {
      if (!drag) return;
      drag = null;
      saveWinRect(currentWinRect());
    }
    header.addEventListener('pointerup', endDrag);
    header.addEventListener('pointercancel', endDrag);
  })();

  /* ============================================================
     FENSTER GRÖSSE ÄNDERN (Ecke unten rechts)
  ============================================================ */
  (function enableWindowResize() {
    const handle = root.querySelector('#cp-resize-handle');
    let res = null;
    handle.addEventListener('pointerdown', e => {
      res = { startX: e.clientX, startY: e.clientY, startW: root.offsetWidth, startH: root.offsetHeight };
      handle.setPointerCapture(e.pointerId);
      e.preventDefault();
      e.stopPropagation();
    });
    handle.addEventListener('pointermove', e => {
      if (!res) return;
      const dw = e.clientX - res.startX, dh = e.clientY - res.startY;
      const maxW = window.innerWidth - root.offsetLeft;
      const maxH = window.innerHeight - root.offsetTop;
      const nw = clamp(res.startW + dw, 300, maxW);
      const nh = clamp(res.startH + dh, 260, maxH);
      root.style.width = nw + 'px';
      root.style.height = nh + 'px';
    });
    function endResize() {
      if (!res) return;
      res = null;
      saveWinRect(currentWinRect());
    }
    handle.addEventListener('pointerup', endResize);
    handle.addEventListener('pointercancel', endResize);
  })();

  /* ============================================================
     FAB VERSCHIEBEN
  ============================================================ */
  (function enableFabDrag() {
    let drag = null;
    const THRESHOLD = 6;
    fab.addEventListener('pointerdown', e => {
      drag = { startX: e.clientX, startY: e.clientY, startLeft: fab.offsetLeft, startTop: fab.offsetTop, moved: false };
      fab.setPointerCapture(e.pointerId);
    });
    fab.addEventListener('pointermove', e => {
      if (!drag) return;
      const dx = e.clientX - drag.startX, dy = e.clientY - drag.startY;
      if (Math.abs(dx) > THRESHOLD || Math.abs(dy) > THRESHOLD) drag.moved = true;
      if (!drag.moved) return;
      const nl = clamp(drag.startLeft + dx, 0, window.innerWidth - fab.offsetWidth);
      const nt = clamp(drag.startTop + dy, 0, window.innerHeight - fab.offsetHeight);
      fab.style.left = nl + 'px';
      fab.style.top = nt + 'px';
    });
    function endDrag() {
      if (!drag) return;
      if (drag.moved) {
        fab._didDrag = true;
        saveFabPos({ x: fab.offsetLeft, y: fab.offsetTop });
      }
      drag = null;
    }
    fab.addEventListener('pointerup', endDrag);
    fab.addEventListener('pointercancel', endDrag);
  })();

  /* ============================================================
     HELPERS
  ============================================================ */
  function showToast(msg) {
    toast.textContent = msg;
    toast.classList.add('cp-show');
    clearTimeout(toast._t);
    toast._t = setTimeout(() => toast.classList.remove('cp-show'), 2200);
  }
  function escapeHtml(s) {
    return String(s).replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
  }
  function newId() { return '_' + Math.random().toString(36).slice(2, 9); }

  /* ============================================================
     MEMBER-VERWALTUNG (Stammdaten)
  ============================================================ */
  function renderMemberList() {
    const ul = root.querySelector('#cp-member-list');
    const countBadge = root.querySelector('#cp-stamm-count');
    if (countBadge) countBadge.textContent = state.members.length;
    if (state.members.length === 0) {
      ul.innerHTML = '<div class="cp-empty">Noch keine Member. Füge oben Member hinzu.</div>';
    } else {
      ul.innerHTML = [...state.members].sort((a, b) => a.localeCompare(b, 'de')).map(name => `
        <li>
          <span class="cp-nm">${escapeHtml(name)}</span>
          <button data-remove="${escapeHtml(name)}">Entfernen</button>
        </li>`).join('');
      ul.querySelectorAll('[data-remove]').forEach(btn => {
        btn.addEventListener('click', () => {
          const name = btn.dataset.remove;
          if (!confirm(`"${name}" entfernen?`)) return;
          state.members = state.members.filter(m => m !== name);
          const removedIds = state.planner.members.filter(m => m.name === name).map(m => m.id);
          state.planner.members = state.planner.members.filter(m => m.name !== name);
          removedIds.forEach(id => {
            state.planner.sequence.filter(s => s.memberId === id).forEach(s => delete state.planner.completed[s.id]);
            state.planner.sequence = state.planner.sequence.filter(s => s.memberId !== id);
          });
          saveState();
          renderMemberList();
          renderPlanerMembers();
          renderPlanerSequence();
          renderPlanerOutput();
        });
      });
    }
    refreshPlanerSelect();
  }

  root.querySelector('#cp-btn-add-member').addEventListener('click', addMember);
  root.querySelector('#cp-inp-new-member').addEventListener('keydown', e => { if (e.key === 'Enter') addMember(); });
  function addMember() {
    const inp = root.querySelector('#cp-inp-new-member');
    const name = inp.value.trim();
    if (!name) { showToast('Bitte einen Namen eingeben.'); return; }
    if (state.members.includes(name)) { showToast('Dieser Member existiert bereits.'); return; }
    state.members.push(name);
    saveState();
    inp.value = '';
    renderMemberList();
  }

  /* ============================================================
     TORN API - FRAKTIONSMITGLIEDER SYNC
  ============================================================ */
  const apiKeyInput = root.querySelector('#cp-api-key');
  const syncStatus = root.querySelector('#cp-sync-status');
  const syncBtn = root.querySelector('#cp-btn-sync');
  apiKeyInput.value = loadApiKey();

  apiKeyInput.addEventListener('change', () => {
    saveApiKey(apiKeyInput.value.trim());
  });

  syncBtn.addEventListener('click', syncFactionMembers);

  function syncFactionMembers() {
    const key = apiKeyInput.value.trim();
    if (!key) { showToast('Bitte zuerst einen Torn API-Key eintragen.'); return; }
    saveApiKey(key);

    syncBtn.disabled = true;
    syncStatus.textContent = 'Synchronisiere Fraktionsmitglieder …';

    GM_xmlhttpRequest({
      method: 'GET',
      url: `https://api.torn.com/faction/?selections=basic&key=${encodeURIComponent(key)}`,
      onload: function (res) {
        syncBtn.disabled = false;
        let data;
        try { data = JSON.parse(res.responseText); }
        catch (e) { syncStatus.textContent = 'Antwort konnte nicht gelesen werden.'; showToast('API-Antwort ungültig.'); return; }

        if (data.error) {
          syncStatus.textContent = `API-Fehler: ${data.error.error}`;
          showToast(`API-Fehler: ${data.error.error}`);
          return;
        }

        const membersObj = data.members || {};
        const names = Object.values(membersObj).map(m => m.name).filter(Boolean);

        if (names.length === 0) {
          syncStatus.textContent = 'Keine Mitglieder in der API-Antwort gefunden.';
          showToast('Keine Mitglieder gefunden.');
          return;
        }

        let added = 0;
        names.forEach(n => {
          if (!state.members.includes(n)) { state.members.push(n); added++; }
        });
        saveState();
        renderMemberList();

        const factionName = data.name ? ` (${data.name})` : '';
        syncStatus.textContent = `Zuletzt synct: ${names.length} Member${factionName}, ${added} neu hinzugefügt.`;
        showToast(`Sync fertig: ${added} neue Member.`);
      },
      onerror: function () {
        syncBtn.disabled = false;
        syncStatus.textContent = 'Netzwerkfehler bei der API-Anfrage.';
        showToast('Netzwerkfehler bei der API-Anfrage.');
      }
    });
  }

  /* ============================================================
     CHAIN PLANER
  ============================================================ */
  let planerMode = 'normal';

  function planerEnsure() {
    if (!state.planner) state.planner = { date: '', members: [], sequence: [], completed: {} };
    if (!state.planner.completed) state.planner.completed = {};
  }
  function planerSave() { planerEnsure(); saveState(); }

  function planerAllocated(memberId) {
    return state.planner.sequence.filter(s => s.memberId === memberId).reduce((s, i) => s + i.hits, 0);
  }

  function refreshPlanerSelect() {
    const sel = root.querySelector('#cp-planer-member-select');
    const added = new Set(state.planner.members.map(m => m.name));
    const available = state.members.filter(n => !added.has(n));
    sel.innerHTML = available.length
      ? available.map(n => `<option value="${escapeHtml(n)}">${escapeHtml(n)}</option>`).join('')
      : '<option value="">Alle Member bereits hinzugefügt</option>';
    sel.disabled = !available.length;
    root.querySelector('#cp-btn-planer-add').disabled = !available.length;
  }

  // ---- Fenster 1 ----
  function renderPlanerMembers() {
    planerEnsure();
    const ul = root.querySelector('#cp-planer-member-list');
    refreshPlanerSelect();

    if (state.planner.members.length === 0) {
      ul.innerHTML = '<div class="cp-empty" style="margin:8px;">Noch keine Member hinzugefügt.</div>';
      return;
    }

    ul.innerHTML = state.planner.members.map(m => {
      const alloc = planerAllocated(m.id);
      const diff = alloc - m.totalHits;
      let cls = 'cp-ok', txt = `${alloc}/${m.totalHits}`;
      if (diff > 0) { cls = 'cp-over'; txt = `${alloc}/${m.totalHits} +${diff}`; }
      else if (diff < 0) { cls = 'cp-partial'; txt = `${alloc}/${m.totalHits} ${diff}`; }
      return `<li class="cp-member-item2" data-mid="${m.id}">
        <span class="cp-name">${escapeHtml(m.name)}</span>
        <span class="cp-mode-badge ${m.mode === 'schnell' ? 'cp-schnell' : 'cp-normal'}">${m.mode === 'schnell' ? '⚡ Schnell' : 'Normal'}</span>
        <input type="number" min="1" max="999" value="${m.totalHits}" data-total-for="${m.id}" title="Gesamt-Hits">
        <span class="cp-alloc ${cls}" title="Verteilt / Gesamt">${txt}</span>
        <button class="cp-del-btn" data-del-member="${m.id}" title="Entfernen">✕</button>
      </li>`;
    }).join('');

    ul.querySelectorAll('[data-total-for]').forEach(inp => {
      inp.addEventListener('change', () => {
        const m = state.planner.members.find(m => m.id === inp.dataset.totalFor);
        if (m) { m.totalHits = Math.max(1, parseInt(inp.value) || 1); planerSave(); renderPlanerMembers(); }
      });
    });

    ul.querySelectorAll('[data-del-member]').forEach(btn => {
      btn.addEventListener('click', () => {
        const id = btn.dataset.delMember;
        state.planner.members = state.planner.members.filter(m => m.id !== id);
        state.planner.sequence.filter(s => s.memberId === id).forEach(s => delete state.planner.completed[s.id]);
        state.planner.sequence = state.planner.sequence.filter(s => s.memberId !== id);
        planerSave();
        renderPlanerMembers();
        renderPlanerSequence();
        renderPlanerOutput();
      });
    });
  }

  // ---- Fenster 2 (Reihenfolge, mobil: ▲▼ statt Drag&Drop) ----
  function renderPlanerSequence() {
    planerEnsure();
    const ul = root.querySelector('#cp-planer-seq-list');
    const info = root.querySelector('#cp-planer-seq-info');
    const totalHits = state.planner.sequence.reduce((s, i) => s + i.hits, 0);
    info.textContent = `${state.planner.sequence.length} Block(s) · ${totalHits} Hit(s) total`;

    if (state.planner.sequence.length === 0) {
      ul.innerHTML = '<div class="cp-empty" style="margin:8px;">Noch keine Einträge. Füge Member in Fenster 1 hinzu.</div>';
      return;
    }

    const lastIdx = state.planner.sequence.length - 1;
    ul.innerHTML = state.planner.sequence.map((item, idx) => {
      const member = state.planner.members.find(m => m.id === item.memberId);
      const alloc = planerAllocated(item.memberId);
      const total = member ? member.totalHits : '?';
      const diff = member ? alloc - member.totalHits : 0;
      let cls = 'cp-ok';
      if (diff > 0) cls = 'cp-over'; else if (diff < 0) cls = 'cp-partial';
      return `<li class="cp-seq-item" data-seq-id="${item.id}">
        <div class="cp-reorder">
          <button class="cp-move" data-move-up="${item.id}" ${idx === 0 ? 'disabled' : ''} title="Nach oben">▲</button>
          <button class="cp-move" data-move-down="${item.id}" ${idx === lastIdx ? 'disabled' : ''} title="Nach unten">▼</button>
        </div>
        <span class="cp-name">${escapeHtml(item.name)}</span>
        <input type="number" min="1" max="999" value="${item.hits}" data-seq-hits="${item.id}" title="Hits">
        <span class="cp-alloc ${cls}" title="${alloc}/${total} gesamt">${alloc}/${total}</span>
        <button class="cp-split-btn" data-split="${item.id}" title="Block aufteilen">÷ Split</button>
        <button class="cp-del-btn" data-del-seq="${item.id}" title="Entfernen">✕</button>
      </li>`;
    }).join('');

    // Nach oben
    ul.querySelectorAll('[data-move-up]').forEach(btn => {
      btn.addEventListener('click', () => {
        const idx = state.planner.sequence.findIndex(s => s.id === btn.dataset.moveUp);
        if (idx > 0) {
          const [moved] = state.planner.sequence.splice(idx, 1);
          state.planner.sequence.splice(idx - 1, 0, moved);
          planerSave(); renderPlanerSequence(); renderPlanerOutput();
        }
      });
    });

    // Nach unten
    ul.querySelectorAll('[data-move-down]').forEach(btn => {
      btn.addEventListener('click', () => {
        const idx = state.planner.sequence.findIndex(s => s.id === btn.dataset.moveDown);
        if (idx >= 0 && idx < state.planner.sequence.length - 1) {
          const [moved] = state.planner.sequence.splice(idx, 1);
          state.planner.sequence.splice(idx + 1, 0, moved);
          planerSave(); renderPlanerSequence(); renderPlanerOutput();
        }
      });
    });

    // Hits ändern
    ul.querySelectorAll('[data-seq-hits]').forEach(inp => {
      inp.addEventListener('change', () => {
        const item = state.planner.sequence.find(s => s.id === inp.dataset.seqHits);
        if (item) { item.hits = Math.max(1, parseInt(inp.value) || 1); planerSave(); renderPlanerMembers(); renderPlanerSequence(); renderPlanerOutput(); }
      });
    });

    // Split
    ul.querySelectorAll('[data-split]').forEach(btn => {
      btn.addEventListener('click', () => {
        const id = btn.dataset.split;
        const idx = state.planner.sequence.findIndex(s => s.id === id);
        if (idx < 0) return;
        const item = state.planner.sequence[idx];
        if (item.hits < 2) { showToast('Mindestens 2 Hits nötig zum Aufteilen.'); return; }
        const h1 = Math.ceil(item.hits / 2), h2 = item.hits - h1;
        item.hits = h1;
        state.planner.sequence.splice(idx + 1, 0, { id: newId(), memberId: item.memberId, name: item.name, hits: h2 });
        planerSave(); renderPlanerMembers(); renderPlanerSequence(); renderPlanerOutput();
      });
    });

    // Block löschen
    ul.querySelectorAll('[data-del-seq]').forEach(btn => {
      btn.addEventListener('click', () => {
        const id = btn.dataset.delSeq;
        state.planner.sequence = state.planner.sequence.filter(s => s.id !== id);
        delete state.planner.completed[id];
        planerSave(); renderPlanerMembers(); renderPlanerSequence(); renderPlanerOutput();
      });
    });
  }

  // ---- Fenster 3 ----
  function renderPlanerOutput() {
    planerEnsure();
    const out = root.querySelector('#cp-planer-output');
    const p = state.planner;

    if (p.sequence.length === 0) {
      out.innerHTML = '<div class="cp-empty" style="margin:0;">Noch keine Chain-Sequenz vorhanden.</div>';
      return;
    }

    let ctr = 1;
    const lines = p.sequence.map(item => {
      const from = ctr, to = ctr + item.hits - 1; ctr = to + 1;
      return { id: item.id, name: item.name, from, to, hits: item.hits, done: !!p.completed[item.id] };
    });

    const dateStr = p.date
      ? 'Chain ' + new Date(p.date + 'T00:00:00').toLocaleDateString('de-CH', { day: '2-digit', month: '2-digit', year: 'numeric' })
      : 'Chain (kein Datum)';

    out.innerHTML = `<div class="cp-output-date">${escapeHtml(dateStr)}</div>` +
      lines.map(line => {
        const range = line.hits === 1 ? `${line.from}` : `${line.from}-${line.to}`;
        return `<div class="cp-output-line ${line.done ? 'cp-done' : ''}">
          <label>
            <input type="checkbox" ${line.done ? 'checked' : ''} data-out-cb="${line.id}">
            <span class="cp-text">${escapeHtml(line.name + ': ' + range)}${line.done ? ' ✅' : ''}</span>
          </label>
        </div>`;
      }).join('');

    out.querySelectorAll('[data-out-cb]').forEach(cb => {
      cb.addEventListener('change', () => {
        if (cb.checked) p.completed[cb.dataset.outCb] = true; else delete p.completed[cb.dataset.outCb];
        planerSave(); renderPlanerOutput();
      });
    });
  }

  // ---- Member zu Planer hinzufügen (Fenster 1) ----
  root.querySelector('#cp-btn-planer-add').addEventListener('click', () => {
    planerEnsure();
    const sel = root.querySelector('#cp-planer-member-select');
    const hInp = root.querySelector('#cp-planer-hits-input');
    const name = sel.value.trim();
    const hits = Math.max(1, parseInt(hInp.value) || 1);
    if (!name) { showToast('Bitte einen Member auswählen.'); return; }
    if (state.planner.members.some(m => m.name === name)) { showToast('Bereits in der Liste.'); return; }

    const member = { id: newId(), name, totalHits: hits, mode: planerMode };
    state.planner.members.push(member);

    const seqItem = { id: newId(), memberId: member.id, name, hits };
    if (planerMode === 'schnell') {
      const firstActive = state.planner.sequence.findIndex(s => !state.planner.completed[s.id]);
      state.planner.sequence.splice(firstActive < 0 ? 0 : firstActive, 0, seqItem);
    } else {
      state.planner.sequence.push(seqItem);
    }

    planerSave();
    renderPlanerMembers();
    renderPlanerSequence();
    renderPlanerOutput();
  });

  // Modus-Umschalter
  root.querySelectorAll('.cp-pm-btn').forEach(btn => {
    btn.addEventListener('click', () => {
      root.querySelectorAll('.cp-pm-btn').forEach(b => b.classList.remove('cp-active'));
      btn.classList.add('cp-active');
      planerMode = btn.dataset.mode;
    });
  });

  // Datum
  root.querySelector('#cp-planer-date').addEventListener('change', e => {
    planerEnsure(); state.planner.date = e.target.value; planerSave(); renderPlanerOutput();
  });

  // Kopieren
  root.querySelector('#cp-btn-planer-copy').addEventListener('click', () => {
    const p = state.planner;
    const dateStr = p.date
      ? 'Chain ' + new Date(p.date + 'T00:00:00').toLocaleDateString('de-CH', { day: '2-digit', month: '2-digit', year: 'numeric' })
      : 'Chain';
    let ctr = 1;
    const rows = [dateStr, ...p.sequence.map(item => {
      const from = ctr, to = ctr + item.hits - 1; ctr = to + 1;
      const range = item.hits === 1 ? `${from}` : `${from}-${to}`;
      return `${item.name}: ${range}${p.completed[item.id] ? ' ✅' : ''}`;
    })];
    const text = rows.join('\n');
    if (navigator.clipboard && navigator.clipboard.writeText) {
      navigator.clipboard.writeText(text)
        .then(() => showToast('In Zwischenablage kopiert.'))
        .catch(() => fallbackCopy(text));
    } else {
      fallbackCopy(text);
    }
  });
  function fallbackCopy(text) {
    const ta = document.createElement('textarea');
    ta.value = text;
    ta.style.position = 'fixed';
    ta.style.opacity = '0';
    document.body.appendChild(ta);
    ta.select();
    try {
      document.execCommand('copy');
      showToast('In Zwischenablage kopiert.');
    } catch (e) {
      showToast('Kopieren fehlgeschlagen.');
    }
    document.body.removeChild(ta);
  }

  // Reset
  root.querySelector('#cp-btn-planer-reset').addEventListener('click', () => {
    if (!confirm('Den ganzen Planer zurücksetzen?')) return;
    state.planner = { date: '', members: [], sequence: [], completed: {} };
    root.querySelector('#cp-planer-date').value = '';
    planerSave();
    renderPlanerMembers(); renderPlanerSequence(); renderPlanerOutput();
  });

  /* ============================================================
     INIT
  ============================================================ */
  function init() {
    planerEnsure();
    if (state.planner.date) root.querySelector('#cp-planer-date').value = state.planner.date;
    renderMemberList();
    renderPlanerMembers();
    renderPlanerSequence();
    renderPlanerOutput();
  }
  init();

})();