MediaWiki:Common.js: Difference between revisions

From TimeRO Wiki
Jump to navigation Jump to search
(Undo revision 558823 by Admin (talk))
Tag: Undo
No edit summary
Tag: Reverted
Line 1: Line 1:
/* TimeRO MediaWiki:Common.js
/* TimeRO MediaWiki:Common.js
   Fixes Card Database and Beginner Leveling Guide loaders.
   FULL REVISED LOGIC-ONLY VERSION
   ES5-safe: no const, let, arrow functions, template literals or nullish coalescing. */
  - No CSS injection / no <style> creation
  - Does not overwrite #farming-guide-root with generic Zeny renderer
  - Keeps interactive logic for: Cards, Leveling, Zeny mount, MVP guide, Merchant Ledger farming guide
   - ES5-safe: no const/let, no arrow functions, no template literals
*/
(function () {
(function () {
   'use strict';
   'use strict';


   function esc(v){return String(v==null?'':v).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#039;');}
  /* =========================================================
   function style(id,css){if(document.getElementById(id))return;var s=document.createElement('style');s.id=id;s.appendChild(document.createTextNode(css));document.head.appendChild(s);}
    Shared helpers
   function ajax(url,ok,fail){var x=new XMLHttpRequest();x.open('GET',url,true);x.setRequestHeader('Accept','application/json');x.onreadystatechange=function(){if(x.readyState!==4)return;if(x.status<200||x.status>=300){fail('HTTP '+x.status+': '+x.responseText.substr(0,240));return;}try{ok(JSON.parse(x.responseText));}catch(e){fail('JSON inválido: '+x.responseText.substr(0,240));}};x.onerror=function(){fail('Falha de rede.');};x.send(null);}
  ========================================================= */
   function debounce(fn,ms){var t;return function(){var a=arguments,self=this;clearTimeout(t);t=setTimeout(function(){fn.apply(self,a);},ms);};}
  function byId(id) { return document.getElementById(id); }
   function rgba(hex,a){var h=String(hex||'#58d7ff').replace('#',''),n=parseInt(h,16);return 'rgba('+((n>>16)&255)+','+((n>>8)&255)+','+(n&255)+','+a+')';}
  function qs(sel, root) { return (root || document).querySelector(sel); }
  function qsa(sel, root) { return Array.prototype.slice.call((root || document).querySelectorAll(sel)); }
  function trim(v) { return String(v == null ? '' : v).replace(/^\s+|\s+$/g, ''); }
   function esc(v) {
    return String(v == null ? '' : v)
      .replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;')
      .replace(/'/g, '&#039;');
  }
   function debounce(fn, ms) {
    var t = null;
    return function () {
      var self = this;
      var args = arguments;
      clearTimeout(t);
      t = setTimeout(function () { fn.apply(self, args); }, ms);
    };
  }
   function ajaxJson(url, ok, fail) {
    var xhr = new XMLHttpRequest();
    xhr.open('GET', url, true);
    xhr.setRequestHeader('Accept', 'application/json');
    xhr.onreadystatechange = function () {
      if (xhr.readyState !== 4) return;
      if (xhr.status < 200 || xhr.status >= 300) {
        fail('HTTP ' + xhr.status + ': ' + String(xhr.responseText || '').substr(0, 260));
        return;
      }
      try {
        ok(JSON.parse(xhr.responseText));
      } catch (e) {
        fail('JSON inválido: ' + String(xhr.responseText || '').substr(0, 260));
      }
    };
    xhr.onerror = function () { fail('Falha de rede.'); };
    xhr.send(null);
  }
   function storageGet(key, fallback) {
    try {
      var raw = localStorage.getItem(key);
      if (raw == null) return fallback;
      return JSON.parse(raw);
    } catch (e) {
      return fallback;
    }
  }
  function storageSet(key, value) {
    try { localStorage.setItem(key, JSON.stringify(value)); } catch (e) {}
  }
   function number(v, fallback) {
    var n = Number(v);
    return isNaN(n) ? (fallback || 0) : n;
  }
  function pad2(n) {
    n = Math.floor(Math.max(0, n));
    return n < 10 ? '0' + n : String(n);
  }
  function nowMs() { return new Date().getTime(); }
  function classOn(el, cls, on) {
    if (!el) return;
    if (el.classList) {
      el.classList.toggle(cls, !!on);
      return;
    }
    var c = ' ' + el.className + ' ';
    if (on && c.indexOf(' ' + cls + ' ') < 0) el.className = trim(el.className + ' ' + cls);
    if (!on) el.className = trim(c.replace(' ' + cls + ' ', ' '));
  }


   function injectSharedCss(){style('timero-apps-fixed-css',
   /* =========================================================
    '.tr-app,.tr-app *{box-sizing:border-box}.tr-app{font-family:Segoe UI,system-ui,sans-serif;color:#d8ecff}.tr-shell{background:radial-gradient(circle at 18% 0%,rgba(74,144,217,.10),transparent 34%),radial-gradient(circle at 88% 24%,rgba(92,70,180,.08),transparent 30%),linear-gradient(135deg,rgba(5,10,22,.98),rgba(2,5,14,.99));border:1px solid rgba(80,170,255,.18);border-radius:18px;padding:24px;margin:0 0 34px;box-shadow:0 0 30px rgba(0,0,0,.35),inset 0 0 28px rgba(80,160,255,.025)}'+
    CARD DATABASE APP
     '.tr-head{background:linear-gradient(90deg,rgba(74,144,217,.14),rgba(74,144,217,.04),transparent);border:1px solid rgba(74,144,217,.18);border-left:5px solid #4a90d9;border-radius:12px;padding:16px 18px;margin:0 0 18px}.tr-kicker{color:#58d7ff;font-weight:900;letter-spacing:.10em;text-transform:uppercase;font-size:.78rem;margin-bottom:8px}.tr-title{color:#fff;font-size:clamp(1.4rem,2.6vw,2rem);font-weight:900;margin:0 0 6px;line-height:1.1}.tr-sub{color:rgba(180,205,230,.72);font-size:.92rem;line-height:1.65}.tr-btn{border:1px solid rgba(255,255,255,.08);background:rgba(255,255,255,.04);color:rgba(180,205,230,.72);border-radius:999px;padding:8px 13px;cursor:pointer;font-family:inherit;font-weight:800;font-size:.78rem}.tr-btn.active{background:linear-gradient(135deg,rgba(74,144,217,.22),rgba(74,144,217,.08));border-color:rgba(88,215,255,.30);color:#58d7ff}.tr-pill{display:inline-flex;align-items:center;justify-content:center;padding:6px 10px;border-radius:999px;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.08);font-weight:800;font-size:.76rem;white-space:nowrap}'+
    Mount point: #timero-card-db-app
     '.tc-toolbar{background:linear-gradient(135deg,rgba(8,14,26,.94),rgba(4,8,18,.99));border:1px solid rgba(255,255,255,.08);border-radius:14px;padding:16px;margin:0 0 16px}.tc-top{display:flex;gap:12px;align-items:center;flex-wrap:wrap}.tc-search-wrap{flex:1;min-width:260px;position:relative}.tc-search-wrap span{position:absolute;left:13px;top:50%;transform:translateY(-50%);opacity:.55}.tc-search{width:100%;padding:11px 12px 11px 40px;border-radius:11px;background:rgba(0,0,0,.34);border:1px solid rgba(255,255,255,.09);color:#e8eef8;font-size:.92rem;outline:none}.tc-toggle{display:inline-flex;gap:9px;align-items:center;cursor:pointer;padding:10px 14px;border-radius:11px;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.08);font-weight:800}.tc-tabs{display:flex;gap:8px;flex-wrap:wrap;margin-top:12px}.tc-table{border:1px solid rgba(255,255,255,.08);border-radius:14px;overflow:hidden;background:rgba(3,6,14,.96)}.tc-head-row,.tc-row{display:grid;grid-template-columns:88px 1.35fr 150px 145px 120px;align-items:center}.tc-head-row{background:rgba(0,0,0,.42);border-bottom:1px solid rgba(255,255,255,.06)}.tc-th{padding:12px 14px;color:#6070a0;font-weight:900;letter-spacing:.08em;text-transform:uppercase;font-size:.68rem;border-right:1px solid rgba(255,255,255,.045);cursor:pointer}.tc-row{border-bottom:1px solid rgba(255,255,255,.045);cursor:pointer}.tc-row:hover{background:rgba(74,144,217,.06)}.tc-cell{padding:13px 14px;border-right:1px solid rgba(255,255,255,.035)}.tc-id{font-family:Courier New,monospace;color:#f0c840;font-weight:900}.tc-name{display:flex;align-items:center;gap:12px}.tc-icon{width:42px;height:42px;border-radius:10px;background:rgba(0,0,0,.28);border:1px solid rgba(255,255,255,.08);display:flex;align-items:center;justify-content:center;overflow:hidden;flex:0 0 42px}.tc-icon img{max-width:36px;max-height:36px}.tc-card-title{color:#58d7ff;font-weight:900}.tc-small{color:rgba(180,205,230,.58);font-size:.76rem;margin-top:3px}.tc-status{display:inline-flex;padding:6px 10px;border-radius:999px;font-size:.72rem;font-weight:900}.tc-changed{background:rgba(112,216,144,.10);border:1px solid rgba(112,216,144,.24);color:#70d890}.tc-original{background:rgba(255,255,255,.035);border:1px solid rgba(255,255,255,.08);color:rgba(180,205,230,.55)}.tc-detail{display:none;background:rgba(0,0,0,.18);border-bottom:1px solid rgba(255,255,255,.055)}.tc-detail.open{display:block}.tc-detail-grid{display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px;padding:16px}.tc-box{border-radius:12px;padding:14px;background:linear-gradient(135deg,rgba(8,14,26,.96),rgba(4,8,18,.99));border:1px solid rgba(255,255,255,.08);min-height:112px}.tc-box h4{color:#6070a0;font-weight:900;letter-spacing:.08em;text-transform:uppercase;font-size:.68rem;margin:0 0 8px}.tc-box div{color:rgba(220,235,255,.78);font-size:.84rem;line-height:1.65;white-space:pre-wrap}.tc-msg{padding:34px;text-align:center;color:rgba(180,205,230,.58);font-weight:700}.tc-error{color:#ff8a8a}.tc-groups{margin-top:26px}.tc-group{border-radius:14px;border:1px solid rgba(255,255,255,.08);background:linear-gradient(135deg,rgba(8,14,26,.94),rgba(4,8,18,.99));overflow:hidden;margin-bottom:10px}.tc-group-main{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;display:flex;align-items:center;justify-content:center;font-size:1.25rem}.tc-group-key{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,.055);color:rgba(220,235,255,.74);line-height:1.55;font-size:.84rem}'+
    CSS must live in the wiki page or MediaWiki:Common.css.
     '.tl-root{margin:-1em -1.5em;min-height:100vh;background:radial-gradient(circle at 18% 0%,rgba(88,215,255,.09),transparent 34%),radial-gradient(circle at 88% 24%,rgba(176,108,255,.08),transparent 30%),linear-gradient(135deg,#050914,#02050e);overflow:hidden}.tl-inner{max-width:1280px;margin:0 auto;padding:44px 28px 60px}.tl-hero{background:linear-gradient(135deg,rgba(8,14,26,.94),rgba(4,8,18,.99));border:1px solid rgba(88,215,255,.18);border-radius:18px;padding:34px;margin-bottom:18px}.tl-hero h1{font-size:clamp(2.2rem,5vw,4rem);line-height:.96;font-weight:900;color:#fff;letter-spacing:-.035em;margin:0 0 16px}.tl-hero h1 span{background:linear-gradient(90deg,#00ff88,#58d7ff,#f9a826);-webkit-background-clip:text;background-clip:text;color:transparent}.tl-stats,.tl-grid,.tl-checks,.tl-related{display:grid;gap:12px}.tl-stats{grid-template-columns:repeat(5,1fr);margin:18px 0}.tl-stat{background:rgba(0,0,0,.24);border:1px solid rgba(255,255,255,.08);border-radius:14px;padding:14px;text-align:center}.tl-stat strong{display:block;font-size:1.35rem;color:#58d7ff;font-weight:900}.tl-stat span{font-size:.72rem;color:rgba(122,144,176,.72);letter-spacing:.12em;text-transform:uppercase;font-weight:800}.tl-panel{background:linear-gradient(135deg,rgba(8,14,26,.94),rgba(4,8,18,.99));border:1px solid rgba(255,255,255,.08);border-radius:18px;padding:16px;margin-bottom:18px}.tl-label{color:#7a90b0;font-weight:900;letter-spacing:.11em;text-transform:uppercase;font-size:.72rem;margin-bottom:10px}.tl-flex{display:flex;gap:9px;flex-wrap:wrap}.tl-track{height:12px;border-radius:999px;background:rgba(255,255,255,.05);overflow:hidden;display:grid;grid-template-columns:24.5fr 24.5fr 20fr 15fr 16fr;margin-bottom:14px}.tl-track span:nth-child(1){background:#00ff88}.tl-track span:nth-child(2){background:#00d4ff}.tl-track span:nth-child(3){background:#f9a826}.tl-track span:nth-child(4){background:#b06cff}.tl-track span:nth-child(5){background:#ff3d5a}.tl-phase{background:linear-gradient(135deg,rgba(8,14,26,.94),rgba(4,8,18,.99));border:1px solid var(--line);border-radius:18px;padding:24px;margin-bottom:22px}.tl-head{display:flex;gap:18px;margin-bottom:22px}.tl-icon{width:64px;height:64px;border-radius:18px;display:flex;align-items:center;justify-content:center;flex:0 0 64px;font-size:1.8rem;background:var(--soft);border:1px solid var(--line)}.tl-chip{display:inline-flex;margin:0 8px 8px 0;padding:3px 10px;border-radius:999px;background:var(--soft);border:1px solid var(--line);color:var(--color);font-size:.65rem;letter-spacing:.13em;text-transform:uppercase;font-weight:900}.tl-phase h2{color:#fff;margin:0 0 8px;font-size:clamp(1.35rem,2.5vw,2rem);border:0}.tl-phase p{color:rgba(176,192,224,.75);line-height:1.7;margin:0}.tl-grid{grid-template-columns:1fr 1fr;margin-bottom:18px}.tl-box{border-radius:16px;overflow:hidden;background:rgba(0,0,0,.24);border:1px solid var(--line)}.tl-box-head{padding:13px 16px;background:var(--soft);border-bottom:1px solid var(--line);color:var(--color);font-weight:900;letter-spacing:.08em;text-transform:uppercase;font-size:.78rem}.tl-box-body{padding:15px;display:flex;flex-direction:column;gap:10px}.tl-card{border-radius:12px;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.07);padding:12px 14px}.tl-map{display:flex;justify-content:space-between;gap:14px}.tl-name{color:#fff;font-size:.95rem;font-weight:900}.tl-muted{color:rgba(122,144,176,.72);font-size:.76rem;margin-top:3px}.tl-level{color:var(--color);font-size:.78rem;font-weight:900;white-space:nowrap;text-align:right}.tl-mob{cursor:pointer}.tl-mob-main{display:flex;align-items:center;gap:12px}.tl-mob-icon{width:42px;height:42px;border-radius:10px;display:flex;align-items:center;justify-content:center;background:var(--soft);border:1px solid var(--line);font-size:1.2rem}.tl-mob-info{flex:1}.tl-detail{display:none;padding:10px 0 0 54px;color:rgba(180,205,230,.78);font-size:.82rem;line-height:1.6}.tl-mob.open .tl-detail{display:block}.tl-checks{grid-template-columns:1fr 1fr}.tl-check{display:flex;align-items:center;gap:10px;cursor:pointer}.tl-mark{width:18px;height:18px;border-radius:5px;border:1px solid var(--line);background:var(--soft);display:flex;align-items:center;justify-content:center;color:var(--color);font-weight:900}.tl-check.checked .tl-text{text-decoration:line-through;opacity:.55}.tl-note{display:none;margin-top:14px;border-radius:12px;padding:14px 16px;color:rgba(220,235,255,.78);line-height:1.65;font-size:.88rem}.tl-note strong{display:block;margin-bottom:5px;letter-spacing:.09em;text-transform:uppercase;font-size:.75rem}.tl-footer{display:flex;justify-content:space-between;gap:12px;flex-wrap:wrap;margin-top:20px}.tl-related{grid-template-columns:repeat(4,1fr);margin-top:24px}.tl-related .tl-panel{text-align:center;margin:0}@media(max-width:980px){.tl-stats,.tl-grid,.tl-checks,.tl-related{grid-template-columns:1fr}.tl-inner{padding:26px 14px 44px}.tl-hero,.tl-phase{padding:20px}.tl-head{flex-direction:column}.tl-map{flex-direction:column}.tl-level{text-align:left}}');}
  ========================================================= */
  function initCardDatabase() {
     var root = byId('timero-card-db-app');
     if (!root || root.getAttribute('data-timero-card-ready') === '1') return;
     root.setAttribute('data-timero-card-ready', '1');


  /* Card app */
    var cats = [
  function initCards(){var root=document.getElementById('timero-card-db-app');if(!root||root.getAttribute('data-ready')==='1')return;root.setAttribute('data-ready','1');injectSharedCss();var state={api:root.getAttribute('data-api')||'/pt/api/wiki_cards.php',q:'',cat:'all',changed:false,sort:'card_name',dir:'asc',cards:[],groups:[],err:'',load:false};var cats=[['all','Todas','◆','#7c6aff'],['weapon','Armas','⚔️','#ff6b7a'],['armor','Armaduras','🛡','#00d4ff'],['accessory','Acessórios','💎','#f9a826'],['headgear','Headgear','🎩','#b06cff'],['shield','Escudos','🛡️','#70b8ff'],['garment','Capas','🧥','#60d090'],['shoes','Sapatos','👢','#f0c840']];function meta(id){for(var i=0;i<cats.length;i++)if(cats[i][0]===id)return cats[i];return cats[0];}function readScript(s){return String(s||'').replace(/bonus\s+/gi,'').replace(/;/g,'; ').replace(/bAllStats/gi,'All Stats').replace(/bMaxHP/gi,'Max HP').replace(/bMaxSP/gi,'Max SP').replace(/bBaseAtk/gi,'ATK').replace(/bMatk/gi,'MATK').replace(/bStr/gi,'STR').replace(/bAgi/gi,'AGI').replace(/bVit/gi,'VIT').replace(/bInt/gi,'INT').replace(/bDex/gi,'DEX').replace(/bLuk/gi,'LUK')||'—';}
      { id: 'all', label: 'Todas', icon: '', color: '#7c6aff' },
    root.className+=' tr-app';root.innerHTML='<div class="tr-shell"><div class="tr-head"><div class="tr-kicker">◇ Banco de Dados</div><h2 class="tr-title">Todas as Cartas Balanceadas</h2><div class="tr-sub">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-top"><div class="tc-search-wrap"><span>🔍</span><input id="tc-search" class="tc-search" placeholder="Buscar carta, ID, monstro, efeito ou coleção..."></div><label class="tc-toggle"><input id="tc-changed" type="checkbox"> ⚡ Apenas alteradas</label><div id="tc-count" class="tr-pill">— cartas</div></div><div id="tc-tabs" class="tc-tabs"></div></div><div class="tc-table"><div class="tc-head-row"><div class="tc-th" data-sort="id">ID ↕</div><div class="tc-th" data-sort="name">Carta / Monstro ↕</div><div class="tc-th" data-sort="slot">Slot ↕</div><div class="tc-th" data-sort="category">Categoria ↕</div><div class="tc-th">Status</div></div><div id="tc-body"><div class="tc-msg">Carregando cartas...</div></div></div><div class="tc-groups"><div class="tr-head" style="margin-top:26px"><div class="tr-kicker">◇ Grupos de Coleção</div><h2 class="tr-title" style="font-size:1.35rem">Bônus permanentes por grupo</h2><div class="tr-sub">Dados carregados da tabela <strong>wiki_card_groups</strong>.</div></div><div id="tc-groups"></div></div></div>';
      { id: 'weapon', label: 'Armas', icon: '⚔️', color: '#ff6b7a' },
    var tabs=root.querySelector('#tc-tabs');for(var i=0;i<cats.length;i++)(function(c){var b=document.createElement('button');b.className='tr-btn'+(c[0]===state.cat?' active':'');b.innerHTML=esc(c[2])+' '+esc(c[1]);b.onclick=function(){state.cat=c[0];fetchCards();};tabs.appendChild(b);})(cats[i]);root.querySelector('#tc-search').oninput=debounce(function(){state.q=this.value;fetchCards();},220);root.querySelector('#tc-changed').onchange=function(){state.changed=this.checked;fetchCards();};var hs=root.querySelectorAll('.tc-th[data-sort]');for(i=0;i<hs.length;i++)hs[i].onclick=function(){var s=this.getAttribute('data-sort');if(state.sort===s)state.dir=state.dir==='asc'?'desc':'asc';else{state.sort=s;state.dir='asc';}fetchCards();};function fetchCards(){var bs=root.querySelectorAll('#tc-tabs .tr-btn');for(var b=0;b<bs.length;b++)bs[b].className='tr-btn'+(cats[b][0]===state.cat?' active':'');state.load=true;render();var q='q='+encodeURIComponent(state.q)+'&category='+state.cat+'&changed='+(state.changed?'1':'0')+'&sort='+state.sort+'&dir='+state.dir;ajax(state.api+(state.api.indexOf('?')===-1?'?':'&')+q,function(d){state.load=false;if(!d||!d.ok){state.err=(d&&d.error)||'Erro desconhecido';state.cards=[];state.groups=[];}else{state.err='';state.cards=d.cards||[];state.groups=d.groups||[];}render();},function(e){state.load=false;state.err=e;state.cards=[];state.groups=[];render();});}function render(){var body=root.querySelector('#tc-body'),count=root.querySelector('#tc-count');count.innerHTML=state.cards.length+' carta'+(state.cards.length===1?'':'s');if(state.load){body.innerHTML='<div class="tc-msg">Carregando cartas...</div>';return;}if(state.err){body.innerHTML='<div class="tc-msg tc-error">Erro: '+esc(state.err)+'</div>';return;}if(!state.cards.length){body.innerHTML='<div class="tc-msg">Nenhuma carta encontrada.</div>';return;}body.innerHTML='';for(var i=0;i<state.cards.length;i++)(function(card){var m=meta(card.category),row=document.createElement('div'),det=document.createElement('div');row.className='tc-row';row.innerHTML='<div class="tc-cell tc-id">#'+esc(card.card_id)+'</div><div class="tc-cell"><div class="tc-name"><div class="tc-icon"><img src="https://timero.com.br/images/item/icons/'+esc(card.card_id)+'.png"></div><div><div class="tc-card-title">'+esc(card.card_name)+'</div>'+(card.monster_name?'<div class="tc-small">'+esc(card.monster_name)+'</div>':'')+'</div></div></div><div class="tc-cell"><span class="tr-pill">'+esc(card.slot_type||'Unknown')+'</span></div><div class="tc-cell"><span class="tr-pill" style="color:'+m[3]+';border-color:'+m[3]+'44">'+m[2]+' '+esc(card.collection_group_name||'—')+'</span></div><div class="tc-cell">'+(String(card.changed)==='1'?'<span class="tc-status tc-changed">Alterada</span>':'<span class="tc-status tc-original">Original</span>')+'</div>';var img=row.querySelector('img');img.onerror=function(){this.parentNode.innerHTML='🃏';};det.className='tc-detail';det.innerHTML='<div class="tc-detail-grid"><div class="tc-box"><h4>Efeito Antigo</h4><div>'+esc(card.old_effect||'—')+'</div></div><div class="tc-box"><h4>Efeito Novo</h4><div>'+esc(card.new_effect||'—')+'</div></div><div class="tc-box"><h4>Coleção</h4><div><b style="color:#f0c840">Grupo:</b> '+esc(card.collection_group_name||'—')+'<br><b style="color:#f0c840">Pontos:</b> '+esc(card.collection_points||0)+'<br><b style="color:#f0c840">Efeito:</b> '+esc(card.collection_effect||'—')+'<br><b style="color:#f0c840">Script:</b> '+esc(readScript(card.collection_script))+'</div></div></div>';row.onclick=function(){var open=det.className.indexOf('open')>-1,ds=body.querySelectorAll('.tc-detail.open'),rs=body.querySelectorAll('.tc-row.open');for(var a=0;a<ds.length;a++)ds[a].className='tc-detail';for(var r=0;r<rs.length;r++)rs[r].className='tc-row';if(!open){det.className='tc-detail open';row.className='tc-row open';}};body.appendChild(row);body.appendChild(det);})(state.cards[i]);var gw=root.querySelector('#tc-groups');gw.innerHTML='';if(!state.groups.length){gw.innerHTML='<div class="tc-msg">Nenhum grupo de coleção cadastrado.</div>';return;}for(i=0;i<state.groups.length;i++){var g=state.groups[i],c=g.color||'#7c6aff',el=document.createElement('div');el.className='tc-group';el.style.borderColor=c+'33';el.innerHTML='<div class="tc-group-main"><div class="tc-group-icon" style="background:'+c+'14;border:1px solid '+c+'44">'+esc(g.icon||'🃏')+'</div><div><div class="tc-group-key" style="color:'+c+'">Grupo · '+esc(g.group_key)+'</div><div class="tc-group-name">'+esc(g.group_name)+'</div></div><div class="tr-pill" style="color:'+c+';border-color:'+c+'33">'+esc(g.card_count||0)+' cartas · Min '+esc(g.min_cards||1)+'</div></div><div class="tc-group-bonus"><b style="color:'+c+'">Bônus:</b> '+esc(g.collection_effect||'—')+'</div>';gw.appendChild(el);}}fetchCards();}
      { 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' }
    ];


  /* Leveling app */
    var state = {
  function initLeveling(){var root=document.getElementById('timero-leveling-guide-app');if(!root||root.getAttribute('data-ready')==='1')return;root.setAttribute('data-ready','1');injectSharedCss();var cls=[['all','Todas','◆','#58d7ff'],['swordsman','Espadachim','⚔️','#ff6b7a'],['mage','Mago','🔮','#b06cff'],['archer','Arqueiro','🏹','#00ff88'],['acolyte','Acolyte','✨','#f9a826'],['thief','Ladrão','🗡️','#7a90b0']],notes={swordsman:['⚔️','Dica: Espadachim','Priorize sobrevivência e dano consistente.'],mage:['🔮','Dica: Mago','Controle distância, elemento e consumo de SP.'],archer:['🏹','Dica: Arqueiro','Use vantagem de alcance e kite.'],acolyte:['✨','Dica: Acolyte','Heal contra Undead pode ser muito eficiente.'],thief:['🗡️','Dica: Ladrão','AGI e evasão tornam a progressão mais segura.']},ph=[['phase-1','FASE 1','Nível 1 → 25','~XX–YY minutos','🌿','#00ff88','Primeiros Passos — Poring Island & Prontera Field'],['phase-2','FASE 2','Nível 25 → 50','~XX–YY horas','⚡','#00d4ff','Construindo Momentum — Mapas Intermediários'],['phase-3','FASE 3','Nível 50 → 70','~XX–YY horas','🔥','#f9a826','Mid-Game Eficiente — Zonas de Farming'],['phase-4','FASE 4','Nível 70 → 85','~XX–YY horas','⚔️','#b06cff','Pré-Endgame — Transições e Dungeons'],['phase-5','FASE FINAL','Nível 85 → 99','~XX–YY horas','🏁','#ff3d5a','Reta Final — Caminho ao 99']];function phase(p,i){var c=p[5],maps='',mobs='',checks='',j;for(j=0;j<(i<2?3:2);j++)maps+='<div class="tl-card tl-map"><div><div class="tl-name">Mapa Fase '+(i+1)+' · '+(j+1)+'</div><div class="tl-muted">mapa_code</div></div><div><div class="tl-level">Nv. '+(i*20+1)+'–'+(i*20+20)+'</div><div class="tl-muted">Rota recomendada</div></div></div>';for(j=0;j<(i===0?2:1);j++)mobs+='<div class="tl-card tl-mob"><div class="tl-mob-main"><div class="tl-mob-icon">'+p[4]+'</div><div class="tl-mob-info"><div class="tl-name">Monstro Fase '+(i+1)+' · '+String.fromCharCode(65+j)+'</div><div class="tl-muted">Substitua pelo monstro real</div></div><div class="tl-level">EXP: ???</div><div>▼</div></div><div class="tl-detail">HP: ??? · Drops: Item 1, Item 2<br><span style="color:'+c+'">Dica:</span> Adicione dicas de combate aqui.</div></div>';var list=['Confirmar rota de leveling','Comprar consumíveis necessários','Atualizar equipamentos principais','Avançar para a próxima fase'];for(j=0;j<list.length;j++)checks+='<div class="tl-card tl-check"><div class="tl-mark"></div><div class="tl-text">'+list[j]+'</div></div>';return '<section id="'+p[0]+'" class="tl-phase" style="--color:'+c+';--soft:'+rgba(c,.08)+';--line:'+rgba(c,.22)+'"><div class="tl-head"><div class="tl-icon">'+p[4]+'</div><div><span class="tl-chip">'+p[1]+'</span><span class="tl-chip">'+p[2]+'</span><span class="tl-chip">⏱ '+p[3]+'</span><h2>'+p[6]+'</h2><p>Substitua com a descrição real da fase, mapas ideais, monstros principais, consumíveis e momento correto de troca de classe.</p></div></div><div class="tl-grid"><div class="tl-box"><div class="tl-box-head">🗺️ Mapas Recomendados</div><div class="tl-box-body">'+maps+'</div></div><div class="tl-box"><div class="tl-box-head">🐉 Monstros-Alvo</div><div class="tl-box-body">'+mobs+'</div></div></div><div class="tl-box"><div class="tl-box-head">✅ Checklist da '+p[1]+'</div><div class="tl-box-body"><div class="tl-checks">'+checks+'</div></div></div><div class="tl-note"></div><div class="tl-footer">'+(i>0?'<button class="tr-btn" data-scroll="'+ph[i-1][0]+'">← '+ph[i-1][1]+'</button>':'<span></span>')+(i<ph.length-1?'<button class="tr-btn active" data-scroll="'+ph[i+1][0]+'">Próxima Fase: '+ph[i+1][2]+' →</button>':'<button class="tr-btn active" data-scroll="timero-leveling-guide-app">Voltar ao topo ↑</button>')+'</div></section>';}
      api: root.getAttribute('data-api') || '/pt/api/wiki_cards.php',
    root.className+=' tr-app';var filters='',nav='',blocks='',i;for(i=0;i<cls.length;i++)filters+='<button class="tr-btn'+(i===0?' active':'')+'" data-class="'+cls[i][0]+'">'+cls[i][2]+' '+cls[i][1]+'</button>';for(i=0;i<ph.length;i++){nav+='<button class="tr-btn'+(i===0?' active':'')+'" data-scroll="'+ph[i][0]+'">'+ph[i][1]+' · '+ph[i][2]+'</button>';blocks+=phase(ph[i],i);}root.innerHTML='<div class="tl-root"><div class="tl-inner"><header class="tl-hero"><div class="tr-kicker">// Guia de Progressão</div><h1>Guia de Leveling<br><span>para Iniciantes</span></h1><p class="tr-sub">Do nível 1 ao 99 — rotas otimizadas, mapas ideais por fase, dicas de consumíveis e estratégias para cada etapa da progressão no TimeRO.</p></header><div class="tl-stats"><div class="tl-stat"><strong>5</strong><span>Fases</span></div><div class="tl-stat"><strong>10+</strong><span>Mapas</span></div><div class="tl-stat"><strong>98</strong><span>Níveis</span></div><div class="tl-stat"><strong>Solo</strong><span>Estilo</span></div><div class="tl-stat"><strong>Pre-R</strong><span>Modalidade</span></div></div><div class="tl-panel"><div class="tl-label">Filtrar por classe</div><div class="tl-flex">'+filters+'</div></div><div class="tl-panel"><div class="tl-label">Progressão de Níveis</div><div class="tl-track"><span></span><span></span><span></span><span></span><span></span></div><div class="tl-flex">'+nav+'</div></div>'+blocks+'<div class="tl-related"><div class="tl-panel">🧰<b>Random Options</b></div><div class="tl-panel">🏰<b>Instances</b></div><div class="tl-panel">🔥<b>Fever System</b></div><div class="tl-panel">⏱️<b>MVP Timer</b></div></div></div></div>';var sc=root.querySelectorAll('[data-scroll]');for(i=0;i<sc.length;i++)sc[i].onclick=function(){var t=document.getElementById(this.getAttribute('data-scroll'));if(t)t.scrollIntoView({behavior:'smooth',block:'start'});};var mb=root.querySelectorAll('.tl-mob');for(i=0;i<mb.length;i++)mb[i].onclick=function(){this.className=this.className.indexOf('open')<0?this.className+' open':this.className.replace(' open','');};var ch=root.querySelectorAll('.tl-check');for(i=0;i<ch.length;i++)ch[i].onclick=function(){var on=this.className.indexOf('checked')<0,m=this.querySelector('.tl-mark');this.className=on?this.className+' checked':this.className.replace(' checked','');if(m)m.innerHTML=on?'✓':'';};var cb=root.querySelectorAll('[data-class]');for(i=0;i<cb.length;i++)cb[i].onclick=function(){var v=this.getAttribute('data-class'),j;for(j=0;j<cb.length;j++){cb[j].className='tr-btn'+(cb[j]===this?' active':'');cb[j].style.opacity=(v!=='all'&&cb[j]!==this)?.45:1;}var nt=root.querySelectorAll('.tl-note');for(j=0;j<nt.length;j++){if(v==='all'||!notes[v]){nt[j].style.display='none';nt[j].innerHTML='';}else{var n=notes[v],color='#58d7ff';for(var k=0;k<cls.length;k++)if(cls[k][0]===v)color=cls[k][3];nt[j].style.display='block';nt[j].style.border='1px solid '+rgba(color,.24);nt[j].style.background=rgba(color,.07);nt[j].innerHTML='<strong style="color:'+color+'">'+n[0]+' '+n[1]+'</strong>'+esc(n[2]);}}};}
      q: '',
      category: 'all',
      changed: false,
      sort: 'card_name',
      dir: 'asc',
      loading: false,
      error: '',
      cards: [],
      groups: []
    };


  function boot(){initCards();initLeveling();}
    function catMeta(id) {
  if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',boot);else boot();
      var i;
  if(window.mw&&mw.hook)mw.hook('wikipage.content').add(boot);
      for (i = 0; i < cats.length; i++) if (cats[i].id === id) return cats[i];
})();
      return cats[0];
    }
    function iconUrl(id) { return 'https://timero.com.br/images/item/icons/' + encodeURIComponent(id) + '.png'; }
    function itemUrl(id) { return 'https://timero.com.br/pt/item?id=' + encodeURIComponent(id); }
    function scriptReadable(v) {
      v = String(v || '');
      if (!v) return '—';
      return v
        .replace(/bonus\s+/gi, '')
        .replace(/;/g, '; ')
        .replace(/bAllStats/gi, 'All Stats')
        .replace(/bMaxHP/gi, 'Max HP')
        .replace(/bMaxSP/gi, 'Max SP')
        .replace(/bBaseAtk/gi, 'ATK')
        .replace(/bAtkRate/gi, 'ATK %')
        .replace(/bMatkRate/gi, 'MATK %')
        .replace(/bMatk/gi, 'MATK')
        .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(/\s+/g, ' ');
    }


/* =======================================
    root.innerHTML = '' +
      '<div class="tc-app tr-app">' +
        '<div class="tr-head tc-page-head">' +
          '<div class="tr-kicker">◇ Banco de Dados</div>' +
          '<h2 class="tr-title">Todas as Cartas Balanceadas</h2>' +
          '<div class="tr-sub">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-top">' +
            '<div class="tc-search-wrap"><span class="tc-search-icon">🔍</span><input id="tc-search" class="tc-search" type="text" placeholder="Buscar carta, ID, monstro, efeito ou coleção..." autocomplete="off"></div>' +
            '<label class="tc-toggle"><input id="tc-changed" type="checkbox"> ⚡ Apenas alteradas</label>' +
            '<div id="tc-count" class="tr-pill tc-count">— cartas</div>' +
          '</div>' +
          '<div id="tc-tabs" class="tc-tabs"></div>' +
        '</div>' +
        '<div class="tc-table">' +
          '<div class="tc-head-row">' +
            '<div class="tc-th tc-sort" data-sort="id">ID ↕</div>' +
            '<div class="tc-th tc-sort" data-sort="name">Carta / Monstro ↕</div>' +
            '<div class="tc-th tc-sort" data-sort="slot">Slot ↕</div>' +
            '<div class="tc-th tc-sort" data-sort="category">Categoria ↕</div>' +
            '<div class="tc-th">Status</div>' +
          '</div>' +
          '<div id="tc-body"><div class="tc-msg">Carregando cartas...</div><


/* =========================================================
   if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot);
  TIMERO WIKI MVP HUNTING GUIDE
  else boot();
  Safe renderer: fixes inline onclick/filter/timer JavaScript on MediaWiki.
   if (window.mw && mw.hook) mw.hook('wikipage.content').add(boot);
  Mount points supported:
  - #timero-mvp-guide-app
  - #mvp-guide-root (legacy page root)
========================================================= */
(function(){
  'use strict';
  function esc(v){return String(v==null?'':v).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#039;');}
  function byId(id){return document.getElementById(id);}
  function addStyle(id,css){if(byId(id))return;var s=document.createElement('style');s.id=id;s.appendChild(document.createTextNode(css));document.head.appendChild(s);}
  function now(){return new Date().getTime();}
  function pad(n){n=Math.floor(n);return n<10?'0'+n:String(n);}
  var bosses=[
    {id:1511,name:'Amon Ra',tier:'S',danger:10,exp:9000000,respawn:60,hp:'1.2M',map:'moc_pryd06',element:'Earth 3',race:'Demi-Human',size:'Large',party:'Party',drops:'Amon Ra Card, Safety Ring, Yggdrasil Berry',tips:'Use fire damage, Pneuma support and avoid standing stacked.'},
    {id:1039,name:'Baphomet',tier:'S',danger:9,exp:8500000,respawn:120,hp:'668k',map:'prt_maze03',element:'Dark 3',race:'Demon',size:'Large',party:'Party',drops:'Baphomet Card, Crescent Scythe, Oridecon',tips:'Bring Shadow resistance, Safety Wall/Pneuma rotation and burst windows.'},
    {id:1086,name:'Golden Thief Bug',tier:'A',danger:8,exp:5200000,respawn:60,hp:'126k',map:'prt_sewb4',element:'Fire 2',race:'Insect',size:'Large',party:'Solo/Party',drops:'GTB Card, Golden Gear, Gold',tips:'Magic immunity card makes it extremely valuable. Track timer carefully.'},
    {id:1150,name:'Moonlight Flower',tier:'A',danger:7,exp:4600000,respawn:60,hp:'120k',map:'pay_dun04',element:'Fire 3',race:'Demon',size:'Medium',party:'Solo+',drops:'Moonlight Flower Card, Long Mace, Fox Tail',tips:'Good early MVP target. Watch clones and keep consumables ready.'},
    {id:1115,name:'Eddga',tier:'B',danger:6,exp:3500000,respawn:120,hp:'152k',map:'pay_fild11',element:'Fire 1',race:'Brute',size:'Large',party:'Solo+',drops:'Eddga Card, Tiger Footskin, Fireblend',tips:'Accessible starter MVP. Water damage and fire armor help.'},
    {id:1087,name:'Orc Hero',tier:'B',danger:6,exp:3300000,respawn:60,hp:'295k',map:'gef_fild03',element:'Earth 2',race:'Demi-Human',size:'Large',party:'Solo+',drops:'Orc Hero Card, Orcish Voucher, Shield',tips:'Solid target for geared players. Bring demi-human reduction.'}
  ];
  var state={tier:'all',sort:'danger'};
  function css(){addStyle('timero-mvp-guide-css',
    '.tm-root,.tm-root *{box-sizing:border-box}.tm-root{margin:-1em -1.5em;min-height:100vh;background:radial-gradient(circle at 18% 0%,rgba(255,61,90,.09),transparent 34%),radial-gradient(circle at 88% 24%,rgba(255,107,0,.07),transparent 30%),linear-gradient(135deg,#050914,#02050e);font-family:Segoe UI,system-ui,sans-serif;color:#e8eef8}.tm-shell{max-width:1280px;margin:0 auto;padding:44px 28px 60px}.tm-hero,.tm-panel,.tm-card{background:linear-gradient(135deg,rgba(8,14,26,.94),rgba(4,8,18,.99));border:1px solid rgba(255,61,90,.20);border-radius:18px;box-shadow:0 0 30px rgba(0,0,0,.34),inset 0 0 24px rgba(255,61,90,.025)}.tm-hero{position:relative;overflow:hidden;padding:34px;margin-bottom:18px}.tm-tape{height:6px;background:repeating-linear-gradient(90deg,#ff3d5a 0,#ff3d5a 32px,#ff6b00 32px,#ff6b00 64px,transparent 64px,transparent 80px);opacity:.75;border-radius:999px;margin-bottom:22px}.tm-kicker{display:inline-flex;padding:5px 14px;border-radius:999px;background:rgba(255,61,90,.10);border:1px solid rgba(255,61,90,.25);color:#ff3d5a;font-weight:900;letter-spacing:.16em;text-transform:uppercase;font-size:.72rem;margin-bottom:16px}.tm-title{font-size:clamp(2.2rem,5vw,4rem);line-height:.96;font-weight:900;color:#fff;letter-spacing:-.035em;margin:0 0 16px}.tm-title span{background:linear-gradient(90deg,#ff3d5a,#ff6b00,#f9c500);-webkit-background-clip:text;background-clip:text;color:transparent}.tm-sub{max-width:850px;color:rgba(180,205,230,.76);line-height:1.72;font-size:1rem}.tm-stats{display:grid;grid-template-columns:repeat(5,1fr);gap:12px;margin:18px 0}.tm-stat{background:rgba(0,0,0,.24);border:1px solid rgba(255,255,255,.08);border-radius:14px;padding:14px;text-align:center}.tm-stat strong{display:block;font-size:1.35rem;color:#ff3d5a;font-weight:900}.tm-stat span{font-size:.72rem;color:rgba(122,144,176,.72);letter-spacing:.12em;text-transform:uppercase;font-weight:800}.tm-panel{padding:18px;margin-bottom:18px}.tm-label{color:#7a90b0;font-weight:900;letter-spacing:.11em;text-transform:uppercase;font-size:.72rem;margin-bottom:10px}.tm-flex{display:flex;gap:9px;flex-wrap:wrap;align-items:center}.tm-btn{border:1px solid rgba(255,255,255,.08);background:rgba(255,255,255,.04);color:rgba(180,205,230,.72);border-radius:999px;padding:9px 14px;cursor:pointer;font-family:inherit;font-weight:900;font-size:.78rem;letter-spacing:.04em}.tm-btn.active{background:linear-gradient(135deg,rgba(255,61,90,.20),rgba(255,61,90,.07));border-color:rgba(255,61,90,.34);color:#ff3d5a}.tm-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:16px}.tm-card{overflow:hidden;border-color:var(--line)}.tm-card-head{padding:18px 20px;display:grid;grid-template-columns:60px 1fr auto;gap:14px;align-items:center;cursor:pointer}.tm-avatar{width:60px;height:60px;border-radius:14px;background:var(--soft);border:1px solid var(--line);display:flex;align-items:center;justify-content:center;font-size:1.7rem}.tm-tier{display:inline-flex;padding:2px 8px;border-radius:5px;background:var(--color);color:#fff;font-size:.60rem;font-weight:900;letter-spacing:.12em;margin-bottom:6px}.tm-name{font-size:1.08rem;font-weight:900;color:#fff}.tm-meta{font-size:.70rem;color:rgba(122,144,176,.72);margin-top:4px}.tm-stars{color:var(--color);white-space:nowrap}.tm-strip{display:grid;grid-template-columns:repeat(4,1fr);border-top:1px solid rgba(255,255,255,.06);border-bottom:1px solid rgba(255,255,255,.06)}.tm-strip div{padding:10px;text-align:center;border-right:1px solid rgba(255,255,255,.05)}.tm-strip b{display:block;color:var(--color);font-size:.82rem}.tm-strip span{font-size:.58rem;letter-spacing:.10em;text-transform:uppercase;color:rgba(122,144,176,.58)}.tm-dossier{display:none;padding:18px 20px;border-top:1px solid rgba(255,255,255,.06)}.tm-card.open .tm-dossier{display:block}.tm-dossier-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px}.tm-box{background:rgba(0,0,0,.24);border:1px solid rgba(255,255,255,.08);border-radius:12px;padding:12px;color:rgba(180,205,230,.78);font-size:.84rem;line-height:1.6}.tm-box b{color:var(--color)}.tm-add{margin-top:12px;width:100%;border-radius:10px;border:1px solid var(--line);background:var(--soft);color:var(--color);font-weight:900;font-family:inherit;padding:10px;cursor:pointer}.tm-timers{display:grid;grid-template-columns:1fr 1fr;gap:14px}.tm-form{display:grid;grid-template-columns:1fr 150px auto auto;gap:10px}.tm-form label{display:block;color:#7a90b0;font-size:.68rem;font-weight:900;letter-spacing:.10em;text-transform:uppercase;margin-bottom:6px}.tm-form input{width:100%;padding:10px 11px;border-radius:10px;background:rgba(0,0,0,.32);border:1px solid rgba(255,255,255,.10);color:#e8eef8;font-family:inherit}.tm-timer-list{display:flex;flex-direction:column;gap:10px}.tm-timer{display:flex;align-items:center;justify-content:space-between;gap:12px;background:rgba(0,0,0,.24);border:1px solid rgba(255,255,255,.08);border-radius:12px;padding:12px}.tm-timer b{color:#fff}.tm-time{color:#00d4ff;font-weight:900;font-family:Courier New,monospace}.tm-ready .tm-time{color:#00ff88}.tm-related{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}.tm-related a{display:block;text-decoration:none;background:rgba(0,0,0,.24);border:1px solid rgba(255,255,255,.08);border-radius:14px;padding:16px;text-align:center;color:rgba(180,205,230,.72)}@media(max-width:980px){.tm-grid,.tm-timers,.tm-form,.tm-stats,.tm-related,.tm-dossier-grid{grid-template-columns:1fr}.tm-shell{padding:26px 14px 44px}.tm-card-head{grid-template-columns:52px 1fr}.tm-stars{grid-column:1/-1;text-align:left}.tm-strip{grid-template-columns:1fr 1fr}}'
  );}
  function color(t){return t==='S'?'#ff3d5a':(t==='A'?'#ff6b00':'#00d4ff');}
  function saveTimers(list){try{localStorage.setItem('timero-mvp-guide-timers',JSON.stringify(list));}catch(e){}}
  function loadTimers(){try{var v=JSON.parse(localStorage.getItem('timero-mvp-guide-timers')||'[]');return v&&v.length?v:[];}catch(e){return [];}}
  function render(root){css();root.className='tm-root';root.innerHTML='<div class="tm-shell"><header class="tm-hero"><div class="tm-tape"></div><div class="tm-kicker">// Hunter Dossier</div><h1 class="tm-title">Guia de Caça<br><span>a MVPs</span></h1><div class="tm-sub">Encontre, filtre, ordene e rastreie bosses do TimeRO. Esta versão remove o JavaScript inline quebrado e reconstrói filtros, dossiês e timers diretamente pelo Common.js.</div></header><div class="tm-stats"><div class="tm-stat"><strong>S/A/B</strong><span>Tiers</span></div><div class="tm-stat"><strong>'+bosses.length+'</strong><span>MVPs Base</span></div><div class="tm-stat"><strong>Drops</strong><span>Loot Raro</span></div><div class="tm-stat"><strong>Timer</strong><span>Respawn</span></div><div class="tm-stat"><strong>Builds</strong><span>Classes</span></div></div><section class="tm-panel"><div class="tm-label">Filtrar e ordenar</div><div class="tm-flex"><button class="tm-btn active" data-tier="all">Todos</button><button class="tm-btn" data-tier="S">💀 S-Tier</button><button class="tm-btn" data-tier="A">🔥 A-Tier</button><button class="tm-btn" data-tier="B">⚡ B-Tier</button><span style="width:1px;height:28px;background:rgba(255,255,255,.08)"></span><button class="tm-btn active" data-sort="danger">Dificuldade</button><button class="tm-btn" data-sort="exp">EXP</button><button class="tm-btn" data-sort="respawn">Respawn</button><div id="tm-count" style="margin-left:auto;color:#7a90b0;font-size:.78rem;font-weight:800"></div></div></section><section class="tm-grid" id="tm-boss-grid"></section><section class="tm-panel"><div class="tm-label">Adicionar MVP manualmente</div><div class="tm-form"><div><label>Nome do MVP</label><input id="tm-manual-name" type="text" placeholder="Ex: Eddga"></div><div><label>Respawn (min)</label><input id="tm-manual-respawn" type="number" placeholder="60" min="1"></div><button class="tm-btn active" id="tm-add-manual" type="button">+ Adicionar</button><button class="tm-btn" id="tm-clear" type="button">Limpar Todos</button></div></section><section class="tm-panel"><div class="tm-label">Timers salvos neste navegador</div><div class="tm-timer-list" id="tm-timer-list"></div></section><section class="tm-panel"><div class="tm-label">Guias relacionados</div><div class="tm-related"><a href="/wiki/Beginner_Leveling_Guide">🌿<br><b>Leveling</b></a><a href="/wiki/Farming_Guide">💰<br><b>Farming</b></a><a href="/wiki/PvP">⚔️<br><b>PvP</b></a><a href="/wiki/MVP_Timer">⏱️<br><b>MVP Timer</b></a></div></section></div>';bind(root);renderBosses(root);renderTimers();if(!window.__timeroMvpGuideTimer)window.__timeroMvpGuideTimer=setInterval(renderTimers,1000);}
  function renderBosses(root){var grid=byId('tm-boss-grid'),list=[],i;for(i=0;i<bosses.length;i++)if(state.tier==='all'||bosses[i].tier===state.tier)list.push(bosses[i]);list.sort(function(a,b){if(state.sort==='exp')return b.exp-a.exp;if(state.sort==='respawn')return a.respawn-b.respawn;return b.danger-a.danger;});var html='',b,c,stars,j;for(i=0;i<list.length;i++){b=list[i];c=color(b.tier);stars='';for(j=0;j<5;j++)stars+=j<Math.ceil(b.danger/2)?'★':'☆';html+='<article class="tm-card" data-boss-id="'+b.id+'" style="--color:'+c+';--soft:'+c+'14;--line:'+c+'44"><div class="tm-card-head"><div class="tm-avatar">💀</div><div><div class="tm-tier">'+b.tier+'-TIER</div><div class="tm-name">'+esc(b.name)+'</div><div class="tm-meta">ID #'+b.id+' · '+esc(b.element)+' · '+esc(b.race)+' · '+esc(b.size)+'</div></div><div class="tm-stars">'+stars+'<br><span style="font-size:.68rem;color:#7a90b0">▼ Dossiê</span></div></div><div class="tm-strip"><div><b>'+esc(b.hp)+'</b><span>HP</span></div><div><b>'+esc(Math.round(b.exp/1000))+'k</b><span>Base EXP</span></div><div><b>'+b.respawn+'min</b><span>Respawn</span></div><div><b>'+esc(b.party)+'</b><span>Recomendado</span></div></div><div class="tm-dossier"><div class="tm-dossier-grid"><div class="tm-box"><b>Localização:</b> '+esc(b.map)+'<br><b>Drops:</b> '+esc(b.drops)+'</div><div class="tm-box"><b>Dica:</b> '+esc(b.tips)+'</div></div><button class="tm-add" data-add-boss="'+b.id+'" type="button">+ Adicionar ao timer ('+b.respawn+'min)</button></div></article>';}
grid.innerHTML=html||'<div class="tm-panel">Nenhum MVP encontrado.</div>';var count=byId('tm-count');if(count)count.innerHTML='Exibindo '+list.length+' MVP(s)';var cards=grid.querySelectorAll('.tm-card-head');for(i=0;i<cards.length;i++)cards[i].onclick=function(){var card=this.parentNode;card.className=card.className.indexOf('open')<0?card.className+' open':card.className.replace(' open','');};var adds=grid.querySelectorAll('[data-add-boss]');for(i=0;i<adds.length;i++)adds[i].onclick=function(e){e.stopPropagation();var id=this.getAttribute('data-add-boss');for(var k=0;k<bosses.length;k++)if(String(bosses[k].id)===String(id)){addTimer(bosses[k].name,bosses[k].respawn);break;}};}
  function addTimer(name,respawn){name=String(name||'').replace(/^\s+|\s+$/g,'');respawn=Number(respawn)||0;if(!name||respawn<=0)return;var list=loadTimers();list.push({name:name,respawn:respawn,deadAt:now(),readyAt:now()+respawn*60000});saveTimers(list);renderTimers();}
  function renderTimers(){var wrap=byId('tm-timer-list');if(!wrap)return;var list=loadTimers(),html='',i,t,remain,h,m,s,ready;if(!list.length){wrap.innerHTML='<div class="tm-box">Nenhum timer ativo. Adicione um MVP pelo dossiê ou pelo formulário manual.</div>';return;}for(i=0;i<list.length;i++){t=list[i];remain=Math.max(0,t.readyAt-now());ready=remain<=0;h=Math.floor(remain/3600000);m=Math.floor((remain%3600000)/60000);s=Math.floor((remain%60000)/1000);html+='<div class="tm-timer'+(ready?' tm-ready':'')+'"><div><b>'+esc(t.name)+'</b><div class="tm-muted">Respawn: '+esc(t.respawn)+'min</div></div><div class="tm-time">'+(ready?'READY':(h>0?pad(h)+':':'')+pad(m)+':'+pad(s))+'</div><button class="tm-btn" data-remove-timer="'+i+'" type="button">Remover</button></div>';}wrap.innerHTML=html;var rm=wrap.querySelectorAll('[data-remove-timer]');for(i=0;i<rm.length;i++)rm[i].onclick=function(){var idx=Number(this.getAttribute('data-remove-timer')),l=loadTimers();l.splice(idx,1);saveTimers(l);renderTimers();};}
  function bind(root){var i,tb=root.querySelectorAll('[data-tier]');for(i=0;i<tb.length;i++)tb[i].onclick=function(){state.tier=this.getAttribute('data-tier');var all=root.querySelectorAll('[data-tier]');for(var j=0;j<all.length;j++)all[j].className='tm-btn'+(all[j]===this?' active':'');renderBosses(root);};var sb=root.querySelectorAll('[data-sort]');for(i=0;i<sb.length;i++)sb[i].onclick=function(){state.sort=this.getAttribute('data-sort');var all=root.querySelectorAll('[data-sort]');for(var j=0;j<all.length;j++)all[j].className='tm-btn'+(all[j]===this?' active':'');renderBosses(root);};byId('tm-add-manual').onclick=function(){addTimer((byId('tm-manual-name')||{}).value,(byId('tm-manual-respawn')||{}).value);};byId('tm-clear').onclick=function(){saveTimers([]);renderTimers();};}
  function init(){var root=byId('timero-mvp-guide-app')||byId('mvp-guide-root');if(!root||root.getAttribute('data-timero-mvp-ready')==='1')return;root.setAttribute('data-timero-mvp-ready','1');render(root);}
   if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',init);else init();
   if(window.mw&&mw.hook)mw.hook('wikipage.content').add(init);
})();
})();

Revision as of 18:13, 30 April 2026

/* TimeRO MediaWiki:Common.js
   FULL REVISED LOGIC-ONLY VERSION
   - No CSS injection / no <style> creation
   - Does not overwrite #farming-guide-root with generic Zeny renderer
   - Keeps interactive logic for: Cards, Leveling, Zeny mount, MVP guide, Merchant Ledger farming guide
   - ES5-safe: no const/let, no arrow functions, no template literals
*/
(function () {
  'use strict';

  /* =========================================================
     Shared helpers
  ========================================================= */
  function byId(id) { return document.getElementById(id); }
  function qs(sel, root) { return (root || document).querySelector(sel); }
  function qsa(sel, root) { return Array.prototype.slice.call((root || document).querySelectorAll(sel)); }
  function trim(v) { return String(v == null ? '' : v).replace(/^\s+|\s+$/g, ''); }
  function esc(v) {
    return String(v == null ? '' : v)
      .replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;')
      .replace(/'/g, '&#039;');
  }
  function debounce(fn, ms) {
    var t = null;
    return function () {
      var self = this;
      var args = arguments;
      clearTimeout(t);
      t = setTimeout(function () { fn.apply(self, args); }, ms);
    };
  }
  function ajaxJson(url, ok, fail) {
    var xhr = new XMLHttpRequest();
    xhr.open('GET', url, true);
    xhr.setRequestHeader('Accept', 'application/json');
    xhr.onreadystatechange = function () {
      if (xhr.readyState !== 4) return;
      if (xhr.status < 200 || xhr.status >= 300) {
        fail('HTTP ' + xhr.status + ': ' + String(xhr.responseText || '').substr(0, 260));
        return;
      }
      try {
        ok(JSON.parse(xhr.responseText));
      } catch (e) {
        fail('JSON inválido: ' + String(xhr.responseText || '').substr(0, 260));
      }
    };
    xhr.onerror = function () { fail('Falha de rede.'); };
    xhr.send(null);
  }
  function storageGet(key, fallback) {
    try {
      var raw = localStorage.getItem(key);
      if (raw == null) return fallback;
      return JSON.parse(raw);
    } catch (e) {
      return fallback;
    }
  }
  function storageSet(key, value) {
    try { localStorage.setItem(key, JSON.stringify(value)); } catch (e) {}
  }
  function number(v, fallback) {
    var n = Number(v);
    return isNaN(n) ? (fallback || 0) : n;
  }
  function pad2(n) {
    n = Math.floor(Math.max(0, n));
    return n < 10 ? '0' + n : String(n);
  }
  function nowMs() { return new Date().getTime(); }
  function classOn(el, cls, on) {
    if (!el) return;
    if (el.classList) {
      el.classList.toggle(cls, !!on);
      return;
    }
    var c = ' ' + el.className + ' ';
    if (on && c.indexOf(' ' + cls + ' ') < 0) el.className = trim(el.className + ' ' + cls);
    if (!on) el.className = trim(c.replace(' ' + cls + ' ', ' '));
  }

  /* =========================================================
     CARD DATABASE APP
     Mount point: #timero-card-db-app
     CSS must live in the wiki page or MediaWiki:Common.css.
  ========================================================= */
  function initCardDatabase() {
    var root = byId('timero-card-db-app');
    if (!root || root.getAttribute('data-timero-card-ready') === '1') return;
    root.setAttribute('data-timero-card-ready', '1');

    var cats = [
      { 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' }
    ];

    var state = {
      api: root.getAttribute('data-api') || '/pt/api/wiki_cards.php',
      q: '',
      category: 'all',
      changed: false,
      sort: 'card_name',
      dir: 'asc',
      loading: false,
      error: '',
      cards: [],
      groups: []
    };

    function catMeta(id) {
      var i;
      for (i = 0; i < cats.length; i++) if (cats[i].id === id) return cats[i];
      return cats[0];
    }
    function iconUrl(id) { return 'https://timero.com.br/images/item/icons/' + encodeURIComponent(id) + '.png'; }
    function itemUrl(id) { return 'https://timero.com.br/pt/item?id=' + encodeURIComponent(id); }
    function scriptReadable(v) {
      v = String(v || '');
      if (!v) return '—';
      return v
        .replace(/bonus\s+/gi, '')
        .replace(/;/g, '; ')
        .replace(/bAllStats/gi, 'All Stats')
        .replace(/bMaxHP/gi, 'Max HP')
        .replace(/bMaxSP/gi, 'Max SP')
        .replace(/bBaseAtk/gi, 'ATK')
        .replace(/bAtkRate/gi, 'ATK %')
        .replace(/bMatkRate/gi, 'MATK %')
        .replace(/bMatk/gi, 'MATK')
        .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(/\s+/g, ' ');
    }

    root.innerHTML = '' +
      '<div class="tc-app tr-app">' +
        '<div class="tr-head tc-page-head">' +
          '<div class="tr-kicker">◇ Banco de Dados</div>' +
          '<h2 class="tr-title">Todas as Cartas Balanceadas</h2>' +
          '<div class="tr-sub">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-top">' +
            '<div class="tc-search-wrap"><span class="tc-search-icon">🔍</span><input id="tc-search" class="tc-search" type="text" placeholder="Buscar carta, ID, monstro, efeito ou coleção..." autocomplete="off"></div>' +
            '<label class="tc-toggle"><input id="tc-changed" type="checkbox"> ⚡ Apenas alteradas</label>' +
            '<div id="tc-count" class="tr-pill tc-count">— cartas</div>' +
          '</div>' +
          '<div id="tc-tabs" class="tc-tabs"></div>' +
        '</div>' +
        '<div class="tc-table">' +
          '<div class="tc-head-row">' +
            '<div class="tc-th tc-sort" data-sort="id">ID ↕</div>' +
            '<div class="tc-th tc-sort" data-sort="name">Carta / Monstro ↕</div>' +
            '<div class="tc-th tc-sort" data-sort="slot">Slot ↕</div>' +
            '<div class="tc-th tc-sort" data-sort="category">Categoria ↕</div>' +
            '<div class="tc-th">Status</div>' +
          '</div>' +
          '<div id="tc-body"><div class="tc-msg">Carregando cartas...</div></div>' +
        '</div>' +
        '<div class="tc-groups">' +
          '<div class="tr-head tc-groups-head">' +
            '<div class="tr-kicker">◇ Grupos de Coleção</div>' +
            '<h2 class="tr-title">Bônus permanentes por grupo</h2>' +
            '<div class="tr-sub">Dados carregados da tabela wiki_card_groups.</div>' +
          '</div>' +
          '<div id="tc-groups"></div>' +
        '</div>' +
      '</div>';

    var tabs = byId('tc-tabs');
    var i;
    for (i = 0; i < cats.length; i++) {
      (function (cat) {
        var b = document.createElement('button');
        b.type = 'button';
        b.className = 'tr-btn tc-tab' + (cat.id === state.category ? ' active' : '');
        b.setAttribute('data-category', cat.id);
        b.innerHTML = esc(cat.icon) + ' ' + esc(cat.label);
        b.onclick = function () {
          state.category = cat.id;
          fetchCards();
        };
        tabs.appendChild(b);
      })(cats[i]);
    }

    qs('#tc-search', root).oninput = debounce(function () {
      state.q = this.value || '';
      fetchCards();
    }, 220);
    qs('#tc-changed', root).onchange = function () {
      state.changed = !!this.checked;
      fetchCards();
    };
    qsa('.tc-sort', root).forEach(function (th) {
      th.onclick = function () {
        var s = this.getAttribute('data-sort');
        if (state.sort === s) state.dir = state.dir === 'asc' ? 'desc' : 'asc';
        else { state.sort = s; state.dir = 'asc'; }
        fetchCards();
      };
    });

    function updateTabs() {
      qsa('.tc-tab', root).forEach(function (b) {
        classOn(b, 'active', b.getAttribute('data-category') === state.category);
      });
    }
    function fetchCards() {
      updateTabs();
      state.loading = true;
      state.error = '';
      renderCards();
      var params = 'q=' + encodeURIComponent(state.q) +
        '&category=' + encodeURIComponent(state.category) +
        '&changed=' + (state.changed ? '1' : '0') +
        '&sort=' + encodeURIComponent(state.sort) +
        '&dir=' + encodeURIComponent(state.dir);
      ajaxJson(state.api + (state.api.indexOf('?') === -1 ? '?' : '&') + params, function (data) {
        state.loading = false;
        if (!data || !data.ok) {
          state.error = (data && data.error) || 'Erro desconhecido.';
          state.cards = [];
          state.groups = [];
        } else {
          state.error = '';
          state.cards = data.cards || [];
          state.groups = data.groups || [];
        }
        renderCards();
        renderGroups();
      }, function (err) {
        state.loading = false;
        state.error = err;
        state.cards = [];
        state.groups = [];
        renderCards();
        renderGroups();
      });
    }
    function renderCards() {
      var body = qs('#tc-body', root);
      var count = qs('#tc-count', root);
      if (count) count.innerHTML = state.cards.length + ' carta' + (state.cards.length === 1 ? '' : 's');
      if (state.loading) { body.innerHTML = '<div class="tc-msg">Carregando cartas...</div>'; return; }
      if (state.error) { body.innerHTML = '<div class="tc-msg tc-error">Erro: ' + esc(state.error) + '</div>'; return; }
      if (!state.cards.length) { body.innerHTML = '<div class="tc-msg">Nenhuma carta encontrada.</div>'; return; }
      body.innerHTML = '';
      qsa('.tc-detail.open', body).forEach(function (d) { classOn(d, 'open', false); });
      state.cards.forEach(function (card) {
        var meta = catMeta(card.category);
        var row = document.createElement('div');
        var detail = document.createElement('div');
        row.className = 'tc-row';
        row.innerHTML = '' +
          '<div class="tc-cell tc-id">#' + esc(card.card_id) + '</div>' +
          '<div class="tc-cell"><div class="tc-name"><div class="tc-icon"><img src="' + iconUrl(card.card_id) + '" alt="' + esc(card.card_name) + '"></div><div><div class="tc-card-title"><a href="' + itemUrl(card.card_id) + '" target="_blank" rel="noopener">' + esc(card.card_name) + '</a></div>' + (card.monster_name ? '<div class="tc-small">' + esc(card.monster_name) + '</div>' : '') + '</div></div></div>' +
          '<div class="tc-cell"><span class="tr-pill tc-pill">' + esc(card.slot_type || 'Unknown') + '</span></div>' +
          '<div class="tc-cell"><span class="tr-pill tc-pill tc-category-pill" data-color="' + esc(meta.color) + '">' + esc(meta.icon) + ' ' + esc(card.collection_group_name || '—') + '</span></div>' +
          '<div class="tc-cell">' + (String(card.changed) === '1' ? '<span class="tc-status tc-changed">Alterada</span>' : '<span class="tc-status tc-original">Original</span>') + '</div>';
        var img = qs('img', row);
        if (img) img.onerror = function () { this.parentNode.innerHTML = '🃏'; };
        detail.className = 'tc-detail';
        detail.innerHTML = '' +
          '<div class="tc-detail-grid">' +
            '<div class="tc-box"><h4>Efeito Antigo</h4><div>' + esc(card.old_effect || '—') + '</div></div>' +
            '<div class="tc-box tc-box-new"><h4>Efeito Novo</h4><div>' + esc(card.new_effect || '—') + '</div></div>' +
            '<div class="tc-box tc-box-collection"><h4>Coleção</h4><div><b>Grupo:</b> ' + esc(card.collection_group_name || '—') + '<br><b>Pontos:</b> ' + esc(card.collection_points || 0) + '<br><b>Efeito:</b> ' + esc(card.collection_effect || '—') + '<br><b>Script:</b> ' + esc(scriptReadable(card.collection_script)) + '</div></div>' +
          '</div>';
        row.onclick = function () {
          var isOpen = detail.className.indexOf('open') !== -1;
          qsa('.tc-row.open', body).forEach(function (r) { classOn(r, 'open', false); });
          qsa('.tc-detail.open', body).forEach(function (d) { classOn(d, 'open', false); });
          if (!isOpen) { classOn(row, 'open', true); classOn(detail, 'open', true); }
        };
        body.appendChild(row);
        body.appendChild(detail);
      });
    }
    function renderGroups() {
      var wrap = qs('#tc-groups', root);
      if (!wrap) return;
      if (state.error) { wrap.innerHTML = ''; return; }
      if (!state.groups.length) { wrap.innerHTML = '<div class="tc-msg">Nenhum grupo de coleção cadastrado.</div>'; return; }
      wrap.innerHTML = '';
      state.groups.forEach(function (g) {
        var el = document.createElement('div');
        el.className = 'tc-group';
        el.innerHTML = '' +
          '<div class="tc-group-main">' +
            '<div class="tc-group-icon">' + esc(g.icon || '🃏') + '</div>' +
            '<div><div class="tc-group-key">Grupo · ' + esc(g.group_key || '') + '</div><div class="tc-group-name">' + esc(g.group_name || '') + '</div></div>' +
            '<div class="tr-pill tc-pill">' + esc(g.card_count || 0) + ' cartas · Min ' + esc(g.min_cards || 1) + '</div>' +
          '</div>' +
          '<div class="tc-group-bonus"><b>Bônus:</b> ' + esc(g.collection_effect || '—') + '</div>';
        wrap.appendChild(el);
      });
    }
    fetchCards();
  }

  /* =========================================================
     BEGINNER LEVELING GUIDE APP
     Mount point: #timero-leveling-guide-app
     CSS must live in the wiki page or MediaWiki:Common.css.
  ========================================================= */
  function initLevelingGuide() {
    var root = byId('timero-leveling-guide-app');
    if (!root || root.getAttribute('data-timero-leveling-ready') === '1') return;
    root.setAttribute('data-timero-leveling-ready', '1');

    var classes = [
      { id: 'all', label: 'Todas', icon: '◆', color: '#58d7ff' },
      { id: 'swordsman', label: 'Espadachim', icon: '⚔️', color: '#ff6b7a', note: 'Priorize sobrevivência e dano consistente. Builds STR/VIT ou STR/AGI evoluem com menos risco.' },
      { id: 'mage', label: 'Mago', icon: '🔮', color: '#b06cff', note: 'Controle distância, elemento e SP. Escolha monstros vulneráveis ao elemento correto.' },
      { id: 'archer', label: 'Arqueiro', icon: '🏹', color: '#00ff88', note: 'Use vantagem de alcance e kite. DEX acelera o dano e reduz tempo morto.' },
      { id: 'acolyte', label: 'Acolyte', icon: '✨', color: '#f9a826', note: 'Acolytes podem alternar entre suporte e dano sagrado. Heal contra Undead é eficiente em rotas específicas.' },
      { id: 'thief', label: 'Ladrão', icon: '🗡️', color: '#7a90b0', note: 'AGI e evasão tornam a progressão segura. Double Attack ajuda em mapas leves e rápidos.' }
    ];
    var phases = [
      { id: 'phase-1', phase: 'FASE 1', range: 'Nível 1 → 25', time: '~XX–YY minutos', icon: '🌿', color: '#00ff88', title: 'Primeiros Passos — Poring Island & Prontera Field' },
      { id: 'phase-2', phase: 'FASE 2', range: 'Nível 25 → 50', time: '~XX–YY horas', icon: '⚡', color: '#00d4ff', title: 'Construindo Momentum — Mapas Intermediários' },
      { id: 'phase-3', phase: 'FASE 3', range: 'Nível 50 → 70', time: '~XX–YY horas', icon: '🔥', color: '#f9a826', title: 'Mid-Game Eficiente — Zonas de Farming' },
      { id: 'phase-4', phase: 'FASE 4', range: 'Nível 70 → 85', time: '~XX–YY horas', icon: '⚔️', color: '#b06cff', title: 'Pré-Endgame — Transições e Dungeons' },
      { id: 'phase-5', phase: 'FASE FINAL', range: 'Nível 85 → 99', time: '~XX–YY horas', icon: '🏁', color: '#ff3d5a', title: 'Reta Final — Caminho ao 99' }
    ];

    function phaseBlock(p, idx) {
      var maps = '';
      var mobs = '';
      var checks = '';
      var i;
      for (i = 0; i < (idx < 2 ? 3 : 2); i++) {
        maps += '<div class="tl-card tl-map"><div><div class="tl-name">Mapa Fase ' + (idx + 1) + ' · ' + (i + 1) + '</div><div class="tl-muted">mapa_code</div></div><div><div class="tl-level">Nv. ' + (idx * 20 + 1) + '–' + (idx * 20 + 20) + '</div><div class="tl-muted">Rota recomendada</div></div></div>';
      }
      for (i = 0; i < (idx === 0 ? 2 : 1); i++) {
        mobs += '<div class="tl-card tl-mob"><div class="tl-mob-main"><div class="tl-mob-icon">' + esc(p.icon) + '</div><div class="tl-mob-info"><div class="tl-name">Monstro Fase ' + (idx + 1) + ' · ' + String.fromCharCode(65 + i) + '</div><div class="tl-muted">Substitua pelo monstro real</div></div><div class="tl-level">EXP: ???</div><div class="tl-arrow">▼</div></div><div class="tl-detail">HP: ??? · Drops: Item 1, Item 2<br><span>Dica:</span> Adicione dicas de combate aqui.</div></div>';
      }
      var list = ['Confirmar rota de leveling', 'Comprar consumíveis necessários', 'Atualizar equipamentos principais', 'Avançar para a próxima fase'];
      for (i = 0; i < list.length; i++) {
        checks += '<div class="tl-card tl-check" data-check-key="' + esc(p.id) + '-' + i + '"><div class="tl-mark"></div><div class="tl-text">' + esc(list[i]) + '</div></div>';
      }
      return '<section id="' + esc(p.id) + '" class="tl-phase" data-phase-color="' + esc(p.color) + '"><div class="tl-head"><div class="tl-icon">' + esc(p.icon) + '</div><div><span class="tl-chip">' + esc(p.phase) + '</span><span class="tl-chip">' + esc(p.range) + '</span><span class="tl-chip">⏱ ' + esc(p.time) + '</span><h2>' + esc(p.title) + '</h2><p>Substitua com a descrição real da fase, mapas ideais, monstros principais, consumíveis e momento correto de troca de classe.</p></div></div><div class="tl-grid"><div class="tl-box"><div class="tl-box-head">🗺️ Mapas Recomendados</div><div class="tl-box-body">' + maps + '</div></div><div class="tl-box"><div class="tl-box-head">🐉 Monstros-Alvo</div><div class="tl-box-body">' + mobs + '</div></div></div><div class="tl-box"><div class="tl-box-head">✅ Checklist da ' + esc(p.phase) + '</div><div class="tl-box-body"><div class="tl-checks">' + checks + '</div></div></div><div class="tl-note" data-class-note></div><div class="tl-footer">' + (idx > 0 ? '<button class="tr-btn" type="button" data-scroll="' + esc(phases[idx - 1].id) + '">← ' + esc(phases[idx - 1].phase) + '</button>' : '<span></span>') + (idx < phases.length - 1 ? '<button class="tr-btn active" type="button" data-scroll="' + esc(phases[idx + 1].id) + '">Próxima Fase: ' + esc(phases[idx + 1].range) + ' →</button>' : '<button class="tr-btn active" type="button" data-scroll="timero-leveling-guide-app">Voltar ao topo ↑</button>') + '</div></section>';
    }

    var filters = '';
    var nav = '';
    var blocks = '';
    var i;
    for (i = 0; i < classes.length; i++) filters += '<button class="tr-btn' + (i === 0 ? ' active' : '') + '" type="button" data-class-filter="' + esc(classes[i].id) + '">' + esc(classes[i].icon) + ' ' + esc(classes[i].label) + '</button>';
    for (i = 0; i < phases.length; i++) { nav += '<button class="tr-btn' + (i === 0 ? ' active' : '') + '" type="button" data-scroll="' + esc(phases[i].id) + '" data-phase-nav="' + esc(phases[i].id) + '">' + esc(phases[i].phase) + ' · ' + esc(phases[i].range) + '</button>'; blocks += phaseBlock(phases[i], i); }

    root.innerHTML = '<div class="tl-root tr-app"><div class="tl-inner"><header class="tl-hero"><div class="tr-kicker">// Guia de Progressão</div><h1>Guia de Leveling<br><span>para Iniciantes</span></h1><p class="tr-sub">Do nível 1 ao 99 — rotas otimizadas, mapas ideais por fase, dicas de consumíveis e estratégias para cada etapa da progressão no TimeRO.</p></header><div class="tl-stats"><div class="tl-stat"><strong>5</strong><span>Fases</span></div><div class="tl-stat"><strong>10+</strong><span>Mapas</span></div><div class="tl-stat"><strong>98</strong><span>Níveis</span></div><div class="tl-stat"><strong>Solo</strong><span>Estilo</span></div><div class="tl-stat"><strong>Pre-R</strong><span>Modalidade</span></div></div><div class="tl-panel"><div class="tl-label">Filtrar por classe</div><div class="tl-flex">' + filters + '</div></div><div class="tl-panel"><div class="tl-label">Progressão de Níveis</div><div class="tl-track"><span></span><span></span><span></span><span></span><span></span></div><div class="tl-flex">' + nav + '</div></div>' + blocks + '<div class="tl-related"><div class="tl-panel">🧰<b>Random Options</b></div><div class="tl-panel">🏰<b>Instances</b></div><div class="tl-panel">🔥<b>Fever System</b></div><div class="tl-panel">⏱️<b>MVP Timer</b></div></div></div></div>';

    qsa('[data-scroll]', root).forEach(function (btn) {
      btn.onclick = function () {
        var t = byId(btn.getAttribute('data-scroll'));
        if (t) t.scrollIntoView({ behavior: 'smooth', block: 'start' });
      };
    });
    qsa('.tl-mob', root).forEach(function (m) {
      m.onclick = function () {
        var open = m.className.indexOf('open') === -1;
        classOn(m, 'open', open);
        var arrow = qs('.tl-arrow', m);
        if (arrow) arrow.innerHTML = open ? '▲' : '▼';
      };
    });
    qsa('.tl-check', root).forEach(function (c) {
      var key = 'timero-leveling-check-' + c.getAttribute('data-check-key');
      var initial = storageGet(key, false) === true;
      setCheck(c, initial);
      c.onclick = function () {
        var checked = c.className.indexOf('checked') === -1;
        setCheck(c, checked);
        storageSet(key, checked);
      };
    });
    function setCheck(el, checked) {
      classOn(el, 'checked', checked);
      var mark = qs('.tl-mark', el);
      if (mark) mark.innerHTML = checked ? '✓' : '';
    }
    qsa('[data-class-filter]', root).forEach(function (btn) {
      btn.onclick = function () {
        var id = btn.getAttribute('data-class-filter');
        qsa('[data-class-filter]', root).forEach(function (b) { classOn(b, 'active', b === btn); });
        var data = null;
        var i;
        for (i = 0; i < classes.length; i++) if (classes[i].id === id) data = classes[i];
        qsa('[data-class-note]', root).forEach(function (note) {
          if (!data || id === 'all') { note.style.display = 'none'; note.innerHTML = ''; }
          else { note.style.display = 'block'; note.innerHTML = '<strong>' + esc(data.icon) + ' Dica: ' + esc(data.label) + '</strong>' + esc(data.note); }
        });
      };
    });
  }

  /* =========================================================
     ZENY GUIDE MOUNT APP
     Mount point: #timero-zeny-guide-app ONLY.
     IMPORTANT: this intentionally does NOT bind #farming-guide-root.
     The original Merchant Ledger page uses #farming-guide-root and has its own binder below.
  ========================================================= */
  function initZenyGuideMount() {
    var root = byId('timero-zeny-guide-app');
    if (!root || root.getAttribute('data-timero-zeny-ready') === '1') return;
    root.setAttribute('data-timero-zeny-ready', '1');

    var methods = [
      { id: 'grind', icon: '⚔️', name: 'Grind Ativo', desc: 'Matar monstros, coletar drops e vender no mercado.', pros: ['Lucro imediato', 'Ganha EXP junto', 'Começa com pouco capital'], cons: ['Requer atenção ativa', 'Consome poções', 'Varia conforme drops'] },
      { id: 'market', icon: '🏪', name: 'Mercado', desc: 'Comprar itens subvalorizados e revender com margem.', pros: ['Escala com capital', 'Pode ser semi-passivo', 'Lucro alto em oportunidades'], cons: ['Precisa conhecer preços', 'Risco de encalhe', 'Requer capital inicial'] },
      { id: 'passive', icon: '💤', name: 'Renda Passiva', desc: 'Lojas, produção e rotinas repetíveis.', pros: ['Combina com alt chars', 'Baixo estresse', 'Bom para rotina diária'], cons: ['Lucro mais lento', 'Depende da demanda', 'Exige organização'] }
    ];
    var routes = [
      { method: 'grind', name: 'Payon Cave', range: 'Lv. 25–45', profit: 'Baixo → Médio', risk: 'Baixo', items: 'Drops simples, cartas iniciais, consumíveis', note: 'Rota segura para começar.' },
      { method: 'grind', name: 'Orc Dungeon / Orc Field', range: 'Lv. 45–70', profit: 'Médio', risk: 'Médio', items: 'Oridecon, Elunium, itens de NPC, cartas úteis', note: 'Bom equilíbrio entre EXP e Zeny.' },
      { method: 'grind', name: 'Geffênia', range: 'Lv. 80+', profit: 'Alto', risk: 'Alto', items: 'Equipamentos, cartas, drops valiosos', note: 'Rota clássica para personagens com gear.' },
      { method: 'market', name: 'Compra Subvalorizada', range: 'Qualquer', profit: 'Variável', risk: 'Médio', items: 'Cartas, minérios, consumíveis', note: 'Compre abaixo da média e revenda.' },
      { method: 'passive', name: 'Loja de Revenda', range: 'Qualquer', profit: 'Médio', risk: 'Baixo', items: 'Consumíveis, flechas, materiais', note: 'Mantenha loja aberta em pontos de tráfego.' }
    ];

    function money(n) {
      n = Math.round(number(n, 0));
      if (n >= 1000000) return (n / 1000000).toFixed(2) + 'M z';
      if (n >= 1000) return (n / 1000).toFixed(1) + 'k z';
      return n + ' z';
    }
    function methodById(id) {
      var i;
      for (i = 0; i < methods.length; i++) if (methods[i].id === id) return methods[i];
      return methods[0];
    }
    function renderMethod(id) {
      var m = methodById(id);
      var i;
      var html = '<div class="tz-card"><div class="tz-card-head"><div class="tz-icon">' + esc(m.icon) + '</div><div><h2>' + esc(m.name) + '</h2><div class="tz-muted">Método selecionado</div></div></div><p>' + esc(m.desc) + '</p><div class="tz-two"><div class="tz-mini"><b>Vantagens</b><ul>';
      for (i = 0; i < m.pros.length; i++) html += '<li>' + esc(m.pros[i]) + '</li>';
      html += '</ul></div><div class="tz-mini"><b>Cuidados</b><ul>';
      for (i = 0; i < m.cons.length; i++) html += '<li>' + esc(m.cons[i]) + '</li>';
      html += '</ul></div></div></div>';
      qs('#tz-method-panel', root).innerHTML = html;
      qsa('[data-tz-method]', root).forEach(function (b) { classOn(b, 'active', b.getAttribute('data-tz-method') === id); });
      renderRoutes(id);
    }
    function renderRoutes(id) {
      var wrap = qs('#tz-routes', root);
      var html = '';
      routes.forEach(function (r) {
        if (r.method !== id) return;
        html += '<div class="tz-route"><div class="tz-route-name">' + esc(r.name) + '</div><div class="tz-route-meta">' + esc(r.range) + ' · ' + esc(r.profit) + '</div><div class="tz-muted"><b>Risco:</b> ' + esc(r.risk) + '<br><b>Itens:</b> ' + esc(r.items) + '<br>' + esc(r.note) + '</div></div>';
      });
      wrap.innerHTML = html || '<div class="tz-muted">Nenhuma rota para este método.</div>';
    }
    function calc() {
      var kills = number(qs('#tz-kills', root).value, 0);
      var val = number(qs('#tz-value', root).value, 0);
      var drop = number(qs('#tz-drop', root).value, 0);
      var cost = number(qs('#tz-cost', root).value, 0);
      qs('#tz-result', root).innerHTML = 'Lucro estimado: ' + money((kills * val * (drop / 100)) - cost) + ' / hora';
    }

    var tabs = '';
    var i;
    for (i = 0; i < methods.length; i++) tabs += '<button class="tz-btn' + (i === 0 ? ' active' : '') + '" type="button" data-tz-method="' + esc(methods[i].id) + '">' + esc(methods[i].icon) + ' ' + esc(methods[i].name) + '</button>';
    root.innerHTML = '<div class="tz-root"><div class="tz-shell"><header class="tz-hero"><div class="tz-kicker">// Economia TimeRO</div><h1 class="tz-title">Guia de Farming<br><span>de Zeny</span></h1><div class="tz-sub">Métodos, rotas, calculadora de lucro e estratégias práticas para acumular Zeny.</div></header><section class="tz-panel"><div class="tz-label">Escolha sua abordagem</div><div class="tz-tabs">' + tabs + '</div></section><section class="tz-grid"><div id="tz-method-panel"></div><div><div class="tz-card"><div class="tz-label">Calculadora rápida</div><div class="tz-calc"><div class="tz-field"><label>Kills / hora</label><input id="tz-kills" type="number" value="700" min="0"></div><div class="tz-field"><label>Valor item</label><input id="tz-value" type="number" value="2500" min="0"></div><div class="tz-field"><label>Drop %</label><input id="tz-drop" type="number" value="25" min="0" max="100"></div><div class="tz-field"><label>Custo/h</label><input id="tz-cost" type="number" value="150000" min="0"></div></div><div class="tz-result" id="tz-result">—</div></div><div class="tz-card"><div class="tz-label">Rotas recomendadas</div><div class="tz-routes" id="tz-routes"></div></div></div></section></div></div>';
    qsa('[data-tz-method]', root).forEach(function (b) { b.onclick = function () { renderMethod(b.getAttribute('data-tz-method')); }; });
    qsa('.tz-field input', root).forEach(function (input) { input.oninput = calc; });
    renderMethod('grind');
    calc();
  }

  /* =========================================================
     MVP GUIDE APP
     Mount points: #timero-mvp-guide-app or #mvp-guide-root
     CSS must live in the wiki page or MediaWiki:Common.css.
  ========================================================= */
  function initMvpGuide() {
    var root = byId('timero-mvp-guide-app') || byId('mvp-guide-root');
    if (!root || root.getAttribute('data-timero-mvp-ready') === '1') return;
    root.setAttribute('data-timero-mvp-ready', '1');

    var bosses = [
      { id: 1511, name: 'Amon Ra', tier: 'S', danger: 10, exp: 9000000, respawn: 60, hp: '1.2M', map: 'moc_pryd06', element: 'Earth 3', race: 'Demi-Human', size: 'Large', party: 'Party', drops: 'Amon Ra Card, Safety Ring, Yggdrasil Berry', tips: 'Use fire damage, Pneuma support and avoid standing stacked.' },
      { id: 1039, name: 'Baphomet', tier: 'S', danger: 9, exp: 8500000, respawn: 120, hp: '668k', map: 'prt_maze03', element: 'Dark 3', race: 'Demon', size: 'Large', party: 'Party', drops: 'Baphomet Card, Crescent Scythe, Oridecon', tips: 'Bring Shadow resistance, Safety Wall/Pneuma rotation and burst windows.' },
      { id: 1086, name: 'Golden Thief Bug', tier: 'A', danger: 8, exp: 5200000, respawn: 60, hp: '126k', map: 'prt_sewb4', element: 'Fire 2', race: 'Insect', size: 'Large', party: 'Solo/Party', drops: 'GTB Card, Golden Gear, Gold', tips: 'Magic immunity card makes it extremely valuable. Track timer carefully.' },
      { id: 1150, name: 'Moonlight Flower', tier: 'A', danger: 7, exp: 4600000, respawn: 60, hp: '120k', map: 'pay_dun04', element: 'Fire 3', race: 'Demon', size: 'Medium', party: 'Solo+', drops: 'Moonlight Flower Card, Long Mace, Fox Tail', tips: 'Good early MVP target. Watch clones and keep consumables ready.' },
      { id: 1115, name: 'Eddga', tier: 'B', danger: 6, exp: 3500000, respawn: 120, hp: '152k', map: 'pay_fild11', element: 'Fire 1', race: 'Brute', size: 'Large', party: 'Solo+', drops: 'Eddga Card, Tiger Footskin, Fireblend', tips: 'Accessible starter MVP. Water damage and fire armor help.' },
      { id: 1087, name: 'Orc Hero', tier: 'B', danger: 6, exp: 3300000, respawn: 60, hp: '295k', map: 'gef_fild03', element: 'Earth 2', race: 'Demi-Human', size: 'Large', party: 'Solo+', drops: 'Orc Hero Card, Orcish Voucher, Shield', tips: 'Solid target for geared players. Bring demi-human reduction.' }
    ];
    var state = { tier: 'all', sort: 'danger' };
    var TIMER_KEY = 'timero-mvp-guide-timers';
    function getTimers() { return storageGet(TIMER_KEY, []); }
    function setTimers(v) { storageSet(TIMER_KEY, v); }
    function addTimer(name, respawn) {
      name = trim(name);
      respawn = number(respawn, 0);
      if (!name || respawn <= 0) return;
      var list = getTimers();
      list.push({ name: name, respawn: respawn, deadAt: nowMs(), readyAt: nowMs() + respawn * 60000 });
      setTimers(list);
      renderTimers();
    }
    function removeTimer(idx) {
      var list = getTimers();
      list.splice(idx, 1);
      setTimers(list);
      renderTimers();
    }
    function renderTimers() {
      var wrap = qs('#tm-timer-list', root);
      if (!wrap) return;
      var list = getTimers();
      var html = '';
      if (!list.length) {
        wrap.innerHTML = '<div class="tm-box">Nenhum timer ativo. Adicione um MVP pelo dossiê ou pelo formulário manual.</div>';
        return;
      }
      list.forEach(function (t, i) {
        var remain = Math.max(0, number(t.readyAt, nowMs()) - nowMs());
        var ready = remain <= 0;
        var h = Math.floor(remain / 3600000);
        var m = Math.floor((remain % 3600000) / 60000);
        var s = Math.floor((remain % 60000) / 1000);
        var time = ready ? 'READY' : (h > 0 ? pad2(h) + ':' : '') + pad2(m) + ':' + pad2(s);
        html += '<div class="tm-timer' + (ready ? ' tm-ready' : '') + '"><div><b>' + esc(t.name) + '</b><div class="tm-muted">Respawn: ' + esc(t.respawn) + 'min</div></div><div class="tm-time">' + esc(time) + '</div><button class="tm-btn" type="button" data-remove-timer="' + i + '">Remover</button></div>';
      });
      wrap.innerHTML = html;
      qsa('[data-remove-timer]', wrap).forEach(function (btn) { btn.onclick = function () { removeTimer(number(btn.getAttribute('data-remove-timer'), 0)); }; });
    }
    function renderBosses() {
      var grid = qs('#tm-boss-grid', root);
      var list = [];
      bosses.forEach(function (b) { if (state.tier === 'all' || b.tier === state.tier) list.push(b); });
      list.sort(function (a, b) { if (state.sort === 'exp') return b.exp - a.exp; if (state.sort === 'respawn') return a.respawn - b.respawn; return b.danger - a.danger; });
      var html = '';
      list.forEach(function (b) {
        var stars = '';
        var j;
        for (j = 0; j < 5; j++) stars += j < Math.ceil(b.danger / 2) ? '★' : '☆';
        html += '<article class="tm-card" data-boss-id="' + b.id + '"><div class="tm-card-head"><div class="tm-avatar">💀</div><div><div class="tm-tier">' + esc(b.tier) + '-TIER</div><div class="tm-name">' + esc(b.name) + '</div><div class="tm-meta">ID #' + esc(b.id) + ' · ' + esc(b.element) + ' · ' + esc(b.race) + ' · ' + esc(b.size) + '</div></div><div class="tm-stars">' + stars + '<br><span>▼ Dossiê</span></div></div><div class="tm-strip"><div><b>' + esc(b.hp) + '</b><span>HP</span></div><div><b>' + esc(Math.round(b.exp / 1000)) + 'k</b><span>Base EXP</span></div><div><b>' + esc(b.respawn) + 'min</b><span>Respawn</span></div><div><b>' + esc(b.party) + '</b><span>Recomendado</span></div></div><div class="tm-dossier"><div class="tm-dossier-grid"><div class="tm-box"><b>Localização:</b> ' + esc(b.map) + '<br><b>Drops:</b> ' + esc(b.drops) + '</div><div class="tm-box"><b>Dica:</b> ' + esc(b.tips) + '</div></div><button class="tm-add" data-add-boss="' + b.id + '" type="button">+ Adicionar ao timer (' + esc(b.respawn) + 'min)</button></div></article>';
      });
      grid.innerHTML = html || '<div class="tm-panel">Nenhum MVP encontrado.</div>';
      var count = qs('#tm-count', root);
      if (count) count.innerHTML = 'Exibindo ' + list.length + ' MVP(s)';
      qsa('.tm-card-head', grid).forEach(function (head) { head.onclick = function () { classOn(head.parentNode, 'open', head.parentNode.className.indexOf('open') === -1); }; });
      qsa('[data-add-boss]', grid).forEach(function (btn) {
        btn.onclick = function (e) {
          if (e && e.stopPropagation) e.stopPropagation();
          var id = btn.getAttribute('data-add-boss');
          var i;
          for (i = 0; i < bosses.length; i++) if (String(bosses[i].id) === String(id)) addTimer(bosses[i].name, bosses[i].respawn);
        };
      });
    }

    root.innerHTML = '<div class="tm-root"><div class="tm-shell"><header class="tm-hero"><div class="tm-tape"></div><div class="tm-kicker">// Hunter Dossier</div><h1 class="tm-title">Guia de Caça<br><span>a MVPs</span></h1><div class="tm-sub">Encontre, filtre, ordene e rastreie bosses do TimeRO.</div></header><section class="tm-panel"><div class="tm-label">Filtrar e ordenar</div><div class="tm-flex"><button class="tm-btn active" type="button" data-tier="all">Todos</button><button class="tm-btn" type="button" data-tier="S">💀 S-Tier</button><button class="tm-btn" type="button" data-tier="A">🔥 A-Tier</button><button class="tm-btn" type="button" data-tier="B">⚡ B-Tier</button><button class="tm-btn active" type="button" data-sort="danger">Dificuldade</button><button class="tm-btn" type="button" data-sort="exp">EXP</button><button class="tm-btn" type="button" data-sort="respawn">Respawn</button><div id="tm-count"></div></div></section><section class="tm-grid" id="tm-boss-grid"></section><section class="tm-panel"><div class="tm-label">Adicionar MVP manualmente</div><div class="tm-form"><div><label>Nome do MVP</label><input id="tm-manual-name" type="text" placeholder="Ex: Eddga"></div><div><label>Respawn (min)</label><input id="tm-manual-respawn" type="number" placeholder="60" min="1"></div><button class="tm-btn active" id="tm-add-manual" type="button">+ Adicionar</button><button class="tm-btn" id="tm-clear" type="button">Limpar Todos</button></div></section><section class="tm-panel"><div class="tm-label">Timers salvos neste navegador</div><div class="tm-timer-list" id="tm-timer-list"></div></section></div></div>';
    qsa('[data-tier]', root).forEach(function (btn) { btn.onclick = function () { state.tier = btn.getAttribute('data-tier'); qsa('[data-tier]', root).forEach(function (b) { classOn(b, 'active', b === btn); }); renderBosses(); }; });
    qsa('[data-sort]', root).forEach(function (btn) { btn.onclick = function () { state.sort = btn.getAttribute('data-sort'); qsa('[data-sort]', root).forEach(function (b) { classOn(b, 'active', b === btn); }); renderBosses(); }; });
    qs('#tm-add-manual', root).onclick = function () { addTimer(qs('#tm-manual-name', root).value, qs('#tm-manual-respawn', root).value); };
    qs('#tm-clear', root).onclick = function () { setTimers([]); renderTimers(); };
    renderBosses();
    renderTimers();
    if (!window.__timeroMvpGuideTimer) window.__timeroMvpGuideTimer = setInterval(renderTimers, 1000);
  }

  /* =========================================================
     MERCHANT LEDGER FARMING GUIDE BINDER
     Original full page root: #farming-guide-root
     This DOES NOT render/replace the page and DOES NOT inject CSS.
  ========================================================= */
  function initFarmingMerchantLedger() {
    var root = byId('farming-guide-root');
    if (!root || root.getAttribute('data-timero-farming-ready') === '1') return;
    root.setAttribute('data-timero-farming-ready', '1');

    function setMethod(method) {
      method = method || 'grind';
      root.setAttribute('data-open-method', method);
      qsa('.method-panel', root).forEach(function (panel) {
        panel.style.display = panel.id === 'method-' + method ? 'grid' : 'none';
        classOn(panel, 'is-active', panel.id === 'method-' + method);
      });
      qsa('.method-tab', root).forEach(function (tab) {
        classOn(tab, 'is-active', tab.getAttribute('data-method') === method);
      });
    }
    function setRoute(routeId) {
      var currentlyOpen = root.getAttribute('data-open-route') || '';
      var next = currentlyOpen === routeId ? '' : routeId;
      root.setAttribute('data-open-route', next);
      qsa('.route-detail', root).forEach(function (panel) {
        panel.style.display = panel.id === next ? 'block' : 'none';
        classOn(panel, 'is-active', panel.id === next);
      });
      qsa('.route-pill', root).forEach(function (pill) {
        classOn(pill, 'is-active', pill.getAttribute('data-route') === next);
      });
      if (next && byId(next)) setTimeout(function () { byId(next).scrollIntoView({ behavior: 'smooth', block: 'nearest' }); }, 60);
    }
    function fmtMillions(v) {
      v = Math.max(0, Math.round(number(v, 0)));
      if (v >= 1000) return (Math.round((v / 1000) * 10) / 10).toLocaleString('pt-BR') + 'B z';
      return v.toLocaleString('pt-BR') + 'M z';
    }
    function getCalcState() {
      var hoursEl = byId('calc-hours');
      var labelEl = byId('calc-method-label');
      return {
        hours: number(root.getAttribute('data-calc-hours') || (hoursEl ? hoursEl.textContent : '2'), 2),
        rate: number(root.getAttribute('data-calc-rate') || '70', 70),
        booster: number(root.getAttribute('data-calc-booster') || '0', 0),
        label: root.getAttribute('data-calc-label') || (labelEl ? labelEl.textContent : 'Rota Mid-Game (70M/hr)')
      };
    }
    function calc() {
      var st = getCalcState();
      var mult = st.booster ? 1.5 : 1;
      var session = st.hours * st.rate * mult;
      var week = session * 7;
      var pct = Math.min(100, Math.round((week / 12000) * 100));
      var hoursEl = byId('calc-hours');
      var labelEl = byId('calc-method-label');
      var sessionEl = byId('result-session');
      var dayEl = byId('result-day');
      var weekEl = byId('result-week');
      var pctEl = byId('result-pct');
      var barEl = byId('result-bar');
      if (hoursEl) hoursEl.textContent = String(st.hours).replace('.', ',');
      if (labelEl) labelEl.textContent = st.label;
      if (sessionEl) sessionEl.textContent = fmtMillions(session);
      if (dayEl) dayEl.textContent = fmtMillions(session);
      if (weekEl) weekEl.textContent = fmtMillions(week);
      if (pctEl) pctEl.textContent = pct + '%';
      if (barEl) barEl.style.width = pct + '%';
      classOn(byId('boost-yes'), 'is-active', !!st.booster);
      classOn(byId('boost-no'), 'is-active', !st.booster);
    }
    function updateLoadout() {
      var total = 0;
      var count = 0;
      qsa('.loadout-item.is-selected', root).forEach(function (item) {
        total += number(item.getAttribute('data-cost'), 0);
        count++;
      });
      var totalEl = byId('loadout-total');
      var countEl = byId('loadout-count');
      if (totalEl) totalEl.textContent = total.toLocaleString('pt-BR') + 'k z';
      if (countEl) countEl.textContent = String(count);
    }

    qsa('.method-tab', root).forEach(function (tab) { tab.addEventListener('click', function () { setMethod(tab.getAttribute('data-method')); }); });
    qsa('.route-pill', root).forEach(function (pill) { pill.addEventListener('click', function () { setRoute(pill.getAttribute('data-route')); }); });
    qsa('.route-close', root).forEach(function (btn) { btn.addEventListener('click', function (e) { if (e && e.stopPropagation) e.stopPropagation(); setRoute(btn.getAttribute('data-route')); }); });
    qsa('[data-calc-adjust]', root).forEach(function (btn) {
      btn.addEventListener('click', function () {
        var st = getCalcState();
        var delta = number(btn.getAttribute('data-delta'), 0);
        root.setAttribute('data-calc-hours', String(Math.max(0.5, Math.min(24, st.hours + delta))));
        calc();
      });
    });

    var display = byId('calc-method-display');
    var menu = byId('calc-method-menu');
    var calcSection = byId('calculator-section');
    if (display && menu) {
      display.addEventListener('click', function (e) {
        if (e && e.stopPropagation) e.stopPropagation();
        var open = menu.style.display === 'block';
        menu.style.display = open ? 'none' : 'block';
        if (calcSection) calcSection.setAttribute('data-method-menu-open', open ? '0' : '1');
      });
      document.addEventListener('click', function () {
        menu.style.display = 'none';
        if (calcSection) calcSection.setAttribute('data-method-menu-open', '0');
      });
    }
    qsa('.calc-method-option', root).forEach(function (opt) {
      opt.addEventListener('click', function (e) {
        if (e && e.stopPropagation) e.stopPropagation();
        root.setAttribute('data-calc-rate', opt.getAttribute('data-rate') || '70');
        root.setAttribute('data-calc-label', opt.getAttribute('data-label') || trim(opt.textContent));
        if (menu) menu.style.display = 'none';
        calc();
      });
    });
    ['boost-no', 'boost-yes'].forEach(function (id) {
      var el = byId(id);
      if (el) el.addEventListener('click', function () { root.setAttribute('data-calc-booster', el.getAttribute('data-booster') || '0'); calc(); });
    });
    qsa('.loadout-item', root).forEach(function (item) {
      item.addEventListener('click', function () {
        classOn(item, 'is-selected', item.className.indexOf('is-selected') === -1);
        var check = qs('.li-check', item);
        if (check) check.textContent = item.className.indexOf('is-selected') !== -1 ? '✓' : '';
        updateLoadout();
      });
    });
    var clear = qs('.loadout-clear', root);
    if (clear) clear.addEventListener('click', function () {
      qsa('.loadout-item', root).forEach(function (item) {
        classOn(item, 'is-selected', false);
        var check = qs('.li-check', item);
        if (check) check.textContent = '';
      });
      updateLoadout();
    });

    setMethod(root.getAttribute('data-open-method') || 'grind');
    calc();
    updateLoadout();
  }

  /* =========================================================
     Boot all modules safely
  ========================================================= */
  function boot() {
    initCardDatabase();
    initLevelingGuide();
    initZenyGuideMount();
    initMvpGuide();
    initFarmingMerchantLedger();
  }

  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot);
  else boot();
  if (window.mw && mw.hook) mw.hook('wikipage.content').add(boot);
})();