[GC] - Scarab 21 Deck Tracker + Keyboard Controls

Tracks cards, adds keyboard shortcuts and logs scores for Scarab 21

// ==UserScript==
// @name         [GC] - Scarab 21 Deck Tracker + Keyboard Controls
// @namespace    http://tampermonkey.net/
// @version      1.4
// @description  Tracks cards, adds keyboard shortcuts and logs scores for Scarab 21
// @author       Heda
// @match        https://www.grundos.cafe/games/scarab21/
// @license      MIT
// @grant        GM_getValue
// @grant        GM_setValue
// ==/UserScript==

(function () {
  'use strict';

  const VALUES = [...Array(13)].map((_, i) => i + 2),
        SUITS = ['spades', 'hearts', 'diamonds', 'clubs'],
        SUIT_SYMBOLS = { spades: '♠', hearts: '♥', diamonds: '♦', clubs: '♣' },
        SUIT_NAMES = { spades: 'Spades', hearts: 'Hearts', diamonds: 'Diamonds', clubs: 'Clubs' },
        FACE_NAMES = { 11: 'J', 12: 'Q', 13: 'K', 14: 'A' },
        BASE = 'https://grundoscafe.b-cdn.net/games/cards',
        TEN_VALS = [10,11,12,13],
        keyList = [...'1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()-=+[]{};:\",.<>/?`~\\|'];

  let groupBySuit = true,
      defaultKeys = ['1','2','3','4','5'],
      keyBindings = GM_getValue('scarab21_keys', defaultKeys),
      seenCards = new Set(),
      canLogScore = true,
      scale = parseFloat(GM_getValue('ui_scale', 1)).toFixed(2),
      manualMode = false;

  const displayValue = v => FACE_NAMES[v] || v;

  const createRecentGamesFieldset = () => {
    const games = GM_getValue('recentGames', []);
    return `
    <fieldset id="recentGamesBox" style="border:2px solid #fa0;padding:10px;margin:10px;border-radius:6px;min-width:160px;max-height:calc(100% - 20px);overflow:auto;">
      <legend style="color:#fa0;font-weight:bold;">Score Log (Last 50)</legend>
      <div id="recentGamesList" style="font-size:14px;">
        ${games.map(s => `<div>${s}</div>`).join('')}
      </div>
    </fieldset>`;
  };

  function addRecentScore(score) {
    let games = GM_getValue('recentGames', []);
    games.push(score);
    if (games.length > 50) games = [];
    GM_setValue('recentGames', games);
    const list = document.getElementById('recentGamesList');
    if (list) list.innerHTML = games.map(s => `<div>${s}</div>`).join('');
  }

  const win = document.createElement('div');
  win.id = 'scarabUI';
  win.style.cssText = `
    position:fixed;top:50px;left:50px;max-height:95vh;
    background:#2a3439;border:2px solid #fff;border-radius:8px;
    overflow:hidden;z-index:9999;color:#eee;font-family:sans-serif;
    display:flex;flex-direction:column;transform:scale(${scale});transform-origin:top left;
  `;

  win.innerHTML = `

    <div id="drag_header" style="width:100%;cursor:move;background:#fff;color:#000;padding:4px 8px;font-weight:bold;border-radius:6px 6px 0 0;">Scarab 21 Deck Tracker</div>
    <div style="padding:5px 10px;background:#333;color:#fff;font-size:14px;">
      Scale: <input type="range" id="scaleSlider" min="0.6" max="1.6" step="0.05" value="${scale}" style="width:150px;vertical-align:middle;">
      <span id="scaleVal">${scale}</span>
      <label style="margin-left:20px;"><input type="checkbox" id="anchorToggle"> Anchor Below Board</label>
      <label style="margin-left:20px;"><input type="checkbox" id="manualToggle"> Manually Add Seen Cards</label>
    </div>
    <div style="display:flex;flex:1;overflow:hidden;">
      <div style="flex-grow:1;min-width:700px;overflow:auto;">
        <fieldset style="border:2px solid #33c4ff;padding:10px;margin:10px;border-radius:6px;">
          <legend style="color:#33c4ff;font-weight:bold;">Keyboard Controls</legend>
          <div style="display:flex;justify-content:space-between;gap:8px;">
            ${[0,1,2,3,4].map(i => `
              <label style="display:flex;flex-direction:column;font-size:14px;">
                Col ${i+1}:
                <select id="keyCol${i}" style="padding:4px;min-width:60px;">
                  ${keyList.map(k => `<option value="${k}"${keyBindings[i]===k?' selected':''}>${k}</option>`).join('')}
                </select>
              </label>`).join('')}
          </div>
          <div style="margin-top:10px;text-align:center;">
            <button id="saveKeys" style="padding:8px 16px;background:#007bff;color:#fff;border:none;border-radius:4px;width:100%;font-weight:bold;">Save Keys</button>
          </div>
        </fieldset>
        <fieldset style="border:2px solid #fff;padding:10px 14px;margin:10px;border-radius:6px;">
          <div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;">
            <div><strong>Total in Deck:</strong> <span id="deckCount">52</span></div>
            <div><strong>10-Value Cards:</strong> <span id="tenCardInfo">--</span></div>
            <label style="margin-top:6px;font-size:14px;">
              <input type="checkbox" id="toggleView"${groupBySuit?' checked':''}> Group by Suit
            </label>
          </div>
        </fieldset>
        <div id="cardContainer" style="padding-bottom:10px;"></div>
      </div>
      ${createRecentGamesFieldset()}
    </div>
  `;

  const savedPos = GM_getValue('windowPosition');
  if (savedPos?.top && savedPos?.left) {
    win.style.top = `${savedPos.top}px`;
    win.style.left = `${savedPos.left}px`;
  }
  document.body.appendChild(win);

  document.getElementById('scaleSlider').addEventListener('input', e => {
    const val = parseFloat(e.target.value).toFixed(2);
    document.getElementById('scarabUI').style.transform = `scale(${val})`;
    document.getElementById('scaleVal').textContent = val;
    GM_setValue('ui_scale', val);
  });

  document.getElementById('manualToggle').addEventListener('change', e => {
    manualMode = e.target.checked;
  });

  document.getElementById('toggleView').addEventListener('change', e => {
    groupBySuit = e.target.checked;
    updateUI();
  });
let isAnchored = GM_getValue('scarab21_anchor', false);
      document.getElementById('anchorToggle').checked = isAnchored;
const anchorToggle = document.getElementById('anchorToggle');
anchorToggle.addEventListener('change', () => {
  isAnchored = anchorToggle.checked;
  GM_setValue('scarab21_anchor', isAnchored);
  if (isAnchored) {
    anchorWindow();
  } else {
    win.style.position = 'fixed';
  }
});

function anchorWindow() {
  const outerTable = document.querySelector('table.board');
  if (!outerTable) return;

  const rect = outerTable.getBoundingClientRect();
  win.style.position = 'absolute';
  win.style.left = `${rect.left + window.scrollX}px`;
  win.style.top = `${rect.bottom + window.scrollY + 10}px`;
}

window.addEventListener('scroll', () => isAnchored && anchorWindow());
window.addEventListener('resize', () => isAnchored && anchorWindow());

  document.getElementById('saveKeys').addEventListener('click', () => {
    keyBindings = [0,1,2,3,4].map(i => document.getElementById(`keyCol${i}`).value);
    GM_setValue('scarab21_keys', keyBindings);
    alert('Key bindings saved!');
  });

  document.addEventListener('keydown', e => {
    const i = keyBindings.indexOf(e.key);
    if (i !== -1) document.querySelector(`a[onclick="play_card(${i})"]`)?.click();
  });

  const header = document.getElementById('drag_header');
  let isDragging = false, startX, startY;
 header.addEventListener('mousedown', e => {
  if (isAnchored) return;
  isDragging = true;
  startX = e.clientX - win.offsetLeft;
  startY = e.clientY - win.offsetTop;
  document.addEventListener('mousemove', drag);
  document.addEventListener('mouseup', stopDrag);
});

 const drag = e => {
  if (!isDragging || isAnchored) return;
  win.style.left = `${e.clientX - startX}px`;
  win.style.top = `${e.clientY - startY}px`;
};

  const stopDrag = () => {
    isDragging = false;
    document.removeEventListener('mousemove', drag);
    document.removeEventListener('mouseup', stopDrag);
    GM_setValue('windowPosition', { top: win.offsetTop, left: win.offsetLeft });
  };

  function updateUI() {
    const remaining = Object.fromEntries(SUITS.flatMap(s => VALUES.map(v => [`${v}_${s}`, 1])));
    seenCards.forEach(k => { if (remaining[k]) remaining[k] = 0; });

    let total = 0, tenCount = 0;
    const suitCounts = Object.fromEntries(SUITS.map(s => [s, 0])), valueCounts = Object.fromEntries(VALUES.map(v => [v, 0]));

    for (let k in remaining) {
      if (remaining[k]) {
        total++;
        const [val, suit] = k.split('_');
        suitCounts[suit]++;
        valueCounts[val]++;
        if (TEN_VALS.includes(+val)) tenCount++;
      }
    }

    document.getElementById('deckCount').textContent = total;
    document.getElementById('tenCardInfo').textContent = `${tenCount} cards (${total ? ((tenCount/total)*100).toFixed(1) : '0.0'}%)`;

    const container = document.getElementById('cardContainer');
    container.innerHTML = '';

    const appendCard = (k, gray) => {
      const div = document.createElement('div');
      div.style = `width:60px;text-align:center;margin:4px;cursor:${manualMode ? 'pointer' : 'default'};`;
      div.onclick = () => {
        if (manualMode && !seenCards.has(k)) {
          seenCards.add(k);
          updateUI();
        }
      };
      div.innerHTML = `<div style="width:48px;height:70px;padding:0;margin:0;border:${gray ? '3px solid red' : 'none'};box-sizing:border-box;">
        <img src="${BASE}/${k}.gif" style="width:100%;height:100%;display:block;${gray ? 'filter:grayscale(100%) opacity(0.5);' : 'filter:none;opacity:1;'}">
      </div>`;
      return div;
    };

    if (groupBySuit) {
      SUITS.forEach(s => {
        const count = suitCounts[s], pct = total ? ((count/total)*100).toFixed(1) : '0.0';
        const field = document.createElement('fieldset');
        field.style = 'border:2px solid #888;padding:6px 10px;margin:6px 10px;border-radius:6px;';
        field.innerHTML = `<legend style="color:#fff;font-weight:bold;">${SUIT_SYMBOLS[s]} ${SUIT_NAMES[s]} [${count} cards, ${pct}%]</legend>`;
        const wrap = document.createElement('div');
        wrap.style = 'display:grid;grid-template-columns:repeat(auto-fill, 60px);gap:6px;';
        VALUES.forEach(v => {
          const k = `${v}_${s}`;
          wrap.appendChild(appendCard(k, !remaining[k]));
        });
        field.appendChild(wrap);
        container.appendChild(field);
      });
    } else {
      const grid = document.createElement('div');
      grid.style = 'display:grid;grid-template-columns:1fr 1fr;gap:10px;';
      VALUES.forEach(v => {
        const count = valueCounts[v], pct = total ? ((count/total)*100).toFixed(1) : '0.0';
        const field = document.createElement('fieldset');
        field.style = 'border:2px solid #888;padding:6px 10px;border-radius:6px;';
        field.innerHTML = `<legend style="color:#fff;font-weight:bold;">${displayValue(v)} [${count} cards, ${pct}%]</legend>`;
        const wrap = document.createElement('div');
        wrap.style = 'display:flex;flex-wrap:wrap;';
        SUITS.forEach(s => {
          const k = `${v}_${s}`;
          wrap.appendChild(appendCard(k, !remaining[k]));
        });
        field.appendChild(wrap);
        grid.appendChild(field);
      });
      container.appendChild(grid);
    }
  }

  const cache = document.createElement('div');
  cache.style.cssText = 'position:absolute;width:0;height:0;overflow:hidden;visibility:hidden;';
  for (let v of VALUES) {
    for (let s of SUITS) {
      const img = new Image();
      img.src = `${BASE}/${v}_${s}.gif`;
      cache.appendChild(img);
    }
  }
  document.body.appendChild(cache);

  const originalPlayCard = unsafeWindow.play_card;
  unsafeWindow.play_card = function (n) {
    setTimeout(() => {
      const card = document.querySelector('td[align="center"] > img.card');
      const key = card?.src.match(/\/(\d+)_([a-z]+)\.gif/i);
      if (key) {
        seenCards.add(`${key[1]}_${key[2]}`);
        updateUI();
      }
    }, 100);
    return originalPlayCard(n);
  };

setInterval(() => {
  const html = document.body.innerHTML;
  if (html.includes('onclick="collect_winnings()"')) canLogScore = true;

  if (canLogScore && html.includes("Congratulations!!!")) {
    const match = html.match(/<p>You have scored <strong>(\d+)<\/strong>/i);
    if (match) {
      addRecentScore(Number(match[1]));
      seenCards.clear();
      updateUI();
      canLogScore = false;
    }
  }

if (html.includes("Deck Cleared!")) {
  const boardCards = document.querySelectorAll("div.cards img.card");
  const currentBoard = [];
  seenCards.clear(); // Clear first
  boardCards.forEach(img => {
    const match = img.src.match(/\/(\d+)_([a-z]+)\.gif/i);
    if (match) {
      const cardKey = `${match[1]}_${match[2]}`;
      seenCards.add(cardKey);
      currentBoard.push(cardKey);
    }
  });
  updateUI();
}
  }, 150);
    if (isAnchored) {
  anchorWindow();
}
  updateUI();
})();