GCTCG Helper

Gamescom EPIX Trading Cards Helper

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey, το Greasemonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

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

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Userscripts για να εγκαταστήσετε αυτόν τον κώδικα.

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

Θα χρειαστεί να εγκαταστήσετε μια επέκταση διαχείρισης κώδικα χρήστη για να εγκαταστήσετε αυτόν τον κώδικα.

(Έχω ήδη έναν διαχειριστή κώδικα χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Έχω ήδη έναν διαχειριστή στυλ χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

// ==UserScript==
// @name         GCTCG Helper
// @namespace    https://github.com/MixingFlow/GCTCG-Helper
// @version      1.0.1
// @description  Gamescom EPIX Trading Cards Helper
// @author       MixingFlow
// @match        *://*.gamescom.global/*
// @match        *://gamescom.global/*
// @icon         https://www.gamescom.global/static/meta/favicon.ico
// @license      MIT
// @grant        GM_setClipboard
// @run-at       document-idle
// ==/UserScript==

(function () {
  'use strict';

  const DB_URL = 'https://raw.githubusercontent.com/MixingFlow/GCTCG-Helper/main/cards/2026.json';
  const IMG = 'https://eu-central-1-gamescom.graphassets.com/AMwDHZTUSMaIlRlMFLL7Qz/quality=value:95/resize=w:320,h:494,fit:crop/sharpen=amount:1/auto_image/';
  const $ = (s, c = document) => c.querySelector(s);
  const $$ = (s, c = document) => [...c.querySelectorAll(s)];
  const de = () => document.documentElement.lang === 'de' || location.pathname.startsWith('/de');
  const isCardsPage = () => location.pathname.includes('/epix/cards');
  const log = (...a) => console.log('%c[GCTCG]', 'background:#7c3aed;color:#fff;padding:2px 5px;border-radius:3px;font-weight:bold', ...a);

  let DB = [], byHash = new Map(), byName = new Map(), lastSig = '', observer, timer;

  function placeholderSvg(num, lbl) {
    return 'data:image/svg+xml,' + encodeURIComponent([
      '<svg xmlns="http://www.w3.org/2000/svg" width="320" height="494">',
      '<defs><linearGradient id="b" x2="100%" y2="100%">',
      '<stop stop-color="#24123a"/><stop offset="100%" stop-color="#12071f"/>',
      '</linearGradient></defs>',
      '<rect width="320" height="494" rx="8" fill="url(#b)"/>',
      '<rect x="12" y="12" width="296" height="470" rx="6" fill="none" stroke="#6b21a8" stroke-width="2" stroke-dasharray="8,6"/>',
      '<circle cx="160" cy="205" r="34" fill="#3b0764" stroke="#7e22ce" stroke-width="2"/>',
      '<text x="160" y="217" fill="#d8b4fe" font-family="system-ui" font-size="32" font-weight="bold" text-anchor="middle">?</text>',
      `<text x="160" y="270" fill="#faf5ff" font-family="system-ui" font-size="17" font-weight="bold" text-anchor="middle" letter-spacing="1.5">${lbl.toUpperCase()}</text>`,
      `<text x="160" y="300" fill="#c084fc" font-family="system-ui" font-size="15" font-weight="600" text-anchor="middle">${num}</text>`,
      '</svg>'
    ].join(''));
  }

  function prepCard(el, src, alt, clickTarget) {
    el.setAttribute('data-gctcg', '');
    const img = $$('img', el);
    if (img.length) {
      const m = img.find(i => i.classList.contains('image--img')) || img[0];
      ['srcset', 'sizes', 'data-nimg'].forEach(a => m.removeAttribute(a));
      Object.assign(m.style, { filter: '', opacity: '1', visibility: 'visible', color: 'transparent' });
      m.className = 'image--img is--loaded';
      m.src = src;
      m.alt = alt || '';
      img.forEach(i => i !== m && i.remove());
    }
    const vid = $('video', el);
    if (vid) {
      vid.muted = vid.playsInline = vid.loop = vid.autoplay = true;
      setTimeout(() => vid.play().catch(() => {}), 50);
    }
    if (clickTarget) el.onclick = e => { e.preventDefault(); e.stopPropagation(); clickTarget.click(); };
    return el;
  }

  function section(id, title, pillCls, badge, cards = [], extra = '') {
    const s = document.createElement('section');
    s.id = id;
    s.setAttribute('data-gctcg-section', '');
    s.innerHTML = `
      <div class="gctcg-hdr d-flex justify-content-between align-items-center flex-wrap gap-2">
        <h2 class="gctcg-title d-flex align-items-center gap-2">
          ${title} <span class="gctcg-pill ${pillCls}">${badge}</span>
        </h2>
        ${extra}
      </div>`;
    if (cards.length) {
      const g = document.createElement('div');
      g.className = 'row gx-3 gy-4 g-sm-4';
      cards.forEach(c => g.appendChild(c));
      s.appendChild(g);
    }
    return s;
  }

  function render() {
    if (!isCardsPage()) { lastSig = ''; return; }
    if (!DB.length) return;

    const cards = $$('.card-list--list-item:not([data-gctcg-section] *)');
    if (!cards.length) return;

    const container = cards[0].parentElement;
    const native = [...container.children].filter(e => !e.hasAttribute('data-gctcg'));
    if (!native.length) return;

    if (!$('#gctcg-styles')) document.head.insertAdjacentHTML('beforeend', `<style id="gctcg-styles">${CSS}</style>`);

    // Scan owned cards
    const owned = new Map();
    native.forEach((el, i) => {
      if ($('.card-tile--is-locked', el)) return;
      const img = $('img', el);
      if (!img) return;
      const alt = (img.alt || '').trim(), src = img.src || '', hash = src.split('/').pop().split('?')[0];
      const count = ($('.card-list--list-item-count', el) || el).textContent.match(/x\s*(\d+)/i);
      const db = (hash && byHash.get(hash)) || (alt && byName.get(alt.toLowerCase()));
      owned.set(db?.id || hash || alt || `unknown-${i}`, {
        dbCard: db || { num: '??', id: 0, name: alt || 'Unknown', imageId: hash },
        count: count ? +count[1] : 1, el, alt, src, hash
      });
    });

    const sig = [...owned].map(([k, v]) => `${k}:${v.count}`).sort().join();
    if (!owned.size || (sig === lastSig && $('#gctcg-dup'))) return;
    lastSig = sig;

    // Compute dups & missing
    const dups = [], miss = DB.filter(c => !owned.has(c.id));
    let ext = 0;
    owned.forEach(v => { if (v.count > 1) { dups.push(v); ext += v.count - 1; } });
    dups.sort((a, b) => (a.dbCard.id || 0) - (b.dbCard.id || 0));

    // Rebuild sections
    observer?.disconnect();
    $$('[data-gctcg-section]').forEach(e => e.remove());
    const g = de();

    const dupEls = dups.map(d => prepCard(d.el.cloneNode(true), d.hash ? IMG + d.hash : d.src, d.alt, $('[role="button"],button,a,img', d.el) || d.el));
    const missEls = miss.map(c => {
      const el = cards[0].cloneNode(true);
      el.setAttribute('data-gctcg-miss', '');
      $$('.card-tile--is-advertised, [class*="list-item-count"]', el).forEach(n => n.remove());
      const unk = !c.imageId || c.name.startsWith('Card ');
      const lbl = g ? 'Unveröffentlicht' : 'Unreleased';
      el.title = `${c.num}: ${unk ? lbl : c.name}`;
      return prepCard(el, c.imageId ? IMG + c.imageId : placeholderSvg(c.num, lbl), unk ? lbl : c.name);
    });

    const copyBtn = `<button id="gctcg-copy" class="gctcg-copy">${g ? 'Tauschliste kopieren' : 'Copy Trade List'}</button>`;
    container.before(
      section('gctcg-dup', g ? 'Doppelte Karten' : 'Duplicate Cards', 'gctcg-pill-g', `${dups.length} (+${ext})`, dupEls, copyBtn),
      section('gctcg-miss', g ? 'Fehlende Karten' : 'Missing Cards', 'gctcg-pill-r', `${miss.length}/${DB.length}`, missEls),
      section('gctcg-own', g ? 'Alle Karten' : 'All Cards', 'gctcg-pill-p', `${owned.size}`)
    );

    // Copy handler
    $('#gctcg-copy')?.addEventListener('click', () => {
      const want = miss.filter(m => m.imageId && !m.name.startsWith('Card ')).map(m => m.num.split('-')[1]);
      const have = dups.map(d => `${d.dbCard.num} - ${d.dbCard.name}`).join('\n') || (g ? 'Keine' : 'None');
      const t = `__**${g ? 'BIETE' : 'HAVE'}**__\n\n${have}\n\n__**${g ? 'BRAUCHE' : 'WANT'}**__ (2026 ${g ? 'Karten' : 'cards'})\n\n${want.join(', ') || (g ? 'Keine' : 'None')}`;
      if (window.GM_setClipboard) GM_setClipboard(t); else navigator.clipboard?.writeText(t);
      document.body.insertAdjacentHTML('beforeend', `<div class="gctcg-toast">${g ? 'Kopiert!' : 'Copied!'}</div>`);
      setTimeout(() => $('.gctcg-toast')?.remove(), 2500);
    });

    log(`${owned.size} owned, ${dups.length} dups, ${miss.length} missing`);
    observer?.observe(document.body, { childList: true, subtree: true });
  }

  const CSS = `
    [data-gctcg-section] { margin: 2rem 0 }
    .gctcg-hdr { border-bottom: 2px solid rgba(255,255,255,.15); padding-bottom: .75rem; margin-bottom: 1.25rem }
    .gctcg-title { font-size: 1.5rem; font-weight: 700; color: #fff; margin: 0 }
    .gctcg-pill { font-size: .8rem; font-weight: 600; padding: .2rem .65rem; border-radius: 999px; border: 1px solid }
    .gctcg-pill-g { background: rgba(16,185,129,.2); border-color: rgba(16,185,129,.4); color: #34d399 }
    .gctcg-pill-r { background: rgba(239,68,68,.2); border-color: rgba(239,68,68,.4); color: #f87171 }
    .gctcg-pill-p { background: rgba(168,85,247,.2); border-color: rgba(168,85,247,.4); color: #c084fc }
    [data-gctcg-miss] img { filter: grayscale(1) brightness(.85) contrast(1.05) !important; opacity: 1 !important; transition: filter .25s }
    [data-gctcg-miss]:hover img { filter: grayscale(.25) brightness(.98) !important }
    [data-gctcg-miss] video { filter: grayscale(1) brightness(.8) !important; opacity: 1 !important; transition: filter .25s }
    [data-gctcg-miss]:hover video { filter: none !important }
    .gctcg-copy { cursor: pointer; font-size: .85rem; font-weight: 600; padding: .35rem .8rem; border-radius: 6px; background: #7c3aed; border: 1px solid #8b5cf6; color: #fff }
    .gctcg-toast { position: fixed; bottom: 24px; right: 24px; background: #0f172a; border: 1px solid #10b981; color: #fff; padding: 10px 18px; border-radius: 8px; z-index: 999999 }`;

  const trigger = () => { clearTimeout(timer); timer = setTimeout(render, 250); };
  observer = new MutationObserver(m => m.some(x => x.addedNodes.length && !x.target.closest?.('[data-gctcg-section]')) && trigger());
  observer.observe(document.body, { childList: true, subtree: true });

  // Hook Next.js SPA navigation
  ['pushState', 'replaceState'].forEach(method => {
    const original = history[method];
    history[method] = function (...args) {
      const result = original.apply(this, args);
      trigger();
      return result;
    };
  });
  window.addEventListener('popstate', trigger);

  fetch(DB_URL).then(r => r.json()).then(res => {
    DB = res.map((r, i) => {
      const [id, name, hash] = Array.isArray(r) ? r : [r.id, r.name, r.imageId];
      const num = `26-${String(id || i + 1).padStart(2, '0')}`;
      return { id: id || i + 1, name: name || `Card ${num}`, imageId: hash || '', num };
    });
    DB.forEach(c => { if (c.imageId) byHash.set(c.imageId, c); if (c.name) byName.set(c.name.toLowerCase(), c); });
    log(`Loaded ${DB.length} cards`);
    trigger();
  }).catch(e => log('DB load failed', e));
})();