Greasy Fork is available in English.
Let you check monster's debuff in official Orna Codex page.
// ==UserScript== // @name Monster debuff checker for Orna.RPG // @namespace http://tampermonkey.net/ // @version 1.4.2 // @description Let you check monster's debuff in official Orna Codex page. // @author RplusTW // @match https://playorna.com/codex/raids/*/* // @match https://playorna.com/codex/bosses/*/* // @match https://playorna.com/codex/followers/*/* // @match https://playorna.com/codex/monsters/*/* // @match https://playorna.com/codex/classes/*/* // @icon https://www.google.com/s2/favicons?sz=64&domain=playorna.com // @require https://cdn.jsdelivr.net/npm/[email protected] // @grant GM_registerMenuCommand // @grant GM_setValue // @grant GM_getValue // @grant GM_xmlhttpRequest // @connect playorna.com // @connect orna.guide // @run-at document-end // @license MIT // ==/UserScript== let autoInit = GM_getValue('autoInit') || false; GM_registerMenuCommand('Auto Init. ?', toggleAutoInit, 'A'); function toggleAutoInit() { autoInit = window.confirm('Enable Auto initialize for debuff checker?') GM_setValue('autoInit', autoInit); } window.addEventListener('load', function() { if (autoInit) { init(); } else { document.querySelector('.codex-page-icon')?.addEventListener('dblclick', init, { once: true, }); } }, false); async function GET(url) { // console.log('GET', {url}); return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: url, anonymous: true, onload: (response) => { resolve(response) }, onerror: (response) => { reject(response) }, }); }) } async function init() { let style = document.createElement('style'); style.textContent = `.cus-checker{opacity:.3}.cus-checker:checked{opacity:.75}.cus-checker:checked+*{opacity:.5}`; document.head.append(style); collapsePage(); let monster = await getEnInfo(); linkToGuide(monster); initEffects(monster.effects); initStatus(monster.title); } function linkToGuide(monster) { let h1 = document.querySelector('h1.herotext'); h1.innerHTML += ` <a href="https://orna.guide/search?searchstr=${monster.title || ''}" target="guide" title="check in orna.guide">🔍</a>`; } function collapsePage() { let tags = [...document.querySelectorAll('.codex-page h4, .codex-page h4 ~ div')]; if (!tags.length) { return; } let box = null; let sections = tags.reduce((all, tag) => { if (tag.tagName === 'H4') { all[all.length] = [ tag, [] ]; } else if (tag.tagName === 'DIV') { all[all.length - 1][1].push(genDetailsItem('', tag.innerHTML)); tag.remove(); } return all; }, []); sections.forEach(section => { section[0].insertAdjacentHTML( 'beforebegin', genDetailsWrapper( genDetails( section[0].textContent.trim(), section[1].join('') ) ) ); section[0].remove(); }); } function initEffects(effects) { let box = document.querySelector('.codex-page'); let html = ''; // console.log(effects); for (let prop in effects) { // effects[prop] = slimEffects(effects[prop]); html += genEffectHtml(prop, slimEffects(effects[prop])); }; box.innerHTML += `<hr>${genDetailsWrapper(html)}`; } function genEffectHtml(prop, effects) { let items = effects.map(eff => genDetailsItem(eff[0], ` <span> ${eff[0]}, <sub>${eff[1].join()}%</sub> </span> `)).join(''); return genDetails(prop, items); } function initStatus(name) { let tier = Number(document.querySelector('.codex-page-meta')?.textContent?.match(/★(\d+)/)?.[1]); fetch('https://orna.guide/api/v1/monster', { method: 'post', body: JSON.stringify({ name, tier: tier || null, }), }).then(r => r.json()) .then(d => { if (d.length !== 1) { return; } // spawns let catas = [ 'immune_to', 'immune_to_status', 'resistant_to', 'weak_to', ]; let data = d[0]; let box = document.querySelector('.codex-page'); if (data.immune_to_status) { data.immune_to_status.sort(sortStatus); } let html = genDetailsWrapper( catas.map(cata => !data[cata] ? '' : genDetails( _(cata), data[cata].map(i => genDetailsItem(_(i))).join(''), ) ).join('') ) box.innerHTML += `<hr>${html}`; }); } function sortStatus(a, b) { return statusOrder.findIndex(s => s === a) - statusOrder.findIndex(s => s === b); } function genStatusHtml(prop, effects) { let items = effects.map(eff => genDetailsItem(eff[0], ` <span> ${eff[0]}, <sub>${eff[1].join()}%</sub> </span> `)).join(''); return genDetails(prop, items); } function genDetailsItem(name, ctx = name) { return ` <li> <label> <input type="checkbox" value="${name}" class="cus-checker"> <span>${ctx}</span> </label> </li> `; } function genDetailsWrapper(html) { return `<div style="display:flex;justify-content:space-evenly;flex-wrap:wrap;">${html}</div>` } function genDetails(title, listHtml) { return ` <details open style="width:fit-content;"> <summary style="text-transform:capitalize;"> ${title} </summary> <ul style="list-style:none;text-align:start;padding:0;">${listHtml}</ul> </details>` } function slimEffects(effects) { let eff = effects.reduce((all, e) => { let o = e.match(/^(\D+)\s\((\d+)/) || [,e, 100]; all[o[1]] = all[o[1]] || []; all[o[1]].push(+o[2]); return all; }, {}); return Object.keys(eff).map(prop => { return [prop, [...new Set(eff[prop])].sort().reverse()]; }).sort((a, b) => a[0].localeCompare(b[0])); return eff; } async function getEnInfo() { let html = await getUrlSource(getURL(location.href, 'en')); let h1 = parseHtml(html, 'h1.herotext'); let title = h1[0].textContent.trim(); let data = itemParse(html); let skillWord = skillWords.find(str => data[str]); let skills = itemParse(html)[skillWord]; let effects = await parseSkillEffect(skills); return { title, skills, effects, }; } async function parseSkillEffect(skills) { // getURL() let sources = await Promise.all( skills.map( skill => getUrlSource(getURL(skill.url)) ) ); let effects = skills.reduce((all, skill, index) => { skill.effect = itemParse(sources[index]); // console.log(skill.effect); for (let prop in skill.effect) { if (!all[prop]) { all[prop] = []; } let _es = skill.effect[prop].map(e => e.title); all[prop] = all[prop].concat(_es); } return all; }, {}); return effects; } async function getUrlSource(url) { return GET(url).then(res => res.responseText) // return fetch(url).then(res => { // if (res.ok) { // return res.text(); // } // window.open(res.url); // }); } function parseHtml(html, selectoor = '') { let doc = document.implementation.createHTMLDocument(); doc.body.innerHTML = html; return [...doc.querySelectorAll(selectoor)]; } function itemParse(html) { let dataDivs = parseHtml(html, '.codex-page h4, .codex-page h4 ~ div'); let data = dataDivs.reduce((all, div) => { if (div.tagName === 'H4') { let _prop = div.textContent.replace(/[::]/, '').trim().toLowerCase(); all.currentProp = _prop; all[_prop] = all[_prop] || []; } else if (div.tagName === 'DIV') { let icon = div.querySelector('img')?.src; if (!div.querySelector('a[href^="/codex/classes/"]')) { // sucks learning-by all[all.currentProp].push({ icon: div.querySelector('img')?.src, url: div.querySelector('a')?.href, title: div.textContent.trim(), }); } } return all; }, {}); delete data.currentProp; for (let i in data) { if (!data[i]?.length) { delete data[i]; } } return data; } function getURL(url = location.href, lang = unsafeWindow.LANG_CODE) { if (lang === 'en') { let a = document.createElement('a'); a.href = url; a.search = `lang=en`; // return `https://cors-anywhere.herokuapp.com/${a.href}`; // a.href = 'https://api.codetabs.com/v1/proxy?quest=' + a.href; return a.href; // return `https://api.allorigins.win/raw?url=${encodeURIComponent(a.href)}`; } return url; } const skillWords = [ "Skills", "Compétences ", "Habilidades", "Fähigkeiten", "Умения", "技能", "Umiejętności", "Készségek", "Навички", "Abilità", "스킬", "スキル" ].map(str => str.toLowerCase()); let i18n = { langs: ['zh', 'en', ], words: { 'immune_to': ['免疫', 'Immune'], 'immune_to_status': ['狀態免疫', 'Status Immunity'], 'resistant_to': ['抗性', 'Resists'], 'weak_to': ['弱點', 'Weakness'], 'Water': ['水',], 'Fire': ['火',], 'Earthen': ['土',], 'Lightning': ['雷',], 'Dark': ['暗',], 'Dragon': ['龍',], 'Arcane': ['奧',], 'Holy': ['聖',], 'Physical': ['物',], 'Asleep': ['入睡',], 'Bleeding': ['流血',], 'Blight': ['枯萎',], 'Blind': ['致盲',], 'Burning': ['燃燒',], 'Confused': ['迷惑',], 'Cursed': ['詛咒',], 'Dark Sigil': ['暗之印記',], 'Darkblight': ['暗黑疫病',], 'Doom': ['厄運'], 'Foresight ↓': ['預知 ↓'], 'Frozen': ['冰凍'], 'Lulled': ['恍惚'], 'Paralyzed': ['麻痺'], 'Petrified': ['石化'], 'Poisoned': ['中#'], 'Rot': ['#敗'], 'Starstruck': ['暈星'], 'Stasis': ['停滯'], 'Stunned': ['暈眩'], 'Toxic': ['劇#'], 'Windswept': ['逆風'], }, }; const statusOrder = [ 'Poisoned', 'Bleeding', 'Burning', 'Frozen', 'Paralyzed', 'Rot', 'Cursed', 'Toxic', 'Blind', 'Asleep', 'Lulled', 'Drenched', 'Stunned', 'Blight', 'Petrified', 'Stasis', 'Doom', 'Confused', ] let langIndex = i18n.langs.findIndex( lang => lang === unsafeWindow.LANG_CODE?.replace(/-.+/, '') ); // get i18n function _(key) { return i18n.words[key]?.[langIndex] || key; }