Ironwood RPG - Pancake-Scripts

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

当前为 2024-04-03 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Ironwood RPG - Pancake-Scripts
  3. // @namespace http://tampermonkey.net/
  4. // @version 4.3.0
  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. // @require https://code.jquery.com/jquery-3.6.4.min.js
  11. // @require https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.0/chart.umd.js
  12. // ==/UserScript==
  13.  
  14. window.PANCAKE_ROOT = 'https://iwrpg.vectordungeon.com';
  15. window.PANCAKE_VERSION = '4.3.0';
  16. (() => {
  17.  
  18. if(window.moduleRegistry) {
  19. return;
  20. }
  21.  
  22. window.moduleRegistry = {
  23. add,
  24. get,
  25. build
  26. };
  27.  
  28. const modules = {};
  29.  
  30. function add(name, initialiser) {
  31. modules[name] = createModule(name, initialiser);
  32. }
  33.  
  34. function get(name) {
  35. return modules[name] || null;
  36. }
  37.  
  38. async function build() {
  39. for(const module of Object.values(modules)) {
  40. await buildModule(module);
  41. }
  42. }
  43.  
  44. function createModule(name, initialiser) {
  45. const dependencies = extractParametersFromFunction(initialiser).map(dependency => {
  46. const name = dependency.replaceAll('_', '');
  47. const module = get(name);
  48. const optional = dependency.startsWith('_');
  49. return { name, module, optional };
  50. });
  51. const module = {
  52. name,
  53. initialiser,
  54. dependencies
  55. };
  56. for(const other of Object.values(modules)) {
  57. for(const dependency of other.dependencies) {
  58. if(dependency.name === name) {
  59. dependency.module = module;
  60. }
  61. }
  62. }
  63. return module;
  64. }
  65.  
  66. async function buildModule(module, partial, chain) {
  67. if(module.built) {
  68. return true;
  69. }
  70.  
  71. chain = chain || [];
  72. if(chain.includes(module.name)) {
  73. chain.push(module.name);
  74. throw `Circular dependency in chain : ${chain.join(' -> ')}`;
  75. }
  76. chain.push(module.name);
  77.  
  78. for(const dependency of module.dependencies) {
  79. if(!dependency.module) {
  80. if(partial) {
  81. return false;
  82. }
  83. if(dependency.optional) {
  84. continue;
  85. }
  86. throw `Unresolved dependency : ${dependency.name}`;
  87. }
  88. const built = await buildModule(dependency.module, partial, chain);
  89. if(!built) {
  90. return false;
  91. }
  92. }
  93.  
  94. const parameters = module.dependencies.map(a => a.module?.reference);
  95. try {
  96. module.reference = await module.initialiser.apply(null, parameters);
  97. } catch(e) {
  98. console.error(`Failed building ${module.name}`, e);
  99. return false;
  100. }
  101. module.built = true;
  102.  
  103. chain.pop();
  104. return true;
  105. }
  106.  
  107. function extractParametersFromFunction(fn) {
  108. const PARAMETER_NAMES = /([^\s,]+)/g;
  109. var fnStr = fn.toString();
  110. var result = fnStr.slice(fnStr.indexOf('(')+1, fnStr.indexOf(')')).match(PARAMETER_NAMES);
  111. return result || [];
  112. }
  113.  
  114. })();
  115. // colorMapper
  116. window.moduleRegistry.add('colorMapper', () => {
  117.  
  118. const colorMappings = {
  119. // https://colorswall.com/palette/3
  120. primary: '#0275d8',
  121. success: '#5cb85c',
  122. info: '#5bc0de',
  123. warning: '#f0ad4e',
  124. danger: '#d9534f',
  125. inverse: '#292b2c',
  126. // component styling
  127. componentLight: '#393532',
  128. componentRegular: '#28211b',
  129. componentDark: '#211a12'
  130. };
  131.  
  132. function mapColor(color) {
  133. return colorMappings[color] || color;
  134. }
  135.  
  136. return mapColor;
  137.  
  138. }
  139. );
  140. // components
  141. window.moduleRegistry.add('components', (elementWatcher, colorMapper, elementCreator, localDatabase, Promise) => {
  142.  
  143. const exports = {
  144. addComponent,
  145. removeComponent,
  146. search
  147. };
  148.  
  149. const initialised = new Promise.Expiring(2000, 'components');
  150. const STORE_NAME = 'component-tabs';
  151. const rowTypeMappings = {
  152. item: createRow_Item,
  153. input: createRow_Input,
  154. break: createRow_Break,
  155. buttons: createRow_Button,
  156. dropdown: createRow_Select,
  157. header: createRow_Header,
  158. checkbox: createRow_Checkbox,
  159. segment: createRow_Segment,
  160. progress: createRow_Progress,
  161. chart: createRow_Chart,
  162. list: createRow_List
  163. };
  164. let selectedTabs = null;
  165.  
  166. async function initialise() {
  167. elementCreator.addStyles(styles);
  168. selectedTabs = await localDatabase.getAllEntries(STORE_NAME);
  169. initialised.resolve(exports);
  170. }
  171.  
  172. function removeComponent(blueprint) {
  173. $(`#${blueprint.componentId}`).remove();
  174. }
  175.  
  176. async function addComponent(blueprint) {
  177. if($(blueprint.dependsOn).length) {
  178. actualAddComponent(blueprint);
  179. return;
  180. }
  181. await elementWatcher.exists(blueprint.dependsOn);
  182. actualAddComponent(blueprint);
  183. }
  184.  
  185. function actualAddComponent(blueprint) {
  186. const component =
  187. $('<div/>')
  188. .addClass('customComponent')
  189. .attr('id', blueprint.componentId);
  190. if(blueprint.onClick) {
  191. component
  192. .click(blueprint.onClick)
  193. .css('cursor', 'pointer');
  194. }
  195.  
  196. // TABS
  197. const selectedTabMatch = selectedTabs.find(a => a.key === blueprint.componentId);
  198. if(selectedTabMatch) {
  199. blueprint.selectedTabIndex = selectedTabMatch.value;
  200. selectedTabs = selectedTabs.filter(a => a.key !== blueprint.componentId);
  201. }
  202. const theTabs = createTab(blueprint);
  203. component.append(theTabs);
  204.  
  205. // PAGE
  206. const selectedTabBlueprint = blueprint.tabs[blueprint.selectedTabIndex] || blueprint.tabs[0];
  207. selectedTabBlueprint.rows.forEach((rowBlueprint, index) => {
  208. component.append(createRow(rowBlueprint));
  209. });
  210.  
  211. const existing = $(`#${blueprint.componentId}`);
  212. if(existing.length) {
  213. existing.replaceWith(component);
  214. } else if(blueprint.prepend) {
  215. $(blueprint.parent).prepend(component);
  216. } else {
  217. $(blueprint.parent).append(component);
  218. }
  219. }
  220.  
  221. function createTab(blueprint) {
  222. if(!blueprint.selectedTabIndex) {
  223. blueprint.selectedTabIndex = 0;
  224. }
  225. if(blueprint.tabs.length === 1) {
  226. return;
  227. }
  228. const tabContainer = $('<div/>').addClass('tabs');
  229. blueprint.tabs.forEach((element, index) => {
  230. if(element.hidden) {
  231. return;
  232. }
  233. const tab = $('<button/>')
  234. .attr('type', 'button')
  235. .addClass('tabButton')
  236. .text(element.title)
  237. .click(changeTab.bind(null, blueprint, index));
  238. if(blueprint.selectedTabIndex !== index) {
  239. tab.addClass('tabButtonInactive')
  240. }
  241. if(index !== 0) {
  242. tab.addClass('lineLeft')
  243. }
  244. tabContainer.append(tab);
  245. });
  246. return tabContainer;
  247. }
  248.  
  249. function createRow(rowBlueprint) {
  250. if(!rowTypeMappings[rowBlueprint.type]) {
  251. console.warn(`Skipping unknown row type in blueprint: ${rowBlueprint.type}`, rowBlueprint);
  252. return;
  253. }
  254. if(rowBlueprint.hidden) {
  255. return;
  256. }
  257. return rowTypeMappings[rowBlueprint.type](rowBlueprint);
  258. }
  259.  
  260. function createRow_Item(itemBlueprint) {
  261. const parentRow = $('<div/>').addClass('customRow');
  262. if(itemBlueprint.image) {
  263. parentRow.append(createImage(itemBlueprint));
  264. }
  265. if(itemBlueprint?.name) {
  266. parentRow
  267. .append(
  268. $('<div/>')
  269. .addClass('myItemName name')
  270. .text(itemBlueprint.name)
  271. );
  272. }
  273. parentRow // always added because it spreads pushes name left and value right !
  274. .append(
  275. $('<div/>')
  276. .addClass('myItemValue')
  277. .text(itemBlueprint?.extra || '')
  278. );
  279. if(itemBlueprint?.value) {
  280. parentRow
  281. .append(
  282. $('<div/>')
  283. .addClass('myItemWorth')
  284. .text(itemBlueprint.value)
  285. )
  286. }
  287. return parentRow;
  288. }
  289.  
  290. function createRow_Input(inputBlueprint) {
  291. const parentRow = $('<div/>').addClass('customRow');
  292. if(inputBlueprint.text) {
  293. parentRow
  294. .append(
  295. $('<div/>')
  296. .addClass('myItemInputText')
  297. .addClass(inputBlueprint.class || '')
  298. .text(inputBlueprint.text)
  299. .css('flex', `${inputBlueprint.layout?.split('/')[0] || 1}`)
  300. )
  301. }
  302. parentRow
  303. .append(
  304. $('<input/>')
  305. .attr('id', inputBlueprint.id)
  306. .addClass('myItemInput')
  307. .addClass(inputBlueprint.class || '')
  308. .attr('type', inputBlueprint.inputType || 'text')
  309. .attr('placeholder', inputBlueprint.name)
  310. .attr('value', inputBlueprint.value || '')
  311. .css('flex', `${inputBlueprint.layout?.split('/')[1] || 1}`)
  312. .keyup(inputDelay(function(e) {
  313. inputBlueprint.value = e.target.value;
  314. if(inputBlueprint.action) {
  315. inputBlueprint.action(inputBlueprint.value);
  316. }
  317. }, inputBlueprint.delay || 0))
  318. )
  319. return parentRow;
  320. }
  321.  
  322. function createRow_Break(breakBlueprint) {
  323. const parentRow = $('<div/>').addClass('customRow');
  324. parentRow.append('<br/>');
  325. return parentRow;
  326. }
  327.  
  328. function createRow_Button(buttonBlueprint) {
  329. const parentRow = $('<div/>').addClass('customRow');
  330. for(const button of buttonBlueprint.buttons) {
  331. parentRow
  332. .append(
  333. $(`<button class='myButton'>${button.text}</button>`)
  334. .css('background-color', button.disabled ? '#ffffff0a' : colorMapper(button.color || 'primary'))
  335. .css('flex', `${button.size || 1} 1 0`)
  336. .prop('disabled', !!button.disabled)
  337. .addClass(button.class || '')
  338. .click(button.action)
  339. );
  340. }
  341. return parentRow;
  342. }
  343.  
  344. function createRow_Select(selectBlueprint) {
  345. const parentRow = $('<div/>').addClass('customRow');
  346. const select = $('<select/>')
  347. .addClass('myItemSelect')
  348. .addClass(selectBlueprint.class || '')
  349. .change(inputDelay(function(e) {
  350. for(const option of selectBlueprint.options) {
  351. option.selected = this.value === option.value;
  352. }
  353. if(selectBlueprint.action) {
  354. selectBlueprint.action(this.value);
  355. }
  356. }, selectBlueprint.delay || 0));
  357. for(const option of selectBlueprint.options) {
  358. select.append(`<option value='${option.value}' ${option.selected ? 'selected' : ''}>${option.text}</option>`);
  359. }
  360. parentRow.append(select);
  361. return parentRow;
  362. }
  363.  
  364. function createRow_Header(headerBlueprint) {
  365. const parentRow =
  366. $('<div/>')
  367. .addClass('myHeader lineTop')
  368. if(headerBlueprint.image) {
  369. parentRow.append(createImage(headerBlueprint));
  370. }
  371. parentRow.append(
  372. $('<div/>')
  373. .addClass('myName')
  374. .text(headerBlueprint.title)
  375. )
  376. if(headerBlueprint.action) {
  377. parentRow
  378. .append(
  379. $('<button/>')
  380. .addClass('myHeaderAction')
  381. .text(headerBlueprint.name)
  382. .attr('type', 'button')
  383. .css('background-color', colorMapper(headerBlueprint.color || 'success'))
  384. .click(headerBlueprint.action)
  385. )
  386. } else if(headerBlueprint.textRight) {
  387. parentRow.append(
  388. $('<div/>')
  389. .addClass('level')
  390. .text(headerBlueprint.title)
  391. .css('margin-left', 'auto')
  392. .html(headerBlueprint.textRight)
  393. )
  394. }
  395. if(headerBlueprint.centered) {
  396. parentRow.css('justify-content', 'center');
  397. }
  398. return parentRow;
  399. }
  400.  
  401. function createRow_Checkbox(checkboxBlueprint) {
  402. 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>`;
  403. 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>`;
  404.  
  405. const buttonInnerHTML = checkboxBlueprint.checked ? checked_true : checked_false;
  406.  
  407. const parentRow = $('<div/>').addClass('customRow')
  408. .append(
  409. $('<div/>')
  410. .addClass('customCheckBoxText')
  411. .text(checkboxBlueprint?.text || '')
  412. )
  413. .append(
  414. $('<div/>')
  415. .addClass('customCheckboxCheckbox')
  416. .append(
  417. $(`<button>${buttonInnerHTML}</button>`)
  418. .html(buttonInnerHTML)
  419. .click(() => {
  420. checkboxBlueprint.checked = !checkboxBlueprint.checked;
  421. if(checkboxBlueprint.action) {
  422. checkboxBlueprint.action(checkboxBlueprint.checked);
  423. }
  424. })
  425. )
  426.  
  427. );
  428.  
  429. return parentRow;
  430. }
  431.  
  432. function createRow_Segment(segmentBlueprint) {
  433. if(segmentBlueprint.hidden) {
  434. return;
  435. }
  436. return segmentBlueprint.rows.flatMap(createRow);
  437. }
  438.  
  439. function createRow_Progress(progressBlueprint) {
  440. const parentRow = $('<div/>').addClass('customRow');
  441. const up = progressBlueprint.numerator;
  442. const down = progressBlueprint.denominator;
  443. parentRow.append(
  444. $('<div/>')
  445. .addClass('myBar')
  446. .append(
  447. $('<div/>')
  448. .css('height', '100%')
  449. .css('width', progressBlueprint.progressPercent + '%')
  450. .css('background-color', colorMapper(progressBlueprint.color || 'rgb(122, 118, 118)'))
  451. )
  452. );
  453. parentRow.append(
  454. $('<div/>')
  455. .addClass('myPercent')
  456. .text(progressBlueprint.progressPercent + '%')
  457. )
  458. parentRow.append(
  459. $('<div/>')
  460. .css('margin-left', 'auto')
  461. .text(progressBlueprint.progressText)
  462. )
  463. return parentRow;
  464. }
  465.  
  466. function createRow_Chart(chartBlueprint) {
  467. const parentRow = $('<div/>')
  468. .addClass('lineTop')
  469. .append(
  470. $('<canvas/>')
  471. .attr('id', chartBlueprint.chartId)
  472. );
  473. return parentRow;
  474. }
  475.  
  476. function createRow_List(listBlueprint) {
  477. const parentRow = $('<div/>').addClass('customRow');
  478. parentRow // always added because it spreads pushes name left and value right !
  479. .append(
  480. $('<ul/>')
  481. .addClass('myListDescription')
  482. .append(...listBlueprint.entries.map(entry =>
  483. $('<li/>')
  484. .addClass('myListLine')
  485. .text(entry)
  486. ))
  487. );
  488. return parentRow;
  489. }
  490. function createImage(blueprint) {
  491. return $('<div/>')
  492. .addClass('myItemImage image')
  493. .append(
  494. $('<img/>')
  495. .attr('src', `${blueprint.image}`)
  496. .css('filter', `${blueprint.imageFilter}`)
  497. .css('image-rendering', blueprint.imagePixelated ? 'pixelated' : 'auto')
  498. )
  499. }
  500.  
  501. function changeTab(blueprint, index) {
  502. blueprint.selectedTabIndex = index;
  503. localDatabase.saveEntry(STORE_NAME, {
  504. key: blueprint.componentId,
  505. value: index
  506. });
  507. selectedTabs = selectedTabs.filter(a => a.key !== blueprint.componentId);
  508. addComponent(blueprint);
  509. }
  510.  
  511. function inputDelay(callback, ms) {
  512. var timer = 0;
  513. return function() {
  514. var context = this, args = arguments;
  515. window.clearTimeout(timer);
  516. timer = window.setTimeout(function() {
  517. callback.apply(context, args);
  518. }, ms || 0);
  519. };
  520. }
  521.  
  522. function search(blueprint, query) {
  523. if(!blueprint.idMappings) {
  524. generateIdMappings(blueprint);
  525. }
  526. if(!blueprint.idMappings[query]) {
  527. throw `Could not find id ${query} in blueprint ${blueprint.componentId}`;
  528. }
  529. return blueprint.idMappings[query];
  530. }
  531.  
  532. function generateIdMappings(blueprint) {
  533. blueprint.idMappings = {};
  534. for(const tab of blueprint.tabs) {
  535. addIdMapping(blueprint, tab);
  536. for(const row of tab.rows) {
  537. addIdMapping(blueprint, row);
  538. }
  539. }
  540. }
  541.  
  542. function addIdMapping(blueprint, element) {
  543. if(element.id) {
  544. if(blueprint.idMappings[element.id]) {
  545. throw `Detected duplicate id ${element.id} in blueprint ${blueprint.componentId}`;
  546. }
  547. blueprint.idMappings[element.id] = element;
  548. }
  549. let subelements = null;
  550. if(element.type === 'segment') {
  551. subelements = element.rows;
  552. }
  553. if(element.type === 'buttons') {
  554. subelements = element.buttons;
  555. }
  556. if(subelements) {
  557. for(const subelement of subelements) {
  558. addIdMapping(blueprint, subelement);
  559. }
  560. }
  561. }
  562.  
  563. const styles = `
  564. :root {
  565. --background-color: ${colorMapper('componentRegular')};
  566. --border-color: ${colorMapper('componentLight')};
  567. --darker-color: ${colorMapper('componentDark')};
  568. }
  569. .customComponent {
  570. margin-top: var(--gap);
  571. background-color: var(--background-color);
  572. box-shadow: 0 6px 12px -6px #0006;
  573. border-radius: 4px;
  574. width: 100%;
  575. }
  576. .myHeader {
  577. display: flex;
  578. align-items: center;
  579. padding: 12px var(--gap);
  580. gap: var(--gap);
  581. }
  582. .myName {
  583. font-weight: 600;
  584. letter-spacing: .25px;
  585. }
  586. .myHeaderAction{
  587. margin: 0px 0px 0px auto;
  588. border: 1px solid var(--border-color);
  589. border-radius: 4px;
  590. padding: 0px 5px;
  591. }
  592. .customRow {
  593. display: flex;
  594. justify-content: center;
  595. align-items: center;
  596. border-top: 1px solid var(--border-color);
  597. /*padding: 5px 12px 5px 6px;*/
  598. min-height: 0px;
  599. min-width: 0px;
  600. gap: var(--margin);
  601. padding: calc(var(--gap) / 2) var(--gap);
  602. }
  603. .myItemImage {
  604. position: relative;
  605. display: flex;
  606. align-items: center;
  607. justify-content: center;
  608. height: 24px;
  609. width: 24px;
  610. min-height: 0px;
  611. min-width: 0px;
  612. }
  613. .myItemImage > img {
  614. max-width: 100%;
  615. max-height: 100%;
  616. width: 100%;
  617. height: 100%;
  618. }
  619. .myItemValue {
  620. display: flex;
  621. align-items: center;
  622. flex: 1;
  623. color: #aaa;
  624. }
  625. .myItemInputText {
  626. height: 40px;
  627. width: 100%;
  628. display: flex;
  629. align-items: center;
  630. padding: 12px var(--gap);
  631. }
  632. .myItemInput {
  633. height: 40px;
  634. width: 100%;
  635. background-color: #ffffff0a;
  636. padding: 0 12px;
  637. text-align: center;
  638. border-radius: 4px;
  639. border: 1px solid var(--border-color);
  640. }
  641. .myItemSelect {
  642. height: 40px;
  643. width: 100%;
  644. background-color: #ffffff0a;
  645. padding: 0 12px;
  646. text-align: center;
  647. border-radius: 4px;
  648. border: 1px solid var(--border-color);
  649. }
  650. .myItemSelect > option {
  651. background-color: var(--darker-color);
  652. }
  653. .myButton {
  654. flex: 1;
  655. display: flex;
  656. align-items: center;
  657. justify-content: center;
  658. border-radius: 4px;
  659. height: 40px;
  660. font-weight: 600;
  661. letter-spacing: .25px;
  662. }
  663. .myButton[disabled] {
  664. pointer-events: none;
  665. }
  666. .sort {
  667. padding: 12px var(--gap);
  668. border-top: 1px solid var(--border-color);
  669. display: flex;
  670. align-items: center;
  671. justify-content: space-between;
  672. }
  673. .sortButtonContainer {
  674. display: flex;
  675. align-items: center;
  676. border-radius: 4px;
  677. box-shadow: 0 1px 2px #0003;
  678. border: 1px solid var(--border-color);
  679. overflow: hidden;
  680. }
  681. .sortButton {
  682. display: flex;
  683. border: none;
  684. background: transparent;
  685. font-family: inherit;
  686. font-size: inherit;
  687. line-height: 1.5;
  688. font-weight: inherit;
  689. color: inherit;
  690. resize: none;
  691. text-transform: inherit;
  692. letter-spacing: inherit;
  693. cursor: pointer;
  694. padding: 4px var(--gap);
  695. flex: 1;
  696. text-align: center;
  697. justify-content: center;
  698. background-color: var(--darker-color);
  699. }
  700. .tabs {
  701. display: flex;
  702. align-items: center;
  703. overflow: hidden;
  704. border-radius: inherit;
  705. }
  706. .tabButton {
  707. border: none;
  708. border-radius: 0px !important;
  709. background: transparent;
  710. font-family: inherit;
  711. font-size: inherit;
  712. line-height: 1.5;
  713. color: inherit;
  714. resize: none;
  715. text-transform: inherit;
  716. cursor: pointer;
  717. flex: 1;
  718. display: flex;
  719. align-items: center;
  720. justify-content: center;
  721. height: 48px;
  722. font-weight: 600;
  723. letter-spacing: .25px;
  724. padding: 0 var(--gap);
  725. border-radius: 4px 0 0;
  726. }
  727. .tabButtonInactive{
  728. background-color: var(--darker-color);
  729. }
  730. .lineRight {
  731. border-right: 1px solid var(--border-color);
  732. }
  733. .lineLeft {
  734. border-left: 1px solid var(--border-color);
  735. }
  736. .lineTop {
  737. border-top: 1px solid var(--border-color);
  738. }
  739. .customCheckBoxText {
  740. flex: 1;
  741. color: #aaa
  742. }
  743. .customCheckboxCheckbox {
  744. display: flex;
  745. justify-content: flex-end;
  746. min-width: 32px;
  747. margin-left: var(--margin);
  748. }
  749. .customCheckBoxEnabled {
  750. color: #53bd73
  751. }
  752. .customCheckBoxDisabled {
  753. color: #aaa
  754. }
  755. .myBar {
  756. height: 12px;
  757. flex: 1;
  758. background-color: #ffffff0a;
  759. overflow: hidden;
  760. max-width: 50%;
  761. border-radius: 999px;
  762. }
  763. .myPercent {
  764. margin-left: var(--margin);
  765. margin-right: var(--margin);
  766. color: #aaa;
  767. }
  768. .myListDescription {
  769. list-style: disc;
  770. width: 100%;
  771. }
  772. .myListLine {
  773. margin-left: 20px;
  774. }
  775. `;
  776.  
  777. initialise();
  778.  
  779. return initialised;
  780.  
  781. }
  782. );
  783. // configuration
  784. window.moduleRegistry.add('configuration', (Promise, configurationStore) => {
  785.  
  786. const exports = {
  787. registerCheckbox,
  788. registerInput,
  789. registerDropdown,
  790. registerJson,
  791. items: []
  792. };
  793.  
  794. const configs = configurationStore.getConfigs();
  795.  
  796. const CHECKBOX_KEYS = ['category', 'key', 'name', 'default', 'handler'];
  797. function registerCheckbox(item) {
  798. validate(item, CHECKBOX_KEYS);
  799. return register(Object.assign(item, {
  800. type: 'checkbox'
  801. }));
  802. }
  803.  
  804. const INPUT_KEYS = ['category', 'key', 'name', 'default', 'inputType', 'handler'];
  805. function registerInput(item) {
  806. validate(item, INPUT_KEYS);
  807. return register(Object.assign(item, {
  808. type: 'input'
  809. }));
  810. }
  811.  
  812. const DROPDOWN_KEYS = ['category', 'key', 'name', 'options', 'default', 'handler'];
  813. function registerDropdown(item) {
  814. validate(item, DROPDOWN_KEYS);
  815. return register(Object.assign(item, {
  816. type: 'dropdown'
  817. }));
  818. }
  819.  
  820. const JSON_KEYS = ['key', 'default', 'handler'];
  821. function registerJson(item) {
  822. validate(item, JSON_KEYS);
  823. return register(Object.assign(item, {
  824. type: 'json'
  825. }));
  826. }
  827.  
  828. function register(item) {
  829. const handler = item.handler;
  830. item.handler = (value, isInitial) => {
  831. item.value = value;
  832. handler(value, item.key, isInitial);
  833. if(!isInitial) {
  834. save(item, value);
  835. }
  836. }
  837. let initialValue;
  838. if(item.key in configs) {
  839. initialValue = configs[item.key];
  840. } else {
  841. initialValue = item.default;
  842. }
  843. item.handler(initialValue, true);
  844. exports.items.push(item);
  845. return item;
  846. }
  847.  
  848. async function save(item, value) {
  849. if(item.type === 'toggle') {
  850. value = !!value;
  851. }
  852. if(item.type === 'input' || item.type === 'json') {
  853. value = JSON.stringify(value);
  854. }
  855. await configurationStore.save(item.key, value);
  856. }
  857.  
  858. function validate(item, keys) {
  859. for(const key of keys) {
  860. if(!(key in item)) {
  861. throw `Missing ${key} while registering a configuration item`;
  862. }
  863. }
  864. }
  865.  
  866. return exports;
  867.  
  868. }
  869. );
  870. // Distribution
  871. window.moduleRegistry.add('Distribution', () => {
  872.  
  873. class Distribution {
  874.  
  875. #map = new Map();
  876.  
  877. constructor(initial) {
  878. if(initial) {
  879. this.add(initial, 1);
  880. }
  881. }
  882.  
  883. add(value, probability) {
  884. if(this.#map.has(value)) {
  885. this.#map.set(value, this.#map.get(value) + probability);
  886. } else {
  887. this.#map.set(value, probability);
  888. }
  889. }
  890.  
  891. addDistribution(other, weight) {
  892. other.#map.forEach((probability, value) => {
  893. this.add(value, probability * weight);
  894. });
  895. }
  896.  
  897. convolution(other, multiplier) {
  898. const old = this.#map;
  899. this.#map = new Map();
  900. old.forEach((probability, value) => {
  901. other.#map.forEach((probability2, value2) => {
  902. this.add(multiplier(value, value2), probability * probability2);
  903. });
  904. });
  905. }
  906.  
  907. convolutionWithGenerator(generator, multiplier) {
  908. const result = new Distribution();
  909. this.#map.forEach((probability, value) => {
  910. const other = generator(value);
  911. other.#map.forEach((probability2, value2) => {
  912. result.add(multiplier(value, value2), probability * probability2);
  913. });
  914. });
  915. return result;
  916. }
  917.  
  918. count() {
  919. return this.#map.size;
  920. }
  921.  
  922. average() {
  923. let result = 0;
  924. this.#map.forEach((probability, value) => {
  925. result += value * probability;
  926. });
  927. return result;
  928. }
  929.  
  930. sum() {
  931. let result = 0;
  932. this.#map.forEach(probability => {
  933. result += probability;
  934. });
  935. return result;
  936. }
  937.  
  938. min() {
  939. return Array.from(this.#map, ([k, v]) => k).reduce((a,b) => Math.min(a,b), Infinity);
  940. }
  941.  
  942. max() {
  943. return Array.from(this.#map, ([k, v]) => k).reduce((a,b) => Math.max(a,b), -Infinity);
  944. }
  945.  
  946. variance() {
  947. let result = 0;
  948. const average = this.average();
  949. this.#map.forEach((probability, value) => {
  950. const dist = average - value;
  951. result += dist * dist * probability;
  952. });
  953. return result;
  954. }
  955.  
  956. normalize() {
  957. const sum = this.sum();
  958. this.#map = new Map(Array.from(this.#map, ([k, v]) => [k, v / sum]));
  959. }
  960.  
  961. expectedRollsUntill(limit) {
  962. const x = (this.count() - 1) / 2.0;
  963. const y = x * (x + 1) * (2 * x + 1) / 6;
  964. const z = 2*y / this.variance();
  965. const average = this.average();
  966. const a = y + average * (average - 1) * z / 2;
  967. const b = z * average * average;
  968. return limit / average + a / b;
  969. }
  970.  
  971. clone() {
  972. const result = new Distribution();
  973. result.#map = new Map(this.#map);
  974. return result;
  975. }
  976.  
  977. getLeftTail(rolls, cutoff) {
  978. const mean = rolls * this.average();
  979. const variance = rolls * this.variance();
  980. const stdev = Math.sqrt(variance);
  981. return Distribution.cdf(cutoff, mean, stdev);
  982. }
  983.  
  984. getRightTail(rolls, cutoff) {
  985. return 1 - this.getLeftTail(rolls, cutoff);
  986. }
  987.  
  988. getRange(rolls, left, right) {
  989. return 1 - this.getLeftTail(rolls, left) - this.getRightTail(rolls, right);
  990. }
  991.  
  992. getMeanLeftTail(rolls, cutoff) {
  993. return this.getMeanRange(rolls, -Infinity, cutoff);
  994. }
  995.  
  996. getMeanRightTail(rolls, cutoff) {
  997. return this.getMeanRange(rolls, cutoff, Infinity);
  998. }
  999.  
  1000. getMeanRange(rolls, left, right) {
  1001. const mean = rolls * this.average();
  1002. const variance = rolls * this.variance();
  1003. const stdev = Math.sqrt(variance);
  1004. const alpha = (left - mean) / stdev;
  1005. const beta = (right - mean) / stdev;
  1006. const c = Distribution.pdf(beta) - Distribution.pdf(alpha);
  1007. const d = Distribution.cdf(beta, 0, 1) - Distribution.cdf(alpha, 0, 1);
  1008. if(!c || !d) {
  1009. return (left + right) / 2;
  1010. }
  1011. return mean - stdev * c / d;
  1012. }
  1013.  
  1014. toChart(other) {
  1015. if(other) {
  1016. const min = Math.min(this.min(), other.min());
  1017. const max = Math.max(this.max(), other.max());
  1018. for(let i=min;i<=max;i++) {
  1019. if(!this.#map.has(i)) {
  1020. this.#map.set(i, 0);
  1021. }
  1022. }
  1023. }
  1024. const result = Array.from(this.#map, ([k, v]) => ({x:k,y:v}));
  1025. result.sort((a,b) => a.x - b.x);
  1026. return result;
  1027. }
  1028.  
  1029. redistribute(value, exceptions) {
  1030. // redistributes this single value across all others, except the exceptions
  1031. const probability = this.#map.get(value);
  1032. if(!probability) {
  1033. return;
  1034. }
  1035. this.#map.delete(value);
  1036.  
  1037. let sum = 0;
  1038. this.#map.forEach((p, v) => {
  1039. if(!exceptions.includes(v)) {
  1040. sum += p;
  1041. }
  1042. });
  1043. this.#map.forEach((p, v) => {
  1044. if(!exceptions.includes(v)) {
  1045. this.#map.set(v, p + probability*p/sum);
  1046. }
  1047. });
  1048. }
  1049.  
  1050. };
  1051.  
  1052. Distribution.getRandomChance = function(probability) {
  1053. const result = new Distribution();
  1054. result.add(true, probability);
  1055. result.add(false, 1-probability);
  1056. return result;
  1057. };
  1058.  
  1059. // probability density function -> probability mass function
  1060. Distribution.getRandomOutcomeFloored = function(min, max) {
  1061. const result = new Distribution();
  1062. const rangeMult = 1 / (max - min);
  1063. for(let value=Math.floor(min); value<max; value++) {
  1064. let lower = value;
  1065. let upper = value + 1;
  1066. if(lower < min) {
  1067. lower = min;
  1068. }
  1069. if(upper > max) {
  1070. upper = max;
  1071. }
  1072. result.add(value, (upper - lower) * rangeMult);
  1073. }
  1074. return result;
  1075. };
  1076.  
  1077. Distribution.getRandomOutcomeRounded = function(min, max) {
  1078. return Distribution.getRandomOutcomeFloored(min + 0.5, max + 0.5);
  1079. }
  1080.  
  1081. // Cumulative Distribution Function
  1082. // https://stackoverflow.com/a/59217784
  1083. Distribution.cdf = function(value, mean, std) {
  1084. const z = (value - mean) / std;
  1085. const t = 1 / (1 + .2315419 * Math.abs(z));
  1086. const d =.3989423 * Math.exp( -z * z / 2);
  1087. let prob = d * t * (.3193815 + t * ( -.3565638 + t * (1.781478 + t * (-1.821256 + t * 1.330274))));
  1088. if(z > 0 ) {
  1089. prob = 1 - prob;
  1090. }
  1091. return prob
  1092. };
  1093.  
  1094. Distribution.pdf = function(zScore) {
  1095. return (Math.E ** (-zScore*zScore/2)) / Math.sqrt(2 * Math.PI);
  1096. };
  1097.  
  1098. return Distribution;
  1099.  
  1100. }
  1101. );
  1102. // elementCreator
  1103. window.moduleRegistry.add('elementCreator', () => {
  1104.  
  1105. const exports = {
  1106. addStyles
  1107. };
  1108.  
  1109. function addStyles(css) {
  1110. const head = document.getElementsByTagName('head')[0]
  1111. if(!head) {
  1112. console.error('Could not add styles, missing head');
  1113. return;
  1114. }
  1115. const style = document.createElement('style');
  1116. style.type = 'text/css';
  1117. style.innerHTML = css;
  1118. head.appendChild(style);
  1119. }
  1120.  
  1121. return exports;
  1122.  
  1123. }
  1124. );
  1125. // elementWatcher
  1126. window.moduleRegistry.add('elementWatcher', (Promise) => {
  1127.  
  1128. const exports = {
  1129. exists,
  1130. childAdded,
  1131. childAddedContinuous,
  1132. idle
  1133. }
  1134.  
  1135. const $ = window.$;
  1136.  
  1137. async function exists(selector, delay, timeout, inverted) {
  1138. delay = delay !== undefined ? delay : 10;
  1139. timeout = timeout !== undefined ? timeout : 5000;
  1140. const promiseWrapper = new Promise.Checking(() => {
  1141. let result = $(selector)[0];
  1142. return inverted ? !result : result;
  1143. }, delay, timeout, `elementWatcher - exists - ${selector}`);
  1144. return promiseWrapper;
  1145. }
  1146.  
  1147. async function childAdded(selector) {
  1148. const promiseWrapper = new Promise.Expiring(5000, `elementWatcher - childAdded - ${selector}`);
  1149.  
  1150. try {
  1151. const parent = await exists(selector);
  1152. const observer = new MutationObserver(function(mutations, observer) {
  1153. for(const mutation of mutations) {
  1154. if(mutation.addedNodes?.length) {
  1155. observer.disconnect();
  1156. promiseWrapper.resolve();
  1157. }
  1158. }
  1159. });
  1160. observer.observe(parent, { childList: true });
  1161. } catch(error) {
  1162. promiseWrapper.reject(error);
  1163. }
  1164.  
  1165. return promiseWrapper;
  1166. }
  1167.  
  1168. async function childAddedContinuous(selector, callback) {
  1169. const parent = await exists(selector);
  1170. const observer = new MutationObserver(function(mutations, observer) {
  1171. if(mutations.find(a => a.addedNodes?.length)) {
  1172. callback();
  1173. }
  1174. });
  1175. observer.observe(parent, { childList: true });
  1176. }
  1177.  
  1178. async function idle() {
  1179. const promise = new Promise.Expiring(1000, 'elementWatcher - idle');
  1180. window.requestIdleCallback(() => {
  1181. promise.resolve();
  1182. });
  1183. return promise;
  1184. }
  1185.  
  1186. return exports;
  1187.  
  1188. }
  1189. );
  1190. // EstimationGenerator
  1191. window.moduleRegistry.add('EstimationGenerator', (events, estimator, statsStore, util, skillCache, itemCache, structuresCache) => {
  1192.  
  1193. const EVENTS = {
  1194. exp: {
  1195. event: 'state-exp',
  1196. default: skillCache.list.reduce((a,b) => (a[b.id] = {id:b.id,exp:0,level:1}, a), {})
  1197. },
  1198. tomes: {
  1199. event: 'state-equipment-tomes',
  1200. default: {}
  1201. },
  1202. equipment: {
  1203. event: 'state-equipment-equipment',
  1204. default: {}
  1205. },
  1206. runes: {
  1207. event: 'state-equipment-runes',
  1208. default: {}
  1209. },
  1210. structures: {
  1211. event: 'state-structures',
  1212. default: {}
  1213. },
  1214. enhancements: {
  1215. event: 'state-enhancements',
  1216. default: {}
  1217. },
  1218. guild: {
  1219. event: 'state-structures-guild',
  1220. default: {}
  1221. }
  1222. };
  1223.  
  1224. class EstimationGenerator {
  1225.  
  1226. #backup;
  1227. #state;
  1228.  
  1229. constructor() {
  1230. this.#backup = {};
  1231. this.#state = this.#backup;
  1232. for(const name in EVENTS) {
  1233. this.#backup[name] = events.getLast(EVENTS[name].event);
  1234. }
  1235. }
  1236.  
  1237. reset() {
  1238. for(const name in EVENTS) {
  1239. this.#state[name] = structuredClone(EVENTS[name].default);
  1240. }
  1241. return this;
  1242. }
  1243.  
  1244. run(skillId, actionId) {
  1245. this.#sendCustomEvents();
  1246. statsStore.update(new Set());
  1247. const estimation = estimator.get(skillId, actionId);
  1248. this.#sendBackupEvents();
  1249. return estimation;
  1250. }
  1251.  
  1252. #sendCustomEvents() {
  1253. for(const name in this.#state) {
  1254. events.emit(EVENTS[name].event, this.#state[name]);
  1255. }
  1256. }
  1257.  
  1258. #sendBackupEvents() {
  1259. for(const name in this.#backup) {
  1260. events.emit(EVENTS[name].event, this.#backup[name]);
  1261. }
  1262. }
  1263.  
  1264. level(skill, level, exp = 0) {
  1265. if(typeof skill === 'string') {
  1266. const match = skillCache.byName[skill];
  1267. if(!match) {
  1268. throw `Could not find skill ${skill}`;
  1269. }
  1270. skill = match.id;
  1271. }
  1272. if(!exp) {
  1273. exp = util.levelToExp(level);
  1274. }
  1275. this.#state.exp[skill] = {
  1276. id: skill,
  1277. exp,
  1278. level
  1279. };
  1280. return this;
  1281. }
  1282.  
  1283. equipment(item, amount = 1) {
  1284. if(typeof item === 'string') {
  1285. const match = itemCache.byName[item];
  1286. if(!match) {
  1287. throw `Could not find item ${item}`;
  1288. }
  1289. item = match.id;
  1290. }
  1291. this.#state.equipment[item] = amount;
  1292. return this;
  1293. }
  1294.  
  1295. rune(item, amount = 1) {
  1296. if(typeof item === 'string') {
  1297. const match = itemCache.byName[item];
  1298. if(!match) {
  1299. throw `Could not find item ${item}`;
  1300. }
  1301. item = match.id;
  1302. }
  1303. this.#state.runes[item] = amount;
  1304. return this;
  1305. }
  1306.  
  1307. tome(item) {
  1308. if(typeof item === 'string') {
  1309. const match = itemCache.byName[item];
  1310. if(!match) {
  1311. throw `Could not find item ${item}`;
  1312. }
  1313. item = match.id;
  1314. }
  1315. this.#state.tomes[item] = 1;
  1316. return this;
  1317. }
  1318.  
  1319. structure(structure, level) {
  1320. if(typeof structure === 'string') {
  1321. const match = structuresCache.byName[structure];
  1322. if(!match) {
  1323. throw `Could not find structure ${structure}`;
  1324. }
  1325. structure = match.id;
  1326. }
  1327. this.#state.structures[structure] = level;
  1328. return this;
  1329. }
  1330.  
  1331. enhancement(structure, level) {
  1332. if(typeof structure === 'string') {
  1333. const match = structuresCache.byName[structure];
  1334. if(!match) {
  1335. throw `Could not find structure ${structure}`;
  1336. }
  1337. structure = match.id;
  1338. }
  1339. this.#state.enhancements[structure] = level;
  1340. return this;
  1341. }
  1342.  
  1343. guild(structure, level) {
  1344. if(typeof structure === 'string') {
  1345. const match = structuresCache.byName[structure];
  1346. if(!match) {
  1347. throw `Could not find structure ${structure}`;
  1348. }
  1349. structure = match.id;
  1350. }
  1351. this.#state.guild[structure] = level;
  1352. return this;
  1353. }
  1354.  
  1355. export() {
  1356. return structuredClone(this.#state);
  1357. }
  1358.  
  1359. import(state) {
  1360. this.#state = structuredClone(state);
  1361. return this;
  1362. }
  1363.  
  1364. }
  1365.  
  1366. return EstimationGenerator;
  1367.  
  1368. }
  1369. );
  1370. // events
  1371. window.moduleRegistry.add('events', () => {
  1372.  
  1373. const exports = {
  1374. register,
  1375. emit,
  1376. getLast,
  1377. getLastCache
  1378. };
  1379.  
  1380. const handlers = {};
  1381. const lastCache = {};
  1382.  
  1383. function register(name, handler) {
  1384. if(!handlers[name]) {
  1385. handlers[name] = [];
  1386. }
  1387. handlers[name].push(handler);
  1388. if(lastCache[name]) {
  1389. handle(handler, lastCache[name]);
  1390. }
  1391. }
  1392.  
  1393. // options = { skipCache }
  1394. function emit(name, data, options) {
  1395. if(!options?.skipCache) {
  1396. lastCache[name] = data;
  1397. }
  1398. if(!handlers[name]) {
  1399. return;
  1400. }
  1401. for(const handler of handlers[name]) {
  1402. handle(handler, data);
  1403. }
  1404. }
  1405.  
  1406. function handle(handler, data) {
  1407. try {
  1408. handler(data);
  1409. } catch(e) {
  1410. console.error('Something went wrong', e);
  1411. }
  1412. }
  1413.  
  1414. function getLast(name) {
  1415. return lastCache[name];
  1416. }
  1417.  
  1418. function getLastCache() {
  1419. return lastCache;
  1420. }
  1421.  
  1422. return exports;
  1423.  
  1424. }
  1425. );
  1426. // interceptor
  1427. window.moduleRegistry.add('interceptor', (events) => {
  1428.  
  1429. function initialise() {
  1430. registerInterceptorUrlChange();
  1431. events.emit('url', window.location.href);
  1432. }
  1433.  
  1434. function registerInterceptorUrlChange() {
  1435. const pushState = history.pushState;
  1436. history.pushState = function() {
  1437. pushState.apply(history, arguments);
  1438. console.debug(`Detected page ${arguments[2]}`);
  1439. events.emit('url', arguments[2]);
  1440. };
  1441. const replaceState = history.replaceState;
  1442. history.replaceState = function() {
  1443. replaceState.apply(history, arguments);
  1444. console.debug(`Detected page ${arguments[2]}`);
  1445. events.emit('url', arguments[2]);
  1446. }
  1447. }
  1448.  
  1449. initialise();
  1450.  
  1451. }
  1452. );
  1453. // itemUtil
  1454. window.moduleRegistry.add('itemUtil', (util, itemCache) => {
  1455.  
  1456. const exports = {
  1457. extractItem
  1458. };
  1459.  
  1460. function extractItem(element, target, ignoreMissing) {
  1461. element = $(element);
  1462. const name = element.find('.name').text();
  1463. let item = itemCache.byName[name];
  1464. if(!item) {
  1465. const src = element.find('img').attr('src');
  1466. if(src) {
  1467. const image = src.split('/').at(-1);
  1468. item = itemCache.byImage[image];
  1469. }
  1470. }
  1471. if(!item) {
  1472. if(!ignoreMissing) {
  1473. console.warn(`Could not find item with name [${name}]`);
  1474. }
  1475. return false;
  1476. }
  1477. let amount = 1;
  1478. let amountElements = element.find('.amount, .value');
  1479. let uses = 0;
  1480. if(amountElements.length) {
  1481. var amountText = amountElements.text();
  1482. if(!amountText) {
  1483. return false;
  1484. }
  1485. if(amountText.includes(' / ')) {
  1486. amountText = amountText.split(' / ')[0];
  1487. }
  1488. amount = util.parseNumber(amountText);
  1489. if(amountText.includes('&')) {
  1490. const usesText = amountText.split('&')[1];
  1491. uses = util.parseNumber(usesText);
  1492. }
  1493. }
  1494. if(!uses) {
  1495. const usesText = element.find('.uses, .use').text();
  1496. if(usesText && !usesText.endsWith('HP')) {
  1497. uses = util.parseNumber(usesText);
  1498. }
  1499. }
  1500. amount += uses;
  1501. target[item.id] = (target[item.id] || 0) + amount;
  1502. return item;
  1503. }
  1504.  
  1505. return exports;
  1506.  
  1507. }
  1508. );
  1509. // localDatabase
  1510. window.moduleRegistry.add('localDatabase', (Promise) => {
  1511.  
  1512. const exports = {
  1513. getAllEntries,
  1514. saveEntry,
  1515. removeEntry
  1516. };
  1517.  
  1518. const initialised = new Promise.Expiring(2000, 'localDatabase');
  1519. let database = null;
  1520.  
  1521. const databaseName = 'PancakeScripts';
  1522.  
  1523. function initialise() {
  1524. const request = window.indexedDB.open(databaseName, 4);
  1525. request.onsuccess = function(event) {
  1526. database = this.result;
  1527. initialised.resolve(exports);
  1528. };
  1529. request.onerror = function(event) {
  1530. console.error(`Failed creating IndexedDB : ${event.target.errorCode}`);
  1531. };
  1532. request.onupgradeneeded = function(event) {
  1533. const db = event.target.result;
  1534. if(event.oldVersion <= 0) {
  1535. console.debug('Creating IndexedDB');
  1536. db
  1537. .createObjectStore('settings', { keyPath: 'key' })
  1538. .createIndex('key', 'key', { unique: true });
  1539. }
  1540. if(event.oldVersion <= 1) {
  1541. db
  1542. .createObjectStore('sync-tracking', { keyPath: 'key' })
  1543. .createIndex('key', 'key', { unique: true });
  1544. }
  1545. if(event.oldVersion <= 2) {
  1546. db
  1547. .createObjectStore('market-filters', { keyPath: 'key' })
  1548. .createIndex('key', 'key', { unique: true });
  1549. }
  1550. if(event.oldVersion <= 3) {
  1551. db
  1552. .createObjectStore('component-tabs', { keyPath: 'key' })
  1553. .createIndex('key', 'key', { unique: true });
  1554. }
  1555. };
  1556. }
  1557.  
  1558. async function getAllEntries(storeName) {
  1559. const result = new Promise.Expiring(1000, 'localDatabase - getAllEntries');
  1560. const entries = [];
  1561. const store = database.transaction(storeName, 'readonly').objectStore(storeName);
  1562. const request = store.openCursor();
  1563. request.onsuccess = function(event) {
  1564. const cursor = event.target.result;
  1565. if(cursor) {
  1566. entries.push(cursor.value);
  1567. cursor.continue();
  1568. } else {
  1569. result.resolve(entries);
  1570. }
  1571. };
  1572. request.onerror = function(event) {
  1573. result.reject(event.error);
  1574. };
  1575. return result;
  1576. }
  1577.  
  1578. async function saveEntry(storeName, entry) {
  1579. const result = new Promise.Expiring(1000, 'localDatabase - saveEntry');
  1580. const store = database.transaction(storeName, 'readwrite').objectStore(storeName);
  1581. const request = store.put(entry);
  1582. request.onsuccess = function(event) {
  1583. result.resolve();
  1584. };
  1585. request.onerror = function(event) {
  1586. result.reject(event.error);
  1587. };
  1588. return result;
  1589. }
  1590.  
  1591. async function removeEntry(storeName, key) {
  1592. const result = new Promise.Expiring(1000, 'localDatabase - removeEntry');
  1593. const store = database.transaction(storeName, 'readwrite').objectStore(storeName);
  1594. const request = store.delete(key);
  1595. request.onsuccess = function(event) {
  1596. result.resolve();
  1597. };
  1598. request.onerror = function(event) {
  1599. result.reject(event.error);
  1600. };
  1601. return result;
  1602. }
  1603.  
  1604. initialise();
  1605.  
  1606. return initialised;
  1607.  
  1608. }
  1609. );
  1610. // logService
  1611. window.moduleRegistry.add('logService', () => {
  1612.  
  1613. const exports = {
  1614. error,
  1615. get
  1616. };
  1617.  
  1618. const errors = [];
  1619.  
  1620. function error() {
  1621. errors.push({
  1622. time: Date.now(),
  1623. value: [...arguments]
  1624. });
  1625. }
  1626.  
  1627. function get() {
  1628. return errors;
  1629. }
  1630.  
  1631. return exports;
  1632.  
  1633. });
  1634. // pageDetector
  1635. window.moduleRegistry.add('pageDetector', (events, elementWatcher, util) => {
  1636.  
  1637. const registerUrlHandler = events.register.bind(null, 'url');
  1638. const emitEvent = events.emit.bind(null, 'page');
  1639.  
  1640. async function initialise() {
  1641. registerUrlHandler(util.debounce(handleUrl, 200));
  1642. }
  1643.  
  1644. async function handleUrl(url) {
  1645. let result = null;
  1646. const parts = url.split('/');
  1647. if(url.includes('/skill/') && url.includes('/action/')) {
  1648. result = {
  1649. type: 'action',
  1650. skill: +parts[parts.length-3],
  1651. action: +parts[parts.length-1]
  1652. };
  1653. } else if(url.includes('house/build')) {
  1654. result = {
  1655. type: 'structure',
  1656. structure: +parts[parts.length-1]
  1657. };
  1658. } else if(url.includes('house/enhance')) {
  1659. result = {
  1660. type: 'enhancement',
  1661. structure: +parts[parts.length-1]
  1662. };
  1663. } else if(url.includes('house/produce')) {
  1664. result = {
  1665. type: 'automation',
  1666. structure: +parts[parts.length-2],
  1667. action: +parts[parts.length-1]
  1668. };
  1669. } else {
  1670. result = {
  1671. type: parts.pop()
  1672. };
  1673. }
  1674. await elementWatcher.idle();
  1675. emitEvent(result);
  1676. }
  1677.  
  1678. initialise();
  1679.  
  1680. }
  1681. );
  1682. // pages
  1683. window.moduleRegistry.add('pages', (elementWatcher, events, colorMapper, util, skillCache, elementCreator) => {
  1684.  
  1685. const registerPageHandler = events.register.bind(null, 'page');
  1686. const getLastPage = events.getLast.bind(null, 'page');
  1687.  
  1688. const exports = {
  1689. register,
  1690. requestRender,
  1691. show,
  1692. hide,
  1693. open: visitPage
  1694. }
  1695.  
  1696. const pages = [];
  1697.  
  1698. function initialise() {
  1699. registerPageHandler(handlePage);
  1700. elementCreator.addStyles(styles);
  1701. }
  1702.  
  1703. function handlePage(page) {
  1704. // handle navigating away
  1705. if(!pages.some(p => p.path === page.type)) {
  1706. $('custom-page').remove();
  1707. $('nav-component > div.nav > div.scroll > button')
  1708. .removeClass('customActiveLink');
  1709. $('header-component div.wrapper > div.image > img')
  1710. .css('image-rendering', '');
  1711. headerPageNameChangeBugFix(page);
  1712. }
  1713. }
  1714.  
  1715. async function register(page) {
  1716. if(pages.some(p => p.name === page.name)) {
  1717. console.error(`Custom page already registered : ${page.name}`);
  1718. return;
  1719. }
  1720. page.path = page.name.toLowerCase().replaceAll(' ', '-');
  1721. page.class = `customMenuButton_${page.path}`;
  1722. page.image = page.image || 'https://ironwoodrpg.com/assets/misc/settings.png';
  1723. page.category = page.category?.toUpperCase() || 'MISC';
  1724. page.columns = page.columns || 1;
  1725. pages.push(page);
  1726. console.debug('Registered pages', pages);
  1727. await setupNavigation(page);
  1728. }
  1729.  
  1730. function show(name) {
  1731. const page = pages.find(p => p.name === name)
  1732. if(!page) {
  1733. console.error(`Could not find page : ${name}`);
  1734. return;
  1735. }
  1736. $(`.${page.class}`).show();
  1737. }
  1738.  
  1739. function hide(name) {
  1740. const page = pages.find(p => p.name === name)
  1741. if(!page) {
  1742. console.error(`Could not find page : ${name}`);
  1743. return;
  1744. }
  1745. $(`.${page.class}`).hide();
  1746. }
  1747.  
  1748. function requestRender(name) {
  1749. const page = pages.find(p => p.name === name)
  1750. if(!page) {
  1751. console.error(`Could not find page : ${name}`);
  1752. return;
  1753. }
  1754. if(getLastPage()?.type === page.path) {
  1755. render(page);
  1756. }
  1757. }
  1758.  
  1759. function render(page) {
  1760. $('.customComponent').remove();
  1761. page.render();
  1762. }
  1763.  
  1764. async function setupNavigation(page) {
  1765. await elementWatcher.exists('div.nav > div.scroll');
  1766. // MENU HEADER / CATEGORY
  1767. let menuHeader = $(`nav-component > div.nav > div.scroll > div.header:contains('${page.category}'), div.customMenuHeader:contains('${page.category}')`);
  1768. if(!menuHeader.length) {
  1769. menuHeader = createMenuHeader(page.category);
  1770. }
  1771. // MENU BUTTON / PAGE LINK
  1772. const menuButton = createMenuButton(page)
  1773. // POSITIONING
  1774. if(page.after) {
  1775. $(`nav-component button:contains('${page.after}')`).after(menuButton);
  1776. } else {
  1777. menuHeader.after(menuButton);
  1778. }
  1779. }
  1780.  
  1781. function createMenuHeader(text) {
  1782. const menuHeader =
  1783. $('<div/>')
  1784. .addClass('header customMenuHeader')
  1785. .append(
  1786. $('<div/>')
  1787. .addClass('customMenuHeaderText')
  1788. .text(text)
  1789. );
  1790. $('nav-component > div.nav > div.scroll')
  1791. .prepend(menuHeader);
  1792. return menuHeader;
  1793. }
  1794.  
  1795. function createMenuButton(page) {
  1796. const menuButton =
  1797. $('<button/>')
  1798. .attr('type', 'button')
  1799. .addClass(`customMenuButton ${page.class}`)
  1800. .css('display', 'none')
  1801. .click(() => visitPage(page.name))
  1802. .append(
  1803. $('<img/>')
  1804. .addClass('customMenuButtonImage')
  1805. .attr('src', page.image)
  1806. .css('image-rendering', page.imagePixelated ? 'pixelated' : 'auto')
  1807. )
  1808. .append(
  1809. $('<div/>')
  1810. .addClass('customMenuButtonText')
  1811. .text(page.name)
  1812. );
  1813. return menuButton;
  1814. }
  1815.  
  1816. async function visitPage(name) {
  1817. const page = pages.find(p => p.name === name);
  1818. if($('custom-page').length) {
  1819. $('custom-page').remove();
  1820. } else {
  1821. await setupEmptyPage();
  1822. }
  1823. createPage(page.columns);
  1824. updatePageHeader(page);
  1825. updateActivePageInNav(page.name);
  1826. history.pushState({}, '', page.path);
  1827. page.render();
  1828. }
  1829.  
  1830. async function setupEmptyPage() {
  1831. util.goToPage('settings');
  1832. await elementWatcher.exists('settings-page');
  1833. $('settings-page').remove();
  1834. }
  1835.  
  1836. function createPage(columnCount) {
  1837. const custompage = $('<custom-page/>');
  1838. const columns = $('<div/>')
  1839. .addClass('customGroups');
  1840. for(let columnIndex = 0; columnIndex < columnCount; columnIndex++) {
  1841. columns.append(
  1842. $('<div/>')
  1843. .addClass('customGroup')
  1844. .addClass(`column${columnIndex}`)
  1845. )
  1846. };
  1847. custompage.append(columns);
  1848. $('div.padding > div.wrapper > router-outlet').after(custompage);
  1849. }
  1850.  
  1851. function updatePageHeader(page) {
  1852. $('header-component div.wrapper > div.image > img')
  1853. .attr('src', page.image)
  1854. .css('image-rendering', page.imagePixelated ? 'pixelated' : 'auto');
  1855. $('header-component div.wrapper > div.title').text(page.name);
  1856. }
  1857.  
  1858. function updateActivePageInNav(name) {
  1859. //Set other pages as inactive
  1860. $(`nav-component > div.nav > div.scroll > button`)
  1861. .removeClass('active-link')
  1862. .removeClass('customActiveLink');
  1863. //Set this page as active
  1864. $(`nav-component > div.nav > div.scroll > button > div.customMenuButtonText:contains('${name}')`)
  1865. .parent()
  1866. .addClass('customActiveLink');
  1867. }
  1868.  
  1869. // hacky shit, idk why angular stops updating page header title ???
  1870. async function headerPageNameChangeBugFix(page) {
  1871. await elementWatcher.exists('nav-component > div.nav');
  1872. let headerName = null;
  1873. if(page.type === 'action') {
  1874. headerName = skillCache.byId[page.skill].displayName;
  1875. } else if(page.type === 'structure') {
  1876. headerName = 'House';
  1877. } else if(page.type === 'enhancement') {
  1878. headerName = 'House';
  1879. } else if(page.type === 'automation') {
  1880. headerName = 'House';
  1881. } else {
  1882. headerName = page.type;
  1883. headerName = headerName.charAt(0).toUpperCase() + headerName.slice(1);
  1884. }
  1885. $('header-component div.wrapper > div.title').text(headerName);
  1886. }
  1887.  
  1888. const styles = `
  1889. :root {
  1890. --background-color: ${colorMapper('componentRegular')};
  1891. --border-color: ${colorMapper('componentLight')};
  1892. --darker-color: ${colorMapper('componentDark')};
  1893. }
  1894. .customMenuHeader {
  1895. height: 56px;
  1896. display: flex;
  1897. align-items: center;
  1898. padding: 0 24px;
  1899. color: #aaa;
  1900. font-size: .875rem;
  1901. font-weight: 600;
  1902. letter-spacing: 1px;
  1903. text-transform: uppercase;
  1904. border-bottom: 1px solid var(--border-color);
  1905. background-color: var(--background-color);
  1906. }
  1907. .customMenuHeaderText {
  1908. flex: 1;
  1909. }
  1910. .customMenuButton {
  1911. border: none;
  1912. background: transparent;
  1913. font-family: inherit;
  1914. font-size: inherit;
  1915. line-height: 1.5;
  1916. font-weight: inherit;
  1917. color: inherit;
  1918. resize: none;
  1919. text-transform: inherit;
  1920. letter-spacing: inherit;
  1921. cursor: pointer;
  1922. height: 56px;
  1923. display: flex;
  1924. align-items: center;
  1925. padding: 0 24px;
  1926. border-bottom: 1px solid var(--border-color);
  1927. width: 100%;
  1928. text-align: left;
  1929. position: relative;
  1930. background-color: var(--background-color);
  1931. }
  1932. .customMenuButtonImage {
  1933. max-width: 100%;
  1934. max-height: 100%;
  1935. height: 32px;
  1936. width: 32px;
  1937. }
  1938. .customMenuButtonText {
  1939. margin-left: var(--margin);
  1940. flex: 1;
  1941. }
  1942. .customGroups {
  1943. display: flex;
  1944. gap: var(--gap);
  1945. flex-wrap: wrap;
  1946. }
  1947. .customGroup {
  1948. flex: 1;
  1949. min-width: 360px;
  1950. }
  1951. .customActiveLink {
  1952. background-color: var(--darker-color);
  1953. }
  1954. `;
  1955.  
  1956. initialise();
  1957.  
  1958. return exports
  1959. }
  1960. );
  1961. // Promise
  1962. window.moduleRegistry.add('Promise', (logService) => {
  1963.  
  1964. class Deferred {
  1965. #name;
  1966. #promise;
  1967. resolve;
  1968. reject;
  1969. constructor(name) {
  1970. this.#name = name;
  1971. this.#promise = new Promise((resolve, reject) => {
  1972. this.resolve = resolve;
  1973. this.reject = reject;
  1974. }).catch(error => {
  1975. if(error) {
  1976. console.warn(error);
  1977. logService.error(`error in ${this.constructor.name} (${this.#name})`, error);
  1978. }
  1979. throw error;
  1980. });
  1981. }
  1982.  
  1983. then() {
  1984. this.#promise.then.apply(this.#promise, arguments);
  1985. return this;
  1986. }
  1987.  
  1988. catch() {
  1989. this.#promise.catch.apply(this.#promise, arguments);
  1990. return this;
  1991. }
  1992.  
  1993. finally() {
  1994. this.#promise.finally.apply(this.#promise, arguments);
  1995. return this;
  1996. }
  1997. }
  1998.  
  1999. class Delayed extends Deferred {
  2000. constructor(timeout, name) {
  2001. super(name);
  2002. const timeoutReference = window.setTimeout(() => {
  2003. this.resolve();
  2004. }, timeout);
  2005. this.finally(() => {
  2006. window.clearTimeout(timeoutReference)
  2007. });
  2008. }
  2009. }
  2010.  
  2011. class Expiring extends Deferred {
  2012. constructor(timeout, name) {
  2013. super(name);
  2014. if(timeout <= 0) {
  2015. return;
  2016. }
  2017. const timeoutReference = window.setTimeout(() => {
  2018. this.reject(`Timed out after ${timeout} ms`);
  2019. }, timeout);
  2020. this.finally(() => {
  2021. window.clearTimeout(timeoutReference)
  2022. });
  2023. }
  2024. }
  2025.  
  2026. class Checking extends Expiring {
  2027. #checker;
  2028. constructor(checker, interval, timeout, name) {
  2029. super(timeout, name);
  2030. this.#checker = checker;
  2031. this.#check();
  2032. const intervalReference = window.setInterval(this.#check.bind(this), interval);
  2033. this.finally(() => {
  2034. window.clearInterval(intervalReference)
  2035. });
  2036. }
  2037. #check() {
  2038. const checkResult = this.#checker();
  2039. if(!checkResult) {
  2040. return;
  2041. }
  2042. this.resolve(checkResult);
  2043. }
  2044. }
  2045.  
  2046. return {
  2047. Deferred,
  2048. Delayed,
  2049. Expiring,
  2050. Checking
  2051. };
  2052.  
  2053. }
  2054. );
  2055. // request
  2056. window.moduleRegistry.add('request', () => {
  2057.  
  2058. async function request(url, body, headers) {
  2059. if(!headers) {
  2060. headers = {};
  2061. }
  2062. headers['Content-Type'] = 'application/json';
  2063. const method = body ? 'POST' : 'GET';
  2064. try {
  2065. if(body) {
  2066. body = JSON.stringify(body);
  2067. }
  2068. const fetchResponse = await fetch(`${window.PANCAKE_ROOT}/${url}`, {method, headers, body});
  2069. if(fetchResponse.status !== 200) {
  2070. console.error(await fetchResponse.text());
  2071. console.log('response', fetchResponse);
  2072. throw fetchResponse;
  2073. }
  2074. try {
  2075. const contentType = fetchResponse.headers.get('Content-Type');
  2076. if(contentType.startsWith('text/plain')) {
  2077. return await fetchResponse.text();
  2078. } else if(contentType.startsWith('application/json')) {
  2079. return await fetchResponse.json();
  2080. } else {
  2081. console.error(`Unknown content type : ${contentType}`);
  2082. }
  2083. } catch(e) {
  2084. if(body) {
  2085. return 'OK';
  2086. }
  2087. }
  2088. } catch(e) {
  2089. console.log('error', e);
  2090. throw `Failed fetching ${url} : ${e}`;
  2091. }
  2092. }
  2093.  
  2094. // alphabetical
  2095.  
  2096. request.listActions = () => request('public/list/action');
  2097. request.listDrops = () => request('public/list/drop');
  2098. request.listItems = () => request('public/list/item');
  2099. request.listItemAttributes = () => request('public/list/itemAttribute');
  2100. request.listIngredients = () => request('public/list/ingredient');
  2101. request.listMonsters = () => request('public/list/monster');
  2102. request.listRecipes = () => request('public/list/recipe');
  2103. request.listSkills = () => request('public/list/skill');
  2104. request.listStructures = () => request('public/list/structure');
  2105.  
  2106. request.report = (data) => request('public/report', data);
  2107.  
  2108. request.getChangelogs = () => request('public/settings/changelog');
  2109. request.getVersion = () => request('public/settings/version');
  2110.  
  2111. return request;
  2112.  
  2113. }
  2114. );
  2115. // toast
  2116. window.moduleRegistry.add('toast', (util, elementCreator) => {
  2117.  
  2118. const exports = {
  2119. create
  2120. };
  2121.  
  2122. function initialise() {
  2123. elementCreator.addStyles(styles);
  2124. }
  2125.  
  2126. // text, time, image
  2127. async function create(config) {
  2128. config.time ||= 2000;
  2129. config.image ||= 'https://ironwoodrpg.com/assets/misc/quests.png';
  2130. const notificationId = `customNotification_${Math.floor(Date.now() * Math.random())}`
  2131. const notificationDiv =
  2132. $('<div/>')
  2133. .addClass('customNotification')
  2134. .attr('id', notificationId)
  2135. .append(
  2136. $('<div/>')
  2137. .addClass('customNotificationImageDiv')
  2138. .append(
  2139. $('<img/>')
  2140. .addClass('customNotificationImage')
  2141. .attr('src', config.image)
  2142. )
  2143. )
  2144. .append(
  2145. $('<div/>')
  2146. .addClass('customNotificationDetails')
  2147. .html(config.text)
  2148. );
  2149. $('div.notifications').append(notificationDiv);
  2150. await util.sleep(config.time);
  2151. $(`#${notificationId}`).fadeOut('slow', () => {
  2152. $(`#${notificationId}`).remove();
  2153. });
  2154. }
  2155.  
  2156. const styles = `
  2157. .customNotification {
  2158. padding: 8px 16px 8px 12px;
  2159. border-radius: 4px;
  2160. backdrop-filter: blur(8px);
  2161. background: rgba(255,255,255,.15);
  2162. box-shadow: 0 8px 16px -4px #00000080;
  2163. display: flex;
  2164. align-items: center;
  2165. min-height: 48px;
  2166. margin-top: 12px;
  2167. pointer-events: all;
  2168. }
  2169. .customNotificationImageDiv {
  2170. display: flex;
  2171. align-items: center;
  2172. justify-content: center;
  2173. width: 32px;
  2174. height: 32px;
  2175. }
  2176. .customNotificationImage {
  2177. filter: drop-shadow(0px 8px 4px rgba(0,0,0,.1));
  2178. image-rendering: auto;
  2179. }
  2180. .customNotificationDetails {
  2181. margin-left: 8px;
  2182. text-align: center;
  2183. }
  2184. `;
  2185.  
  2186. initialise();
  2187.  
  2188. return exports;
  2189. }
  2190. );
  2191. // util
  2192. window.moduleRegistry.add('util', () => {
  2193.  
  2194. const exports = {
  2195. levelToExp,
  2196. expToLevel,
  2197. expToCurrentExp,
  2198. expToNextLevel,
  2199. expToNextTier,
  2200. tierToLevel,
  2201. formatNumber,
  2202. parseNumber,
  2203. secondsToDuration,
  2204. parseDuration,
  2205. divmod,
  2206. sleep,
  2207. goToPage,
  2208. compareObjects,
  2209. debounce
  2210. };
  2211.  
  2212. function levelToExp(level) {
  2213. if(level === 1) {
  2214. return 0;
  2215. }
  2216. if(level <= 100) {
  2217. return Math.floor(Math.pow(level, 3.5) * 6 / 5);
  2218. }
  2219. return Math.round(12_000_000 * Math.pow(Math.pow(3500, .01), level - 100));
  2220. }
  2221.  
  2222. function expToLevel(exp) {
  2223. if(exp <= 0) {
  2224. return 1;
  2225. }
  2226. if(exp <= 12_000_000) {
  2227. return Math.floor(Math.pow((exp + 1) / 1.2, 1 / 3.5));
  2228. }
  2229. return 100 + Math.floor(Math.log((exp + 1) / 12_000_000) / Math.log(Math.pow(3500, .01)));
  2230. }
  2231.  
  2232. function expToCurrentExp(exp) {
  2233. const level = expToLevel(exp);
  2234. return exp - levelToExp(level);
  2235. }
  2236.  
  2237. function expToNextLevel(exp) {
  2238. const level = expToLevel(exp);
  2239. return levelToExp(level + 1) - exp;
  2240. }
  2241.  
  2242. function expToNextTier(exp) {
  2243. const level = expToLevel(exp);
  2244. let target = 10;
  2245. while(target <= level) {
  2246. target += 15;
  2247. }
  2248. return levelToExp(target) - exp;
  2249. }
  2250.  
  2251. function tierToLevel(tier) {
  2252. if(tier <= 1) {
  2253. return tier;
  2254. }
  2255. return tier * 15 - 20;
  2256. }
  2257.  
  2258. function formatNumber(number) {
  2259. let digits = 2;
  2260. if(number < .1 && number > -.1) {
  2261. digits = 3;
  2262. }
  2263. if(number < .01 && number > -.01) {
  2264. digits = 4;
  2265. }
  2266. return number.toLocaleString(undefined, {maximumFractionDigits:digits});
  2267. }
  2268.  
  2269. function parseNumber(text) {
  2270. if(!text) {
  2271. return 0;
  2272. }
  2273. if(text.includes('Empty')) {
  2274. return 0;
  2275. }
  2276. const regexMatch = /\d+.*/.exec(text);
  2277. if(!regexMatch) {
  2278. return 0;
  2279. }
  2280. text = regexMatch[0];
  2281. text = text.replaceAll(/,/g, '');
  2282. text = text.replaceAll(/&.*$/g, '');
  2283. let multiplier = 1;
  2284. if(text.endsWith('%')) {
  2285. multiplier = 1 / 100;
  2286. }
  2287. if(text.endsWith('K')) {
  2288. multiplier = 1_000;
  2289. }
  2290. if(text.endsWith('M')) {
  2291. multiplier = 1_000_000;
  2292. }
  2293. return (parseFloat(text) || 0) * multiplier;
  2294. }
  2295.  
  2296. function secondsToDuration(seconds) {
  2297. seconds = Math.floor(seconds);
  2298. if(seconds > 60 * 60 * 24 * 100) {
  2299. // > 100 days
  2300. return 'A very long time';
  2301. }
  2302.  
  2303. var [minutes, seconds] = divmod(seconds, 60);
  2304. var [hours, minutes] = divmod(minutes, 60);
  2305. var [days, hours] = divmod(hours, 24);
  2306.  
  2307. seconds = `${seconds}`.padStart(2, '0');
  2308. minutes = `${minutes}`.padStart(2, '0');
  2309. hours = `${hours}`.padStart(2, '0');
  2310. days = `${days}`.padStart(2, '0');
  2311.  
  2312. let result = '';
  2313. if(result || +days) {
  2314. result += `${days}d `;
  2315. }
  2316. if(result || +hours) {
  2317. result += `${hours}h `;
  2318. }
  2319. if(result || +minutes) {
  2320. result += `${minutes}m `;
  2321. }
  2322. result += `${seconds}s`;
  2323.  
  2324. return result;
  2325. }
  2326.  
  2327. function parseDuration(duration) {
  2328. const parts = duration.split(' ');
  2329. let seconds = 0;
  2330. for(const part of parts) {
  2331. const value = parseFloat(part);
  2332. if(part.endsWith('m')) {
  2333. seconds += value * 60;
  2334. } else if(part.endsWith('h')) {
  2335. seconds += value * 60 * 60;
  2336. } else if(part.endsWith('d')) {
  2337. seconds += value * 60 * 60 * 24;
  2338. } else {
  2339. console.warn(`Unexpected duration being parsed : ${part}`);
  2340. }
  2341. }
  2342. return seconds;
  2343. }
  2344.  
  2345. function divmod(x, y) {
  2346. return [Math.floor(x / y), x % y];
  2347. }
  2348.  
  2349. function goToPage(page) {
  2350. window.history.pushState({}, '', page);
  2351. window.history.pushState({}, '', page);
  2352. window.history.back();
  2353. }
  2354.  
  2355. async function sleep(millis) {
  2356. await new Promise(r => window.setTimeout(r, millis));
  2357. }
  2358.  
  2359. function compareObjects(object1, object2, doLog) {
  2360. const keys1 = Object.keys(object1);
  2361. const keys2 = Object.keys(object2);
  2362. if(keys1.length !== keys2.length) {
  2363. if(doLog) {
  2364. console.warn(`key length not matching`, object1, object2);
  2365. }
  2366. return false;
  2367. }
  2368. keys1.sort();
  2369. keys2.sort();
  2370. for(let i=0;i<keys1.length;i++) {
  2371. if(keys1[i] !== keys2[i]) {
  2372. if(doLog) {
  2373. console.warn(`keys not matching`, keys1[i], keys2[i], object1, object2);
  2374. }
  2375. return false;
  2376. }
  2377. if(typeof object1[keys1[i]] === 'object' && typeof object2[keys2[i]] === 'object') {
  2378. if(!compareObjects(object1[keys1[i]], object2[keys2[i]], doLog)) {
  2379. return false;
  2380. }
  2381. } else if(object1[keys1[i]] !== object2[keys2[i]]) {
  2382. if(doLog) {
  2383. console.warn(`values not matching`, object1[keys1[i]], object2[keys2[i]], object1, object2);
  2384. }
  2385. return false;
  2386. }
  2387. }
  2388. return true;
  2389. }
  2390.  
  2391. function debounce(callback, delay) {
  2392. let timer;
  2393. return function(...args) {
  2394. clearTimeout(timer);
  2395. timer = setTimeout(() => {
  2396. callback(...args);
  2397. }, delay);
  2398. }
  2399. }
  2400.  
  2401. return exports;
  2402.  
  2403. }
  2404. );
  2405. // enhancementsReader
  2406. window.moduleRegistry.add('enhancementsReader', (events, util, structuresCache) => {
  2407.  
  2408. const emitEvent = events.emit.bind(null, 'reader-enhancements');
  2409.  
  2410. function initialise() {
  2411. events.register('page', update);
  2412. window.setInterval(update, 1000);
  2413. }
  2414.  
  2415. function update() {
  2416. const page = events.getLast('page');
  2417. if(!page) {
  2418. return;
  2419. }
  2420. if(page.type === 'enhancement' && $('home-page .categories .category-active').text() === 'Enhance') {
  2421. readEnhancementsScreen();
  2422. }
  2423. }
  2424.  
  2425. function readEnhancementsScreen() {
  2426. const enhancements = {};
  2427. $('home-page .categories + .card button').each((i,element) => {
  2428. element = $(element);
  2429. const name = element.find('.name').text();
  2430. const structure = structuresCache.byName[name];
  2431. if(!structure) {
  2432. return;
  2433. }
  2434. const level = util.parseNumber(element.find('.level').text());
  2435. enhancements[structure.id] = level;
  2436. });
  2437. emitEvent({
  2438. type: 'full',
  2439. value: enhancements
  2440. });
  2441. }
  2442.  
  2443. initialise();
  2444.  
  2445. }
  2446. );
  2447. // equipmentReader
  2448. window.moduleRegistry.add('equipmentReader', (events, itemCache, util, itemUtil) => {
  2449.  
  2450. function initialise() {
  2451. events.register('page', update);
  2452. window.setInterval(update, 1000);
  2453. }
  2454.  
  2455. function update() {
  2456. const page = events.getLast('page');
  2457. if(!page) {
  2458. return;
  2459. }
  2460. if(page.type === 'equipment') {
  2461. readEquipmentScreen();
  2462. }
  2463. if(page.type === 'action') {
  2464. readActionScreen();
  2465. }
  2466. }
  2467.  
  2468. function readEquipmentScreen() {
  2469. const equipment = {};
  2470. const activeTab = $('equipment-page .categories button[disabled]').text().toLowerCase();
  2471. $('equipment-page .header + .items > .item > .description').parent().each((i,element) => {
  2472. itemUtil.extractItem(element, equipment);
  2473. });
  2474. events.emit(`reader-equipment-${activeTab}`, {
  2475. type: 'full',
  2476. value: equipment
  2477. });
  2478. }
  2479.  
  2480. function readActionScreen() {
  2481. const equipment = {};
  2482. $('skill-page .header > .name:contains("Consumables")').closest('.card').find('button > .name:not(.placeholder)').parent().each((i,element) => {
  2483. itemUtil.extractItem(element, equipment);
  2484. });
  2485. events.emit('reader-equipment-equipment', {
  2486. type: 'partial',
  2487. value: equipment
  2488. });
  2489. }
  2490.  
  2491. initialise();
  2492.  
  2493. }
  2494. );
  2495. // expReader
  2496. window.moduleRegistry.add('expReader', (events, skillCache, util) => {
  2497.  
  2498. const emitEvent = events.emit.bind(null, 'reader-exp');
  2499.  
  2500. function initialise() {
  2501. events.register('page', update);
  2502. window.setInterval(update, 1000);
  2503. }
  2504.  
  2505. function update() {
  2506. const page = events.getLast('page');
  2507. if(!page) {
  2508. return;
  2509. }
  2510. if(page.type === 'action') {
  2511. readActionScreen(page.skill);
  2512. }
  2513. readSidebar();
  2514. }
  2515.  
  2516. function readActionScreen(id) {
  2517. const text = $('skill-page .header > .name:contains("Stats")')
  2518. .closest('.card')
  2519. .find('.row > .name:contains("Total"):contains("XP")')
  2520. .closest('.row')
  2521. .find('.value')
  2522. .text();
  2523. const exp = util.parseNumber(text);
  2524. emitEvent([{ id, exp }]);
  2525. }
  2526.  
  2527. function readSidebar() {
  2528. const levels = [];
  2529. $('nav-component button.skill').each((i,element) => {
  2530. element = $(element);
  2531. const name = element.find('.name').text();
  2532. const id = skillCache.byName[name].id;
  2533. const level = +(/\d+/.exec(element.find('.level').text())?.[0]);
  2534. const exp = util.levelToExp(level);
  2535. levels.push({ id, exp });
  2536. });
  2537. emitEvent(levels);
  2538. }
  2539.  
  2540. initialise();
  2541.  
  2542. }
  2543. );
  2544. // guildStructuresReader
  2545. window.moduleRegistry.add('guildStructuresReader', (events, util, structuresCache) => {
  2546.  
  2547. const emitEvent = events.emit.bind(null, 'reader-structures-guild');
  2548.  
  2549. function initialise() {
  2550. events.register('page', update);
  2551. window.setInterval(update, 1000);
  2552. }
  2553.  
  2554. function update() {
  2555. const page = events.getLast('page');
  2556. if(!page) {
  2557. return;
  2558. }
  2559. if(page.type === 'guild' && $('guild-page .tracker + div button.row-active').text() === 'Buildings') {
  2560. readGuildStructuresScreen();
  2561. }
  2562. }
  2563.  
  2564. function readGuildStructuresScreen() {
  2565. const structures = {};
  2566. $('guild-page .card').first().find('button').each((i,element) => {
  2567. element = $(element);
  2568. const name = element.find('.name').text();
  2569. const structure = structuresCache.byName[name];
  2570. if(!structure) {
  2571. return;
  2572. }
  2573. const level = util.parseNumber(element.find('.amount').text());
  2574. structures[structure.id] = level;
  2575. });
  2576. emitEvent({
  2577. type: 'full',
  2578. value: structures
  2579. });
  2580. }
  2581.  
  2582. initialise();
  2583.  
  2584. }
  2585. );
  2586. // inventoryReader
  2587. window.moduleRegistry.add('inventoryReader', (events, itemCache, util, itemUtil) => {
  2588.  
  2589. const emitEvent = events.emit.bind(null, 'reader-inventory');
  2590.  
  2591. function initialise() {
  2592. events.register('page', update);
  2593. window.setInterval(update, 1000);
  2594. }
  2595.  
  2596. function update() {
  2597. const page = events.getLast('page');
  2598. if(!page) {
  2599. return;
  2600. }
  2601. if(page.type === 'inventory') {
  2602. readInventoryScreen();
  2603. }
  2604. if(page.type === 'action') {
  2605. readActionScreen();
  2606. }
  2607. }
  2608.  
  2609. function readInventoryScreen() {
  2610. const inventory = {};
  2611. $('inventory-page .items > .item').each((i,element) => {
  2612. itemUtil.extractItem(element, inventory, true);
  2613. });
  2614. emitEvent({
  2615. type: 'full',
  2616. value: inventory
  2617. });
  2618. }
  2619.  
  2620. function readActionScreen() {
  2621. const inventory = {};
  2622. $('skill-page .header > .name:contains("Materials")').closest('.card').find('.row').each((i,element) => {
  2623. itemUtil.extractItem(element, inventory);
  2624. });
  2625. emitEvent({
  2626. type: 'partial',
  2627. value: inventory
  2628. });
  2629. }
  2630.  
  2631. initialise();
  2632.  
  2633. }
  2634. );
  2635. // marketReader
  2636. window.moduleRegistry.add('marketReader', (events, elementWatcher, itemCache, util) => {
  2637.  
  2638. const emitEvent = events.emit.bind(null, 'reader-market');
  2639. let inProgress = false;
  2640.  
  2641. const exports = {
  2642. trigger: update
  2643. };
  2644.  
  2645. function initialise() {
  2646. events.register('page', update);
  2647. window.setInterval(update, 10000);
  2648. }
  2649.  
  2650. function update() {
  2651. const page = events.getLast('page');
  2652. if(!page) {
  2653. return;
  2654. }
  2655. if(page.type === 'market') {
  2656. readMarketScreen();
  2657. }
  2658. }
  2659.  
  2660. async function readMarketScreen() {
  2661. if(inProgress) {
  2662. return;
  2663. }
  2664. try {
  2665. inProgress = true;
  2666. const selectedTab = $('market-listings-component .card > .tabs > button.tab-active').text().toLowerCase();
  2667. const type = selectedTab === 'orders' ? 'BUY' : selectedTab === 'listings' ? 'OWN' : 'SELL';
  2668. await elementWatcher.exists('market-listings-component .search ~ button', undefined, 10000);
  2669. if($('market-listings-component .search > input').val()) {
  2670. return;
  2671. }
  2672. const listings = [];
  2673. $('market-listings-component .search ~ button').each((i,element) => {
  2674. element = $(element);
  2675. const name = element.find('.name').text();
  2676. const item = itemCache.byName[name];
  2677. if(!item) {
  2678. return;
  2679. }
  2680. const amount = util.parseNumber(element.find('.amount').text());
  2681. const price = util.parseNumber(element.find('.cost').text());
  2682. const listingType = type !== 'OWN' ? type : element.find('.tag').length ? 'BUY' : 'SELL';
  2683. const isOwn = !!element.attr('disabled');
  2684. listings.push({
  2685. type: listingType,
  2686. item: item.id,
  2687. amount,
  2688. price,
  2689. isOwn,
  2690. element
  2691. });
  2692. });
  2693. emitEvent({
  2694. type,
  2695. listings,
  2696. });
  2697. } catch(e) {
  2698. console.error('error in market reader', e);
  2699. return;
  2700. } finally {
  2701. inProgress = false;
  2702. }
  2703. }
  2704.  
  2705. initialise();
  2706.  
  2707. return exports;
  2708.  
  2709. }
  2710. );
  2711. // structuresReader
  2712. window.moduleRegistry.add('structuresReader', (events, util, structuresCache) => {
  2713.  
  2714. const emitEvent = events.emit.bind(null, 'reader-structures');
  2715.  
  2716. function initialise() {
  2717. events.register('page', update);
  2718. window.setInterval(update, 1000);
  2719. }
  2720.  
  2721. function update() {
  2722. const page = events.getLast('page');
  2723. if(!page) {
  2724. return;
  2725. }
  2726. if(page.type === 'structure' && $('home-page .categories .category-active').text() === 'Build') {
  2727. readStructuresScreen();
  2728. }
  2729. }
  2730.  
  2731. function readStructuresScreen() {
  2732. const structures = {};
  2733. $('home-page .categories + .card button').each((i,element) => {
  2734. element = $(element);
  2735. const name = element.find('.name').text();
  2736. const structure = structuresCache.byName[name];
  2737. if(!structure) {
  2738. return;
  2739. }
  2740. const level = util.parseNumber(element.find('.level').text());
  2741. structures[structure.id] = level;
  2742. });
  2743. emitEvent({
  2744. type: 'full',
  2745. value: structures
  2746. });
  2747. }
  2748.  
  2749. initialise();
  2750.  
  2751. }
  2752. );
  2753. // variousReader
  2754. window.moduleRegistry.add('variousReader', (events, util) => {
  2755.  
  2756. const emitEvent = events.emit.bind(null, 'reader-various');
  2757.  
  2758. function initialise() {
  2759. events.register('page', update);
  2760. window.setInterval(update, 1000);
  2761. }
  2762.  
  2763. function update() {
  2764. const page = events.getLast('page');
  2765. if(!page) {
  2766. return;
  2767. }
  2768. const various = {};
  2769. if(page.type === 'action') {
  2770. readActionScreen(various, page.skill);
  2771. }
  2772. if(page.type === 'settings') {
  2773. readSettingsScreen(various);
  2774. }
  2775. emitEvent(various);
  2776. }
  2777.  
  2778. function readActionScreen(various, skillId) {
  2779. const amountText = $('skill-page .header > .name:contains("Loot")').parent().find('.amount').text();
  2780. const amountValue = !amountText ? null : util.parseNumber(amountText.split(' / ')[1]) - util.parseNumber(amountText.split(' / ')[0]);
  2781. various.maxAmount = {
  2782. [skillId]: amountValue
  2783. };
  2784. }
  2785.  
  2786. function readSettingsScreen(various) {
  2787. const username = $('settings-page .row:contains("Username") :last-child').text();
  2788. if(username) {
  2789. various.username = username;
  2790. }
  2791. }
  2792.  
  2793. initialise();
  2794.  
  2795. }
  2796. );
  2797. // authToast
  2798. window.moduleRegistry.add('authToast', (toast) => {
  2799.  
  2800. function initialise() {
  2801. toast.create({
  2802. text: 'Pancake-Scripts initialised!',
  2803. image: 'https://img.icons8.com/?size=48&id=1ODJ62iG96gX&format=png'
  2804. });
  2805. }
  2806.  
  2807. initialise();
  2808.  
  2809. }
  2810. );
  2811. // changelog
  2812. window.moduleRegistry.add('changelog', (Promise, pages, components, request, util, configuration) => {
  2813.  
  2814. const PAGE_NAME = 'Plugin changelog';
  2815. const loaded = new Promise.Deferred('changelog');
  2816.  
  2817. let changelogs = null;
  2818.  
  2819. async function initialise() {
  2820. await pages.register({
  2821. category: 'Skills',
  2822. after: 'Changelog',
  2823. name: PAGE_NAME,
  2824. image: 'https://ironwoodrpg.com/assets/misc/changelog.png',
  2825. render: renderPage
  2826. });
  2827. configuration.registerCheckbox({
  2828. category: 'Pages',
  2829. key: 'changelog-enabled',
  2830. name: 'Changelog',
  2831. default: true,
  2832. handler: handleConfigStateChange
  2833. });
  2834. load();
  2835. }
  2836.  
  2837. function handleConfigStateChange(state, name) {
  2838. if(state) {
  2839. pages.show(PAGE_NAME);
  2840. } else {
  2841. pages.hide(PAGE_NAME);
  2842. }
  2843. }
  2844.  
  2845. async function load() {
  2846. changelogs = await request.getChangelogs();
  2847. loaded.resolve();
  2848. }
  2849.  
  2850. async function renderPage() {
  2851. await loaded;
  2852. const header = components.search(componentBlueprint, 'header');
  2853. const list = components.search(componentBlueprint, 'list');
  2854. for(const index in changelogs) {
  2855. componentBlueprint.componentId = `changelogComponent_${index}`;
  2856. header.title = changelogs[index].title;
  2857. header.textRight = new Date(changelogs[index].time).toLocaleDateString();
  2858. list.entries = changelogs[index].entries;
  2859. components.addComponent(componentBlueprint);
  2860. }
  2861. }
  2862.  
  2863. const componentBlueprint = {
  2864. componentId: 'changelogComponent',
  2865. dependsOn: 'custom-page',
  2866. parent: '.column0',
  2867. selectedTabIndex: 0,
  2868. tabs: [{
  2869. title: 'tab',
  2870. rows: [{
  2871. id: 'header',
  2872. type: 'header',
  2873. title: '',
  2874. textRight: ''
  2875. },{
  2876. id: 'list',
  2877. type: 'list',
  2878. entries: []
  2879. }]
  2880. }]
  2881. };
  2882.  
  2883. initialise();
  2884.  
  2885. }
  2886. );
  2887. // configurationPage
  2888. window.moduleRegistry.add('configurationPage', (pages, components, elementWatcher, configuration, elementCreator) => {
  2889.  
  2890. const PAGE_NAME = 'Configuration';
  2891.  
  2892. async function initialise() {
  2893. await pages.register({
  2894. category: 'Misc',
  2895. after: 'Settings',
  2896. name: PAGE_NAME,
  2897. image: 'https://cdn-icons-png.flaticon.com/512/3953/3953226.png',
  2898. columns: '2',
  2899. render: renderPage
  2900. });
  2901. elementCreator.addStyles(styles);
  2902. pages.show(PAGE_NAME);
  2903. }
  2904.  
  2905. function generateBlueprint() {
  2906. const categories = {};
  2907. for(const item of configuration.items) {
  2908. if(!categories[item.category]) {
  2909. categories[item.category] = {
  2910. name: item.category,
  2911. items: []
  2912. }
  2913. }
  2914. categories[item.category].items.push(item);
  2915. }
  2916. const blueprints = [];
  2917. let column = 1;
  2918. for(const category in categories) {
  2919. column = 1 - column;
  2920. const rows = [{
  2921. type: 'header',
  2922. title: category,
  2923. centered: true
  2924. }];
  2925. rows.push(...categories[category].items.flatMap(createRows));
  2926. blueprints.push({
  2927. componentId: `configurationComponent_${category}`,
  2928. dependsOn: 'custom-page',
  2929. parent: `.column${column}`,
  2930. selectedTabIndex: 0,
  2931. tabs: [{
  2932. rows: rows
  2933. }]
  2934. });
  2935. }
  2936. return blueprints;
  2937. }
  2938.  
  2939. function createRows(item) {
  2940. switch(item.type) {
  2941. case 'checkbox': return createRows_Checkbox(item);
  2942. case 'input': return createRows_Input(item);
  2943. case 'dropdown': return createRows_Dropdown(item);
  2944. case 'json': break;
  2945. default: throw `Unknown configuration type : ${item.type}`;
  2946. }
  2947. }
  2948.  
  2949. function createRows_Checkbox(item) {
  2950. return [{
  2951. type: 'checkbox',
  2952. text: item.name,
  2953. checked: item.value,
  2954. delay: 500,
  2955. action: (value) => {
  2956. item.handler(value);
  2957. pages.requestRender(PAGE_NAME);
  2958. }
  2959. }]
  2960. }
  2961.  
  2962. function createRows_Input(item) {
  2963. const value = item.value || item.default;
  2964. return [{
  2965. type: 'item',
  2966. name: item.name
  2967. },{
  2968. type: 'input',
  2969. name: item.name,
  2970. value: value,
  2971. inputType: item.inputType,
  2972. delay: 500,
  2973. action: (value) => {
  2974. item.handler(value);
  2975. }
  2976. }]
  2977. }
  2978.  
  2979. function createRows_Dropdown(item) {
  2980. const value = item.value || item.default;
  2981. const options = item.options.map(option => ({
  2982. text: option,
  2983. value: option,
  2984. selected: option === value
  2985. }));
  2986. return [{
  2987. type: 'item',
  2988. name: item.name
  2989. },{
  2990. type: 'dropdown',
  2991. options: options,
  2992. delay: 500,
  2993. action: (value) => {
  2994. item.handler(value);
  2995. }
  2996. }]
  2997. }
  2998.  
  2999. function renderPage() {
  3000. const blueprints = generateBlueprint();
  3001. for(const blueprint of blueprints) {
  3002. components.addComponent(blueprint);
  3003. }
  3004. }
  3005.  
  3006. const styles = `
  3007. .modifiedHeight {
  3008. height: 28px;
  3009. }
  3010. `;
  3011.  
  3012. initialise();
  3013. }
  3014. );
  3015. // debugService
  3016. window.moduleRegistry.add('debugService', (request, toast, statsStore, EstimationGenerator, logService, events) => {
  3017.  
  3018. const exports = {
  3019. submit
  3020. };
  3021.  
  3022. async function submit() {
  3023. const data = get();
  3024. try {
  3025. await forward(data);
  3026. } catch(e) {
  3027. exportToClipboard(data);
  3028. }
  3029. }
  3030.  
  3031. function get() {
  3032. return {
  3033. stats: statsStore.get(),
  3034. state: (new EstimationGenerator()).export(),
  3035. logs: logService.get(),
  3036. events: events.getLastCache()
  3037. };
  3038. }
  3039.  
  3040. async function forward(data) {
  3041. await request.report(data);
  3042. toast.create({
  3043. text: 'Forwarded debug data',
  3044. image: 'https://img.icons8.com/?size=48&id=13809'
  3045. });
  3046. }
  3047.  
  3048. function exportToClipboard(data) {
  3049. navigator.clipboard.writeText(JSON.stringify(data));
  3050. toast.create({
  3051. text: 'Failed to forward, exported to clipboard instead',
  3052. image: 'https://img.icons8.com/?size=48&id=22244'
  3053. });
  3054. }
  3055.  
  3056. return exports;
  3057.  
  3058. });
  3059. // estimator
  3060. window.moduleRegistry.add('estimator', (configuration, events, skillCache, actionCache, itemCache, estimatorAction, estimatorOutskirts, estimatorActivity, estimatorCombat, components, util, statsStore) => {
  3061.  
  3062. let enabled = false;
  3063.  
  3064. const exports = {
  3065. get
  3066. }
  3067.  
  3068. function initialise() {
  3069. configuration.registerCheckbox({
  3070. category: 'Data',
  3071. key: 'estimations',
  3072. name: 'Estimations',
  3073. default: true,
  3074. handler: handleConfigStateChange
  3075. });
  3076. events.register('page', update);
  3077. events.register('state-stats', update);
  3078. $(document).on('click', '.close', update);
  3079. }
  3080.  
  3081. function handleConfigStateChange(state) {
  3082. enabled = state;
  3083. }
  3084.  
  3085. function update() {
  3086. if(!enabled) {
  3087. return;
  3088. }
  3089. const page = events.getLast('page');
  3090. const stats = events.getLast('state-stats');
  3091. if(!page || !stats || page.type !== 'action') {
  3092. return;
  3093. }
  3094. const estimation = get(page.skill, page.action);
  3095. if(estimation) {
  3096. enrichTimings(estimation);
  3097. enrichValues(estimation);
  3098. render(estimation);
  3099. }
  3100. }
  3101.  
  3102. function get(skillId, actionId) {
  3103. const skill = skillCache.byId[skillId];
  3104. const action = actionCache.byId[actionId];
  3105. if(action.type === 'OUTSKIRTS') {
  3106. return estimatorOutskirts.get(skillId, actionId);
  3107. } else if(skill.type === 'Gathering' || skill.type === 'Crafting') {
  3108. return estimatorActivity.get(skillId, actionId);
  3109. } else if(skill.type === 'Combat') {
  3110. return estimatorCombat.get(skillId, actionId);
  3111. }
  3112. }
  3113.  
  3114. function enrichTimings(estimation) {
  3115. const inventory = Object.entries(estimation.ingredients).map(([id,amount]) => ({
  3116. id,
  3117. stored: statsStore.getInventoryItem(id),
  3118. secondsLeft: statsStore.getInventoryItem(id) * 3600 / amount
  3119. })).reduce((a,b) => (a[b.id] = b, a), {});
  3120. const equipment = Object.entries(estimation.equipments).map(([id,amount]) => ({
  3121. id,
  3122. stored: statsStore.getEquipmentItem(id),
  3123. secondsLeft: statsStore.getEquipmentItem(id) * 3600 / amount
  3124. })).reduce((a,b) => (a[b.id] = b, a), {});
  3125. let maxAmount = statsStore.get('MAX_AMOUNT', estimation.skill);
  3126. maxAmount = {
  3127. value: maxAmount,
  3128. secondsLeft: estimation.productionSpeed / 10 * (maxAmount || Infinity)
  3129. };
  3130. const merchantSellChance = statsStore.get('MERCHANT_SELL_CHANCE', estimation.skill) / 100;
  3131. if(merchantSellChance) {
  3132. maxAmount.secondsLeft /= 1 - merchantSellChance;
  3133. }
  3134. const levelState = statsStore.getLevel(estimation.skill);
  3135. estimation.timings = {
  3136. inventory,
  3137. equipment,
  3138. maxAmount,
  3139. finished: Math.min(maxAmount.secondsLeft, ...Object.values(inventory).concat(Object.values(equipment)).map(a => a.secondsLeft)),
  3140. level: util.expToNextLevel(levelState.exp) * 3600 / estimation.exp,
  3141. tier: levelState.level >= 100 ? 0 : util.expToNextTier(levelState.exp) * 3600 / estimation.exp,
  3142. };
  3143. }
  3144.  
  3145. function enrichValues(estimation) {
  3146. estimation.values = {
  3147. drop: getSellPrice(estimation.drops),
  3148. ingredient: getSellPrice(estimation.ingredients),
  3149. equipment: getSellPrice(estimation.equipments),
  3150. net: 0
  3151. };
  3152. estimation.values.net = estimation.values.drop - estimation.values.ingredient - estimation.values.equipment;
  3153. }
  3154.  
  3155. function getSellPrice(object) {
  3156. return Object.entries(object)
  3157. .map(a => a[1] * itemCache.byId[a[0]].attributes.SELL_PRICE)
  3158. .filter(a => a)
  3159. .reduce((a,b) => a+b, 0);
  3160. }
  3161.  
  3162. function render(estimation) {
  3163. components.search(componentBlueprint, 'actions').value
  3164. = util.formatNumber(estimatorAction.LOOPS_PER_HOUR / estimation.speed);
  3165. components.search(componentBlueprint, 'exp').hidden
  3166. = estimation.exp === 0;
  3167. components.search(componentBlueprint, 'exp').value
  3168. = util.formatNumber(estimation.exp);
  3169. components.search(componentBlueprint, 'survivalChance').hidden
  3170. = estimation.type === 'ACTIVITY';
  3171. components.search(componentBlueprint, 'survivalChance').value
  3172. = util.formatNumber(estimation.survivalChance * 100) + ' %';
  3173. components.search(componentBlueprint, 'finishedTime').value
  3174. = util.secondsToDuration(estimation.timings.finished);
  3175. components.search(componentBlueprint, 'levelTime').hidden
  3176. = estimation.exp === 0 || estimation.timings.level === 0;
  3177. components.search(componentBlueprint, 'levelTime').value
  3178. = util.secondsToDuration(estimation.timings.level);
  3179. components.search(componentBlueprint, 'tierTime').hidden
  3180. = estimation.exp === 0 || estimation.timings.tier === 0;
  3181. components.search(componentBlueprint, 'tierTime').value
  3182. = util.secondsToDuration(estimation.timings.tier);
  3183. components.search(componentBlueprint, 'dropValue').hidden
  3184. = estimation.values.drop === 0;
  3185. components.search(componentBlueprint, 'dropValue').value
  3186. = util.formatNumber(estimation.values.drop);
  3187. components.search(componentBlueprint, 'ingredientValue').hidden
  3188. = estimation.values.ingredient === 0;
  3189. components.search(componentBlueprint, 'ingredientValue').value
  3190. = util.formatNumber(estimation.values.ingredient);
  3191. components.search(componentBlueprint, 'equipmentValue').hidden
  3192. = estimation.values.equipment === 0;
  3193. components.search(componentBlueprint, 'equipmentValue').value
  3194. = util.formatNumber(estimation.values.equipment);
  3195. components.search(componentBlueprint, 'netValue').hidden
  3196. = estimation.values.net === 0;
  3197. components.search(componentBlueprint, 'netValue').value
  3198. = util.formatNumber(estimation.values.net);
  3199. components.search(componentBlueprint, 'tabTime').hidden
  3200. = (estimation.timings.inventory.length + estimation.timings.equipment.length) === 0;
  3201.  
  3202. const dropRows = components.search(componentBlueprint, 'dropRows');
  3203. const ingredientRows = components.search(componentBlueprint, 'ingredientRows');
  3204. const timeRows = components.search(componentBlueprint, 'timeRows');
  3205. dropRows.rows = [];
  3206. ingredientRows.rows = [];
  3207. timeRows.rows = [];
  3208. if(estimation.timings.maxAmount.value) {
  3209. timeRows.rows.push({
  3210. type: 'item',
  3211. image: 'https://img.icons8.com/?size=48&id=1HQMXezy5LeT&format=png',
  3212. imageFilter: 'invert(100%)',
  3213. name: `Max amount [${util.formatNumber(estimation.timings.maxAmount.value)}]`,
  3214. value: util.secondsToDuration(estimation.timings.maxAmount.secondsLeft)
  3215. });
  3216. }
  3217. for(const id in estimation.drops) {
  3218. const item = itemCache.byId[id];
  3219. dropRows.rows.push({
  3220. type: 'item',
  3221. image: `/assets/${item.image}`,
  3222. imagePixelated: true,
  3223. name: item.name,
  3224. value: util.formatNumber(estimation.drops[id]) + ' / hour'
  3225. });
  3226. }
  3227. for(const id in estimation.ingredients) {
  3228. const item = itemCache.byId[id];
  3229. const timing = estimation.timings.inventory[id];
  3230. ingredientRows.rows.push({
  3231. type: 'item',
  3232. image: `/assets/${item.image}`,
  3233. imagePixelated: true,
  3234. name: item.name,
  3235. value: util.formatNumber(estimation.ingredients[id]) + ' / hour'
  3236. });
  3237. timeRows.rows.push({
  3238. type: 'item',
  3239. image: `/assets/${item.image}`,
  3240. imagePixelated: true,
  3241. name: `${item.name} [${util.formatNumber(timing.stored)}]`,
  3242. value: util.secondsToDuration(timing.secondsLeft)
  3243. });
  3244. }
  3245. for(const id in estimation.equipments) {
  3246. const item = itemCache.byId[id];
  3247. const timing = estimation.timings.equipment[id];
  3248. ingredientRows.rows.push({
  3249. type: 'item',
  3250. image: `/assets/${item.image}`,
  3251. imagePixelated: true,
  3252. name: item.name,
  3253. value: util.formatNumber(estimation.equipments[id]) + ' / hour'
  3254. });
  3255. timeRows.rows.push({
  3256. type: 'item',
  3257. image: `/assets/${item.image}`,
  3258. imagePixelated: true,
  3259. name: `${item.name} [${util.formatNumber(timing.stored)}]`,
  3260. value: util.secondsToDuration(timing.secondsLeft)
  3261. });
  3262. }
  3263.  
  3264. components.addComponent(componentBlueprint);
  3265. }
  3266.  
  3267. const componentBlueprint = {
  3268. componentId: 'estimatorComponent',
  3269. dependsOn: 'skill-page',
  3270. parent: 'actions-component',
  3271. selectedTabIndex: 0,
  3272. tabs: [{
  3273. title: 'Overview',
  3274. rows: [{
  3275. type: 'item',
  3276. id: 'actions',
  3277. name: 'Actions/hour',
  3278. image: 'https://cdn-icons-png.flaticon.com/512/3563/3563395.png',
  3279. value: ''
  3280. },{
  3281. type: 'item',
  3282. id: 'exp',
  3283. name: 'Exp/hour',
  3284. image: 'https://cdn-icons-png.flaticon.com/512/616/616490.png',
  3285. value: ''
  3286. },{
  3287. type: 'item',
  3288. id: 'survivalChance',
  3289. name: 'Survival chance',
  3290. image: 'https://cdn-icons-png.flaticon.com/512/3004/3004458.png',
  3291. value: ''
  3292. },{
  3293. type: 'item',
  3294. id: 'finishedTime',
  3295. name: 'Finished',
  3296. image: 'https://cdn-icons-png.flaticon.com/512/1505/1505471.png',
  3297. value: ''
  3298. },{
  3299. type: 'item',
  3300. id: 'levelTime',
  3301. name: 'Level up',
  3302. image: 'https://cdn-icons-png.flaticon.com/512/4614/4614145.png',
  3303. value: ''
  3304. },{
  3305. type: 'item',
  3306. id: 'tierTime',
  3307. name: 'Tier up',
  3308. image: 'https://cdn-icons-png.flaticon.com/512/4789/4789514.png',
  3309. value: ''
  3310. },{
  3311. type: 'item',
  3312. id: 'dropValue',
  3313. name: 'Gold/hour (loot)',
  3314. image: 'https://cdn-icons-png.flaticon.com/512/9028/9028024.png',
  3315. imageFilter: 'invert(100%) sepia(47%) saturate(3361%) hue-rotate(313deg) brightness(106%) contrast(108%)',
  3316. value: ''
  3317. },{
  3318. type: 'item',
  3319. id: 'ingredientValue',
  3320. name: 'Gold/hour (materials)',
  3321. image: 'https://cdn-icons-png.flaticon.com/512/9028/9028031.png',
  3322. imageFilter: 'invert(100%) sepia(47%) saturate(3361%) hue-rotate(313deg) brightness(106%) contrast(108%)',
  3323. value: ''
  3324. },{
  3325. type: 'item',
  3326. id: 'equipmentValue',
  3327. name: 'Gold/hour (equipments)',
  3328. image: 'https://cdn-icons-png.flaticon.com/512/9028/9028031.png',
  3329. imageFilter: 'invert(100%) sepia(47%) saturate(3361%) hue-rotate(313deg) brightness(106%) contrast(108%)',
  3330. value: ''
  3331. },{
  3332. type: 'item',
  3333. id: 'netValue',
  3334. name: 'Gold/hour (total)',
  3335. image: 'https://cdn-icons-png.flaticon.com/512/11937/11937869.png',
  3336. imageFilter: 'invert(100%) sepia(47%) saturate(3361%) hue-rotate(313deg) brightness(106%) contrast(108%)',
  3337. value: ''
  3338. }]
  3339. },{
  3340. title: 'Items',
  3341. rows: [{
  3342. type: 'header',
  3343. title: 'Produced'
  3344. },{
  3345. type: 'segment',
  3346. id: 'dropRows',
  3347. rows: []
  3348. },{
  3349. type: 'header',
  3350. title: 'Consumed'
  3351. },{
  3352. type: 'segment',
  3353. id: 'ingredientRows',
  3354. rows: []
  3355. }]
  3356. },{
  3357. title: 'Time',
  3358. id: 'tabTime',
  3359. rows: [{
  3360. type: 'segment',
  3361. id: 'timeRows',
  3362. rows: []
  3363. }]
  3364. }]
  3365. };
  3366.  
  3367. initialise();
  3368.  
  3369. return exports;
  3370.  
  3371. }
  3372. );
  3373. // estimatorAction
  3374. window.moduleRegistry.add('estimatorAction', (dropCache, actionCache, ingredientCache, skillCache, itemCache, statsStore) => {
  3375.  
  3376. const SECONDS_PER_HOUR = 60 * 60;
  3377. const LOOPS_PER_HOUR = 10 * SECONDS_PER_HOUR; // 1 second = 10 loops
  3378. const LOOPS_PER_FOOD = 150;
  3379.  
  3380. const exports = {
  3381. LOOPS_PER_HOUR,
  3382. LOOPS_PER_FOOD,
  3383. getDrops,
  3384. getIngredients,
  3385. getEquipmentUses
  3386. };
  3387.  
  3388. function getDrops(skillId, actionId, isCombat, multiplier = 1) {
  3389. const drops = dropCache.byAction[actionId];
  3390. if(!drops) {
  3391. return [];
  3392. }
  3393. const hasFailDrops = !!drops.find(a => a.type === 'FAILED');
  3394. const hasMonsterDrops = !!drops.find(a => a.type === 'MONSTER');
  3395. const successChance = hasFailDrops ? getSuccessChance(skillId, actionId) / 100 : 1;
  3396. return drops.map(drop => {
  3397. let amount = (1 + drop.amount) / 2 * multiplier * drop.chance;
  3398. if(drop.type !== 'MONSTER' && isCombat && hasMonsterDrops) {
  3399. amount = 0;
  3400. } else if(drop.type === 'MONSTER' && !isCombat) {
  3401. amount = 0;
  3402. } else if(drop.type === 'FAILED') {
  3403. amount *= 1 - successChance;
  3404. } else {
  3405. amount *= successChance;
  3406. }
  3407. if(amount) {
  3408. return {
  3409. id: drop.item,
  3410. amount
  3411. };
  3412. }
  3413. })
  3414. .filter(a => a)
  3415. .map(a => {
  3416. const mapFindChance = statsStore.get('MAP_FIND_CHANCE', skillId) / 100;
  3417. if(!mapFindChance || !itemCache.specialIds.dungeonMap.includes(a.id)) {
  3418. return a;
  3419. }
  3420. a.amount *= 1 + mapFindChance;
  3421. return a;
  3422. })
  3423. .reduce((a,b) => (a[b.id] = b.amount, a), {});
  3424. }
  3425.  
  3426. function getSuccessChance(skillId, actionId) {
  3427. const action = actionCache.byId[actionId];
  3428. const level = statsStore.getLevel(skillId).level;
  3429. return Math.min(95, 80 + level - action.level) + Math.floor(level / 20);
  3430. }
  3431.  
  3432. function getIngredients(actionId, multiplier = 1) {
  3433. const ingredients = ingredientCache.byAction[actionId];
  3434. if(!ingredients) {
  3435. return [];
  3436. }
  3437. return ingredients.map(ingredient => ({
  3438. id: ingredient.item,
  3439. amount: ingredient.amount * multiplier
  3440. }))
  3441. .reduce((a,b) => (a[b.id] = b.amount, a), {});
  3442. }
  3443.  
  3444. function getEquipmentUses(skillId, actionId, isCombat = false, foodPerHour = 0) {
  3445. const skill = skillCache.byId[skillId];
  3446. const action = actionCache.byId[actionId];
  3447. const result = {};
  3448. const potionMultiplier = 1 + statsStore.get('DECREASED_POTION_DURATION') / 100;
  3449. if(isCombat) {
  3450. if(action.type !== 'OUTSKIRTS') {
  3451. // combat potions
  3452. statsStore.getManyEquipmentItems(itemCache.specialIds.combatPotion)
  3453. .forEach(a => result[a.id] = 20 * potionMultiplier);
  3454. }
  3455. if(action.type === 'DUNGEON') {
  3456. // dungeon map
  3457. statsStore.getManyEquipmentItems(itemCache.specialIds.dungeonMap)
  3458. .forEach(a => result[a.id] = 3 / 24);
  3459. }
  3460. if(foodPerHour && action.type !== 'OUTSKIRTS' && statsStore.get('HEAL')) {
  3461. // active food
  3462. statsStore.getManyEquipmentItems(itemCache.specialIds.food)
  3463. .forEach(a => result[a.id] = foodPerHour);
  3464. }
  3465. if(statsStore.getWeapon()?.name?.endsWith('Bow')) {
  3466. // ammo
  3467. const attacksPerHour = SECONDS_PER_HOUR / statsStore.get('ATTACK_SPEED');
  3468. const ammoPerHour = attacksPerHour * (1 - statsStore.get('AMMO_PRESERVATION_CHANCE') / 100);
  3469. statsStore.getManyEquipmentItems(itemCache.specialIds.ammo)
  3470. .forEach(a => result[a.id] = ammoPerHour);
  3471. }
  3472. } else {
  3473. if(skill.type === 'Gathering') {
  3474. // gathering potions
  3475. statsStore.getManyEquipmentItems(itemCache.specialIds.gatheringPotion)
  3476. .forEach(a => result[a.id] = 20 * potionMultiplier);
  3477. }
  3478. if(skill.type === 'Crafting') {
  3479. // crafting potions
  3480. statsStore.getManyEquipmentItems(itemCache.specialIds.craftingPotion)
  3481. .forEach(a => result[a.id] = 20 * potionMultiplier);
  3482. }
  3483. }
  3484. if(statsStore.get('PASSIVE_FOOD_CONSUMPTION') && statsStore.get('HEAL')) {
  3485. // passive food
  3486. statsStore.getManyEquipmentItems(itemCache.specialIds.food)
  3487. .forEach(a => result[a.id] = (result[a.id] || 0) + statsStore.get('PASSIVE_FOOD_CONSUMPTION') * 3600 / 5 / statsStore.get('HEAL'));
  3488. }
  3489. return result;
  3490. }
  3491.  
  3492. return exports;
  3493.  
  3494. }
  3495. );
  3496. // estimatorActivity
  3497. window.moduleRegistry.add('estimatorActivity', (skillCache, actionCache, estimatorAction, statsStore, itemCache, dropCache) => {
  3498.  
  3499. const exports = {
  3500. get
  3501. };
  3502.  
  3503. function get(skillId, actionId) {
  3504. const skill = skillCache.byId[skillId];
  3505. const action = actionCache.byId[actionId];
  3506. const speed = getSpeed(skill.technicalName, action);
  3507. const actionCount = estimatorAction.LOOPS_PER_HOUR / speed;
  3508. const actualActionCount = actionCount * (1 + statsStore.get('EFFICIENCY', skill.technicalName) / 100);
  3509. const dropCount = actualActionCount * (1 + statsStore.get('DOUBLE_DROP', skill.technicalName) / 100);
  3510. const ingredientCount = actualActionCount * (1 - statsStore.get('PRESERVATION', skill.technicalName) / 100);
  3511. const exp = actualActionCount * action.exp * (1 + statsStore.get('DOUBLE_EXP', skill.technicalName) / 100);
  3512. const drops = estimatorAction.getDrops(skillId, actionId, false, dropCount);
  3513. const ingredients = estimatorAction.getIngredients(actionId, ingredientCount);
  3514. const equipments = estimatorAction.getEquipmentUses(skillId, actionId);
  3515.  
  3516. let statLowerTierChance;
  3517. if(skill.type === 'Gathering' && (statLowerTierChance = statsStore.get('LOWER_TIER_CHANCE', skill.technicalName) / 100)) {
  3518. for(const item in drops) {
  3519. const mappings = dropCache.lowerGatherMappings[item];
  3520. if(mappings) {
  3521. for(const other of mappings) {
  3522. drops[other] = (drops[other] || 0) + statLowerTierChance * drops[item] / mappings.length;
  3523. }
  3524. drops[item] *= 1 - statLowerTierChance;
  3525. }
  3526. }
  3527. }
  3528.  
  3529. let statMerchantSellChance;
  3530. if(skill.type === 'Crafting' && (statMerchantSellChance = statsStore.get('MERCHANT_SELL_CHANCE', skill.technicalName) / 100)) {
  3531. for(const item in drops) {
  3532. drops[itemCache.specialIds.coins] = (drops[itemCache.specialIds.coins] || 0) + 2 * statMerchantSellChance * drops[item] * itemCache.byId[item].attributes.SELL_PRICE;
  3533. drops[item] *= 1 - statMerchantSellChance;
  3534. }
  3535. }
  3536.  
  3537. return {
  3538. type: 'ACTIVITY',
  3539. skill: skillId,
  3540. speed,
  3541. productionSpeed: speed * actionCount / dropCount,
  3542. exp,
  3543. drops,
  3544. ingredients,
  3545. equipments
  3546. };
  3547. }
  3548.  
  3549. function getSpeed(skillName, action) {
  3550. const speedBonus = statsStore.get('SKILL_SPEED', skillName);
  3551. return Math.round(action.speed * 1000 / (100 + speedBonus)) + 1;
  3552. }
  3553.  
  3554. return exports;
  3555.  
  3556. }
  3557. );
  3558. // estimatorCombat
  3559. window.moduleRegistry.add('estimatorCombat', (skillCache, actionCache, monsterCache, itemCache, dropCache, statsStore, Distribution, estimatorAction) => {
  3560.  
  3561. const exports = {
  3562. get,
  3563. getDamageDistributions,
  3564. getSurvivalChance
  3565. };
  3566.  
  3567. function get(skillId, actionId) {
  3568. const skill = skillCache.byId[skillId];
  3569. const action = actionCache.byId[actionId];
  3570. const monsterIds = action.monster ? [action.monster] : action.monsterGroup;
  3571. const playerStats = getPlayerStats();
  3572. const sampleMonsterStats = getMonsterStats(monsterIds[Math.floor(monsterIds.length / 2)]);
  3573. playerStats.damage_ = new Distribution();
  3574. sampleMonsterStats.damage_ = new Distribution();
  3575. for(const monsterId of monsterIds) {
  3576. const monsterStats = getMonsterStats(monsterId);
  3577. let damage_ = getInternalDamageDistribution(playerStats, monsterStats, monsterIds.length > 1);
  3578. const weight = damage_.expectedRollsUntill(monsterStats.health);
  3579. playerStats.damage_.addDistribution(damage_, weight);
  3580. damage_ = getInternalDamageDistribution(monsterStats, playerStats, monsterIds.length > 1);
  3581. sampleMonsterStats.damage_.addDistribution(damage_, weight);
  3582. }
  3583. playerStats.damage_.normalize();
  3584. sampleMonsterStats.damage_.normalize();
  3585.  
  3586. const loopsPerKill = playerStats.attackSpeed * playerStats.damage_.expectedRollsUntill(sampleMonsterStats.health) * 10 + 5;
  3587. const actionCount = estimatorAction.LOOPS_PER_HOUR / loopsPerKill;
  3588. const efficiency = 1 + statsStore.get('EFFICIENCY', skill.technicalName) / 100;
  3589. const actualActionCount = actionCount * efficiency;
  3590. const dropCount = actualActionCount * (1 + statsStore.get('DOUBLE_DROP', skill.technicalName) / 100);
  3591. const attacksReceivedPerHour = estimatorAction.LOOPS_PER_HOUR / 10 / sampleMonsterStats.attackSpeed;
  3592. const healPerFood = statsStore.get('HEAL') * (1 + statsStore.get('FOOD_EFFECT') / 100);
  3593. const damagePerHour = attacksReceivedPerHour * sampleMonsterStats.damage_.average();
  3594. const foodPerHour = damagePerHour / healPerFood * (1 - statsStore.get('FOOD_PRESERVATION_CHANCE') / 100);
  3595.  
  3596. let exp = estimatorAction.LOOPS_PER_HOUR * action.exp / 1000;
  3597. exp *= efficiency;
  3598. exp *= 1 + statsStore.get('DOUBLE_EXP', skill.technicalName) / 100;
  3599. exp *= 1 + statsStore.get('COMBAT_EXP', skill.technicalName) / 100;
  3600. exp *= getDamageTriangleModifier(playerStats, sampleMonsterStats) - 0.1;
  3601. const drops = estimatorAction.getDrops(skillId, actionId, true, dropCount);
  3602. const equipments = estimatorAction.getEquipmentUses(skillId, actionId, true, foodPerHour);
  3603. const survivalChance = getSurvivalChance(playerStats, sampleMonsterStats, loopsPerKill);
  3604.  
  3605. let statCoinSnatch;
  3606. if(statCoinSnatch = statsStore.get('COIN_SNATCH')) {
  3607. const attacksPerHour = estimatorAction.LOOPS_PER_HOUR / 10 / playerStats.attackSpeed;
  3608. const coinsPerHour = (statCoinSnatch + 1) / 2 * attacksPerHour;
  3609. drops[itemCache.specialIds.coins] = (drops[itemCache.specialIds.coins] || 0) + coinsPerHour;
  3610. }
  3611.  
  3612. let statCarveChance = 0.1;
  3613. if(action.type !== 'OUTSKIRTS' && (statCarveChance = statsStore.get('CARVE_CHANCE') / 100)) {
  3614. const boneDrop = dropCache.byAction[actionId].find(a => a.chance === 1);
  3615. const boneDropCount = drops[boneDrop.item];
  3616. const coinDrop = dropCache.byAction[actionId].find(a => a.item === itemCache.specialIds.coins);
  3617. const averageAmount = (1 + coinDrop.amount) / 2;
  3618. drops[itemCache.specialIds.coins] -= statCarveChance * coinDrop.chance * averageAmount / 2 * boneDropCount;
  3619. const mappings = dropCache.boneCarveMappings[boneDrop.item];
  3620. for(const other of mappings) {
  3621. drops[other] = (drops[other] || 0) + statCarveChance * coinDrop.chance * boneDropCount / mappings.length;
  3622. }
  3623. }
  3624.  
  3625. return {
  3626. type: 'COMBAT',
  3627. skill: skillId,
  3628. speed: loopsPerKill,
  3629. productionSpeed: loopsPerKill * actionCount / dropCount,
  3630. exp,
  3631. drops,
  3632. ingredients: {},
  3633. equipments,
  3634. player: playerStats,
  3635. monster: sampleMonsterStats,
  3636. survivalChance
  3637. };
  3638. }
  3639.  
  3640. function getPlayerStats() {
  3641. const attackStyle = statsStore.getAttackStyle();
  3642. const attackSkill = skillCache.byTechnicalName[attackStyle];
  3643. const attackLevel = statsStore.getLevel(attackSkill.id).level;
  3644. const defenseLevel = statsStore.getLevel(8).level;
  3645. return {
  3646. isPlayer: true,
  3647. attackStyle,
  3648. attackSpeed: statsStore.get('ATTACK_SPEED'),
  3649. damage: statsStore.get('DAMAGE'),
  3650. armour: statsStore.get('ARMOUR'),
  3651. health: statsStore.get('HEALTH'),
  3652. blockChance: statsStore.get('BLOCK_CHANCE')/100,
  3653. critChance: statsStore.get('CRIT_CHANCE')/100,
  3654. stunChance: statsStore.get('STUN_CHANCE')/100,
  3655. parryChance: statsStore.get('PARRY_CHANCE')/100,
  3656. bleedChance: statsStore.get('BLEED_CHANCE')/100,
  3657. damageRange: (75 + statsStore.get('DAMAGE_RANGE'))/100,
  3658. dungeonDamage: 1 + statsStore.get('DUNGEON_DAMAGE')/100,
  3659. attackLevel,
  3660. defenseLevel
  3661. };
  3662. }
  3663.  
  3664. function getMonsterStats(monsterId) {
  3665. const monster = monsterCache.byId[monsterId];
  3666. return {
  3667. isPlayer: false,
  3668. attackStyle: monster.attackStyle,
  3669. attackSpeed: monster.speed,
  3670. damage: monster.attack,
  3671. armour: monster.armour,
  3672. health: monster.health,
  3673. blockChance: 0,
  3674. critChance: 0,
  3675. stunChance: 0,
  3676. parryChance: 0,
  3677. bleedChance: 0,
  3678. damageRange: 0.75,
  3679. dungeonDamage: 1,
  3680. attackLevel: monster.level,
  3681. defenseLevel: monster.level
  3682. };
  3683. }
  3684.  
  3685. function getInternalDamageDistribution(attacker, defender, isDungeon) {
  3686. let damage = attacker.damage;
  3687. damage *= getDamageTriangleModifier(attacker, defender);
  3688. damage *= getDamageScalingRatio(attacker, defender);
  3689. damage *= getDamageArmourRatio(attacker, defender);
  3690. damage *= !isDungeon ? 1 : attacker.dungeonDamage;
  3691.  
  3692. const maxDamage_ = new Distribution(damage);
  3693. // crit
  3694. if(attacker.critChance) {
  3695. maxDamage_.convolution(
  3696. Distribution.getRandomChance(attacker.critChance),
  3697. (dmg, crit) => dmg * (crit ? 1.5 : 1)
  3698. );
  3699. }
  3700. // damage range
  3701. const result = maxDamage_.convolutionWithGenerator(
  3702. dmg => Distribution.getRandomOutcomeRounded(dmg * attacker.damageRange, dmg),
  3703. (dmg, randomDamage) => randomDamage
  3704. );
  3705. // block
  3706. if(defender.blockChance) {
  3707. result.convolution(
  3708. Distribution.getRandomChance(defender.blockChance),
  3709. (dmg, blocked) => blocked ? 0 : dmg
  3710. );
  3711. }
  3712. // stun
  3713. if(defender.stunChance) {
  3714. let stunChance = defender.stunChance;
  3715. // only when defender accurate
  3716. stunChance *= getAccuracy(defender, attacker);
  3717. // can also happen on defender parries
  3718. stunChance *= 1 + defender.parryChance;
  3719. // modifier based on speed
  3720. stunChance *= attacker.attackSpeed / defender.attackSpeed;
  3721. // convert to actual stunned percentage
  3722. const stunnedPercentage = stunChance * 2.5 / attacker.attackSpeed;
  3723. result.convolution(
  3724. Distribution.getRandomChance(stunnedPercentage),
  3725. (dmg, stunned) => stunned ? 0 : dmg
  3726. );
  3727. }
  3728. // accuracy
  3729. const accuracy = getAccuracy(attacker, defender);
  3730. result.convolution(
  3731. Distribution.getRandomChance(accuracy),
  3732. (dmg, accurate) => accurate ? dmg : 0
  3733. );
  3734. // === special effects ===
  3735. const intermediateClone_ = result.clone();
  3736. // parry attacker - deal back 25% of a regular attack
  3737. if(attacker.parryChance) {
  3738. let parryChance = attacker.parryChance;
  3739. if(attacker.attackSpeed < defender.attackSpeed) {
  3740. parryChance *= attacker.attackSpeed / defender.attackSpeed;
  3741. }
  3742. const parried_ = intermediateClone_.clone();
  3743. parried_.convolution(
  3744. Distribution.getRandomChance(parryChance),
  3745. (dmg, parried) => parried ? Math.round(dmg/4.0) : 0
  3746. );
  3747. result.convolution(
  3748. parried_,
  3749. (dmg, extra) => dmg + extra
  3750. );
  3751. if(attacker.attackSpeed > defender.attackSpeed) {
  3752. // we can parry multiple times during one turn
  3753. parryChance *= (attacker.attackSpeed - defender.attackSpeed) / attacker.attackSpeed;
  3754. parried_.convolution(
  3755. Distribution.getRandomChance(parryChance),
  3756. (dmg, parried) => parried ? dmg : 0
  3757. );
  3758. result.convolution(
  3759. parried_,
  3760. (dmg, extra) => dmg + extra
  3761. );
  3762. }
  3763. }
  3764. // parry defender - deal 50% of a regular attack
  3765. if(defender.parryChance) {
  3766. result.convolution(
  3767. Distribution.getRandomChance(defender.parryChance),
  3768. (dmg, parried) => parried ? Math.round(dmg/2) : dmg
  3769. );
  3770. }
  3771. // bleed - 50% of damage over 3 seconds (assuming to be within one attack round)
  3772. if(attacker.bleedChance) {
  3773. const bleed_ = intermediateClone_.clone();
  3774. bleed_.convolution(
  3775. Distribution.getRandomChance(attacker.bleedChance),
  3776. (dmg, bleed) => bleed ? 5 * Math.round(dmg/10) : 0
  3777. );
  3778. result.convolution(
  3779. bleed_,
  3780. (dmg, extra) => dmg + extra
  3781. );
  3782. }
  3783. return result;
  3784. }
  3785.  
  3786. function getDamageTriangleModifier(attacker, defender) {
  3787. if(!attacker.attackStyle || !defender.attackStyle) {
  3788. return 1.0;
  3789. }
  3790. if(attacker.attackStyle === defender.attackStyle) {
  3791. return 1.0;
  3792. }
  3793. if(attacker.attackStyle === 'OneHanded' && defender.attackStyle === 'Ranged') {
  3794. return 1.1;
  3795. }
  3796. if(attacker.attackStyle === 'Ranged' && defender.attackStyle === 'TwoHanded') {
  3797. return 1.1;
  3798. }
  3799. if(attacker.attackStyle === 'TwoHanded' && defender.attackStyle === 'OneHanded') {
  3800. return 1.1;
  3801. }
  3802. return 0.9;
  3803. }
  3804.  
  3805. function getDamageScalingRatio(attacker, defender) {
  3806. const ratio = attacker.attackLevel / defender.defenseLevel;
  3807. if(attacker.isPlayer) {
  3808. return Math.min(1, ratio);
  3809. }
  3810. return Math.max(1, ratio);
  3811. }
  3812.  
  3813. function getDamageArmourRatio(attacker, defender) {
  3814. if(!defender.armour) {
  3815. return 1;
  3816. }
  3817. const scale = 25 + Math.min(70, (defender.armour - 25) * 50 / 105);
  3818. return (100 - scale) / 100;
  3819. }
  3820.  
  3821. function getAccuracy(attacker, defender) {
  3822. let accuracy = 75 + (attacker.attackLevel - defender.defenseLevel) / 2.0;
  3823. accuracy = Math.max(60, accuracy);
  3824. accuracy = Math.min(90, accuracy);
  3825. return accuracy / 100;
  3826. }
  3827.  
  3828. function getDamageDistributions(monsterId) {
  3829. const playerStats = getPlayerStats();
  3830. const monsterStats = getMonsterStats(monsterId);
  3831. const playerDamage_ = getInternalDamageDistribution(playerStats, monsterStats);
  3832. const monsterDamage_ = getInternalDamageDistribution(monsterStats, playerStats);
  3833. playerDamage_.normalize();
  3834. monsterDamage_.normalize();
  3835. return [playerDamage_, monsterDamage_];
  3836. }
  3837.  
  3838. function getSurvivalChance(player, monster, loopsPerFight, fights = 10, applyCringeMultiplier = false) {
  3839. const loopsPerAttack = monster.attackSpeed * 10;
  3840. let attacksPerFight = loopsPerFight / loopsPerAttack;
  3841. if(fights === 1 && applyCringeMultiplier) {
  3842. const playerLoopsPerAttack = player.attackSpeed * 10;
  3843. const playerAttacksPerFight = loopsPerFight / playerLoopsPerAttack;
  3844. const cringeMultiplier = Math.min(1.4, Math.max(1, 1.4 - playerAttacksPerFight / 50));
  3845. attacksPerFight *= cringeMultiplier;
  3846. }
  3847. const foodPerAttack = loopsPerAttack / estimatorAction.LOOPS_PER_FOOD;
  3848. const healPerFood = statsStore.get('HEAL') * (1 + statsStore.get('FOOD_EFFECT') / 100);
  3849. const healPerAttack = Math.round(healPerFood * foodPerAttack);
  3850. const healPerFight = healPerAttack * attacksPerFight;
  3851. let deathChance = 0;
  3852. let scenarioChance = 1;
  3853. let health = player.health;
  3854. for(let i=0;i<fights;i++) {
  3855. const currentDeathChance = monster.damage_.getRightTail(attacksPerFight, health + healPerFight);
  3856. deathChance += currentDeathChance * scenarioChance;
  3857. scenarioChance *= 1 - currentDeathChance;
  3858. const damage = monster.damage_.getMeanRange(attacksPerFight, healPerFight, health + healPerFight);
  3859. health -= damage - healPerFight;
  3860. if(isNaN(health) || health === Infinity || health === -Infinity) {
  3861. // TODO NaN / Infinity result from above?
  3862. break;
  3863. }
  3864. }
  3865. const cringeCutoff = 0.10;
  3866. if(fights === 1 && !applyCringeMultiplier && deathChance < cringeCutoff) {
  3867. const other = getSurvivalChance(player, monster, loopsPerFight, fights, true);
  3868. const avg = (1 - deathChance + other) / 2;
  3869. if(avg > 1 - cringeCutoff / 2) {
  3870. return avg;
  3871. }
  3872. }
  3873. return 1 - deathChance;
  3874. }
  3875.  
  3876. return exports;
  3877.  
  3878. }
  3879. );
  3880. // estimatorOutskirts
  3881. window.moduleRegistry.add('estimatorOutskirts', (actionCache, itemCache, statsStore, estimatorActivity, estimatorCombat) => {
  3882.  
  3883. const exports = {
  3884. get
  3885. };
  3886.  
  3887. function get(skillId, actionId) {
  3888. try {
  3889. const action = actionCache.byId[actionId];
  3890.  
  3891. const activityEstimation = estimatorActivity.get(skillId, actionId);
  3892. const excludedItemIds = itemCache.specialIds.food.concat(itemCache.specialIds.combatPotion);
  3893. statsStore.update(new Set(excludedItemIds));
  3894. const combatEstimation = estimatorCombat.get(skillId, actionId);
  3895. const monsterChance = (1000 - action.outskirtsMonsterChance) / 1000;
  3896.  
  3897. // Axioms:
  3898. // combatRatio = 1 - activityRatio
  3899. // activityLoops = totalLoops * activityRatio
  3900. // combatLoops = totalLoops * combatRatio
  3901. // fights = combatLoops / combatSpeed
  3902. // actions = activityLoops / activitySpeed
  3903. // encounterChance = fights / (fights + actions)
  3904. const combatRatio = combatEstimation.speed / (activityEstimation.speed * (1 / monsterChance + combatEstimation.speed / activityEstimation.speed - 1));
  3905. const activityRatio = 1 - combatRatio;
  3906.  
  3907. const survivalChance = estimatorCombat.getSurvivalChance(combatEstimation.player, combatEstimation.monster, combatEstimation.speed, 1);
  3908.  
  3909. const exp = activityEstimation.exp * activityRatio;
  3910. const drops = {};
  3911. merge(drops, activityEstimation.drops, activityRatio);
  3912. merge(drops, combatEstimation.drops, combatRatio);
  3913. const ingredients = {};
  3914. merge(ingredients, activityEstimation.ingredients, activityRatio);
  3915. merge(ingredients, combatEstimation.ingredients, combatRatio);
  3916. const equipments = {};
  3917. merge(equipments, activityEstimation.equipments, activityRatio);
  3918. merge(equipments, combatEstimation.equipments, combatRatio);
  3919.  
  3920. return {
  3921. type: 'OUTSKIRTS',
  3922. skill: skillId,
  3923. speed: activityEstimation.speed,
  3924. productionSpeed: activityEstimation.productionSpeed,
  3925. exp,
  3926. drops,
  3927. ingredients,
  3928. equipments,
  3929. player: combatEstimation.player,
  3930. monster: combatEstimation.monster,
  3931. survivalChance
  3932. };
  3933. } finally {
  3934. statsStore.update(new Set());
  3935. }
  3936. }
  3937.  
  3938. function merge(target, source, ratio) {
  3939. for(const key in source) {
  3940. target[key] = (target[key] || 0) + source[key] * ratio;
  3941. }
  3942. }
  3943.  
  3944. return exports;
  3945.  
  3946.  
  3947.  
  3948. }
  3949. );
  3950. // guildSorts
  3951. window.moduleRegistry.add('guildSorts', (events, elementWatcher, util, elementCreator, configuration, colorMapper) => {
  3952.  
  3953. let enabled = false;
  3954.  
  3955. function initialise() {
  3956. configuration.registerCheckbox({
  3957. category: 'UI Features',
  3958. key: 'guild-sorts',
  3959. name: 'Guild sorts',
  3960. default: true,
  3961. handler: handleConfigStateChange
  3962. });
  3963. elementCreator.addStyles(styles);
  3964. events.register('page', setup);
  3965. }
  3966.  
  3967. function handleConfigStateChange(state) {
  3968. enabled = state;
  3969. }
  3970.  
  3971. async function setup() {
  3972. if(!enabled) {
  3973. return;
  3974. }
  3975. try {
  3976. await elementWatcher.exists('.card > .row');
  3977. if(events.getLast('page').type !== 'guild') {
  3978. return;
  3979. }
  3980. await addAdditionGuildSortButtons();
  3981. setupGuildMenuButtons();
  3982. } catch(e) {}
  3983. }
  3984.  
  3985. function setupGuildMenuButtons() {
  3986. $(`button > div.name:contains('Members')`).parent().on('click', async function () {
  3987. await util.sleep(50);
  3988. await addAdditionGuildSortButtons();
  3989. });
  3990. }
  3991.  
  3992. async function addAdditionGuildSortButtons() {
  3993. await elementWatcher.exists('div.sort');
  3994. const orginalButtonGroup = $('div.sort').find('div.container');
  3995.  
  3996. // rename daily to daily xp
  3997. $(`button:contains('Daily')`).text('Daily XP');
  3998. // fix text on 2 lines
  3999. $('div.sort').find('button').addClass('overrideFlex');
  4000. // attach clear custom to game own sorts
  4001. $('div.sort').find('button').on('click', function() {
  4002. clearCustomActiveButtons()
  4003. });
  4004.  
  4005. const customButtonGroup = $('<div/>')
  4006. .addClass('customButtonGroup')
  4007. .addClass('alignButtonGroupLeft')
  4008. .attr('id', 'guildSortButtonGroup')
  4009. .append(
  4010. $('<button/>')
  4011. .attr('type', 'button')
  4012. .addClass('customButtonGroupButton')
  4013. .addClass('customSortByLevel')
  4014. .text('Level')
  4015. .click(sortByLevel)
  4016. )
  4017. .append(
  4018. $('<button/>')
  4019. .attr('type', 'button')
  4020. .addClass('customButtonGroupButton')
  4021. .addClass('customSortByIdle')
  4022. .text('Idle')
  4023. .click(sortByIdle)
  4024. )
  4025. .append(
  4026. $('<button/>')
  4027. .attr('type', 'button')
  4028. .addClass('customButtonGroupButton')
  4029. .addClass('customSortByTotalXP')
  4030. .text('Total XP')
  4031. .click(sortByXp)
  4032. );
  4033.  
  4034. customButtonGroup.insertAfter(orginalButtonGroup);
  4035. }
  4036.  
  4037. function clearCustomActiveButtons() {
  4038. $('.customButtonGroupButton').removeClass('custom-sort-active');
  4039. }
  4040.  
  4041. function clearActiveButtons() {
  4042. $('div.sort').find('button').removeClass('sort-active');
  4043. }
  4044.  
  4045. function sortByXp() {
  4046. $(`button:contains('Date')`).trigger('click');
  4047.  
  4048. clearCustomActiveButtons();
  4049. clearActiveButtons();
  4050. $('.customSortByTotalXP').addClass('custom-sort-active');
  4051.  
  4052. const parent = $('div.sort').parent();
  4053. sortElements({
  4054. elements: parent.find('button.row'),
  4055. extractor: a => util.parseNumber($(a).find('div.amount').text()),
  4056. sorter: (a,b) => b-a,
  4057. target: parent
  4058. });
  4059. }
  4060.  
  4061. function sortByIdle() {
  4062. // make sure the last contributed time is visible
  4063. if(
  4064. !$(`div.sort button:contains('Date')`).hasClass('sort-active') &&
  4065. !$(`button:contains('Daily XP')`).hasClass('sort-active')
  4066. ) {
  4067. $(`button:contains('Date')`).trigger('click');
  4068. }
  4069.  
  4070. clearCustomActiveButtons();
  4071. clearActiveButtons();
  4072. $('.customSortByIdle').addClass('custom-sort-active');
  4073.  
  4074. const parent = $('div.sort').parent();
  4075. sortElements({
  4076. elements: parent.find('button.row'),
  4077. extractor: a => util.parseDuration($(a).find('div.time').text()),
  4078. sorter: (a,b) => b-a,
  4079. target: parent
  4080. });
  4081. }
  4082.  
  4083. function sortByLevel() {
  4084. clearCustomActiveButtons();
  4085. clearActiveButtons();
  4086. $('.customSortByLevel').addClass('custom-sort-active');
  4087.  
  4088. const parent = $('div.sort').parent();
  4089. sortElements({
  4090. elements: parent.find('button.row'),
  4091. extractor: a => util.parseNumber($(a).find('div.level').text().replace('Lv. ', '')),
  4092. sorter: (a,b) => b-a,
  4093. target: parent
  4094. });
  4095. }
  4096.  
  4097. // sorts a list of `elements` according to the extracted property from `extractor`,
  4098. // sorts them using `sorter`, and appends them to the `target`
  4099. // elements is a jquery list
  4100. // target is a jquery element
  4101. // { elements, target, extractor, sorter }
  4102. function sortElements(config) {
  4103. const list = config.elements.get().map(element => ({
  4104. element,
  4105. value: config.extractor(element)
  4106. }));
  4107. list.sort((a,b) => config.sorter(a.value, b.value));
  4108. for(const item of list) {
  4109. config.target.append(item.element);
  4110. }
  4111. }
  4112.  
  4113. const styles = `
  4114. .alignButtonGroupLeft {
  4115. margin-right: auto;
  4116. margin-left: 8px;
  4117. }
  4118. .customButtonGroup {
  4119. display: flex;
  4120. align-items: center;
  4121. border-radius: 4px;
  4122. box-shadow: 0 1px 2px #0003;
  4123. border: 1px solid #263849;
  4124. overflow: hidden;
  4125. }
  4126. .customButtonGroupButton {
  4127. padding: 4px var(--gap);
  4128. flex: none !important;
  4129. text-align: center;
  4130. justify-content: center;
  4131. background-color: ${colorMapper('componentRegular')};
  4132. }
  4133. .customButtonGroupButton:not(:first-of-type) {
  4134. border-left: 1px solid #263849;
  4135. }
  4136. .overrideFlex {
  4137. flex: none !important
  4138. }
  4139. .custom-sort-active {
  4140. background-color: ${colorMapper('componentLight')};
  4141. }
  4142. `;
  4143.  
  4144. initialise();
  4145. }
  4146. );
  4147. // idleBeep
  4148. window.moduleRegistry.add('idleBeep', (configuration, events, util) => {
  4149.  
  4150. const audio = new Audio('data:audio/mpeg;base64,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');
  4151. const sleepAmount = 2000;
  4152. let enabled = false;
  4153. let started = false;
  4154.  
  4155. function initialise() {
  4156. configuration.registerCheckbox({
  4157. category: 'Other',
  4158. key: 'idle-beep-enabled',
  4159. name: 'Idle beep',
  4160. default: false,
  4161. handler: handleConfigStateChange
  4162. });
  4163. events.register('xhr', handleXhr);
  4164. }
  4165.  
  4166. function handleConfigStateChange(state, name) {
  4167. enabled = state;
  4168. }
  4169.  
  4170. async function handleXhr(xhr) {
  4171. if(!enabled) {
  4172. return;
  4173. }
  4174. if(xhr.url.endsWith('startAction')) {
  4175. started = true;
  4176. }
  4177. if(xhr.url.endsWith('stopAction')) {
  4178. started = false;
  4179. console.debug(`Triggering beep in ${sleepAmount}ms`);
  4180. await util.sleep(sleepAmount);
  4181. beep();
  4182. }
  4183. }
  4184.  
  4185. function beep() {
  4186. if(!started) {
  4187. audio.play();
  4188. }
  4189. }
  4190.  
  4191. initialise();
  4192.  
  4193. }
  4194. );
  4195. // itemHover
  4196. window.moduleRegistry.add('itemHover', (configuration, itemCache, util) => {
  4197.  
  4198. let enabled = false;
  4199. let entered = false;
  4200. let element;
  4201. const converters = {
  4202. SPEED: a => a/2,
  4203. DURATION: a => util.secondsToDuration(a/10)
  4204. }
  4205.  
  4206. function initialise() {
  4207. configuration.registerCheckbox({
  4208. category: 'UI Features',
  4209. key: 'item-hover',
  4210. name: 'Item hover info',
  4211. default: true,
  4212. handler: handleConfigStateChange
  4213. });
  4214. setup();
  4215. $(document).on('mouseenter', 'div.image > img', handleMouseEnter);
  4216. $(document).on('mouseleave', 'div.image > img', handleMouseLeave);
  4217. $(document).on('click', 'div.image > img', handleMouseLeave);
  4218. }
  4219.  
  4220. function handleConfigStateChange(state) {
  4221. enabled = state;
  4222. }
  4223.  
  4224. function handleMouseEnter(event) {
  4225. if(!enabled || entered || !itemCache.byId) {
  4226. return;
  4227. }
  4228. entered = true;
  4229. const name = $(event.relatedTarget).find('.name').text();
  4230. const nameMatch = itemCache.byName[name];
  4231. if(nameMatch) {
  4232. return show(nameMatch);
  4233. }
  4234.  
  4235. const parts = event.target.src.split('/');
  4236. const lastPart = parts[parts.length-1];
  4237. const imageMatch = itemCache.byImage[lastPart];
  4238. if(imageMatch) {
  4239. return show(imageMatch);
  4240. }
  4241. }
  4242.  
  4243. function handleMouseLeave(event) {
  4244. if(!enabled || !itemCache.byId) {
  4245. return;
  4246. }
  4247. entered = false;
  4248. hide();
  4249. }
  4250.  
  4251. function show(item) {
  4252. element.find('.image').attr('src', `/assets/${item.image}`);
  4253. element.find('.name').text(item.name);
  4254. for(const attribute of itemCache.attributes) {
  4255. let value = item.attributes[attribute.technicalName];
  4256. if(value && converters[attribute.technicalName]) {
  4257. value = converters[attribute.technicalName](value);
  4258. }
  4259. if(value && Number.isInteger(value)) {
  4260. value = util.formatNumber(value);
  4261. }
  4262. updateRow(attribute.technicalName, value);
  4263. }
  4264. element.show();
  4265. }
  4266.  
  4267. function updateRow(name, value) {
  4268. if(!value) {
  4269. element.find(`.${name}-row`).hide();
  4270. } else {
  4271. element.find(`.${name}`).text(value);
  4272. element.find(`.${name}-row`).show();
  4273. }
  4274. }
  4275.  
  4276. function hide() {
  4277. element.hide();
  4278. }
  4279.  
  4280. function setup() {
  4281. const attributesHtml = itemCache.attributes
  4282. .map(a => `<div class='${a.technicalName}-row'><img src='${a.image}'/><span>${a.name}</span><span class='${a.technicalName}'/></div>`)
  4283. .join('');
  4284. $('head').append(`
  4285. <style>
  4286. #custom-item-hover {
  4287. position: fixed;
  4288. right: .5em;
  4289. top: .5em;
  4290. display: flex;
  4291. font-family: Jost,Helvetica Neue,Arial,sans-serif;
  4292. flex-direction: column;
  4293. white-space: nowrap;
  4294. z-index: 1;
  4295. background-color: black;
  4296. padding: .4rem;
  4297. border: 1px solid #3e3e3e;
  4298. border-radius: .4em;
  4299. gap: .4em;
  4300. }
  4301. #custom-item-hover > div {
  4302. display: flex;
  4303. gap: .4em;
  4304. }
  4305. #custom-item-hover > div > *:last-child {
  4306. margin-left: auto;
  4307. }
  4308. #custom-item-hover img {
  4309. width: 24px;
  4310. height: 24px;
  4311. image-rendering: auto;
  4312. }
  4313. #custom-item-hover img.pixelated {
  4314. image-rendering: pixelated;
  4315. }
  4316. </style>
  4317. `);
  4318. element = $(`
  4319. <div id='custom-item-hover' style='display:none'>
  4320. <div>
  4321. <img class='image pixelated'/>
  4322. <span class='name'/>
  4323. </div>
  4324. ${attributesHtml}
  4325. </div>
  4326. `);
  4327. $('body').append(element);
  4328. }
  4329.  
  4330. initialise();
  4331.  
  4332. }
  4333. );
  4334. // marketCompetition
  4335. window.moduleRegistry.add('marketCompetition', (configuration, events, toast, util, elementCreator, colorMapper) => {
  4336.  
  4337. let enabled = false;
  4338.  
  4339. function initialise() {
  4340. configuration.registerCheckbox({
  4341. category: 'Data',
  4342. key: 'market-competition',
  4343. name: 'Market competition indicator',
  4344. default: false,
  4345. handler: handleConfigStateChange
  4346. });
  4347. events.register('state-market', handleMarketData);
  4348. elementCreator.addStyles(styles);
  4349. }
  4350.  
  4351. function handleConfigStateChange(state) {
  4352. enabled = state;
  4353. }
  4354.  
  4355. function handleMarketData(marketData) {
  4356. if(!enabled || marketData.lastType !== 'OWN') {
  4357. return;
  4358. }
  4359. const page = events.getLast('page');
  4360. if(page.type !== 'market') {
  4361. return;
  4362. }
  4363. showToasts(marketData);
  4364. showCircles(marketData);
  4365. }
  4366.  
  4367. function showToasts(marketData) {
  4368. if(!marketData.SELL) {
  4369. toast.create({
  4370. text: 'Missing "Buy" listing data for the competition checker'
  4371. });
  4372. }
  4373. if(!marketData.BUY) {
  4374. toast.create({
  4375. text: 'Missing "Orders" listing data for the competition checker'
  4376. });
  4377. }
  4378. }
  4379.  
  4380. function showCircles(marketData) {
  4381. $('.market-competition').remove();
  4382. for(const listing of marketData.OWN) {
  4383. if(!marketData[listing.type]) {
  4384. continue;
  4385. }
  4386. const matching = marketData[listing.type].filter(a => !a.isOwn && a.item === listing.item);
  4387. const same = matching.filter(a => a.price === listing.price);
  4388. const better = matching.filter(a =>
  4389. (listing.type === 'SELL' && a.price < listing.price) ||
  4390. (listing.type === 'BUY' && a.price > listing.price)
  4391. );
  4392. if(!same.length && !better.length) {
  4393. continue;
  4394. }
  4395. const color = better.length ? 'danger' : 'warning';
  4396. const text = better.concat(same)
  4397. .map(a => `${util.formatNumber(a.amount)} @ ${util.formatNumber(a.price)}`)
  4398. .join(' / ');
  4399. listing.element.find('.cost').before(`<div class='market-competition market-competition-${color}' title='${text}'></div>`);
  4400. }
  4401. }
  4402.  
  4403. const styles = `
  4404. .market-competition {
  4405. width: 16px;
  4406. height: 16px;
  4407. border-radius: 50%;
  4408. }
  4409.  
  4410. .market-competition-warning {
  4411. background-color: ${colorMapper('warning')}
  4412. }
  4413.  
  4414. .market-competition-danger {
  4415. background-color: ${colorMapper('danger')}
  4416. }
  4417. `;
  4418.  
  4419. initialise();
  4420.  
  4421. }
  4422. );
  4423. // marketFilter
  4424. window.moduleRegistry.add('marketFilter', (configuration, localDatabase, events, components, elementWatcher, Promise, itemCache, dropCache, marketReader, elementCreator) => {
  4425.  
  4426. const STORE_NAME = 'market-filters';
  4427. const TYPE_TO_ITEM = {
  4428. 'Food': itemCache.byName['Health'].id,
  4429. 'Charcoal': itemCache.byName['Charcoal'].id,
  4430. 'Compost': itemCache.byName['Compost'].id,
  4431. 'Arcane Powder': itemCache.byName['Arcane Powder'].id,
  4432. 'Pet Snacks': itemCache.byName['Pet Snacks'].id,
  4433. };
  4434. let savedFilters = [];
  4435. let enabled = false;
  4436. let currentFilter = {
  4437. type: 'None',
  4438. amount: 0,
  4439. key: 'SELL-None'
  4440. };
  4441. let pageInitialised = false;
  4442. let listingsUpdatePromise = null;
  4443.  
  4444. async function initialise() {
  4445. configuration.registerCheckbox({
  4446. category: 'Data',
  4447. key: 'market-filter',
  4448. name: 'Market filter',
  4449. default: true,
  4450. handler: handleConfigStateChange
  4451. });
  4452. events.register('page', update);
  4453. events.register('state-market', update);
  4454.  
  4455. savedFilters = await localDatabase.getAllEntries(STORE_NAME);
  4456.  
  4457. // detect elements changing
  4458.  
  4459. // clear filters when searching yourself
  4460. $(document).on('click', 'market-listings-component .search > .clear-button', clearFilter);
  4461. $(document).on('input', 'market-listings-component .search > input', clearFilter);
  4462.  
  4463. // Buy tab -> trigger update
  4464. $(document).on('click', 'market-listings-component .card > .tabs > :nth-child(1)', function() {
  4465. showComponent();
  4466. marketReader.trigger();
  4467. });
  4468. $(document).on('click', 'market-listings-component .card > .tabs > :nth-child(2)', function() {
  4469. showComponent();
  4470. marketReader.trigger();
  4471. });
  4472. $(document).on('click', 'market-listings-component .card > .tabs > :nth-child(3)', function() {
  4473. hideComponent();
  4474. marketReader.trigger();
  4475. });
  4476.  
  4477. elementCreator.addStyles(`
  4478. .greenOutline {
  4479. outline: 2px solid rgb(83, 189, 115) !important;
  4480. }
  4481. `);
  4482.  
  4483. // on save hover, highlight saved fields
  4484. $(document).on('mouseenter mouseleave click', '.saveFilterHoverTrigger', function(e) {
  4485. switch(e.type) {
  4486. case 'mouseenter':
  4487. if(currentFilter.type === 'None') {
  4488. return $('.saveFilterHover.search').addClass('greenOutline');
  4489. }
  4490. return $('.saveFilterHover:not(.search)').addClass('greenOutline');
  4491. case 'mouseleave':
  4492. case 'click':
  4493. return $('.saveFilterHover').removeClass('greenOutline');
  4494. }
  4495. });
  4496. }
  4497.  
  4498. function handleConfigStateChange(state) {
  4499. enabled = state;
  4500. }
  4501.  
  4502. function update() {
  4503. if(!enabled) {
  4504. return;
  4505. }
  4506. if(events.getLast('page')?.type !== 'market') {
  4507. pageInitialised = false;
  4508. return;
  4509. }
  4510. initialisePage();
  4511. $('market-listings-component .search').addClass('saveFilterHover');
  4512. syncListingsView();
  4513. }
  4514.  
  4515. async function initialisePage() {
  4516. if(pageInitialised) {
  4517. return;
  4518. }
  4519. clearFilter();
  4520. try {
  4521. await elementWatcher.childAddedContinuous('market-listings-component .card', () => {
  4522. if(listingsUpdatePromise) {
  4523. listingsUpdatePromise.resolve();
  4524. listingsUpdatePromise = null;
  4525. }
  4526. });
  4527. pageInitialised = true;
  4528. } catch(error) {
  4529. console.warn(`Could probably not detect the market listing component, cause : ${error}`);
  4530. }
  4531. }
  4532.  
  4533. async function clearFilter() {
  4534. await applyFilter({
  4535. type: 'None',
  4536. amount: 0
  4537. });
  4538. syncCustomView();
  4539. }
  4540.  
  4541. async function applyFilter(filter) {
  4542. Object.assign(currentFilter, {search:null}, filter);
  4543. currentFilter.key = `${currentFilter.listingType}-${currentFilter.type}`;
  4544. if(currentFilter.type && currentFilter.type !== 'None') {
  4545. await clearSearch();
  4546. }
  4547. syncListingsView();
  4548. }
  4549.  
  4550. async function clearSearch() {
  4551. if(!$('market-listings-component .search > input').val()) {
  4552. return;
  4553. }
  4554. listingsUpdatePromise = new Promise.Expiring(5000, 'marketFilter - clearSearch');
  4555. setSearch('');
  4556. await listingsUpdatePromise;
  4557. marketReader.trigger();
  4558. }
  4559.  
  4560. function setSearch(value) {
  4561. const searchReference = $('market-listings-component .search > input');
  4562. searchReference.val(value);
  4563. searchReference[0].dispatchEvent(new Event('input'));
  4564. }
  4565.  
  4566. async function saveFilter() {
  4567. let filter = structuredClone(currentFilter);
  4568. if(currentFilter.type === 'None') {
  4569. filter.search = $('market-listings-component .search > input').val();
  4570. if(!filter.search) {
  4571. return;
  4572. }
  4573. }
  4574. if(filter.search) {
  4575. filter.key = `SEARCH-${filter.search}`;
  4576. } else {
  4577. filter.key = `${filter.type}-${filter.amount}`;
  4578. }
  4579. if(!savedFilters.find(a => a.key === filter.key)) {
  4580. localDatabase.saveEntry(STORE_NAME, filter);
  4581. savedFilters.push(filter);
  4582. }
  4583. componentBlueprint.selectedTabIndex = 0;
  4584. syncCustomView();
  4585. }
  4586.  
  4587. async function removeFilter(filter) {
  4588. localDatabase.removeEntry(STORE_NAME, filter.key);
  4589. savedFilters = savedFilters.filter(a => a.key !== filter.key);
  4590. syncCustomView();
  4591. }
  4592.  
  4593. function syncListingsView() {
  4594. const marketData = events.getLast('state-market');
  4595. if(!marketData) {
  4596. return;
  4597. }
  4598. // do nothing on own listings tab
  4599. if(marketData.lastType === 'OWN') {
  4600. resetListingsView(marketData);
  4601. return;
  4602. }
  4603. // search
  4604. if(currentFilter.search) {
  4605. resetListingsView(marketData);
  4606. setSearch(currentFilter.search);
  4607. return;
  4608. }
  4609. // no type
  4610. if(currentFilter.type === 'None') {
  4611. resetListingsView(marketData);
  4612. return;
  4613. }
  4614. // type
  4615. const itemId = TYPE_TO_ITEM[currentFilter.type];
  4616. const conversionsByItem = dropCache.conversionMappings[itemId].reduce((a,b) => (a[b.from] = b, a), {});
  4617. let matchingListings = marketData.last.filter(listing => listing.item in conversionsByItem);
  4618. for(const listing of matchingListings) {
  4619. listing.ratio = listing.price / conversionsByItem[listing.item].amount;
  4620. }
  4621. matchingListings.sort((a,b) => (a.type === 'BUY' ? 1 : -1) * (b.ratio - a.ratio));
  4622. if(currentFilter.amount) {
  4623. matchingListings = matchingListings.slice(0, currentFilter.amount);
  4624. }
  4625. for(const listing of marketData.last) {
  4626. if(matchingListings.includes(listing)) {
  4627. listing.element.show();
  4628. if(!listing.element.find('.ratio').length) {
  4629. listing.element.find('.amount').after(`<div class='ratio'>(${listing.ratio.toFixed(2)})</div>`);
  4630. }
  4631. } else {
  4632. listing.element.hide();
  4633. }
  4634. }
  4635. }
  4636.  
  4637. function resetListingsView(marketData) {
  4638. for(const element of marketData.last.map(a => a.element)) {
  4639. element.find('.ratio').remove();
  4640. element.show();
  4641. }
  4642. }
  4643.  
  4644. function syncCustomView() {
  4645. for(const option of components.search(componentBlueprint, 'filterDropdown').options) {
  4646. option.selected = option.value === currentFilter.type;
  4647. }
  4648. components.search(componentBlueprint, 'amountInput').value = currentFilter.amount;
  4649. components.search(componentBlueprint, 'savedFiltersTab').hidden = !savedFilters.length;
  4650. if(!savedFilters.length) {
  4651. componentBlueprint.selectedTabIndex = 1;
  4652. }
  4653. const savedFiltersSegment = components.search(componentBlueprint, 'savedFiltersSegment');
  4654. savedFiltersSegment.rows = [];
  4655. for(const savedFilter of savedFilters) {
  4656. let text = `Type : ${savedFilter.type}`;
  4657. if(savedFilter.amount) {
  4658. text = `Type : ${savedFilter.amount} x ${savedFilter.type}`;
  4659. }
  4660. if(savedFilter.search) {
  4661. text = `Search : ${savedFilter.search}`;
  4662. }
  4663. savedFiltersSegment.rows.push({
  4664. type: 'buttons',
  4665. buttons: [{
  4666. text: text,
  4667. size: 3,
  4668. color: 'primary',
  4669. action: async function() {
  4670. await applyFilter(savedFilter);
  4671. syncCustomView();
  4672. }
  4673. },{
  4674. text: 'Remove',
  4675. color: 'danger',
  4676. action: removeFilter.bind(null,savedFilter)
  4677. }]
  4678. });
  4679. }
  4680. showComponent();
  4681. }
  4682.  
  4683. function hideComponent() {
  4684. components.removeComponent(componentBlueprint);
  4685. }
  4686.  
  4687. function showComponent() {
  4688. componentBlueprint.prepend = screen.width < 750;
  4689. components.addComponent(componentBlueprint);
  4690. }
  4691.  
  4692. const componentBlueprint = {
  4693. componentId : 'marketFilterComponent',
  4694. dependsOn: 'market-page',
  4695. parent : 'market-listings-component > .groups > :last-child',
  4696. prepend: false,
  4697. selectedTabIndex : 0,
  4698. tabs : [{
  4699. id: 'savedFiltersTab',
  4700. title : 'Saved filters',
  4701. hidden: true,
  4702. rows: [{
  4703. type: 'segment',
  4704. id: 'savedFiltersSegment',
  4705. rows: []
  4706. }, {
  4707. type: 'buttons',
  4708. buttons: [{
  4709. text: 'Clear filter',
  4710. color: 'warning',
  4711. action: async function() {
  4712. await clearFilter();
  4713. await clearSearch();
  4714. }
  4715. }]
  4716. }]
  4717. }, {
  4718. title : 'Filter',
  4719. rows: [{
  4720. type: 'dropdown',
  4721. id: 'filterDropdown',
  4722. action: type => applyFilter({type}),
  4723. class: 'saveFilterHover',
  4724. options: [{
  4725. text: 'None',
  4726. value: 'None',
  4727. selected: false
  4728. }].concat(Object.keys(TYPE_TO_ITEM).map(a => ({
  4729. text: a,
  4730. value: a,
  4731. selected: false
  4732. })))
  4733. }, {
  4734. type: 'input',
  4735. id: 'amountInput',
  4736. name: 'Amount',
  4737. value: '',
  4738. inputType: 'number',
  4739. action: amount => applyFilter({amount:+amount}),
  4740. class: 'saveFilterHover'
  4741. }, {
  4742. type: 'buttons',
  4743. buttons: [{
  4744. text: 'Save filter',
  4745. action: saveFilter,
  4746. color: 'success',
  4747. class: 'saveFilterHoverTrigger'
  4748. }]
  4749. }, {
  4750. type: 'buttons',
  4751. buttons: [{
  4752. text: 'Clear filter',
  4753. color: 'warning',
  4754. action: async function() {
  4755. await clearFilter();
  4756. await clearSearch();
  4757. }
  4758. }]
  4759. }]
  4760. }]
  4761. };
  4762.  
  4763. initialise();
  4764.  
  4765. }
  4766. );
  4767. // recipeClickthrough
  4768. window.moduleRegistry.add('recipeClickthrough', (recipeCache, configuration, util) => {
  4769.  
  4770. let enabled = false;
  4771.  
  4772. function initialise() {
  4773. configuration.registerCheckbox({
  4774. category: 'UI Features',
  4775. key: 'recipe-click',
  4776. name: 'Recipe clickthrough',
  4777. default: true,
  4778. handler: handleConfigStateChange
  4779. });
  4780. $(document).on('click', 'div.image > img', handleClick);
  4781. }
  4782.  
  4783. function handleConfigStateChange(state) {
  4784. enabled = state;
  4785. }
  4786.  
  4787. function handleClick(event) {
  4788. if(!enabled) {
  4789. return;
  4790. }
  4791. if($(event.currentTarget).closest('button').length) {
  4792. return;
  4793. }
  4794. event.stopPropagation();
  4795. const name = $(event.relatedTarget).find('.name').text();
  4796. const nameMatch = recipeCache.byName[name];
  4797. if(nameMatch) {
  4798. return followRecipe(nameMatch);
  4799. }
  4800.  
  4801. const parts = event.target.src.split('/');
  4802. const lastPart = parts[parts.length-1];
  4803. const imageMatch = recipeCache.byImage[lastPart];
  4804. if(imageMatch) {
  4805. return followRecipe(imageMatch);
  4806. }
  4807. }
  4808.  
  4809. function followRecipe(recipe) {
  4810. util.goToPage(recipe.url);
  4811. }
  4812.  
  4813. initialise();
  4814.  
  4815. }
  4816. );
  4817. // syncTracker
  4818. window.moduleRegistry.add('syncTracker', (events, localDatabase, pages, components, util, toast, elementWatcher, debugService) => {
  4819.  
  4820. const STORE_NAME = 'sync-tracking';
  4821. const PAGE_NAME = 'Sync State';
  4822. const TOAST_SUCCESS_TIME = 1000*60*5; // 5 minutes
  4823. const TOAST_WARN_TIME = 1000*60*60*24*3; // 3 days
  4824. const TOAST_REWARN_TIME = 1000*60*60*4; // 4 hours
  4825.  
  4826. const sources = {
  4827. inventory: {
  4828. name: 'Inventory',
  4829. event: 'reader-inventory',
  4830. page: 'inventory'
  4831. },
  4832. 'equipment-equipment': {
  4833. name: 'Equipment',
  4834. event: 'reader-equipment-equipment',
  4835. page: 'equipment'
  4836. },
  4837. 'equipment-runes': {
  4838. name: 'Runes',
  4839. event: 'reader-equipment-runes',
  4840. page: 'equipment',
  4841. element: 'equipment-page .categories button:contains("Runes")'
  4842. },
  4843. 'equipment-tomes': {
  4844. name: 'Tomes',
  4845. event: 'reader-equipment-tomes',
  4846. page: 'equipment',
  4847. element: 'equipment-page .categories button:contains("Tomes")'
  4848. },
  4849. structures: {
  4850. name: 'Buildings',
  4851. event: 'reader-structures',
  4852. page: 'house/build/2'
  4853. },
  4854. enhancements: {
  4855. name: 'Building enhancements',
  4856. event: 'reader-enhancements',
  4857. page: 'house/enhance/2'
  4858. },
  4859. 'structures-guild': {
  4860. name: 'Guild buildings',
  4861. event: 'reader-structures-guild',
  4862. page: 'guild',
  4863. element: 'guild-page button:contains("Buildings")'
  4864. }
  4865. };
  4866.  
  4867. let autoVisiting = false;
  4868.  
  4869. async function initialise() {
  4870. await loadSavedData();
  4871. for(const key of Object.keys(sources)) {
  4872. events.register(sources[key].event, handleReader.bind(null, key));
  4873. }
  4874. await pages.register({
  4875. category: 'Misc',
  4876. name: PAGE_NAME,
  4877. image: 'https://img.icons8.com/?size=48&id=1ODJ62iG96gX&format=png',
  4878. columns: '3',
  4879. render: renderPage
  4880. });
  4881. pages.show(PAGE_NAME);
  4882. setInterval(update, 1000);
  4883. }
  4884.  
  4885. async function loadSavedData() {
  4886. const entries = await localDatabase.getAllEntries(STORE_NAME);
  4887. for(const entry of entries) {
  4888. if(!sources[entry.key]) {
  4889. continue;
  4890. }
  4891. sources[entry.key].lastSeen = entry.value.time;
  4892. events.emit(`reader-${entry.key}`, {
  4893. type: 'cache',
  4894. value: entry.value.value
  4895. });
  4896. }
  4897. }
  4898.  
  4899. function handleReader(key, event) {
  4900. if(event.type !== 'full') {
  4901. return;
  4902. }
  4903. const time = Date.now();
  4904. let newData = false;
  4905. if(!sources[key].lastSeen || sources[key].lastSeen + TOAST_SUCCESS_TIME < time) {
  4906. newData = true;
  4907. }
  4908. sources[key].lastSeen = time;
  4909. sources[key].notified = false;
  4910. localDatabase.saveEntry(STORE_NAME, {
  4911. key: key,
  4912. value: {
  4913. time,
  4914. value: event.value
  4915. }
  4916. });
  4917. if(newData) {
  4918. toast.create({
  4919. text: `${sources[key].name} synced`,
  4920. image: 'https://img.icons8.com/?size=48&id=1ODJ62iG96gX&format=png'
  4921. });
  4922. if(autoVisiting) {
  4923. triggerAutoVisitor();
  4924. }
  4925. }
  4926. }
  4927.  
  4928. function update() {
  4929. pages.requestRender(PAGE_NAME);
  4930. const time = Date.now();
  4931. for(const source of Object.values(sources)) {
  4932. if(source.lastSeen && source.lastSeen + TOAST_WARN_TIME >= time) {
  4933. continue;
  4934. }
  4935. if(source.notified && source.notified + TOAST_REWARN_TIME >= time) {
  4936. continue;
  4937. }
  4938. toast.create({
  4939. text: `${source.name} needs a sync`,
  4940. image: 'https://img.icons8.com/?size=48&id=1ODJ62iG96gX&format=png',
  4941. time: 5000
  4942. });
  4943. source.notified = time;
  4944. }
  4945. }
  4946.  
  4947. async function visit(source) {
  4948. if(!source.page) {
  4949. return;
  4950. }
  4951. util.goToPage(source.page);
  4952. if(source.element) {
  4953. await elementWatcher.exists(source.element);
  4954. $(source.element).click();
  4955. }
  4956. }
  4957.  
  4958. function startAutoVisiting() {
  4959. autoVisiting = true;
  4960. triggerAutoVisitor();
  4961. }
  4962.  
  4963. const stopAutoVisiting = util.debounce(function() {
  4964. autoVisiting = false;
  4965. pages.open(PAGE_NAME);
  4966. toast.create({
  4967. text: `Auto sync finished`,
  4968. image: 'https://img.icons8.com/?size=48&id=1ODJ62iG96gX&format=png'
  4969. });
  4970. }, 1500);
  4971.  
  4972. function triggerAutoVisitor() {
  4973. try {
  4974. const time = Date.now();
  4975. for(const source of Object.values(sources)) {
  4976. let secondsAgo = (time - source.lastSeen) / 1000;
  4977. if(source.page && (!source.lastSeen || secondsAgo >= 60*60)) {
  4978. visit(source);
  4979. return;
  4980. }
  4981. }
  4982. } finally {
  4983. stopAutoVisiting();
  4984. }
  4985. }
  4986.  
  4987. function renderPage() {
  4988. components.addComponent(autoVisitBlueprint);
  4989. const header = components.search(sourceBlueprint, 'header');
  4990. const item = components.search(sourceBlueprint, 'item');
  4991. const buttons = components.search(sourceBlueprint, 'buttons');
  4992. const time = Date.now();
  4993. for(const source of Object.values(sources)) {
  4994. sourceBlueprint.componentId = `syncTrackerSourceComponent_${source.name}`;
  4995. header.title = source.name;
  4996. let secondsAgo = (time - source.lastSeen) / 1000;
  4997. if(!secondsAgo) {
  4998. secondsAgo = Number.MAX_VALUE;
  4999. }
  5000. item.value = util.secondsToDuration(secondsAgo);
  5001. buttons.hidden = secondsAgo < 60*60;
  5002. buttons.buttons[0].action = visit.bind(null, source);
  5003. components.addComponent(sourceBlueprint);
  5004. }
  5005. }
  5006.  
  5007. const autoVisitBlueprint = {
  5008. componentId: 'syncTrackerAutoVisitComponent',
  5009. dependsOn: 'custom-page',
  5010. parent: '.column0',
  5011. selectedTabIndex: 0,
  5012. tabs: [
  5013. {
  5014. rows: [
  5015. {
  5016. type: 'buttons',
  5017. buttons: [
  5018. {
  5019. text: 'Auto sync',
  5020. color: 'primary',
  5021. action: startAutoVisiting
  5022. }
  5023. ]
  5024. },
  5025. {
  5026. type: 'buttons',
  5027. buttons: [
  5028. {
  5029. text: 'Submit debug info',
  5030. color: 'primary',
  5031. action: debugService.submit
  5032. }
  5033. ]
  5034. }
  5035. ]
  5036. }
  5037. ]
  5038. };
  5039.  
  5040. const sourceBlueprint = {
  5041. componentId: 'syncTrackerSourceComponent',
  5042. dependsOn: 'custom-page',
  5043. parent: '.column0',
  5044. selectedTabIndex: 0,
  5045. tabs: [
  5046. {
  5047. rows: [
  5048. {
  5049. type: 'header',
  5050. id: 'header',
  5051. title: '',
  5052. centered: true
  5053. }, {
  5054. type: 'item',
  5055. id: 'item',
  5056. name: 'Last detected',
  5057. value: ''
  5058. }, {
  5059. type: 'buttons',
  5060. id: 'buttons',
  5061. buttons: [
  5062. {
  5063. text: 'Visit',
  5064. color: 'danger',
  5065. action: undefined
  5066. }
  5067. ]
  5068. }
  5069. ]
  5070. },
  5071. ]
  5072. };
  5073.  
  5074. initialise();
  5075.  
  5076. }
  5077. );
  5078. // ui
  5079. window.moduleRegistry.add('ui', (configuration) => {
  5080.  
  5081. const id = crypto.randomUUID();
  5082. const sections = [
  5083. 'challenges-page',
  5084. 'changelog-page',
  5085. 'daily-quest-page',
  5086. 'equipment-page',
  5087. 'guild-page',
  5088. 'home-page',
  5089. 'leaderboards-page',
  5090. 'market-page',
  5091. 'merchant-page',
  5092. 'quests-page',
  5093. 'settings-page',
  5094. 'skill-page',
  5095. 'upgrade-page',
  5096. 'taming-page'
  5097. ].join(', ');
  5098. const selector = `:is(${sections})`;
  5099.  
  5100. function initialise() {
  5101. configuration.registerCheckbox({
  5102. category: 'UI Features',
  5103. key: 'ui-changes',
  5104. name: 'UI changes',
  5105. default: true,
  5106. handler: handleConfigStateChange
  5107. });
  5108. }
  5109.  
  5110. function handleConfigStateChange(state) {
  5111. if(state) {
  5112. add();
  5113. } else {
  5114. remove();
  5115. }
  5116. }
  5117.  
  5118. function add() {
  5119. document.documentElement.style.setProperty('--gap', '8px');
  5120. const element = $(`
  5121. <style>
  5122. ${selector} :not(.multi-row) > :is(
  5123. button.item,
  5124. button.row,
  5125. button.socket-button,
  5126. button.level-button,
  5127. div.item,
  5128. div.row
  5129. ) {
  5130. padding: 2px 6px !important;
  5131. min-height: 0 !important;
  5132. }
  5133.  
  5134. ${selector} :not(.multi-row) > :is(
  5135. button.item div.image,
  5136. button.row div.image,
  5137. div.item div.image,
  5138. div.item div.placeholder-image,
  5139. div.row div.image
  5140. ) {
  5141. height: 32px !important;
  5142. width: 32px !important;
  5143. min-height: 0 !important;
  5144. min-width: 0 !important;
  5145. }
  5146.  
  5147. ${selector} div.lock {
  5148. height: unset !important;
  5149. padding: 0 !important;
  5150. }
  5151.  
  5152. action-component div.body > div.image,
  5153. produce-component div.body > div.image,
  5154. daily-quest-page div.body > div.image {
  5155. height: 48px !important;
  5156. width: 48px !important;
  5157. }
  5158.  
  5159. div.progress div.body {
  5160. padding: 8px !important;
  5161. }
  5162.  
  5163. action-component div.bars {
  5164. padding: 0 !important;
  5165. }
  5166.  
  5167. equipment-component button {
  5168. padding: 0 !important;
  5169. }
  5170.  
  5171. inventory-page .items {
  5172. grid-gap: 0 !important;
  5173. }
  5174.  
  5175. div.scroll.custom-scrollbar .header,
  5176. div.scroll.custom-scrollbar button {
  5177. height: 28px !important;
  5178. }
  5179.  
  5180. div.scroll.custom-scrollbar img {
  5181. height: 16px !important;
  5182. width: 16px !important;
  5183. }
  5184.  
  5185. .scroll {
  5186. overflow-y: auto !important;
  5187. }
  5188. .scroll {
  5189. -ms-overflow-style: none; /* Internet Explorer 10+ */
  5190. scrollbar-width: none; /* Firefox */
  5191. }
  5192. .scroll::-webkit-scrollbar {
  5193. display: none; /* Safari and Chrome */
  5194. }
  5195. </style>
  5196. `).attr('id', id);
  5197. window.$('head').append(element);
  5198. }
  5199.  
  5200. function remove() {
  5201. document.documentElement.style.removeProperty('--gap');
  5202. $(`#${id}`).remove();
  5203. }
  5204.  
  5205. initialise();
  5206.  
  5207. }
  5208. );
  5209. // versionWarning
  5210. window.moduleRegistry.add('versionWarning', (request, toast) => {
  5211.  
  5212. function initialise() {
  5213. setInterval(run, 1000 * 60 * 5);
  5214. run();
  5215. }
  5216.  
  5217. async function run() {
  5218. const version = await request.getVersion();
  5219. if(!window.PANCAKE_VERSION || version === window.PANCAKE_VERSION) {
  5220. return;
  5221. }
  5222. toast.create({
  5223. text: `<a href='https://greasyfork.org/en/scripts/475356-ironwood-rpg-pancake-scripts' target='_blank'>Consider updating Pancake-Scripts to ${version}!<br>Click here to go to GreasyFork</a`,
  5224. image: 'https://img.icons8.com/?size=48&id=iAqIpjeFjcYz&format=png',
  5225. time: 5000
  5226. });
  5227. }
  5228.  
  5229. initialise();
  5230.  
  5231. }
  5232. );
  5233. // abstractStateStore
  5234. window.moduleRegistry.add('abstractStateStore', (events, util) => {
  5235.  
  5236. const SOURCES = [
  5237. 'inventory',
  5238. 'equipment-runes',
  5239. 'equipment-tomes',
  5240. 'structures',
  5241. 'enhancements',
  5242. 'structures-guild'
  5243. ];
  5244.  
  5245. const stateBySource = {};
  5246.  
  5247. function initialise() {
  5248. for(const source of SOURCES) {
  5249. stateBySource[source] = {};
  5250. events.register(`reader-${source}`, handleReader.bind(null, source));
  5251. }
  5252. }
  5253.  
  5254. function handleReader(source, event) {
  5255. let updated = false;
  5256. if(event.type === 'full' || event.type === 'cache') {
  5257. if(util.compareObjects(stateBySource[source], event.value)) {
  5258. return;
  5259. }
  5260. updated = true;
  5261. stateBySource[source] = event.value;
  5262. }
  5263. if(event.type === 'partial') {
  5264. for(const key of Object.keys(event.value)) {
  5265. if(stateBySource[source][key] === event.value[key]) {
  5266. continue;
  5267. }
  5268. updated = true;
  5269. stateBySource[source][key] = event.value[key];
  5270. }
  5271. }
  5272. if(updated) {
  5273. events.emit(`state-${source}`, stateBySource[source]);
  5274. }
  5275. }
  5276.  
  5277. initialise();
  5278.  
  5279. }
  5280. );
  5281. // configurationStore
  5282. window.moduleRegistry.add('configurationStore', (Promise, localConfigurationStore, _remoteConfigurationStore) => {
  5283.  
  5284. const initialised = new Promise.Expiring(2000, 'configurationStore');
  5285. let configs = null;
  5286.  
  5287. const exports = {
  5288. save,
  5289. getConfigs
  5290. };
  5291.  
  5292. const configurationStore = _remoteConfigurationStore || localConfigurationStore;
  5293.  
  5294. async function initialise() {
  5295. configs = await configurationStore.load();
  5296. for(const key in configs) {
  5297. configs[key] = JSON.parse(configs[key]);
  5298. }
  5299. initialised.resolve(exports);
  5300. }
  5301.  
  5302. async function save(key, value) {
  5303. await configurationStore.save(key, value);
  5304. configs[key] = value;
  5305. }
  5306.  
  5307. function getConfigs() {
  5308. return configs;
  5309. }
  5310.  
  5311. initialise();
  5312.  
  5313. return initialised;
  5314.  
  5315. }
  5316. );
  5317. // equipmentStateStore
  5318. window.moduleRegistry.add('equipmentStateStore', (events, util, itemCache) => {
  5319.  
  5320. let state = {};
  5321.  
  5322. function initialise() {
  5323. events.register('reader-equipment-equipment', handleEquipmentReader);
  5324. }
  5325.  
  5326. function handleEquipmentReader(event) {
  5327. let updated = false;
  5328. if(event.type === 'full' || event.type === 'cache') {
  5329. if(util.compareObjects(state, event.value)) {
  5330. return;
  5331. }
  5332. updated = true;
  5333. state = event.value;
  5334. }
  5335. if(event.type === 'partial') {
  5336. for(const key of Object.keys(event.value)) {
  5337. if(state[key] === event.value[key]) {
  5338. continue;
  5339. }
  5340. updated = true;
  5341. // remove items of similar type
  5342. for(const itemType in itemCache.specialIds) {
  5343. if(Array.isArray(itemCache.specialIds[itemType]) && itemCache.specialIds[itemType].includes(+key)) {
  5344. for(const itemId of itemCache.specialIds[itemType]) {
  5345. delete state[itemId];
  5346. }
  5347. }
  5348. }
  5349. state[key] = event.value[key];
  5350. }
  5351. }
  5352. if(updated) {
  5353. events.emit('state-equipment-equipment', state);
  5354. }
  5355. }
  5356.  
  5357. initialise();
  5358.  
  5359. }
  5360. );
  5361. // expStateStore
  5362. window.moduleRegistry.add('expStateStore', (events, util) => {
  5363.  
  5364. const emitEvent = events.emit.bind(null, 'state-exp');
  5365. const state = {};
  5366.  
  5367. function initialise() {
  5368. events.register('reader-exp', handleExpReader);
  5369. }
  5370.  
  5371. function handleExpReader(event) {
  5372. let updated = false;
  5373. for(const skill of event) {
  5374. if(!state[skill.id]) {
  5375. state[skill.id] = {
  5376. id: skill.id,
  5377. exp: 0,
  5378. level: 1
  5379. };
  5380. }
  5381. if(skill.exp > state[skill.id].exp) {
  5382. updated = true;
  5383. state[skill.id].exp = skill.exp;
  5384. state[skill.id].level = util.expToLevel(skill.exp);
  5385. }
  5386. }
  5387. if(updated) {
  5388. emitEvent(state);
  5389. }
  5390. }
  5391.  
  5392. initialise();
  5393.  
  5394. }
  5395. );
  5396. // localConfigurationStore
  5397. window.moduleRegistry.add('localConfigurationStore', (localDatabase) => {
  5398.  
  5399. const exports = {
  5400. load,
  5401. save
  5402. };
  5403.  
  5404. const STORE_NAME = 'settings';
  5405.  
  5406. async function load() {
  5407. const entries = await localDatabase.getAllEntries(STORE_NAME);
  5408. const configurations = {};
  5409. for(const entry of entries) {
  5410. configurations[entry.key] = entry.value;
  5411. }
  5412. return configurations;
  5413. }
  5414.  
  5415. async function save(key, value) {
  5416. await localDatabase.saveEntry(STORE_NAME, {key, value});
  5417. }
  5418.  
  5419. return exports;
  5420.  
  5421. }
  5422. );
  5423. // marketStore
  5424. window.moduleRegistry.add('marketStore', (events) => {
  5425.  
  5426. const emitEvent = events.emit.bind(null, 'state-market');
  5427. let state = {};
  5428.  
  5429. function initialise() {
  5430. events.register('page', handlePage);
  5431. events.register('reader-market', handleMarketReader);
  5432. }
  5433.  
  5434. function handlePage(event) {
  5435. if(event.type == 'market') {
  5436. state = {};
  5437. }
  5438. }
  5439.  
  5440. function handleMarketReader(event) {
  5441. state[event.type] = event.listings;
  5442. state.lastType = event.type;
  5443. state.last = event.listings;
  5444. emitEvent(state);
  5445. }
  5446.  
  5447. initialise();
  5448.  
  5449. }
  5450. );
  5451. // statsStore
  5452. window.moduleRegistry.add('statsStore', (events, util, skillCache, itemCache, structuresCache, statNameCache) => {
  5453.  
  5454. const emitEvent = events.emit.bind(null, 'state-stats');
  5455.  
  5456. const exports = {
  5457. get,
  5458. getLevel,
  5459. getInventoryItem,
  5460. getEquipmentItem,
  5461. getManyEquipmentItems,
  5462. getWeapon,
  5463. getAttackStyle,
  5464. update
  5465. };
  5466.  
  5467. let exp = {};
  5468. let inventory = {};
  5469. let tomes = {};
  5470. let equipment = {};
  5471. let runes = {};
  5472. let structures = {};
  5473. let enhancements = {};
  5474. let guildStructures = {};
  5475. let various = {};
  5476.  
  5477. let stats;
  5478.  
  5479. function initialise() {
  5480. let _update = util.debounce(update, 200);
  5481. events.register('state-exp', event => (exp = event, _update()));
  5482. events.register('state-inventory', event => (inventory = event, _update()));
  5483. events.register('state-equipment-tomes', event => (tomes = event, _update()));
  5484. events.register('state-equipment-equipment', event => (equipment = event, _update()));
  5485. events.register('state-equipment-runes', event => (runes = event, _update()));
  5486. events.register('state-structures', event => (structures = event, _update()));
  5487. events.register('state-enhancements', event => (enhancements = event, _update()));
  5488. events.register('state-structures-guild', event => (guildStructures = event, _update()));
  5489. events.register('state-various', event => (various = event, _update()));
  5490. }
  5491.  
  5492. function get(stat, skill) {
  5493. if(!stat) {
  5494. return stats;
  5495. }
  5496. statNameCache.validate(stat);
  5497. let value = 0;
  5498. if(stats && stats.global[stat]) {
  5499. value += stats.global[stat] || 0;
  5500. }
  5501. if(Number.isInteger(skill)) {
  5502. skill = skillCache.byId[skill]?.technicalName;
  5503. }
  5504. if(stats && stats.bySkill[stat] && stats.bySkill[stat][skill]) {
  5505. value += stats.bySkill[stat][skill];
  5506. }
  5507. return value;
  5508. }
  5509.  
  5510. function getLevel(skillId) {
  5511. return exp[skillId] || {
  5512. id: skillId,
  5513. exp: 0,
  5514. level: 1
  5515. };
  5516. }
  5517.  
  5518. function getInventoryItem(itemId) {
  5519. return inventory[itemId] || 0;
  5520. }
  5521.  
  5522. function getEquipmentItem(itemId) {
  5523. return equipment[itemId] || tomes[itemId] || runes[itemId] || 0;
  5524. }
  5525.  
  5526. function getManyEquipmentItems(ids) {
  5527. return ids.map(id => ({
  5528. id,
  5529. amount: getEquipmentItem(id)
  5530. })).filter(a => a.amount);
  5531. }
  5532.  
  5533. function getWeapon() {
  5534. return stats.weapon;
  5535. }
  5536.  
  5537. function getAttackStyle() {
  5538. return stats.attackStyle;
  5539. }
  5540.  
  5541. function update(excludedItemIds) {
  5542. reset();
  5543. processExp();
  5544. processTomes();
  5545. processEquipment(excludedItemIds);
  5546. processRunes();
  5547. processStructures();
  5548. processEnhancements();
  5549. processGuildStructures();
  5550. processVarious();
  5551. cleanup();
  5552. if(!excludedItemIds) {
  5553. emitEvent(stats);
  5554. }
  5555. }
  5556.  
  5557. function reset() {
  5558. stats = {
  5559. weapon: null,
  5560. attackStyle: null,
  5561. bySkill: {},
  5562. global: {}
  5563. };
  5564. }
  5565.  
  5566. function processExp() {
  5567. for(const id in exp) {
  5568. const skill = skillCache.byId[id];
  5569. addStats({
  5570. bySkill: {
  5571. EFFICIENCY : {
  5572. [skill.technicalName]: 0.25
  5573. }
  5574. }
  5575. }, exp[id].level, 4);
  5576. if(skill.displayName === 'Ranged') {
  5577. addStats({
  5578. global: {
  5579. AMMO_PRESERVATION_CHANCE : 0.5
  5580. }
  5581. }, exp[id].level, 2);
  5582. }
  5583. }
  5584. }
  5585.  
  5586. // first tomes, then equipments
  5587. // because we need to know the potion effect multiplier first
  5588. function processTomes() {
  5589. for(const id in tomes) {
  5590. const item = itemCache.byId[id];
  5591. if(!item) {
  5592. continue;
  5593. }
  5594. addStats(item.stats);
  5595. }
  5596. }
  5597.  
  5598. function processEquipment(excludedItemIds) {
  5599. let arrow;
  5600. let bow;
  5601. const potionMultiplier = get('INCREASED_POTION_EFFECT');
  5602. for(const id in equipment) {
  5603. if(equipment[id] <= 0) {
  5604. continue;
  5605. }
  5606. if(excludedItemIds && excludedItemIds.has(+id)) {
  5607. continue;
  5608. }
  5609. const item = itemCache.byId[id];
  5610. if(!item) {
  5611. continue;
  5612. }
  5613. if(item.stats.global.ATTACK_SPEED) {
  5614. stats.weapon = item;
  5615. stats.attackStyle = item.skill;
  5616. }
  5617. if(item.name.endsWith('Arrow')) {
  5618. arrow = item;
  5619. addStats({
  5620. global: {
  5621. AMMO_PRESERVATION_CHANCE : -0.5
  5622. }
  5623. }, util.tierToLevel(item.tier), 2);
  5624. continue;
  5625. }
  5626. if(item.name.endsWith('Bow')) {
  5627. bow = item;
  5628. }
  5629. let multiplier = 1;
  5630. let accuracy = 2;
  5631. if(potionMultiplier && /(Potion|Mix)$/.exec(item.name)) {
  5632. multiplier = 1 + potionMultiplier / 100;
  5633. accuracy = 10;
  5634. }
  5635. if(item.name.endsWith('Rune')) {
  5636. multiplier = equipment[id];
  5637. accuracy = 10;
  5638. }
  5639. addStats(item.stats, multiplier, accuracy);
  5640. }
  5641. if(bow && arrow) {
  5642. addStats(arrow.stats);
  5643. }
  5644. }
  5645. function processRunes() {
  5646. for(const id in runes) {
  5647. const item = itemCache.byId[id];
  5648. if(!item) {
  5649. continue;
  5650. }
  5651. addStats(item.stats, runes[id]);
  5652. }
  5653. }
  5654.  
  5655. function processStructures() {
  5656. for(const id in structures) {
  5657. const structure = structuresCache.byId[id];
  5658. if(!structure) {
  5659. continue;
  5660. }
  5661. addStats(structure.regular, structures[id] + 2/3);
  5662. }
  5663. }
  5664.  
  5665. function processEnhancements() {
  5666. for(const id in enhancements) {
  5667. const structure = structuresCache.byId[id];
  5668. if(!structure) {
  5669. continue;
  5670. }
  5671. addStats(structure.enhance, enhancements[id]);
  5672. }
  5673. }
  5674.  
  5675. function processGuildStructures() {
  5676. for(const id in guildStructures) {
  5677. const structure = structuresCache.byId[id];
  5678. if(!structure) {
  5679. continue;
  5680. }
  5681. addStats(structure.regular, guildStructures[id]);
  5682. }
  5683. }
  5684.  
  5685. function processVarious() {
  5686. if(various.maxAmount) {
  5687. const stats = {
  5688. bySkill: {
  5689. MAX_AMOUNT: {}
  5690. }
  5691. };
  5692. for(const skillId in various.maxAmount) {
  5693. const skill = skillCache.byId[skillId];
  5694. if(various.maxAmount[skillId]) {
  5695. stats.bySkill.MAX_AMOUNT[skill.technicalName] = various.maxAmount[skillId];
  5696. }
  5697. }
  5698. addStats(stats);
  5699. }
  5700. }
  5701.  
  5702. function cleanup() {
  5703. // base
  5704. addStats({
  5705. global: {
  5706. HEALTH: 10,
  5707. AMMO_PRESERVATION_CHANCE : 65
  5708. }
  5709. });
  5710. // fallback
  5711. if(!stats.weapon) {
  5712. stats.weapon = null;
  5713. stats.attackStyle = '';
  5714. stats.global.ATTACK_SPEED = 3;
  5715. }
  5716. // health percent
  5717. const healthPercent = get('HEALTH_PERCENT');
  5718. if(healthPercent) {
  5719. const health = get('HEALTH');
  5720. addStats({
  5721. global: {
  5722. HEALTH : Math.floor(healthPercent * health / 100)
  5723. }
  5724. })
  5725. }
  5726. // damage percent
  5727. const damagePercent = get('DAMAGE_PERCENT');
  5728. if(damagePercent) {
  5729. const damage = get('DAMAGE');
  5730. addStats({
  5731. global: {
  5732. DAMAGE : Math.floor(damagePercent * damage / 100)
  5733. }
  5734. })
  5735. }
  5736. // bonus level efficiency
  5737. if(stats.bySkill['BONUS_LEVEL']) {
  5738. for(const skill in stats.bySkill['BONUS_LEVEL']) {
  5739. addStats({
  5740. bySkill: {
  5741. EFFICIENCY: {
  5742. [skill]: 0.25
  5743. }
  5744. }
  5745. }, Math.round(stats.bySkill['BONUS_LEVEL'][skill]), 4);
  5746. }
  5747. }
  5748. // clamping
  5749. if(stats.global['AMMO_PRESERVATION_CHANCE'] < 65) {
  5750. stats.global['AMMO_PRESERVATION_CHANCE'] = 65;
  5751. }
  5752. if(stats.global['AMMO_PRESERVATION_CHANCE'] > 80) {
  5753. stats.global['AMMO_PRESERVATION_CHANCE'] = 80;
  5754. }
  5755. }
  5756.  
  5757. function addStats(newStats, multiplier = 1, accuracy = 1) {
  5758. if(newStats.global) {
  5759. for(const stat in newStats.global) {
  5760. if(!stats.global[stat]) {
  5761. stats.global[stat] = 0;
  5762. }
  5763. stats.global[stat] += Math.round(accuracy * multiplier * newStats.global[stat]) / accuracy;
  5764. }
  5765. }
  5766. if(newStats.bySkill) {
  5767. for(const stat in newStats.bySkill) {
  5768. if(!stats.bySkill[stat]) {
  5769. stats.bySkill[stat] = {};
  5770. }
  5771. for(const skill in newStats.bySkill[stat]) {
  5772. if(!stats.bySkill[stat][skill]) {
  5773. stats.bySkill[stat][skill] = 0;
  5774. }
  5775. stats.bySkill[stat][skill] += Math.round(accuracy * multiplier * newStats.bySkill[stat][skill]) / accuracy;
  5776. }
  5777. }
  5778. }
  5779. }
  5780.  
  5781. initialise();
  5782.  
  5783. return exports;
  5784.  
  5785. }
  5786. );
  5787. // variousStateStore
  5788. window.moduleRegistry.add('variousStateStore', (events, skillCache) => {
  5789.  
  5790. const emitEvent = events.emit.bind(null, 'state-various');
  5791. const state = {};
  5792.  
  5793. function initialise() {
  5794. events.register('reader-various', handleReader);
  5795. }
  5796.  
  5797. function handleReader(event) {
  5798. const updated = merge(state, event);
  5799. if(updated) {
  5800. emitEvent(state);
  5801. }
  5802. }
  5803.  
  5804. function merge(target, source) {
  5805. let updated = false;
  5806. for(const key in source) {
  5807. if(!(key in target)) {
  5808. target[key] = source[key];
  5809. updated = true;
  5810. continue;
  5811. }
  5812. if(typeof target[key] === 'object' && typeof source[key] === 'object') {
  5813. updated |= merge(target[key], source[key]);
  5814. continue;
  5815. }
  5816. if(target[key] !== source[key]) {
  5817. target[key] = source[key];
  5818. updated = true;
  5819. continue;
  5820. }
  5821. }
  5822. return updated;
  5823. }
  5824.  
  5825. initialise();
  5826.  
  5827. }
  5828. );
  5829. // actionCache
  5830. window.moduleRegistry.add('actionCache', (request, Promise) => {
  5831.  
  5832. const initialised = new Promise.Expiring(2000, 'actionCache');
  5833.  
  5834. const exports = {
  5835. list: [],
  5836. byId: null,
  5837. byName: null
  5838. };
  5839.  
  5840. async function tryInitialise() {
  5841. try {
  5842. await initialise();
  5843. initialised.resolve(exports);
  5844. } catch(e) {
  5845. initialised.reject(e);
  5846. }
  5847. }
  5848.  
  5849. async function initialise() {
  5850. const actions = await request.listActions();
  5851. exports.byId = {};
  5852. exports.byName = {};
  5853. for(const action of actions) {
  5854. exports.list.push(action);
  5855. exports.byId[action.id] = action;
  5856. exports.byName[action.name] = action;
  5857. }
  5858. }
  5859.  
  5860. tryInitialise();
  5861.  
  5862. return initialised;
  5863.  
  5864. }
  5865. );
  5866. // dropCache
  5867. window.moduleRegistry.add('dropCache', (request, Promise, itemCache, actionCache, skillCache, ingredientCache) => {
  5868.  
  5869. const initialised = new Promise.Expiring(2000, 'dropCache');
  5870.  
  5871. const exports = {
  5872. list: [],
  5873. byAction: null,
  5874. byItem: null,
  5875. boneCarveMappings: null,
  5876. lowerGatherMappings: null,
  5877. conversionMappings: null
  5878. };
  5879.  
  5880. Object.defineProperty(Array.prototype, '_groupBy', {
  5881. enumerable: false,
  5882. value: function(selector) {
  5883. return Object.values(this.reduce(function(rv, x) {
  5884. (rv[selector(x)] = rv[selector(x)] || []).push(x);
  5885. return rv;
  5886. }, {}));
  5887. }
  5888. });
  5889.  
  5890. Object.defineProperty(Array.prototype, '_distinct', {
  5891. enumerable: false,
  5892. value: function(selector) {
  5893. return [...new Set(this)];
  5894. }
  5895. });
  5896.  
  5897. async function tryInitialise() {
  5898. try {
  5899. await initialise();
  5900. initialised.resolve(exports);
  5901. } catch(e) {
  5902. initialised.reject(e);
  5903. }
  5904. }
  5905.  
  5906. async function initialise() {
  5907. const drops = await request.listDrops();
  5908. exports.byAction = {};
  5909. exports.byItem = {};
  5910. for(const drop of drops) {
  5911. exports.list.push(drop);
  5912. if(!exports.byAction[drop.action]) {
  5913. exports.byAction[drop.action] = [];
  5914. }
  5915. exports.byAction[drop.action].push(drop);
  5916. if(!exports.byItem[drop.item]) {
  5917. exports.byItem[drop.item] = [];
  5918. }
  5919. exports.byItem[drop.item].push(drop);
  5920. }
  5921. extractBoneCarvings();
  5922. extractLowerGathers();
  5923. extractConversions();
  5924. }
  5925.  
  5926. // I'm sorry for what follows
  5927. function extractBoneCarvings() {
  5928. let name;
  5929. exports.boneCarveMappings = exports.list
  5930. // filtering
  5931. .filter(drop => drop.type === 'GUARANTEED')
  5932. .filter(drop => (name = itemCache.byId[drop.item].name, name.endsWith('Bone') || name.endsWith('Fang')))
  5933. .filter(drop => actionCache.byId[drop.action].skill === 'Combat')
  5934. // sort
  5935. .sort((a,b) => actionCache.byId[a.action].level - actionCache.byId[b.action].level)
  5936. // per level
  5937. ._groupBy(drop => actionCache.byId[drop.action].level)
  5938. .map(a => a[0].item)
  5939. .map((item,i,all) => ({
  5940. from: item,
  5941. to: [].concat([all[i-1]]).concat([all[i-2]]).filter(a => a)
  5942. }))
  5943. .reduce((a,b) => (a[b.from] = b.to, a), {});
  5944. }
  5945.  
  5946. function extractLowerGathers() {
  5947. exports.lowerGatherMappings = exports.list
  5948. // filtering
  5949. .filter(drop => drop.type === 'REGULAR')
  5950. .filter(drop => skillCache.byName[actionCache.byId[drop.action].skill].type === 'Gathering')
  5951. // sort
  5952. .sort((a,b) => actionCache.byId[a.action].level - actionCache.byId[b.action].level)
  5953. // per action, the highest chance drop
  5954. ._groupBy(drop => drop.action)
  5955. .map(a => a.reduce((a,b) => a.chance >= b.chance ? a : b))
  5956. // per skill, and for farming,
  5957. ._groupBy(drop => {
  5958. const action = actionCache.byId[drop.action];
  5959. let skill = action.skill
  5960. if(skill === 'Farming') {
  5961. // add flower or vegetable suffix
  5962. skill += `-${action.image.split('/')[1].split('-')[0]}`;
  5963. }
  5964. return skill;
  5965. })
  5966. .flatMap(a => a
  5967. ._groupBy(drop => actionCache.byId[drop.action].level)
  5968. .map(b => b.map(drop => drop.item)._distinct())
  5969. .flatMap((b,i,all) => b.map(item => ({
  5970. from: item,
  5971. to: [].concat(all[i-1]).concat(all[i-2]).filter(a => a)
  5972. })))
  5973. )
  5974. .reduce((a,b) => (a[b.from] = b.to, a), {});
  5975. }
  5976.  
  5977. function extractConversions() {
  5978. exports.conversionMappings = exports.list
  5979. .filter(a => actionCache.byId[a.action].type === 'CONVERSION')
  5980. .map(drop => ({
  5981. from: ingredientCache.byAction[drop.action][0].item,
  5982. to: drop.item,
  5983. amount: drop.amount
  5984. }))
  5985. ._groupBy(a => a.to)
  5986. .reduce((a,b) => (a[b[0].to] = b, a), {});
  5987. }
  5988.  
  5989. tryInitialise();
  5990.  
  5991. return initialised;
  5992.  
  5993. }
  5994. );
  5995. // ingredientCache
  5996. window.moduleRegistry.add('ingredientCache', (request, Promise) => {
  5997.  
  5998. const initialised = new Promise.Expiring(2000, 'ingredientCache');
  5999.  
  6000. const exports = {
  6001. list: [],
  6002. byAction: null,
  6003. byItem: null
  6004. };
  6005.  
  6006. async function tryInitialise() {
  6007. try {
  6008. await initialise();
  6009. initialised.resolve(exports);
  6010. } catch(e) {
  6011. initialised.reject(e);
  6012. }
  6013. }
  6014.  
  6015. async function initialise() {
  6016. const ingredients = await request.listIngredients();
  6017. exports.byAction = {};
  6018. exports.byItem = {};
  6019. for(const ingredient of ingredients) {
  6020. if(!exports.byAction[ingredient.action]) {
  6021. exports.byAction[ingredient.action] = [];
  6022. }
  6023. exports.byAction[ingredient.action].push(ingredient);
  6024. if(!exports.byItem[ingredient.item]) {
  6025. exports.byItem[ingredient.item] = [];
  6026. }
  6027. exports.byItem[ingredient.item].push(ingredient);
  6028. }
  6029. }
  6030.  
  6031. tryInitialise();
  6032.  
  6033. return initialised;
  6034.  
  6035. }
  6036. );
  6037. // itemCache
  6038. window.moduleRegistry.add('itemCache', (request, Promise) => {
  6039.  
  6040. const initialised = new Promise.Expiring(2000, 'itemCache');
  6041.  
  6042. const exports = {
  6043. list: [],
  6044. byId: null,
  6045. byName: null,
  6046. byImage: null,
  6047. attributes: null,
  6048. specialIds: {
  6049. coins: null,
  6050. mainHand: null,
  6051. offHand: null,
  6052. helmet: null,
  6053. body: null,
  6054. gloves: null,
  6055. boots: null,
  6056. amulet: null,
  6057. ring: null,
  6058. bracelet: null,
  6059. hatchet: null,
  6060. pickaxe: null,
  6061. spade: null,
  6062. rod: null,
  6063. dagger: null,
  6064. telescope: null,
  6065. food: null,
  6066. ammo: null,
  6067. gatheringPotion: null,
  6068. craftingPotion: null,
  6069. combatPotion: null,
  6070. dungeonMap: null,
  6071. woodcuttingRune: null,
  6072. miningRune: null,
  6073. farmingRune: null,
  6074. fishingRune: null,
  6075. gatheringRune: null,
  6076. oneHandedRune: null,
  6077. twoHandedRune: null,
  6078. rangedRune: null,
  6079. defenseRune: null,
  6080. utilityRune: null,
  6081. savageLootingTome: null,
  6082. bountifulHarvestTome: null,
  6083. opulentCraftingTome: null,
  6084. eternalLifeTome: null,
  6085. insatiablePowerTome: null,
  6086. potentConcoctionTome: null,
  6087. }
  6088. };
  6089.  
  6090. async function tryInitialise() {
  6091. try {
  6092. await initialise();
  6093. initialised.resolve(exports);
  6094. } catch(e) {
  6095. initialised.reject(e);
  6096. }
  6097. }
  6098.  
  6099. async function initialise() {
  6100. const enrichedItems = await request.listItems();
  6101. exports.byId = {};
  6102. exports.byName = {};
  6103. exports.byImage = {};
  6104. for(const enrichedItem of enrichedItems) {
  6105. const item = Object.assign(enrichedItem.item, enrichedItem);
  6106. delete item.item;
  6107. exports.list.push(item);
  6108. exports.byId[item.id] = item;
  6109. exports.byName[item.name] = item;
  6110. const lastPart = item.image.split('/').at(-1);
  6111. if(exports.byImage[lastPart]) {
  6112. exports.byImage[lastPart].duplicate = true;
  6113. } else {
  6114. exports.byImage[lastPart] = item;
  6115. }
  6116. if(!item.attributes) {
  6117. item.attributes = {};
  6118. }
  6119. if(item.charcoal) {
  6120. item.attributes.CHARCOAL = item.charcoal;
  6121. }
  6122. if(item.compost) {
  6123. item.attributes.COMPOST = item.compost;
  6124. }
  6125. if(item.arcanePowder) {
  6126. item.attributes.ARCANE_POWDER = item.arcanePowder;
  6127. }
  6128. if(item.petSnacks) {
  6129. item.attributes.PET_SNACKS = item.petSnacks;
  6130. }
  6131. if(item.attributes.ATTACK_SPEED) {
  6132. item.attributes.ATTACK_SPEED /= 2;
  6133. }
  6134. for(const stat in item.stats.bySkill) {
  6135. if(item.stats.bySkill[stat].All) {
  6136. item.stats.global[stat] = item.stats.bySkill[stat].All;
  6137. delete item.stats.bySkill[stat].All;
  6138. if(!Object.keys(item.stats.bySkill[stat]).length) {
  6139. delete item.stats.bySkill[stat];
  6140. }
  6141. }
  6142. }
  6143. }
  6144. for(const image of Object.keys(exports.byImage)) {
  6145. if(exports.byImage[image].duplicate) {
  6146. delete exports.byImage[image];
  6147. }
  6148. }
  6149. exports.attributes = await request.listItemAttributes();
  6150. exports.attributes.push({
  6151. technicalName: 'CHARCOAL',
  6152. name: 'Charcoal',
  6153. image: '/assets/items/charcoal.png'
  6154. },{
  6155. technicalName: 'COMPOST',
  6156. name: 'Compost',
  6157. image: '/assets/misc/compost.png'
  6158. },{
  6159. technicalName: 'ARCANE_POWDER',
  6160. name: 'Arcane Powder',
  6161. image: '/assets/misc/arcane-powder.png'
  6162. },{
  6163. technicalName: 'PET_SNACKS',
  6164. name: 'Pet Snacks',
  6165. image: '/assets/misc/pet-snacks.png'
  6166. });
  6167. const potions = exports.list.filter(a => /(Potion|Mix)$/.exec(a.name));
  6168. // we do not cover any event items
  6169. exports.specialIds.coins = exports.byName['Coins'].id;
  6170. exports.specialIds.mainHand = getAllIdsEnding('Sword', 'Hammer', 'Spear', 'Scythe', 'Bow', 'Boomerang');
  6171. exports.specialIds.offHand = getAllIdsEnding('Shield');
  6172. exports.specialIds.helmet = getAllIdsEnding('Helmet');
  6173. exports.specialIds.body = getAllIdsEnding('Body');
  6174. exports.specialIds.gloves = getAllIdsEnding('Gloves');
  6175. exports.specialIds.boots = getAllIdsEnding('Boots');
  6176. exports.specialIds.amulet = getAllIdsEnding('Amulet');
  6177. exports.specialIds.ring = getAllIdsEnding('Ring');
  6178. exports.specialIds.bracelet = getAllIdsEnding('Bracelet');
  6179. exports.specialIds.hatchet = getAllIdsEnding('Hatchet');
  6180. exports.specialIds.pickaxe = getAllIdsEnding('Pickaxe');
  6181. exports.specialIds.spade = getAllIdsEnding('Spade');
  6182. exports.specialIds.rod = getAllIdsEnding('Rod');
  6183. exports.specialIds.dagger = getAllIdsEnding('Dagger');
  6184. exports.specialIds.telescope = getAllIdsEnding('Telescope');
  6185. exports.specialIds.food = exports.list.filter(a => a.stats.global.HEAL).map(a => a.id);
  6186. exports.specialIds.ammo = getAllIdsEnding('Arrow');
  6187. exports.specialIds.gatheringPotion = potions.filter(a => a.name.includes('Gather')).map(a => a.id);
  6188. exports.specialIds.craftingPotion = potions.filter(a => a.name.includes('Craft') || a.name.includes('Preservation')).map(a => a.id);
  6189. exports.specialIds.combatPotion = potions.filter(a => !a.name.includes('Gather') && !a.name.includes('Craft') && !a.name.includes('Preservation')).map(a => a.id);
  6190. exports.specialIds.dungeonMap = getAllIdsStarting('Dungeon Map');
  6191. exports.specialIds.woodcuttingRune = getAllIdsEnding('Woodcutting Rune');
  6192. exports.specialIds.miningRune = getAllIdsEnding('Mining Rune');
  6193. exports.specialIds.farmingRune = getAllIdsEnding('Farming Rune');
  6194. exports.specialIds.fishingRune = getAllIdsEnding('Fishing Rune');
  6195. exports.specialIds.gatheringRune = [
  6196. ...exports.specialIds.woodcuttingRune,
  6197. ...exports.specialIds.miningRune,
  6198. ...exports.specialIds.farmingRune,
  6199. ...exports.specialIds.fishingRune
  6200. ];
  6201. exports.specialIds.oneHandedRune = getAllIdsEnding('One-handed Rune');
  6202. exports.specialIds.twoHandedRune = getAllIdsEnding('Two-handed Rune');
  6203. exports.specialIds.rangedRune = getAllIdsEnding('Ranged Rune');
  6204. exports.specialIds.defenseRune = getAllIdsEnding('Defense Rune');
  6205. exports.specialIds.utilityRune = getAllIdsEnding('Crit Rune', 'Damage Rune', 'Block Rune', 'Stun Rune', 'Bleed Rune', 'Parry Rune');
  6206. exports.specialIds.savageLootingTome = getAllIdsStarting('Savage Looting Tome');
  6207. exports.specialIds.bountifulHarvestTome = getAllIdsStarting('Bountiful Harvest Tome');
  6208. exports.specialIds.opulentCraftingTome = getAllIdsStarting('Opulent Crafting Tome');
  6209. exports.specialIds.eternalLifeTome = getAllIdsStarting('Eternal Life Tome');
  6210. exports.specialIds.insatiablePowerTome = getAllIdsStarting('Insatiable Power Tome');
  6211. exports.specialIds.potentConcoctionTome = getAllIdsStarting('Potent Concoction Tome');
  6212. }
  6213.  
  6214. function getAllIdsEnding() {
  6215. const suffixes = Array.prototype.slice.call(arguments);
  6216. return exports.list.filter(a => new RegExp(`(${suffixes.join('|')})$`).exec(a.name)).map(a => a.id);
  6217. }
  6218.  
  6219. function getAllIdsStarting() {
  6220. const prefixes = Array.prototype.slice.call(arguments);
  6221. return exports.list.filter(a => new RegExp(`^(${prefixes.join('|')})`).exec(a.name)).map(a => a.id);
  6222. }
  6223.  
  6224. tryInitialise();
  6225.  
  6226. return initialised;
  6227.  
  6228. }
  6229. );
  6230. // monsterCache
  6231. window.moduleRegistry.add('monsterCache', (request, Promise) => {
  6232.  
  6233. const initialised = new Promise.Expiring(2000, 'monsterCache');
  6234.  
  6235. const exports = {
  6236. list: [],
  6237. byId: null,
  6238. byName: null
  6239. };
  6240.  
  6241. async function tryInitialise() {
  6242. try {
  6243. await initialise();
  6244. initialised.resolve(exports);
  6245. } catch(e) {
  6246. initialised.reject(e);
  6247. }
  6248. }
  6249.  
  6250. async function initialise() {
  6251. const monsters = await request.listMonsters();
  6252. exports.byId = {};
  6253. exports.byName = {};
  6254. for(const monster of monsters) {
  6255. exports.list.push(monster);
  6256. exports.byId[monster.id] = monster;
  6257. exports.byName[monster.name] = monster;
  6258. }
  6259. }
  6260.  
  6261. tryInitialise();
  6262.  
  6263. return initialised;
  6264.  
  6265. }
  6266. );
  6267. // recipeCache
  6268. window.moduleRegistry.add('recipeCache', (request, Promise) => {
  6269.  
  6270. const initialised = new Promise.Expiring(2000, 'recipeCache');
  6271.  
  6272. const exports = {
  6273. list: [],
  6274. byId: null,
  6275. byName: null,
  6276. byImage: null
  6277. };
  6278.  
  6279. async function tryInitialise() {
  6280. try {
  6281. await initialise();
  6282. initialised.resolve(exports);
  6283. } catch(e) {
  6284. initialised.reject(e);
  6285. }
  6286. }
  6287.  
  6288. async function initialise() {
  6289. exports.list = await request.listRecipes();
  6290. exports.byId = {};
  6291. exports.byName = {};
  6292. exports.byImage = {};
  6293. for(const recipe of exports.list) {
  6294. exports.byId[recipe.id] = recipe;
  6295. exports.byName[recipe.name] = recipe;
  6296. const lastPart = recipe.image.split('/').at(-1);
  6297. exports.byImage[lastPart] = recipe;
  6298. }
  6299. }
  6300.  
  6301. tryInitialise();
  6302.  
  6303. return initialised;
  6304.  
  6305. }
  6306. );
  6307. // skillCache
  6308. window.moduleRegistry.add('skillCache', (request, Promise) => {
  6309.  
  6310. const initialised = new Promise.Expiring(2000, 'skillCache');
  6311.  
  6312. const exports = {
  6313. list: [],
  6314. byId: null,
  6315. byName: null,
  6316. byTechnicalName: null,
  6317. };
  6318.  
  6319. async function tryInitialise() {
  6320. try {
  6321. await initialise();
  6322. initialised.resolve(exports);
  6323. } catch(e) {
  6324. initialised.reject(e);
  6325. }
  6326. }
  6327.  
  6328. async function initialise() {
  6329. const skills = await request.listSkills();
  6330. exports.byId = {};
  6331. exports.byName = {};
  6332. exports.byTechnicalName = {};
  6333. for(const skill of skills) {
  6334. exports.list.push(skill);
  6335. exports.byId[skill.id] = skill;
  6336. exports.byName[skill.displayName] = skill;
  6337. exports.byTechnicalName[skill.technicalName] = skill;
  6338. }
  6339. }
  6340.  
  6341. tryInitialise();
  6342.  
  6343. return initialised;
  6344.  
  6345. }
  6346. );
  6347. // statNameCache
  6348. window.moduleRegistry.add('statNameCache', () => {
  6349.  
  6350. const exports = {
  6351. validate
  6352. };
  6353.  
  6354. const statNames = new Set([
  6355. // ITEM_STAT_ATTRIBUTE
  6356. 'AMMO_PRESERVATION_CHANCE',
  6357. 'ATTACK_SPEED',
  6358. 'BONUS_LEVEL',
  6359. 'COIN_SNATCH',
  6360. 'COMBAT_EXP',
  6361. 'DOUBLE_EXP',
  6362. 'DOUBLE_DROP',
  6363. 'EFFICIENCY',
  6364. 'LOWER_TIER_CHANCE',
  6365. 'MERCHANT_SELL_CHANCE',
  6366. 'PRESERVATION',
  6367. 'SKILL_SPEED',
  6368. // ITEM_ATTRIBUTE
  6369. 'ARMOUR',
  6370. 'BLEED_CHANCE',
  6371. 'BLOCK_CHANCE',
  6372. 'CARVE_CHANCE',
  6373. 'COIN_SNATCH',
  6374. 'COMBAT_EXP',
  6375. 'CRIT_CHANCE',
  6376. 'DAMAGE',
  6377. 'DAMAGE_PERCENT',
  6378. 'DAMAGE_RANGE',
  6379. 'DECREASED_POTION_DURATION',
  6380. 'DUNGEON_DAMAGE',
  6381. 'FOOD_EFFECT',
  6382. 'FOOD_PRESERVATION_CHANCE',
  6383. 'HEAL',
  6384. 'HEALTH',
  6385. 'HEALTH_PERCENT',
  6386. 'INCREASED_POTION_EFFECT',
  6387. 'MAP_FIND_CHANCE',
  6388. 'PARRY_CHANCE',
  6389. 'PASSIVE_FOOD_CONSUMPTION',
  6390. 'REVIVE_TIME',
  6391. 'STUN_CHANCE',
  6392. // FRONTEND ONLY
  6393. 'MAX_AMOUNT'
  6394. ]);
  6395.  
  6396. function validate(name) {
  6397. if(!statNames.has(name)) {
  6398. throw `Unsupported stat usage : ${name}`;
  6399. }
  6400. }
  6401.  
  6402. return exports;
  6403.  
  6404. });
  6405. // structuresCache
  6406. window.moduleRegistry.add('structuresCache', (request, Promise) => {
  6407.  
  6408. const initialised = new Promise.Expiring(2000, 'structuresCache');
  6409.  
  6410. const exports = {
  6411. list: [],
  6412. byId: null,
  6413. byName: null
  6414. };
  6415.  
  6416. async function tryInitialise() {
  6417. try {
  6418. await initialise();
  6419. initialised.resolve(exports);
  6420. } catch(e) {
  6421. initialised.reject(e);
  6422. }
  6423. }
  6424.  
  6425. async function initialise() {
  6426. const structures = await request.listStructures();
  6427. exports.byId = {};
  6428. exports.byName = {};
  6429. for(const structure of structures) {
  6430. exports.list.push(structure);
  6431. exports.byId[structure.id] = structure;
  6432. exports.byName[structure.name] = structure;
  6433. }
  6434. }
  6435.  
  6436. tryInitialise();
  6437.  
  6438. return initialised;
  6439.  
  6440. }
  6441. );
  6442. window.moduleRegistry.build();