MediaWiki:Common.js: Difference between revisions
Jump to navigation
Jump to search
Tag: Undo |
No edit summary Tag: Reverted |
||
| Line 1: | Line 1: | ||
/* TimeRO MediaWiki:Common.js | /* TimeRO MediaWiki:Common.js | ||
FULL REVISED LOGIC-ONLY VERSION | |||
ES5-safe: no const | - 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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''');} | /* ========================================================= | ||
function | Shared helpers | ||
function | ========================================================= */ | ||
function | function byId(id) { return document.getElementById(id); } | ||
function | 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, '&') | |||
.replace(/</g, '<') | |||
.replace(/>/g, '>') | |||
.replace(/"/g, '"') | |||
.replace(/'/g, '''); | |||
} | |||
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>< | |||
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot); | |||
else boot(); | |||
if (window.mw && mw.hook) mw.hook('wikipage.content').add(boot); | |||
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded', | |||
if(window.mw&&mw.hook)mw.hook('wikipage.content').add( | |||
})(); | })(); | ||
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, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
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);
})();