Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
bookemarklet to check-orna-item-quality
javascript: (function() {
var s = document.createElement('script');
s.setAttribute('src', 'https://cdn.jsdelivr.net/npm/lil-gui@0.17');
document.body.appendChild(s);
s.onload = () => {
var GUI = lil.GUI;
var itemname = location.href.match(/items\/([^/]+)/);
var data = {
'%': 100,
'assess': () => {
window.open(`https://orna.guide/search?searchstr=${itemname?.[1].replace(/\W/g, ' ')}`);
},
};
let div = document.createElement('div');
document.querySelector('.codex-stats').after(div);
div.style = `display:flex;justify-content:center;`;
var gui = new GUI({
autoPlace: false,
container: div,
});
let stats = document.querySelectorAll('.codex-stat');
stats.forEach(stat => {
let info = stat.textContent.trim().match(/(\D+)(\d+)/);
let p = info?.[1];
let v = info?.[2];
if (p) {
data[p] = +v;
gui.add(data, p, ~~(v * 0.7), v * 2, 1).onChange(_v => {
quality.setValue(~~(100 * _v / +v))
});
}
});
let quality = gui.add(data, '%', 70, 200);
gui.add(data, 'assess').name('GO TO ASSESS');
};
})();
// ==UserScript==
// @name Item quality checker for Orna.RPG
// @namespace http://tampermonkey.net/
// @version 1.2.2
// @description Let you easily calculate item quality in official Orna Codex page.
// @author RplusTW
// @match https://playorna.com/codex/items/*/
// @match https://playorna.com/codex/items/*/?*
// @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
// @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', async function() {
'use strict';
if (autoInit) {
init();
} else {
document.querySelector('.codex-page-icon')?.addEventListener('dblclick', init, { once: true, });
}
async function init() {
var s = document.createElement('script');
s.setAttribute('src', 'https://cdn.jsdelivr.net/npm/lil-gui@0.17');
document.body.appendChild(s);
s.onload = scriptOnload;
}
}, false);
async function scriptOnload() {
let GUI = window.lil.GUI;
let urlPath = location.href.match(/items\/([^/]+)/);
let assessUrl = `https://orna.guide/search?searchstr=${urlPath?.[1].replace(/\W/g, ' ')}`;
let gid = '';
let data = {
'%': 100,
'assess_guide': () => { window.open(assessUrl, 'guide'); },
'assess_api': () => { initAssess(gid); },
}
let div = document.createElement('div');
div.id = 'gui-div';
let statsDiv = document.querySelector('.codex-stats');
statsDiv.after(div);
div.style = `display:flex;justify-content:center;flex-wrap:wrap;`;
var gui = new GUI({
autoPlace: false,
container: div,
});
let stats = getStatValues(statsDiv);
console.log({stats});
stats?.forEach(stat => {
data[stat.prop] = stat.value;
gui.add(
data, stat.prop,
~~(stat.value * 0.7),
stat.value * 2,
1
)
.onChange(_v => {
quality.setValue(~~(100 * _v / stat.value))
});
});
let quality = gui.add(data, '%', 70, 200);
let assessFolder = gui.addFolder( 'Assess' );
assessFolder.close();
let assessOnGuide = assessFolder.add(data, 'assess_guide').name(`🔍 on Orna.Guide `);
let itemInfo = await getItemInfo();
gid = itemInfo.id;
if (gid) {
assessUrl = `https://orna.guide/items?show=${gid}`;
assessOnGuide.name(`🔍 ${itemInfo.name} on Orna.Guide 🔗`);
let assessByAPI = assessFolder.add(data, 'assess_api').name(`Assess ${itemInfo.name} Here! 🌟`);
}
}
function getStatValues(statDom) {
let statDivs = [...statDom.querySelectorAll('.codex-stat')];
if (!statDivs?.length) {
return null;
}
return statDivs.map(div => {
let info = div.textContent.trim().match(/(\D+)(\d+)/);
let prop = info?.[1].trim().replace(':', '').toLowerCase();
let value = +info?.[2];
return {
prop,
value,
};
});
}
function initAssess(gid) {
if (window.assessInited) {
return;
}
window.assessInited = true;
let divForm = document.createElement('div');
let resultBox = document.createElement('details');
resultBox.style = 'width:100%';
let optionsHtml = ['level', 'attack', 'defense', 'magic', 'resistance', 'hp', 'mana', 'dexterity', 'ward', 'crit', ]
.map(i => genLabel(i)).join('');
divForm.innerHTML = `
<details open>
<form id="assess-form" class="lil-gui">
${genLabel('id', gid, 'readonly')}
${optionsHtml}
<div class="controller">
<div class="widget"><input type="submit"></div>
<div class="widget"></div>
<div class="widget"><input type="reset"></div>
</div>
</form>
</details>`;
document.querySelector('#gui-div').appendChild(divForm);
document.querySelector('#gui-div').appendChild(resultBox);
let form = divForm.querySelector('#assess-form');
form.addEventListener('submit', (e) => {
e.preventDefault();
const formData = {};
for (const pair of new FormData(form)) {
if (pair[1]) {
formData[pair[0]] = +pair[1];
}
}
fetch('https://orna.guide/api/v1/assess', {
method: 'post',
body: JSON.stringify(formData),
}).then(r => r.json())
.then(d => {
resultBox.innerHTML = genAssessTable(d);
resultBox.open = true;
});
});
}
function genAssessTable(data) {
let stats = data.stats;
let props = Object.keys(stats);
let title = document.querySelector('h1.herotext')?.textContent || '';
let ths = props.map(prop => genTd(prop, 'th')).join('');
let tbody = stats[props[0]].values.map((_i, index) => {
let tds = props.map(prop => {
return genTd(stats[prop].values[index]);
}).join('');
return `<tr>
${genTd(index + 1)}
${tds}
</tr>`
}).join('');
props.map(prop => {
stats[prop].values
});
return `
<table style="margin:auto;text-transform:capitalize;">
<caption>${title} ${data.quality * 100}%</caption>
<style>details th {border-bottom:1px dotted #fff6;}</style>
<tr>
<th>Lv</th>
${ths}
</tr>
${data.quality * 1 ? tbody : ''}
</table>
`;
}
function genTd(str, tag = 'td') {
return `<${tag}>${str}</${tag}>`;
}
function genLabel(prop = '', value = '', attr) {
return `
<label class="controller number">
<div class="name" style="text-transform:capitalize;">${prop}</div>
<div class="widget">
<input type="number" value="${value}" name="${prop}" ${attr} />
</div>
</label>`;
}
async function getItemInfo() {
let info = await getEnInfo();
let name = info.title;
if (!name) {
return false;
}
let itemData = await postData('https://orna.guide/api/v1/item', {name});
if (itemData?.length !== 1) {
return false;
}
return {
id: itemData[0]?.id,
name,
};
}
function postData(url, data) {
return fetch(url, {
method: 'POST',
body: JSON.stringify(data)
}).then(res => res.json());
}
function getEnURL() {
let a = document.createElement('a');
a.href = location.href;
a.search = 'lang=en';
a.href = `https://api.allorigins.win/raw?url=${encodeURIComponent(a.href)}`;
// a.href = 'https://api.codetabs.com/v1/proxy?quest=' + a.href;
// a.href = ' https://fast-dawn-89938.herokuapp.com/' + a.href;
return a.href;
}
async function getEnInfo() {
let html = await fetch(getEnURL()).then(res => res.text());
let doc = document.implementation.createHTMLDocument();
doc.body.innerHTML = html;
let h1 = doc.querySelector('h1.herotext');
return {
title: h1.textContent.trim(),
// stats: getStatValues(doc.querySelector('.codex-stats')),
};
}
// ==UserScript==
// @name Monster debuff checker for Orna.RPG
// @namespace http://tampermonkey.net/
// @version 1.2.0
// @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
// @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();
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 = '';
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+)/);
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]);
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) {
if (lang === 'en') {
let a = document.createElement('a');
a.href = url;
a.search = `lang=en`;
// 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'],
words: {
'immune_to': ['免疫',],
'immune_to_status': ['狀態免疫',],
'resistant_to': ['抗性',],
'weak_to': ['弱點',],
'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;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment