Monster debuff checker for Orna.RPG

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

目前為 2022-09-15 提交的版本,檢視 最新版本

// ==UserScript==
// @name         Monster debuff checker for Orna.RPG
// @namespace    http://tampermonkey.net/
// @version      1.1.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/*/*
// @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
// @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 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();
	initEffects(monster.effects);
}

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 = '';
	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 genDetailsItem(name, ctx) {
	return `
		<li>
			<label>
				<input type="checkbox" name="${name}" class="cus-checker">
				${ctx}
			</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+)/);
		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());
	let h1 = parseHtml(html, 'h1.herotext');
	let data = itemParse(html);
	let skillWord = skillWords.find(str => data[str]);
	let skills = itemParse(html)[skillWord];
	let effects = await parseSkillEffect(skills);
	return {
		title: h1[0].textContent.trim(),
		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]);
		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 fetch(url).then(res => res.text());
}

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;
			all[all.currentProp].push({
				icon: div.querySelector('img')?.src,
				url: div.querySelector('a')?.href,
				title: div.textContent.trim(),
			});
		}
		return all;
	}, {});
	delete data.currentProp;
	return data;
}

function getURL(url = location.href, lang = 'en') {
	return url;
	// let a = document.createElement('a');
	// a.href = url;
	// a.search = `lang=${lang}`;
	// a.href = 'https://api.codetabs.com/v1/proxy?quest=' + a.href;
	// return a.href;
}

const skillWords = [
  "Skills",
  "Compétences ",
  "Habilidades",
  "Fähigkeiten",
  "Умения",
  "技能",
  "Umiejętności",
  "Készségek",
  "Навички",
  "Abilità",
  "스킬",
  "スキル"
].map(str => str.toLowerCase());