Ironwood RPG - Pancake-Scripts

A collection of scripts to enhance Ironwood RPG - https://github.com/Boldy97/ironwood-scripts

目前为 2023-09-19 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Ironwood RPG - Pancake-Scripts
  3. // @namespace http://tampermonkey.net/
  4. // @version 2.3
  5. // @description A collection of scripts to enhance Ironwood RPG - https://github.com/Boldy97/ironwood-scripts
  6. // @author Pancake
  7. // @match https://ironwoodrpg.com/*
  8. // @icon https://www.google.com/s2/favicons?sz=64&domain=ironwoodrpg.com
  9. // @grant none
  10. // @run-at document-start
  11. // @require https://code.jquery.com/jquery-3.6.4.min.js
  12. // @require https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.0/chart.umd.js
  13. // ==/UserScript==
  14. (() => {
  15.  
  16. if(window.moduleRegistry) {
  17. return;
  18. }
  19.  
  20. window.moduleRegistry = {
  21. add,
  22. get,
  23. build
  24. };
  25.  
  26. const modules = {};
  27.  
  28. function add(name, initialiser) {
  29. modules[name] = createModule(name, initialiser);
  30. buildModule(modules[name], true);
  31. }
  32.  
  33. function get(name) {
  34. return modules[name] || null;
  35. }
  36.  
  37. function build() {
  38. for(const module of Object.values(modules)) {
  39. buildModule(module);
  40. }
  41. }
  42.  
  43. function createModule(name, initialiser) {
  44. const dependencies = extractParametersFromFunction(initialiser).map(dependency => {
  45. const name = dependency.replaceAll('_', '');
  46. const module = get(name);
  47. const optional = dependency.startsWith('_');
  48. return { name, module, optional };
  49. });
  50. const module = {
  51. name,
  52. initialiser,
  53. dependencies
  54. };
  55. for(const other of Object.values(modules)) {
  56. for(const dependency of other.dependencies) {
  57. if(dependency.name === name) {
  58. dependency.module = module;
  59. }
  60. }
  61. }
  62. return module;
  63. }
  64.  
  65. function buildModule(module, partial, chain) {
  66. if(module.built) {
  67. return true;
  68. }
  69.  
  70. chain = chain || [];
  71. if(chain.includes(module.name)) {
  72. chain.push(module.name);
  73. throw `Circular dependency in chain : ${chain.join(' -> ')}`;
  74. }
  75. chain.push(module.name);
  76.  
  77. for(const dependency of module.dependencies) {
  78. if(!dependency.module) {
  79. if(partial) {
  80. return false;
  81. }
  82. if(dependency.optional) {
  83. continue;
  84. }
  85. throw `Unresolved dependency : ${dependency.name}`;
  86. }
  87. const built = buildModule(dependency.module, partial, chain);
  88. if(!built) {
  89. return false;
  90. }
  91. }
  92.  
  93. const parameters = module.dependencies.map(a => a.module?.reference);
  94. module.reference = module.initialiser.apply(null, parameters);
  95. module.built = true;
  96.  
  97. chain.pop();
  98. return true;
  99. }
  100.  
  101. function extractParametersFromFunction(fn) {
  102. const PARAMETER_NAMES = /([^\s,]+)/g;
  103. var fnStr = fn.toString();
  104. var result = fnStr.slice(fnStr.indexOf('(')+1, fnStr.indexOf(')')).match(PARAMETER_NAMES);
  105. return result || [];
  106. }
  107.  
  108. })();
  109. // actionCache
  110. window.moduleRegistry.add('actionCache', (auth, request, Promise) => {
  111.  
  112. const authenticated = auth.ready;
  113. const isReady = new Promise.Deferred();
  114.  
  115. const exports = {
  116. ready: isReady.promise,
  117. list: [],
  118. byId: null,
  119. byName: null
  120. };
  121.  
  122. async function initialise() {
  123. await authenticated;
  124. const actions = await request.listActions();
  125. exports.byId = {};
  126. exports.byName = {};
  127. for(const action of actions) {
  128. exports.list.push(action);
  129. exports.byId[action.id] = action;
  130. exports.byName[action.name] = action;
  131. }
  132. isReady.resolve();
  133. }
  134.  
  135. initialise();
  136.  
  137. return exports;
  138.  
  139. }
  140. );
  141. // auth
  142. window.moduleRegistry.add('auth', (Promise) => {
  143.  
  144. const authenticated = new Promise.Deferred();
  145. let TOKEN = null;
  146.  
  147. const exports = {
  148. ready: authenticated.promise,
  149. isReady: false,
  150. register,
  151. getHeaders
  152. };
  153.  
  154. function register(name, password) {
  155. TOKEN = 'Basic ' + btoa(name + ':' + password);
  156. authenticated.resolve();
  157. exports.isReady = true;
  158. $('#authenticatedMarker').remove();
  159. }
  160.  
  161. function getHeaders() {
  162. return {
  163. 'Content-Type': 'application/json',
  164. 'Authorization': TOKEN
  165. };
  166. }
  167.  
  168. return exports;
  169.  
  170. }
  171. );
  172. // colorMapper
  173. window.moduleRegistry.add('colorMapper', () => {
  174.  
  175. const colorMappings = {
  176. // https://colorswall.com/palette/3
  177. primary: '#0275d8',
  178. success: '#5cb85c',
  179. info: '#5bc0de',
  180. warning: '#f0ad4e',
  181. danger: '#d9534f',
  182. inverse: '#292b2c',
  183. // component styling
  184. componentLight: '#393532',
  185. componentRegular: '#28211b',
  186. componentDark: '#211a12'
  187. };
  188.  
  189. function mapColor(color) {
  190. return colorMappings[color] || color;
  191. }
  192.  
  193. return mapColor;
  194.  
  195. }
  196. );
  197. // components
  198. window.moduleRegistry.add('components', (elementWatcher, colorMapper, elementCreator) => {
  199.  
  200. const exports = {
  201. addComponent,
  202. removeComponent,
  203. search
  204. }
  205.  
  206. const $ = window.$;
  207. const rowTypeMappings = {
  208. item: createRow_Item,
  209. input: createRow_Input,
  210. break: createRow_Break,
  211. buttons: createRow_Button,
  212. dropdown: createRow_Select,
  213. header: createRow_Header,
  214. checkbox: createRow_Checkbox,
  215. segment: createRow_Segment,
  216. progress: createRow_Progress,
  217. chart: createRow_Chart,
  218. list: createRow_List
  219. };
  220.  
  221. function initialise() {
  222. elementCreator.addStyles(styles);
  223. }
  224.  
  225. function removeComponent(blueprint) {
  226. $(`#${blueprint.componentId}`).remove();
  227. }
  228.  
  229. async function addComponent(blueprint) {
  230. if($(blueprint.dependsOn).length) {
  231. actualAddComponent(blueprint);
  232. return;
  233. }
  234. await elementWatcher.exists(blueprint.dependsOn);
  235. actualAddComponent(blueprint);
  236. }
  237.  
  238. function actualAddComponent(blueprint) {
  239. $(`#${blueprint.componentId}`).remove();
  240. const component =
  241. $('<div/>')
  242. .addClass('customComponent')
  243. .attr('id', blueprint.componentId);
  244. if(blueprint.onClick) {
  245. component
  246. .click(blueprint.onClick)
  247. .css('cursor', 'pointer');
  248. }
  249.  
  250. // TABS
  251. const theTabs = createTab(blueprint);
  252. component.append(theTabs);
  253.  
  254. // PAGE
  255. const selectedTabBlueprint = blueprint.tabs[blueprint.selectedTabIndex] || blueprint.tabs[0];
  256. selectedTabBlueprint.rows.forEach((rowBlueprint, index) => {
  257. component.append(createRow(rowBlueprint));
  258. });
  259.  
  260. $(`${blueprint.parent}`).append(component);
  261. }
  262.  
  263. function createTab(blueprint) {
  264. if(!blueprint.selectedTabIndex) {
  265. blueprint.selectedTabIndex = 0;
  266. }
  267. if(blueprint.tabs.length === 1) {
  268. return;
  269. }
  270. const tabContainer = $('<div/>').addClass('tabs');
  271. blueprint.tabs.forEach((element, index) => {
  272. if(element.hidden) {
  273. return;
  274. }
  275. const tab = $('<button/>')
  276. .attr('type', 'button')
  277. .addClass('tabButton')
  278. .text(element.title)
  279. .click(changeTab.bind(null, blueprint, index));
  280. if(blueprint.selectedTabIndex !== index) {
  281. tab.addClass('tabButtonInactive')
  282. }
  283. if(index !== 0) {
  284. tab.addClass('lineLeft')
  285. }
  286. tabContainer.append(tab);
  287. });
  288. return tabContainer;
  289. }
  290.  
  291. function createRow(rowBlueprint) {
  292. if(!rowTypeMappings[rowBlueprint.type]) {
  293. console.warn(`Skipping unknown row type in blueprint: ${rowBlueprint.type}`, rowBlueprint);
  294. return;
  295. }
  296. if(rowBlueprint.hidden) {
  297. return;
  298. }
  299. return rowTypeMappings[rowBlueprint.type](rowBlueprint);
  300. }
  301.  
  302. function createRow_Item(itemBlueprint) {
  303. const parentRow = $('<div/>').addClass('customRow');
  304. if(itemBlueprint.image) {
  305. parentRow.append(createImage(itemBlueprint));
  306. }
  307. if(itemBlueprint?.name) {
  308. parentRow
  309. .append(
  310. $('<div/>')
  311. .addClass('myItemName name')
  312. .text(itemBlueprint.name)
  313. );
  314. }
  315. parentRow // always added because it spreads pushes name left and value right !
  316. .append(
  317. $('<div/>')
  318. .addClass('myItemValue')
  319. .text(itemBlueprint?.extra || '')
  320. );
  321. if(itemBlueprint?.value) {
  322. parentRow
  323. .append(
  324. $('<div/>')
  325. .addClass('myItemWorth')
  326. .text(itemBlueprint.value)
  327. )
  328. }
  329. return parentRow;
  330. }
  331.  
  332. function createRow_Input(inputBlueprint) {
  333. const parentRow = $('<div/>').addClass('customRow myItemInputRowAdjustment');
  334. if(inputBlueprint.text) {
  335. parentRow
  336. .append(
  337. $('<div/>')
  338. .addClass('myItemInputText')
  339. .addClass(inputBlueprint.class || '')
  340. .text(inputBlueprint.text)
  341. .css('flex', `${inputBlueprint.layout?.split('/')[0] || 1}`)
  342. )
  343. }
  344. parentRow
  345. .append(
  346. $('<input/>')
  347. .attr('id', inputBlueprint.id)
  348. .addClass('myItemInput')
  349. .addClass(inputBlueprint.class || '')
  350. .attr('type', inputBlueprint.inputType || 'text')
  351. .attr('placeholder', inputBlueprint.name)
  352. .attr('value', inputBlueprint.value || '')
  353. .css('flex', `${inputBlueprint.layout?.split('/')[1] || 1}`)
  354. .keyup(inputDelay(function(e) {
  355. inputBlueprint.value = e.target.value;
  356. inputBlueprint.action(inputBlueprint.value);
  357. }, inputBlueprint.delay || 0))
  358. )
  359. return parentRow;
  360. }
  361.  
  362. function createRow_Break(breakBlueprint) {
  363. const parentRow = $('<div/>').addClass('customRow');
  364. parentRow.append('<br/>');
  365. return parentRow;
  366. }
  367.  
  368. function createRow_Button(buttonBlueprint) {
  369. const parentRow = $('<div/>').addClass('customRow myItemInputRowAdjustment');
  370. for(const button of buttonBlueprint.buttons) {
  371. parentRow
  372. .append(
  373. $(`<button class='myButton'>${button.text}</button>`)
  374. .css('background-color', button.disabled ? '#ffffff0a' : colorMapper(button.color || 'primary'))
  375. .css('flex', `${button.size || 1} 1 0`)
  376. .prop('disabled', !!button.disabled)
  377. .addClass(button.class || '')
  378. .click(button.action)
  379. );
  380. }
  381. return parentRow;
  382. }
  383.  
  384. function createRow_Select(selectBlueprint) {
  385. const parentRow = $('<div/>').addClass('customRow myItemInputRowAdjustment');
  386. const select = $('<select/>')
  387. .addClass('myItemSelect')
  388. .addClass(selectBlueprint.class || '')
  389. .change(inputDelay(function(e) {
  390. for(const option of selectBlueprint.options) {
  391. option.selected = this.value === option.value;
  392. }
  393. selectBlueprint.action(this.value);
  394. }, selectBlueprint.delay || 0));
  395. for(const option of selectBlueprint.options) {
  396. select.append(`<option value='${option.value}' ${option.selected ? 'selected' : ''}>${option.text}</option>`);
  397. }
  398. parentRow.append(select);
  399. return parentRow;
  400. }
  401.  
  402. function createRow_Header(headerBlueprint) {
  403. const parentRow =
  404. $('<div/>')
  405. .addClass('myHeader lineTop')
  406. if(headerBlueprint.image) {
  407. parentRow.append(createImage(headerBlueprint));
  408. }
  409. parentRow.append(
  410. $('<div/>')
  411. .addClass('myName')
  412. .text(headerBlueprint.title)
  413. )
  414. if(headerBlueprint.action) {
  415. parentRow
  416. .append(
  417. $('<button/>')
  418. .addClass('myHeaderAction')
  419. .text(headerBlueprint.name)
  420. .attr('type', 'button')
  421. .css('background-color', colorMapper(headerBlueprint.color || 'success'))
  422. .click(headerBlueprint.action)
  423. )
  424. } else if(headerBlueprint.textRight) {
  425. parentRow.append(
  426. $('<div/>')
  427. .addClass('level')
  428. .text(headerBlueprint.title)
  429. .css('margin-left', 'auto')
  430. .html(headerBlueprint.textRight)
  431. )
  432. }
  433. if(headerBlueprint.centered) {
  434. parentRow.css('justify-content', 'center');
  435. }
  436. return parentRow;
  437. }
  438.  
  439. function createRow_Checkbox(checkboxBlueprint) {
  440. const checked_false = `<svg xmlns='http://www.w3.org/2000/svg' width='44' height='44' viewBox='0 0 24 24' stroke-width='1.5' stroke='currentColor' fill='none' stroke-linecap='round' stroke-linejoin='round' class='customCheckBoxDisabled ng-star-inserted'><path stroke='none' d='M0 0h24v24H0z' fill='none'></path><rect x='4' y='4' width='16' height='16' rx='2'></rect></svg>`;
  441. const checked_true = `<svg xmlns='http://www.w3.org/2000/svg' width='44' height='44' viewBox='0 0 24 24' stroke-width='1.5' stroke='currentColor' fill='none' stroke-linecap='round' stroke-linejoin='round' class='customCheckBoxEnabled ng-star-inserted'><path stroke='none' d='M0 0h24v24H0z' fill='none'></path><rect x='4' y='4' width='16' height='16' rx='2'></rect><path d='M9 12l2 2l4 -4'></path></svg>`;
  442.  
  443. const buttonInnerHTML = checkboxBlueprint.checked ? checked_true : checked_false;
  444.  
  445. const parentRow = $('<div/>').addClass('customRow')
  446. .append(
  447. $('<div/>')
  448. .addClass('customCheckBoxText')
  449. .text(checkboxBlueprint?.text || '')
  450. )
  451. .append(
  452. $('<div/>')
  453. .addClass('customCheckboxCheckbox')
  454. .append(
  455. $(`<button>${buttonInnerHTML}</button>`)
  456. .html(buttonInnerHTML)
  457. .click(() => {
  458. checkboxBlueprint.checked = !checkboxBlueprint.checked;
  459. checkboxBlueprint.action(checkboxBlueprint.checked);
  460. })
  461. )
  462.  
  463. );
  464.  
  465. return parentRow;
  466. }
  467.  
  468. function createRow_Segment(segmentBlueprint) {
  469. if(segmentBlueprint.hidden) {
  470. return;
  471. }
  472. return segmentBlueprint.rows.flatMap(createRow);
  473. }
  474.  
  475. function createRow_Progress(progressBlueprint) {
  476. const parentRow = $('<div/>').addClass('customRow');
  477. const up = progressBlueprint.numerator;
  478. const down = progressBlueprint.denominator;
  479. parentRow.append(
  480. $('<div/>')
  481. .addClass('myBar')
  482. .append(
  483. $('<div/>')
  484. .css('height', '100%')
  485. .css('width', progressBlueprint.progressPercent + '%')
  486. .css('background-color', colorMapper(progressBlueprint.color || 'rgb(122, 118, 118)'))
  487. )
  488. );
  489. parentRow.append(
  490. $('<div/>')
  491. .addClass('myPercent')
  492. .text(progressBlueprint.progressPercent + '%')
  493. )
  494. parentRow.append(
  495. $('<div/>')
  496. .css('margin-left', 'auto')
  497. .text(progressBlueprint.progressText)
  498. )
  499. return parentRow;
  500. }
  501.  
  502. function createRow_Chart(chartBlueprint) {
  503. const parentRow = $('<div/>')
  504. .addClass('lineTop')
  505. .append(
  506. $('<canvas/>')
  507. .attr('id', chartBlueprint.chartId)
  508. );
  509. return parentRow;
  510. }
  511.  
  512. function createRow_List(listBlueprint) {
  513. const parentRow = $('<div/>').addClass('customRow');
  514. parentRow // always added because it spreads pushes name left and value right !
  515. .append(
  516. $('<ul/>')
  517. .addClass('myListDescription')
  518. .append(...listBlueprint.entries.map(entry =>
  519. $('<li/>')
  520. .addClass('myListLine')
  521. .text(entry)
  522. ))
  523. );
  524. return parentRow;
  525. }
  526. function createImage(blueprint) {
  527. return $('<div/>')
  528. .addClass('myItemImage image')
  529. .append(
  530. $('<img/>')
  531. .attr('src', `${blueprint.image}`)
  532. .css('filter', `${blueprint.imageFilter}`)
  533. .css('image-rendering', blueprint.imagePixelated ? 'pixelated' : 'auto')
  534. )
  535. }
  536.  
  537. function changeTab(blueprint, index) {
  538. blueprint.selectedTabIndex = index;
  539. addComponent(blueprint);
  540. }
  541.  
  542. function inputDelay(callback, ms) {
  543. var timer = 0;
  544. return function() {
  545. var context = this, args = arguments;
  546. window.clearTimeout(timer);
  547. timer = window.setTimeout(function() {
  548. callback.apply(context, args);
  549. }, ms || 0);
  550. };
  551. }
  552.  
  553. function search(blueprint, query) {
  554. if(!blueprint.idMappings) {
  555. generateIdMappings(blueprint);
  556. }
  557. if(!blueprint.idMappings[query]) {
  558. throw `Could not find id ${query} in blueprint ${blueprint.componentId}`;
  559. }
  560. return blueprint.idMappings[query];
  561. }
  562.  
  563. function generateIdMappings(blueprint) {
  564. blueprint.idMappings = {};
  565. for(const tab of blueprint.tabs) {
  566. addIdMapping(blueprint, tab);
  567. for(const row of tab.rows) {
  568. addIdMapping(blueprint, row);
  569. }
  570. }
  571. }
  572.  
  573. function addIdMapping(blueprint, element) {
  574. if(element.id) {
  575. if(blueprint.idMappings[element.id]) {
  576. throw `Detected duplicate id ${element.id} in blueprint ${blueprint.componentId}`;
  577. }
  578. blueprint.idMappings[element.id] = element;
  579. }
  580. let subelements = null;
  581. if(element.type === 'segment') {
  582. subelements = element.rows;
  583. }
  584. if(element.type === 'buttons') {
  585. subelements = element.buttons;
  586. }
  587. if(subelements) {
  588. for(const subelement of subelements) {
  589. addIdMapping(blueprint, subelement);
  590. }
  591. }
  592. }
  593.  
  594. const styles = `
  595. :root {
  596. --background-color: ${colorMapper('componentRegular')};
  597. --border-color: ${colorMapper('componentLight')};
  598. --darker-color: ${colorMapper('componentDark')};
  599. }
  600. .customComponent {
  601. margin-top: var(--gap);
  602. background-color: var(--background-color);
  603. box-shadow: 0 6px 12px -6px #0006;
  604. border-radius: 4px;
  605. width: 100%;
  606. }
  607. .myHeader {
  608. display: flex;
  609. align-items: center;
  610. padding: 12px var(--gap);
  611. gap: var(--gap);
  612. }
  613. .myName {
  614. font-weight: 600;
  615. letter-spacing: .25px;
  616. }
  617. .myHeaderAction{
  618. margin: 0px 0px 0px auto;
  619. border: 1px solid var(--border-color);
  620. border-radius: 4px;
  621. padding: 0px 5px;
  622. }
  623. .customRow {
  624. display: flex;
  625. justify-content: center;
  626. align-items: center;
  627. border-top: 1px solid var(--border-color);
  628. padding: 5px 12px 5px 6px;
  629. min-height: 0px;
  630. min-width: 0px;
  631. gap: var(--margin);
  632. }
  633. .myItemImage {
  634. position: relative;
  635. display: flex;
  636. align-items: center;
  637. justify-content: center;
  638. height: 24px;
  639. width: 24px;
  640. min-height: 0px;
  641. min-width: 0px;
  642. }
  643. .myItemImage > img {
  644. max-width: 100%;
  645. max-height: 100%;
  646. width: 100%;
  647. height: 100%;
  648. }
  649. .myItemValue {
  650. display: flex;
  651. align-items: center;
  652. flex: 1;
  653. color: #aaa;
  654. }
  655. .myItemInputText {
  656. height: 40px;
  657. width: 100%;
  658. display: flex;
  659. align-items: center;
  660. padding: 12px var(--gap);
  661. }
  662. .myItemInput {
  663. height: 40px;
  664. width: 100%;
  665. background-color: #ffffff0a;
  666. padding: 0 12px;
  667. text-align: center;
  668. border-radius: 4px;
  669. border: 1px solid var(--border-color);
  670. }
  671. .myItemInputRowAdjustment {
  672. padding-right: 6px !important;
  673. }
  674. .myItemSelect {
  675. height: 40px;
  676. width: 100%;
  677. background-color: #ffffff0a;
  678. padding: 0 12px;
  679. text-align: center;
  680. border-radius: 4px;
  681. border: 1px solid var(--border-color);
  682. }
  683. .myItemSelect > option {
  684. background-color: var(--darker-color);
  685. }
  686. .myButton {
  687. flex: 1;
  688. display: flex;
  689. align-items: center;
  690. justify-content: center;
  691. border-radius: 4px;
  692. height: 40px;
  693. font-weight: 600;
  694. letter-spacing: .25px;
  695. }
  696. .myButton[disabled] {
  697. pointer-events: none;
  698. }
  699. .sort {
  700. padding: 12px var(--gap);
  701. border-top: 1px solid var(--border-color);
  702. display: flex;
  703. align-items: center;
  704. justify-content: space-between;
  705. }
  706. .sortButtonContainer {
  707. display: flex;
  708. align-items: center;
  709. border-radius: 4px;
  710. box-shadow: 0 1px 2px #0003;
  711. border: 1px solid var(--border-color);
  712. overflow: hidden;
  713. }
  714. .sortButton {
  715. display: flex;
  716. border: none;
  717. background: transparent;
  718. font-family: inherit;
  719. font-size: inherit;
  720. line-height: 1.5;
  721. font-weight: inherit;
  722. color: inherit;
  723. resize: none;
  724. text-transform: inherit;
  725. letter-spacing: inherit;
  726. cursor: pointer;
  727. padding: 4px var(--gap);
  728. flex: 1;
  729. text-align: center;
  730. justify-content: center;
  731. background-color: var(--darker-color);
  732. }
  733. .tabs {
  734. display: flex;
  735. align-items: center;
  736. overflow: hidden;
  737. border-radius: inherit;
  738. }
  739. .tabButton {
  740. border: none;
  741. border-radius: 0px !important;
  742. background: transparent;
  743. font-family: inherit;
  744. font-size: inherit;
  745. line-height: 1.5;
  746. color: inherit;
  747. resize: none;
  748. text-transform: inherit;
  749. cursor: pointer;
  750. flex: 1;
  751. display: flex;
  752. align-items: center;
  753. justify-content: center;
  754. height: 48px;
  755. font-weight: 600;
  756. letter-spacing: .25px;
  757. padding: 0 var(--gap);
  758. border-radius: 4px 0 0;
  759. }
  760. .tabButtonInactive{
  761. background-color: var(--darker-color);
  762. }
  763. .lineRight {
  764. border-right: 1px solid var(--border-color);
  765. }
  766. .lineLeft {
  767. border-left: 1px solid var(--border-color);
  768. }
  769. .lineTop {
  770. border-top: 1px solid var(--border-color);
  771. }
  772. .customCheckBoxText {
  773. flex: 1;
  774. color: #aaa
  775. }
  776. .customCheckboxCheckbox {
  777. display: flex;
  778. justify-content: flex-end;
  779. min-width: 32px;
  780. margin-left: var(--margin);
  781. }
  782. .customCheckBoxEnabled {
  783. color: #53bd73
  784. }
  785. .customCheckBoxDisabled {
  786. color: #aaa
  787. }
  788. .myBar {
  789. height: 12px;
  790. flex: 1;
  791. background-color: #ffffff0a;
  792. overflow: hidden;
  793. max-width: 50%;
  794. border-radius: 999px;
  795. }
  796. .myPercent {
  797. margin-left: var(--margin);
  798. margin-right: var(--margin);
  799. color: #aaa;
  800. }
  801. .myListDescription {
  802. list-style: disc;
  803. width: 100%;
  804. }
  805. .myListLine {
  806. margin-left: 20px;
  807. }
  808. `;
  809.  
  810. initialise();
  811.  
  812. return exports;
  813. }
  814. );
  815. // configuration
  816. window.moduleRegistry.add('configuration', (auth, request, Promise) => {
  817.  
  818. const loaded = new Promise.Deferred();
  819.  
  820. const exports = {
  821. ready: loaded.promise,
  822. registerCheckbox,
  823. registerInput,
  824. registerDropdown,
  825. registerJson,
  826. items: []
  827. };
  828.  
  829. async function initialise() {
  830. await load();
  831. }
  832.  
  833. const CHECKBOX_KEYS = ['category', 'key', 'name', 'default', 'handler'];
  834. function registerCheckbox(item) {
  835. validate(item, CHECKBOX_KEYS);
  836. return register(Object.assign(item, {
  837. type: 'checkbox'
  838. }));
  839. }
  840.  
  841. const INPUT_KEYS = ['category', 'key', 'name', 'default', 'inputType', 'handler'];
  842. function registerInput(item) {
  843. validate(item, INPUT_KEYS);
  844. return register(Object.assign(item, {
  845. type: 'input'
  846. }));
  847. }
  848.  
  849. const DROPDOWN_KEYS = ['category', 'key', 'name', 'options', 'default', 'handler'];
  850. function registerDropdown(item) {
  851. validate(item, DROPDOWN_KEYS);
  852. return register(Object.assign(item, {
  853. type: 'dropdown'
  854. }));
  855. }
  856.  
  857. const JSON_KEYS = ['key', 'default', 'handler'];
  858. function registerJson(item) {
  859. validate(item, JSON_KEYS);
  860. return register(Object.assign(item, {
  861. type: 'json'
  862. }));
  863. }
  864.  
  865. function register(item) {
  866. const handler = item.handler;
  867. item.handler = (value, isInitial) => {
  868. item.value = value;
  869. handler(value, item.key, isInitial);
  870. if(!isInitial) {
  871. save(item, value);
  872. }
  873. }
  874. exports.items.push(item);
  875. return item;
  876. }
  877.  
  878. async function load() {
  879. const configs = await request.getConfigurations();
  880. for(const item of exports.items) {
  881. let value;
  882. if(configs[item.key]) {
  883. value = JSON.parse(configs[item.key]);
  884. } else {
  885. value = item.default;
  886. }
  887. item.handler(value, true);
  888. }
  889. loaded.resolve();
  890. }
  891.  
  892. async function save(item, value) {
  893. if(item.type === 'toggle') {
  894. value = !!value;
  895. }
  896. if(item.type === 'input' || item.type === 'json') {
  897. value = JSON.stringify(value);
  898. }
  899. await request.saveConfiguration(item.key, value);
  900. }
  901.  
  902. function validate(item, keys) {
  903. for(const key of keys) {
  904. if(!(key in item)) {
  905. throw `Missing ${key} while registering a configuration item`;
  906. }
  907. }
  908. }
  909.  
  910. initialise();
  911.  
  912. return exports;
  913.  
  914. }
  915. );
  916. // elementCreator
  917. window.moduleRegistry.add('elementCreator', () => {
  918.  
  919. const exports = {
  920. addStyles
  921. };
  922.  
  923. function addStyles(css) {
  924. const head = document.getElementsByTagName('head')[0]
  925. if(!head) {
  926. console.error('Could not add styles, missing head');
  927. return;
  928. }
  929. const style = document.createElement('style');
  930. style.type = 'text/css';
  931. style.innerHTML = css;
  932. head.appendChild(style);
  933. }
  934.  
  935. return exports;
  936.  
  937. }
  938. );
  939. // elementWatcher
  940. window.moduleRegistry.add('elementWatcher', (Promise) => {
  941.  
  942. const exports = {
  943. exists,
  944. childAdded,
  945. childAddedContinuous
  946. }
  947.  
  948. const $ = window.$;
  949.  
  950. async function exists(selector) {
  951. const promiseWrapper = new Promise.Checking(() => {
  952. return $(selector)[0];
  953. }, 10, 5000);
  954. return promiseWrapper.promise;
  955. }
  956.  
  957. async function childAdded(selector) {
  958. const promiseWrapper = new Promise.Expiring(5000);
  959.  
  960. try {
  961. const parent = await exists(selector);
  962. const observer = new MutationObserver(function(mutations, observer) {
  963. for(const mutation of mutations) {
  964. if(mutation.addedNodes?.length) {
  965. observer.disconnect();
  966. promiseWrapper.resolve();
  967. }
  968. }
  969. });
  970. observer.observe(parent, { childList: true });
  971. } catch(error) {
  972. promiseWrapper.reject(error);
  973. }
  974.  
  975. return promiseWrapper.promise;
  976. }
  977.  
  978. async function childAddedContinuous(selector, callback) {
  979. const parent = await exists(selector);
  980. const observer = new MutationObserver(function(mutations, observer) {
  981. for(const mutation of mutations) {
  982. if(mutation.addedNodes?.length) {
  983. callback();
  984. }
  985. }
  986. });
  987. observer.observe(parent, { childList: true });
  988. }
  989.  
  990. return exports;
  991.  
  992. }
  993. );
  994. // estimationCache
  995. window.moduleRegistry.add('estimationCache', (events, request, configuration, itemCache, userCache, util) => {
  996.  
  997. const registerPageHandler = events.register.bind(null, 'page');
  998. const registerXhrHandler = events.register.bind(null, 'xhr');
  999. const registerUserCacheHandler = events.register.bind(null, 'userCache');
  1000. const getLastPage = events.getLast.bind(null, 'page');
  1001. const getLastEstimation = events.getLast.bind(null, 'estimation');
  1002. const emitEvent = events.emit.bind(null, 'estimation');
  1003.  
  1004. let enabled = false;
  1005. let cache = {};
  1006.  
  1007. function initialise() {
  1008. configuration.registerCheckbox({
  1009. category: 'UI Features',
  1010. key: 'estimations',
  1011. name: 'Estimations',
  1012. default: true,
  1013. handler: handleConfigStateChange
  1014. });
  1015.  
  1016. registerPageHandler(handlePage);
  1017. registerXhrHandler(handleXhr);
  1018. registerUserCacheHandler(handleUserCache);
  1019. }
  1020.  
  1021. function handleConfigStateChange(state) {
  1022. const previous = enabled;
  1023. enabled = state;
  1024. if(!enabled) {
  1025. emitEvent(null);
  1026. }
  1027. if(enabled && !previous) {
  1028. handlePage(getLastPage());
  1029. }
  1030. }
  1031.  
  1032. async function handlePage(page) {
  1033. emitEvent(null);
  1034. let result = null;
  1035. if(!enabled || !page) {
  1036. result = null;
  1037. } else if(page.type === 'action') {
  1038. const cacheKey = `action-${page.skill}-${page.action}`;
  1039. const fetcher = getAction.bind(null, page.skill, page.action);
  1040. result = await getEstimationData(cacheKey, fetcher);
  1041. } else if(page.type === 'automation') {
  1042. const cacheKey = `automation-${page.action}`;
  1043. const fetcher = getAutomation.bind(null, page.action);
  1044. result = await getEstimationData(cacheKey, fetcher);
  1045. }
  1046. // it could have changed by now
  1047. if(enabled && page === getLastPage()) {
  1048. emitEvent(result);
  1049. }
  1050. }
  1051.  
  1052. function handleXhr(xhr) {
  1053. if(xhr.url.endsWith('/time')) {
  1054. return;
  1055. }
  1056. cache = {};
  1057. emitEvent(null);
  1058. handlePage(getLastPage());
  1059. }
  1060.  
  1061. async function handleUserCache() {
  1062. await updateAll();
  1063. emitEvent(getLastEstimation());
  1064. }
  1065.  
  1066. async function getEstimationData(cacheKey, fetcher) {
  1067. const estimation = cache[cacheKey] || await fetcher();
  1068. cache[cacheKey] = estimation;
  1069. return estimation;
  1070. }
  1071.  
  1072. async function getAction(skill, action) {
  1073. const result = await request.getActionEstimation(skill, action);
  1074. result.actionId = action;
  1075. return convertEstimation(result);
  1076. }
  1077.  
  1078. async function getAutomation(action) {
  1079. const result = await request.getAutomationEstimation(action);
  1080. result.actionId = action;
  1081. return convertEstimation(result);
  1082. }
  1083.  
  1084. async function convertEstimation(estimation) {
  1085. await itemCache.ready;
  1086. const loot = estimation.loot;
  1087. const materials = estimation.materials;
  1088. const equipments = estimation.equipments;
  1089. estimation.loot = [];
  1090. for(const entry of Object.entries(loot)) {
  1091. estimation.loot.push({
  1092. item: itemCache.byId[entry[0]],
  1093. amount: entry[1],
  1094. gold: entry[1] * (itemCache.byId[entry[0]].attributes.SELL_PRICE || 0)
  1095. });
  1096. }
  1097. estimation.materials = [];
  1098. for(const entry of Object.entries(materials)) {
  1099. estimation.materials.push({
  1100. item: itemCache.byId[entry[0]],
  1101. amount: entry[1],
  1102. stored: 0,
  1103. secondsLeft: 0,
  1104. gold: entry[1] * (itemCache.byId[entry[0]].attributes.SELL_PRICE || 0)
  1105. });
  1106. }
  1107. estimation.equipments = [];
  1108. for(const entry of Object.entries(equipments)) {
  1109. estimation.equipments.push({
  1110. item: itemCache.byId[entry[0]],
  1111. amount: entry[1],
  1112. stored: 0,
  1113. secondsLeft: 0,
  1114. gold: entry[1] * (itemCache.byId[entry[0]].attributes.SELL_PRICE || 0)
  1115. });
  1116. }
  1117. estimation.goldLoot = estimation.loot.map(a => a.gold).reduce((a,v) => a+v, 0);
  1118. estimation.goldMaterials = estimation.materials.map(a => a.gold).reduce((a,v) => a+v, 0);
  1119. estimation.goldEquipments = estimation.equipments.map(a => a.gold).reduce((a,v) => a+v, 0);
  1120. estimation.goldTotal = estimation.goldLoot - estimation.goldMaterials - estimation.goldEquipments;
  1121. await updateOne(estimation);
  1122. return estimation;
  1123. }
  1124.  
  1125. async function updateAll() {
  1126. if(!enabled) {
  1127. return;
  1128. }
  1129. for(const estimation of Object.values(cache)) {
  1130. await updateOne(estimation);
  1131. }
  1132. }
  1133.  
  1134. async function updateOne(estimation) {
  1135. await userCache.ready;
  1136. for(const material of estimation.materials) {
  1137. material.stored = userCache.inventory[material.item.id] || 0;
  1138. material.secondsLeft = material.stored / material.amount * 3600;
  1139. }
  1140. for(const equipment of estimation.equipments) {
  1141. equipment.stored = userCache.equipment[equipment.item.id] || 0;
  1142. equipment.secondsLeft = equipment.stored / equipment.amount * 3600;
  1143. }
  1144. if(estimation.type === 'AUTOMATION' && userCache.automations[estimation.actionId]) {
  1145. estimation.amountSecondsLeft = estimation.actionSpeed * (userCache.automations[estimation.actionId].maxAmount - userCache.automations[estimation.actionId].amount);
  1146. } else if(estimation.maxAmount) {
  1147. estimation.amountSecondsLeft = estimation.actionSpeed * (estimation.maxAmount - userCache.action.amount);
  1148. } else {
  1149. estimation.amountSecondsLeft = Number.MAX_VALUE;
  1150. }
  1151. if(estimation.type === 'AUTOMATION' && estimation.amountSecondsLeft !== Number.MAX_VALUE) {
  1152. estimation.secondsLeft = estimation.amountSecondsLeft;
  1153. } else {
  1154. estimation.secondsLeft = Math.min(
  1155. estimation.amountSecondsLeft,
  1156. ...estimation.materials.map(a => a.secondsLeft),
  1157. ...estimation.equipments.map(a => a.secondsLeft)
  1158. );
  1159. }
  1160. const currentExp = userCache.exp[estimation.skill];
  1161. estimation.secondsToNextlevel = util.expToNextLevel(currentExp) / estimation.exp * 3600;
  1162. estimation.secondsToNextTier = util.expToNextTier(currentExp) / estimation.exp * 3600;
  1163. }
  1164.  
  1165. initialise();
  1166.  
  1167. }
  1168. );
  1169. // events
  1170. window.moduleRegistry.add('events', () => {
  1171.  
  1172. const exports = {
  1173. register,
  1174. emit,
  1175. getLast
  1176. };
  1177.  
  1178. const handlers = {};
  1179. const lastCache = {};
  1180.  
  1181. function register(name, handler) {
  1182. if(!handlers[name]) {
  1183. handlers[name] = [];
  1184. }
  1185. handlers[name].push(handler);
  1186. if(lastCache[name]) {
  1187. handle(handler, lastCache[name]);
  1188. }
  1189. }
  1190.  
  1191. // options = { skipCache }
  1192. function emit(name, data, options) {
  1193. if(!options?.skipCache) {
  1194. lastCache[name] = data;
  1195. }
  1196. if(!handlers[name]) {
  1197. return;
  1198. }
  1199. for(const handler of handlers[name]) {
  1200. handle(handler, data);
  1201. }
  1202. }
  1203.  
  1204. function handle(handler, data) {
  1205. try {
  1206. handler(data);
  1207. } catch(e) {
  1208. console.error('Something went wrong', e);
  1209. }
  1210. }
  1211.  
  1212. function getLast(name) {
  1213. return lastCache[name];
  1214. }
  1215.  
  1216. return exports;
  1217.  
  1218. }
  1219. );
  1220. // interceptor
  1221. window.moduleRegistry.add('interceptor', (events) => {
  1222.  
  1223. function initialise() {
  1224. registerInterceptorXhr();
  1225. registerInterceptorUrlChange();
  1226. events.emit('url', window.location.href);
  1227. }
  1228.  
  1229. function registerInterceptorXhr() {
  1230. const XHR = XMLHttpRequest.prototype;
  1231. const open = XHR.open;
  1232. const send = XHR.send;
  1233. const setRequestHeader = XHR.setRequestHeader;
  1234.  
  1235. XHR.open = function() {
  1236. this._requestHeaders = {};
  1237. return open.apply(this, arguments);
  1238. }
  1239. XHR.setRequestHeader = function(header, value) {
  1240. this._requestHeaders[header] = value;
  1241. return setRequestHeader.apply(this, arguments);
  1242. }
  1243. XHR.send = function() {
  1244. let requestBody = undefined;
  1245. try {
  1246. requestBody = JSON.parse(arguments[0]);
  1247. } catch(e) {}
  1248. this.addEventListener('load', function() {
  1249. const status = this.status
  1250. const url = this.responseURL;
  1251. console.debug(`intercepted ${url}`);
  1252. const responseHeaders = this.getAllResponseHeaders();
  1253. if(this.responseType === 'blob') {
  1254. return;
  1255. }
  1256. const responseBody = extractResponseFromXMLHttpRequest(this);
  1257. events.emit('xhr', {
  1258. url,
  1259. status,
  1260. request: requestBody,
  1261. response: responseBody
  1262. }, { skipCache:true });
  1263. })
  1264.  
  1265. return send.apply(this, arguments);
  1266. }
  1267. }
  1268.  
  1269. function extractResponseFromXMLHttpRequest(xhr) {
  1270. if(xhr.responseType === 'blob') {
  1271. return null;
  1272. }
  1273. let responseBody;
  1274. if(xhr.responseType === '' || xhr.responseType === 'text') {
  1275. try {
  1276. return JSON.parse(xhr.responseText);
  1277. } catch (err) {
  1278. console.debug('Error reading or processing response.', err);
  1279. }
  1280. }
  1281. return xhr.response;
  1282. }
  1283.  
  1284. function registerInterceptorUrlChange() {
  1285. const pushState = history.pushState;
  1286. history.pushState = function() {
  1287. pushState.apply(history, arguments);
  1288. console.debug(`Detected page ${arguments[2]}`);
  1289. events.emit('url', arguments[2]);
  1290. };
  1291. const replaceState = history.replaceState;
  1292. history.replaceState = function() {
  1293. replaceState.apply(history, arguments);
  1294. console.debug(`Detected page ${arguments[2]}`);
  1295. events.emit('url', arguments[2]);
  1296. }
  1297. }
  1298.  
  1299. initialise();
  1300.  
  1301. }
  1302. );
  1303. // itemCache
  1304. window.moduleRegistry.add('itemCache', (auth, request, Promise) => {
  1305.  
  1306. const authenticated = auth.ready;
  1307. const isReady = new Promise.Deferred();
  1308.  
  1309. const exports = {
  1310. ready: isReady.promise,
  1311. list: [],
  1312. byId: null,
  1313. byName: null,
  1314. byImage: null,
  1315. attributes: null
  1316. };
  1317.  
  1318. async function initialise() {
  1319. await authenticated;
  1320. const enrichedItems = await request.listItems();
  1321. exports.byId = {};
  1322. exports.byName = {};
  1323. exports.byImage = {};
  1324. for(const enrichedItem of enrichedItems) {
  1325. const item = Object.assign(enrichedItem.item, enrichedItem);
  1326. delete item.item;
  1327. exports.list.push(item);
  1328. exports.byId[item.id] = item;
  1329. exports.byName[item.name] = item;
  1330. const lastPart = item.image.split('/').at(-1);
  1331. if(exports.byImage[lastPart]) {
  1332. exports.byImage[lastPart].duplicate = true;
  1333. } else {
  1334. exports.byImage[lastPart] = item;
  1335. }
  1336. if(!item.attributes) {
  1337. item.attributes = {};
  1338. }
  1339. if(item.charcoal) {
  1340. item.attributes.CHARCOAL = item.charcoal;
  1341. }
  1342. if(item.compost) {
  1343. item.attributes.COMPOST = item.compost;
  1344. }
  1345. }
  1346. for(const image of Object.keys(exports.byImage)) {
  1347. if(exports.byImage[image].duplicate) {
  1348. exports.byImage[image];
  1349. }
  1350. }
  1351. exports.attributes = await request.listItemAttributes();
  1352. exports.attributes.push({
  1353. technicalName: 'CHARCOAL',
  1354. name: 'Charcoal',
  1355. image: '/assets/items/charcoal.png'
  1356. },{
  1357. technicalName: 'COMPOST',
  1358. name: 'Compost',
  1359. image: '/assets/misc/compost.png'
  1360. });
  1361. isReady.resolve();
  1362. }
  1363.  
  1364. initialise();
  1365.  
  1366. return exports;
  1367.  
  1368. }
  1369. );
  1370. // pageDetector
  1371. window.moduleRegistry.add('pageDetector', (auth, events) => {
  1372.  
  1373. const authenticated = auth.ready;
  1374. const registerUrlHandler = events.register.bind(null, 'url');
  1375. const emitEvent = events.emit.bind(null, 'page');
  1376.  
  1377. async function initialise() {
  1378. await authenticated;
  1379. registerUrlHandler(handleUrl);
  1380. }
  1381.  
  1382. function handleUrl(url) {
  1383. let result = null;
  1384. const parts = url.split('/');
  1385. if(url.includes('/skill/') && url.includes('/action/')) {
  1386. result = {
  1387. type: 'action',
  1388. skill: +parts[parts.length-3],
  1389. action: +parts[parts.length-1]
  1390. };
  1391. } else if(url.includes('house/produce')) {
  1392. result = {
  1393. type: 'automation',
  1394. building: +parts[parts.length-2],
  1395. action: +parts[parts.length-1]
  1396. };
  1397. } else if(url.includes('house/build')) {
  1398. result = {
  1399. type: 'structure',
  1400. building: +parts[parts.length-1]
  1401. };
  1402. } else {
  1403. result = {
  1404. type: parts.pop()
  1405. };
  1406. }
  1407. emitEvent(result);
  1408. }
  1409.  
  1410. initialise();
  1411.  
  1412. }
  1413. );
  1414. // pages
  1415. window.moduleRegistry.add('pages', (elementWatcher, events, colorMapper, util, skillCache, elementCreator) => {
  1416.  
  1417. const registerPageHandler = events.register.bind(null, 'page');
  1418. const getLastPage = events.getLast.bind(null, 'page');
  1419.  
  1420. const exports = {
  1421. register,
  1422. requestRender,
  1423. show,
  1424. hide
  1425. }
  1426.  
  1427. const pages = [];
  1428.  
  1429. function initialise() {
  1430. registerPageHandler(handlePage);
  1431. elementCreator.addStyles(styles);
  1432. }
  1433.  
  1434. function handlePage(page) {
  1435. // handle navigating away
  1436. if(!pages.some(p => p.path === page.type)) {
  1437. $('custom-page').remove();
  1438. $('nav-component > div.nav > div.scroll > button')
  1439. .removeClass('customActiveLink');
  1440. $('header-component div.wrapper > div.image > img')
  1441. .css('image-rendering', '');
  1442. headerPageNameChangeBugFix(page);
  1443. }
  1444. }
  1445.  
  1446. async function register(page) {
  1447. if(pages.some(p => p.name === page.name)) {
  1448. console.error(`Custom page already registered : ${page.name}`);
  1449. return;
  1450. }
  1451. page.path = page.name.toLowerCase().replaceAll(' ', '-');
  1452. page.class = `customMenuButton_${page.path}`;
  1453. page.image = page.image || 'https://ironwoodrpg.com/assets/misc/settings.png';
  1454. page.category = page.category?.toUpperCase() || 'MISC';
  1455. page.columns = page.columns || 1;
  1456. pages.push(page);
  1457. console.debug('Registered pages', pages);
  1458. await setupNavigation(page);
  1459. }
  1460.  
  1461. function show(name) {
  1462. const page = pages.find(p => p.name === name)
  1463. if(!page) {
  1464. console.error(`Could not find page : ${name}`);
  1465. return;
  1466. }
  1467. $(`.${page.class}`).show();
  1468. }
  1469.  
  1470. function hide(name) {
  1471. const page = pages.find(p => p.name === name)
  1472. if(!page) {
  1473. console.error(`Could not find page : ${name}`);
  1474. return;
  1475. }
  1476. $(`.${page.class}`).hide();
  1477. }
  1478.  
  1479. function requestRender(name) {
  1480. const page = pages.find(p => p.name === name)
  1481. if(!page) {
  1482. console.error(`Could not find page : ${name}`);
  1483. return;
  1484. }
  1485. if(getLastPage()?.type === page.path) {
  1486. render(page);
  1487. }
  1488. }
  1489.  
  1490. function render(page) {
  1491. $('.customComponent').remove();
  1492. page.render();
  1493. }
  1494.  
  1495. async function setupNavigation(page) {
  1496. await elementWatcher.exists('div.nav > div.scroll');
  1497. // MENU HEADER / CATEGORY
  1498. let menuHeader = $(`nav-component > div.nav > div.scroll > div.header:contains('${page.category}'), div.customMenuHeader:contains('${page.category}')`);
  1499. if(!menuHeader.length) {
  1500. menuHeader = createMenuHeader(page.category);
  1501. }
  1502. // MENU BUTTON / PAGE LINK
  1503. const menuButton = createMenuButton(page)
  1504. // POSITIONING
  1505. if(page.after) {
  1506. $(`nav-component button:contains('${page.after}')`).after(menuButton);
  1507. } else {
  1508. menuHeader.after(menuButton);
  1509. }
  1510. }
  1511.  
  1512. function createMenuHeader(text) {
  1513. const menuHeader =
  1514. $('<div/>')
  1515. .addClass('header customMenuHeader')
  1516. .append(
  1517. $('<div/>')
  1518. .addClass('customMenuHeaderText')
  1519. .text(text)
  1520. );
  1521. $('nav-component > div.nav > div.scroll')
  1522. .prepend(menuHeader);
  1523. return menuHeader;
  1524. }
  1525.  
  1526. function createMenuButton(page) {
  1527. const menuButton =
  1528. $('<button/>')
  1529. .attr('type', 'button')
  1530. .addClass(`customMenuButton ${page.class}`)
  1531. .css('display', 'none')
  1532. .click(() => visitPage(page))
  1533. .append(
  1534. $('<img/>')
  1535. .addClass('customMenuButtonImage')
  1536. .attr('src', page.image)
  1537. .css('image-rendering', page.imagePixelated ? 'pixelated' : 'auto')
  1538. )
  1539. .append(
  1540. $('<div/>')
  1541. .addClass('customMenuButtonText')
  1542. .text(page.name)
  1543. );
  1544. return menuButton;
  1545. }
  1546.  
  1547. async function visitPage(page) {
  1548. if($('custom-page').length) {
  1549. $('custom-page').remove();
  1550. } else {
  1551. await setupEmptyPage();
  1552. }
  1553. createPage(page.columns);
  1554. updatePageHeader(page);
  1555. updateActivePageInNav(page.name);
  1556. history.pushState({}, '', page.path);
  1557. page.render();
  1558. }
  1559.  
  1560. async function setupEmptyPage() {
  1561. util.goToPage('settings');
  1562. await elementWatcher.exists('settings-page');
  1563. $('settings-page').remove();
  1564. }
  1565.  
  1566. function createPage(columnCount) {
  1567. const custompage = $('<custom-page/>');
  1568. const columns = $('<div/>')
  1569. .addClass('customGroups');
  1570. for(let columnIndex = 0; columnIndex < columnCount; columnIndex++) {
  1571. columns.append(
  1572. $('<div/>')
  1573. .addClass('customGroup')
  1574. .addClass(`column${columnIndex}`)
  1575. )
  1576. };
  1577. custompage.append(columns);
  1578. $('div.padding > div.wrapper > router-outlet').after(custompage);
  1579. }
  1580.  
  1581. function updatePageHeader(page) {
  1582. $('header-component div.wrapper > div.image > img')
  1583. .attr('src', page.image)
  1584. .css('image-rendering', page.imagePixelated ? 'pixelated' : 'auto');
  1585. $('header-component div.wrapper > div.title').text(page.name);
  1586. }
  1587.  
  1588. function updateActivePageInNav(name) {
  1589. //Set other pages as inactive
  1590. $(`nav-component > div.nav > div.scroll > button`)
  1591. .removeClass('active-link')
  1592. .removeClass('customActiveLink');
  1593. //Set this page as active
  1594. $(`nav-component > div.nav > div.scroll > button > div.customMenuButtonText:contains('${name}')`)
  1595. .parent()
  1596. .addClass('customActiveLink');
  1597. }
  1598.  
  1599. // hacky shit, idk why angular stops updating page header title ???
  1600. async function headerPageNameChangeBugFix(page) {
  1601. await elementWatcher.exists('nav-component > div.nav');
  1602. let headerName = null;
  1603. if(page.type === 'action') {
  1604. await skillCache.ready;
  1605. headerName = skillCache.byId[page.skill].name;
  1606. } else if(page.type === 'automation') {
  1607. headerName = 'House';
  1608. } else if(page.type === 'structure') {
  1609. headerName = 'House';
  1610. } else {
  1611. headerName = page.type;
  1612. headerName = headerName.charAt(0).toUpperCase() + headerName.slice(1);
  1613. }
  1614. $('header-component div.wrapper > div.title').text(headerName);
  1615. }
  1616.  
  1617. const styles = `
  1618. :root {
  1619. --background-color: ${colorMapper('componentRegular')};
  1620. --border-color: ${colorMapper('componentLight')};
  1621. --darker-color: ${colorMapper('componentDark')};
  1622. }
  1623. .customMenuHeader {
  1624. height: 56px;
  1625. display: flex;
  1626. align-items: center;
  1627. padding: 0 24px;
  1628. color: #aaa;
  1629. font-size: .875rem;
  1630. font-weight: 600;
  1631. letter-spacing: 1px;
  1632. text-transform: uppercase;
  1633. border-bottom: 1px solid var(--border-color);
  1634. background-color: var(--background-color);
  1635. }
  1636. .customMenuHeaderText {
  1637. flex: 1;
  1638. }
  1639. .customMenuButton {
  1640. border: none;
  1641. background: transparent;
  1642. font-family: inherit;
  1643. font-size: inherit;
  1644. line-height: 1.5;
  1645. font-weight: inherit;
  1646. color: inherit;
  1647. resize: none;
  1648. text-transform: inherit;
  1649. letter-spacing: inherit;
  1650. cursor: pointer;
  1651. height: 56px;
  1652. display: flex;
  1653. align-items: center;
  1654. padding: 0 24px;
  1655. border-bottom: 1px solid var(--border-color);
  1656. width: 100%;
  1657. text-align: left;
  1658. position: relative;
  1659. background-color: var(--background-color);
  1660. }
  1661. .customMenuButtonImage {
  1662. max-width: 100%;
  1663. max-height: 100%;
  1664. height: 32px;
  1665. width: 32px;
  1666. }
  1667. .customMenuButtonText {
  1668. margin-left: var(--margin);
  1669. flex: 1;
  1670. }
  1671. .customGroups {
  1672. display: flex;
  1673. gap: var(--gap);
  1674. flex-wrap: wrap;
  1675. }
  1676. .customGroup {
  1677. flex: 1;
  1678. min-width: 360px;
  1679. }
  1680. .customActiveLink {
  1681. background-color: var(--darker-color);
  1682. }
  1683. `;
  1684.  
  1685. initialise();
  1686.  
  1687. return exports
  1688. }
  1689. );
  1690. // Promise
  1691. window.moduleRegistry.add('Promise', () => {
  1692.  
  1693. class Deferred {
  1694. promise;
  1695. resolve;
  1696. reject;
  1697. isResolved = false;
  1698. constructor() {
  1699. this.promise = new Promise((resolve, reject)=> {
  1700. this.resolve = resolve;
  1701. this.reject = reject;
  1702. }).then(result => {
  1703. this.isResolved = true;
  1704. return result;
  1705. }).catch(error => {
  1706. if(error) {
  1707. console.warn(error);
  1708. }
  1709. throw error;
  1710. });
  1711. }
  1712. }
  1713.  
  1714. class Delayed extends Deferred {
  1715. constructor(timeout) {
  1716. super();
  1717. const timeoutReference = window.setTimeout(() => {
  1718. this.resolve();
  1719. }, timeout);
  1720. this.promise.finally(() => {
  1721. window.clearTimeout(timeoutReference)
  1722. });
  1723. }
  1724. }
  1725.  
  1726. class Expiring extends Deferred {
  1727. constructor(timeout) {
  1728. super();
  1729. const timeoutReference = window.setTimeout(() => {
  1730. this.reject(`Timed out after ${timeout} ms`);
  1731. }, timeout);
  1732. this.promise.finally(() => {
  1733. window.clearTimeout(timeoutReference)
  1734. });
  1735. }
  1736. }
  1737.  
  1738. class Checking extends Expiring {
  1739. #checker;
  1740. constructor(checker, interval, timeout) {
  1741. super(timeout);
  1742. this.#checker = checker;
  1743. this.#check();
  1744. const intervalReference = window.setInterval(this.#check.bind(this), interval);
  1745. this.promise.finally(() => {
  1746. window.clearInterval(intervalReference)
  1747. });
  1748. }
  1749. #check() {
  1750. const checkResult = this.#checker();
  1751. if(!checkResult) {
  1752. return;
  1753. }
  1754. this.resolve(checkResult);
  1755. }
  1756. }
  1757.  
  1758. return {
  1759. Deferred,
  1760. Delayed,
  1761. Expiring,
  1762. Checking
  1763. };
  1764.  
  1765. }
  1766. );
  1767. // request
  1768. window.moduleRegistry.add('request', (auth) => {
  1769.  
  1770. const authenticated = auth.ready;
  1771.  
  1772. const exports = makeRequest;
  1773.  
  1774. let CURRENT_REQUEST = null;
  1775.  
  1776. async function makeRequest(url, body) {
  1777. await authenticated;
  1778. await throttle();
  1779. const headers = auth.getHeaders();
  1780. const method = body ? 'POST' : 'GET';
  1781. try {
  1782. if(body) {
  1783. body = JSON.stringify(body);
  1784. }
  1785. CURRENT_REQUEST = fetch(`${window.PANCAKE_ROOT}/${url}`, {method, headers, body});
  1786. const fetchResponse = await CURRENT_REQUEST;
  1787. if(fetchResponse.status !== 200) {
  1788. console.error(await fetchResponse.text());
  1789. return;
  1790. }
  1791. try {
  1792. return await fetchResponse.json();
  1793. } catch(e) {
  1794. if(body) {
  1795. return 'OK';
  1796. }
  1797. }
  1798. } catch(e) {
  1799. console.error(e);
  1800. }
  1801. }
  1802.  
  1803. async function throttle() {
  1804. if(!CURRENT_REQUEST) {
  1805. CURRENT_REQUEST = Promise.resolve();
  1806. }
  1807. while(CURRENT_REQUEST) {
  1808. const waitingOn = CURRENT_REQUEST;
  1809. try {
  1810. await CURRENT_REQUEST;
  1811. } catch(e) { }
  1812. if(CURRENT_REQUEST === null) {
  1813. CURRENT_REQUEST = Promise.resolve();
  1814. continue;
  1815. }
  1816. if(CURRENT_REQUEST === waitingOn) {
  1817. CURRENT_REQUEST = null;
  1818. }
  1819. }
  1820. }
  1821.  
  1822. // alphabetical
  1823.  
  1824. makeRequest.getConfigurations = () => makeRequest('configuration');
  1825. makeRequest.saveConfiguration = (key, value) => makeRequest('configuration', {[key]: value});
  1826.  
  1827. makeRequest.getActionEstimation = (skill, action) => makeRequest(`estimation/action?skill=${skill}&action=${action}`);
  1828. makeRequest.getAutomationEstimation = (action) => makeRequest(`estimation/automation?id=${action}`);
  1829.  
  1830. makeRequest.getGuildMembers = () => makeRequest('guild/members');
  1831. makeRequest.registerGuildQuest = (itemId, amount) => makeRequest('guild/quest/register', {itemId, amount});
  1832. makeRequest.getGuildQuestStats = () => makeRequest('guild/quest/stats');
  1833. makeRequest.unregisterGuildQuest = (itemId) => makeRequest('guild/quest/unregister', {itemId});
  1834.  
  1835. makeRequest.getLeaderboardGuildRanks = () => makeRequest('leaderboard/ranks/guild');
  1836.  
  1837. makeRequest.listActions = () => makeRequest('list/action');
  1838. makeRequest.listItems = () => makeRequest('list/item');
  1839. makeRequest.listItemAttributes = () => makeRequest('list/itemAttributes');
  1840. makeRequest.listRecipes = () => makeRequest('list/recipe');
  1841. makeRequest.listSkills = () => makeRequest('list/skills');
  1842.  
  1843. makeRequest.getMarketConversion = () => makeRequest('market/conversions');
  1844. makeRequest.getMarketFilters = () => makeRequest('market/filters');
  1845. makeRequest.saveMarketFilter = (filter) => makeRequest('market/filters', filter);
  1846. makeRequest.removeMarketFilter = (id) => makeRequest(`market/filters/${id}/remove`);
  1847.  
  1848. makeRequest.saveWebhook = (webhook) => makeRequest('notification/webhook', webhook);
  1849.  
  1850. makeRequest.handleInterceptedRequest = (interceptedRequest) => makeRequest('request', interceptedRequest);
  1851.  
  1852. makeRequest.getChangelogs = () => makeRequest('settings/changelog');
  1853.  
  1854. return exports;
  1855.  
  1856. }
  1857. );
  1858. // skillCache
  1859. window.moduleRegistry.add('skillCache', (auth, request, Promise) => {
  1860.  
  1861. const authenticated = auth.ready;
  1862. const isReady = new Promise.Deferred();
  1863.  
  1864. const exports = {
  1865. ready: isReady.promise,
  1866. list: [],
  1867. byId: null,
  1868. byName: null,
  1869. };
  1870.  
  1871. async function initialise() {
  1872. await authenticated;
  1873. const skills = await request.listSkills();
  1874. exports.byId = {};
  1875. exports.byName = {};
  1876. for(const skill of skills) {
  1877. exports.list.push(skill);
  1878. exports.byId[skill.id] = skill;
  1879. exports.byName[skill.name] = skill;
  1880. }
  1881. isReady.resolve();
  1882. }
  1883.  
  1884. initialise();
  1885.  
  1886. return exports;
  1887.  
  1888. }
  1889. );
  1890. // toast
  1891. window.moduleRegistry.add('toast', (util, elementCreator) => {
  1892.  
  1893. const exports = {
  1894. create
  1895. };
  1896.  
  1897. function initialise() {
  1898. elementCreator.addStyles(styles);
  1899. }
  1900.  
  1901. async function create(text, time, image) {
  1902. const notificationId = `customNotification_${Date.now()}`
  1903. const notificationDiv =
  1904. $('<div/>')
  1905. .addClass('customNotification')
  1906. .attr('id', notificationId)
  1907. .append(
  1908. $('<div/>')
  1909. .addClass('customNotificationImageDiv')
  1910. .append(
  1911. $('<img/>')
  1912. .addClass('customNotificationImage')
  1913. .attr('src', `${image || 'https://ironwoodrpg.com/assets/misc/quests.png'}`)
  1914. )
  1915. )
  1916. .append(
  1917. $('<div/>')
  1918. .addClass('customNotificationDetails')
  1919. .text(text)
  1920. );
  1921. $('div.notifications').append(notificationDiv);
  1922. await util.sleep(time || 2000);
  1923. $(`#${notificationId}`).fadeOut('slow', () => {
  1924. $(`#${notificationId}`).remove();
  1925. });
  1926. }
  1927.  
  1928. const styles = `
  1929. .customNotification {
  1930. padding: 8px 16px 8px 12px;
  1931. border-radius: 4px;
  1932. backdrop-filter: blur(8px);
  1933. background: rgba(255,255,255,.15);
  1934. box-shadow: 0 8px 16px -4px #00000080;
  1935. display: flex;
  1936. align-items: center;
  1937. min-height: 48px;
  1938. margin-top: 12px;
  1939. }
  1940. .customNotificationImageDiv {
  1941. display: flex;
  1942. align-items: center;
  1943. justify-content: center;
  1944. width: 32px;
  1945. height: 32px;
  1946. }
  1947. .customNotificationImage {
  1948. filter: drop-shadow(0px 8px 4px rgba(0,0,0,.1));
  1949. }
  1950. .customNotificationDetails {
  1951. margin-left: 8px;
  1952. }
  1953. `;
  1954.  
  1955. initialise();
  1956.  
  1957. return exports;
  1958. }
  1959. );
  1960. // userCache
  1961. window.moduleRegistry.add('userCache', (events, itemCache, Promise, util) => {
  1962.  
  1963. const registerPageHandler = events.register.bind(null, 'page');
  1964. const registerXhrHandler = events.register.bind(null, 'xhr');
  1965. const emitEvent = events.emit.bind(null, 'userCache');
  1966. const isReady = new Promise.Deferred();
  1967.  
  1968. const exp = {};
  1969. const inventory = {};
  1970. const equipment = {};
  1971. const action = {
  1972. actionId: null,
  1973. skillId: null,
  1974. amount: null,
  1975. maxAmount: null
  1976. };
  1977. const automations = {};
  1978. let currentPage = null;
  1979.  
  1980. const exports = {
  1981. ready: isReady.promise,
  1982. exp,
  1983. inventory,
  1984. equipment,
  1985. action,
  1986. automations
  1987. };
  1988.  
  1989. function initialise() {
  1990. registerPageHandler(handlePage);
  1991. registerXhrHandler(handleXhr);
  1992.  
  1993. window.setInterval(update, 1000);
  1994. }
  1995.  
  1996. function handlePage(page) {
  1997. currentPage = page;
  1998. update();
  1999. }
  2000.  
  2001. async function handleXhr(xhr) {
  2002. if(xhr.url.endsWith('/getUser')) {
  2003. await handleGetUser(xhr.response);
  2004. isReady.resolve();
  2005. }
  2006. if(xhr.url.endsWith('/startAction')) {
  2007. handleStartAction(xhr.response);
  2008. }
  2009. if(xhr.url.endsWith('/stopAction')) {
  2010. handleStopAction();
  2011. }
  2012. if(xhr.url.endsWith('/startAutomation')) {
  2013. handleStartAutomation();
  2014. }
  2015. }
  2016.  
  2017. async function handleGetUser(response) {
  2018. await itemCache.ready;
  2019. // exp
  2020. const newExp = Object.entries(response.user.skills)
  2021. .map(a => ({id:a[0],exp:a[1].exp}))
  2022. .reduce((a,v) => Object.assign(a,{[v.id]:v.exp}), {});
  2023. Object.assign(exp, newExp);
  2024. // inventory
  2025. const newInventory = Object.values(response.user.inventory)
  2026. .reduce((a,v) => Object.assign(a,{[v.id]:v.amount}), {});
  2027. newInventory[-1] = response.user.compost;
  2028. newInventory[2] = response.user.charcoal;
  2029. Object.assign(inventory, newInventory);
  2030. // equipment
  2031. const newEquipment = Object.values(response.user.equipment)
  2032. .filter(a => a)
  2033. .map(a => {
  2034. if(a.uses) {
  2035. const duration = itemCache.byId[a.id]?.attributes?.DURATION || 1;
  2036. a.amount += a.uses / duration;
  2037. }
  2038. return a;
  2039. })
  2040. .reduce((a,v) => Object.assign(a,{[v.id]:v.amount}), {});
  2041. Object.assign(equipment, newEquipment);
  2042. // action
  2043. if(!response.user.action) {
  2044. action.actionId = null;
  2045. action.skillId = null;
  2046. action.amount = null;
  2047. } else {
  2048. action.actionId = +response.user.action.actionId;
  2049. action.skillId = +response.user.action.skillId;
  2050. action.amount = 0;
  2051. }
  2052. }
  2053.  
  2054. function handleStartAction(response) {
  2055. action.actionId = +response.actionId;
  2056. action.skillId = +response.skillId;
  2057. action.amount = 0;
  2058. action.maxAmount = response.amount;
  2059. }
  2060.  
  2061. function handleStopAction() {
  2062. action.actionId = null;
  2063. action.skillId = null;
  2064. action.amount = null;
  2065. action.maxAmount = null;
  2066. }
  2067.  
  2068. function handleStartAutomation(response) {
  2069. automations[+response.automationId] = {
  2070. amount: 0,
  2071. maxAmount: response.amount
  2072. }
  2073. }
  2074.  
  2075. async function update() {
  2076. await itemCache.ready;
  2077. if(!currentPage) {
  2078. return;
  2079. }
  2080. let updated = false;
  2081. if(currentPage.type === 'action') {
  2082. updated |= updateAction(); // bitwise OR because of lazy evaluation
  2083. }
  2084. if(currentPage.type === 'equipment') {
  2085. updated |= updateEquipment(); // bitwise OR because of lazy evaluation
  2086. }
  2087. if(currentPage.type === 'automation') {
  2088. updated |= updateAutomation(); // bitwise OR because of lazy evaluation
  2089. }
  2090. if(updated) {
  2091. emitEvent();
  2092. }
  2093. }
  2094.  
  2095. function updateAction() {
  2096. let updated = false;
  2097. $('skill-page .card').each((i,element) => {
  2098. const header = $(element).find('.header').text();
  2099. if(header === 'Materials') {
  2100. $(element).find('.row').each((j,row) => {
  2101. updated |= extractItem(row, inventory); // bitwise OR because of lazy evaluation
  2102. });
  2103. } else if(header === 'Consumables') {
  2104. $(element).find('.row').each((j,row) => {
  2105. updated |= extractItem(row, equipment); // bitwise OR because of lazy evaluation
  2106. });
  2107. } else if(header === 'Stats') {
  2108. $(element).find('.row').each((j,row) => {
  2109. const text = $(row).find('.name').text();
  2110. if(text.startsWith('Total ') && text.endsWith(' XP')) {
  2111. let expValue = $(row).find('.value').text().split(' ')[0];
  2112. expValue = util.parseNumber(expValue);
  2113. updated |= exp[currentPage.skill] !== expValue; // bitwise OR because of lazy evaluation
  2114. exp[currentPage.skill] = expValue;
  2115. }
  2116. });
  2117. } else if(header.startsWith('Loot')) {
  2118. const amount = $(element).find('.header .amount').text();
  2119. let newActionAmountValue = null;
  2120. let newActionMaxAmountValue = null;
  2121. if(amount) {
  2122. newActionAmountValue = util.parseNumber(amount.split(' / ')[0]);
  2123. newActionMaxAmountValue = util.parseNumber(amount.split(' / ')[1]);
  2124. }
  2125. updated |= action.amount !== newActionAmountValue; // bitwise OR because of lazy evaluation
  2126. updated |= action.maxAmount !== newActionMaxAmountValue; // bitwise OR because of lazy evaluation
  2127. action.amount = newActionAmountValue;
  2128. action.maxAmount = newActionMaxAmountValue;
  2129. }
  2130. });
  2131. return updated;
  2132. }
  2133.  
  2134. function updateEquipment() {
  2135. let updated = false;
  2136. $('equipment-component .card:nth-child(4) .item').each((i,element) => {
  2137. updated |= extractItem(element, equipment); // bitwise OR because of lazy evaluation
  2138. });
  2139. return updated;
  2140. }
  2141.  
  2142. function updateAutomation() {
  2143. let updated = false;
  2144. $('produce-component .card').each((i,element) => {
  2145. const header = $(element).find('.header').text();
  2146. if(header === 'Materials') {
  2147. $(element).find('.row').each((j,row) => {
  2148. updated |= extractItem(row, inventory); // bitwise OR because of lazy evaluation
  2149. });
  2150. } else if(header.startsWith('Loot')) {
  2151. const amount = $(element).find('.header .amount').text();
  2152. let newAutomationAmountValue = null;
  2153. let newAutomationMaxAmountValue = null;
  2154. if(amount) {
  2155. newAutomationAmountValue = util.parseNumber(amount.split(' / ')[0]);
  2156. newAutomationMaxAmountValue = util.parseNumber(amount.split(' / ')[1]);
  2157. }
  2158. updated |= automations[currentPage.action]?.amount !== newAutomationAmountValue; // bitwise OR because of lazy evaluation
  2159. updated |= automations[currentPage.action]?.maxAmount !== newAutomationMaxAmountValue; // bitwise OR because of lazy evaluation
  2160. automations[currentPage.action] = {
  2161. amount: newAutomationAmountValue,
  2162. maxAmount: newAutomationMaxAmountValue
  2163. }
  2164. }
  2165. });
  2166. return updated;
  2167. }
  2168.  
  2169. function extractItem(element, target) {
  2170. element = $(element);
  2171. const name = element.find('.name').text();
  2172. if(!name) {
  2173. return false;
  2174. }
  2175. const item = itemCache.byName[name];
  2176. if(!item) {
  2177. console.warn(`Could not find item with name [${name}]`);
  2178. return false;
  2179. }
  2180. let amount = element.find('.amount, .value').text();
  2181. if(!amount) {
  2182. return false;
  2183. }
  2184. if(amount.includes(' / ')) {
  2185. amount = amount.split(' / ')[0];
  2186. }
  2187. amount = util.parseNumber(amount);
  2188. let uses = element.find('.uses, .use').text();
  2189. if(uses) {
  2190. amount += util.parseNumber(uses);
  2191. }
  2192. const updated = target[item.id] !== amount;
  2193. target[item.id] = amount;
  2194. return updated;
  2195. }
  2196.  
  2197. initialise();
  2198.  
  2199. return exports;
  2200.  
  2201. }
  2202. );
  2203. // util
  2204. window.moduleRegistry.add('util', () => {
  2205.  
  2206. const exports = {
  2207. levelToExp,
  2208. expToLevel,
  2209. expToCurrentExp,
  2210. expToNextLevel,
  2211. expToNextTier,
  2212. formatNumber,
  2213. parseNumber,
  2214. secondsToDuration,
  2215. parseDuration,
  2216. divmod,
  2217. sleep,
  2218. goToPage
  2219. };
  2220.  
  2221. function levelToExp(level) {
  2222. if(level === 1) {
  2223. return 0;
  2224. }
  2225. return Math.floor(Math.pow(level, 3.5) * 6 / 5);
  2226. }
  2227.  
  2228. function expToLevel(exp) {
  2229. let level = Math.pow((exp + 1) * 5 / 6, 1 / 3.5);
  2230. level = Math.floor(level);
  2231. level = Math.max(1, level);
  2232. return level;
  2233. }
  2234.  
  2235. function expToCurrentExp(exp) {
  2236. const level = expToLevel(exp);
  2237. return exp - levelToExp(level);
  2238. }
  2239.  
  2240. function expToNextLevel(exp) {
  2241. const level = expToLevel(exp);
  2242. return levelToExp(level + 1) - exp;
  2243. }
  2244.  
  2245. function expToNextTier(exp) {
  2246. const level = expToLevel(exp);
  2247. let target = 10;
  2248. while(target <= level) {
  2249. target += 15;
  2250. }
  2251. return levelToExp(target) - exp;
  2252. }
  2253.  
  2254. function formatNumber(number) {
  2255. return number.toLocaleString(undefined, {maximumFractionDigits:2});
  2256. }
  2257.  
  2258. function parseNumber(text) {
  2259. if(!text) {
  2260. return 0;
  2261. }
  2262. text = text.replaceAll(/,/g, '');
  2263. let multiplier = 1;
  2264. if(text.endsWith('%')) {
  2265. multiplier = 1 / 100;
  2266. }
  2267. if(text.endsWith('K')) {
  2268. multiplier = 1_000;
  2269. }
  2270. if(text.endsWith('M')) {
  2271. multiplier = 1_000_000;
  2272. }
  2273. return (parseFloat(text) || 0) * multiplier;
  2274. }
  2275.  
  2276. function secondsToDuration(seconds) {
  2277. seconds = Math.floor(seconds);
  2278. if(seconds > 60 * 60 * 24 * 100) {
  2279. // > 100 days
  2280. return 'A very long time';
  2281. }
  2282.  
  2283. var [minutes, seconds] = divmod(seconds, 60);
  2284. var [hours, minutes] = divmod(minutes, 60);
  2285. var [days, hours] = divmod(hours, 24);
  2286.  
  2287. seconds = `${seconds}`.padStart(2, '0');
  2288. minutes = `${minutes}`.padStart(2, '0');
  2289. hours = `${hours}`.padStart(2, '0');
  2290. days = `${days}`.padStart(2, '0');
  2291.  
  2292. let result = '';
  2293. if(result || +days) {
  2294. result += `${days}d `;
  2295. }
  2296. if(result || +hours) {
  2297. result += `${hours}h `;
  2298. }
  2299. if(result || +minutes) {
  2300. result += `${minutes}m `;
  2301. }
  2302. if(result || +seconds) {
  2303. result += `${seconds}s`;
  2304. }
  2305.  
  2306. return result;
  2307. }
  2308.  
  2309. function parseDuration(duration) {
  2310. const parts = duration.split(' ');
  2311. let seconds = 0;
  2312. for(const part of parts) {
  2313. const value = parseFloat(part);
  2314. if(part.endsWith('m')) {
  2315. seconds += value * 60;
  2316. } else if(part.endsWith('h')) {
  2317. seconds += value * 60 * 60;
  2318. } else if(part.endsWith('d')) {
  2319. seconds += value * 60 * 60 * 24;
  2320. } else {
  2321. console.warn(`Unexpected duration being parsed : ${part}`);
  2322. }
  2323. }
  2324. return seconds;
  2325. }
  2326.  
  2327. function divmod(x, y) {
  2328. return [Math.floor(x / y), x % y];
  2329. }
  2330.  
  2331. function goToPage(page) {
  2332. window.history.pushState({}, '', page);
  2333. window.history.pushState({}, '', page);
  2334. window.history.back();
  2335. }
  2336.  
  2337. async function sleep(millis) {
  2338. await new Promise(r => window.setTimeout(r, millis));
  2339. }
  2340.  
  2341. return exports;
  2342.  
  2343. }
  2344. );
  2345. // webhooks
  2346. window.moduleRegistry.add('webhooks', (request, configuration) => {
  2347.  
  2348. const exports = {
  2349. register: register
  2350. }
  2351.  
  2352. function register(name, text, type) {
  2353. const webhook = {
  2354. type: type,
  2355. enabled: false,
  2356. url: ''
  2357. };
  2358. const handler = handleConfigStateChange.bind(null, webhook);
  2359. configuration.registerCheckbox({
  2360. category: 'Webhooks',
  2361. key: `${name}-enabled`,
  2362. name: `${text} webhook enabled`,
  2363. default: false,
  2364. handler: handler
  2365. });
  2366. configuration.registerInput({
  2367. category: 'Webhooks',
  2368. key: name,
  2369. name: `${text} webhook URL`,
  2370. default: '',
  2371. inputType: 'text',
  2372. handler: handler
  2373. });
  2374. }
  2375.  
  2376. function handleConfigStateChange(webhook, state, name, initial) {
  2377. if(name.endsWith('-enabled')) {
  2378. webhook.enabled = state;
  2379. } else {
  2380. webhook.url = state;
  2381. }
  2382. if(!initial) {
  2383. request.saveWebhook(webhook);
  2384. }
  2385. }
  2386.  
  2387. return exports;
  2388.  
  2389. }
  2390. );
  2391. // changelog
  2392. window.moduleRegistry.add('changelog', (Promise, pages, components, request, util, configuration) => {
  2393.  
  2394. const PAGE_NAME = 'Plugin changelog';
  2395. const loaded = new Promise.Deferred();
  2396.  
  2397. let changelogs = null;
  2398.  
  2399. async function initialise() {
  2400. await pages.register({
  2401. category: 'Skills',
  2402. after: 'Changelog',
  2403. name: PAGE_NAME,
  2404. image: 'https://ironwoodrpg.com/assets/misc/changelog.png',
  2405. render: renderPage
  2406. });
  2407. configuration.registerCheckbox({
  2408. category: 'Pages',
  2409. key: 'changelog-enabled',
  2410. name: 'Changelog',
  2411. default: true,
  2412. handler: handleConfigStateChange
  2413. });
  2414. load();
  2415. }
  2416.  
  2417. function handleConfigStateChange(state, name) {
  2418. if(state) {
  2419. pages.show(PAGE_NAME);
  2420. } else {
  2421. pages.hide(PAGE_NAME);
  2422. }
  2423. }
  2424.  
  2425. async function load() {
  2426. changelogs = await request.getChangelogs();
  2427. loaded.resolve();
  2428. }
  2429.  
  2430. async function renderPage() {
  2431. await loaded.promise;
  2432. const header = components.search(componentBlueprint, 'header');
  2433. const list = components.search(componentBlueprint, 'list');
  2434. for(const index in changelogs) {
  2435. componentBlueprint.componentId = `changelogComponent_${index}`;
  2436. header.title = changelogs[index].title;
  2437. header.textRight = new Date(changelogs[index].time).toLocaleDateString();
  2438. list.entries = changelogs[index].entries;
  2439. components.addComponent(componentBlueprint);
  2440. }
  2441. }
  2442.  
  2443. const componentBlueprint = {
  2444. componentId: 'changelogComponent',
  2445. dependsOn: 'custom-page',
  2446. parent: '.column0',
  2447. selectedTabIndex: 0,
  2448. tabs: [{
  2449. title: 'tab',
  2450. rows: [{
  2451. id: 'header',
  2452. type: 'header',
  2453. title: '',
  2454. textRight: ''
  2455. },{
  2456. id: 'list',
  2457. type: 'list',
  2458. entries: []
  2459. }]
  2460. }]
  2461. };
  2462.  
  2463. initialise();
  2464. }
  2465. );
  2466. // configurationPage
  2467. window.moduleRegistry.add('configurationPage', (pages, components, elementWatcher, configuration, auth, elementCreator) => {
  2468.  
  2469. const PAGE_NAME = 'Configuration';
  2470. const blueprints = [];
  2471.  
  2472. async function initialise() {
  2473. await auth.ready;
  2474. await pages.register({
  2475. category: 'Misc',
  2476. name: PAGE_NAME,
  2477. image: 'https://cdn-icons-png.flaticon.com/512/3953/3953226.png',
  2478. columns: '2',
  2479. render: renderPage
  2480. });
  2481. elementCreator.addStyles(styles);
  2482. await generateBlueprint();
  2483. pages.show(PAGE_NAME);
  2484. }
  2485.  
  2486. async function generateBlueprint() {
  2487. await configuration.ready;
  2488. const categories = {};
  2489. for(const item of configuration.items) {
  2490. if(!categories[item.category]) {
  2491. categories[item.category] = {
  2492. name: item.category,
  2493. items: []
  2494. }
  2495. }
  2496. categories[item.category].items.push(item);
  2497. }
  2498. let column = 1;
  2499. for(const category in categories) {
  2500. column = 1 - column;
  2501. const rows = [{
  2502. type: 'header',
  2503. title: category,
  2504. centered: true
  2505. }];
  2506. rows.push(...categories[category].items.flatMap(createRows));
  2507. blueprints.push({
  2508. componentId: `configurationComponent_${category}`,
  2509. dependsOn: 'custom-page',
  2510. parent: `.column${column}`,
  2511. selectedTabIndex: 0,
  2512. tabs: [{
  2513. rows: rows
  2514. }]
  2515. });
  2516. }
  2517. }
  2518.  
  2519. function createRows(item) {
  2520. switch(item.type) {
  2521. case 'checkbox': return createRows_Checkbox(item);
  2522. case 'input': return createRows_Input(item);
  2523. case 'dropdown': return createRows_Dropdown(item);
  2524. case 'json': break;
  2525. default: throw `Unknown configuration type : ${item.type}`;
  2526. }
  2527. }
  2528.  
  2529. function createRows_Checkbox(item) {
  2530. return [{
  2531. type: 'checkbox',
  2532. text: item.name,
  2533. checked: item.value,
  2534. delay: 500,
  2535. action: (value) => {
  2536. item.handler(value);
  2537. pages.requestRender(PAGE_NAME);
  2538. }
  2539. }]
  2540. }
  2541.  
  2542. function createRows_Input(item) {
  2543. const value = item.value || item.default;
  2544. return [{
  2545. type: 'item',
  2546. name: item.name
  2547. },{
  2548. type: 'input',
  2549. name: item.name,
  2550. value: value,
  2551. inputType: item.inputType,
  2552. delay: 500,
  2553. action: (value) => {
  2554. item.handler(value);
  2555. }
  2556. }]
  2557. }
  2558.  
  2559. function createRows_Dropdown(item) {
  2560. const value = item.value || item.default;
  2561. const options = item.options.map(option => ({
  2562. text: option,
  2563. value: option,
  2564. selected: option === value
  2565. }));
  2566. return [{
  2567. type: 'item',
  2568. name: item.name
  2569. },{
  2570. type: 'dropdown',
  2571. options: options,
  2572. delay: 500,
  2573. action: (value) => {
  2574. item.handler(value);
  2575. }
  2576. }]
  2577. }
  2578.  
  2579. async function renderPage() {
  2580. for(const blueprint of blueprints) {
  2581. components.addComponent(blueprint);
  2582. }
  2583. }
  2584.  
  2585. const styles = `
  2586. .modifiedHeight {
  2587. height: 28px;
  2588. }
  2589. `;
  2590.  
  2591. initialise();
  2592. }
  2593. );
  2594. // dataTransmitter
  2595. window.moduleRegistry.add('dataTransmitter', (auth, request, events) => {
  2596.  
  2597. function initialise() {
  2598. events.register('xhr', handleXhr);
  2599. }
  2600.  
  2601. async function handleXhr(xhr) {
  2602. if(xhr.status !== 200) {
  2603. return;
  2604. }
  2605. let response = xhr.response;
  2606. if(Array.isArray(response)) {
  2607. response = {
  2608. value: response
  2609. };
  2610. }
  2611. if(xhr.url.endsWith('getUser')) {
  2612. const name = response.user.displayName;
  2613. const password = new Date(response.user.createdAt).getTime();
  2614. auth.register(name, password);
  2615. }
  2616. await request.handleInterceptedRequest({
  2617. url: xhr.url,
  2618. status: xhr.status,
  2619. payload: JSON.stringify(xhr.request),
  2620. response: JSON.stringify(response)
  2621. });
  2622. }
  2623.  
  2624. initialise();
  2625.  
  2626. }
  2627. );
  2628. // estimations
  2629. window.moduleRegistry.add('estimations', (events, components, util) => {
  2630.  
  2631. const registerEstimationHandler = events.register.bind(null, 'estimation');
  2632. const addComponent = components.addComponent;
  2633. const removeComponent = components.removeComponent;
  2634. const searchComponent = components.search;
  2635.  
  2636. function initialise() {
  2637. registerEstimationHandler(handleEstimationData);
  2638. }
  2639.  
  2640. function handleEstimationData(estimation) {
  2641. if(!estimation) {
  2642. removeComponent(componentBlueprint);
  2643. return;
  2644. }
  2645.  
  2646. if(estimation.type === 'AUTOMATION') {
  2647. componentBlueprint.dependsOn = 'home-page';
  2648. componentBlueprint.parent = 'produce-component';
  2649. } else {
  2650. componentBlueprint.dependsOn = 'skill-page';
  2651. componentBlueprint.parent = 'actions-component';
  2652. }
  2653.  
  2654. searchComponent(componentBlueprint, 'overviewSpeed').value
  2655. = util.formatNumber(estimation.speed) + ' s';
  2656. searchComponent(componentBlueprint, 'overviewExp').hidden
  2657. = estimation.exp === 0;
  2658. searchComponent(componentBlueprint, 'overviewExp').value
  2659. = util.formatNumber(estimation.exp);
  2660. searchComponent(componentBlueprint, 'overviewSurvivalChance').hidden
  2661. = estimation.type === 'ACTIVITY' || estimation.type === 'AUTOMATION';
  2662. searchComponent(componentBlueprint, 'overviewSurvivalChance').value
  2663. = util.formatNumber(estimation.survivalChance * 100) + ' %';
  2664. searchComponent(componentBlueprint, 'overviewFinishedTime').value
  2665. = util.secondsToDuration(estimation.secondsLeft);
  2666. searchComponent(componentBlueprint, 'overviewLevelTime').hidden
  2667. = estimation.exp === 0;
  2668. searchComponent(componentBlueprint, 'overviewLevelTime').value
  2669. = util.secondsToDuration(estimation.secondsToNextlevel);
  2670. searchComponent(componentBlueprint, 'overviewTierTime').hidden
  2671. = estimation.exp === 0;
  2672. searchComponent(componentBlueprint, 'overviewTierTime').value
  2673. = util.secondsToDuration(estimation.secondsToNextTier);
  2674. searchComponent(componentBlueprint, 'overviewGoldLoot').hidden
  2675. = estimation.goldLoot === 0;
  2676. searchComponent(componentBlueprint, 'overviewGoldLoot').value
  2677. = util.formatNumber(estimation.goldLoot);
  2678. searchComponent(componentBlueprint, 'overviewGoldMaterials').hidden
  2679. = estimation.goldMaterials === 0;
  2680. searchComponent(componentBlueprint, 'overviewGoldMaterials').value
  2681. = util.formatNumber(estimation.goldMaterials);
  2682. searchComponent(componentBlueprint, 'overviewGoldEquipments').hidden
  2683. = estimation.goldEquipments === 0;
  2684. searchComponent(componentBlueprint, 'overviewGoldEquipments').value
  2685. = util.formatNumber(estimation.goldEquipments);
  2686. searchComponent(componentBlueprint, 'overviewGoldTotal').hidden
  2687. = estimation.goldTotal === 0;
  2688. searchComponent(componentBlueprint, 'overviewGoldTotal').value
  2689. = util.formatNumber(estimation.goldTotal);
  2690. searchComponent(componentBlueprint, 'tabTime').hidden
  2691. = (estimation.materials.length + estimation.equipments.length) === 0;
  2692.  
  2693. const dropRows = searchComponent(componentBlueprint, 'dropRows');
  2694. const materialRows = searchComponent(componentBlueprint, 'materialRows');
  2695. const timeRows = searchComponent(componentBlueprint, 'timeRows');
  2696. dropRows.rows = [];
  2697. materialRows.rows = [];
  2698. timeRows.rows = [];
  2699. for(const drop of estimation.loot) {
  2700. dropRows.rows.push({
  2701. type: 'item',
  2702. image: `/assets/${drop.item?.image}`,
  2703. imagePixelated: true,
  2704. name: drop.item?.name,
  2705. value: util.formatNumber(drop.amount) + ' / hour'
  2706. });
  2707. }
  2708. for(const material of estimation.materials) {
  2709. materialRows.rows.push({
  2710. type: 'item',
  2711. image: `/assets/${material.item?.image}`,
  2712. imagePixelated: true,
  2713. name: material.item?.name,
  2714. value: util.formatNumber(material.amount) + ' / hour'
  2715. });
  2716. timeRows.rows.push({
  2717. type: 'item',
  2718. image: `/assets/${material.item?.image}`,
  2719. imagePixelated: true,
  2720. name: `${material.item?.name} [${util.formatNumber(material.stored)}]`,
  2721. value: util.secondsToDuration(material.secondsLeft)
  2722. });
  2723. }
  2724. for(const equipment of estimation.equipments) {
  2725. materialRows.rows.push({
  2726. type: 'item',
  2727. image: `/assets/${equipment.item?.image}`,
  2728. imagePixelated: true,
  2729. name: equipment.item?.name,
  2730. value: util.formatNumber(equipment.amount) + ' / hour'
  2731. });
  2732. timeRows.rows.push({
  2733. type: 'item',
  2734. image: `/assets/${equipment.item?.image}`,
  2735. imagePixelated: true,
  2736. name: `${equipment.item?.name} [${util.formatNumber(equipment.stored)}]`,
  2737. value: util.secondsToDuration(equipment.secondsLeft)
  2738. });
  2739. }
  2740.  
  2741. addComponent(componentBlueprint);
  2742. }
  2743.  
  2744. const componentBlueprint = {
  2745. componentId: 'estimationComponent',
  2746. dependsOn: 'skill-page',
  2747. parent: 'actions-component',
  2748. selectedTabIndex: 0,
  2749. tabs: [{
  2750. title: 'Overview',
  2751. rows: [{
  2752. type: 'item',
  2753. id: 'overviewSpeed',
  2754. name: 'Time per action',
  2755. image: 'https://cdn-icons-png.flaticon.com/512/3563/3563395.png',
  2756. value: ''
  2757. },{
  2758. type: 'item',
  2759. id: 'overviewExp',
  2760. name: 'Exp/hour',
  2761. image: 'https://cdn-icons-png.flaticon.com/512/616/616490.png',
  2762. value: ''
  2763. },{
  2764. type: 'item',
  2765. id: 'overviewSurvivalChance',
  2766. name: 'Survival chance',
  2767. image: 'https://cdn-icons-png.flaticon.com/512/3004/3004458.png',
  2768. value: ''
  2769. },{
  2770. type: 'item',
  2771. id: 'overviewFinishedTime',
  2772. name: 'Finished',
  2773. image: 'https://cdn-icons-png.flaticon.com/512/1505/1505471.png',
  2774. value: ''
  2775. },{
  2776. type: 'item',
  2777. id: 'overviewLevelTime',
  2778. name: 'Level up',
  2779. image: 'https://cdn-icons-png.flaticon.com/512/4614/4614145.png',
  2780. value: ''
  2781. },{
  2782. type: 'item',
  2783. id: 'overviewTierTime',
  2784. name: 'Tier up',
  2785. image: 'https://cdn-icons-png.flaticon.com/512/4789/4789514.png',
  2786. value: ''
  2787. },{
  2788. type: 'item',
  2789. id: 'overviewGoldLoot',
  2790. name: 'Gold/hour (loot)',
  2791. image: 'https://cdn-icons-png.flaticon.com/512/9028/9028024.png',
  2792. imageFilter: 'invert(100%) sepia(47%) saturate(3361%) hue-rotate(313deg) brightness(106%) contrast(108%)',
  2793. value: ''
  2794. },{
  2795. type: 'item',
  2796. id: 'overviewGoldMaterials',
  2797. name: 'Gold/hour (materials)',
  2798. image: 'https://cdn-icons-png.flaticon.com/512/9028/9028031.png',
  2799. imageFilter: 'invert(100%) sepia(47%) saturate(3361%) hue-rotate(313deg) brightness(106%) contrast(108%)',
  2800. value: ''
  2801. },{
  2802. type: 'item',
  2803. id: 'overviewGoldEquipments',
  2804. name: 'Gold/hour (equipments)',
  2805. image: 'https://cdn-icons-png.flaticon.com/512/9028/9028031.png',
  2806. imageFilter: 'invert(100%) sepia(47%) saturate(3361%) hue-rotate(313deg) brightness(106%) contrast(108%)',
  2807. value: ''
  2808. },{
  2809. type: 'item',
  2810. id: 'overviewGoldTotal',
  2811. name: 'Gold/hour (total)',
  2812. image: 'https://cdn-icons-png.flaticon.com/512/11937/11937869.png',
  2813. imageFilter: 'invert(100%) sepia(47%) saturate(3361%) hue-rotate(313deg) brightness(106%) contrast(108%)',
  2814. value: ''
  2815. }]
  2816. },{
  2817. title: 'Items',
  2818. rows: [{
  2819. type: 'header',
  2820. title: 'Produced'
  2821. },{
  2822. type: 'segment',
  2823. id: 'dropRows',
  2824. rows: []
  2825. },{
  2826. type: 'header',
  2827. title: 'Consumed'
  2828. },{
  2829. type: 'segment',
  2830. id: 'materialRows',
  2831. rows: []
  2832. }]
  2833. },{
  2834. title: 'Time',
  2835. id: 'tabTime',
  2836. rows: [{
  2837. type: 'segment',
  2838. id: 'timeRows',
  2839. rows: []
  2840. }]
  2841. }]
  2842. };
  2843.  
  2844. initialise();
  2845.  
  2846. }
  2847. );
  2848. // guildQuestTracking
  2849. window.moduleRegistry.add('guildQuestTracking', (request, configuration, events, components) => {
  2850.  
  2851. let enabled = false;
  2852. let registrationAmount = 0;
  2853. let selectedItem;
  2854. let questsData;
  2855. let combinedData;
  2856.  
  2857. function initialise() {
  2858. configuration.registerCheckbox({
  2859. category: 'Other',
  2860. key: 'guild-quest-tracking',
  2861. name: 'Guild quest tracking',
  2862. default: true,
  2863. handler: handleConfigStateChange
  2864. });
  2865. events.register('xhr', handleXhr);
  2866. }
  2867.  
  2868. function handleConfigStateChange(state) {
  2869. enabled = state;
  2870. }
  2871.  
  2872. async function handleXhr(xhr) {
  2873. if(!enabled) {
  2874. return;
  2875. }
  2876. if(xhr.url.endsWith('/createGuildQuests')) {
  2877. await refresh();
  2878. handleQuestOverviewButtonClick();
  2879. }
  2880. if(xhr.url.endsWith('/giveGuildQuestItems')) {
  2881. refresh(selectedItem);
  2882. }
  2883. }
  2884.  
  2885. async function refresh(item) {
  2886. await fetchData();
  2887. listenNavigateAway();
  2888. injectButtons();
  2889. if(item) {
  2890. showForItem(item);
  2891. }
  2892. }
  2893.  
  2894. async function fetchData() {
  2895. questsData = await request.getGuildQuestStats();
  2896. combinedData = {
  2897. complete: true,
  2898. image: 'items/coin-stack.png',
  2899. registrations: [],
  2900. performers: [],
  2901. contributions: questsData.flatMap(a => a.contributions)
  2902. };
  2903. }
  2904.  
  2905. function listenNavigateAway() {
  2906. $('.tracker + .card > button').click(function() {
  2907. components.removeComponent(componentBlueprint);
  2908. });
  2909. }
  2910.  
  2911. function injectButtons() {
  2912. const rows = $('.row > .image').parent();
  2913. rows.find('.customQuestButton').remove();
  2914. for(const row of rows) {
  2915. const itemName = $(row).find('> .name').text();
  2916. const questData = questsData.find(a => a.name === itemName);
  2917. const count = questData.complete ? '-' : questData.registrations.length + questData.performers.length;
  2918. const element = $(`<button class='customQuestButton'><img src='https://cdn-icons-png.flaticon.com/512/6514/6514927.png' style='width:24px;height:24px;margin-left:12px'><span style='min-width:1.5rem'>${count}</span></button>`);
  2919. element.click(handleQuestButtonClick.bind(null, itemName));
  2920. $(row).find('> .plus').after(element);
  2921. }
  2922.  
  2923. const header = $('.header > .amount').parent();
  2924. header.find('.customQuestButton').remove();
  2925. const element = $(`<button class='customQuestButton'><img src='https://cdn-icons-png.flaticon.com/512/6514/6514927.png' style='width:24px;height:24px;margin-left:12px'></button>`);
  2926. element.click(handleQuestOverviewButtonClick);
  2927. header.append(element);
  2928. }
  2929.  
  2930. function handleQuestButtonClick(item, event) {
  2931. event.stopPropagation();
  2932. selectedItem = item;
  2933. showForItem(item);
  2934. }
  2935.  
  2936. function handleQuestOverviewButtonClick() {
  2937. showComponent(combinedData);
  2938. }
  2939.  
  2940. function showForItem(item) {
  2941. registrationAmount = 0;
  2942. const questData = questsData.find(a => a.name === item);
  2943. showComponent(questData);
  2944. }
  2945.  
  2946. function showComponent(questData) {
  2947. componentBlueprint.selectedTabIndex = 0;
  2948. const registeredSegment = components.search(componentBlueprint, 'registeredSegment');
  2949. const performingSegment = components.search(componentBlueprint, 'performingSegment');
  2950. registeredSegment.hidden = questData.complete;
  2951. performingSegment.hidden = questData.complete;
  2952. components.search(componentBlueprint, 'registeredHeader').image = `/assets/${questData.image}`;
  2953. components.search(componentBlueprint, 'performingHeader').image = `/assets/${questData.image}`;
  2954. components.search(componentBlueprint, 'contributionsHeader').image = `/assets/${questData.image}`;
  2955. components.search(componentBlueprint, 'registerTab').hidden = questData.complete;
  2956. components.search(componentBlueprint, 'registeredRowsSegment').rows = questData.registrations.map(registration => ({
  2957. type: 'item',
  2958. name: registration.name,
  2959. value: registration.amount,
  2960. image: '/assets/misc/quests.png',
  2961. imagePixelated: true
  2962. }));
  2963. components.search(componentBlueprint, 'performingRowsSegment').rows = questData.performers.map(performer => ({
  2964. type: 'item',
  2965. name: performer.name,
  2966. image: `/assets/${questData.image}`,
  2967. imagePixelated: true
  2968. }));
  2969. components.search(componentBlueprint, 'contributionsRowsSegment').rows = questData.contributions.map(contribution => ({
  2970. type: 'item',
  2971. name: contribution.name,
  2972. value: `${contribution.amount} (${new Date(contribution.time).toLocaleTimeString()})`,
  2973. image: `/assets/${contribution.image}`,
  2974. imagePixelated: true
  2975. }));
  2976. const registered = !!questData.registrations.find(a => a.name === questData.requester);
  2977. const registerButton = components.search(componentBlueprint, 'registerButton');
  2978. const unregisterButton = components.search(componentBlueprint, 'unregisterButton');
  2979. registerButton.disabled = !!registered;
  2980. unregisterButton.disabled = !registered;
  2981. registerButton.action = register.bind(null,questData);
  2982. unregisterButton.action = unregister.bind(null,questData);
  2983. components.addComponent(componentBlueprint);
  2984. }
  2985.  
  2986. function setRegistrationAmount(value) {
  2987. registrationAmount = +value;
  2988. }
  2989.  
  2990. async function register(questData) {
  2991. if(!registrationAmount) {
  2992. return;
  2993. }
  2994. await request.registerGuildQuest(questData.itemId, registrationAmount);
  2995. refresh(questData.name);
  2996. }
  2997.  
  2998. async function unregister(questData) {
  2999. await request.unregisterGuildQuest(questData.itemId);
  3000. refresh(questData.name);
  3001. }
  3002.  
  3003. const componentBlueprint = {
  3004. componentId : 'guildQuestComponent',
  3005. dependsOn: 'guild-page',
  3006. parent : 'guild-component > .groups > .group:last-child',
  3007. selectedTabIndex : 0,
  3008. tabs : [{
  3009. id: 'statusTab',
  3010. title : 'Status',
  3011. rows: [{
  3012. type: 'segment',
  3013. id: 'registeredSegment',
  3014. hidden: false,
  3015. rows: [{
  3016. type: 'header',
  3017. id: 'registeredHeader',
  3018. title: 'Registered',
  3019. centered: true,
  3020. image: '',
  3021. imagePixelated: true
  3022. }, {
  3023. type: 'segment',
  3024. id: 'registeredRowsSegment',
  3025. rows: []
  3026. }]
  3027. }, {
  3028. type: 'segment',
  3029. id: 'performingSegment',
  3030. hidden: false,
  3031. rows: [{
  3032. type: 'header',
  3033. id: 'performingHeader',
  3034. title: 'Currently performing',
  3035. centered: true,
  3036. image: '',
  3037. imagePixelated: true
  3038. }, {
  3039. type: 'segment',
  3040. id: 'performingRowsSegment',
  3041. rows: []
  3042. }]
  3043. }, {
  3044. type: 'segment',
  3045. id: 'contributionsSegment',
  3046. rows: [{
  3047. type: 'header',
  3048. id: 'contributionsHeader',
  3049. title: 'Contributions',
  3050. centered: true,
  3051. image: '',
  3052. imagePixelated: true
  3053. }, {
  3054. type: 'segment',
  3055. id: 'contributionsRowsSegment',
  3056. rows: []
  3057. }]
  3058. }]
  3059. }, {
  3060. id: 'registerTab',
  3061. title : 'Register',
  3062. hidden: false,
  3063. rows: [{
  3064. type : 'input',
  3065. name : 'Amount',
  3066. action: setRegistrationAmount
  3067. },{
  3068. type : 'buttons',
  3069. buttons: [{
  3070. id: 'registerButton',
  3071. text: 'Register',
  3072. disabled: true,
  3073. color: 'primary'
  3074. },{
  3075. id: 'unregisterButton',
  3076. text: 'Unregister',
  3077. disabled: true,
  3078. color: 'warning'
  3079. }]
  3080. }]
  3081. }]
  3082. };
  3083.  
  3084. initialise();
  3085.  
  3086. }
  3087. );
  3088. // guildSorts
  3089. window.moduleRegistry.add('guildSorts', (events, elementWatcher, util, elementCreator) => {
  3090.  
  3091. function initialise() {
  3092. elementCreator.addStyles(styles);
  3093. events.register('page', handlePage);
  3094. }
  3095.  
  3096. async function handlePage(page) {
  3097. if(page.type === 'guild') {
  3098. await elementWatcher.exists('.card > .row');
  3099. await addAdditionGuildSortButtons();
  3100. setupGuildMenuButtons();
  3101. }
  3102. if(page.type === 'market') {
  3103. // TODO for another script?
  3104. }
  3105. }
  3106.  
  3107. function setupGuildMenuButtons() {
  3108. $(`button > div.name:contains('Members')`).parent().on('click', async function () {
  3109. await util.sleep(50);
  3110. await addAdditionGuildSortButtons();
  3111. });
  3112. }
  3113.  
  3114. async function addAdditionGuildSortButtons() {
  3115. await elementWatcher.exists('div.sort');
  3116. const orginalButtonGroup = $('div.sort').find('div.container');
  3117.  
  3118. // rename daily to daily xp
  3119. $(`button:contains('Daily')`).text('Daily XP');
  3120. // fix text on 2 lines
  3121. $('div.sort').find('button').addClass('overrideFlex');
  3122. // attach clear custom to game own sorts
  3123. $('div.sort').find('button').on('click', function() {
  3124. clearCustomActiveButtons()
  3125. });
  3126.  
  3127. const customButtonGroup = $('<div/>')
  3128. .addClass('customButtonGroup')
  3129. .addClass('alignButtonGroupLeft')
  3130. .attr('id', 'guildSortButtonGroup')
  3131. .append(
  3132. $('<button/>')
  3133. .attr('type', 'button')
  3134. .addClass('customButtonGroupButton')
  3135. .addClass('customSortByLevel')
  3136. .text('Level')
  3137. .click(() => { sortByLevel(); })
  3138. )
  3139. .append(
  3140. $('<button/>')
  3141. .attr('type', 'button')
  3142. .addClass('customButtonGroupButton')
  3143. .addClass('customSortByIdle')
  3144. .text('Idle')
  3145. .click(() => { sortByIdle(); })
  3146. )
  3147. .append(
  3148. $('<button/>')
  3149. .attr('type', 'button')
  3150. .addClass('customButtonGroupButton')
  3151. .addClass('customSortByTotalXP')
  3152. .text('Total XP')
  3153. .click(() => { sortByXp(); })
  3154. );
  3155.  
  3156. customButtonGroup.insertAfter(orginalButtonGroup);
  3157. }
  3158.  
  3159. function clearCustomActiveButtons() {
  3160. $('.customButtonGroupButton').removeClass('custom-sort-active');
  3161. }
  3162.  
  3163. function clearActiveButtons() {
  3164. $('div.sort').find('button').removeClass('sort-active');
  3165. }
  3166.  
  3167. function sortByXp() {
  3168. $(`button:contains('Date')`).trigger('click');
  3169. clearCustomActiveButtons();
  3170. clearActiveButtons();
  3171. $('.customSortByTotalXP').addClass('custom-sort-active');
  3172.  
  3173. const parent = $('div.sort').parent();
  3174. sortElements({
  3175. elements: parent.find('button.row'),
  3176. extractor: a => util.parseNumber($(a).find('div.amount').text()),
  3177. sorter: (a,b) => b-a,
  3178. target: parent
  3179. });
  3180. }
  3181.  
  3182. function sortByIdle() {
  3183. // make sure the last contributed time is visible
  3184. if(
  3185. !$(`div.sort button:contains('Date')`).hasClass('sort-active') &&
  3186. !$(`button:contains('Daily XP')`).hasClass('sort-active')
  3187. ) {
  3188. $(`button:contains('Date')`).trigger('click');
  3189. }
  3190.  
  3191. clearCustomActiveButtons();
  3192. clearActiveButtons();
  3193. $('.customSortByIdle').addClass('custom-sort-active');
  3194.  
  3195. const parent = $('div.sort').parent();
  3196. sortElements({
  3197. elements: parent.find('button.row'),
  3198. extractor: a => util.parseDuration($(a).find('div.time').text()),
  3199. sorter: (a,b) => b-a,
  3200. target: parent
  3201. });
  3202. }
  3203.  
  3204. function sortByLevel() {
  3205. clearCustomActiveButtons();
  3206. clearActiveButtons();
  3207. $('.customSortByLevel').addClass('custom-sort-active');
  3208.  
  3209. const parent = $('div.sort').parent();
  3210. sortElements({
  3211. elements: parent.find('button.row'),
  3212. extractor: a => util.parseNumber($(a).find('div.level').text().replace('Lv. ', '')),
  3213. sorter: (a,b) => b-a,
  3214. target: parent
  3215. });
  3216. }
  3217.  
  3218. // sorts a list of `elements` according to the extracted property from `extractor`,
  3219. // sorts them using `sorter`, and appends them to the `target`
  3220. // elements is a jquery list
  3221. // target is a jquery element
  3222. // { elements, target, extractor, sorter }
  3223. function sortElements(config) {
  3224. const list = config.elements.get().map(element => ({
  3225. element,
  3226. value: config.extractor(element)
  3227. }));
  3228. list.sort((a,b) => config.sorter(a.value, b.value));
  3229. for(const item of list) {
  3230. config.target.append(item.element);
  3231. }
  3232. }
  3233.  
  3234. const styles = `
  3235. .alignButtonGroupLeft {
  3236. margin-right: auto;
  3237. margin-left: 8px;
  3238. }
  3239. .customButtonGroup {
  3240. display: flex;
  3241. align-items: center;
  3242. border-radius: 4px;
  3243. box-shadow: 0 1px 2px #0003;
  3244. border: 1px solid #263849;
  3245. overflow: hidden;
  3246. }
  3247. .customButtonGroupButton {
  3248. padding: 4px var(--gap);
  3249. flex: none !important;
  3250. text-align: center;
  3251. justify-content: center;
  3252. background-color: #061a2e;
  3253. }
  3254. .customButtonGroupButton:not(:first-of-type) {
  3255. border-left: 1px solid #263849;
  3256. }
  3257. .overrideFlex {
  3258. flex: none !important
  3259. }
  3260. .custom-sort-active {
  3261. background-color: #0d2234;
  3262. }
  3263. `;
  3264.  
  3265. initialise();
  3266. }
  3267. );
  3268. // idleBeep
  3269. window.moduleRegistry.add('idleBeep', (configuration, events, util) => {
  3270.  
  3271. const audio = new Audio('data:audio/mpeg;base64,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');
  3272. const sleepAmount = 2000;
  3273. let enabled = false;
  3274. let started = false;
  3275.  
  3276. function initialise() {
  3277. configuration.registerCheckbox({
  3278. category: 'Other',
  3279. key: 'idle-beep-enabled',
  3280. name: 'Idle beep',
  3281. default: false,
  3282. handler: handleConfigStateChange
  3283. });
  3284. events.register('xhr', handleXhr);
  3285. }
  3286.  
  3287. function handleConfigStateChange(state, name) {
  3288. enabled = state;
  3289. }
  3290.  
  3291. async function handleXhr(xhr) {
  3292. if(!enabled) {
  3293. return;
  3294. }
  3295. if(xhr.url.endsWith('startAction')) {
  3296. started = true;
  3297. }
  3298. if(xhr.url.endsWith('stopAction')) {
  3299. started = false;
  3300. console.debug(`Triggering beep in ${sleepAmount}ms`);
  3301. await util.sleep(sleepAmount);
  3302. beep();
  3303. }
  3304. }
  3305.  
  3306. function beep() {
  3307. if(!started) {
  3308. audio.play();
  3309. }
  3310. }
  3311.  
  3312. initialise();
  3313.  
  3314. }
  3315. );
  3316. // itemHover
  3317. window.moduleRegistry.add('itemHover', (auth, configuration, itemCache, util) => {
  3318.  
  3319. let enabled = false;
  3320. let entered = false;
  3321. let element;
  3322. const converters = {
  3323. SPEED: a => a/2,
  3324. DURATION: a => util.secondsToDuration(a/10)
  3325. }
  3326.  
  3327. async function initialise() {
  3328. configuration.registerCheckbox({
  3329. category: 'UI Features',
  3330. key: 'item-hover',
  3331. name: 'Item hover info',
  3332. default: true,
  3333. handler: handleConfigStateChange
  3334. });
  3335. await setup();
  3336. $(document).on('mouseenter', 'div.image > img', handleMouseEnter);
  3337. $(document).on('mouseleave', 'div.image > img', handleMouseLeave);
  3338. }
  3339.  
  3340. function handleConfigStateChange(state) {
  3341. enabled = state;
  3342. }
  3343.  
  3344. function handleMouseEnter(event) {
  3345. if(!enabled || entered || !itemCache.byId) {
  3346. return;
  3347. }
  3348. entered = true;
  3349. const name = $(event.relatedTarget).find('.name').text();
  3350. const nameMatch = itemCache.byName[name];
  3351. if(nameMatch) {
  3352. return show(nameMatch);
  3353. }
  3354.  
  3355. const parts = event.target.src.split('/');
  3356. const lastPart = parts[parts.length-1];
  3357. const imageMatch = itemCache.byImage[lastPart];
  3358. if(imageMatch) {
  3359. return show(imageMatch);
  3360. }
  3361. }
  3362.  
  3363. function handleMouseLeave(event) {
  3364. if(!enabled || !itemCache.byId) {
  3365. return;
  3366. }
  3367. entered = false;
  3368. hide();
  3369. }
  3370.  
  3371. function show(item) {
  3372. element.find('.image').attr('src', `/assets/${item.image}`);
  3373. element.find('.name').text(item.name);
  3374. for(const attribute of itemCache.attributes) {
  3375. let value = item.attributes[attribute.technicalName];
  3376. if(converters[attribute.technicalName]) {
  3377. value = converters[attribute.technicalName](value);
  3378. }
  3379. updateRow(attribute.technicalName, value);
  3380. }
  3381. element.show();
  3382. }
  3383.  
  3384. function updateRow(name, value) {
  3385. if(!value) {
  3386. element.find(`.${name}-row`).hide();
  3387. } else {
  3388. element.find(`.${name}`).text(value);
  3389. element.find(`.${name}-row`).show();
  3390. }
  3391. }
  3392.  
  3393. function hide() {
  3394. element.hide();
  3395. }
  3396.  
  3397. async function setup() {
  3398. await itemCache.ready;
  3399. const attributesHtml = itemCache.attributes
  3400. .map(a => `<div class='${a.technicalName}-row'><img src='${a.image}'/><span>${a.name}</span><span class='${a.technicalName}'/></div>`)
  3401. .join('');
  3402. $('head').append(`
  3403. <style>
  3404. #custom-item-hover {
  3405. position: fixed;
  3406. right: .5em;
  3407. top: .5em;
  3408. display: flex;
  3409. font-family: Jost,Helvetica Neue,Arial,sans-serif;
  3410. flex-direction: column;
  3411. white-space: nowrap;
  3412. z-index: 1;
  3413. background-color: black;
  3414. padding: .4rem;
  3415. border: 1px solid #3e3e3e;
  3416. border-radius: .4em;
  3417. gap: .4em;
  3418. }
  3419. #custom-item-hover > div {
  3420. display: flex;
  3421. gap: .4em;
  3422. }
  3423. #custom-item-hover > div > *:last-child {
  3424. margin-left: auto;
  3425. }
  3426. #custom-item-hover img {
  3427. width: 24px;
  3428. height: 24px;
  3429. }
  3430. </style>
  3431. `);
  3432. element = $(`
  3433. <div id='custom-item-hover' style='display:none'>
  3434. <div>
  3435. <img class='image'/>
  3436. <span class='name'/>
  3437. </div>
  3438. ${attributesHtml}
  3439. </div>
  3440. `);
  3441. $('body').append(element);
  3442. }
  3443.  
  3444. initialise();
  3445.  
  3446. }
  3447. );
  3448. // marketFilter
  3449. window.moduleRegistry.add('marketFilter', (request, configuration, events, components, elementWatcher, Promise) => {
  3450.  
  3451. let enabled = false;
  3452. let conversionsByType = {};
  3453. let savedFilters = [];
  3454. let currentFilter = {
  3455. listingType: 'SELL',
  3456. type: 'None',
  3457. amount: 0,
  3458. key: 'SELL-None'
  3459. };
  3460. let listUpdatePromiseWrapper = null;
  3461.  
  3462. async function initialise() {
  3463. configuration.registerCheckbox({
  3464. category: 'UI Features',
  3465. key: 'market-filter',
  3466. name: 'Market filter',
  3467. default: true,
  3468. handler: handleConfigStateChange
  3469. });
  3470. events.register('xhr', handleXhr);
  3471.  
  3472. $(document).on('mouseenter mouseleave click', '.saveFilterHoverTrigger', function(e) {
  3473. switch(e.type) {
  3474. case 'mouseenter':
  3475. if(currentFilter.type === 'None') {
  3476. return $('.saveFilterHover.search').addClass('greenOutline');
  3477. }
  3478. return $('.saveFilterHover:not(.search)').addClass('greenOutline');
  3479. case 'mouseleave':
  3480. case 'click':
  3481. return $('.saveFilterHover').removeClass('greenOutline');
  3482. }
  3483. });
  3484.  
  3485. $(document).on('input', 'market-listings-component .search > input', clearFilter);
  3486. $(document).on('click', 'market-listings-component .card > .tabs > :nth-child(1)', async function() {
  3487. currentFilter.listingType = 'SELL';
  3488. showComponent();
  3489. await applyFilter(currentFilter);
  3490. });
  3491. $(document).on('click', 'market-listings-component .card > .tabs > :nth-child(2)', async function() {
  3492. currentFilter.listingType = 'BUY';
  3493. showComponent();
  3494. await applyFilter(currentFilter);
  3495. });
  3496. $(document).on('click', 'market-listings-component .card > .tabs > :nth-child(3)', async function() {
  3497. await clearFilter();
  3498. hideComponent();
  3499. });
  3500.  
  3501. window.$('head').append($(`
  3502. <style>
  3503. .greenOutline {
  3504. outline: 2px solid rgb(83, 189, 115) !important;
  3505. }
  3506. </style>
  3507. `));
  3508. }
  3509.  
  3510. function handleConfigStateChange(state) {
  3511. enabled = state;
  3512. if(!enabled) {
  3513. hideComponent();
  3514. }
  3515. }
  3516.  
  3517. function handleXhr(xhr) {
  3518. if(!enabled) {
  3519. return;
  3520. }
  3521. if(!xhr.url.endsWith('getMarketItems')) {
  3522. return;
  3523. }
  3524. update();
  3525. }
  3526.  
  3527. async function update() {
  3528. const listingsContainer = $('market-listings-component .card')[0];
  3529. if(!listingsContainer) {
  3530. return;
  3531. }
  3532. const conversions = await request.getMarketConversion();
  3533. conversionsByType = {};
  3534. for(const conversion of conversions) {
  3535. const typeKey = `${conversion.listingType}-${conversion.type}`;
  3536. if(!conversionsByType[typeKey]) {
  3537. conversionsByType[typeKey] = [];
  3538. }
  3539. conversion.key = `${conversion.name}-${conversion.amount}-${conversion.price}`;
  3540. conversionsByType[typeKey].push(conversion);
  3541. }
  3542. for(const type in conversionsByType) {
  3543. if(type.startsWith('SELL-')) {
  3544. conversionsByType[type].sort((a,b) => a.ratio - b.ratio);
  3545. } else {
  3546. conversionsByType[type].sort((a,b) => b.ratio - a.ratio);
  3547. }
  3548. }
  3549.  
  3550. savedFilters = await request.getMarketFilters();
  3551.  
  3552. $('market-listings-component .search').addClass('saveFilterHover');
  3553.  
  3554. try {
  3555. await elementWatcher.childAddedContinuous('market-listings-component .card', () => {
  3556. if(listUpdatePromiseWrapper) {
  3557. listUpdatePromiseWrapper.resolve();
  3558. listUpdatePromiseWrapper = null;
  3559. }
  3560. })
  3561. } catch(error) {
  3562. console.warn(`Could probably not detect the market listing component, cause : ${error}`);
  3563. return;
  3564. }
  3565.  
  3566. await clearFilter();
  3567. }
  3568.  
  3569. async function applyFilter(filter) {
  3570. Object.assign(currentFilter, {search:null}, filter);
  3571. currentFilter.key = `${currentFilter.listingType}-${currentFilter.type}`;
  3572. if(currentFilter.type && currentFilter.type !== 'None') {
  3573. await clearSearch();
  3574. }
  3575. syncListingsView();
  3576. }
  3577.  
  3578. async function clearSearch() {
  3579. if(!$('market-listings-component .search > input').val()) {
  3580. return;
  3581. }
  3582. listUpdatePromiseWrapper = new Promise.Expiring(5000);
  3583. $('market-listings-component .search > .clear-button').click();
  3584. return listUpdatePromiseWrapper.promise;
  3585. }
  3586.  
  3587. function syncListingsView() {
  3588. const elements = $('market-listings-component .search ~ button').map(function(index,reference) {
  3589. reference = $(reference);
  3590. return {
  3591. name: reference.find('.name').text(),
  3592. amount: parseInt(reference.find('.amount').text().replace(/[,\.]/g, '')),
  3593. price: parseInt(reference.find('.cost').text().replace(/[,\.]/g, '')),
  3594. reference: reference
  3595. };
  3596. }).toArray();
  3597. for(const element of elements) {
  3598. element.key = `${element.name}-${element.amount}-${element.price}`;
  3599. }
  3600. if(currentFilter.search) {
  3601. for(const element of elements) {
  3602. element.reference.find('.ratio').remove();
  3603. element.reference.show();
  3604. }
  3605. const searchReference = $('market-listings-component .search > input');
  3606. searchReference.val(currentFilter.search);
  3607. searchReference[0].dispatchEvent(new Event('input'));
  3608. return;
  3609. }
  3610. let conversions = conversionsByType[currentFilter.key];
  3611. if(!conversions) {
  3612. for(const element of elements) {
  3613. element.reference.find('.ratio').remove();
  3614. element.reference.show();
  3615. }
  3616. return;
  3617. }
  3618. if(currentFilter.amount) {
  3619. conversions = conversions.slice(0, currentFilter.amount);
  3620. }
  3621. const conversionsByKey = {};
  3622. for(const conversion of conversions) {
  3623. conversionsByKey[conversion.key] = conversion;
  3624. }
  3625. for(const element of elements) {
  3626. element.reference.find('.ratio').remove();
  3627. const match = conversionsByKey[element.key];
  3628. if(match) {
  3629. element.reference.show();
  3630. element.reference.find('.amount').after(`<div class='ratio'>(${match.ratio.toFixed(2)})</div>`);
  3631. } else {
  3632. element.reference.hide();
  3633. }
  3634. }
  3635. }
  3636.  
  3637. async function clearFilter() {
  3638. await applyFilter({
  3639. type: 'None',
  3640. amount: 0
  3641. });
  3642. syncCustomView();
  3643. }
  3644.  
  3645. async function saveFilter() {
  3646. let filter = structuredClone(currentFilter);
  3647. if(currentFilter.type === 'None') {
  3648. filter.search = $('market-listings-component .search > input').val();
  3649. if(!filter.search) {
  3650. return;
  3651. }
  3652. }
  3653. filter = await request.saveMarketFilter(filter);
  3654. savedFilters.push(filter);
  3655. componentBlueprint.selectedTabIndex = 0;
  3656. syncCustomView();
  3657. }
  3658.  
  3659. async function removeFilter(filter) {
  3660. await request.removeMarketFilter(filter.id);
  3661. savedFilters = savedFilters.filter(a => a.id !== filter.id);
  3662. syncCustomView();
  3663. }
  3664.  
  3665. function syncCustomView() {
  3666. for(const option of components.search(componentBlueprint, 'filterDropdown').options) {
  3667. option.selected = option.value === currentFilter.type;
  3668. }
  3669. components.search(componentBlueprint, 'amountInput').value = currentFilter.amount;
  3670. components.search(componentBlueprint, 'savedFiltersTab').hidden = !savedFilters.length;
  3671. if(!savedFilters.length) {
  3672. componentBlueprint.selectedTabIndex = 1;
  3673. }
  3674. const savedFiltersSegment = components.search(componentBlueprint, 'savedFiltersSegment');
  3675. savedFiltersSegment.rows = [];
  3676. for(const savedFilter of savedFilters) {
  3677. let text = `Type : ${savedFilter.type}`;
  3678. if(savedFilter.amount) {
  3679. text = `Type : ${savedFilter.amount} x ${savedFilter.type}`;
  3680. }
  3681. if(savedFilter.search) {
  3682. text = `Search : ${savedFilter.search}`;
  3683. }
  3684. savedFiltersSegment.rows.push({
  3685. type: 'buttons',
  3686. buttons: [{
  3687. text: text,
  3688. size: 3,
  3689. color: 'primary',
  3690. action: async function() {
  3691. await applyFilter(savedFilter);
  3692. syncCustomView();
  3693. }
  3694. },{
  3695. text: 'Remove',
  3696. color: 'danger',
  3697. action: removeFilter.bind(null,savedFilter)
  3698. }]
  3699. });
  3700. }
  3701. showComponent();
  3702. }
  3703.  
  3704. function hideComponent() {
  3705. components.removeComponent(componentBlueprint);
  3706. }
  3707.  
  3708. function showComponent() {
  3709. components.addComponent(componentBlueprint);
  3710. }
  3711.  
  3712. const componentBlueprint = {
  3713. componentId : 'marketFilterComponent',
  3714. dependsOn: 'market-page',
  3715. parent : 'market-listings-component > .groups > :last-child',
  3716. selectedTabIndex : 0,
  3717. tabs : [{
  3718. id: 'savedFiltersTab',
  3719. title : 'Saved filters',
  3720. hidden: true,
  3721. rows: [{
  3722. type: 'segment',
  3723. id: 'savedFiltersSegment',
  3724. rows: []
  3725. }, {
  3726. type: 'buttons',
  3727. buttons: [{
  3728. text: 'Clear filter',
  3729. color: 'warning',
  3730. action: async function() {
  3731. await clearFilter();
  3732. await clearSearch();
  3733. }
  3734. }]
  3735. }]
  3736. }, {
  3737. title : 'Filter',
  3738. rows: [{
  3739. type: 'dropdown',
  3740. id: 'filterDropdown',
  3741. action: type => applyFilter({type}),
  3742. class: 'saveFilterHover',
  3743. options: [{
  3744. text: 'None',
  3745. value: 'None',
  3746. selected: false
  3747. }, {
  3748. text: 'Food',
  3749. value: 'Food',
  3750. selected: false
  3751. }, {
  3752. text: 'Charcoal',
  3753. value: 'Charcoal',
  3754. selected: false
  3755. }, {
  3756. text: 'Compost',
  3757. value: 'Compost',
  3758. selected: false
  3759. }]
  3760. }, {
  3761. type: 'input',
  3762. id: 'amountInput',
  3763. name: 'Amount',
  3764. value: '',
  3765. inputType: 'number',
  3766. action: amount => applyFilter({amount:+amount}),
  3767. class: 'saveFilterHover'
  3768. }, {
  3769. type: 'buttons',
  3770. buttons: [{
  3771. text: 'Save filter',
  3772. action: saveFilter,
  3773. color: 'success',
  3774. class: 'saveFilterHoverTrigger'
  3775. }]
  3776. }, {
  3777. type: 'buttons',
  3778. buttons: [{
  3779. text: 'Clear filter',
  3780. color: 'warning',
  3781. action: async function() {
  3782. await clearFilter();
  3783. await clearSearch();
  3784. }
  3785. }]
  3786. }]
  3787. }]
  3788. };
  3789.  
  3790. initialise();
  3791.  
  3792. }
  3793. );
  3794. // recipeClickthrough
  3795. window.moduleRegistry.add('recipeClickthrough', (request, configuration, util) => {
  3796.  
  3797. let enabled = false;
  3798. let recipeCacheByName;
  3799. let recipeCacheByImage;
  3800. let element;
  3801.  
  3802. async function initialise() {
  3803. configuration.registerCheckbox({
  3804. category: 'UI Features',
  3805. key: 'recipe-click',
  3806. name: 'Recipe clickthrough',
  3807. default: true,
  3808. handler: handleConfigStateChange
  3809. });
  3810. $(document).on('click', 'div.image > img', handleClick);
  3811. }
  3812.  
  3813. function handleConfigStateChange(state) {
  3814. enabled = state;
  3815. setupRecipeCache();
  3816. }
  3817.  
  3818. async function setupRecipeCache() {
  3819. if(!enabled || recipeCacheByName) {
  3820. return;
  3821. }
  3822. recipeCacheByName = {};
  3823. recipeCacheByImage = {};
  3824. const recipes = await request.listRecipes();
  3825. for(const recipe of recipes) {
  3826. if(!recipeCacheByName[recipe.name]) {
  3827. recipeCacheByName[recipe.name] = recipe;
  3828. }
  3829. const lastPart = recipe.image.split('/').at(-1);
  3830. if(!recipeCacheByImage[lastPart]) {
  3831. recipeCacheByImage[lastPart] = recipe;
  3832. }
  3833. }
  3834. }
  3835.  
  3836. function handleClick(event) {
  3837. if(!enabled || !recipeCacheByName) {
  3838. return;
  3839. }
  3840. if($(event.currentTarget).closest('button').length) {
  3841. return;
  3842. }
  3843. event.stopPropagation();
  3844. const name = $(event.relatedTarget).find('.name').text();
  3845. const nameMatch = recipeCacheByName[name];
  3846. if(nameMatch) {
  3847. return followRecipe(nameMatch);
  3848. }
  3849.  
  3850. const parts = event.target.src.split('/');
  3851. const lastPart = parts[parts.length-1];
  3852. const imageMatch = recipeCacheByImage[lastPart];
  3853. if(imageMatch) {
  3854. return followRecipe(imageMatch);
  3855. }
  3856. }
  3857.  
  3858. function followRecipe(recipe) {
  3859. util.goToPage(recipe.url);
  3860. }
  3861.  
  3862. initialise();
  3863.  
  3864. }
  3865. );
  3866. // skillOverviewPage
  3867. window.moduleRegistry.add('skillOverviewPage', (pages, components, elementWatcher, skillCache, userCache, events, util, configuration) => {
  3868.  
  3869. const registerUserCacheHandler = events.register.bind(null, 'userCache');
  3870.  
  3871. const PAGE_NAME = 'Skill overview';
  3872. const SKILL_COUNT = 13;
  3873. const MAX_LEVEL = 100;
  3874. const MAX_TOTAL_LEVEL = SKILL_COUNT * MAX_LEVEL;
  3875. const MAX_TOTAL_EXP = SKILL_COUNT * util.levelToExp(MAX_LEVEL);
  3876.  
  3877. let skillProperties = null;
  3878. let skillTotalLevel = null;
  3879. let skillTotalExp = null;
  3880.  
  3881. async function initialise() {
  3882. registerUserCacheHandler(handleUserCache);
  3883. await pages.register({
  3884. category: 'Skills',
  3885. name: PAGE_NAME,
  3886. image: 'https://cdn-icons-png.flaticon.com/128/1160/1160329.png',
  3887. columns: '2',
  3888. render: renderPage
  3889. });
  3890. configuration.registerCheckbox({
  3891. category: 'Pages',
  3892. key: 'skill-overview-enabled',
  3893. name: 'Skill Overview',
  3894. default: true,
  3895. handler: handleConfigStateChange
  3896. });
  3897.  
  3898. await setupSkillProperties();
  3899. await handleUserCache();
  3900. }
  3901.  
  3902. async function setupSkillProperties() {
  3903. await skillCache.ready;
  3904. await userCache.ready;
  3905. skillProperties = [];
  3906. const skillIds = Object.keys(userCache.exp);
  3907. for(const id of skillIds) {
  3908. if(!skillCache.byId[id]) {
  3909. continue;
  3910. }
  3911. skillProperties.push({
  3912. id: id,
  3913. name: skillCache.byId[id].name,
  3914. image: skillCache.byId[id].image,
  3915. color: skillCache.byId[id].color,
  3916. defaultActionId: skillCache.byId[id].defaultActionId,
  3917. maxLevel: MAX_LEVEL,
  3918. showExp: true,
  3919. showLevel: true
  3920. });
  3921. }
  3922. skillProperties.push(skillTotalLevel = {
  3923. id: skillCache.byName['Total-level'].id,
  3924. name: 'TotalLevel',
  3925. image: skillCache.byName['Total-level'].image,
  3926. color: skillCache.byName['Total-level'].color,
  3927. maxLevel: MAX_TOTAL_LEVEL,
  3928. showExp: false,
  3929. showLevel: true
  3930. });
  3931. skillProperties.push(skillTotalExp = {
  3932. id: skillCache.byName['Total-exp'].id,
  3933. name: 'TotalExp',
  3934. image: skillCache.byName['Total-exp'].image,
  3935. color: skillCache.byName['Total-exp'].color,
  3936. maxLevel: MAX_TOTAL_EXP,
  3937. showExp: true,
  3938. showLevel: false
  3939. });
  3940. }
  3941.  
  3942. function handleConfigStateChange(state, name) {
  3943. if(state) {
  3944. pages.show(PAGE_NAME);
  3945. } else {
  3946. pages.hide(PAGE_NAME);
  3947. }
  3948. }
  3949.  
  3950. async function handleUserCache() {
  3951. if(!skillProperties) {
  3952. return;
  3953. }
  3954. await userCache.ready;
  3955.  
  3956. let totalExp = 0;
  3957. let totalLevel = 0;
  3958. for(const skill of skillProperties) {
  3959. if(skill.id <= 0) {
  3960. continue;
  3961. }
  3962. let exp = userCache.exp[skill.id];
  3963. skill.exp = util.expToCurrentExp(exp);
  3964. skill.level = util.expToLevel(exp);
  3965. skill.expToLevel = util.expToNextLevel(exp);
  3966. totalExp += exp;
  3967. totalLevel += skill.level;
  3968. }
  3969.  
  3970. skillTotalExp.exp = totalExp;
  3971. skillTotalExp.level = totalExp;
  3972. skillTotalExp.expToLevel = MAX_TOTAL_EXP - totalExp;
  3973. skillTotalLevel.exp = totalLevel;
  3974. skillTotalLevel.level = totalLevel;
  3975. skillTotalLevel.expToLevel = MAX_TOTAL_LEVEL - totalLevel;
  3976.  
  3977. pages.requestRender(PAGE_NAME);
  3978. }
  3979.  
  3980. async function renderPage() {
  3981. if(!skillProperties) {
  3982. return;
  3983. }
  3984. await elementWatcher.exists(componentBlueprint.dependsOn);
  3985.  
  3986. let column = 0;
  3987.  
  3988. for(const skill of skillProperties) {
  3989. componentBlueprint.componentId = 'skillOverviewComponent_' + skill.name;
  3990. componentBlueprint.parent = '.column' + column;
  3991. if(skill.defaultActionId) {
  3992. componentBlueprint.onClick = util.goToPage.bind(null, `/skill/${skill.id}/action/${skill.defaultActionId}`);
  3993. } else {
  3994. delete componentBlueprint.onClick;
  3995. }
  3996. column = 1 - column; // alternate columns
  3997.  
  3998. const skillHeader = components.search(componentBlueprint, 'skillHeader');
  3999. skillHeader.title = skill.name;
  4000. skillHeader.image = `/assets/${skill.image}`;
  4001. if(skill.showLevel) {
  4002. skillHeader.textRight = `Lv. ${skill.level} <span style='color: #aaa'>/ ${skill.maxLevel}</span>`;
  4003. } else {
  4004. skillHeader.textRight = '';
  4005. }
  4006.  
  4007.  
  4008. const skillProgress = components.search(componentBlueprint, 'skillProgress');
  4009. if(skill.showExp) {
  4010. skillProgress.progressText = `${util.formatNumber(skill.exp)} / ${util.formatNumber(skill.exp + skill.expToLevel)} XP`;
  4011. } else {
  4012. skillProgress.progressText = '';
  4013. }
  4014. skillProgress.progressPercent = Math.floor(skill.exp / (skill.exp + skill.expToLevel) * 100);
  4015. skillProgress.color = skill.color;
  4016.  
  4017. components.addComponent(componentBlueprint);
  4018. }
  4019. }
  4020.  
  4021. const componentBlueprint = {
  4022. componentId: 'skillOverviewComponent',
  4023. dependsOn: 'custom-page',
  4024. parent: '.column0',
  4025. selectedTabIndex: 0,
  4026. tabs: [
  4027. {
  4028. title: 'Skillname',
  4029. rows: [
  4030. {
  4031. id: 'skillHeader',
  4032. type: 'header',
  4033. title: 'Forging',
  4034. image: '/assets/misc/merchant.png',
  4035. textRight: `Lv. 69 <span style='color: #aaa'>/ 420</span>`
  4036. },
  4037. {
  4038. id: 'skillProgress',
  4039. type: 'progress',
  4040. progressText: '301,313 / 309,469 XP',
  4041. progressPercent: '97'
  4042. }
  4043. ]
  4044. },
  4045. ]
  4046. };
  4047.  
  4048. initialise();
  4049. }
  4050. );
  4051. // syncWarningPage
  4052. window.moduleRegistry.add('syncWarningPage', (auth, pages, components, util) => {
  4053.  
  4054. const PAGE_NAME = 'Plugin not synced';
  4055. const STARTED = new Date().getTime();
  4056.  
  4057. async function initialise() {
  4058. await addSyncedPage();
  4059. window.setInterval(pages.requestRender.bind(null, PAGE_NAME), 1000);
  4060. await auth.ready;
  4061. removeSyncedPage();
  4062. }
  4063.  
  4064. async function addSyncedPage() {
  4065. await pages.register({
  4066. category: 'Character',
  4067. name: PAGE_NAME,
  4068. image: 'https://cdn-icons-png.flaticon.com/512/6119/6119820.png',
  4069. columns: 3,
  4070. render: renderPage
  4071. });
  4072. pages.show(PAGE_NAME);
  4073. }
  4074.  
  4075. function removeSyncedPage() {
  4076. pages.hide(PAGE_NAME);
  4077. }
  4078.  
  4079. function renderPage() {
  4080. const millisElapsed = new Date().getTime() - STARTED;
  4081. const timer = util.secondsToDuration(60 * 15 - millisElapsed/1000);
  4082. const texts = [
  4083. 'For the Pancake-Scripts plugin to work correctly, it needs to be up and running as fast as possible after the page loaded.',
  4084. 'If you see this message, it was not fast enough, and you may need to wait up to 15 minutes for the plugin to work correctly.',
  4085. 'If you used the plugin succesfully before, and this is the first time you see this message, it may just be a one-off issue, and you can try refreshing your page.',
  4086. 'Some things you can do to make the plugin load faster next time:',
  4087. '* Place the script at the top of all of your scripts. They are evaluated in order.',
  4088. '* Double check that "@run-at" is set to "document-start"',
  4089. 'Estimated time until the next authentication check-in : ' + timer,
  4090. 'If you still see this after the above timer runs out, feel free to contact @pancake.lord on Discord'
  4091. ];
  4092.  
  4093. for(const index in texts) {
  4094. componentBlueprint.componentId = 'authWarningComponent_' + index;
  4095. components.search(componentBlueprint, 'infoField').name = texts[index];
  4096. components.addComponent(componentBlueprint);
  4097. }
  4098. }
  4099.  
  4100. const componentBlueprint = {
  4101. componentId: 'authWarningComponent',
  4102. dependsOn: 'custom-page',
  4103. parent: '.column1',
  4104. selectedTabIndex: 0,
  4105. tabs: [
  4106. {
  4107. title: 'Info',
  4108. rows: [
  4109. {
  4110. id: 'infoField',
  4111. type: 'item',
  4112. name: ''
  4113. }
  4114. ]
  4115. },
  4116. ]
  4117. };
  4118.  
  4119. initialise();
  4120.  
  4121. }
  4122. );
  4123. // ui
  4124. window.moduleRegistry.add('ui', (configuration) => {
  4125.  
  4126. const id = crypto.randomUUID();
  4127. const sections = [
  4128. //'inventory-page',
  4129. 'equipment-page',
  4130. 'home-page',
  4131. 'merchant-page',
  4132. 'market-page',
  4133. 'daily-quest-page',
  4134. 'quest-shop-page',
  4135. 'skill-page',
  4136. 'upgrade-page',
  4137. 'leaderboards-page',
  4138. 'changelog-page',
  4139. 'settings-page',
  4140. 'guild-page'
  4141. ].join(', ');
  4142. const selector = `:is(${sections})`;
  4143. let gap
  4144.  
  4145. function initialise() {
  4146. configuration.registerCheckbox({
  4147. category: 'UI Features',
  4148. key: 'ui-changes',
  4149. name: 'UI changes',
  4150. default: true,
  4151. handler: handleConfigStateChange
  4152. });
  4153. }
  4154.  
  4155. function handleConfigStateChange(state) {
  4156. if(state) {
  4157. add();
  4158. } else {
  4159. remove();
  4160. }
  4161. }
  4162.  
  4163. function add() {
  4164. document.documentElement.style.setProperty('--gap', '8px');
  4165. const element = $(`
  4166. <style>
  4167. ${selector} :not(.multi-row) > :is(
  4168. button.item,
  4169. button.row,
  4170. button.socket-button,
  4171. button.level-button,
  4172. div.item,
  4173. div.row
  4174. ) {
  4175. padding: 2px 6px !important;
  4176. min-height: 0 !important;
  4177. min-width: 0 !important;
  4178. }
  4179.  
  4180. ${selector} :not(.multi-row) > :is(
  4181. button.item div.image,
  4182. button.row div.image,
  4183. div.item div.image,
  4184. div.item div.placeholder-image,
  4185. div.row div.image
  4186. ) {
  4187. height: 32px !important;
  4188. width: 32px !important;
  4189. min-height: 0 !important;
  4190. min-width: 0 !important;
  4191. }
  4192.  
  4193. action-component div.body > div.image,
  4194. produce-component div.body > div.image,
  4195. daily-quest-page div.body > div.image {
  4196. height: 48px !important;
  4197. width: 48px !important;
  4198. }
  4199.  
  4200. div.progress div.body {
  4201. padding: 8px !important;
  4202. }
  4203.  
  4204. action-component div.bars {
  4205. padding: 0 !important;
  4206. }
  4207.  
  4208. equipment-component button {
  4209. padding: 0 !important;
  4210. }
  4211.  
  4212. inventory-page .items {
  4213. grid-gap: 0 !important;
  4214. }
  4215.  
  4216. div.scroll.custom-scrollbar .header,
  4217. div.scroll.custom-scrollbar button {
  4218. height: 28px !important;
  4219. }
  4220.  
  4221. div.scroll.custom-scrollbar img {
  4222. height: 16px !important;
  4223. width: 16px !important;
  4224. }
  4225.  
  4226. .scroll {
  4227. overflow-y: auto !important;
  4228. }
  4229. .scroll {
  4230. -ms-overflow-style: none; /* Internet Explorer 10+ */
  4231. scrollbar-width: none; /* Firefox */
  4232. }
  4233. .scroll::-webkit-scrollbar {
  4234. display: none; /* Safari and Chrome */
  4235. }
  4236. </style>
  4237. `).attr('id', id);
  4238. window.$('head').append(element);
  4239. }
  4240.  
  4241. function remove() {
  4242. document.documentElement.style.removeProperty('--gap');
  4243. $(`#${id}`).remove();
  4244. }
  4245.  
  4246. initialise();
  4247.  
  4248. }
  4249. );
  4250. // webhooksRegistry
  4251. window.moduleRegistry.add('webhooksRegistry', (webhooks) => {
  4252.  
  4253. function initialise() {
  4254. webhooks.register('webhook-update', 'Update', 'UPDATE');
  4255. webhooks.register('webhook-guild', 'Guild', 'GUILD');
  4256. }
  4257.  
  4258. initialise();
  4259.  
  4260. }
  4261. );
  4262. window.moduleRegistry.build();