MediaWiki:Common.js: Difference between revisions

From TimeRO Wiki
Jump to navigation Jump to search
No edit summary
No edit summary
Line 997: Line 997:
init(document);
init(document);
});
});
/* =========================================================
  TIMERO WIKI CARD DATABASE
  Safe MediaWiki version: JS creates inputs/buttons instead of
  placing raw <input>/<button> inside wiki pages.
========================================================= */
(function () {
  'use strict';


  const CARD_DB_SELECTOR = '#timero-card-db-app';
  const state = {
    api: '',
    cards: [],
    groups: [],
    q: '',
    category: 'all',
    changed: false,
    sort: 'card_name',
    dir: 'asc',
    expandedCardId: null,
    loading: false,
    error: ''
  };
  const categories = [
    { id: 'all', label: 'Todas', icon: '◆', color: '#7c6aff' },
    { id: 'weapon', label: 'Armas', icon: '⚔️', color: '#ff6b7a' },
    { id: 'armor', label: 'Armaduras', icon: '🛡', color: '#00d4ff' },
    { id: 'accessory', label: 'Acessórios', icon: '💎', color: '#f9a826' },
    { id: 'headgear', label: 'Headgear', icon: '🎩', color: '#b06cff' },
    { id: 'shield', label: 'Escudos', icon: '🛡️', color: '#70b8ff' },
    { id: 'garment', label: 'Capas', icon: '🧥', color: '#60d090' },
    { id: 'shoes', label: 'Sapatos', icon: '👢', color: '#f0c840' }
  ];
  function escapeHtml(value) {
    return String(value ?? '')
      .replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;')
      .replace(/'/g, '&#039;');
  }
  function normalize(value) {
    return String(value ?? '').toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '');
  }
  function debounce(fn, delay) {
    let timer = null;
    return function () {
      const args = arguments;
      clearTimeout(timer);
      timer = setTimeout(function () {
        fn.apply(null, args);
      }, delay);
    };
  }
  function getRoot() {
    return document.querySelector(CARD_DB_SELECTOR);
  }
  function cardIconUrl(cardId) {
    return 'https://timero.com.br/images/item/icons/' + encodeURIComponent(cardId) + '.png';
  }
  function cardLink(cardId) {
    return 'https://timero.com.br/pt/item?id=' + encodeURIComponent(cardId);
  }
  function categoryMeta(category) {
    return categories.find(c => c.id === category) || categories[0];
  }
  function changedBadge(card) {
    if (String(card.changed) === '1') {
      return '<span class="tc-status tc-changed">Alterada</span>';
    }
    return '<span class="tc-status tc-original">Original</span>';
  }
  function scriptToReadable(script) {
    const raw = String(script || '').trim();
    if (!raw) return '—';
    return raw
      .replace(/bonus\s+/gi, '')
      .replace(/;/g, '; ')
      .replace(/bStr/gi, 'STR')
      .replace(/bAgi/gi, 'AGI')
      .replace(/bVit/gi, 'VIT')
      .replace(/bInt/gi, 'INT')
      .replace(/bDex/gi, 'DEX')
      .replace(/bLuk/gi, 'LUK')
      .replace(/bAllStats/gi, 'All Stats')
      .replace(/bMaxHP/gi, 'Max HP')
      .replace(/bMaxSP/gi, 'Max SP')
      .replace(/bBaseAtk/gi, 'ATK')
      .replace(/bMatk/gi, 'MATK')
      .replace(/bAspdRate/gi, 'ASPD %')
      .replace(/bAtkRate/gi, 'ATK %')
      .replace(/bMatkRate/gi, 'MATK %')
      .replace(/\s+/g, ' ')
      .trim();
  }
  function injectStyles() {
    if (document.getElementById('timero-card-db-style')) return;
    const style = document.createElement('style');
    style.id = 'timero-card-db-style';
    style.textContent = `
      #timero-card-db-app,
      #timero-card-db-app * {
        box-sizing: border-box;
      }
      #timero-card-db-app {
        --tc-bg: #050914;
        --tc-card: rgba(8,14,26,0.94);
        --tc-card-2: rgba(4,8,18,0.99);
        --tc-border: rgba(80,170,255,0.18);
        --tc-border-soft: rgba(255,255,255,0.06);
        --tc-cyan: #58d7ff;
        --tc-blue: #4a90d9;
        --tc-muted: #6070a0;
        --tc-text: #d8ecff;
        --tc-gold: #f0c840;
        --tc-green: #70d890;
        --tc-red: #ff6b7a;
        font-family: 'Segoe UI', system-ui, sans-serif;
      }
      .tc-shell {
        background:
          radial-gradient(circle at 18% 0%, rgba(74,144,217,0.10), transparent 34%),
          radial-gradient(circle at 88% 24%, rgba(92,70,180,0.08), transparent 30%),
          linear-gradient(135deg, rgba(5,10,22,0.98), rgba(2,5,14,0.99));
        border: 1px solid var(--tc-border);
        border-radius: 18px;
        padding: 24px;
        margin: 0 0 34px 0;
        box-shadow: 0 0 30px rgba(0,0,0,0.35), inset 0 0 28px rgba(80,160,255,0.025);
        color: var(--tc-text);
      }
      .tc-head {
        background: linear-gradient(90deg, rgba(74,144,217,0.14), rgba(74,144,217,0.04), transparent);
        border: 1px solid rgba(74,144,217,0.18);
        border-left: 5px solid var(--tc-blue);
        border-radius: 12px;
        padding: 16px 18px;
        margin: 0 0 18px 0;
      }
      .tc-kicker {
        color: var(--tc-cyan);
        font-weight: 900;
        letter-spacing: .10em;
        text-transform: uppercase;
        font-size: .78rem;
        margin-bottom: 8px;
      }
      .tc-title {
        color: #fff;
        font-size: clamp(1.4rem, 2.6vw, 2rem);
        font-weight: 900;
        margin: 0 0 6px 0;
        line-height: 1.1;
      }
      .tc-subtitle {
        color: rgba(180,205,230,0.72);
        font-size: .92rem;
        line-height: 1.65;
      }
      .tc-toolbar {
        background: linear-gradient(135deg, rgba(8,14,26,0.94), rgba(4,8,18,0.99));
        border: 1px solid rgba(255,255,255,0.08);
        border-radius: 14px;
        padding: 16px;
        margin: 0 0 16px 0;
      }
      .tc-topline {
        display: flex;
        gap: 12px;
        align-items: center;
        flex-wrap: wrap;
      }
      .tc-search-wrap {
        flex: 1;
        min-width: 260px;
        position: relative;
      }
      .tc-search-icon {
        position: absolute;
        left: 13px;
        top: 50%;
        transform: translateY(-50%);
        opacity: .55;
        pointer-events: none;
      }
      .tc-search {
        width: 100%;
        padding: 11px 12px 11px 40px;
        border-radius: 11px;
        background: rgba(0,0,0,0.34);
        border: 1px solid rgba(255,255,255,0.09);
        color: #e8eef8;
        font-size: .92rem;
        outline: none;
        font-family: inherit;
      }
      .tc-search:focus {
        border-color: rgba(88,215,255,0.42);
        box-shadow: 0 0 0 3px rgba(88,215,255,0.08);
      }
      .tc-toggle {
        display: inline-flex;
        align-items: center;
        gap: 9px;
        cursor: pointer;
        padding: 10px 14px;
        border-radius: 11px;
        background: rgba(255,255,255,0.04);
        border: 1px solid rgba(255,255,255,0.08);
        color: rgba(180,205,230,0.75);
        font-size: .82rem;
        font-weight: 800;
        white-space: nowrap;
        user-select: none;
      }
      .tc-toggle input {
        width: 16px;
        height: 16px;
        accent-color: var(--tc-cyan);
      }
      .tc-count {
        padding: 10px 15px;
        border-radius: 11px;
        background: rgba(74,144,217,0.10);
        border: 1px solid rgba(74,144,217,0.22);
        color: var(--tc-cyan);
        font-weight: 900;
        font-size: .82rem;
        white-space: nowrap;
      }
      .tc-tabs {
        display: flex;
        gap: 8px;
        flex-wrap: wrap;
        margin-top: 12px;
      }
      .tc-tab {
        border: 1px solid rgba(255,255,255,0.08);
        background: rgba(255,255,255,0.035);
        color: rgba(180,205,230,0.68);
        border-radius: 999px;
        padding: 8px 13px;
        cursor: pointer;
        font-family: inherit;
        font-weight: 800;
        font-size: .78rem;
      }
      .tc-tab.active {
        background: linear-gradient(135deg, rgba(74,144,217,0.22), rgba(74,144,217,0.08));
        border-color: rgba(88,215,255,0.30);
        color: var(--tc-cyan);
        box-shadow: 0 0 16px rgba(88,215,255,0.08);
      }
      .tc-table {
        border: 1px solid rgba(255,255,255,0.08);
        border-radius: 14px;
        overflow: hidden;
        background: rgba(3,6,14,0.96);
      }
      .tc-table-head {
        display: grid;
        grid-template-columns: 88px 1.35fr 150px 145px 120px;
        background: rgba(0,0,0,0.42);
        border-bottom: 1px solid rgba(255,255,255,0.06);
      }
      .tc-th {
        padding: 12px 14px;
        color: var(--tc-muted);
        font-weight: 900;
        letter-spacing: .08em;
        text-transform: uppercase;
        font-size: .68rem;
        border-right: 1px solid rgba(255,255,255,0.045);
      }
      .tc-th.sortable {
        cursor: pointer;
      }
      .tc-row {
        display: grid;
        grid-template-columns: 88px 1.35fr 150px 145px 120px;
        align-items: center;
        border-bottom: 1px solid rgba(255,255,255,0.045);
        cursor: pointer;
        transition: background .15s ease, border-color .15s ease;
      }
      .tc-row:hover {
        background: rgba(74,144,217,0.06);
      }
      .tc-cell {
        padding: 13px 14px;
        border-right: 1px solid rgba(255,255,255,0.035);
      }
      .tc-id {
        font-family: 'Courier New', monospace;
        color: var(--tc-gold);
        font-weight: 900;
      }
      .tc-name-line {
        display: flex;
        align-items: center;
        gap: 12px;
        min-width: 0;
      }
      .tc-icon {
        width: 42px;
        height: 42px;
        border-radius: 10px;
        background: rgba(0,0,0,0.28);
        border: 1px solid rgba(255,255,255,0.08);
        display: flex;
        align-items: center;
        justify-content: center;
        overflow: hidden;
        flex: 0 0 42px;
      }
      .tc-icon img {
        max-width: 36px;
        max-height: 36px;
        object-fit: contain;
        image-rendering: auto;
      }
      .tc-card-name {
        color: var(--tc-cyan);
        font-weight: 900;
        font-size: .96rem;
        line-height: 1.2;
      }
      .tc-monster-name {
        color: rgba(180,205,230,0.58);
        font-size: .76rem;
        margin-top: 3px;
      }
      .tc-pill {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        padding: 6px 10px;
        border-radius: 999px;
        background: rgba(255,255,255,0.04);
        border: 1px solid rgba(255,255,255,0.08);
        color: rgba(200,220,240,0.72);
        font-weight: 800;
        font-size: .76rem;
        white-space: nowrap;
      }
      .tc-status {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        padding: 6px 10px;
        border-radius: 999px;
        font-weight: 900;
        font-size: .72rem;
        white-space: nowrap;
      }
      .tc-changed {
        background: rgba(112,216,144,0.10);
        border: 1px solid rgba(112,216,144,0.24);
        color: var(--tc-green);
      }
      .tc-original {
        background: rgba(255,255,255,0.035);
        border: 1px solid rgba(255,255,255,0.08);
        color: rgba(180,205,230,0.55);
      }
      .tc-detail {
        display: none;
        grid-column: 1 / -1;
        padding: 0;
        background: rgba(0,0,0,0.18);
        border-bottom: 1px solid rgba(255,255,255,0.055);
      }
      .tc-row.open + .tc-detail {
        display: block;
      }
      .tc-detail-grid {
        display: grid;
        grid-template-columns: 1fr 1fr 1fr;
        gap: 12px;
        padding: 16px;
      }
      .tc-effect-box {
        border-radius: 12px;
        padding: 14px;
        background: linear-gradient(135deg, rgba(8,14,26,0.96), rgba(4,8,18,0.99));
        border: 1px solid rgba(255,255,255,0.08);
        min-height: 112px;
      }
      .tc-effect-title {
        color: var(--tc-muted);
        font-weight: 900;
        letter-spacing: .08em;
        text-transform: uppercase;
        font-size: .68rem;
        margin-bottom: 8px;
      }
      .tc-effect-text {
        color: rgba(220,235,255,0.78);
        font-size: .84rem;
        line-height: 1.65;
        white-space: pre-wrap;
      }
      .tc-effect-box.new {
        border-color: rgba(112,216,144,0.18);
      }
      .tc-effect-box.collection {
        border-color: rgba(240,200,64,0.18);
      }
      .tc-empty,
      .tc-loading,
      .tc-error {
        padding: 34px;
        text-align: center;
        color: rgba(180,205,230,0.58);
        font-weight: 700;
      }
      .tc-error {
        color: #ff8a8a;
      }
      .tc-groups {
        margin-top: 26px;
      }
      .tc-group-list {
        display: grid;
        gap: 10px;
      }
      .tc-group-card {
        border-radius: 14px;
        border: 1px solid rgba(255,255,255,0.08);
        background: linear-gradient(135deg, rgba(8,14,26,0.94), rgba(4,8,18,0.99));
        overflow: hidden;
      }
      .tc-group-head {
        padding: 15px 18px;
        display: grid;
        grid-template-columns: auto 1fr auto;
        gap: 13px;
        align-items: center;
      }
      .tc-group-icon {
        width: 42px;
        height: 42px;
        border-radius: 12px;
        background: rgba(74,144,217,0.10);
        border: 1px solid rgba(74,144,217,0.22);
        display: flex;
        align-items: center;
        justify-content: center;
        font-size: 1.25rem;
      }
      .tc-group-key {
        color: var(--tc-muted);
        font-size: .64rem;
        font-weight: 900;
        letter-spacing: .11em;
        text-transform: uppercase;
        margin-bottom: 2px;
      }
      .tc-group-name {
        color: #fff;
        font-weight: 900;
      }
      .tc-group-bonus {
        padding: 11px 18px;
        border-top: 1px solid rgba(255,255,255,0.055);
        color: rgba(220,235,255,0.74);
        line-height: 1.55;
        font-size: .84rem;
      }
      @media (max-width: 900px) {
        .tc-table-head {
          display: none;
        }
        .tc-row {
          grid-template-columns: 1fr;
          padding: 12px;
        }
        .tc-cell {
          border-right: none;
          padding: 7px 4px;
        }
        .tc-detail-grid {
          grid-template-columns: 1fr;
        }
        .tc-topline {
          align-items: stretch;
        }
        .tc-toggle,
        .tc-count {
          width: 100%;
          justify-content: center;
        }
      }
    `;
    document.head.appendChild(style);
  }
  function createShell(root) {
    root.innerHTML = `
      <div class="tc-shell">
        <div class="tc-head">
          <div class="tc-kicker">◇ Banco de Dados</div>
          <h2 class="tc-title">Todas as Cartas Balanceadas</h2>
          <div class="tc-subtitle">
            Pesquise por nome da carta, ID, monstro, slot, efeito antigo, efeito novo, bônus de coleção ou script de coleção.
          </div>
        </div>
        <div class="tc-toolbar">
          <div class="tc-topline">
            <div class="tc-search-wrap">
              <span class="tc-search-icon">🔍</span>
              <input class="tc-search" id="tc-search" type="text" placeholder="Buscar carta, ID, monstro, efeito ou coleção..." autocomplete="off">
            </div>
            <label class="tc-toggle" for="tc-changed">
              <input id="tc-changed" type="checkbox">
              ⚡ Apenas alteradas
            </label>
            <div class="tc-count" id="tc-count">— cartas</div>
          </div>
          <div class="tc-tabs" id="tc-tabs"></div>
        </div>
        <div class="tc-table">
          <div class="tc-table-head">
            <div class="tc-th sortable" data-sort="id">ID ↕</div>
            <div class="tc-th sortable" data-sort="name">Carta / Monstro ↕</div>
            <div class="tc-th sortable" data-sort="slot">Slot ↕</div>
            <div class="tc-th sortable" data-sort="category">Categoria ↕</div>
            <div class="tc-th">Status</div>
          </div>
          <div id="tc-body">
            <div class="tc-loading">Carregando cartas...</div>
          </div>
        </div>
        <div class="tc-groups">
          <div class="tc-head" style="margin-top:26px;">
            <div class="tc-kicker">◇ Grupos de Coleção</div>
            <h2 class="tc-title" style="font-size:1.35rem;">Bônus permanentes por grupo</h2>
            <div class="tc-subtitle">Dados carregados da tabela <strong>wiki_card_groups</strong>.</div>
          </div>
          <div class="tc-group-list" id="tc-groups"></div>
        </div>
      </div>
    `;
    const tabs = root.querySelector('#tc-tabs');
    categories.forEach(cat => {
      const btn = document.createElement('button');
      btn.type = 'button';
      btn.className = 'tc-tab' + (cat.id === state.category ? ' active' : '');
      btn.dataset.category = cat.id;
      btn.innerHTML = `${cat.icon} ${escapeHtml(cat.label)}`;
      btn.addEventListener('click', function () {
        state.category = cat.id;
        refresh(root);
      });
      tabs.appendChild(btn);
    });
    root.querySelector('#tc-search').addEventListener('input', debounce(function (e) {
      state.q = e.target.value.trim();
      refresh(root);
    }, 220));
    root.querySelector('#tc-changed').addEventListener('change', function (e) {
      state.changed = !!e.target.checked;
      refresh(root);
    });
    root.querySelectorAll('.tc-th.sortable').forEach(th => {
      th.addEventListener('click', function () {
        const sort = th.dataset.sort;
        if (state.sort === sort) {
          state.dir = state.dir === 'asc' ? 'desc' : 'asc';
        } else {
          state.sort = sort;
          state.dir = 'asc';
        }
        refresh(root);
      });
    });
  }
  async function fetchCards(root) {
    state.loading = true;
    state.error = '';
    renderBody(root);
    const params = new URLSearchParams();
    params.set('q', state.q);
    params.set('category', state.category);
    params.set('changed', state.changed ? '1' : '0');
    params.set('sort', state.sort);
    params.set('dir', state.dir);
    try {
      const response = await fetch(state.api + '?' + params.toString(), {
        credentials: 'same-origin',
        cache: 'no-store'
      });
      const text = await response.text();
      let data;
      try {
        data = JSON.parse(text);
      } catch (err) {
        throw new Error('Invalid JSON returned by API: ' + text.slice(0, 300));
      }
      if (!data.ok) {
        throw new Error(data.error || 'API returned an unknown error.');
      }
      state.cards = Array.isArray(data.cards) ? data.cards : [];
      state.groups = Array.isArray(data.groups) ? data.groups : [];
      state.expandedCardId = null;
    } catch (err) {
      state.cards = [];
      state.groups = [];
      state.error = err.message || String(err);
    } finally {
      state.loading = false;
      renderAll(root);
    }
  }
  function refresh(root) {
    updateTabs(root);
    fetchCards(root);
  }
  function updateTabs(root) {
    root.querySelectorAll('.tc-tab').forEach(btn => {
      btn.classList.toggle('active', btn.dataset.category === state.category);
    });
  }
  function renderAll(root) {
    renderBody(root);
    renderGroups(root);
    const count = root.querySelector('#tc-count');
    if (count) {
      count.textContent = state.cards.length + ' carta' + (state.cards.length === 1 ? '' : 's');
    }
  }
  function renderBody(root) {
    const body = root.querySelector('#tc-body');
    if (!body) return;
    if (state.loading) {
      body.innerHTML = '<div class="tc-loading">Carregando cartas...</div>';
      return;
    }
    if (state.error) {
      body.innerHTML = '<div class="tc-error">Erro ao carregar cartas: ' + escapeHtml(state.error) + '</div>';
      return;
    }
    if (!state.cards.length) {
      body.innerHTML = '<div class="tc-empty">Nenhuma carta encontrada com os filtros atuais.</div>';
      return;
    }
    body.innerHTML = '';
    state.cards.forEach(card => {
      const meta = categoryMeta(card.category);
      const row = document.createElement('div');
      row.className = 'tc-row';
      row.dataset.cardId = card.card_id;
      const monster = card.monster_name ? `<div class="tc-monster-name">${escapeHtml(card.monster_name)}</div>` : '';
      const group = card.collection_group_name || '—';
      row.innerHTML = `
        <div class="tc-cell tc-id">#${escapeHtml(card.card_id)}</div>
        <div class="tc-cell">
          <div class="tc-name-line">
            <div class="tc-icon">
              <img src="${cardIconUrl(card.card_id)}" alt="${escapeHtml(card.card_name)}" onerror="this.style.display='none';this.parentNode.textContent='🃏';">
            </div>
            <div>
              <div class="tc-card-name">
                <a href="${cardLink(card.card_id)}" target="_blank" rel="noopener" style="color:inherit;text-decoration:none;">${escapeHtml(card.card_name)}</a>
              </div>
              ${monster}
            </div>
          </div>
        </div>
        <div class="tc-cell"><span class="tc-pill">${escapeHtml(card.slot_type || 'Unknown')}</span></div>
        <div class="tc-cell"><span class="tc-pill" style="border-color:${meta.color}44;color:${meta.color};">${meta.icon} ${escapeHtml(group)}</span></div>
        <div class="tc-cell">${changedBadge(card)}</div>
      `;
      const detail = document.createElement('div');
      detail.className = 'tc-detail';
      detail.innerHTML = `
        <div class="tc-detail-grid">
          <div class="tc-effect-box">
            <div class="tc-effect-title">Efeito Antigo</div>
            <div class="tc-effect-text">${escapeHtml(card.old_effect || '—')}</div>
          </div>
          <div class="tc-effect-box new">
            <div class="tc-effect-title">Efeito Novo</div>
            <div class="tc-effect-text">${escapeHtml(card.new_effect || '—')}</div>
          </div>
          <div class="tc-effect-box collection">
            <div class="tc-effect-title">Coleção</div>
            <div class="tc-effect-text"><strong style="color:#f0c840;">Grupo:</strong> ${escapeHtml(card.collection_group_name || '—')}<br><strong style="color:#f0c840;">Pontos:</strong> ${escapeHtml(card.collection_points || 0)}<br><strong style="color:#f0c840;">Efeito:</strong> ${escapeHtml(card.collection_effect || '—')}<br><strong style="color:#f0c840;">Script:</strong> ${escapeHtml(scriptToReadable(card.collection_script))}</div>
          </div>
        </div>
      `;
      row.addEventListener('click', function () {
        const isOpen = row.classList.contains('open');
        body.querySelectorAll('.tc-row.open').forEach(r => r.classList.remove('open'));
        if (!isOpen) row.classList.add('open');
      });
      body.appendChild(row);
      body.appendChild(detail);
    });
  }
  function renderGroups(root) {
    const wrap = root.querySelector('#tc-groups');
    if (!wrap) return;
    if (state.error) {
      wrap.innerHTML = '';
      return;
    }
    if (!state.groups.length) {
      wrap.innerHTML = '<div class="tc-empty">Nenhum grupo de coleção cadastrado.</div>';
      return;
    }
    wrap.innerHTML = '';
    state.groups.forEach(group => {
      const color = group.color || '#7c6aff';
      const el = document.createElement('div');
      el.className = 'tc-group-card';
      el.style.borderColor = color + '33';
      el.innerHTML = `
        <div class="tc-group-head">
          <div class="tc-group-icon" style="border-color:${color}44;background:${color}14;">${escapeHtml(group.icon || '🃏')}</div>
          <div>
            <div class="tc-group-key" style="color:${color};">Grupo · ${escapeHtml(group.group_key)}</div>
            <div class="tc-group-name">${escapeHtml(group.group_name)}</div>
          </div>
          <div class="tc-pill" style="border-color:${color}33;color:${color};">${escapeHtml(group.card_count || 0)} cartas · Min ${escapeHtml(group.min_cards || 1)}</div>
        </div>
        <div class="tc-group-bonus">
          <strong style="color:${color};">Bônus:</strong> ${escapeHtml(group.collection_effect || '—')}
        </div>
      `;
      wrap.appendChild(el);
    });
  }
  function initCardDb() {
    const root = getRoot();
    if (!root || root.dataset.tcReady === '1') return;
    root.dataset.tcReady = '1';
    state.api = root.dataset.api || '/pt/api/wiki_cards.php';
    injectStyles();
    createShell(root);
    fetchCards(root);
  }
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', initCardDb);
  } else {
    initCardDb();
  }
  if (window.mw && mw.hook) {
    mw.hook('wikipage.content').add(initCardDb);
  }
})();
if (mw && mw.hook) {
if (mw && mw.hook) {
mw.hook('wikipage.content').add(function ($content) {
mw.hook('wikipage.content').add(function ($content) {

Revision as of 20:47, 29 April 2026

/* Any JavaScript here will be loaded for all users on every page load. */
(function ($, mw) {
	'use strict';

	/* =========================================================
	   GLOBAL BASICS
	========================================================= */

	function setLogoHref() {
		var logoLink = document.querySelector('#p-logo a');
		if (logoLink) {
			logoLink.setAttribute('href', 'https://timero.com.br/');
		}
	}

	function ensureFontAwesome() {
		if (document.querySelector('link[data-timero-fa]')) return;

		var fa = document.createElement('link');
		fa.rel = 'stylesheet';
		fa.href = 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css';
		fa.setAttribute('data-timero-fa', '1');
		document.head.appendChild(fa);
	}

	function bindActionButton(el, handler) {
		if (!el || el.dataset.timeroBound === '1') return;

		el.dataset.timeroBound = '1';
		el.setAttribute('role', 'button');
		el.setAttribute('tabindex', '0');

		el.addEventListener('click', function (e) {
			e.preventDefault();
			handler.call(this, e);
		});

		el.addEventListener('keydown', function (e) {
			if (e.key === 'Enter' || e.key === ' ') {
				e.preventDefault();
				handler.call(this, e);
			}
		});
	}

	function toNumber(value, fallback) {
		var n = parseFloat(value);
		return isNaN(n) ? fallback : n;
	}

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

	/* =========================================================
	   GUIDE PAGE SKIN SWITCH
	========================================================= */

	function applyTimeroGuideSkin() {
		var page = mw.config.get('wgPageName') || '';
		var html = document.documentElement;
		var body = document.body;
		var hasGuideRoot =
			document.getElementById('farming-guide-root') ||
			document.getElementById('mvp-guide-root') ||
			document.getElementById('leveling-guide-root') ||
			document.querySelector('.timero-wiki-root');

		if (!html || !body) return;

		Array.prototype.slice.call(html.classList).forEach(function (cls) {
			if (cls === 'timero-guide-skin' || cls.indexOf('guide-page-') === 0) {
				html.classList.remove(cls);
			}
		});

		Array.prototype.slice.call(body.classList).forEach(function (cls) {
			if (cls === 'timero-guide-skin' || cls.indexOf('guide-page-') === 0) {
				body.classList.remove(cls);
			}
		});

		if (/^Guia_de_/.test(page) || hasGuideRoot) {
			var safePageClass = 'guide-page-' + String(page || 'custom-guide').replace(/[^A-Za-z0-9_-]/g, '-');
			html.classList.add('timero-guide-skin');
			body.classList.add('timero-guide-skin');
			html.classList.add(safePageClass);
			body.classList.add(safePageClass);
		}
	}

	/* =========================================================
	   ROUTES
	========================================================= */

	function resetRoutePill(pill) {
		if (!pill) return;
		pill.style.transform = '';
		pill.style.fontWeight = '800';
		pill.style.boxShadow = 'none';
		pill.style.filter = 'none';
	}

	function activateRoutePill(pill) {
		if (!pill) return;
		pill.style.transform = 'translateY(-1px)';
		pill.style.fontWeight = '900';
		pill.style.boxShadow = '0 0 0 1px rgba(255,255,255,0.05) inset, 0 0 22px rgba(255,255,255,0.05)';
		pill.style.filter = 'brightness(1.08)';
	}

	function closeAllRoutes(section) {
		if (!section) return;

		var details = section.querySelectorAll('.route-detail');
		var pills = section.querySelectorAll('.route-pill[data-route]');

		for (var i = 0; i < details.length; i++) {
			details[i].style.display = 'none';
		}

		for (var j = 0; j < pills.length; j++) {
			resetRoutePill(pills[j]);
		}

		section.setAttribute('data-open-route', '');
	}

	window.expandRoute = function (routeId) {
		var section = document.getElementById('routes-section');
		if (!section) return;

		var target = document.getElementById(routeId);
		if (!target) return;

		var currentOpen = section.getAttribute('data-open-route');

		if (currentOpen === routeId) {
			closeAllRoutes(section);
			return;
		}

		closeAllRoutes(section);

		target.style.display = 'block';
		target.style.animation = 'route-in 0.3s ease both';
		section.setAttribute('data-open-route', routeId);

		var activePill = document.getElementById('pill-' + routeId);
		activateRoutePill(activePill);

		if (typeof target.scrollIntoView === 'function') {
			setTimeout(function () {
				target.scrollIntoView({
					behavior: 'smooth',
					block: 'nearest'
				});
			}, 80);
		}
	};

	function bindRouteViewer(context) {
		var scope = context && context.querySelector ? context : document;
		var section = scope.querySelector('#routes-section') || document.getElementById('routes-section');
		if (!section) return;

		var pills = section.querySelectorAll('.route-pill[data-route]');
		for (var i = 0; i < pills.length; i++) {
			(function (pill) {
				bindActionButton(pill, function () {
					expandRoute(this.getAttribute('data-route'));
				});
			})(pills[i]);
		}

		var closeButtons = section.querySelectorAll('.route-close[data-route]');
		for (var j = 0; j < closeButtons.length; j++) {
			(function (closeBtn) {
				bindActionButton(closeBtn, function () {
					expandRoute(this.getAttribute('data-route'));
				});
			})(closeButtons[j]);
		}
	}

	/* =========================================================
	   METHODS
	========================================================= */

	function resetMethodTab(tab, type) {
		if (!tab) return;

		tab.style.background = 'transparent';
		tab.style.fontWeight = '700';

		if (type === 'grind') tab.style.color = 'rgba(249,197,0,0.60)';
		if (type === 'market') tab.style.color = 'rgba(0,212,255,0.60)';
		if (type === 'passive') tab.style.color = 'rgba(176,108,255,0.60)';
	}

	function activateMethodTab(tab, type) {
		if (!tab) return;

		tab.style.fontWeight = '900';

		if (type === 'grind') {
			tab.style.background = 'linear-gradient(135deg,rgba(249,197,0,0.18),rgba(249,197,0,0.07))';
			tab.style.color = '#f9c500';
		}

		if (type === 'market') {
			tab.style.background = 'linear-gradient(135deg,rgba(0,212,255,0.14),rgba(0,212,255,0.05))';
			tab.style.color = '#00d4ff';
		}

		if (type === 'passive') {
			tab.style.background = 'linear-gradient(135deg,rgba(176,108,255,0.14),rgba(176,108,255,0.05))';
			tab.style.color = '#b06cff';
		}
	}

	window.switchMethod = function (methodId) {
		var section = document.getElementById('methods-section');
		if (!section) return;

		var panels = section.querySelectorAll('.method-panel');
		var tabs = section.querySelectorAll('.method-tab[data-method]');

		for (var i = 0; i < panels.length; i++) {
			panels[i].style.display = 'none';
		}

		for (var j = 0; j < tabs.length; j++) {
			resetMethodTab(tabs[j], tabs[j].getAttribute('data-method'));
		}

		var activePanel = document.getElementById('method-' + methodId);
		if (activePanel) {
			activePanel.style.display = 'grid';
			activePanel.style.animation = 'method-in 0.3s ease both';
		}

		var activeTab = document.getElementById('mtab-' + methodId);
		activateMethodTab(activeTab, methodId);

		section.setAttribute('data-open-method', methodId);
	};

	function bindMethodViewer(context) {
		var scope = context && context.querySelector ? context : document;
		var section = scope.querySelector('#methods-section') || document.getElementById('methods-section');
		if (!section) return;

		var tabs = section.querySelectorAll('.method-tab[data-method]');
		for (var i = 0; i < tabs.length; i++) {
			(function (tab) {
				bindActionButton(tab, function () {
					switchMethod(this.getAttribute('data-method'));
				});
			})(tabs[i]);
		}

		if (!section.getAttribute('data-open-method')) {
			switchMethod('grind');
		} else {
			switchMethod(section.getAttribute('data-open-method'));
		}
	}

	/* =========================================================
	   CALCULATOR
	========================================================= */

	var calcState = {
		hours: 2,
		booster: false,
		rate: 70,
		label: 'Rota Mid-Game (70M/hr)',
		weeklyTarget: 12000
	};

	function formatMillions(value) {
		var rounded = Math.round(value * 10) / 10;

		if (rounded >= 1000) {
			var billions = rounded / 1000;
			var bText = (Math.round(billions * 10) / 10).toString().replace('.0', '').replace('.', ',');
			return bText + 'B z';
		}

		if (rounded >= 1) {
			return rounded.toString().replace('.0', '').replace('.', ',') + 'M z';
		}

		return Math.round(rounded * 1000) + 'k z';
	}

	function refreshCalcMethodUI(section) {
		if (!section) return;

		var labelEl = section.querySelector('#calc-method-label');
		var menuEl = section.querySelector('#calc-method-menu');
		var options = section.querySelectorAll('.calc-method-option');

		if (labelEl) {
			labelEl.textContent = calcState.label;
		}

		for (var i = 0; i < options.length; i++) {
			var opt = options[i];
			var isActive = String(opt.getAttribute('data-rate')) === String(calcState.rate);

			if (isActive) {
				opt.style.background = 'rgba(0,212,255,0.12)';
				opt.style.borderColor = 'rgba(0,212,255,0.25)';
				opt.style.color = '#00d4ff';
				opt.style.fontWeight = '800';
			} else {
				opt.style.background = 'rgba(255,255,255,0.03)';
				opt.style.borderColor = 'rgba(255,255,255,0.06)';
				opt.style.color = 'rgba(232,238,248,0.82)';
				opt.style.fontWeight = '700';
			}
		}

		if (menuEl) {
			menuEl.style.display = section.getAttribute('data-method-menu-open') === '1' ? 'block' : 'none';
		}
	}

	function syncCalcStateFromNativeSelect(section) {
		if (!section) return;

		var select = section.querySelector('#calc-method');
		if (!select) return;

		calcState.rate = toNumber(select.value, calcState.rate);

		if (select.options && select.selectedIndex >= 0) {
			calcState.label = select.options[select.selectedIndex].text;
		}
	}

	window.adjustCalc = function (field, delta) {
		if (field !== 'hours') return;

		calcState.hours = clamp(calcState.hours + delta, 0.5, 24);
		updateCalc();
	};

	window.setBooster = function (enabled) {
		calcState.booster = !!enabled;
		updateCalc();
	};

	window.setCalcMethod = function (rate, label) {
		calcState.rate = toNumber(rate, calcState.rate);
		calcState.label = label || calcState.label;

		var section = document.getElementById('calculator-section');
		if (section) {
			section.setAttribute('data-method-menu-open', '0');

			var select = section.querySelector('#calc-method');
			if (select) {
				select.value = String(calcState.rate);
			}
		}

		updateCalc();
	};

	window.toggleCalcMethodMenu = function () {
		var section = document.getElementById('calculator-section');
		if (!section) return;

		var isOpen = section.getAttribute('data-method-menu-open') === '1';
		section.setAttribute('data-method-menu-open', isOpen ? '0' : '1');
		refreshCalcMethodUI(section);
	};

	window.closeCalcMethodMenu = function () {
		var section = document.getElementById('calculator-section');
		if (!section) return;

		section.setAttribute('data-method-menu-open', '0');
		refreshCalcMethodUI(section);
	};

	window.updateCalc = function () {
		var section = document.getElementById('calculator-section');
		if (!section) return;

		syncCalcStateFromNativeSelect(section);

		var hoursEl = document.getElementById('calc-hours');
		var resultSession = document.getElementById('result-session');
		var resultDay = document.getElementById('result-day');
		var resultWeek = document.getElementById('result-week');
		var resultPct = document.getElementById('result-pct');
		var resultBar = document.getElementById('result-bar');
		var boostNo = document.getElementById('boost-no');
		var boostYes = document.getElementById('boost-yes');

		var effectiveRate = calcState.booster ? calcState.rate * 1.5 : calcState.rate;
		var session = calcState.hours * effectiveRate;
		var day = session;
		var week = day * 7;
		var pct = Math.max(0, Math.min(100, Math.round((week / calcState.weeklyTarget) * 100)));

		if (hoursEl) {
			hoursEl.textContent = calcState.hours % 1 === 0 ? String(calcState.hours) : String(calcState.hours).replace('.', ',');
		}
		if (resultSession) resultSession.textContent = formatMillions(session);
		if (resultDay) resultDay.textContent = formatMillions(day);
		if (resultWeek) resultWeek.textContent = formatMillions(week);
		if (resultPct) resultPct.textContent = pct + '%';
		if (resultBar) resultBar.style.width = pct + '%';

		if (boostNo && boostYes) {
			if (calcState.booster) {
				boostNo.style.background = 'rgba(255,255,255,0.04)';
				boostNo.style.borderColor = 'rgba(255,255,255,0.09)';
				boostNo.style.color = 'rgba(122,144,176,0.60)';
				boostNo.style.fontWeight = '700';

				boostYes.style.background = 'rgba(0,212,255,0.15)';
				boostYes.style.borderColor = 'rgba(0,212,255,0.30)';
				boostYes.style.color = '#00d4ff';
				boostYes.style.fontWeight = '800';
			} else {
				boostNo.style.background = 'rgba(255,61,90,0.15)';
				boostNo.style.borderColor = 'rgba(255,61,90,0.30)';
				boostNo.style.color = '#ff3d5a';
				boostNo.style.fontWeight = '800';

				boostYes.style.background = 'rgba(255,255,255,0.04)';
				boostYes.style.borderColor = 'rgba(255,255,255,0.09)';
				boostYes.style.color = 'rgba(122,144,176,0.60)';
				boostYes.style.fontWeight = '700';
			}
		}

		refreshCalcMethodUI(section);
	};

	function bindCalculator(context) {
		var scope = context && context.querySelector ? context : document;
		var section = scope.querySelector('#calculator-section') || document.getElementById('calculator-section');
		if (!section) return;

		var adjusters = section.querySelectorAll('[data-calc-adjust]');
		for (var i = 0; i < adjusters.length; i++) {
			(function (adjuster) {
				bindActionButton(adjuster, function (e) {
					e.stopPropagation();
					adjustCalc(this.getAttribute('data-calc-adjust'), toNumber(this.getAttribute('data-delta'), 0));
				});
			})(adjusters[i]);
		}

		var boosters = section.querySelectorAll('[data-booster]');
		for (var j = 0; j < boosters.length; j++) {
			(function (booster) {
				bindActionButton(booster, function (e) {
					e.stopPropagation();
					setBooster(this.getAttribute('data-booster') === '1');
				});
			})(boosters[j]);
		}

		var methodDisplay = section.querySelector('#calc-method-display');
		if (methodDisplay) {
			bindActionButton(methodDisplay, function (e) {
				e.stopPropagation();
				toggleCalcMethodMenu();
			});
		}

		var options = section.querySelectorAll('.calc-method-option');
		for (var k = 0; k < options.length; k++) {
			(function (option) {
				bindActionButton(option, function (e) {
					e.stopPropagation();
					setCalcMethod(this.getAttribute('data-rate'), this.getAttribute('data-label'));
				});
			})(options[k]);
		}

		var nativeSelect = section.querySelector('#calc-method');
		if (nativeSelect && nativeSelect.dataset.timeroBound !== '1') {
			nativeSelect.dataset.timeroBound = '1';
			nativeSelect.addEventListener('change', function () {
				updateCalc();
			});
		}

		if (!window.__timeroCalcDocBound) {
			window.__timeroCalcDocBound = true;

			document.addEventListener('click', function (e) {
				var liveSection = document.getElementById('calculator-section');
				if (!liveSection) return;

				if (!liveSection.contains(e.target)) {
					closeCalcMethodMenu();
				}
			});
		}

		if (!section.getAttribute('data-method-menu-open')) {
			section.setAttribute('data-method-menu-open', '0');
		}

		updateCalc();
	}

	/* =========================================================
	   LOADOUT
	========================================================= */

	function setLoadoutItemState(item, active) {
		if (!item) return;

		var check = item.querySelector('.li-check');
		if (!check) return;

		if (active) {
			item.classList.add('is-active');
			item.style.background = 'rgba(176,108,255,0.08)';
			item.style.borderColor = 'rgba(176,108,255,0.24)';
			item.style.boxShadow = '0 0 0 1px rgba(176,108,255,0.05) inset';

			check.style.background = 'rgba(176,108,255,0.16)';
			check.style.borderColor = 'rgba(176,108,255,0.45)';
			check.style.color = '#b06cff';
			check.innerHTML = '✓';
		} else {
			item.classList.remove('is-active');
			item.style.background = 'rgba(255,255,255,0.03)';
			item.style.borderColor = 'rgba(255,255,255,0.07)';
			item.style.boxShadow = 'none';

			check.style.background = 'rgba(176,108,255,0.04)';
			check.style.borderColor = 'rgba(176,108,255,0.30)';
			check.style.color = 'transparent';
			check.innerHTML = '';
		}
	}

	function updateLoadoutSummary() {
		var section = document.getElementById('loadout-section');
		if (!section) return;

		var items = section.querySelectorAll('.loadout-item');
		var total = 0;
		var count = 0;

		for (var i = 0; i < items.length; i++) {
			if (items[i].classList.contains('is-active')) {
				total += toNumber(items[i].getAttribute('data-cost'), 0);
				count += 1;
			}
		}

		var totalEl = document.getElementById('loadout-total');
		var countEl = document.getElementById('loadout-count');

		if (totalEl) totalEl.textContent = total + 'k z';
		if (countEl) countEl.textContent = String(count);
	}

	window.toggleLoadout = function (element) {
		if (!element) return;

		var active = !element.classList.contains('is-active');
		setLoadoutItemState(element, active);
		updateLoadoutSummary();
	};

	window.clearLoadout = function () {
		var section = document.getElementById('loadout-section');
		if (!section) return;

		var items = section.querySelectorAll('.loadout-item');
		for (var i = 0; i < items.length; i++) {
			setLoadoutItemState(items[i], false);
		}

		updateLoadoutSummary();
	};

	function bindLoadout(context) {
		var scope = context && context.querySelector ? context : document;
		var section = scope.querySelector('#loadout-section') || document.getElementById('loadout-section');
		if (!section) return;

		var items = section.querySelectorAll('.loadout-item');
		for (var i = 0; i < items.length; i++) {
			(function (item) {
				setLoadoutItemState(item, item.classList.contains('is-active'));

				bindActionButton(item, function () {
					toggleLoadout(this);
				});
			})(items[i]);
		}

		var clearBtn = section.querySelector('.loadout-clear');
		if (clearBtn) {
			bindActionButton(clearBtn, function () {
				clearLoadout();
			});
		}

		updateLoadoutSummary();
	}

	/* =========================================================
	   TIMERO CLICKABLE CARDS
	========================================================= */

	function bindTimeroNavCards(context) {
		var scope = context && context.querySelector ? context : document;
		var cards = scope.querySelectorAll('.timero-nav-card[data-href]');

		for (var i = 0; i < cards.length; i++) {
			(function (card) {
				if (card.dataset.timeroNavBound === '1') return;

				card.dataset.timeroNavBound = '1';
				card.setAttribute('role', 'button');
				card.setAttribute('tabindex', '0');
				card.style.cursor = 'pointer';

				card.addEventListener('click', function (e) {
					if (e.target.closest('a, button, input, select, textarea')) return;

					var href = this.getAttribute('data-href');
					var target = this.getAttribute('data-target') || '';

					if (!href) return;

					if (target === '_blank') {
						window.open(href, '_blank', 'noopener');
					} else {
						window.location.href = href;
					}
				});

				card.addEventListener('keydown', function (e) {
					if (e.key === 'Enter' || e.key === ' ') {
						e.preventDefault();
						this.click();
					}
				});
			})(cards[i]);
		}
	}

	/* =========================================================
	   MVP GUIDE
	========================================================= */

	var MVP_TIMER_STORAGE_KEY = 'timero_mvp_timers_v1';

	function readStoredMvpTimers() {
		try {
			var raw = window.localStorage.getItem(MVP_TIMER_STORAGE_KEY);
			if (!raw) return [];
			var parsed = JSON.parse(raw);
			return Array.isArray(parsed) ? parsed : [];
		} catch (err) {
			return [];
		}
	}

	function writeStoredMvpTimers(list) {
		try {
			window.localStorage.setItem(MVP_TIMER_STORAGE_KEY, JSON.stringify(list));
		} catch (err) {
			/* ignore */
		}
	}

	function getRemainingTimerMs(timerObj) {
		var now = Date.now();
		var endAt = timerObj.killTime + timerObj.respawnMs;
		return Math.max(0, endAt - now);
	}

	function formatTimerMs(ms) {
		var totalSec = Math.max(0, Math.floor(ms / 1000));
		var hours = Math.floor(totalSec / 3600);
		var mins = Math.floor((totalSec % 3600) / 60);
		var secs = totalSec % 60;

		if (hours > 0) {
			return String(hours).padStart(2, '0') + ':' + String(mins).padStart(2, '0') + ':' + String(secs).padStart(2, '0');
		}

		return String(mins).padStart(2, '0') + ':' + String(secs).padStart(2, '0');
	}

	function removeTimerById(timerId) {
		var list = readStoredMvpTimers().filter(function (item) {
			return item.id !== timerId;
		});
		writeStoredMvpTimers(list);

		var row = document.getElementById(timerId);
		if (row && row.parentNode) {
			row.parentNode.removeChild(row);
		}

		var timerList = document.getElementById('timer-list');
		var empty = document.getElementById('timer-empty');
		if (timerList && empty && timerList.children.length === 0) {
			empty.style.display = '';
		}
	}

	function createTimerRow(timerObj) {
		var list = document.getElementById('timer-list');
		if (!list) return null;

		var existing = document.getElementById(timerObj.id);
		if (existing) return existing;

		var empty = document.getElementById('timer-empty');
		if (empty) empty.style.display = 'none';

		var row = document.createElement('div');
		row.id = timerObj.id;
		row.className = 'mvp-timer-row';
		row.style.cssText = 'display:grid;grid-template-columns:1fr auto auto;gap:12px;align-items:center;padding:13px 16px;border-radius:12px;background:rgba(0,212,255,0.05);border:1px solid rgba(0,212,255,0.14);';

		var killTs = new Date(timerObj.killTime);
		var h = killTs.getHours().toString().padStart(2, '0');
		var m = killTs.getMinutes().toString().padStart(2, '0');

		row.innerHTML =
			'<div>' +
				'<div style="font-size:0.90rem;font-weight:800;color:#fff;margin-bottom:2px;">💀 ' + timerObj.name + '</div>' +
				'<div style="font-size:0.70rem;color:rgba(122,144,176,0.55);">Kill registrada às ' + h + ':' + m + ' · Respawn ' + timerObj.respawnMins + 'min</div>' +
			'</div>' +
			'<div class="mvp-timer-countdown" style="font-size:0.86rem;font-weight:900;color:#00d4ff;min-width:78px;text-align:right;">--:--</div>' +
			'<button type="button" class="mvp-timer-remove" style="padding:8px 12px;border-radius:8px;background:rgba(255,61,90,0.10);border:1px solid rgba(255,61,90,0.22);color:#ff3d5a;font-size:0.75rem;font-weight:800;cursor:pointer;font-family:inherit;">Remover</button>';

		var removeBtn = row.querySelector('.mvp-timer-remove');
		if (removeBtn) {
			removeBtn.addEventListener('click', function () {
				removeTimerById(timerObj.id);
			});
		}

		list.appendChild(row);
		return row;
	}

	function refreshSingleTimerRow(timerObj) {
		var row = document.getElementById(timerObj.id);
		if (!row) {
			row = createTimerRow(timerObj);
		}
		if (!row) return;

		var countdown = row.querySelector('.mvp-timer-countdown');
		var remaining = getRemainingTimerMs(timerObj);

		if (countdown) {
			if (remaining <= 0) {
				countdown.textContent = 'Respawn';
				countdown.style.color = '#00ff88';
				row.style.borderColor = 'rgba(0,255,136,0.22)';
				row.style.background = 'rgba(0,255,136,0.05)';
			} else {
				countdown.textContent = formatTimerMs(remaining);
				countdown.style.color = '#00d4ff';
				row.style.borderColor = 'rgba(0,212,255,0.14)';
				row.style.background = 'rgba(0,212,255,0.05)';
			}
		}
	}

	function refreshAllMvpTimers() {
		var list = readStoredMvpTimers();
		for (var i = 0; i < list.length; i++) {
			refreshSingleTimerRow(list[i]);
		}
	}

	window.toggleBoss = function (card) {
		if (!card) return;

		var dossier = card.querySelector('.boss-dossier');
		var chevron = card.querySelector('.boss-chevron');
		if (!dossier) return;

		var isOpen = dossier.style.display !== 'none';
		dossier.style.display = isOpen ? 'none' : 'block';

		if (chevron) {
			chevron.textContent = isOpen ? '▼ Dossiê' : '▲ Fechar';
		}

		card.style.boxShadow = isOpen ? '' : '0 0 32px rgba(255,61,90,0.12)';
	};

	window.filterTier = function (tier) {
		var btns = document.querySelectorAll('.tf-btn');
		for (var i = 0; i < btns.length; i++) {
			btns[i].style.opacity = '0.45';
			btns[i].style.fontWeight = '700';
		}

		var active = document.getElementById('tf-' + tier);
		if (active) {
			active.style.opacity = '1';
			active.style.fontWeight = '900';
		}

		var cards = document.querySelectorAll('.boss-card');
		var shown = 0;

		for (var j = 0; j < cards.length; j++) {
			var t = cards[j].dataset.tier;
			var show = tier === 'all' || t === tier;
			cards[j].style.display = show ? '' : 'none';
			if (show) shown++;
		}

		var label = document.getElementById('boss-count-label');
		if (label) {
			label.textContent = shown + (shown === 1 ? ' MVP exibido' : ' MVPs exibidos');
		}
	};

	window.sortBosses = function (by) {
		var btns = document.querySelectorAll('.sort-btn');
		for (var i = 0; i < btns.length; i++) {
			btns[i].style.background = 'rgba(255,255,255,0.06)';
			btns[i].style.borderColor = 'rgba(255,255,255,0.10)';
			btns[i].style.color = 'rgba(232,238,248,0.65)';
		}

		var active = document.getElementById('sort-' + by);
		if (active) {
			active.style.background = 'rgba(255,61,90,0.12)';
			active.style.borderColor = 'rgba(255,61,90,0.30)';
			active.style.color = '#ff3d5a';
		}

		var grid = document.getElementById('boss-grid');
		if (!grid) return;

		var cards = Array.prototype.slice.call(grid.querySelectorAll('.boss-card'));
		cards.sort(function (a, b) {
			if (by === 'danger') return parseInt(b.dataset.danger || '0', 10) - parseInt(a.dataset.danger || '0', 10);
			if (by === 'exp') return parseInt(b.dataset.exp || '0', 10) - parseInt(a.dataset.exp || '0', 10);
			if (by === 'respawn') return parseInt(a.dataset.respawn || '0', 10) - parseInt(b.dataset.respawn || '0', 10);
			return 0;
		});

		for (var j = 0; j < cards.length; j++) {
			grid.appendChild(cards[j]);
		}
	};

	window.toggleClassMvp = function (row) {
		if (!row) return;
		var detail = row.querySelector('.class-mvp-detail');
		var chevron = row.querySelector('.class-chevron');
		if (!detail) return;

		var open = detail.style.display !== 'none';
		detail.style.display = open ? 'none' : 'block';
		if (chevron) {
			chevron.style.transform = open ? 'rotate(0deg)' : 'rotate(180deg)';
		}
	};

	window.logKill = function (btn, name, respawnMins) {
		var killTime = Date.now();
		var respawnMs = toNumber(respawnMins, 0) * 60 * 1000;
		var timerObj = {
			id: 'timer-' + killTime,
			name: name || 'MVP',
			respawnMins: toNumber(respawnMins, 0),
			respawnMs: respawnMs,
			killTime: killTime
		};

		var list = readStoredMvpTimers();
		list.unshift(timerObj);
		writeStoredMvpTimers(list);

		createTimerRow(timerObj);
		refreshSingleTimerRow(timerObj);

		if (btn) {
			btn.style.filter = 'brightness(1.08)';
			setTimeout(function () {
				btn.style.filter = '';
			}, 450);
		}
	};

	function bindMvpGuide(context) {
		var scope = context && context.querySelector ? context : document;
		var root = scope.querySelector('#mvp-guide-root') || document.getElementById('mvp-guide-root');
		if (!root) return;

		var tierButtons = root.querySelectorAll('.tf-btn[data-tier]');
		for (var i = 0; i < tierButtons.length; i++) {
			(function (btn) {
				bindActionButton(btn, function () {
					filterTier(this.getAttribute('data-tier'));
				});
			})(tierButtons[i]);
		}

		var sortButtons = root.querySelectorAll('.sort-btn[data-sort]');
		for (var j = 0; j < sortButtons.length; j++) {
			(function (btn) {
				bindActionButton(btn, function () {
					sortBosses(this.getAttribute('data-sort'));
				});
			})(sortButtons[j]);
		}

		if (!window.__timeroMvpTimerInterval) {
			window.__timeroMvpTimerInterval = window.setInterval(refreshAllMvpTimers, 1000);
		}

		refreshAllMvpTimers();

		var label = document.getElementById('boss-count-label');
		if (label && label.textContent.replace(/\s+/g, '') === '') {
			filterTier('all');
		}
	}

	/* =========================================================
	   OPTIONAL GUIDE HELPERS
	========================================================= */

	function bindPhaseNav(context) {
		var scope = context && context.querySelector ? context : document;
		var nav = scope.querySelector('#phase-nav');
		if (!nav) return;

		var links = nav.querySelectorAll('a[href^="#"], a[data-target]');
		for (var i = 0; i < links.length; i++) {
			(function (link) {
				if (link.dataset.timeroPhaseBound === '1') return;
				link.dataset.timeroPhaseBound = '1';

				link.addEventListener('click', function (e) {
					var targetSelector = this.getAttribute('href');
					if (!targetSelector || targetSelector === '#') {
						targetSelector = this.getAttribute('data-target');
					}
					if (!targetSelector) return;

					var target = document.querySelector(targetSelector);
					if (!target) return;

					e.preventDefault();
					target.scrollIntoView({
						behavior: 'smooth',
						block: 'start'
					});
				});
			})(links[i]);
		}
	}

	/* =========================================================
	   INIT
	========================================================= */

	function init(context) {
		setLogoHref();
		ensureFontAwesome();
		applyTimeroGuideSkin();
		bindMethodViewer(context || document);
		bindRouteViewer(context || document);
		bindCalculator(context || document);
		bindLoadout(context || document);
		bindTimeroNavCards(context || document);
		bindMvpGuide(context || document);
		bindPhaseNav(context || document);
	}

	$(function () {
		init(document);
	});
/* =========================================================
   TIMERO WIKI CARD DATABASE
   Safe MediaWiki version: JS creates inputs/buttons instead of
   placing raw <input>/<button> inside wiki pages.
========================================================= */
(function () {
  'use strict';

  const CARD_DB_SELECTOR = '#timero-card-db-app';

  const state = {
    api: '',
    cards: [],
    groups: [],
    q: '',
    category: 'all',
    changed: false,
    sort: 'card_name',
    dir: 'asc',
    expandedCardId: null,
    loading: false,
    error: ''
  };

  const categories = [
    { id: 'all', label: 'Todas', icon: '◆', color: '#7c6aff' },
    { id: 'weapon', label: 'Armas', icon: '⚔️', color: '#ff6b7a' },
    { id: 'armor', label: 'Armaduras', icon: '🛡', color: '#00d4ff' },
    { id: 'accessory', label: 'Acessórios', icon: '💎', color: '#f9a826' },
    { id: 'headgear', label: 'Headgear', icon: '🎩', color: '#b06cff' },
    { id: 'shield', label: 'Escudos', icon: '🛡️', color: '#70b8ff' },
    { id: 'garment', label: 'Capas', icon: '🧥', color: '#60d090' },
    { id: 'shoes', label: 'Sapatos', icon: '👢', color: '#f0c840' }
  ];

  function escapeHtml(value) {
    return String(value ?? '')
      .replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;')
      .replace(/'/g, '&#039;');
  }

  function normalize(value) {
    return String(value ?? '').toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '');
  }

  function debounce(fn, delay) {
    let timer = null;
    return function () {
      const args = arguments;
      clearTimeout(timer);
      timer = setTimeout(function () {
        fn.apply(null, args);
      }, delay);
    };
  }

  function getRoot() {
    return document.querySelector(CARD_DB_SELECTOR);
  }

  function cardIconUrl(cardId) {
    return 'https://timero.com.br/images/item/icons/' + encodeURIComponent(cardId) + '.png';
  }

  function cardLink(cardId) {
    return 'https://timero.com.br/pt/item?id=' + encodeURIComponent(cardId);
  }

  function categoryMeta(category) {
    return categories.find(c => c.id === category) || categories[0];
  }

  function changedBadge(card) {
    if (String(card.changed) === '1') {
      return '<span class="tc-status tc-changed">Alterada</span>';
    }
    return '<span class="tc-status tc-original">Original</span>';
  }

  function scriptToReadable(script) {
    const raw = String(script || '').trim();
    if (!raw) return '—';

    return raw
      .replace(/bonus\s+/gi, '')
      .replace(/;/g, '; ')
      .replace(/bStr/gi, 'STR')
      .replace(/bAgi/gi, 'AGI')
      .replace(/bVit/gi, 'VIT')
      .replace(/bInt/gi, 'INT')
      .replace(/bDex/gi, 'DEX')
      .replace(/bLuk/gi, 'LUK')
      .replace(/bAllStats/gi, 'All Stats')
      .replace(/bMaxHP/gi, 'Max HP')
      .replace(/bMaxSP/gi, 'Max SP')
      .replace(/bBaseAtk/gi, 'ATK')
      .replace(/bMatk/gi, 'MATK')
      .replace(/bAspdRate/gi, 'ASPD %')
      .replace(/bAtkRate/gi, 'ATK %')
      .replace(/bMatkRate/gi, 'MATK %')
      .replace(/\s+/g, ' ')
      .trim();
  }

  function injectStyles() {
    if (document.getElementById('timero-card-db-style')) return;

    const style = document.createElement('style');
    style.id = 'timero-card-db-style';
    style.textContent = `
      #timero-card-db-app,
      #timero-card-db-app * {
        box-sizing: border-box;
      }

      #timero-card-db-app {
        --tc-bg: #050914;
        --tc-card: rgba(8,14,26,0.94);
        --tc-card-2: rgba(4,8,18,0.99);
        --tc-border: rgba(80,170,255,0.18);
        --tc-border-soft: rgba(255,255,255,0.06);
        --tc-cyan: #58d7ff;
        --tc-blue: #4a90d9;
        --tc-muted: #6070a0;
        --tc-text: #d8ecff;
        --tc-gold: #f0c840;
        --tc-green: #70d890;
        --tc-red: #ff6b7a;
        font-family: 'Segoe UI', system-ui, sans-serif;
      }

      .tc-shell {
        background:
          radial-gradient(circle at 18% 0%, rgba(74,144,217,0.10), transparent 34%),
          radial-gradient(circle at 88% 24%, rgba(92,70,180,0.08), transparent 30%),
          linear-gradient(135deg, rgba(5,10,22,0.98), rgba(2,5,14,0.99));
        border: 1px solid var(--tc-border);
        border-radius: 18px;
        padding: 24px;
        margin: 0 0 34px 0;
        box-shadow: 0 0 30px rgba(0,0,0,0.35), inset 0 0 28px rgba(80,160,255,0.025);
        color: var(--tc-text);
      }

      .tc-head {
        background: linear-gradient(90deg, rgba(74,144,217,0.14), rgba(74,144,217,0.04), transparent);
        border: 1px solid rgba(74,144,217,0.18);
        border-left: 5px solid var(--tc-blue);
        border-radius: 12px;
        padding: 16px 18px;
        margin: 0 0 18px 0;
      }

      .tc-kicker {
        color: var(--tc-cyan);
        font-weight: 900;
        letter-spacing: .10em;
        text-transform: uppercase;
        font-size: .78rem;
        margin-bottom: 8px;
      }

      .tc-title {
        color: #fff;
        font-size: clamp(1.4rem, 2.6vw, 2rem);
        font-weight: 900;
        margin: 0 0 6px 0;
        line-height: 1.1;
      }

      .tc-subtitle {
        color: rgba(180,205,230,0.72);
        font-size: .92rem;
        line-height: 1.65;
      }

      .tc-toolbar {
        background: linear-gradient(135deg, rgba(8,14,26,0.94), rgba(4,8,18,0.99));
        border: 1px solid rgba(255,255,255,0.08);
        border-radius: 14px;
        padding: 16px;
        margin: 0 0 16px 0;
      }

      .tc-topline {
        display: flex;
        gap: 12px;
        align-items: center;
        flex-wrap: wrap;
      }

      .tc-search-wrap {
        flex: 1;
        min-width: 260px;
        position: relative;
      }

      .tc-search-icon {
        position: absolute;
        left: 13px;
        top: 50%;
        transform: translateY(-50%);
        opacity: .55;
        pointer-events: none;
      }

      .tc-search {
        width: 100%;
        padding: 11px 12px 11px 40px;
        border-radius: 11px;
        background: rgba(0,0,0,0.34);
        border: 1px solid rgba(255,255,255,0.09);
        color: #e8eef8;
        font-size: .92rem;
        outline: none;
        font-family: inherit;
      }

      .tc-search:focus {
        border-color: rgba(88,215,255,0.42);
        box-shadow: 0 0 0 3px rgba(88,215,255,0.08);
      }

      .tc-toggle {
        display: inline-flex;
        align-items: center;
        gap: 9px;
        cursor: pointer;
        padding: 10px 14px;
        border-radius: 11px;
        background: rgba(255,255,255,0.04);
        border: 1px solid rgba(255,255,255,0.08);
        color: rgba(180,205,230,0.75);
        font-size: .82rem;
        font-weight: 800;
        white-space: nowrap;
        user-select: none;
      }

      .tc-toggle input {
        width: 16px;
        height: 16px;
        accent-color: var(--tc-cyan);
      }

      .tc-count {
        padding: 10px 15px;
        border-radius: 11px;
        background: rgba(74,144,217,0.10);
        border: 1px solid rgba(74,144,217,0.22);
        color: var(--tc-cyan);
        font-weight: 900;
        font-size: .82rem;
        white-space: nowrap;
      }

      .tc-tabs {
        display: flex;
        gap: 8px;
        flex-wrap: wrap;
        margin-top: 12px;
      }

      .tc-tab {
        border: 1px solid rgba(255,255,255,0.08);
        background: rgba(255,255,255,0.035);
        color: rgba(180,205,230,0.68);
        border-radius: 999px;
        padding: 8px 13px;
        cursor: pointer;
        font-family: inherit;
        font-weight: 800;
        font-size: .78rem;
      }

      .tc-tab.active {
        background: linear-gradient(135deg, rgba(74,144,217,0.22), rgba(74,144,217,0.08));
        border-color: rgba(88,215,255,0.30);
        color: var(--tc-cyan);
        box-shadow: 0 0 16px rgba(88,215,255,0.08);
      }

      .tc-table {
        border: 1px solid rgba(255,255,255,0.08);
        border-radius: 14px;
        overflow: hidden;
        background: rgba(3,6,14,0.96);
      }

      .tc-table-head {
        display: grid;
        grid-template-columns: 88px 1.35fr 150px 145px 120px;
        background: rgba(0,0,0,0.42);
        border-bottom: 1px solid rgba(255,255,255,0.06);
      }

      .tc-th {
        padding: 12px 14px;
        color: var(--tc-muted);
        font-weight: 900;
        letter-spacing: .08em;
        text-transform: uppercase;
        font-size: .68rem;
        border-right: 1px solid rgba(255,255,255,0.045);
      }

      .tc-th.sortable {
        cursor: pointer;
      }

      .tc-row {
        display: grid;
        grid-template-columns: 88px 1.35fr 150px 145px 120px;
        align-items: center;
        border-bottom: 1px solid rgba(255,255,255,0.045);
        cursor: pointer;
        transition: background .15s ease, border-color .15s ease;
      }

      .tc-row:hover {
        background: rgba(74,144,217,0.06);
      }

      .tc-cell {
        padding: 13px 14px;
        border-right: 1px solid rgba(255,255,255,0.035);
      }

      .tc-id {
        font-family: 'Courier New', monospace;
        color: var(--tc-gold);
        font-weight: 900;
      }

      .tc-name-line {
        display: flex;
        align-items: center;
        gap: 12px;
        min-width: 0;
      }

      .tc-icon {
        width: 42px;
        height: 42px;
        border-radius: 10px;
        background: rgba(0,0,0,0.28);
        border: 1px solid rgba(255,255,255,0.08);
        display: flex;
        align-items: center;
        justify-content: center;
        overflow: hidden;
        flex: 0 0 42px;
      }

      .tc-icon img {
        max-width: 36px;
        max-height: 36px;
        object-fit: contain;
        image-rendering: auto;
      }

      .tc-card-name {
        color: var(--tc-cyan);
        font-weight: 900;
        font-size: .96rem;
        line-height: 1.2;
      }

      .tc-monster-name {
        color: rgba(180,205,230,0.58);
        font-size: .76rem;
        margin-top: 3px;
      }

      .tc-pill {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        padding: 6px 10px;
        border-radius: 999px;
        background: rgba(255,255,255,0.04);
        border: 1px solid rgba(255,255,255,0.08);
        color: rgba(200,220,240,0.72);
        font-weight: 800;
        font-size: .76rem;
        white-space: nowrap;
      }

      .tc-status {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        padding: 6px 10px;
        border-radius: 999px;
        font-weight: 900;
        font-size: .72rem;
        white-space: nowrap;
      }

      .tc-changed {
        background: rgba(112,216,144,0.10);
        border: 1px solid rgba(112,216,144,0.24);
        color: var(--tc-green);
      }

      .tc-original {
        background: rgba(255,255,255,0.035);
        border: 1px solid rgba(255,255,255,0.08);
        color: rgba(180,205,230,0.55);
      }

      .tc-detail {
        display: none;
        grid-column: 1 / -1;
        padding: 0;
        background: rgba(0,0,0,0.18);
        border-bottom: 1px solid rgba(255,255,255,0.055);
      }

      .tc-row.open + .tc-detail {
        display: block;
      }

      .tc-detail-grid {
        display: grid;
        grid-template-columns: 1fr 1fr 1fr;
        gap: 12px;
        padding: 16px;
      }

      .tc-effect-box {
        border-radius: 12px;
        padding: 14px;
        background: linear-gradient(135deg, rgba(8,14,26,0.96), rgba(4,8,18,0.99));
        border: 1px solid rgba(255,255,255,0.08);
        min-height: 112px;
      }

      .tc-effect-title {
        color: var(--tc-muted);
        font-weight: 900;
        letter-spacing: .08em;
        text-transform: uppercase;
        font-size: .68rem;
        margin-bottom: 8px;
      }

      .tc-effect-text {
        color: rgba(220,235,255,0.78);
        font-size: .84rem;
        line-height: 1.65;
        white-space: pre-wrap;
      }

      .tc-effect-box.new {
        border-color: rgba(112,216,144,0.18);
      }

      .tc-effect-box.collection {
        border-color: rgba(240,200,64,0.18);
      }

      .tc-empty,
      .tc-loading,
      .tc-error {
        padding: 34px;
        text-align: center;
        color: rgba(180,205,230,0.58);
        font-weight: 700;
      }

      .tc-error {
        color: #ff8a8a;
      }

      .tc-groups {
        margin-top: 26px;
      }

      .tc-group-list {
        display: grid;
        gap: 10px;
      }

      .tc-group-card {
        border-radius: 14px;
        border: 1px solid rgba(255,255,255,0.08);
        background: linear-gradient(135deg, rgba(8,14,26,0.94), rgba(4,8,18,0.99));
        overflow: hidden;
      }

      .tc-group-head {
        padding: 15px 18px;
        display: grid;
        grid-template-columns: auto 1fr auto;
        gap: 13px;
        align-items: center;
      }

      .tc-group-icon {
        width: 42px;
        height: 42px;
        border-radius: 12px;
        background: rgba(74,144,217,0.10);
        border: 1px solid rgba(74,144,217,0.22);
        display: flex;
        align-items: center;
        justify-content: center;
        font-size: 1.25rem;
      }

      .tc-group-key {
        color: var(--tc-muted);
        font-size: .64rem;
        font-weight: 900;
        letter-spacing: .11em;
        text-transform: uppercase;
        margin-bottom: 2px;
      }

      .tc-group-name {
        color: #fff;
        font-weight: 900;
      }

      .tc-group-bonus {
        padding: 11px 18px;
        border-top: 1px solid rgba(255,255,255,0.055);
        color: rgba(220,235,255,0.74);
        line-height: 1.55;
        font-size: .84rem;
      }

      @media (max-width: 900px) {
        .tc-table-head {
          display: none;
        }

        .tc-row {
          grid-template-columns: 1fr;
          padding: 12px;
        }

        .tc-cell {
          border-right: none;
          padding: 7px 4px;
        }

        .tc-detail-grid {
          grid-template-columns: 1fr;
        }

        .tc-topline {
          align-items: stretch;
        }

        .tc-toggle,
        .tc-count {
          width: 100%;
          justify-content: center;
        }
      }
    `;
    document.head.appendChild(style);
  }

  function createShell(root) {
    root.innerHTML = `
      <div class="tc-shell">
        <div class="tc-head">
          <div class="tc-kicker">◇ Banco de Dados</div>
          <h2 class="tc-title">Todas as Cartas Balanceadas</h2>
          <div class="tc-subtitle">
            Pesquise por nome da carta, ID, monstro, slot, efeito antigo, efeito novo, bônus de coleção ou script de coleção.
          </div>
        </div>

        <div class="tc-toolbar">
          <div class="tc-topline">
            <div class="tc-search-wrap">
              <span class="tc-search-icon">🔍</span>
              <input class="tc-search" id="tc-search" type="text" placeholder="Buscar carta, ID, monstro, efeito ou coleção..." autocomplete="off">
            </div>

            <label class="tc-toggle" for="tc-changed">
              <input id="tc-changed" type="checkbox">
              ⚡ Apenas alteradas
            </label>

            <div class="tc-count" id="tc-count">— cartas</div>
          </div>

          <div class="tc-tabs" id="tc-tabs"></div>
        </div>

        <div class="tc-table">
          <div class="tc-table-head">
            <div class="tc-th sortable" data-sort="id">ID ↕</div>
            <div class="tc-th sortable" data-sort="name">Carta / Monstro ↕</div>
            <div class="tc-th sortable" data-sort="slot">Slot ↕</div>
            <div class="tc-th sortable" data-sort="category">Categoria ↕</div>
            <div class="tc-th">Status</div>
          </div>
          <div id="tc-body">
            <div class="tc-loading">Carregando cartas...</div>
          </div>
        </div>

        <div class="tc-groups">
          <div class="tc-head" style="margin-top:26px;">
            <div class="tc-kicker">◇ Grupos de Coleção</div>
            <h2 class="tc-title" style="font-size:1.35rem;">Bônus permanentes por grupo</h2>
            <div class="tc-subtitle">Dados carregados da tabela <strong>wiki_card_groups</strong>.</div>
          </div>
          <div class="tc-group-list" id="tc-groups"></div>
        </div>
      </div>
    `;

    const tabs = root.querySelector('#tc-tabs');
    categories.forEach(cat => {
      const btn = document.createElement('button');
      btn.type = 'button';
      btn.className = 'tc-tab' + (cat.id === state.category ? ' active' : '');
      btn.dataset.category = cat.id;
      btn.innerHTML = `${cat.icon} ${escapeHtml(cat.label)}`;
      btn.addEventListener('click', function () {
        state.category = cat.id;
        refresh(root);
      });
      tabs.appendChild(btn);
    });

    root.querySelector('#tc-search').addEventListener('input', debounce(function (e) {
      state.q = e.target.value.trim();
      refresh(root);
    }, 220));

    root.querySelector('#tc-changed').addEventListener('change', function (e) {
      state.changed = !!e.target.checked;
      refresh(root);
    });

    root.querySelectorAll('.tc-th.sortable').forEach(th => {
      th.addEventListener('click', function () {
        const sort = th.dataset.sort;
        if (state.sort === sort) {
          state.dir = state.dir === 'asc' ? 'desc' : 'asc';
        } else {
          state.sort = sort;
          state.dir = 'asc';
        }
        refresh(root);
      });
    });
  }

  async function fetchCards(root) {
    state.loading = true;
    state.error = '';
    renderBody(root);

    const params = new URLSearchParams();
    params.set('q', state.q);
    params.set('category', state.category);
    params.set('changed', state.changed ? '1' : '0');
    params.set('sort', state.sort);
    params.set('dir', state.dir);

    try {
      const response = await fetch(state.api + '?' + params.toString(), {
        credentials: 'same-origin',
        cache: 'no-store'
      });

      const text = await response.text();
      let data;

      try {
        data = JSON.parse(text);
      } catch (err) {
        throw new Error('Invalid JSON returned by API: ' + text.slice(0, 300));
      }

      if (!data.ok) {
        throw new Error(data.error || 'API returned an unknown error.');
      }

      state.cards = Array.isArray(data.cards) ? data.cards : [];
      state.groups = Array.isArray(data.groups) ? data.groups : [];
      state.expandedCardId = null;
    } catch (err) {
      state.cards = [];
      state.groups = [];
      state.error = err.message || String(err);
    } finally {
      state.loading = false;
      renderAll(root);
    }
  }

  function refresh(root) {
    updateTabs(root);
    fetchCards(root);
  }

  function updateTabs(root) {
    root.querySelectorAll('.tc-tab').forEach(btn => {
      btn.classList.toggle('active', btn.dataset.category === state.category);
    });
  }

  function renderAll(root) {
    renderBody(root);
    renderGroups(root);
    const count = root.querySelector('#tc-count');
    if (count) {
      count.textContent = state.cards.length + ' carta' + (state.cards.length === 1 ? '' : 's');
    }
  }

  function renderBody(root) {
    const body = root.querySelector('#tc-body');
    if (!body) return;

    if (state.loading) {
      body.innerHTML = '<div class="tc-loading">Carregando cartas...</div>';
      return;
    }

    if (state.error) {
      body.innerHTML = '<div class="tc-error">Erro ao carregar cartas: ' + escapeHtml(state.error) + '</div>';
      return;
    }

    if (!state.cards.length) {
      body.innerHTML = '<div class="tc-empty">Nenhuma carta encontrada com os filtros atuais.</div>';
      return;
    }

    body.innerHTML = '';

    state.cards.forEach(card => {
      const meta = categoryMeta(card.category);
      const row = document.createElement('div');
      row.className = 'tc-row';
      row.dataset.cardId = card.card_id;

      const monster = card.monster_name ? `<div class="tc-monster-name">${escapeHtml(card.monster_name)}</div>` : '';
      const group = card.collection_group_name || '—';

      row.innerHTML = `
        <div class="tc-cell tc-id">#${escapeHtml(card.card_id)}</div>
        <div class="tc-cell">
          <div class="tc-name-line">
            <div class="tc-icon">
              <img src="${cardIconUrl(card.card_id)}" alt="${escapeHtml(card.card_name)}" onerror="this.style.display='none';this.parentNode.textContent='🃏';">
            </div>
            <div>
              <div class="tc-card-name">
                <a href="${cardLink(card.card_id)}" target="_blank" rel="noopener" style="color:inherit;text-decoration:none;">${escapeHtml(card.card_name)}</a>
              </div>
              ${monster}
            </div>
          </div>
        </div>
        <div class="tc-cell"><span class="tc-pill">${escapeHtml(card.slot_type || 'Unknown')}</span></div>
        <div class="tc-cell"><span class="tc-pill" style="border-color:${meta.color}44;color:${meta.color};">${meta.icon} ${escapeHtml(group)}</span></div>
        <div class="tc-cell">${changedBadge(card)}</div>
      `;

      const detail = document.createElement('div');
      detail.className = 'tc-detail';

      detail.innerHTML = `
        <div class="tc-detail-grid">
          <div class="tc-effect-box">
            <div class="tc-effect-title">Efeito Antigo</div>
            <div class="tc-effect-text">${escapeHtml(card.old_effect || '—')}</div>
          </div>

          <div class="tc-effect-box new">
            <div class="tc-effect-title">Efeito Novo</div>
            <div class="tc-effect-text">${escapeHtml(card.new_effect || '—')}</div>
          </div>

          <div class="tc-effect-box collection">
            <div class="tc-effect-title">Coleção</div>
            <div class="tc-effect-text"><strong style="color:#f0c840;">Grupo:</strong> ${escapeHtml(card.collection_group_name || '—')}<br><strong style="color:#f0c840;">Pontos:</strong> ${escapeHtml(card.collection_points || 0)}<br><strong style="color:#f0c840;">Efeito:</strong> ${escapeHtml(card.collection_effect || '—')}<br><strong style="color:#f0c840;">Script:</strong> ${escapeHtml(scriptToReadable(card.collection_script))}</div>
          </div>
        </div>
      `;

      row.addEventListener('click', function () {
        const isOpen = row.classList.contains('open');
        body.querySelectorAll('.tc-row.open').forEach(r => r.classList.remove('open'));
        if (!isOpen) row.classList.add('open');
      });

      body.appendChild(row);
      body.appendChild(detail);
    });
  }

  function renderGroups(root) {
    const wrap = root.querySelector('#tc-groups');
    if (!wrap) return;

    if (state.error) {
      wrap.innerHTML = '';
      return;
    }

    if (!state.groups.length) {
      wrap.innerHTML = '<div class="tc-empty">Nenhum grupo de coleção cadastrado.</div>';
      return;
    }

    wrap.innerHTML = '';

    state.groups.forEach(group => {
      const color = group.color || '#7c6aff';
      const el = document.createElement('div');
      el.className = 'tc-group-card';
      el.style.borderColor = color + '33';

      el.innerHTML = `
        <div class="tc-group-head">
          <div class="tc-group-icon" style="border-color:${color}44;background:${color}14;">${escapeHtml(group.icon || '🃏')}</div>
          <div>
            <div class="tc-group-key" style="color:${color};">Grupo · ${escapeHtml(group.group_key)}</div>
            <div class="tc-group-name">${escapeHtml(group.group_name)}</div>
          </div>
          <div class="tc-pill" style="border-color:${color}33;color:${color};">${escapeHtml(group.card_count || 0)} cartas · Min ${escapeHtml(group.min_cards || 1)}</div>
        </div>
        <div class="tc-group-bonus">
          <strong style="color:${color};">Bônus:</strong> ${escapeHtml(group.collection_effect || '—')}
        </div>
      `;

      wrap.appendChild(el);
    });
  }

  function initCardDb() {
    const root = getRoot();
    if (!root || root.dataset.tcReady === '1') return;

    root.dataset.tcReady = '1';
    state.api = root.dataset.api || '/pt/api/wiki_cards.php';

    injectStyles();
    createShell(root);
    fetchCards(root);
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', initCardDb);
  } else {
    initCardDb();
  }

  if (window.mw && mw.hook) {
    mw.hook('wikipage.content').add(initCardDb);
  }
})();
	if (mw && mw.hook) {
		mw.hook('wikipage.content').add(function ($content) {
			init(($content && $content[0]) || document);
		});
	}
})(jQuery, mediaWiki);