Greasy Fork is available in English.

HWM_show_info

Подробная инфа артов/существ/навыков

  1. // ==UserScript==
  2. // @name HWM_show_info
  3. // @author Мифист
  4. // @namespace Мифист
  5. // @version 1.0.0
  6. // @description Подробная инфа артов/существ/навыков
  7. // @match https://www.heroeswm.ru/*
  8. // @match https://*.lordswm.com/*
  9. // @exclude */war.php*
  10. // @exclude */roulette.php*
  11. // @run-at document-end
  12. // @grant none
  13. // @license MIT
  14. // @noframes
  15. // ==/UserScript==
  16.  
  17. (function initModule(view) {
  18. 'use strict';
  19.  
  20. if (document.visibilityState === 'hidden') {
  21. const handler = () => initModule(view);
  22. document.addEventListener('visibilitychange', handler, { once: true });
  23. return;
  24. }
  25.  
  26. if (document.readyState === 'loading') {
  27. const handler = () => initModule(view);
  28. document.addEventListener('DOMContentLoaded', handler, { once: true });
  29. return;
  30. }
  31.  
  32. // ==========================
  33.  
  34. const MODULE_NAME = 'HWM_show_info';
  35. const MODULE_VERSION = '1.0.0';
  36.  
  37. const modules = (function(symbol) {
  38. return view[symbol] || (view[symbol] = {
  39. stack: new Map,
  40. has(key) { return this.stack.has(key); },
  41. delete(key) { return this.stack.delete(key); },
  42. get(key) { return this.stack.get(key); },
  43. add(key, version, exports) {
  44. if (this.stack.has(key)) return;
  45. this.stack.set(key, { version, exports });
  46. }
  47. });
  48. })(Symbol.for('__5781303__'));
  49.  
  50. if (modules.has('HWM_auction_upd')) return;
  51.  
  52. // ==========================
  53.  
  54. let hideFrame = Function.prototype;
  55. const outerStyleSheet = document.createElement('style');
  56.  
  57. const $ = (selector, ctx = document) => ctx.querySelector(selector);
  58.  
  59. function fetch(url) {
  60. return new Promise((resolve, reject) => {
  61. const xhr = new XMLHttpRequest();
  62. xhr.open('GET', url);
  63. xhr.responseType = 'document';
  64.  
  65. xhr.onload = () => {
  66. if (xhr.status === 200) return resolve(xhr.response);
  67. reject(new Error(`Error status: ${xhr.status}`));
  68. };
  69.  
  70. xhr.onerror = () => reject(new Error('ERR_INTERNET_DISCONNECTED'));
  71.  
  72. xhr.send(null);
  73. });
  74. }
  75.  
  76. // ==========================
  77.  
  78. async function handleTarget(e) {
  79. const trg = e.target;
  80. const anchor = getElemAnchor(trg);
  81.  
  82. if (!anchor) return;
  83.  
  84. e.preventDefault();
  85. hideFrame();
  86.  
  87. const self = InfoFrame.instances[anchor.flag];
  88. let data = self.cache.get(anchor.id);
  89.  
  90. if (!document.contains(self.frame)) {
  91. if (!navigator.onLine) return onInternetDisconnect();
  92. await self.onConnect(self.getURL(anchor.id));
  93. }
  94.  
  95. if (data == null) {
  96. data = await pullRequest(self, anchor.id);
  97. if (data == null) return;
  98. }
  99.  
  100. self.setHide(trg);
  101. self.render(data);
  102. self.shell.classList.add('__shown');
  103. centerFrame(self, getClosestBlockElem(trg));
  104.  
  105. if (self === ArmyInfo) self.initHints();
  106. }
  107.  
  108. function onInternetDisconnect(err) {
  109. const msg = err ? err.message : 'ERR_INTERNET_DISCONNECTED';
  110. alert(`${msg}\nНет подключения к Интернету`);
  111. }
  112.  
  113. function pullRequest(self, id) {
  114. return self.request(id)
  115. .then(data => self.cache.set(id, data).get(id))
  116. .catch(onInternetDisconnect);
  117. }
  118.  
  119. // ==========================
  120.  
  121. class InfoFrame {
  122. static __init__() {
  123. if (this !== InfoFrame) return;
  124.  
  125. const instances = this.instances = [PerkInfo, ArtInfo, ArmyInfo];
  126.  
  127. instances.forEach((self, ind) => {
  128. self.flag = ind;
  129.  
  130. if (!$(self.selector)) return;
  131.  
  132. self.cache = new Map;
  133. self.shell = document.createElement('div');
  134. self.frame = document.createElement('iframe');
  135. self.shell.classList.add(`${MODULE_NAME}-shell`);
  136. self.frame.classList.add(`${MODULE_NAME}-frame`);
  137. });
  138.  
  139. if (instances.every(self => !self.hasOwnProperty('cache'))) return;
  140.  
  141. outerStyleSheet.append(this.outerCSS);
  142. document.addEventListener('contextmenu', handleTarget);
  143.  
  144. const destroyType = MODULE_NAME + '__destroy';
  145. const destroyHandler = this.__destroy__.bind(this);
  146. document.addEventListener(destroyType, destroyHandler, { once: true });
  147.  
  148. modules.add(MODULE_NAME, MODULE_VERSION, this);
  149. }
  150.  
  151. static __destroy__() {
  152. hideFrame();
  153.  
  154. this.instances.splice(0).forEach(self => {
  155. if (!self.hasOwnProperty('cache')) return;
  156. self.cache.clear();
  157. self.shell.remove();
  158. });
  159.  
  160. outerStyleSheet.remove();
  161. document.removeEventListener('contextmenu', handleTarget);
  162. modules.delete(MODULE_NAME);
  163. }
  164.  
  165. static get outerCSS() {
  166. return /*css*/`
  167. .${MODULE_NAME}-shell {
  168. --w: 0;
  169. --h: 0;
  170. --x: 0;
  171. --y: 0;
  172. width: var(--w);
  173. height: var(--h);
  174. display: none;
  175. position: absolute;
  176. left: var(--x);
  177. top: var(--y);
  178. z-index: 100;
  179. }
  180. .${MODULE_NAME}-shell.__shown {
  181. display: block;
  182. }
  183. .${MODULE_NAME}-shell::before,
  184. .${MODULE_NAME}-shell::after {
  185. content: "";
  186. height: 5px;
  187. position: absolute;
  188. left: 0;
  189. right: 0;
  190. top: -5px;
  191. }
  192. .${MODULE_NAME}-shell::after {
  193. top: auto;
  194. bottom: -5px;
  195. }
  196. .${MODULE_NAME}-frame {
  197. width: 100%;
  198. height: 100%;
  199. display: block;
  200. border: none;
  201. outline: 2px solid #72787c;
  202. resize: none;
  203. overflow: hidden;
  204. user-select: none;
  205. }
  206. .hwm_hint_css,
  207. div[style^="z-index:1;top:0;right:0;"] {
  208. pointer-events: none;
  209. }
  210. `.replace(/^ +/gm, '');
  211. }
  212.  
  213. static get frameView() {
  214. return this.frame.contentWindow;
  215. }
  216.  
  217. static get frameDoc() {
  218. return this.frame.contentDocument;
  219. }
  220.  
  221. static onConnect(url) {
  222. const {frame, shell} = this;
  223.  
  224. if (!document.contains(outerStyleSheet)) {
  225. document.head.append(outerStyleSheet);
  226. }
  227.  
  228. frame.src = url;
  229. shell.append(frame);
  230. document.body.prepend(shell);
  231.  
  232. return new Promise(resolve => {
  233. frame.onload = () => {
  234. this.onFrameLoad();
  235. resolve();
  236. };
  237. });
  238. }
  239.  
  240. static onFrameLoad() {
  241. const {frame, frameView, frameDoc} = this;
  242. const target = this.target = frameDoc.createElement('div');
  243. target.id = 'cont';
  244.  
  245. clearAsyncQueue(frameView);
  246. frameView.setTimeout(() => clearAsyncQueue(frameView), 200);
  247.  
  248. frameView.addEventListener('error', (e) => {
  249. console.log(e);
  250. alert(`@${MODULE_NAME} => ${this.name}:\n${e.message}`);
  251. if (this === ArmyInfo) this.shell.remove();
  252. });
  253.  
  254. frameDoc.head.innerHTML = /*html*/`
  255. <base target="_parent">
  256. <style>${this.innerCCS}</style>
  257. `;
  258. frameDoc.body.replaceChildren(target);
  259. }
  260.  
  261. static onFrameHide() {}
  262.  
  263. static setHide(trg) {
  264. const {frame, shell, frameDoc} = this;
  265.  
  266. const onKeyUp = (e) => void (e.key === 'Escape' && hide());
  267.  
  268. const hide = hideFrame = () => {
  269. toggleHandlers(false);
  270. shell.classList.remove('__shown');
  271. shell.removeAttribute('style');
  272. hideFrame = Function.prototype;
  273. this.onFrameHide();
  274. };
  275.  
  276. toggleHandlers(true);
  277.  
  278. function toggleHandlers(force) {
  279. const method = (force ? 'add' : 'remove') + 'EventListener';
  280. document[method]('click', hide);
  281. document[method]('keyup', onKeyUp);
  282. frameDoc[method]('keyup', onKeyUp);
  283. shell[method]('mouseleave', hide);
  284. trg[method]('mouseleave', leave);
  285. trg[method]('click', preventClick, true);
  286. }
  287.  
  288. function leave({type, relatedTarget}) {
  289. this.removeEventListener(type, leave);
  290. if (relatedTarget && !relatedTarget.contains(frame)) hide();
  291. }
  292.  
  293. function preventClick(e) {
  294. e.preventDefault();
  295. e.stopPropagation();
  296. hide();
  297. }
  298. }
  299.  
  300. static render(html) {
  301. this.target.innerHTML = html;
  302. }
  303. }
  304.  
  305. // ==========================
  306.  
  307. class PerkInfo extends InfoFrame {
  308. static get innerCCS() {
  309. return /*css*/`
  310. * {
  311. font-family: inherit;
  312. font-size: inherit;
  313. box-sizing: border-box;
  314. }
  315. :root {
  316. font-size: 10px;
  317. }
  318. body {
  319. font-family: Verdana, Arial, sans-serif;
  320. font-size: 1.3rem;
  321. line-height: 1.3;
  322. margin: 0;
  323. background-image: linear-gradient(45deg, #cdc9c0, #fff);
  324. overflow: hidden;
  325. user-select: none;
  326. }
  327. #cont {
  328. width: 60rem;
  329. display: flex;
  330. flex-wrap: wrap;
  331. align-items: flex-start;
  332. justify-content: flex-start;
  333. padding: 1rem;
  334. }
  335. #cont > h1 {
  336. font-size: 1.1em;
  337. width: 100%;
  338. margin: 0 0 1rem;
  339. color: #435970;
  340. text-transform: uppercase;
  341. }
  342. #cont > div {
  343. flex: 1;
  344. padding-left: 1rem;
  345. }
  346. `.replace(/^ +/gm, '');
  347. }
  348.  
  349. static get selector() {
  350. return 'a[href*="showperkinfo."]';
  351. }
  352.  
  353. static getURL(id) {
  354. return `/showperkinfo.php?name=${id}`;
  355. }
  356.  
  357. static getItemId(el) {
  358. return new URLSearchParams(el.search).get('name');
  359. }
  360.  
  361. static async request(id) {
  362. const responseDoc = await fetch(this.getURL(id));
  363. const img = $('img[src*="/perks/"]', responseDoc);
  364. let elem = img && img.closest('td');
  365. elem = elem && elem.nextElementSibling;
  366.  
  367. if (!elem) return '';
  368.  
  369. return /*html*/`
  370. <h1>${img.alt.slice(7)}</h1>
  371. <img src="${img.src}">
  372. <div>${elem.innerHTML.slice(20)}</div>
  373. `;
  374. }
  375. }
  376.  
  377. // ==========================
  378.  
  379. class ArtInfo extends InfoFrame {
  380. static get innerCCS() {
  381. return /*css*/`
  382. * {
  383. font-family: inherit;
  384. font-size: inherit;
  385. box-sizing: border-box;
  386. }
  387. :root {
  388. font-size: 10px;
  389. }
  390. body {
  391. font-family: Verdana, Arial, sans-serif;
  392. font-size: 1.3rem;
  393. line-height: 1.3;
  394. margin: 0;
  395. background-image: linear-gradient(45deg, #cdc9c0, #fff);
  396. overflow: hidden;
  397. user-select: none;
  398. }
  399. #cont {
  400. width: 74rem;
  401. max-height: 42rem;
  402. display: flex;
  403. position: relative;
  404. overflow-y: auto;
  405. scrollbar-width: thin;
  406. }
  407. .global_container_block_header {
  408. font-size: 1.1em;
  409. position: absolute;
  410. right: 2rem;
  411. top: 1rem;
  412. }
  413. .global_container_block_header h1 {
  414. line-height: normal !important;
  415. text-transform: uppercase;
  416. margin: 0;
  417. }
  418. .global_container_block_header b {
  419. color: #435970;
  420. }
  421. .art_info_left_block {
  422. padding: 2rem 1rem;
  423. }
  424. .s_art_prop_amount_icon {
  425. min-height: 2.8rem;
  426. display: flex;
  427. align-items: center;
  428. justify-content: center;
  429. color: #0a2b4b;
  430. background-image: linear-gradient(#eee, #bbc4b1);
  431. border: 1px solid #78878d;
  432. }
  433. .s_art_prop_amount_icon:hover {
  434. filter: saturate(1.5);
  435. }
  436. .s_art_prop_amount_icon img {
  437. width: 2rem;
  438. margin-right: .5rem;
  439. }
  440. .cre_mon_image1 {
  441. display: none;
  442. }
  443. .art_info_desc {
  444. padding: 3rem 1rem 1rem;
  445. background: transparent !important;
  446. }
  447. .rs {
  448. margin: 0 2px;
  449. }
  450. b {
  451. color: #332f2f;
  452. }
  453. i {
  454. color: #315473;
  455. }
  456. a[href*="=40#"] {
  457. font-weight: bold;
  458. font-style: normal;
  459. text-decoration: none;
  460. }
  461. `.replace(/^ +/gm, '');
  462. }
  463.  
  464. static get selector() {
  465. return location.pathname === '/inventory.php'
  466. ? '.inv_art_outside'
  467. : 'a[href*="art_info."]';
  468. }
  469.  
  470. static getURL(id) {
  471. return `/art_info.php?id=${id}`;
  472. }
  473.  
  474. static getItemId(el) {
  475. if (el.tagName === 'A') return new URLSearchParams(el.search).get('id');
  476.  
  477. const imgPathReg = /\/artifacts\/([^.]+)/;
  478. const getArtName = (html) => html.match(imgPathReg)[1];
  479. const artName = getArtName(el.outerHTML);
  480.  
  481. const {arts = []} = view;
  482. const art = arts.find(art => getArtName(art.html) === artName) || {};
  483. return art.art_id;
  484. }
  485.  
  486. static async request(id) {
  487. const responseDoc = await fetch(this.getURL(id));
  488. const elem = $('#set_mobile_max_width', responseDoc);
  489. return elem ? elem.innerHTML : '';
  490. }
  491. }
  492.  
  493. // ==========================
  494.  
  495. class ArmyInfo extends InfoFrame {
  496. static get innerCCS() {
  497. return /*css*/`
  498. * {
  499. box-sizing: border-box;
  500. }
  501. :root {
  502. font-size: 10px;
  503. }
  504. body {
  505. font-family: Verdana, Arial, sans-serif;
  506. font-size: 1.2rem;
  507. margin: 0;
  508. background-image: linear-gradient(45deg, #dad1be, #fff);
  509. overflow: hidden;
  510. user-select: none;
  511. }
  512. .hwm_hint_css {
  513. font-size: inherit !important;
  514. max-width: 34rem !important;
  515. position: fixed;
  516. display: none;
  517. padding: .4em .7em;
  518. color: #ddd;
  519. background-color: #3a3a3a;
  520. border: 2px solid #888;
  521. z-index: 2;
  522. }
  523. #cont {
  524. width: 70rem;
  525. display: flex;
  526. flex-wrap: wrap;
  527. }
  528. .info_header_content {
  529. width: 100%;
  530. height: 3.5em;
  531. display: flex;
  532. align-items: center;
  533. justify-content: center;
  534. background: #afc2d747;
  535. border-bottom: 1px solid #757575;
  536. }
  537. a:first-child {
  538. font-size: 1.6em;
  539. color: #506263;
  540. text-decoration: none;
  541. }
  542. .info_text_content {
  543. width: calc(100% - 20rem);
  544. display: flex;
  545. flex-wrap: wrap;
  546. border-right: 1px solid #757575;
  547. }
  548. .info_text_content > div {
  549. font-size: 1.1em;
  550. width: 50%;
  551. display: flex;
  552. align-items: center;
  553. column-gap: .5rem;
  554. padding: 0 1.4rem;
  555. }
  556. .info_text_content img {
  557. width: 2.4rem;
  558. height: auto;
  559. }
  560. .info_text_content div:last-child {
  561. margin-left: auto;
  562. }
  563. canvas,
  564. #show_army,
  565. .konvajs-content {
  566. width: 20rem !important;
  567. height: 20rem !important;
  568. }
  569. .army_info_skills {
  570. font-size: 1.1em;
  571. width: 100%;
  572. display: flex;
  573. flex-wrap: wrap;
  574. column-gap: 0.4em;
  575. padding: 1rem 1.4rem;
  576. border-top: 1px solid #757575;
  577. }
  578. .army_info_skills > div {
  579. font-weight: bold;
  580. margin-right: -0.5em;
  581. }
  582. .army_info_skills > span:hover {
  583. color: brown;
  584. cursor: help;
  585. }
  586. `.replace(/^ +/gm, '');
  587. }
  588.  
  589. static get selector() {
  590. return 'a[href*="army_info."]';
  591. }
  592.  
  593. static getURL(id) {
  594. return `/army_info.php?name=${id}`;
  595. }
  596.  
  597. static getItemId(el) {
  598. return new URLSearchParams(el.search).get('name');
  599. }
  600.  
  601. static async request(id) {
  602. const responseDoc = await fetch(this.getURL(id));
  603. const linkHTML = `<a href="${this.getURL(id)}">$1</a>`;
  604. const html = $('.army_info', responseDoc).innerHTML.trim()
  605. .replaceAll('\n', '')
  606. .replace(/\s{2,}/g, ' ')
  607. .replace(' style="display: show;"', '')
  608. .replaceAll('> ', '>')
  609. .replaceAll(' width="48" height="48" alt="" title=""', '')
  610. .replace(/<div><h1 [^>]+>(.+?)<\/h1><\/div>/, linkHTML)
  611. .replace(/<div(?:><img| class="corner).+?div>/g, '')
  612. .replaceAll(' class="scroll_content_half"', '');
  613.  
  614. const reg = /info\((.+?)\);/;
  615. const script = [...responseDoc.scripts].pop();
  616. const paramsStr = (script.text.match(reg) || ['', ''])[1];
  617.  
  618. return [html, paramsStr];
  619. }
  620.  
  621. static render([html, paramsStr]) {
  622. super.render(html);
  623.  
  624. const {frameView} = this;
  625. const params = new Function(`return [${paramsStr}]`)();
  626. frameView.setTimeout(() => frameView.init_army_info(...params));
  627. }
  628.  
  629. static onFrameLoad() {
  630. super.onFrameLoad();
  631.  
  632. const {frameView, target} = this;
  633. const hwmHint = frameView.hwm_hint;
  634.  
  635. if (!(hwmHint instanceof frameView.HTMLElement)) return;
  636.  
  637. target.after(hwmHint);
  638. }
  639.  
  640. static onFrameHide() {
  641. const {frameView: ctx} = this;
  642. const stages = ctx.Konva && ctx.Konva.stages || [];
  643. stages.splice(0).forEach(stage => ctx.clearInterval(stage.interval));
  644. }
  645.  
  646. static initHints() {
  647. const initHwmHints = this.frameView.hwm_hints_init;
  648. if (typeof initHwmHints === 'function') initHwmHints();
  649. }
  650. }
  651.  
  652. // ==========================
  653.  
  654. InfoFrame.__init__();
  655.  
  656. // ==========================
  657.  
  658. function centerFrame({target, shell}, elem) {
  659. const {offsetHeight, offsetWidth} = target;
  660. const {left, right, top, bottom} = elem.getBoundingClientRect();
  661. const offset = 5;
  662. const halfw = offsetWidth / 2;
  663. const height = offsetHeight + offset;
  664. const maxX = document.documentElement.clientWidth - offset;
  665. const centerX = left + (right - left) / 2;
  666.  
  667. const x = Math.max(offset, Math.min(centerX - halfw, maxX - offsetWidth));
  668. const y = (bottom + height < view.innerHeight || top - height <= 0)
  669. ? bottom + offset
  670. : top - height;
  671.  
  672. const setCSS = shell.style.setProperty.bind(shell.style);
  673. setCSS('--w', `${offsetWidth >> 0}px`);
  674. setCSS('--h', `${offsetHeight >> 0}px`);
  675. setCSS('--x', `${x + view.scrollX >> 0}px`);
  676. setCSS('--y', `${y + view.scrollY >> 0}px`);
  677. }
  678.  
  679. function getElemAnchor(el) {
  680. const propName = `__cachedInfoFrameAnchor`;
  681. if (el.hasOwnProperty(propName)) return el[propName];
  682.  
  683. const set = (val = null) => (el[propName] = val);
  684. const artElem = el.closest(ArtInfo.selector);
  685. const perkElem = artElem ? null : el.closest(PerkInfo.selector);
  686. const elem = artElem || perkElem || el.closest(ArmyInfo.selector);
  687.  
  688. if (!elem) return set();
  689.  
  690. const self = perkElem ? PerkInfo : artElem ? ArtInfo : ArmyInfo;
  691. const id = self.getItemId(elem);
  692.  
  693. return !id ? set() : set({ flag: self.flag, id });
  694. }
  695.  
  696. function getClosestBlockElem(elem) {
  697. if (elem.offsetWidth && elem.offsetHeight) return elem;
  698. return getClosestBlockElem(elem.parentNode);
  699. }
  700.  
  701. function clearAsyncQueue(ctx) {
  702. let i = ctx.setTimeout(0);
  703. while (i) ctx.clearTimeout(i--);
  704. }
  705. })(document.defaultView);