Greasy Fork is available in English.

Monster debuff checker for Orna.RPG

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

  1. // ==UserScript==
  2. // @name Monster debuff checker for Orna.RPG
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.4.2
  5. // @description Let you check monster's debuff in official Orna Codex page.
  6. // @author RplusTW
  7. // @match https://playorna.com/codex/raids/*/*
  8. // @match https://playorna.com/codex/bosses/*/*
  9. // @match https://playorna.com/codex/followers/*/*
  10. // @match https://playorna.com/codex/monsters/*/*
  11. // @match https://playorna.com/codex/classes/*/*
  12. // @icon https://www.google.com/s2/favicons?sz=64&domain=playorna.com
  13. // @require https://cdn.jsdelivr.net/npm/lil-gui@0.17
  14. // @grant GM_registerMenuCommand
  15. // @grant GM_setValue
  16. // @grant GM_getValue
  17. // @grant GM_xmlhttpRequest
  18. // @connect playorna.com
  19. // @connect orna.guide
  20. // @run-at document-end
  21. // @license MIT
  22. // ==/UserScript==
  23.  
  24. let autoInit = GM_getValue('autoInit') || false;
  25.  
  26. GM_registerMenuCommand('Auto Init. ?', toggleAutoInit, 'A');
  27. function toggleAutoInit() {
  28. autoInit = window.confirm('Enable Auto initialize for debuff checker?')
  29. GM_setValue('autoInit', autoInit);
  30. }
  31.  
  32.  
  33. window.addEventListener('load', function() {
  34. if (autoInit) {
  35. init();
  36. } else {
  37. document.querySelector('.codex-page-icon')?.addEventListener('dblclick', init, { once: true, });
  38. }
  39. }, false);
  40.  
  41.  
  42. async function GET(url) {
  43. // console.log('GET', {url});
  44. return new Promise((resolve, reject) => {
  45. GM_xmlhttpRequest({
  46. method: 'GET',
  47. url: url,
  48. anonymous: true,
  49. onload: (response) => {
  50. resolve(response)
  51. },
  52. onerror: (response) => {
  53. reject(response)
  54. },
  55. });
  56. })
  57. }
  58.  
  59. async function init() {
  60. let style = document.createElement('style');
  61. style.textContent = `.cus-checker{opacity:.3}.cus-checker:checked{opacity:.75}.cus-checker:checked+*{opacity:.5}`;
  62. document.head.append(style);
  63. collapsePage();
  64. let monster = await getEnInfo();
  65. linkToGuide(monster);
  66. initEffects(monster.effects);
  67. initStatus(monster.title);
  68. }
  69.  
  70. function linkToGuide(monster) {
  71. let h1 = document.querySelector('h1.herotext');
  72. h1.innerHTML += ` <a href="https://orna.guide/search?searchstr=${monster.title || ''}" target="guide" title="check in orna.guide">🔍</a>`;
  73. }
  74.  
  75. function collapsePage() {
  76. let tags = [...document.querySelectorAll('.codex-page h4, .codex-page h4 ~ div')];
  77. if (!tags.length) { return; }
  78.  
  79. let box = null;
  80.  
  81. let sections = tags.reduce((all, tag) => {
  82. if (tag.tagName === 'H4') {
  83. all[all.length] = [
  84. tag,
  85. []
  86. ];
  87. } else if (tag.tagName === 'DIV') {
  88. all[all.length - 1][1].push(genDetailsItem('', tag.innerHTML));
  89. tag.remove();
  90. }
  91. return all;
  92. }, []);
  93.  
  94. sections.forEach(section => {
  95. section[0].insertAdjacentHTML(
  96. 'beforebegin',
  97. genDetailsWrapper(
  98. genDetails(
  99. section[0].textContent.trim(),
  100. section[1].join('')
  101. )
  102. )
  103. );
  104. section[0].remove();
  105. });
  106. }
  107.  
  108. function initEffects(effects) {
  109. let box = document.querySelector('.codex-page');
  110. let html = '';
  111. // console.log(effects);
  112. for (let prop in effects) {
  113. // effects[prop] = slimEffects(effects[prop]);
  114. html += genEffectHtml(prop, slimEffects(effects[prop]));
  115. };
  116. box.innerHTML += `<hr>${genDetailsWrapper(html)}`;
  117. }
  118.  
  119. function genEffectHtml(prop, effects) {
  120. let items = effects.map(eff => genDetailsItem(eff[0], `
  121. <span>
  122. ${eff[0]},
  123. <sub>${eff[1].join()}%</sub>
  124. </span>
  125. `)).join('');
  126.  
  127. return genDetails(prop, items);
  128. }
  129.  
  130. function initStatus(name) {
  131. let tier = Number(document.querySelector('.codex-page-meta')?.textContent?.match(/★(\d+)/)?.[1]);
  132. fetch('https://orna.guide/api/v1/monster', {
  133. method: 'post',
  134. body: JSON.stringify({
  135. name,
  136. tier: tier || null,
  137. }),
  138. }).then(r => r.json())
  139. .then(d => {
  140. if (d.length !== 1) {
  141. return;
  142. }
  143. // spawns
  144. let catas = [
  145. 'immune_to',
  146. 'immune_to_status',
  147. 'resistant_to',
  148. 'weak_to',
  149. ];
  150.  
  151. let data = d[0];
  152. let box = document.querySelector('.codex-page');
  153.  
  154. if (data.immune_to_status) {
  155. data.immune_to_status.sort(sortStatus);
  156. }
  157. let html = genDetailsWrapper(
  158. catas.map(cata => !data[cata] ? '' :
  159. genDetails(
  160. _(cata),
  161. data[cata].map(i => genDetailsItem(_(i))).join(''),
  162. )
  163. ).join('')
  164. )
  165. box.innerHTML += `<hr>${html}`;
  166. });
  167. }
  168.  
  169. function sortStatus(a, b) {
  170. return statusOrder.findIndex(s => s === a) - statusOrder.findIndex(s => s === b);
  171. }
  172.  
  173. function genStatusHtml(prop, effects) {
  174. let items = effects.map(eff => genDetailsItem(eff[0], `
  175. <span>
  176. ${eff[0]},
  177. <sub>${eff[1].join()}%</sub>
  178. </span>
  179. `)).join('');
  180.  
  181. return genDetails(prop, items);
  182. }
  183.  
  184. function genDetailsItem(name, ctx = name) {
  185. return `
  186. <li>
  187. <label>
  188. <input type="checkbox" value="${name}" class="cus-checker">
  189. <span>${ctx}</span>
  190. </label>
  191. </li>
  192. `;
  193. }
  194.  
  195. function genDetailsWrapper(html) {
  196. return `<div style="display:flex;justify-content:space-evenly;flex-wrap:wrap;">${html}</div>`
  197. }
  198.  
  199. function genDetails(title, listHtml) {
  200. return `
  201. <details open style="width:fit-content;">
  202. <summary style="text-transform:capitalize;">
  203. ${title}
  204. </summary>
  205. <ul style="list-style:none;text-align:start;padding:0;">${listHtml}</ul>
  206. </details>`
  207. }
  208.  
  209. function slimEffects(effects) {
  210. let eff = effects.reduce((all, e) => {
  211. let o = e.match(/^(\D+)\s\((\d+)/) || [,e, 100];
  212. all[o[1]] = all[o[1]] || [];
  213. all[o[1]].push(+o[2]);
  214. return all;
  215. }, {});
  216.  
  217. return Object.keys(eff).map(prop => {
  218. return [prop, [...new Set(eff[prop])].sort().reverse()];
  219. }).sort((a, b) => a[0].localeCompare(b[0]));
  220. return eff;
  221. }
  222.  
  223. async function getEnInfo() {
  224. let html = await getUrlSource(getURL(location.href, 'en'));
  225. let h1 = parseHtml(html, 'h1.herotext');
  226. let title = h1[0].textContent.trim();
  227. let data = itemParse(html);
  228. let skillWord = skillWords.find(str => data[str]);
  229. let skills = itemParse(html)[skillWord];
  230. let effects = await parseSkillEffect(skills);
  231. return {
  232. title,
  233. skills,
  234. effects,
  235. };
  236. }
  237.  
  238. async function parseSkillEffect(skills) {
  239. // getURL()
  240. let sources = await Promise.all(
  241. skills.map( skill => getUrlSource(getURL(skill.url)) )
  242. );
  243.  
  244. let effects = skills.reduce((all, skill, index) => {
  245. skill.effect = itemParse(sources[index]);
  246. // console.log(skill.effect);
  247. for (let prop in skill.effect) {
  248. if (!all[prop]) {
  249. all[prop] = [];
  250. }
  251. let _es = skill.effect[prop].map(e => e.title);
  252. all[prop] = all[prop].concat(_es);
  253. }
  254. return all;
  255. }, {});
  256.  
  257. return effects;
  258. }
  259.  
  260. async function getUrlSource(url) {
  261. return GET(url).then(res => res.responseText)
  262. // return fetch(url).then(res => {
  263. // if (res.ok) {
  264. // return res.text();
  265. // }
  266. // window.open(res.url);
  267. // });
  268. }
  269.  
  270. function parseHtml(html, selectoor = '') {
  271. let doc = document.implementation.createHTMLDocument();
  272. doc.body.innerHTML = html;
  273. return [...doc.querySelectorAll(selectoor)];
  274. }
  275.  
  276. function itemParse(html) {
  277. let dataDivs = parseHtml(html, '.codex-page h4, .codex-page h4 ~ div');
  278. let data = dataDivs.reduce((all, div) => {
  279. if (div.tagName === 'H4') {
  280. let _prop = div.textContent.replace(/[::]/, '').trim().toLowerCase();
  281. all.currentProp = _prop;
  282. all[_prop] = all[_prop] || [];
  283. } else if (div.tagName === 'DIV') {
  284. let icon = div.querySelector('img')?.src;
  285. if (!div.querySelector('a[href^="/codex/classes/"]')) { // sucks learning-by
  286. all[all.currentProp].push({
  287. icon: div.querySelector('img')?.src,
  288. url: div.querySelector('a')?.href,
  289. title: div.textContent.trim(),
  290. });
  291. }
  292. }
  293. return all;
  294. }, {});
  295. delete data.currentProp;
  296. for (let i in data) {
  297. if (!data[i]?.length) {
  298. delete data[i];
  299. }
  300. }
  301. return data;
  302. }
  303.  
  304. function getURL(url = location.href, lang = unsafeWindow.LANG_CODE) {
  305. if (lang === 'en') {
  306. let a = document.createElement('a');
  307. a.href = url;
  308. a.search = `lang=en`;
  309. // return `https://cors-anywhere.herokuapp.com/${a.href}`;
  310. // a.href = 'https://api.codetabs.com/v1/proxy?quest=' + a.href;
  311. return a.href;
  312. // return `https://api.allorigins.win/raw?url=${encodeURIComponent(a.href)}`;
  313. }
  314. return url;
  315. }
  316.  
  317. const skillWords = [
  318. "Skills",
  319. "Compétences ",
  320. "Habilidades",
  321. "Fähigkeiten",
  322. "Умения",
  323. "技能",
  324. "Umiejętności",
  325. "Készségek",
  326. "Навички",
  327. "Abilità",
  328. "스킬",
  329. "スキル"
  330. ].map(str => str.toLowerCase());
  331.  
  332.  
  333. let i18n = {
  334. langs: ['zh', 'en', ],
  335. words: {
  336. 'immune_to': ['免疫', 'Immune'],
  337. 'immune_to_status': ['狀態免疫', 'Status Immunity'],
  338. 'resistant_to': ['抗性', 'Resists'],
  339. 'weak_to': ['弱點', 'Weakness'],
  340. 'Water': ['水',],
  341. 'Fire': ['火',],
  342. 'Earthen': ['土',],
  343. 'Lightning': ['雷',],
  344. 'Dark': ['暗',],
  345. 'Dragon': ['龍',],
  346. 'Arcane': ['奧',],
  347. 'Holy': ['聖',],
  348. 'Physical': ['物',],
  349. 'Asleep': ['入睡',],
  350. 'Bleeding': ['流血',],
  351. 'Blight': ['枯萎',],
  352. 'Blind': ['致盲',],
  353. 'Burning': ['燃燒',],
  354. 'Confused': ['迷惑',],
  355. 'Cursed': ['詛咒',],
  356. 'Dark Sigil': ['暗之印記',],
  357. 'Darkblight': ['暗黑疫病',],
  358. 'Doom': ['厄運'],
  359. 'Foresight ↓': ['預知 ↓'],
  360. 'Frozen': ['冰凍'],
  361. 'Lulled': ['恍惚'],
  362. 'Paralyzed': ['麻痺'],
  363. 'Petrified': ['石化'],
  364. 'Poisoned': ['中毒'],
  365. 'Rot': ['腐敗'],
  366. 'Starstruck': ['暈星'],
  367. 'Stasis': ['停滯'],
  368. 'Stunned': ['暈眩'],
  369. 'Toxic': ['劇毒'],
  370. 'Windswept': ['逆風'],
  371. },
  372. };
  373.  
  374. const statusOrder = [
  375. 'Poisoned',
  376. 'Bleeding',
  377. 'Burning',
  378. 'Frozen',
  379. 'Paralyzed',
  380. 'Rot',
  381. 'Cursed',
  382. 'Toxic',
  383. 'Blind',
  384. 'Asleep',
  385. 'Lulled',
  386. 'Drenched',
  387. 'Stunned',
  388. 'Blight',
  389. 'Petrified',
  390. 'Stasis',
  391. 'Doom',
  392. 'Confused',
  393. ]
  394.  
  395. let langIndex = i18n.langs.findIndex(
  396. lang => lang === unsafeWindow.LANG_CODE?.replace(/-.+/, '')
  397. );
  398.  
  399. // get i18n
  400. function _(key) {
  401. return i18n.words[key]?.[langIndex] || key;
  402. }