Monster debuff checker for Orna.RPG

Let you check monster's debuff in official Orna Codex page.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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;
}