- // ==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/lil-gui@0.17
- // @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;
- }