Ironwood RPG - Pancake-Scripts

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

当前为 2024-05-21 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Ironwood RPG - Pancake-Scripts
  3. // @namespace http://tampermonkey.net/
  4. // @version 4.4.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.4.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', (colorMapper) => {
  1104.  
  1105. const exports = {
  1106. addStyles,
  1107. getButton,
  1108. getTag
  1109. };
  1110.  
  1111. function addStyles(css) {
  1112. const head = document.getElementsByTagName('head')[0]
  1113. if(!head) {
  1114. console.error('Could not add styles, missing head');
  1115. return;
  1116. }
  1117. const style = document.createElement('style');
  1118. style.type = 'text/css';
  1119. style.innerHTML = css;
  1120. head.appendChild(style);
  1121. }
  1122.  
  1123. function getButton(text, onClick) {
  1124. const element = $(`<button class='myButton'>${text}</button>`)
  1125. .css('background-color', colorMapper('componentRegular'))
  1126. .css('display', 'inline-block')
  1127. .css('padding', '0 5px')
  1128. .css('margin', '0 5px');
  1129. if(onClick) {
  1130. element.click(onClick);
  1131. }
  1132. return element;
  1133. }
  1134.  
  1135. function getTag(text, image, clazz) {
  1136. const element = $(`<div>${text}</div>`)
  1137. .css('border-radius', '4px')
  1138. .css('padding', '2px 6px')
  1139. .css('border', '1px solid #263849')
  1140. .css('font-size', '14px')
  1141. .css('color', '#aaa')
  1142. .css('display', 'flex')
  1143. .css('align-items', 'center')
  1144. .addClass(clazz);
  1145. if(image) {
  1146. const imageElement = $(`<img src='${image}'/>`)
  1147. .css('width', '16px')
  1148. .css('height', '16px')
  1149. .css('image-rendering', 'auto');
  1150. element.prepend(imageElement);
  1151. }
  1152. return element;
  1153. }
  1154.  
  1155. return exports;
  1156.  
  1157. }
  1158. );
  1159. // elementWatcher
  1160. window.moduleRegistry.add('elementWatcher', (Promise, polyfill) => {
  1161.  
  1162. const exports = {
  1163. exists,
  1164. childAdded,
  1165. childAddedContinuous,
  1166. idle,
  1167. addRecursiveObserver
  1168. }
  1169.  
  1170. const $ = window.$;
  1171.  
  1172. async function exists(selector, delay, timeout, inverted) {
  1173. delay = delay !== undefined ? delay : 10;
  1174. timeout = timeout !== undefined ? timeout : 5000;
  1175. const promiseWrapper = new Promise.Checking(() => {
  1176. let result = $(selector)[0];
  1177. return inverted ? !result : result;
  1178. }, delay, timeout, `elementWatcher - exists - ${selector}`);
  1179. return promiseWrapper;
  1180. }
  1181.  
  1182. async function childAdded(selector) {
  1183. const promiseWrapper = new Promise.Expiring(5000, `elementWatcher - childAdded - ${selector}`);
  1184.  
  1185. try {
  1186. const parent = await exists(selector);
  1187. const observer = new MutationObserver(function(mutations, observer) {
  1188. for(const mutation of mutations) {
  1189. if(mutation.addedNodes?.length) {
  1190. observer.disconnect();
  1191. promiseWrapper.resolve();
  1192. }
  1193. }
  1194. });
  1195. observer.observe(parent, { childList: true });
  1196. } catch(error) {
  1197. promiseWrapper.reject(error);
  1198. }
  1199.  
  1200. return promiseWrapper;
  1201. }
  1202.  
  1203. async function childAddedContinuous(selector, callback) {
  1204. const parent = await exists(selector);
  1205. const observer = new MutationObserver(function(mutations, observer) {
  1206. if(mutations.find(a => a.addedNodes?.length)) {
  1207. callback();
  1208. }
  1209. });
  1210. observer.observe(parent, { childList: true });
  1211. }
  1212.  
  1213. async function addRecursiveObserver(callback, ...chain) {
  1214. const root = await exists(chain[0]);
  1215. chain = chain.slice(1).map(a => a.toUpperCase());
  1216. _addRecursiveObserver(callback, root, chain);
  1217. }
  1218.  
  1219. function _addRecursiveObserver(callback, element, chain) {
  1220. if(chain.length === 0) {
  1221. callback(element);
  1222. }
  1223. const observer = new MutationObserver(function(mutations, observer) {
  1224. const match = mutations
  1225. .flatMap(a => Array.from(a.addedNodes))
  1226. .find(a => a.tagName === chain[0]);
  1227. if(match) {
  1228. _addRecursiveObserver(callback, match, chain.slice(1));
  1229. }
  1230. });
  1231. observer.observe(element, { childList: true });
  1232. for(const child of element.children) {
  1233. if(child.tagName === chain[0]) {
  1234. _addRecursiveObserver(callback, child, chain.slice(1));
  1235. }
  1236. }
  1237. }
  1238.  
  1239. async function idle() {
  1240. const promise = new Promise.Expiring(1000, 'elementWatcher - idle');
  1241. polyfill.requestIdleCallback(() => {
  1242. promise.resolve();
  1243. });
  1244. return promise;
  1245. }
  1246.  
  1247. return exports;
  1248.  
  1249. }
  1250. );
  1251. // EstimationGenerator
  1252. window.moduleRegistry.add('EstimationGenerator', (events, estimator, statsStore, util, skillCache, itemCache, structuresCache) => {
  1253.  
  1254. const EVENTS = {
  1255. exp: {
  1256. event: 'state-exp',
  1257. default: skillCache.list.reduce((a,b) => (a[b.id] = {id:b.id,exp:0,level:1}, a), {})
  1258. },
  1259. tomes: {
  1260. event: 'state-equipment-tomes',
  1261. default: {}
  1262. },
  1263. equipment: {
  1264. event: 'state-equipment-equipment',
  1265. default: {}
  1266. },
  1267. runes: {
  1268. event: 'state-equipment-runes',
  1269. default: {}
  1270. },
  1271. structures: {
  1272. event: 'state-structures',
  1273. default: {}
  1274. },
  1275. enhancements: {
  1276. event: 'state-enhancements',
  1277. default: {}
  1278. },
  1279. guild: {
  1280. event: 'state-structures-guild',
  1281. default: {}
  1282. }
  1283. };
  1284.  
  1285. class EstimationGenerator {
  1286.  
  1287. #backup;
  1288. #state;
  1289.  
  1290. constructor() {
  1291. this.#backup = {};
  1292. this.#state = this.#backup;
  1293. for(const name in EVENTS) {
  1294. this.#backup[name] = events.getLast(EVENTS[name].event);
  1295. }
  1296. }
  1297.  
  1298. reset() {
  1299. for(const name in EVENTS) {
  1300. this.#state[name] = structuredClone(EVENTS[name].default);
  1301. }
  1302. return this;
  1303. }
  1304.  
  1305. run(skillId, actionId) {
  1306. this.#sendCustomEvents();
  1307. statsStore.update(new Set());
  1308. const estimation = estimator.get(skillId, actionId);
  1309. this.#sendBackupEvents();
  1310. return estimation;
  1311. }
  1312.  
  1313. #sendCustomEvents() {
  1314. for(const name in this.#state) {
  1315. events.emit(EVENTS[name].event, this.#state[name]);
  1316. }
  1317. }
  1318.  
  1319. #sendBackupEvents() {
  1320. for(const name in this.#backup) {
  1321. events.emit(EVENTS[name].event, this.#backup[name]);
  1322. }
  1323. }
  1324.  
  1325. level(skill, level, exp = 0) {
  1326. if(typeof skill === 'string') {
  1327. const match = skillCache.byName[skill];
  1328. if(!match) {
  1329. throw `Could not find skill ${skill}`;
  1330. }
  1331. skill = match.id;
  1332. }
  1333. if(!exp) {
  1334. exp = util.levelToExp(level);
  1335. }
  1336. this.#state.exp[skill] = {
  1337. id: skill,
  1338. exp,
  1339. level
  1340. };
  1341. return this;
  1342. }
  1343.  
  1344. equipment(item, amount = 1) {
  1345. if(typeof item === 'string') {
  1346. const match = itemCache.byName[item];
  1347. if(!match) {
  1348. throw `Could not find item ${item}`;
  1349. }
  1350. item = match.id;
  1351. }
  1352. this.#state.equipment[item] = amount;
  1353. return this;
  1354. }
  1355.  
  1356. rune(item, amount = 1) {
  1357. if(typeof item === 'string') {
  1358. const match = itemCache.byName[item];
  1359. if(!match) {
  1360. throw `Could not find item ${item}`;
  1361. }
  1362. item = match.id;
  1363. }
  1364. this.#state.runes[item] = amount;
  1365. return this;
  1366. }
  1367.  
  1368. tome(item) {
  1369. if(typeof item === 'string') {
  1370. const match = itemCache.byName[item];
  1371. if(!match) {
  1372. throw `Could not find item ${item}`;
  1373. }
  1374. item = match.id;
  1375. }
  1376. this.#state.tomes[item] = 1;
  1377. return this;
  1378. }
  1379.  
  1380. structure(structure, level) {
  1381. if(typeof structure === 'string') {
  1382. const match = structuresCache.byName[structure];
  1383. if(!match) {
  1384. throw `Could not find structure ${structure}`;
  1385. }
  1386. structure = match.id;
  1387. }
  1388. this.#state.structures[structure] = level;
  1389. return this;
  1390. }
  1391.  
  1392. enhancement(structure, level) {
  1393. if(typeof structure === 'string') {
  1394. const match = structuresCache.byName[structure];
  1395. if(!match) {
  1396. throw `Could not find structure ${structure}`;
  1397. }
  1398. structure = match.id;
  1399. }
  1400. this.#state.enhancements[structure] = level;
  1401. return this;
  1402. }
  1403.  
  1404. guild(structure, level) {
  1405. if(typeof structure === 'string') {
  1406. const match = structuresCache.byName[structure];
  1407. if(!match) {
  1408. throw `Could not find structure ${structure}`;
  1409. }
  1410. structure = match.id;
  1411. }
  1412. this.#state.guild[structure] = level;
  1413. return this;
  1414. }
  1415.  
  1416. export() {
  1417. return structuredClone(this.#state);
  1418. }
  1419.  
  1420. import(state) {
  1421. this.#state = structuredClone(state);
  1422. return this;
  1423. }
  1424.  
  1425. }
  1426.  
  1427. return EstimationGenerator;
  1428.  
  1429. }
  1430. );
  1431. // events
  1432. window.moduleRegistry.add('events', () => {
  1433.  
  1434. const exports = {
  1435. register,
  1436. emit,
  1437. getLast,
  1438. getLastCache
  1439. };
  1440.  
  1441. const handlers = {};
  1442. const lastCache = {};
  1443.  
  1444. function register(name, handler) {
  1445. if(!handlers[name]) {
  1446. handlers[name] = [];
  1447. }
  1448. handlers[name].push(handler);
  1449. if(lastCache[name]) {
  1450. handle(handler, lastCache[name]);
  1451. }
  1452. }
  1453.  
  1454. // options = { skipCache }
  1455. function emit(name, data, options) {
  1456. if(!options?.skipCache) {
  1457. lastCache[name] = data;
  1458. }
  1459. if(!handlers[name]) {
  1460. return;
  1461. }
  1462. for(const handler of handlers[name]) {
  1463. handle(handler, data);
  1464. }
  1465. }
  1466.  
  1467. function handle(handler, data) {
  1468. try {
  1469. handler(data);
  1470. } catch(e) {
  1471. console.error('Something went wrong', e);
  1472. }
  1473. }
  1474.  
  1475. function getLast(name) {
  1476. return lastCache[name];
  1477. }
  1478.  
  1479. function getLastCache() {
  1480. return lastCache;
  1481. }
  1482.  
  1483. return exports;
  1484.  
  1485. }
  1486. );
  1487. // interceptor
  1488. window.moduleRegistry.add('interceptor', (events) => {
  1489.  
  1490. function initialise() {
  1491. registerInterceptorUrlChange();
  1492. events.emit('url', window.location.href);
  1493. }
  1494.  
  1495. function registerInterceptorUrlChange() {
  1496. const pushState = history.pushState;
  1497. history.pushState = function() {
  1498. pushState.apply(history, arguments);
  1499. console.debug(`Detected page ${arguments[2]}`);
  1500. events.emit('url', arguments[2]);
  1501. };
  1502. const replaceState = history.replaceState;
  1503. history.replaceState = function() {
  1504. replaceState.apply(history, arguments);
  1505. console.debug(`Detected page ${arguments[2]}`);
  1506. events.emit('url', arguments[2]);
  1507. }
  1508. }
  1509.  
  1510. initialise();
  1511.  
  1512. }
  1513. );
  1514. // itemUtil
  1515. window.moduleRegistry.add('itemUtil', (util, itemCache) => {
  1516.  
  1517. const exports = {
  1518. extractItem
  1519. };
  1520.  
  1521. function extractItem(element, target, ignoreMissing) {
  1522. element = $(element);
  1523. const name = element.find('.name').text();
  1524. let item = itemCache.byName[name];
  1525. if(!item) {
  1526. const src = element.find('img').attr('src');
  1527. if(src) {
  1528. const image = src.split('/').at(-1);
  1529. item = itemCache.byImage[image];
  1530. }
  1531. }
  1532. if(!item) {
  1533. if(!ignoreMissing) {
  1534. console.warn(`Could not find item with name [${name}]`);
  1535. }
  1536. return false;
  1537. }
  1538. let amount = 1;
  1539. let amountElements = element.find('.amount, .value');
  1540. let uses = 0;
  1541. if(amountElements.length) {
  1542. var amountText = amountElements.text();
  1543. if(!amountText) {
  1544. return false;
  1545. }
  1546. if(amountText.includes(' / ')) {
  1547. amountText = amountText.split(' / ')[0];
  1548. }
  1549. amount = util.parseNumber(amountText);
  1550. if(amountText.includes('&')) {
  1551. const usesText = amountText.split('&')[1];
  1552. uses = util.parseNumber(usesText);
  1553. }
  1554. }
  1555. if(!uses) {
  1556. const usesText = element.find('.uses, .use').text();
  1557. if(usesText && !usesText.endsWith('HP')) {
  1558. uses = util.parseNumber(usesText);
  1559. }
  1560. }
  1561. amount += uses;
  1562. target[item.id] = (target[item.id] || 0) + amount;
  1563. return item;
  1564. }
  1565.  
  1566. return exports;
  1567.  
  1568. }
  1569. );
  1570. // localDatabase
  1571. window.moduleRegistry.add('localDatabase', (Promise) => {
  1572.  
  1573. const exports = {
  1574. getAllEntries,
  1575. saveEntry,
  1576. removeEntry
  1577. };
  1578.  
  1579. const initialised = new Promise.Expiring(2000, 'localDatabase');
  1580. let database = null;
  1581.  
  1582. const databaseName = 'PancakeScripts';
  1583.  
  1584. function initialise() {
  1585. const request = window.indexedDB.open(databaseName, 5);
  1586. request.onsuccess = function(event) {
  1587. database = this.result;
  1588. initialised.resolve(exports);
  1589. };
  1590. request.onerror = function(event) {
  1591. console.error(`Failed creating IndexedDB : ${event.target.errorCode}`);
  1592. };
  1593. request.onupgradeneeded = function(event) {
  1594. const db = event.target.result;
  1595. if(event.oldVersion <= 0) {
  1596. console.debug('Creating IndexedDB');
  1597. db
  1598. .createObjectStore('settings', { keyPath: 'key' })
  1599. .createIndex('key', 'key', { unique: true });
  1600. }
  1601. if(event.oldVersion <= 1) {
  1602. db
  1603. .createObjectStore('sync-tracking', { keyPath: 'key' })
  1604. .createIndex('key', 'key', { unique: true });
  1605. }
  1606. if(event.oldVersion <= 2) {
  1607. db
  1608. .createObjectStore('market-filters', { keyPath: 'key' })
  1609. .createIndex('key', 'key', { unique: true });
  1610. }
  1611. if(event.oldVersion <= 3) {
  1612. db
  1613. .createObjectStore('component-tabs', { keyPath: 'key' })
  1614. .createIndex('key', 'key', { unique: true });
  1615. }
  1616. if(event.oldVersion <= 4) {
  1617. db
  1618. .createObjectStore('various', { keyPath: 'key' })
  1619. .createIndex('key', 'key', { unique: true });
  1620. }
  1621. };
  1622. }
  1623.  
  1624. async function getAllEntries(storeName) {
  1625. const result = new Promise.Expiring(1000, 'localDatabase - getAllEntries');
  1626. const entries = [];
  1627. const store = database.transaction(storeName, 'readonly').objectStore(storeName);
  1628. const request = store.openCursor();
  1629. request.onsuccess = function(event) {
  1630. const cursor = event.target.result;
  1631. if(cursor) {
  1632. entries.push(cursor.value);
  1633. cursor.continue();
  1634. } else {
  1635. result.resolve(entries);
  1636. }
  1637. };
  1638. request.onerror = function(event) {
  1639. result.reject(event.error);
  1640. };
  1641. return result;
  1642. }
  1643.  
  1644. async function saveEntry(storeName, entry) {
  1645. const result = new Promise.Expiring(1000, 'localDatabase - saveEntry');
  1646. const store = database.transaction(storeName, 'readwrite').objectStore(storeName);
  1647. const request = store.put(entry);
  1648. request.onsuccess = function(event) {
  1649. result.resolve();
  1650. };
  1651. request.onerror = function(event) {
  1652. result.reject(event.error);
  1653. };
  1654. return result;
  1655. }
  1656.  
  1657. async function removeEntry(storeName, key) {
  1658. const result = new Promise.Expiring(1000, 'localDatabase - removeEntry');
  1659. const store = database.transaction(storeName, 'readwrite').objectStore(storeName);
  1660. const request = store.delete(key);
  1661. request.onsuccess = function(event) {
  1662. result.resolve();
  1663. };
  1664. request.onerror = function(event) {
  1665. result.reject(event.error);
  1666. };
  1667. return result;
  1668. }
  1669.  
  1670. initialise();
  1671.  
  1672. return initialised;
  1673.  
  1674. }
  1675. );
  1676. // logService
  1677. window.moduleRegistry.add('logService', () => {
  1678.  
  1679. const exports = {
  1680. error,
  1681. get
  1682. };
  1683.  
  1684. const errors = [];
  1685.  
  1686. function initialise() {
  1687. window.onerror = function(message, url, lineNumber, columnNumber, error) {
  1688. errors.push({
  1689. time: Date.now(),
  1690. message,
  1691. url,
  1692. lineNumber,
  1693. columnNumber,
  1694. error
  1695. });
  1696. return false;
  1697. };
  1698. }
  1699.  
  1700. function error() {
  1701. errors.push({
  1702. time: Date.now(),
  1703. value: [...arguments]
  1704. });
  1705. }
  1706.  
  1707. function get() {
  1708. return errors;
  1709. }
  1710.  
  1711. initialise();
  1712.  
  1713. return exports;
  1714.  
  1715. });
  1716. // pageDetector
  1717. window.moduleRegistry.add('pageDetector', (events, elementWatcher, util) => {
  1718.  
  1719. const emitEvent = events.emit.bind(null, 'page');
  1720. const debouncedUpdate = util.debounce(update, 100);
  1721.  
  1722. async function initialise() {
  1723. events.register('url', debouncedUpdate);
  1724. $(document).on('click', 'taming-page .header:contains("Menu") ~ button', () => debouncedUpdate());
  1725. $(document).on('click', 'taming-page .header:contains("Expeditions") ~ button', () => debouncedUpdate());
  1726. $(document).on('click', 'taming-page .header:contains("Expeditions") > button', () => debouncedUpdate());
  1727. }
  1728.  
  1729. async function update(url) {
  1730. if(!url) {
  1731. url = events.getLast('url');
  1732. }
  1733. let result = null;
  1734. const parts = url.split('/');
  1735. await elementWatcher.idle();
  1736. if(url.includes('/skill/15')) {
  1737. const menu = $('taming-page .header:contains("Menu") ~ button.row-active .name').text().toLowerCase();
  1738. let tier = 0;
  1739. if(menu === 'expeditions') {
  1740. const level = util.parseNumber($('taming-page .header:contains("Expeditions") ~ button.row-active .level').text());
  1741. tier = util.levelToTier(level);
  1742. }
  1743. result = {
  1744. type: 'taming',
  1745. menu,
  1746. tier
  1747. };
  1748. } else if(url.includes('/skill/') && url.includes('/action/')) {
  1749. result = {
  1750. type: 'action',
  1751. skill: +parts[parts.length-3],
  1752. action: +parts[parts.length-1]
  1753. };
  1754. } else if(url.includes('house/build')) {
  1755. result = {
  1756. type: 'structure',
  1757. structure: +parts[parts.length-1]
  1758. };
  1759. } else if(url.includes('house/enhance')) {
  1760. result = {
  1761. type: 'enhancement',
  1762. structure: +parts[parts.length-1]
  1763. };
  1764. } else if(url.includes('house/produce')) {
  1765. result = {
  1766. type: 'automation',
  1767. structure: +parts[parts.length-2],
  1768. action: +parts[parts.length-1]
  1769. };
  1770. } else {
  1771. result = {
  1772. type: parts.pop()
  1773. };
  1774. }
  1775. emitEvent(result);
  1776. }
  1777.  
  1778. initialise();
  1779.  
  1780. }
  1781. );
  1782. // pages
  1783. window.moduleRegistry.add('pages', (elementWatcher, events, colorMapper, util, skillCache, elementCreator) => {
  1784.  
  1785. const registerPageHandler = events.register.bind(null, 'page');
  1786. const getLastPage = events.getLast.bind(null, 'page');
  1787.  
  1788. const exports = {
  1789. register,
  1790. requestRender,
  1791. show,
  1792. hide,
  1793. open: visitPage
  1794. }
  1795.  
  1796. const pages = [];
  1797.  
  1798. function initialise() {
  1799. registerPageHandler(handlePage);
  1800. elementCreator.addStyles(styles);
  1801. }
  1802.  
  1803. function handlePage(page) {
  1804. // handle navigating away
  1805. if(!pages.some(p => p.path === page.type)) {
  1806. $('custom-page').remove();
  1807. $('nav-component > div.nav > div.scroll > button')
  1808. .removeClass('customActiveLink');
  1809. $('header-component div.wrapper > div.image > img')
  1810. .css('image-rendering', '');
  1811. headerPageNameChangeBugFix(page);
  1812. }
  1813. }
  1814.  
  1815. async function register(page) {
  1816. if(pages.some(p => p.name === page.name)) {
  1817. console.error(`Custom page already registered : ${page.name}`);
  1818. return;
  1819. }
  1820. page.path = page.name.toLowerCase().replaceAll(' ', '-');
  1821. page.class = `customMenuButton_${page.path}`;
  1822. page.image = page.image || 'https://ironwoodrpg.com/assets/misc/settings.png';
  1823. page.category = page.category?.toUpperCase() || 'MISC';
  1824. page.columns = page.columns || 1;
  1825. pages.push(page);
  1826. console.debug('Registered pages', pages);
  1827. await setupNavigation(page);
  1828. }
  1829.  
  1830. function show(name) {
  1831. const page = pages.find(p => p.name === name)
  1832. if(!page) {
  1833. console.error(`Could not find page : ${name}`);
  1834. return;
  1835. }
  1836. $(`.${page.class}`).show();
  1837. }
  1838.  
  1839. function hide(name) {
  1840. const page = pages.find(p => p.name === name)
  1841. if(!page) {
  1842. console.error(`Could not find page : ${name}`);
  1843. return;
  1844. }
  1845. $(`.${page.class}`).hide();
  1846. }
  1847.  
  1848. function requestRender(name) {
  1849. const page = pages.find(p => p.name === name)
  1850. if(!page) {
  1851. console.error(`Could not find page : ${name}`);
  1852. return;
  1853. }
  1854. if(getLastPage()?.type === page.path) {
  1855. render(page);
  1856. }
  1857. }
  1858.  
  1859. function render(page) {
  1860. $('.customComponent').remove();
  1861. page.render();
  1862. }
  1863.  
  1864. async function setupNavigation(page) {
  1865. await elementWatcher.exists('div.nav > div.scroll');
  1866. // MENU HEADER / CATEGORY
  1867. let menuHeader = $(`nav-component > div.nav > div.scroll > div.header:contains('${page.category}'), div.customMenuHeader:contains('${page.category}')`);
  1868. if(!menuHeader.length) {
  1869. menuHeader = createMenuHeader(page.category);
  1870. }
  1871. // MENU BUTTON / PAGE LINK
  1872. const menuButton = createMenuButton(page)
  1873. // POSITIONING
  1874. if(page.after) {
  1875. $(`nav-component button:contains('${page.after}')`).after(menuButton);
  1876. } else {
  1877. menuHeader.after(menuButton);
  1878. }
  1879. }
  1880.  
  1881. function createMenuHeader(text) {
  1882. const menuHeader =
  1883. $('<div/>')
  1884. .addClass('header customMenuHeader')
  1885. .append(
  1886. $('<div/>')
  1887. .addClass('customMenuHeaderText')
  1888. .text(text)
  1889. );
  1890. $('nav-component > div.nav > div.scroll')
  1891. .prepend(menuHeader);
  1892. return menuHeader;
  1893. }
  1894.  
  1895. function createMenuButton(page) {
  1896. const menuButton =
  1897. $('<button/>')
  1898. .attr('type', 'button')
  1899. .addClass(`customMenuButton ${page.class}`)
  1900. .css('display', 'none')
  1901. .click(() => visitPage(page.name))
  1902. .append(
  1903. $('<img/>')
  1904. .addClass('customMenuButtonImage')
  1905. .attr('src', page.image)
  1906. .css('image-rendering', page.imagePixelated ? 'pixelated' : 'auto')
  1907. )
  1908. .append(
  1909. $('<div/>')
  1910. .addClass('customMenuButtonText')
  1911. .text(page.name)
  1912. );
  1913. return menuButton;
  1914. }
  1915.  
  1916. async function visitPage(name) {
  1917. const page = pages.find(p => p.name === name);
  1918. if($('custom-page').length) {
  1919. $('custom-page').remove();
  1920. } else {
  1921. await setupEmptyPage();
  1922. }
  1923. createPage(page.columns);
  1924. updatePageHeader(page);
  1925. updateActivePageInNav(page.name);
  1926. history.pushState({}, '', page.path);
  1927. page.render();
  1928. }
  1929.  
  1930. async function setupEmptyPage() {
  1931. util.goToPage('settings');
  1932. await elementWatcher.exists('settings-page');
  1933. $('settings-page').remove();
  1934. }
  1935.  
  1936. function createPage(columnCount) {
  1937. const custompage = $('<custom-page/>');
  1938. const columns = $('<div/>')
  1939. .addClass('customGroups');
  1940. for(let columnIndex = 0; columnIndex < columnCount; columnIndex++) {
  1941. columns.append(
  1942. $('<div/>')
  1943. .addClass('customGroup')
  1944. .addClass(`column${columnIndex}`)
  1945. )
  1946. };
  1947. custompage.append(columns);
  1948. $('div.padding > div.wrapper > router-outlet').after(custompage);
  1949. }
  1950.  
  1951. function updatePageHeader(page) {
  1952. $('header-component div.wrapper > div.image > img')
  1953. .attr('src', page.image)
  1954. .css('image-rendering', page.imagePixelated ? 'pixelated' : 'auto');
  1955. $('header-component div.wrapper > div.title').text(page.name);
  1956. }
  1957.  
  1958. function updateActivePageInNav(name) {
  1959. //Set other pages as inactive
  1960. $(`nav-component > div.nav > div.scroll > button`)
  1961. .removeClass('active-link')
  1962. .removeClass('customActiveLink');
  1963. //Set this page as active
  1964. $(`nav-component > div.nav > div.scroll > button > div.customMenuButtonText:contains('${name}')`)
  1965. .parent()
  1966. .addClass('customActiveLink');
  1967. }
  1968.  
  1969. // hacky shit, idk why angular stops updating page header title ???
  1970. async function headerPageNameChangeBugFix(page) {
  1971. await elementWatcher.exists('nav-component > div.nav');
  1972. let headerName = null;
  1973. if(page.type === 'action') {
  1974. headerName = skillCache.byId[page.skill].displayName;
  1975. } else if(page.type === 'structure') {
  1976. headerName = 'House';
  1977. } else if(page.type === 'enhancement') {
  1978. headerName = 'House';
  1979. } else if(page.type === 'automation') {
  1980. headerName = 'House';
  1981. } else if(page.type === 'taming') {
  1982. headerName = 'Taming';
  1983. } else {
  1984. headerName = page.type;
  1985. headerName = headerName.charAt(0).toUpperCase() + headerName.slice(1);
  1986. }
  1987. $('header-component div.wrapper > div.title').text(headerName);
  1988. }
  1989.  
  1990. const styles = `
  1991. :root {
  1992. --background-color: ${colorMapper('componentRegular')};
  1993. --border-color: ${colorMapper('componentLight')};
  1994. --darker-color: ${colorMapper('componentDark')};
  1995. }
  1996. .customMenuHeader {
  1997. height: 56px;
  1998. display: flex;
  1999. align-items: center;
  2000. padding: 0 24px;
  2001. color: #aaa;
  2002. font-size: .875rem;
  2003. font-weight: 600;
  2004. letter-spacing: 1px;
  2005. text-transform: uppercase;
  2006. border-bottom: 1px solid var(--border-color);
  2007. background-color: var(--background-color);
  2008. }
  2009. .customMenuHeaderText {
  2010. flex: 1;
  2011. }
  2012. .customMenuButton {
  2013. border: none;
  2014. background: transparent;
  2015. font-family: inherit;
  2016. font-size: inherit;
  2017. line-height: 1.5;
  2018. font-weight: inherit;
  2019. color: inherit;
  2020. resize: none;
  2021. text-transform: inherit;
  2022. letter-spacing: inherit;
  2023. cursor: pointer;
  2024. height: 56px;
  2025. display: flex;
  2026. align-items: center;
  2027. padding: 0 24px;
  2028. border-bottom: 1px solid var(--border-color);
  2029. width: 100%;
  2030. text-align: left;
  2031. position: relative;
  2032. background-color: var(--background-color);
  2033. }
  2034. .customMenuButtonImage {
  2035. max-width: 100%;
  2036. max-height: 100%;
  2037. height: 32px;
  2038. width: 32px;
  2039. }
  2040. .customMenuButtonText {
  2041. margin-left: var(--margin);
  2042. flex: 1;
  2043. }
  2044. .customGroups {
  2045. display: flex;
  2046. gap: var(--gap);
  2047. flex-wrap: wrap;
  2048. }
  2049. .customGroup {
  2050. flex: 1;
  2051. min-width: 360px;
  2052. }
  2053. .customActiveLink {
  2054. background-color: var(--darker-color);
  2055. }
  2056. `;
  2057.  
  2058. initialise();
  2059.  
  2060. return exports
  2061. }
  2062. );
  2063. // petUtil
  2064. window.moduleRegistry.add('petUtil', (petCache, petTraitCache, petPassiveCache, expeditionCache, util) => {
  2065.  
  2066. const STATS_BASE = ['health', 'speed', 'attack', 'specialAttack', 'defense', 'specialDefense'];
  2067. const STATS_SPECIAL = ['hunger', 'stealth', 'loot'];
  2068. const STATS_ABILITIES = ['bones', 'fish', 'flowers', 'ore', 'veges', 'wood'];
  2069. const IMAGES = {
  2070. health: 'https://cdn-icons-png.flaticon.com/512/2589/2589054.png',
  2071. speed: 'https://img.icons8.com/?size=48&id=TE1T4XfT3xeN',
  2072. attack: 'https://cdn-icons-png.flaticon.com/512/9743/9743017.png',
  2073. defense: 'https://cdn-icons-png.flaticon.com/512/2592/2592488.png',
  2074. specialAttack: 'https://img.icons8.com/?size=48&id=18515',
  2075. specialDefense: 'https://img.icons8.com/?size=48&id=CWksSHWEtOtX',
  2076. hunger: 'https://img.icons8.com/?size=48&id=AXExnoyylJdK',
  2077. stealth: 'https://img.icons8.com/?size=48&id=4GYmMTXrMp8g',
  2078. loot: 'https://img.icons8.com/?size=48&id=M2yQkpBAlIS8'
  2079. };
  2080. const exports = {
  2081. STATS_BASE,
  2082. STATS_SPECIAL,
  2083. IMAGES,
  2084. petToText,
  2085. textToPet,
  2086. isEncodedPetName,
  2087. petToStats,
  2088. getExpeditionStats
  2089. };
  2090.  
  2091. const SPECIAL_CHAR = '_';
  2092. const VALID_CHARS = '!"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_abcdefghijklmnopqrstuvwxyz{|}'.split('');
  2093. const VALID_CHARS_LENGTH = BigInt(VALID_CHARS.length);
  2094. const OPTIONS = [
  2095. petCache.list.length, // species
  2096. petTraitCache.list.length, // traits
  2097. ...Array(6).fill(50), // stats
  2098. ...Array(4).fill(petPassiveCache.list.length+1) // passives, 0 = empty
  2099. ];
  2100.  
  2101. const MILLIS_PER_MINUTE = 1000*60;
  2102. const MILLIS_PER_WEEK = 1000*60*60*24*7;
  2103.  
  2104. function numberToText(number) {
  2105. let text = SPECIAL_CHAR;
  2106. while(number > 0) {
  2107. text += VALID_CHARS[number%VALID_CHARS_LENGTH];
  2108. number /= VALID_CHARS_LENGTH;
  2109. }
  2110. return text;
  2111. }
  2112.  
  2113. function textToNumber(text) {
  2114. let number = 0n;
  2115. text = text.slice(1);
  2116. while(text.length) {
  2117. number *= VALID_CHARS_LENGTH;
  2118. number += BigInt(VALID_CHARS.indexOf(text[text.length-1]));
  2119. text = text.slice(0,-1);
  2120. }
  2121. return number;
  2122. }
  2123.  
  2124. function choicesToNumber(choices, options) {
  2125. if(choices.length !== options.length) {
  2126. throw `Expected lengths to be equal : ${choices.length} and ${options.length}`;
  2127. }
  2128. let number = 0n;
  2129. for(let i=0;i<choices.length;i++) {
  2130. if(choices[i] >= options[i]) {
  2131. throw `${choices[i]} is outside of options range ${options[i]}`;
  2132. }
  2133. number *= BigInt(options[i]);
  2134. number += BigInt(choices[i]);
  2135. }
  2136. return number;
  2137. }
  2138.  
  2139. function numberToChoices(number, options) {
  2140. const choices = [];
  2141. for(let i=options.length-1;i>=0;i--) {
  2142. if(i > 0) {
  2143. choices.unshift(Number(number % BigInt(options[i])));
  2144. number /= BigInt(options[i]);
  2145. } else {
  2146. choices.unshift(Number(number));
  2147. }
  2148. }
  2149. return choices;
  2150. }
  2151.  
  2152. function petToChoices(pet) {
  2153. const passives = pet.passives.map(a => petPassiveCache.idToIndex[a]+1);
  2154. while(passives.length < 4) {
  2155. passives.push(0);
  2156. }
  2157. return [
  2158. petCache.idToIndex[pet.species], // species
  2159. petTraitCache.idToIndex[pet.traits], // traits
  2160. pet.health/2-1,
  2161. pet.attack/2-1,
  2162. pet.defense/2-1,
  2163. pet.specialAttack/2-1,
  2164. pet.specialDefense/2-1,
  2165. pet.speed/2-1, // stats
  2166. ...passives // passives, 0 = empty
  2167. ];
  2168. }
  2169.  
  2170. function choicesToPet(choices, text) {
  2171. return {
  2172. parsed: true,
  2173. species: petCache.list[choices[0]].id,
  2174. name: text,
  2175. traits: petTraitCache.list[choices[1]].id,
  2176. health: (choices[2]+1)*2,
  2177. attack: (choices[3]+1)*2,
  2178. defense: (choices[4]+1)*2,
  2179. specialAttack: (choices[5]+1)*2,
  2180. specialDefense: (choices[6]+1)*2,
  2181. speed: (choices[7]+1)*2,
  2182. passives: choices.slice(8).filter(a => a).map(a => petPassiveCache.list[a-1].id)
  2183. };
  2184. }
  2185.  
  2186. function petToText(pet) {
  2187. const choices = petToChoices(pet);
  2188. const number = choicesToNumber(choices, OPTIONS);
  2189. return numberToText(number);
  2190. }
  2191.  
  2192. function textToPet(text) {
  2193. const number = textToNumber(text);
  2194. const choices = numberToChoices(number, OPTIONS);
  2195. return choicesToPet(choices, text);
  2196. }
  2197.  
  2198. function isEncodedPetName(text) {
  2199. return text.startsWith(SPECIAL_CHAR);
  2200. }
  2201.  
  2202. function petToStats(pet) {
  2203. const result = {};
  2204. const passives = pet.passives.map(id => petPassiveCache.byId[id]);
  2205. const traits = petTraitCache.byId[pet.traits];
  2206. for(const stat of STATS_BASE) {
  2207. result[stat] = 0;
  2208. let value = (petCache.byId[pet.species].power + pet[stat] / 2 - 10) / 100 * pet.level + 10;
  2209. value *= traits[stat] ? 1.25 : 1;
  2210. const passive = passives.find(a => a.stats.name === stat + 'Percent');
  2211. if(passive) {
  2212. value *= 1 + passive.stats.value / 100;
  2213. }
  2214. result[stat] += value;
  2215. }
  2216. for(const stat of STATS_SPECIAL) {
  2217. result[stat] = 0;
  2218. const passive = passives.find(a => a.stats.name === stat + 'Percent');
  2219. if(passive) {
  2220. result[stat] += passive.stats.value;
  2221. }
  2222. }
  2223. for(const ability of STATS_ABILITIES) {
  2224. result[ability] = 0;
  2225. }
  2226. const abilities = petCache.byId[pet.species].abilities;
  2227. for(const ability of abilities) {
  2228. const key = Object.keys(ability)[0];
  2229. result[key] = ability[key];
  2230. }
  2231. for(const key of Object.keys(result)) {
  2232. result[key] = Math.round(result[key]);
  2233. }
  2234. return result;
  2235. }
  2236.  
  2237. function getExpeditionStats(tier) {
  2238. const expedition = expeditionCache.byTier[tier];
  2239. const rotation = getCurrentRotation(expedition.tier);
  2240. const stats = {};
  2241. for(const stat of STATS_BASE) {
  2242. stats[stat] = expedition.power;
  2243. if(rotation[stat]) {
  2244. stats[stat] *= 1.25;
  2245. }
  2246. }
  2247. return Object.assign({rotation,stats}, expedition);
  2248. }
  2249.  
  2250. function getCurrentRotation(offset) {
  2251. const now = new Date();
  2252. const date = new Date(now.getTime() + MILLIS_PER_MINUTE * now.getTimezoneOffset());
  2253. const millisPassed = util.startOfWeek(date) - util.startOfWeek(util.startOfYear(date));
  2254. const startOfWeek = util.startOfWeek(date);
  2255. let index = 2 + offset + Math.round(millisPassed / MILLIS_PER_WEEK);
  2256. index %= petTraitCache.list.length;
  2257. return petTraitCache.byId[index];
  2258. }
  2259.  
  2260. return exports;
  2261.  
  2262. });
  2263. // polyfill
  2264. window.moduleRegistry.add('polyfill', () => {
  2265.  
  2266. const exports = {
  2267. requestIdleCallback
  2268. };
  2269.  
  2270. function requestIdleCallback() {
  2271. if(!window.requestIdleCallback) {
  2272. window.requestIdleCallback = function(callback, options) {
  2273. var options = options || {};
  2274. var relaxation = 1;
  2275. var timeout = options.timeout || relaxation;
  2276. var start = performance.now();
  2277. return setTimeout(function () {
  2278. callback({
  2279. get didTimeout() {
  2280. return options.timeout ? false : (performance.now() - start) - relaxation > timeout;
  2281. },
  2282. timeRemaining: function () {
  2283. return Math.max(0, relaxation + (performance.now() - start));
  2284. },
  2285. });
  2286. }, relaxation);
  2287. };
  2288. }
  2289. return window.requestIdleCallback(...arguments);
  2290. }
  2291.  
  2292. return exports;
  2293.  
  2294. }
  2295. );
  2296. // Promise
  2297. window.moduleRegistry.add('Promise', (logService) => {
  2298.  
  2299. class Deferred {
  2300. #name;
  2301. #promise;
  2302. resolve;
  2303. reject;
  2304. constructor(name) {
  2305. this.#name = name;
  2306. this.#promise = new Promise((resolve, reject) => {
  2307. this.resolve = resolve;
  2308. this.reject = reject;
  2309. }).catch(error => {
  2310. if(error) {
  2311. console.warn(error);
  2312. logService.error(`error in ${this.constructor.name} (${this.#name})`, error);
  2313. }
  2314. throw error;
  2315. });
  2316. }
  2317.  
  2318. then() {
  2319. this.#promise.then.apply(this.#promise, arguments);
  2320. return this;
  2321. }
  2322.  
  2323. catch() {
  2324. this.#promise.catch.apply(this.#promise, arguments);
  2325. return this;
  2326. }
  2327.  
  2328. finally() {
  2329. this.#promise.finally.apply(this.#promise, arguments);
  2330. return this;
  2331. }
  2332. }
  2333.  
  2334. class Delayed extends Deferred {
  2335. constructor(timeout, name) {
  2336. super(name);
  2337. const timeoutReference = window.setTimeout(() => {
  2338. this.resolve();
  2339. }, timeout);
  2340. this.finally(() => {
  2341. window.clearTimeout(timeoutReference)
  2342. });
  2343. }
  2344. }
  2345.  
  2346. class Expiring extends Deferred {
  2347. constructor(timeout, name) {
  2348. super(name);
  2349. if(timeout <= 0) {
  2350. return;
  2351. }
  2352. const timeoutReference = window.setTimeout(() => {
  2353. this.reject(`Timed out after ${timeout} ms`);
  2354. }, timeout);
  2355. this.finally(() => {
  2356. window.clearTimeout(timeoutReference)
  2357. });
  2358. }
  2359. }
  2360.  
  2361. class Checking extends Expiring {
  2362. #checker;
  2363. constructor(checker, interval, timeout, name) {
  2364. super(timeout, name);
  2365. this.#checker = checker;
  2366. this.#check();
  2367. const intervalReference = window.setInterval(this.#check.bind(this), interval);
  2368. this.finally(() => {
  2369. window.clearInterval(intervalReference)
  2370. });
  2371. }
  2372. #check() {
  2373. const checkResult = this.#checker();
  2374. if(!checkResult) {
  2375. return;
  2376. }
  2377. this.resolve(checkResult);
  2378. }
  2379. }
  2380.  
  2381. return {
  2382. Deferred,
  2383. Delayed,
  2384. Expiring,
  2385. Checking
  2386. };
  2387.  
  2388. }
  2389. );
  2390. // request
  2391. window.moduleRegistry.add('request', () => {
  2392.  
  2393. async function request(url, body, headers) {
  2394. if(!headers) {
  2395. headers = {};
  2396. }
  2397. headers['Content-Type'] = 'application/json';
  2398. const method = body ? 'POST' : 'GET';
  2399. try {
  2400. if(body) {
  2401. body = JSON.stringify(body);
  2402. }
  2403. const fetchResponse = await fetch(`${window.PANCAKE_ROOT}/${url}`, {method, headers, body});
  2404. if(fetchResponse.status !== 200) {
  2405. console.error(await fetchResponse.text());
  2406. console.log('response', fetchResponse);
  2407. throw fetchResponse;
  2408. }
  2409. try {
  2410. const contentType = fetchResponse.headers.get('Content-Type');
  2411. if(contentType.startsWith('text/plain')) {
  2412. return await fetchResponse.text();
  2413. } else if(contentType.startsWith('application/json')) {
  2414. return await fetchResponse.json();
  2415. } else {
  2416. console.error(`Unknown content type : ${contentType}`);
  2417. }
  2418. } catch(e) {
  2419. if(body) {
  2420. return 'OK';
  2421. }
  2422. }
  2423. } catch(e) {
  2424. console.log('error', e);
  2425. throw `Failed fetching ${url} : ${e}`;
  2426. }
  2427. }
  2428.  
  2429. // alphabetical
  2430.  
  2431. request.listActions = () => request('public/list/action');
  2432. request.listDrops = () => request('public/list/drop');
  2433. request.listExpeditions = () => request('public/list/expedition');
  2434. request.listExpeditionDrops = () => request('public/list/expeditionDrop');
  2435. request.listItems = () => request('public/list/item');
  2436. request.listItemAttributes = () => request('public/list/itemAttribute');
  2437. request.listIngredients = () => request('public/list/ingredient');
  2438. request.listMonsters = () => request('public/list/monster');
  2439. request.listPets = () => request('public/list/pet');
  2440. request.listPetPassives = () => request('public/list/petPassive');
  2441. request.listRecipes = () => request('public/list/recipe');
  2442. request.listSkills = () => request('public/list/skill');
  2443. request.listStructures = () => request('public/list/structure');
  2444.  
  2445. request.report = (data) => request('public/report', data);
  2446.  
  2447. request.getChangelogs = () => request('public/settings/changelog');
  2448. request.getVersion = () => request('public/settings/version');
  2449.  
  2450. return request;
  2451.  
  2452. }
  2453. );
  2454. // toast
  2455. window.moduleRegistry.add('toast', (util, elementCreator) => {
  2456.  
  2457. const exports = {
  2458. create,
  2459. copyToClipboard
  2460. };
  2461.  
  2462. function initialise() {
  2463. elementCreator.addStyles(styles);
  2464. }
  2465.  
  2466. // text, time, image
  2467. async function create(config) {
  2468. config.time ||= 2000;
  2469. config.image ||= 'https://ironwoodrpg.com/assets/misc/quests.png';
  2470. const notificationId = `customNotification_${Math.floor(Date.now() * Math.random())}`
  2471. const notificationDiv =
  2472. $('<div/>')
  2473. .addClass('customNotification')
  2474. .attr('id', notificationId)
  2475. .append(
  2476. $('<div/>')
  2477. .addClass('customNotificationImageDiv')
  2478. .append(
  2479. $('<img/>')
  2480. .addClass('customNotificationImage')
  2481. .attr('src', config.image)
  2482. )
  2483. )
  2484. .append(
  2485. $('<div/>')
  2486. .addClass('customNotificationDetails')
  2487. .html(config.text)
  2488. );
  2489. $('div.notifications').append(notificationDiv);
  2490. await util.sleep(config.time);
  2491. $(`#${notificationId}`).fadeOut('slow', () => {
  2492. $(`#${notificationId}`).remove();
  2493. });
  2494. }
  2495.  
  2496. function copyToClipboard(text, message) {
  2497. navigator.clipboard.writeText(text);
  2498. create({
  2499. text: message,
  2500. image: 'https://img.icons8.com/?size=48&id=22244'
  2501. });
  2502. }
  2503.  
  2504. const styles = `
  2505. .customNotification {
  2506. padding: 8px 16px 8px 12px;
  2507. border-radius: 4px;
  2508. backdrop-filter: blur(8px);
  2509. background: rgba(255,255,255,.15);
  2510. box-shadow: 0 8px 16px -4px #00000080;
  2511. display: flex;
  2512. align-items: center;
  2513. min-height: 48px;
  2514. margin-top: 12px;
  2515. pointer-events: all;
  2516. }
  2517. .customNotificationImageDiv {
  2518. display: flex;
  2519. align-items: center;
  2520. justify-content: center;
  2521. width: 32px;
  2522. height: 32px;
  2523. }
  2524. .customNotificationImage {
  2525. filter: drop-shadow(0px 8px 4px rgba(0,0,0,.1));
  2526. image-rendering: auto;
  2527. }
  2528. .customNotificationDetails {
  2529. margin-left: 8px;
  2530. text-align: center;
  2531. }
  2532. `;
  2533.  
  2534. initialise();
  2535.  
  2536. return exports;
  2537. }
  2538. );
  2539. // util
  2540. window.moduleRegistry.add('util', () => {
  2541.  
  2542. const exports = {
  2543. levelToExp,
  2544. expToLevel,
  2545. expToCurrentExp,
  2546. expToNextLevel,
  2547. expToNextTier,
  2548. tierToLevel,
  2549. levelToTier,
  2550. formatNumber,
  2551. parseNumber,
  2552. secondsToDuration,
  2553. parseDuration,
  2554. divmod,
  2555. sleep,
  2556. goToPage,
  2557. compareObjects,
  2558. debounce,
  2559. distinct,
  2560. getDuplicates,
  2561. sumObjects,
  2562. startOfWeek,
  2563. startOfYear,
  2564. generateCombinations
  2565. };
  2566.  
  2567. function levelToExp(level) {
  2568. if(level === 1) {
  2569. return 0;
  2570. }
  2571. if(level <= 100) {
  2572. return Math.floor(Math.pow(level, 3.5) * 6 / 5);
  2573. }
  2574. return Math.round(12_000_000 * Math.pow(Math.pow(3500, .01), level - 100));
  2575. }
  2576.  
  2577. function expToLevel(exp) {
  2578. if(exp <= 0) {
  2579. return 1;
  2580. }
  2581. if(exp <= 12_000_000) {
  2582. return Math.floor(Math.pow((exp + 1) / 1.2, 1 / 3.5));
  2583. }
  2584. return 100 + Math.floor(Math.log((exp + 1) / 12_000_000) / Math.log(Math.pow(3500, .01)));
  2585. }
  2586.  
  2587. function expToCurrentExp(exp) {
  2588. const level = expToLevel(exp);
  2589. return exp - levelToExp(level);
  2590. }
  2591.  
  2592. function expToNextLevel(exp) {
  2593. const level = expToLevel(exp);
  2594. return levelToExp(level + 1) - exp;
  2595. }
  2596.  
  2597. function expToNextTier(exp) {
  2598. const level = expToLevel(exp);
  2599. let target = 10;
  2600. while(target <= level) {
  2601. target += 15;
  2602. }
  2603. return levelToExp(target) - exp;
  2604. }
  2605.  
  2606. function tierToLevel(tier) {
  2607. if(tier <= 1) {
  2608. return tier;
  2609. }
  2610. return tier * 15 - 20;
  2611. }
  2612.  
  2613. function levelToTier(level) {
  2614. if(level <= 1) {
  2615. return level;
  2616. }
  2617. return (level + 20) / 15;
  2618. }
  2619.  
  2620. function formatNumber(number) {
  2621. let digits = 2;
  2622. if(number < .1 && number > -.1) {
  2623. digits = 3;
  2624. }
  2625. if(number < .01 && number > -.01) {
  2626. digits = 4;
  2627. }
  2628. return number.toLocaleString(undefined, {maximumFractionDigits:digits});
  2629. }
  2630.  
  2631. function parseNumber(text) {
  2632. if(!text) {
  2633. return 0;
  2634. }
  2635. if(text.includes('Empty')) {
  2636. return 0;
  2637. }
  2638. const regexMatch = /\d+[^\s]*/.exec(text);
  2639. if(!regexMatch) {
  2640. return 0;
  2641. }
  2642. text = regexMatch[0];
  2643. text = text.replaceAll(/,/g, '');
  2644. text = text.replaceAll(/&.*$/g, '');
  2645. let multiplier = 1;
  2646. if(text.endsWith('%')) {
  2647. multiplier = 1 / 100;
  2648. }
  2649. if(text.endsWith('K')) {
  2650. multiplier = 1_000;
  2651. }
  2652. if(text.endsWith('M')) {
  2653. multiplier = 1_000_000;
  2654. }
  2655. return (parseFloat(text) || 0) * multiplier;
  2656. }
  2657.  
  2658. function secondsToDuration(seconds) {
  2659. seconds = Math.floor(seconds);
  2660. if(seconds > 60 * 60 * 24 * 100) {
  2661. // > 100 days
  2662. return 'A very long time';
  2663. }
  2664.  
  2665. var [minutes, seconds] = divmod(seconds, 60);
  2666. var [hours, minutes] = divmod(minutes, 60);
  2667. var [days, hours] = divmod(hours, 24);
  2668.  
  2669. seconds = `${seconds}`.padStart(2, '0');
  2670. minutes = `${minutes}`.padStart(2, '0');
  2671. hours = `${hours}`.padStart(2, '0');
  2672. days = `${days}`.padStart(2, '0');
  2673.  
  2674. let result = '';
  2675. if(result || +days) {
  2676. result += `${days}d `;
  2677. }
  2678. if(result || +hours) {
  2679. result += `${hours}h `;
  2680. }
  2681. if(result || +minutes) {
  2682. result += `${minutes}m `;
  2683. }
  2684. result += `${seconds}s`;
  2685.  
  2686. return result;
  2687. }
  2688.  
  2689. function parseDuration(duration) {
  2690. const parts = duration.split(' ');
  2691. let seconds = 0;
  2692. for(const part of parts) {
  2693. const value = parseFloat(part);
  2694. if(part.endsWith('m')) {
  2695. seconds += value * 60;
  2696. } else if(part.endsWith('h')) {
  2697. seconds += value * 60 * 60;
  2698. } else if(part.endsWith('d')) {
  2699. seconds += value * 60 * 60 * 24;
  2700. } else {
  2701. console.warn(`Unexpected duration being parsed : ${part}`);
  2702. }
  2703. }
  2704. return seconds;
  2705. }
  2706.  
  2707. function divmod(x, y) {
  2708. return [Math.floor(x / y), x % y];
  2709. }
  2710.  
  2711. function goToPage(page) {
  2712. window.history.pushState({}, '', page);
  2713. window.history.pushState({}, '', page);
  2714. window.history.back();
  2715. }
  2716.  
  2717. async function sleep(millis) {
  2718. await new Promise(r => window.setTimeout(r, millis));
  2719. }
  2720.  
  2721. function compareObjects(object1, object2, doLog) {
  2722. const keys1 = Object.keys(object1);
  2723. const keys2 = Object.keys(object2);
  2724. if(keys1.length !== keys2.length) {
  2725. if(doLog) {
  2726. console.warn(`key length not matching`, object1, object2);
  2727. }
  2728. return false;
  2729. }
  2730. keys1.sort();
  2731. keys2.sort();
  2732. for(let i=0;i<keys1.length;i++) {
  2733. if(keys1[i] !== keys2[i]) {
  2734. if(doLog) {
  2735. console.warn(`keys not matching`, keys1[i], keys2[i], object1, object2);
  2736. }
  2737. return false;
  2738. }
  2739. if(typeof object1[keys1[i]] === 'object' && typeof object2[keys2[i]] === 'object') {
  2740. if(!compareObjects(object1[keys1[i]], object2[keys2[i]], doLog)) {
  2741. return false;
  2742. }
  2743. } else if(object1[keys1[i]] !== object2[keys2[i]]) {
  2744. if(doLog) {
  2745. console.warn(`values not matching`, object1[keys1[i]], object2[keys2[i]], object1, object2);
  2746. }
  2747. return false;
  2748. }
  2749. }
  2750. return true;
  2751. }
  2752.  
  2753. function debounce(callback, delay) {
  2754. let timer;
  2755. return function(...args) {
  2756. clearTimeout(timer);
  2757. timer = setTimeout(() => {
  2758. callback(...args);
  2759. }, delay);
  2760. }
  2761. }
  2762.  
  2763. function distinct(array) {
  2764. return array.filter((value, index) => {
  2765. return array.indexOf(value) === index;
  2766. });
  2767. }
  2768.  
  2769. function getDuplicates(array) {
  2770. const sorted = array.slice().sort();
  2771. const result = [];
  2772. for(let i=0;i<sorted.length-1;i++) {
  2773. if(sorted[i+1] == sorted[i]) {
  2774. result.push(sorted[i]);
  2775. }
  2776. }
  2777. return result;
  2778. }
  2779.  
  2780. function sumObjects(array) {
  2781. const result = {};
  2782. for(const element of array) {
  2783. for(const key of Object.keys(element)) {
  2784. if(typeof element[key] === 'number') {
  2785. result[key] = (result[key] || 0) + element[key];
  2786. }
  2787. }
  2788. }
  2789. return result;
  2790. }
  2791.  
  2792. function startOfWeek(date) {
  2793. const result = new Date();
  2794. result.setDate(date.getDate() - date.getDay());
  2795. result.setHours(0,0,0,0);
  2796. return result;
  2797. }
  2798.  
  2799. function startOfYear(date) {
  2800. const result = new Date(date.getFullYear(), 0, 1);
  2801. return result;
  2802. }
  2803.  
  2804. function generateCombinations(objects, count, grouper) {
  2805. const objectsByGroup = {};
  2806. for(const object of objects) {
  2807. const group = grouper(object);
  2808. if(!objectsByGroup[group]) {
  2809. objectsByGroup[group] = [];
  2810. }
  2811. objectsByGroup[group].push(object);
  2812. }
  2813. const result = [];
  2814. const groups = Object.keys(objectsByGroup);
  2815. addOneCombination(result, objectsByGroup, groups, count);
  2816. return result;
  2817. }
  2818.  
  2819. function addOneCombination(result, objectsByGroup, groups, count, combination = [], groupStart = 0) {
  2820. if(!count) {
  2821. result.push(combination);
  2822. return;
  2823. }
  2824. for(let i=groupStart;i<groups.length-count+1;i++) {
  2825. const contents = objectsByGroup[groups[i]];
  2826. for(let j=0;j<contents.length;j++) {
  2827. addOneCombination(result, objectsByGroup, groups, count-1, combination.concat([contents[j]]), i+1);
  2828. }
  2829. }
  2830. }
  2831.  
  2832. return exports;
  2833.  
  2834. }
  2835. );
  2836. // enhancementsReader
  2837. window.moduleRegistry.add('enhancementsReader', (events, util, structuresCache) => {
  2838.  
  2839. const emitEvent = events.emit.bind(null, 'reader-enhancements');
  2840.  
  2841. function initialise() {
  2842. events.register('page', update);
  2843. window.setInterval(update, 1000);
  2844. }
  2845.  
  2846. function update() {
  2847. const page = events.getLast('page');
  2848. if(!page) {
  2849. return;
  2850. }
  2851. if(page.type === 'enhancement' && $('home-page .categories .category-active').text() === 'Enhance') {
  2852. readEnhancementsScreen();
  2853. }
  2854. }
  2855.  
  2856. function readEnhancementsScreen() {
  2857. const enhancements = {};
  2858. $('home-page .categories + .card button').each((i,element) => {
  2859. element = $(element);
  2860. const name = element.find('.name').text();
  2861. const structure = structuresCache.byName[name];
  2862. if(!structure) {
  2863. return;
  2864. }
  2865. const level = util.parseNumber(element.find('.level').text());
  2866. enhancements[structure.id] = level;
  2867. });
  2868. emitEvent({
  2869. type: 'full',
  2870. value: enhancements
  2871. });
  2872. }
  2873.  
  2874. initialise();
  2875.  
  2876. }
  2877. );
  2878. // equipmentReader
  2879. window.moduleRegistry.add('equipmentReader', (events, itemCache, util, itemUtil) => {
  2880.  
  2881. function initialise() {
  2882. events.register('page', update);
  2883. window.setInterval(update, 1000);
  2884. }
  2885.  
  2886. function update() {
  2887. const page = events.getLast('page');
  2888. if(!page) {
  2889. return;
  2890. }
  2891. if(page.type === 'equipment') {
  2892. readEquipmentScreen();
  2893. }
  2894. if(page.type === 'action') {
  2895. readActionScreen();
  2896. }
  2897. }
  2898.  
  2899. function readEquipmentScreen() {
  2900. const equipment = {};
  2901. const activeTab = $('equipment-page .categories button[disabled]').text().toLowerCase();
  2902. $('equipment-page .header + .items > .item > .description').parent().each((i,element) => {
  2903. itemUtil.extractItem(element, equipment);
  2904. });
  2905. events.emit(`reader-equipment-${activeTab}`, {
  2906. type: 'full',
  2907. value: equipment
  2908. });
  2909. }
  2910.  
  2911. function readActionScreen() {
  2912. const equipment = {};
  2913. $('skill-page .header > .name:contains("Consumables")').closest('.card').find('button > .name:not(.placeholder)').parent().each((i,element) => {
  2914. itemUtil.extractItem(element, equipment);
  2915. });
  2916. events.emit('reader-equipment-equipment', {
  2917. type: 'partial',
  2918. value: equipment
  2919. });
  2920. }
  2921.  
  2922. initialise();
  2923.  
  2924. }
  2925. );
  2926. // expReader
  2927. window.moduleRegistry.add('expReader', (events, skillCache, util) => {
  2928.  
  2929. const emitEvent = events.emit.bind(null, 'reader-exp');
  2930.  
  2931. function initialise() {
  2932. events.register('page', update);
  2933. window.setInterval(update, 1000);
  2934. }
  2935.  
  2936. function update() {
  2937. const page = events.getLast('page');
  2938. if(!page) {
  2939. return;
  2940. }
  2941. if(page.type === 'action') {
  2942. readActionScreen(page.skill);
  2943. }
  2944. if(page.type === 'taming') {
  2945. readTamingScreen();
  2946. }
  2947. readSidebar();
  2948. }
  2949.  
  2950. function readActionScreen(id) {
  2951. const text = $('skill-page .header > .name:contains("Stats")')
  2952. .closest('.card')
  2953. .find('.row > .name:contains("Total"):contains("XP")')
  2954. .closest('.row')
  2955. .find('.value')
  2956. .text();
  2957. const exp = util.parseNumber(text);
  2958. emitEvent([{ id, exp }]);
  2959. }
  2960.  
  2961. function readTamingScreen() {
  2962. const text = $('taming-page .header > .name:contains("Stats")')
  2963. .closest('.card')
  2964. .find('.row > .name:contains("Total"):contains("XP")')
  2965. .closest('.row')
  2966. .find('.amount')
  2967. .text();
  2968. const exp = util.parseNumber(text);
  2969. emitEvent([{
  2970. exp,
  2971. id: skillCache.byName['Taming'].id
  2972. }]);
  2973. }
  2974.  
  2975. function readSidebar() {
  2976. const levels = [];
  2977. $('nav-component button.skill').each((i,element) => {
  2978. element = $(element);
  2979. const name = element.find('.name').text();
  2980. const id = skillCache.byName[name].id;
  2981. const level = +(/\d+/.exec(element.find('.level').text())?.[0]);
  2982. const exp = util.levelToExp(level);
  2983. levels.push({ id, exp });
  2984. });
  2985. emitEvent(levels);
  2986. }
  2987.  
  2988. initialise();
  2989.  
  2990. }
  2991. );
  2992. // guildStructuresReader
  2993. window.moduleRegistry.add('guildStructuresReader', (events, util, structuresCache) => {
  2994.  
  2995. const emitEvent = events.emit.bind(null, 'reader-structures-guild');
  2996.  
  2997. function initialise() {
  2998. events.register('page', update);
  2999. window.setInterval(update, 1000);
  3000. }
  3001.  
  3002. function update() {
  3003. const page = events.getLast('page');
  3004. if(!page) {
  3005. return;
  3006. }
  3007. if(page.type === 'guild' && $('guild-page .tracker ~ div button.row-active').text() === 'Buildings') {
  3008. readGuildStructuresScreen();
  3009. }
  3010. }
  3011.  
  3012. function readGuildStructuresScreen() {
  3013. const structures = {};
  3014. $('guild-page .card').first().find('button').each((i,element) => {
  3015. element = $(element);
  3016. const name = element.find('.name').text();
  3017. const structure = structuresCache.byName[name];
  3018. if(!structure) {
  3019. return;
  3020. }
  3021. const level = util.parseNumber(element.find('.amount').text());
  3022. structures[structure.id] = level;
  3023. });
  3024. emitEvent({
  3025. type: 'full',
  3026. value: structures
  3027. });
  3028. }
  3029.  
  3030. initialise();
  3031.  
  3032. }
  3033. );
  3034. // inventoryReader
  3035. window.moduleRegistry.add('inventoryReader', (events, itemCache, util, itemUtil) => {
  3036.  
  3037. const emitEvent = events.emit.bind(null, 'reader-inventory');
  3038.  
  3039. function initialise() {
  3040. events.register('page', update);
  3041. window.setInterval(update, 1000);
  3042. }
  3043.  
  3044. function update() {
  3045. const page = events.getLast('page');
  3046. if(!page) {
  3047. return;
  3048. }
  3049. if(page.type === 'inventory') {
  3050. readInventoryScreen();
  3051. }
  3052. if(page.type === 'action') {
  3053. readActionScreen();
  3054. }
  3055. if(page.type === 'taming' && page.menu === 'expeditions') {
  3056. readExpeditionsScreen();
  3057. }
  3058. }
  3059.  
  3060. function readInventoryScreen() {
  3061. const inventory = {};
  3062. $('inventory-page .items > .item').each((i,element) => {
  3063. itemUtil.extractItem(element, inventory, true);
  3064. });
  3065. emitEvent({
  3066. type: 'full',
  3067. value: inventory
  3068. });
  3069. }
  3070.  
  3071. function readActionScreen() {
  3072. const inventory = {};
  3073. $('skill-page .header > .name:contains("Materials")').closest('.card').find('.row').each((i,element) => {
  3074. itemUtil.extractItem(element, inventory);
  3075. });
  3076. emitEvent({
  3077. type: 'partial',
  3078. value: inventory
  3079. });
  3080. }
  3081.  
  3082. function readExpeditionsScreen() {
  3083. const inventory = {};
  3084. $('taming-page .heading:contains("Materials") + button').each((i,element) => {
  3085. itemUtil.extractItem(element, inventory);
  3086. });
  3087. emitEvent({
  3088. type: 'partial',
  3089. value: inventory
  3090. });
  3091. }
  3092.  
  3093. initialise();
  3094.  
  3095. }
  3096. );
  3097. // marketReader
  3098. window.moduleRegistry.add('marketReader', (events, elementWatcher, itemCache, util) => {
  3099.  
  3100. const emitEvent = events.emit.bind(null, 'reader-market');
  3101. let inProgress = false;
  3102.  
  3103. const exports = {
  3104. trigger: update
  3105. };
  3106.  
  3107. function initialise() {
  3108. events.register('page', update);
  3109. window.setInterval(update, 10000);
  3110. }
  3111.  
  3112. function update() {
  3113. const page = events.getLast('page');
  3114. if(!page) {
  3115. return;
  3116. }
  3117. if(page.type === 'market') {
  3118. readMarketScreen();
  3119. }
  3120. }
  3121.  
  3122. async function readMarketScreen() {
  3123. if(inProgress) {
  3124. return;
  3125. }
  3126. try {
  3127. inProgress = true;
  3128. const selectedTab = $('market-listings-component .card > .tabs > button.tab-active').text().toLowerCase();
  3129. const type = selectedTab === 'orders' ? 'BUY' : selectedTab === 'listings' ? 'OWN' : 'SELL';
  3130. await elementWatcher.exists('market-listings-component .search ~ button', undefined, 10000);
  3131. if($('market-listings-component .search > input').val()) {
  3132. return;
  3133. }
  3134. const listings = [];
  3135. $('market-listings-component .search ~ button').each((i,element) => {
  3136. element = $(element);
  3137. const name = element.find('.name').text();
  3138. const item = itemCache.byName[name];
  3139. if(!item) {
  3140. return;
  3141. }
  3142. const amount = util.parseNumber(element.find('.amount').text());
  3143. const price = util.parseNumber(element.find('.cost').text());
  3144. const listingType = type !== 'OWN' ? type : element.find('.tag').length ? 'BUY' : 'SELL';
  3145. const isOwn = !!element.attr('disabled');
  3146. listings.push({
  3147. type: listingType,
  3148. item: item.id,
  3149. amount,
  3150. price,
  3151. isOwn,
  3152. element
  3153. });
  3154. });
  3155. emitEvent({
  3156. type,
  3157. listings,
  3158. });
  3159. } catch(e) {
  3160. console.error('error in market reader', e);
  3161. return;
  3162. } finally {
  3163. inProgress = false;
  3164. }
  3165. }
  3166.  
  3167. initialise();
  3168.  
  3169. return exports;
  3170.  
  3171. }
  3172. );
  3173. // petReader
  3174. window.moduleRegistry.add('petReader', (events, petCache, petPassiveCache, petTraitCache, elementWatcher, util) => {
  3175.  
  3176. const emitEvent = events.emit.bind(null, 'reader-pet');
  3177.  
  3178. function initialise() {
  3179. events.register('page', handlePage);
  3180. elementWatcher.addRecursiveObserver(readPetModal, 'app-component > div.scroll div.wrapper', 'taming-page', 'modal-component');
  3181. }
  3182.  
  3183. function handlePage(page) {
  3184. if(page.type === 'taming' && page.menu === 'pets') {
  3185. readTamingScreen();
  3186. }
  3187. }
  3188.  
  3189. function readTamingScreen() {
  3190. const elements = $('button.row.ng-star-inserted').get();
  3191. const values = [];
  3192. for(let element of elements) {
  3193. element = $(element);
  3194. const image = element.find('.image img').attr('src').split('/').at(-1);
  3195. const name = element.find('.image').next().find('.flex > :nth-child(1)')[0].textContent;
  3196. const level = util.parseNumber(element.find('.image').next().find('.flex > :nth-child(2)')[0].textContent);
  3197. const partOfTeam = !!element.closest('.card').find('.header:contains("Expedition Team")').length;
  3198. values.push({
  3199. parsed: false,
  3200. species: petCache.byImage[image].id,
  3201. family: petCache.byImage[image].family,
  3202. name,
  3203. level,
  3204. partOfTeam,
  3205. element: element.get()
  3206. });
  3207. }
  3208. emitEvent({
  3209. type: 'list',
  3210. value: values
  3211. });
  3212. }
  3213.  
  3214. function readPetModal(modal) {
  3215. if(!$(modal).find('.name:contains("Traits")').length) {
  3216. return; // avoid triggering on other modals
  3217. }
  3218. const image = $(modal).find('.header img').attr('src').split('/').at(-1);
  3219. const name = $(modal).find('.header .description button').text().trim();
  3220. const traits = $(modal).find('.name:contains("Traits")').next().text();
  3221. const health = +($(modal).find('.name:contains("Health") + .mono').text().match('\\((\\d+)%\\)')[1]);
  3222. const attack = +($(modal).find('.name:contains("Attack") + .mono').text().match('\\((\\d+)%\\)')[1]);
  3223. const defense = +($(modal).find('.name:contains("Defense") + .mono').text().match('\\((\\d+)%\\)')[1]);
  3224. const specialAttack = +($(modal).find('.name:contains("Sp. Atk") + .mono').text().match('\\((\\d+)%\\)')[1]);
  3225. const specialDefense = +($(modal).find('.name:contains("Sp. Def") + .mono').text().match('\\((\\d+)%\\)')[1]);
  3226. const speed = +($(modal).find('.name:contains("Speed") + .mono').text().match('\\((\\d+)%\\)')[1]);
  3227. const passives = $(modal).find('.name:contains("Total")').parent().nextAll('.row').find('.name').get().map(a => a.innerText);
  3228. const pet = {
  3229. parsed: true,
  3230. species: petCache.byImage[image].id,
  3231. family: petCache.byImage[image].family,
  3232. name,
  3233. traits: petTraitCache.byName[traits].id,
  3234. health,
  3235. attack,
  3236. defense,
  3237. specialAttack,
  3238. specialDefense,
  3239. speed,
  3240. passives: passives.map(a => petPassiveCache.byName[a].id)
  3241. };
  3242. const healthRow = $(modal).find('.name:contains("Health") + .mono').parent();
  3243. if(!healthRow.hasClass('stat-health')) {
  3244. $(modal).find('.name:contains("Health") + .mono').parent().addClass('stat-health');
  3245. $(modal).find('.name:contains("Attack") + .mono').parent().addClass('stat-attack');
  3246. $(modal).find('.name:contains("Defense") + .mono').parent().addClass('stat-defense');
  3247. $(modal).find('.name:contains("Sp. Atk") + .mono').parent().addClass('stat-specialAttack');
  3248. $(modal).find('.name:contains("Sp. Def") + .mono').parent().addClass('stat-specialDefense');
  3249. $(modal).find('.name:contains("Speed") + .mono').parent().addClass('stat-speed');
  3250. for(const id of pet.passives) {
  3251. const passive = petPassiveCache.byId[id];
  3252. $(modal).find(`.name:contains("${passive.name}")`).parent().addClass(`passive-${passive.stats.name}`);
  3253. }
  3254. }
  3255. emitEvent({
  3256. type: 'single',
  3257. value: pet,
  3258. modal: modal
  3259. });
  3260. }
  3261.  
  3262. initialise();
  3263.  
  3264. }
  3265. );
  3266. // structuresReader
  3267. window.moduleRegistry.add('structuresReader', (events, util, structuresCache) => {
  3268.  
  3269. const emitEvent = events.emit.bind(null, 'reader-structures');
  3270.  
  3271. function initialise() {
  3272. events.register('page', update);
  3273. window.setInterval(update, 1000);
  3274. }
  3275.  
  3276. function update() {
  3277. const page = events.getLast('page');
  3278. if(!page) {
  3279. return;
  3280. }
  3281. if(page.type === 'structure' && $('home-page .categories .category-active').text() === 'Build') {
  3282. readStructuresScreen();
  3283. }
  3284. }
  3285.  
  3286. function readStructuresScreen() {
  3287. const structures = {};
  3288. $('home-page .categories + .card button').each((i,element) => {
  3289. element = $(element);
  3290. const name = element.find('.name').text();
  3291. const structure = structuresCache.byName[name];
  3292. if(!structure) {
  3293. return;
  3294. }
  3295. const level = util.parseNumber(element.find('.level').text());
  3296. structures[structure.id] = level;
  3297. });
  3298. emitEvent({
  3299. type: 'full',
  3300. value: structures
  3301. });
  3302. }
  3303.  
  3304. initialise();
  3305.  
  3306. }
  3307. );
  3308. // variousReader
  3309. window.moduleRegistry.add('variousReader', (events, util) => {
  3310.  
  3311. const emitEvent = events.emit.bind(null, 'reader-various');
  3312.  
  3313. function initialise() {
  3314. events.register('page', update);
  3315. window.setInterval(update, 1000);
  3316. }
  3317.  
  3318. function update() {
  3319. const page = events.getLast('page');
  3320. if(!page) {
  3321. return;
  3322. }
  3323. const various = {};
  3324. if(page.type === 'action') {
  3325. readActionScreen(various, page.skill);
  3326. }
  3327. if(page.type === 'settings') {
  3328. readSettingsScreen(various);
  3329. }
  3330. emitEvent(various);
  3331. }
  3332.  
  3333. function readActionScreen(various, skillId) {
  3334. const amountText = $('skill-page .header > .name:contains("Loot")').parent().find('.amount').text();
  3335. const amountValue = !amountText ? null : util.parseNumber(amountText.split(' / ')[1]) - util.parseNumber(amountText.split(' / ')[0]);
  3336. various.maxAmount = {
  3337. [skillId]: amountValue
  3338. };
  3339. }
  3340.  
  3341. function readSettingsScreen(various) {
  3342. const username = $('settings-page .row:contains("Username") :last-child').text();
  3343. if(username) {
  3344. various.username = username;
  3345. }
  3346. }
  3347.  
  3348. initialise();
  3349.  
  3350. }
  3351. );
  3352. // authToast
  3353. window.moduleRegistry.add('authToast', (toast) => {
  3354.  
  3355. function initialise() {
  3356. toast.create({
  3357. text: 'Pancake-Scripts initialised!',
  3358. image: 'https://img.icons8.com/?size=48&id=1ODJ62iG96gX&format=png'
  3359. });
  3360. }
  3361.  
  3362. initialise();
  3363.  
  3364. }
  3365. );
  3366. // changelog
  3367. window.moduleRegistry.add('changelog', (Promise, pages, components, request, util, configuration) => {
  3368.  
  3369. const PAGE_NAME = 'Plugin changelog';
  3370. const loaded = new Promise.Deferred('changelog');
  3371.  
  3372. let changelogs = null;
  3373.  
  3374. async function initialise() {
  3375. await pages.register({
  3376. category: 'Skills',
  3377. after: 'Changelog',
  3378. name: PAGE_NAME,
  3379. image: 'https://ironwoodrpg.com/assets/misc/changelog.png',
  3380. render: renderPage
  3381. });
  3382. configuration.registerCheckbox({
  3383. category: 'Pages',
  3384. key: 'changelog-enabled',
  3385. name: 'Changelog',
  3386. default: true,
  3387. handler: handleConfigStateChange
  3388. });
  3389. load();
  3390. }
  3391.  
  3392. function handleConfigStateChange(state, name) {
  3393. if(state) {
  3394. pages.show(PAGE_NAME);
  3395. } else {
  3396. pages.hide(PAGE_NAME);
  3397. }
  3398. }
  3399.  
  3400. async function load() {
  3401. changelogs = await request.getChangelogs();
  3402. loaded.resolve();
  3403. }
  3404.  
  3405. async function renderPage() {
  3406. await loaded;
  3407. const header = components.search(componentBlueprint, 'header');
  3408. const list = components.search(componentBlueprint, 'list');
  3409. for(const index in changelogs) {
  3410. componentBlueprint.componentId = `changelogComponent_${index}`;
  3411. header.title = changelogs[index].title;
  3412. header.textRight = new Date(changelogs[index].time).toLocaleDateString();
  3413. list.entries = changelogs[index].entries;
  3414. components.addComponent(componentBlueprint);
  3415. }
  3416. }
  3417.  
  3418. const componentBlueprint = {
  3419. componentId: 'changelogComponent',
  3420. dependsOn: 'custom-page',
  3421. parent: '.column0',
  3422. selectedTabIndex: 0,
  3423. tabs: [{
  3424. title: 'tab',
  3425. rows: [{
  3426. id: 'header',
  3427. type: 'header',
  3428. title: '',
  3429. textRight: ''
  3430. },{
  3431. id: 'list',
  3432. type: 'list',
  3433. entries: []
  3434. }]
  3435. }]
  3436. };
  3437.  
  3438. initialise();
  3439.  
  3440. }
  3441. );
  3442. // configurationPage
  3443. window.moduleRegistry.add('configurationPage', (pages, components, elementWatcher, configuration, elementCreator) => {
  3444.  
  3445. const PAGE_NAME = 'Configuration';
  3446.  
  3447. async function initialise() {
  3448. await pages.register({
  3449. category: 'Misc',
  3450. after: 'Settings',
  3451. name: PAGE_NAME,
  3452. image: 'https://cdn-icons-png.flaticon.com/512/3953/3953226.png',
  3453. columns: '2',
  3454. render: renderPage
  3455. });
  3456. elementCreator.addStyles(styles);
  3457. pages.show(PAGE_NAME);
  3458. }
  3459.  
  3460. function generateBlueprint() {
  3461. const categories = {};
  3462. for(const item of configuration.items) {
  3463. if(!categories[item.category]) {
  3464. categories[item.category] = {
  3465. name: item.category,
  3466. items: []
  3467. }
  3468. }
  3469. categories[item.category].items.push(item);
  3470. }
  3471. const blueprints = [];
  3472. let column = 1;
  3473. for(const category in categories) {
  3474. column = 1 - column;
  3475. const rows = [{
  3476. type: 'header',
  3477. title: category,
  3478. centered: true
  3479. }];
  3480. rows.push(...categories[category].items.flatMap(createRows));
  3481. blueprints.push({
  3482. componentId: `configurationComponent_${category}`,
  3483. dependsOn: 'custom-page',
  3484. parent: `.column${column}`,
  3485. selectedTabIndex: 0,
  3486. tabs: [{
  3487. rows: rows
  3488. }]
  3489. });
  3490. }
  3491. return blueprints;
  3492. }
  3493.  
  3494. function createRows(item) {
  3495. switch(item.type) {
  3496. case 'checkbox': return createRows_Checkbox(item);
  3497. case 'input': return createRows_Input(item);
  3498. case 'dropdown': return createRows_Dropdown(item);
  3499. case 'json': break;
  3500. default: throw `Unknown configuration type : ${item.type}`;
  3501. }
  3502. }
  3503.  
  3504. function createRows_Checkbox(item) {
  3505. return [{
  3506. type: 'checkbox',
  3507. text: item.name,
  3508. checked: item.value,
  3509. delay: 500,
  3510. action: (value) => {
  3511. item.handler(value);
  3512. pages.requestRender(PAGE_NAME);
  3513. }
  3514. }]
  3515. }
  3516.  
  3517. function createRows_Input(item) {
  3518. const value = item.value || item.default;
  3519. return [{
  3520. type: 'item',
  3521. name: item.name
  3522. },{
  3523. type: 'input',
  3524. name: item.name,
  3525. value: value,
  3526. inputType: item.inputType,
  3527. delay: 500,
  3528. action: (value) => {
  3529. item.handler(value);
  3530. }
  3531. }]
  3532. }
  3533.  
  3534. function createRows_Dropdown(item) {
  3535. const value = item.value || item.default;
  3536. const options = item.options.map(option => ({
  3537. text: option,
  3538. value: option,
  3539. selected: option === value
  3540. }));
  3541. return [{
  3542. type: 'item',
  3543. name: item.name
  3544. },{
  3545. type: 'dropdown',
  3546. options: options,
  3547. delay: 500,
  3548. action: (value) => {
  3549. item.handler(value);
  3550. }
  3551. }]
  3552. }
  3553.  
  3554. function renderPage() {
  3555. const blueprints = generateBlueprint();
  3556. for(const blueprint of blueprints) {
  3557. components.addComponent(blueprint);
  3558. }
  3559. }
  3560.  
  3561. const styles = `
  3562. .modifiedHeight {
  3563. height: 28px;
  3564. }
  3565. `;
  3566.  
  3567. initialise();
  3568. }
  3569. );
  3570. // debugService
  3571. window.moduleRegistry.add('debugService', (request, toast, statsStore, EstimationGenerator, logService, events, util) => {
  3572.  
  3573. const exports = {
  3574. submit
  3575. };
  3576.  
  3577. async function submit() {
  3578. const data = get();
  3579. try {
  3580. await forward(data);
  3581. } catch(e) {
  3582. exportToClipboard(data);
  3583. }
  3584. }
  3585.  
  3586. function get() {
  3587. return {
  3588. stats: statsStore.get(),
  3589. state: (new EstimationGenerator()).export(),
  3590. logs: logService.get(),
  3591. events: events.getLastCache()
  3592. };
  3593. }
  3594.  
  3595. async function forward(data) {
  3596. await request.report(data);
  3597. toast.create({
  3598. text: 'Forwarded debug data',
  3599. image: 'https://img.icons8.com/?size=48&id=13809'
  3600. });
  3601. }
  3602.  
  3603. function exportToClipboard(data) {
  3604. toast.copyToClipboard(JSON.stringify(data), 'Failed to forward, exported to clipboard instead');
  3605. }
  3606.  
  3607. return exports;
  3608.  
  3609. });
  3610. // estimator
  3611. window.moduleRegistry.add('estimator', (configuration, events, skillCache, actionCache, itemCache, estimatorAction, estimatorOutskirts, estimatorActivity, estimatorCombat, components, util, statsStore) => {
  3612.  
  3613. let enabled = false;
  3614.  
  3615. const exports = {
  3616. get,
  3617. enrichTimings,
  3618. enrichValues,
  3619. preRenderItems
  3620. }
  3621.  
  3622. function initialise() {
  3623. configuration.registerCheckbox({
  3624. category: 'Data',
  3625. key: 'estimations',
  3626. name: 'Estimations',
  3627. default: true,
  3628. handler: handleConfigStateChange
  3629. });
  3630. events.register('page', update);
  3631. events.register('state-stats', update);
  3632. $(document).on('click', '.close', update);
  3633. }
  3634.  
  3635. function handleConfigStateChange(state) {
  3636. enabled = state;
  3637. }
  3638.  
  3639. function update() {
  3640. if(!enabled) {
  3641. return;
  3642. }
  3643. const page = events.getLast('page');
  3644. if(page?.type === 'action') {
  3645. const stats = events.getLast('state-stats');
  3646. if(stats) {
  3647. const estimation = get(page.skill, page.action);
  3648. enrichTimings(estimation);
  3649. enrichValues(estimation);
  3650. preRender(estimation, componentBlueprint);
  3651. preRenderItems(estimation, componentBlueprint);
  3652. components.addComponent(componentBlueprint);
  3653. }
  3654. }
  3655. }
  3656.  
  3657. function get(skillId, actionId) {
  3658. const skill = skillCache.byId[skillId];
  3659. const action = actionCache.byId[actionId];
  3660. if(action.type === 'OUTSKIRTS') {
  3661. return estimatorOutskirts.get(skillId, actionId);
  3662. } else if(skill.type === 'Gathering' || skill.type === 'Crafting') {
  3663. return estimatorActivity.get(skillId, actionId);
  3664. } else if(skill.type === 'Combat') {
  3665. return estimatorCombat.get(skillId, actionId);
  3666. }
  3667. }
  3668.  
  3669. function enrichTimings(estimation) {
  3670. const inventory = Object.entries(estimation.ingredients).map(([id,amount]) => ({
  3671. id,
  3672. stored: statsStore.getInventoryItem(id),
  3673. secondsLeft: statsStore.getInventoryItem(id) * 3600 / amount
  3674. })).reduce((a,b) => (a[b.id] = b, a), {});
  3675. const equipment = Object.entries(estimation.equipments).map(([id,amount]) => ({
  3676. id,
  3677. stored: statsStore.getEquipmentItem(id),
  3678. secondsLeft: statsStore.getEquipmentItem(id) * 3600 / amount
  3679. })).reduce((a,b) => (a[b.id] = b, a), {});
  3680. let maxAmount = statsStore.get('MAX_AMOUNT', estimation.skill);
  3681. maxAmount = {
  3682. value: maxAmount,
  3683. secondsLeft: estimation.productionSpeed / 10 * (maxAmount || Infinity)
  3684. };
  3685. const merchantSellChance = statsStore.get('MERCHANT_SELL_CHANCE', estimation.skill) / 100;
  3686. if(merchantSellChance) {
  3687. maxAmount.secondsLeft /= 1 - merchantSellChance;
  3688. }
  3689. const levelState = statsStore.getLevel(estimation.skill);
  3690. estimation.timings = {
  3691. inventory,
  3692. equipment,
  3693. maxAmount,
  3694. finished: Math.min(maxAmount.secondsLeft, ...Object.values(inventory).concat(Object.values(equipment)).map(a => a.secondsLeft)),
  3695. level: util.expToNextLevel(levelState.exp) * 3600 / estimation.exp,
  3696. tier: levelState.level >= 100 ? 0 : util.expToNextTier(levelState.exp) * 3600 / estimation.exp,
  3697. };
  3698. }
  3699.  
  3700. function enrichValues(estimation) {
  3701. estimation.values = {
  3702. drop: getSellPrice(estimation.drops),
  3703. ingredient: getSellPrice(estimation.ingredients),
  3704. equipment: getSellPrice(estimation.equipments),
  3705. net: 0
  3706. };
  3707. estimation.values.net = estimation.values.drop - estimation.values.ingredient - estimation.values.equipment;
  3708. }
  3709.  
  3710. function getSellPrice(object) {
  3711. return Object.entries(object)
  3712. .map(a => a[1] * itemCache.byId[a[0]].attributes.SELL_PRICE)
  3713. .filter(a => a)
  3714. .reduce((a,b) => a+b, 0);
  3715. }
  3716.  
  3717. function preRender(estimation, blueprint) {
  3718. components.search(blueprint, 'actions').value
  3719. = util.formatNumber(estimatorAction.LOOPS_PER_HOUR / estimation.speed);
  3720. components.search(blueprint, 'exp').hidden
  3721. = estimation.exp === 0;
  3722. components.search(blueprint, 'exp').value
  3723. = util.formatNumber(estimation.exp);
  3724. components.search(blueprint, 'survivalChance').hidden
  3725. = estimation.type === 'ACTIVITY';
  3726. components.search(blueprint, 'survivalChance').value
  3727. = util.formatNumber(estimation.survivalChance * 100) + ' %';
  3728. components.search(blueprint, 'finishedTime').value
  3729. = util.secondsToDuration(estimation.timings.finished);
  3730. components.search(blueprint, 'levelTime').hidden
  3731. = estimation.exp === 0 || estimation.timings.level === 0;
  3732. components.search(blueprint, 'levelTime').value
  3733. = util.secondsToDuration(estimation.timings.level);
  3734. components.search(blueprint, 'tierTime').hidden
  3735. = estimation.exp === 0 || estimation.timings.tier === 0;
  3736. components.search(blueprint, 'tierTime').value
  3737. = util.secondsToDuration(estimation.timings.tier);
  3738. components.search(blueprint, 'dropValue').hidden
  3739. = estimation.values.drop === 0;
  3740. components.search(blueprint, 'dropValue').value
  3741. = util.formatNumber(estimation.values.drop);
  3742. components.search(blueprint, 'ingredientValue').hidden
  3743. = estimation.values.ingredient === 0;
  3744. components.search(blueprint, 'ingredientValue').value
  3745. = util.formatNumber(estimation.values.ingredient);
  3746. components.search(blueprint, 'equipmentValue').hidden
  3747. = estimation.values.equipment === 0;
  3748. components.search(blueprint, 'equipmentValue').value
  3749. = util.formatNumber(estimation.values.equipment);
  3750. components.search(blueprint, 'netValue').hidden
  3751. = estimation.values.net === 0;
  3752. components.search(blueprint, 'netValue').value
  3753. = util.formatNumber(estimation.values.net);
  3754. components.search(blueprint, 'tabTime').hidden
  3755. = (estimation.timings.inventory.length + estimation.timings.equipment.length) === 0;
  3756. }
  3757.  
  3758. function preRenderItems(estimation, blueprint) {
  3759. const dropRows = components.search(blueprint, 'dropRows');
  3760. const ingredientRows = components.search(blueprint, 'ingredientRows');
  3761. const timeRows = components.search(blueprint, 'timeRows');
  3762. dropRows.rows = [];
  3763. ingredientRows.rows = [];
  3764. timeRows.rows = [];
  3765. if(estimation.timings.maxAmount.value) {
  3766. timeRows.rows.push({
  3767. type: 'item',
  3768. image: 'https://img.icons8.com/?size=48&id=1HQMXezy5LeT&format=png',
  3769. imageFilter: 'invert(100%)',
  3770. name: `Max amount [${util.formatNumber(estimation.timings.maxAmount.value)}]`,
  3771. value: util.secondsToDuration(estimation.timings.maxAmount.secondsLeft)
  3772. });
  3773. }
  3774. for(const id in estimation.drops) {
  3775. const item = itemCache.byId[id];
  3776. dropRows.rows.push({
  3777. type: 'item',
  3778. image: `/assets/${item.image}`,
  3779. imagePixelated: true,
  3780. name: item.name,
  3781. value: util.formatNumber(estimation.drops[id]) + ' / hour'
  3782. });
  3783. }
  3784. for(const id in estimation.ingredients) {
  3785. const item = itemCache.byId[id];
  3786. const timing = estimation.timings.inventory[id];
  3787. ingredientRows.rows.push({
  3788. type: 'item',
  3789. image: `/assets/${item.image}`,
  3790. imagePixelated: true,
  3791. name: item.name,
  3792. value: util.formatNumber(estimation.ingredients[id]) + ' / hour'
  3793. });
  3794. timeRows.rows.push({
  3795. type: 'item',
  3796. image: `/assets/${item.image}`,
  3797. imagePixelated: true,
  3798. name: `${item.name} [${util.formatNumber(timing.stored)}]`,
  3799. value: util.secondsToDuration(timing.secondsLeft)
  3800. });
  3801. }
  3802. for(const id in estimation.equipments) {
  3803. const item = itemCache.byId[id];
  3804. const timing = estimation.timings.equipment[id];
  3805. ingredientRows.rows.push({
  3806. type: 'item',
  3807. image: `/assets/${item.image}`,
  3808. imagePixelated: true,
  3809. name: item.name,
  3810. value: util.formatNumber(estimation.equipments[id]) + ' / hour'
  3811. });
  3812. timeRows.rows.push({
  3813. type: 'item',
  3814. image: `/assets/${item.image}`,
  3815. imagePixelated: true,
  3816. name: `${item.name} [${util.formatNumber(timing.stored)}]`,
  3817. value: util.secondsToDuration(timing.secondsLeft)
  3818. });
  3819. }
  3820. }
  3821.  
  3822. const componentBlueprint = {
  3823. componentId: 'estimatorComponent',
  3824. dependsOn: 'skill-page',
  3825. parent: 'actions-component',
  3826. selectedTabIndex: 0,
  3827. tabs: [{
  3828. title: 'Overview',
  3829. rows: [{
  3830. type: 'item',
  3831. id: 'actions',
  3832. name: 'Actions/hour',
  3833. image: 'https://cdn-icons-png.flaticon.com/512/3563/3563395.png',
  3834. value: ''
  3835. },{
  3836. type: 'item',
  3837. id: 'exp',
  3838. name: 'Exp/hour',
  3839. image: 'https://cdn-icons-png.flaticon.com/512/616/616490.png',
  3840. value: ''
  3841. },{
  3842. type: 'item',
  3843. id: 'survivalChance',
  3844. name: 'Survival chance',
  3845. image: 'https://cdn-icons-png.flaticon.com/512/3004/3004458.png',
  3846. value: ''
  3847. },{
  3848. type: 'item',
  3849. id: 'finishedTime',
  3850. name: 'Finished',
  3851. image: 'https://cdn-icons-png.flaticon.com/512/1505/1505471.png',
  3852. value: ''
  3853. },{
  3854. type: 'item',
  3855. id: 'levelTime',
  3856. name: 'Level up',
  3857. image: 'https://cdn-icons-png.flaticon.com/512/4614/4614145.png',
  3858. value: ''
  3859. },{
  3860. type: 'item',
  3861. id: 'tierTime',
  3862. name: 'Tier up',
  3863. image: 'https://cdn-icons-png.flaticon.com/512/4789/4789514.png',
  3864. value: ''
  3865. },{
  3866. type: 'item',
  3867. id: 'dropValue',
  3868. name: 'Gold/hour (loot)',
  3869. image: 'https://cdn-icons-png.flaticon.com/512/9028/9028024.png',
  3870. imageFilter: 'invert(100%) sepia(47%) saturate(3361%) hue-rotate(313deg) brightness(106%) contrast(108%)',
  3871. value: ''
  3872. },{
  3873. type: 'item',
  3874. id: 'ingredientValue',
  3875. name: 'Gold/hour (materials)',
  3876. image: 'https://cdn-icons-png.flaticon.com/512/9028/9028031.png',
  3877. imageFilter: 'invert(100%) sepia(47%) saturate(3361%) hue-rotate(313deg) brightness(106%) contrast(108%)',
  3878. value: ''
  3879. },{
  3880. type: 'item',
  3881. id: 'equipmentValue',
  3882. name: 'Gold/hour (equipments)',
  3883. image: 'https://cdn-icons-png.flaticon.com/512/9028/9028031.png',
  3884. imageFilter: 'invert(100%) sepia(47%) saturate(3361%) hue-rotate(313deg) brightness(106%) contrast(108%)',
  3885. value: ''
  3886. },{
  3887. type: 'item',
  3888. id: 'netValue',
  3889. name: 'Gold/hour (total)',
  3890. image: 'https://cdn-icons-png.flaticon.com/512/11937/11937869.png',
  3891. imageFilter: 'invert(100%) sepia(47%) saturate(3361%) hue-rotate(313deg) brightness(106%) contrast(108%)',
  3892. value: ''
  3893. }]
  3894. },{
  3895. title: 'Items',
  3896. rows: [{
  3897. type: 'header',
  3898. title: 'Produced'
  3899. },{
  3900. type: 'segment',
  3901. id: 'dropRows',
  3902. rows: []
  3903. },{
  3904. type: 'header',
  3905. title: 'Consumed'
  3906. },{
  3907. type: 'segment',
  3908. id: 'ingredientRows',
  3909. rows: []
  3910. }]
  3911. },{
  3912. title: 'Time',
  3913. id: 'tabTime',
  3914. rows: [{
  3915. type: 'segment',
  3916. id: 'timeRows',
  3917. rows: []
  3918. }]
  3919. }]
  3920. };
  3921.  
  3922. initialise();
  3923.  
  3924. return exports;
  3925.  
  3926. }
  3927. );
  3928. // estimatorAction
  3929. window.moduleRegistry.add('estimatorAction', (dropCache, actionCache, ingredientCache, skillCache, itemCache, statsStore) => {
  3930.  
  3931. const SECONDS_PER_HOUR = 60 * 60;
  3932. const LOOPS_PER_HOUR = 10 * SECONDS_PER_HOUR; // 1 second = 10 loops
  3933. const LOOPS_PER_FOOD = 150;
  3934.  
  3935. const exports = {
  3936. LOOPS_PER_HOUR,
  3937. LOOPS_PER_FOOD,
  3938. getDrops,
  3939. getIngredients,
  3940. getEquipmentUses
  3941. };
  3942.  
  3943. function getDrops(skillId, actionId, isCombat, multiplier = 1) {
  3944. const drops = dropCache.byAction[actionId];
  3945. if(!drops) {
  3946. return [];
  3947. }
  3948. const hasFailDrops = !!drops.find(a => a.type === 'FAILED');
  3949. const hasMonsterDrops = !!drops.find(a => a.type === 'MONSTER');
  3950. const successChance = hasFailDrops ? getSuccessChance(skillId, actionId) / 100 : 1;
  3951. return drops.map(drop => {
  3952. let amount = (1 + drop.amount) / 2 * multiplier * drop.chance;
  3953. if(drop.type !== 'MONSTER' && isCombat && hasMonsterDrops) {
  3954. amount = 0;
  3955. } else if(drop.type === 'MONSTER' && !isCombat) {
  3956. amount = 0;
  3957. } else if(drop.type === 'FAILED') {
  3958. amount *= 1 - successChance;
  3959. } else {
  3960. amount *= successChance;
  3961. }
  3962. if(amount) {
  3963. return {
  3964. id: drop.item,
  3965. amount
  3966. };
  3967. }
  3968. })
  3969. .filter(a => a)
  3970. .map(a => {
  3971. const mapFindChance = statsStore.get('MAP_FIND_CHANCE', skillId) / 100;
  3972. if(!mapFindChance || !itemCache.specialIds.dungeonMap.includes(a.id)) {
  3973. return a;
  3974. }
  3975. a.amount *= 1 + mapFindChance;
  3976. return a;
  3977. })
  3978. .reduce((a,b) => (a[b.id] = b.amount, a), {});
  3979. }
  3980.  
  3981. function getSuccessChance(skillId, actionId) {
  3982. const action = actionCache.byId[actionId];
  3983. const level = statsStore.getLevel(skillId).level;
  3984. return Math.min(95, 80 + level - action.level) + Math.floor(level / 20);
  3985. }
  3986.  
  3987. function getIngredients(actionId, multiplier = 1) {
  3988. const ingredients = ingredientCache.byAction[actionId];
  3989. if(!ingredients) {
  3990. return [];
  3991. }
  3992. return ingredients.map(ingredient => ({
  3993. id: ingredient.item,
  3994. amount: ingredient.amount * multiplier
  3995. }))
  3996. .reduce((a,b) => (a[b.id] = b.amount, a), {});
  3997. }
  3998.  
  3999. function getEquipmentUses(skillId, actionId, isCombat = false, foodPerHour = 0) {
  4000. const skill = skillCache.byId[skillId];
  4001. const action = actionCache.byId[actionId];
  4002. const result = {};
  4003. const potionMultiplier = 1 + statsStore.get('DECREASED_POTION_DURATION') / 100;
  4004. if(isCombat) {
  4005. if(action.type !== 'OUTSKIRTS') {
  4006. // combat potions
  4007. statsStore.getManyEquipmentItems(itemCache.specialIds.combatPotion)
  4008. .forEach(a => result[a.id] = 20 * potionMultiplier);
  4009. }
  4010. if(action.type === 'DUNGEON') {
  4011. // dungeon map
  4012. const lanternMultiplier = 1 + statsStore.get('DUNGEON_TIME') / 100;
  4013. statsStore.getManyEquipmentItems(itemCache.specialIds.dungeonMap)
  4014. .forEach(a => result[a.id] = 3 / 24 / lanternMultiplier);
  4015. }
  4016. if(foodPerHour && action.type !== 'OUTSKIRTS' && statsStore.get('HEAL')) {
  4017. // active food
  4018. statsStore.getManyEquipmentItems(itemCache.specialIds.food)
  4019. .forEach(a => result[a.id] = foodPerHour);
  4020. }
  4021. if(statsStore.getWeapon()?.name?.endsWith('Bow')) {
  4022. // ammo
  4023. const attacksPerHour = SECONDS_PER_HOUR / statsStore.get('ATTACK_SPEED');
  4024. const ammoPerHour = attacksPerHour * (1 - statsStore.get('AMMO_PRESERVATION_CHANCE') / 100);
  4025. statsStore.getManyEquipmentItems(itemCache.specialIds.ammo)
  4026. .forEach(a => result[a.id] = ammoPerHour);
  4027. }
  4028. } else {
  4029. if(skill.type === 'Gathering') {
  4030. // gathering potions
  4031. statsStore.getManyEquipmentItems(itemCache.specialIds.gatheringPotion)
  4032. .forEach(a => result[a.id] = 20 * potionMultiplier);
  4033. }
  4034. if(skill.type === 'Crafting') {
  4035. // crafting potions
  4036. statsStore.getManyEquipmentItems(itemCache.specialIds.craftingPotion)
  4037. .forEach(a => result[a.id] = 20 * potionMultiplier);
  4038. }
  4039. }
  4040. if(statsStore.get('PASSIVE_FOOD_CONSUMPTION') && statsStore.get('HEAL')) {
  4041. // passive food
  4042. statsStore.getManyEquipmentItems(itemCache.specialIds.food)
  4043. .forEach(a => result[a.id] = (result[a.id] || 0) + statsStore.get('PASSIVE_FOOD_CONSUMPTION') * 3600 / 5 / statsStore.get('HEAL'));
  4044. }
  4045. return result;
  4046. }
  4047.  
  4048. return exports;
  4049.  
  4050. }
  4051. );
  4052. // estimatorActivity
  4053. window.moduleRegistry.add('estimatorActivity', (skillCache, actionCache, estimatorAction, statsStore, itemCache, dropCache) => {
  4054.  
  4055. const exports = {
  4056. get
  4057. };
  4058.  
  4059. function get(skillId, actionId) {
  4060. const skill = skillCache.byId[skillId];
  4061. const action = actionCache.byId[actionId];
  4062. const speed = getSpeed(skill.technicalName, action);
  4063. const actionCount = estimatorAction.LOOPS_PER_HOUR / speed;
  4064. const actualActionCount = actionCount * (1 + statsStore.get('EFFICIENCY', skill.technicalName) / 100);
  4065. const dropCount = actualActionCount * (1 + statsStore.get('DOUBLE_DROP', skill.technicalName) / 100);
  4066. const ingredientCount = actualActionCount * (1 - statsStore.get('PRESERVATION', skill.technicalName) / 100);
  4067. const exp = actualActionCount * action.exp * (1 + statsStore.get('DOUBLE_EXP', skill.technicalName) / 100);
  4068. const drops = estimatorAction.getDrops(skillId, actionId, false, dropCount);
  4069. const ingredients = estimatorAction.getIngredients(actionId, ingredientCount);
  4070. const equipments = estimatorAction.getEquipmentUses(skillId, actionId);
  4071.  
  4072. let statLowerTierChance;
  4073. if(skill.type === 'Gathering' && (statLowerTierChance = statsStore.get('LOWER_TIER_CHANCE', skill.technicalName) / 100)) {
  4074. for(const item in drops) {
  4075. const mappings = dropCache.lowerGatherMappings[item];
  4076. if(mappings) {
  4077. for(const other of mappings) {
  4078. drops[other] = (drops[other] || 0) + statLowerTierChance * drops[item] / mappings.length;
  4079. }
  4080. drops[item] *= 1 - statLowerTierChance;
  4081. }
  4082. }
  4083. }
  4084.  
  4085. let statMerchantSellChance;
  4086. if(skill.type === 'Crafting' && (statMerchantSellChance = statsStore.get('MERCHANT_SELL_CHANCE', skill.technicalName) / 100)) {
  4087. for(const item in drops) {
  4088. drops[itemCache.specialIds.coins] = (drops[itemCache.specialIds.coins] || 0) + 2 * statMerchantSellChance * drops[item] * itemCache.byId[item].attributes.SELL_PRICE;
  4089. drops[item] *= 1 - statMerchantSellChance;
  4090. }
  4091. }
  4092.  
  4093. return {
  4094. type: 'ACTIVITY',
  4095. skill: skillId,
  4096. speed,
  4097. productionSpeed: speed * actionCount / dropCount,
  4098. exp,
  4099. drops,
  4100. ingredients,
  4101. equipments
  4102. };
  4103. }
  4104.  
  4105. function getSpeed(skillName, action) {
  4106. const speedBonus = statsStore.get('SKILL_SPEED', skillName);
  4107. return Math.round(action.speed * 1000 / (100 + speedBonus)) + 1;
  4108. }
  4109.  
  4110. return exports;
  4111.  
  4112. }
  4113. );
  4114. // estimatorCombat
  4115. window.moduleRegistry.add('estimatorCombat', (skillCache, actionCache, monsterCache, itemCache, dropCache, statsStore, Distribution, estimatorAction) => {
  4116.  
  4117. const exports = {
  4118. get,
  4119. getDamageDistributions,
  4120. getSurvivalChance
  4121. };
  4122.  
  4123. function get(skillId, actionId) {
  4124. const skill = skillCache.byId[skillId];
  4125. const action = actionCache.byId[actionId];
  4126. const monsterIds = action.monster ? [action.monster] : action.monsterGroup;
  4127. const playerStats = getPlayerStats();
  4128. const sampleMonsterStats = getMonsterStats(monsterIds[Math.floor(monsterIds.length / 2)]);
  4129. playerStats.damage_ = new Distribution();
  4130. sampleMonsterStats.damage_ = new Distribution();
  4131. for(const monsterId of monsterIds) {
  4132. const monsterStats = getMonsterStats(monsterId);
  4133. let damage_ = getInternalDamageDistribution(playerStats, monsterStats, monsterIds.length > 1);
  4134. const weight = damage_.expectedRollsUntill(monsterStats.health);
  4135. playerStats.damage_.addDistribution(damage_, weight);
  4136. damage_ = getInternalDamageDistribution(monsterStats, playerStats, monsterIds.length > 1);
  4137. sampleMonsterStats.damage_.addDistribution(damage_, weight);
  4138. }
  4139. playerStats.damage_.normalize();
  4140. sampleMonsterStats.damage_.normalize();
  4141.  
  4142. const loopsPerKill = playerStats.attackSpeed * playerStats.damage_.expectedRollsUntill(sampleMonsterStats.health) * 10 + 5;
  4143. const actionCount = estimatorAction.LOOPS_PER_HOUR / loopsPerKill;
  4144. const efficiency = 1 + statsStore.get('EFFICIENCY', skill.technicalName) / 100;
  4145. const actualActionCount = actionCount * efficiency;
  4146. const dropCount = actualActionCount * (1 + statsStore.get('DOUBLE_DROP', skill.technicalName) / 100);
  4147. const attacksReceivedPerHour = estimatorAction.LOOPS_PER_HOUR / 10 / sampleMonsterStats.attackSpeed;
  4148. const healPerFood = statsStore.get('HEAL') * (1 + statsStore.get('FOOD_EFFECT') / 100);
  4149. const damagePerHour = attacksReceivedPerHour * sampleMonsterStats.damage_.average();
  4150. const foodPerHour = damagePerHour / healPerFood * (1 - statsStore.get('FOOD_PRESERVATION_CHANCE') / 100);
  4151.  
  4152. let exp = estimatorAction.LOOPS_PER_HOUR * action.exp / 1000;
  4153. exp *= efficiency;
  4154. exp *= 1 + statsStore.get('DOUBLE_EXP', skill.technicalName) / 100;
  4155. exp *= 1 + statsStore.get('COMBAT_EXP', skill.technicalName) / 100;
  4156. exp *= getExpTriangleModifier(playerStats, sampleMonsterStats);
  4157. const drops = estimatorAction.getDrops(skillId, actionId, true, dropCount);
  4158. const equipments = estimatorAction.getEquipmentUses(skillId, actionId, true, foodPerHour);
  4159. const survivalChance = getSurvivalChance(playerStats, sampleMonsterStats, loopsPerKill);
  4160.  
  4161. let statCoinSnatch;
  4162. if(statCoinSnatch = statsStore.get('COIN_SNATCH')) {
  4163. const attacksPerHour = estimatorAction.LOOPS_PER_HOUR / 10 / playerStats.attackSpeed;
  4164. const coinsPerHour = (statCoinSnatch + 1) / 2 * attacksPerHour;
  4165. drops[itemCache.specialIds.coins] = (drops[itemCache.specialIds.coins] || 0) + coinsPerHour;
  4166. }
  4167.  
  4168. let statCarveChance = 0.1;
  4169. if(action.type !== 'OUTSKIRTS' && (statCarveChance = statsStore.get('CARVE_CHANCE') / 100)) {
  4170. const boneDrop = dropCache.byAction[actionId].find(a => a.chance === 1);
  4171. const boneDropCount = drops[boneDrop.item];
  4172. const coinDrop = dropCache.byAction[actionId].find(a => a.item === itemCache.specialIds.coins);
  4173. const averageAmount = (1 + coinDrop.amount) / 2;
  4174. drops[itemCache.specialIds.coins] -= statCarveChance * coinDrop.chance * averageAmount / 2 * boneDropCount;
  4175. const mappings = dropCache.boneCarveMappings[boneDrop.item];
  4176. for(const other of mappings) {
  4177. drops[other] = (drops[other] || 0) + statCarveChance * coinDrop.chance * boneDropCount / mappings.length;
  4178. }
  4179. }
  4180.  
  4181. return {
  4182. type: 'COMBAT',
  4183. skill: skillId,
  4184. speed: loopsPerKill,
  4185. productionSpeed: loopsPerKill * actionCount / dropCount,
  4186. exp,
  4187. drops,
  4188. ingredients: {},
  4189. equipments,
  4190. player: playerStats,
  4191. monster: sampleMonsterStats,
  4192. survivalChance
  4193. };
  4194. }
  4195.  
  4196. function getPlayerStats() {
  4197. const attackStyle = statsStore.getAttackStyle();
  4198. const attackSkill = skillCache.byTechnicalName[attackStyle];
  4199. const attackLevel = statsStore.getLevel(attackSkill.id).level;
  4200. const defenseLevel = statsStore.getLevel(8).level;
  4201. return {
  4202. isPlayer: true,
  4203. attackStyle,
  4204. attackSpeed: statsStore.get('ATTACK_SPEED'),
  4205. damage: statsStore.get('DAMAGE'),
  4206. armour: statsStore.get('ARMOUR'),
  4207. health: statsStore.get('HEALTH'),
  4208. blockChance: statsStore.get('BLOCK_CHANCE')/100,
  4209. critChance: statsStore.get('CRIT_CHANCE')/100,
  4210. stunChance: statsStore.get('STUN_CHANCE')/100,
  4211. parryChance: statsStore.get('PARRY_CHANCE')/100,
  4212. bleedChance: statsStore.get('BLEED_CHANCE')/100,
  4213. damageRange: (75 + statsStore.get('DAMAGE_RANGE'))/100,
  4214. dungeonDamage: 1 + statsStore.get('DUNGEON_DAMAGE')/100,
  4215. attackLevel,
  4216. defenseLevel
  4217. };
  4218. }
  4219.  
  4220. function getMonsterStats(monsterId) {
  4221. const monster = monsterCache.byId[monsterId];
  4222. return {
  4223. isPlayer: false,
  4224. attackStyle: monster.attackStyle,
  4225. attackSpeed: monster.speed,
  4226. damage: monster.attack,
  4227. armour: monster.armour,
  4228. health: monster.health,
  4229. blockChance: 0,
  4230. critChance: 0,
  4231. stunChance: 0,
  4232. parryChance: 0,
  4233. bleedChance: 0,
  4234. damageRange: 0.75,
  4235. dungeonDamage: 1,
  4236. attackLevel: monster.level,
  4237. defenseLevel: monster.level
  4238. };
  4239. }
  4240.  
  4241. function getInternalDamageDistribution(attacker, defender, isDungeon) {
  4242. let damage = attacker.damage;
  4243. damage *= getDamageTriangleModifier(attacker, defender);
  4244. damage *= getDamageScalingRatio(attacker, defender);
  4245. damage *= getDamageArmourRatio(attacker, defender);
  4246. damage *= !isDungeon ? 1 : attacker.dungeonDamage;
  4247.  
  4248. const maxDamage_ = new Distribution(damage);
  4249. // crit
  4250. if(attacker.critChance) {
  4251. maxDamage_.convolution(
  4252. Distribution.getRandomChance(attacker.critChance),
  4253. (dmg, crit) => dmg * (crit ? 1.5 : 1)
  4254. );
  4255. }
  4256. // damage range
  4257. const result = maxDamage_.convolutionWithGenerator(
  4258. dmg => Distribution.getRandomOutcomeRounded(dmg * attacker.damageRange, dmg),
  4259. (dmg, randomDamage) => randomDamage
  4260. );
  4261. // block
  4262. if(defender.blockChance) {
  4263. result.convolution(
  4264. Distribution.getRandomChance(defender.blockChance),
  4265. (dmg, blocked) => blocked ? 0 : dmg
  4266. );
  4267. }
  4268. // stun
  4269. if(defender.stunChance) {
  4270. let stunChance = defender.stunChance;
  4271. // only when defender accurate
  4272. stunChance *= getAccuracy(defender, attacker);
  4273. // can also happen on defender parries
  4274. stunChance *= 1 + defender.parryChance;
  4275. // modifier based on speed
  4276. stunChance *= attacker.attackSpeed / defender.attackSpeed;
  4277. // convert to actual stunned percentage
  4278. const stunnedPercentage = stunChance * 2.5 / attacker.attackSpeed;
  4279. result.convolution(
  4280. Distribution.getRandomChance(stunnedPercentage),
  4281. (dmg, stunned) => stunned ? 0 : dmg
  4282. );
  4283. }
  4284. // accuracy
  4285. const accuracy = getAccuracy(attacker, defender);
  4286. result.convolution(
  4287. Distribution.getRandomChance(accuracy),
  4288. (dmg, accurate) => accurate ? dmg : 0
  4289. );
  4290. // === special effects ===
  4291. const intermediateClone_ = result.clone();
  4292. // parry attacker - deal back 25% of a regular attack
  4293. if(attacker.parryChance) {
  4294. let parryChance = attacker.parryChance;
  4295. if(attacker.attackSpeed < defender.attackSpeed) {
  4296. parryChance *= attacker.attackSpeed / defender.attackSpeed;
  4297. }
  4298. const parried_ = intermediateClone_.clone();
  4299. parried_.convolution(
  4300. Distribution.getRandomChance(parryChance),
  4301. (dmg, parried) => parried ? Math.round(dmg/4.0) : 0
  4302. );
  4303. result.convolution(
  4304. parried_,
  4305. (dmg, extra) => dmg + extra
  4306. );
  4307. if(attacker.attackSpeed > defender.attackSpeed) {
  4308. // we can parry multiple times during one turn
  4309. parryChance *= (attacker.attackSpeed - defender.attackSpeed) / attacker.attackSpeed;
  4310. parried_.convolution(
  4311. Distribution.getRandomChance(parryChance),
  4312. (dmg, parried) => parried ? dmg : 0
  4313. );
  4314. result.convolution(
  4315. parried_,
  4316. (dmg, extra) => dmg + extra
  4317. );
  4318. }
  4319. }
  4320. // parry defender - deal 50% of a regular attack
  4321. if(defender.parryChance) {
  4322. result.convolution(
  4323. Distribution.getRandomChance(defender.parryChance),
  4324. (dmg, parried) => parried ? Math.round(dmg/2) : dmg
  4325. );
  4326. }
  4327. // bleed - 50% of damage over 3 seconds (assuming to be within one attack round)
  4328. if(attacker.bleedChance) {
  4329. const bleed_ = intermediateClone_.clone();
  4330. bleed_.convolution(
  4331. Distribution.getRandomChance(attacker.bleedChance),
  4332. (dmg, bleed) => bleed ? 5 * Math.round(dmg/10) : 0
  4333. );
  4334. result.convolution(
  4335. bleed_,
  4336. (dmg, extra) => dmg + extra
  4337. );
  4338. }
  4339. return result;
  4340. }
  4341.  
  4342. function getDamageTriangleModifier(attacker, defender) {
  4343. if(!attacker.attackStyle || !defender.attackStyle) {
  4344. return 1.0;
  4345. }
  4346. if(attacker.attackStyle === defender.attackStyle) {
  4347. return 1.0;
  4348. }
  4349. if(attacker.attackStyle === 'OneHanded' && defender.attackStyle === 'Ranged') {
  4350. return 1.1;
  4351. }
  4352. if(attacker.attackStyle === 'Ranged' && defender.attackStyle === 'TwoHanded') {
  4353. return 1.1;
  4354. }
  4355. if(attacker.attackStyle === 'TwoHanded' && defender.attackStyle === 'OneHanded') {
  4356. return 1.1;
  4357. }
  4358. return 0.9;
  4359. }
  4360.  
  4361. function getExpTriangleModifier(attacker, defender) {
  4362. if(!attacker.attackStyle || !defender.attackStyle) {
  4363. return 1;
  4364. }
  4365. return getDamageTriangleModifier(attacker, defender) - 0.1;
  4366. }
  4367.  
  4368. function getDamageScalingRatio(attacker, defender) {
  4369. const ratio = attacker.attackLevel / defender.defenseLevel;
  4370. if(attacker.isPlayer) {
  4371. return Math.min(1, ratio);
  4372. }
  4373. return Math.max(1, ratio);
  4374. }
  4375.  
  4376. function getDamageArmourRatio(attacker, defender) {
  4377. if(!defender.armour) {
  4378. return 1;
  4379. }
  4380. const scale = 25 + Math.min(70, (defender.armour - 25) * 50 / 105);
  4381. return (100 - scale) / 100;
  4382. }
  4383.  
  4384. function getAccuracy(attacker, defender) {
  4385. let accuracy = 75 + (attacker.attackLevel - defender.defenseLevel) / 2.0;
  4386. accuracy = Math.max(60, accuracy);
  4387. accuracy = Math.min(90, accuracy);
  4388. return accuracy / 100;
  4389. }
  4390.  
  4391. function getDamageDistributions(monsterId) {
  4392. const playerStats = getPlayerStats();
  4393. const monsterStats = getMonsterStats(monsterId);
  4394. const playerDamage_ = getInternalDamageDistribution(playerStats, monsterStats);
  4395. const monsterDamage_ = getInternalDamageDistribution(monsterStats, playerStats);
  4396. playerDamage_.normalize();
  4397. monsterDamage_.normalize();
  4398. return [playerDamage_, monsterDamage_];
  4399. }
  4400.  
  4401. function getSurvivalChance(player, monster, loopsPerFight, fights = 10, applyCringeMultiplier = false) {
  4402. const loopsPerAttack = monster.attackSpeed * 10;
  4403. let attacksPerFight = loopsPerFight / loopsPerAttack;
  4404. if(fights === 1 && applyCringeMultiplier) {
  4405. const playerLoopsPerAttack = player.attackSpeed * 10;
  4406. const playerAttacksPerFight = loopsPerFight / playerLoopsPerAttack;
  4407. const cringeMultiplier = Math.min(1.4, Math.max(1, 1.4 - playerAttacksPerFight / 50));
  4408. attacksPerFight *= cringeMultiplier;
  4409. }
  4410. const foodPerAttack = loopsPerAttack / estimatorAction.LOOPS_PER_FOOD;
  4411. const healPerFood = statsStore.get('HEAL') * (1 + statsStore.get('FOOD_EFFECT') / 100);
  4412. const healPerAttack = Math.round(healPerFood * foodPerAttack);
  4413. const healPerFight = healPerAttack * attacksPerFight;
  4414. let deathChance = 0;
  4415. let scenarioChance = 1;
  4416. let health = player.health;
  4417. for(let i=0;i<fights;i++) {
  4418. const currentDeathChance = monster.damage_.getRightTail(attacksPerFight, health + healPerFight);
  4419. deathChance += currentDeathChance * scenarioChance;
  4420. scenarioChance *= 1 - currentDeathChance;
  4421. const damage = monster.damage_.getMeanRange(attacksPerFight, healPerFight, health + healPerFight);
  4422. health -= damage - healPerFight;
  4423. if(isNaN(health) || health === Infinity || health === -Infinity) {
  4424. // TODO NaN / Infinity result from above?
  4425. break;
  4426. }
  4427. }
  4428. const cringeCutoff = 0.10;
  4429. if(fights === 1 && !applyCringeMultiplier && deathChance < cringeCutoff) {
  4430. const other = getSurvivalChance(player, monster, loopsPerFight, fights, true);
  4431. const avg = (1 - deathChance + other) / 2;
  4432. if(avg > 1 - cringeCutoff / 2) {
  4433. return avg;
  4434. }
  4435. }
  4436. return 1 - deathChance;
  4437. }
  4438.  
  4439. return exports;
  4440.  
  4441. }
  4442. );
  4443. // estimatorExpeditions
  4444. window.moduleRegistry.add('estimatorExpeditions', (events, estimator, components, petUtil, util, skillCache, itemCache, petCache, colorMapper, petHighlighter, configuration, expeditionDropCache) => {
  4445.  
  4446. let enabled = false;
  4447.  
  4448. const exports = {
  4449. get
  4450. };
  4451.  
  4452. function initialise() {
  4453. configuration.registerCheckbox({
  4454. category: 'Pets',
  4455. key: 'pet-estimations',
  4456. name: 'Estimations',
  4457. default: true,
  4458. handler: handleConfigStateChange
  4459. });
  4460. events.register('page', update);
  4461. events.register('state-stats', update);
  4462. }
  4463.  
  4464. function handleConfigStateChange(state) {
  4465. enabled = state;
  4466. }
  4467.  
  4468. function update() {
  4469. if(!enabled) {
  4470. return;
  4471. }
  4472. const page = events.getLast('page');
  4473. if(page?.type === 'taming' && page.menu === 'expeditions' && page.tier) {
  4474. const estimation = get(page.tier);
  4475. estimator.enrichTimings(estimation);
  4476. estimator.enrichValues(estimation);
  4477. preRender(estimation, componentBlueprint);
  4478. estimator.preRenderItems(estimation, componentBlueprint);
  4479. components.addComponent(componentBlueprint);
  4480. return;
  4481. }
  4482. components.removeComponent(componentBlueprint);
  4483. }
  4484.  
  4485. function get(tier) {
  4486. const teamStats = events.getLast('state-pet')
  4487. .filter(pet => pet.partOfTeam)
  4488. .map(petUtil.petToStats);
  4489. const totalStats = util.sumObjects(teamStats);
  4490. const expedition = petUtil.getExpeditionStats(tier);
  4491. const successChance = getSuccessChance(totalStats, expedition);
  4492.  
  4493. const ingredients = {
  4494. [itemCache.byName['Pet Snacks'].id]: Math.floor(expedition.food / 4 * (1 + totalStats.hunger / 100)) * 4
  4495. };
  4496.  
  4497. const drops = {};
  4498. const expeditionDrops = expeditionDropCache.byExpedition[expedition.id];
  4499. for(const drop of expeditionDrops) {
  4500. if(totalStats[drop.type]) {
  4501. drops[drop.item] = drop.amount * totalStats[drop.type];
  4502. }
  4503. }
  4504.  
  4505. return {
  4506. tier,
  4507. successChance,
  4508. ingredients,
  4509. drops,
  4510. teamStats,
  4511. totalStats,
  4512. exp: expedition.exp,
  4513. skill: skillCache.byName['Taming'].id,
  4514. equipments: {}
  4515. };
  4516. }
  4517.  
  4518. function getSuccessChance(stats, expedition) {
  4519. const attackRatio = Math.max(stats.attack / expedition.stats.defense, stats.specialAttack / expedition.stats.specialDefense);
  4520. let defenseRatio = 0;
  4521. if(expedition.rotation.attack) {
  4522. defenseRatio = expedition.stats.attack / stats.defense;
  4523. } else {
  4524. defenseRatio = expedition.stats.specialAttack / stats.specialDefense;
  4525. }
  4526. const damageRatio = attackRatio / defenseRatio;
  4527. const healthRatio = stats.health / expedition.stats.health;
  4528. const speedRatio = stats.speed / expedition.stats.speed;
  4529. const successChance = 100 * damageRatio * healthRatio * speedRatio + stats.stealth;
  4530. return Math.min(100, Math.max(0, successChance));
  4531. }
  4532.  
  4533. function preRender(estimation, blueprint) {
  4534. components.search(blueprint, 'successChance').value
  4535. = util.formatNumber(estimation.successChance);
  4536. components.search(blueprint, 'exp').value
  4537. = util.formatNumber(estimation.exp);
  4538. components.search(blueprint, 'expActual').value
  4539. = util.formatNumber(estimation.exp * estimation.successChance / 100);
  4540. components.search(blueprint, 'levelTime').value
  4541. = util.secondsToDuration(estimation.timings.level);
  4542. components.search(blueprint, 'tierTime').value
  4543. = util.secondsToDuration(estimation.timings.tier);
  4544. components.search(blueprint, 'dropValue').value
  4545. = util.formatNumber(estimation.values.drop);
  4546. components.search(blueprint, 'ingredientValue').value
  4547. = util.formatNumber(estimation.values.ingredient);
  4548. components.search(blueprint, 'netValue').value
  4549. = util.formatNumber(estimation.values.net);
  4550. components.search(blueprint, 'teamSize').value
  4551. = util.formatNumber(estimation.teamStats.length);
  4552. for(const stat of petUtil.STATS_BASE) {
  4553. components.search(blueprint, `teamStat-${stat}`).value
  4554. = util.formatNumber(estimation.totalStats[stat]);
  4555. }
  4556. for(const stat of petUtil.STATS_SPECIAL) {
  4557. components.search(blueprint, `teamStat-${stat}`).value
  4558. = util.formatNumber(estimation.totalStats[stat]) + ' %';
  4559. }
  4560. }
  4561.  
  4562. function calculateOptimizedTeam() {
  4563. const petsAndStats = events.getLast('state-pet')
  4564. .filter(pet => pet.parsed)
  4565. .map(pet => ({
  4566. pet,
  4567. stats: petUtil.petToStats(pet)
  4568. }));
  4569. // make all combinations of 3 pets of different species (same family is allowed)
  4570. const combinations = util.generateCombinations(petsAndStats, 3, object => object.pet.species);
  4571. if(!combinations.length) {
  4572. return;
  4573. }
  4574. console.debug(`Calculating ${combinations.length} team combinations`);
  4575. const tier = events.getLast('page').tier;
  4576. const expedition = petUtil.getExpeditionStats(tier);
  4577. let bestSuccessChance = 0;
  4578. let bestCombination = null;
  4579. for(const combination of combinations) {
  4580. const teamStats = combination.map(a => a.stats);
  4581. const totalStats = util.sumObjects(teamStats);
  4582. const successChance = getSuccessChance(totalStats, expedition);
  4583. if(successChance > bestSuccessChance) {
  4584. bestCombination = combination;
  4585. }
  4586. }
  4587.  
  4588. const teamRows = components.search(componentBlueprint, 'optimalTeamRows');
  4589. teamRows.rows = [{
  4590. type: 'header',
  4591. title: `Expedition T${tier} : ${expedition.name} (${combinations.length} combinations)`,
  4592. name: 'Highlight',
  4593. action: () => {
  4594. const color = colorMapper('success');
  4595. petHighlighter.highlight(color, bestCombination.map(a => a.pet.name));
  4596. $('taming-page .header:contains("Menu") ~ button:contains("Pets")').click()
  4597. }
  4598. }];
  4599. for(const object of bestCombination) {
  4600. teamRows.rows.push({
  4601. type: 'item',
  4602. name: object.pet.name,
  4603. image: `/assets/${petCache.byId[object.pet.species].image}`,
  4604. imagePixelated: true,
  4605. });
  4606. }
  4607. components.addComponent(componentBlueprint);
  4608. }
  4609.  
  4610. const componentBlueprint = {
  4611. componentId: 'tamingEstimatorComponent',
  4612. dependsOn: 'taming-page',
  4613. parent: 'taming-page > .groups > .group:last-child',
  4614. selectedTabIndex: 0,
  4615. tabs: [{
  4616. title: 'Overview',
  4617. rows: [{
  4618. type: 'item',
  4619. id: 'successChance',
  4620. name: 'Success chance',
  4621. image: 'https://cdn-icons-png.flaticon.com/512/3004/3004458.png',
  4622. value: ''
  4623. },{
  4624. type: 'item',
  4625. id: 'exp',
  4626. name: 'Exp/hour',
  4627. image: 'https://cdn-icons-png.flaticon.com/512/616/616490.png',
  4628. value: ''
  4629. },{
  4630. type: 'item',
  4631. id: 'expActual',
  4632. name: 'Exp/hour (weighted)',
  4633. image: 'https://cdn-icons-png.flaticon.com/512/616/616490.png',
  4634. value: ''
  4635. },{
  4636. type: 'item',
  4637. id: 'levelTime',
  4638. name: 'Level up',
  4639. image: 'https://cdn-icons-png.flaticon.com/512/4614/4614145.png',
  4640. value: ''
  4641. },{
  4642. type: 'item',
  4643. id: 'tierTime',
  4644. name: 'Tier up',
  4645. image: 'https://cdn-icons-png.flaticon.com/512/4789/4789514.png',
  4646. value: ''
  4647. },{
  4648. type: 'item',
  4649. id: 'dropValue',
  4650. name: 'Gold/hour (loot)',
  4651. image: 'https://cdn-icons-png.flaticon.com/512/9028/9028024.png',
  4652. imageFilter: 'invert(100%) sepia(47%) saturate(3361%) hue-rotate(313deg) brightness(106%) contrast(108%)',
  4653. value: ''
  4654. },{
  4655. type: 'item',
  4656. id: 'ingredientValue',
  4657. name: 'Gold/hour (materials)',
  4658. image: 'https://cdn-icons-png.flaticon.com/512/9028/9028031.png',
  4659. imageFilter: 'invert(100%) sepia(47%) saturate(3361%) hue-rotate(313deg) brightness(106%) contrast(108%)',
  4660. value: ''
  4661. },{
  4662. type: 'item',
  4663. id: 'netValue',
  4664. name: 'Gold/hour (total)',
  4665. image: 'https://cdn-icons-png.flaticon.com/512/11937/11937869.png',
  4666. imageFilter: 'invert(100%) sepia(47%) saturate(3361%) hue-rotate(313deg) brightness(106%) contrast(108%)',
  4667. value: ''
  4668. }]
  4669. },{
  4670. title: 'Items',
  4671. rows: [{
  4672. type: 'header',
  4673. title: 'Produced'
  4674. },{
  4675. type: 'segment',
  4676. id: 'dropRows',
  4677. rows: []
  4678. },{
  4679. type: 'header',
  4680. title: 'Consumed'
  4681. },{
  4682. type: 'segment',
  4683. id: 'ingredientRows',
  4684. rows: []
  4685. }]
  4686. },{
  4687. title: 'Time',
  4688. rows: [{
  4689. type: 'segment',
  4690. id: 'timeRows',
  4691. rows: []
  4692. }]
  4693. },{
  4694. title: 'Team',
  4695. rows: [{
  4696. type: 'header',
  4697. title: 'Calculate optimal team',
  4698. name: 'Run',
  4699. action: calculateOptimizedTeam
  4700. },{
  4701. type: 'segment',
  4702. id: 'optimalTeamRows',
  4703. rows: []
  4704. },{
  4705. type: 'header',
  4706. title: 'Stats'
  4707. },{
  4708. type: 'item',
  4709. id: 'teamSize',
  4710. name: 'Size',
  4711. image: 'https://img.icons8.com/?size=48&id=8183',
  4712. value: ''
  4713. },{
  4714. type: 'item',
  4715. id: 'teamStat-health',
  4716. name: 'Health',
  4717. image: petUtil.IMAGES.health,
  4718. value: ''
  4719. },{
  4720. type: 'item',
  4721. id: 'teamStat-speed',
  4722. name: 'Speed',
  4723. image: petUtil.IMAGES.speed,
  4724. value: ''
  4725. },{
  4726. type: 'item',
  4727. id: 'teamStat-attack',
  4728. name: 'Attack',
  4729. image: petUtil.IMAGES.attack,
  4730. value: ''
  4731. },{
  4732. type: 'item',
  4733. id: 'teamStat-specialAttack',
  4734. name: 'Special Attack',
  4735. image: petUtil.IMAGES.specialAttack,
  4736. value: ''
  4737. },{
  4738. type: 'item',
  4739. id: 'teamStat-defense',
  4740. name: 'Defense',
  4741. image: petUtil.IMAGES.defense,
  4742. value: ''
  4743. },{
  4744. type: 'item',
  4745. id: 'teamStat-specialDefense',
  4746. name: 'Special Defense',
  4747. image: petUtil.IMAGES.specialDefense,
  4748. value: ''
  4749. },{
  4750. type: 'item',
  4751. id: 'teamStat-hunger',
  4752. name: 'Hunger',
  4753. image: petUtil.IMAGES.hunger,
  4754. value: ''
  4755. },{
  4756. type: 'item',
  4757. id: 'teamStat-stealth',
  4758. name: 'Stealth',
  4759. image: petUtil.IMAGES.stealth,
  4760. value: ''
  4761. },{
  4762. type: 'item',
  4763. id: 'teamStat-loot',
  4764. name: 'Loot',
  4765. image: petUtil.IMAGES.loot,
  4766. value: ''
  4767. }]
  4768. }]
  4769. };
  4770.  
  4771. initialise();
  4772.  
  4773. return exports;
  4774.  
  4775. }
  4776. );
  4777. // estimatorOutskirts
  4778. window.moduleRegistry.add('estimatorOutskirts', (actionCache, itemCache, statsStore, estimatorActivity, estimatorCombat) => {
  4779.  
  4780. const exports = {
  4781. get
  4782. };
  4783.  
  4784. function get(skillId, actionId) {
  4785. try {
  4786. const action = actionCache.byId[actionId];
  4787.  
  4788. const activityEstimation = estimatorActivity.get(skillId, actionId);
  4789. const excludedItemIds = itemCache.specialIds.food.concat(itemCache.specialIds.combatPotion);
  4790. statsStore.update(new Set(excludedItemIds));
  4791. const combatEstimation = estimatorCombat.get(skillId, actionId);
  4792. const monsterChance = (1000 - action.outskirtsMonsterChance) / 1000;
  4793.  
  4794. // Axioms:
  4795. // combatRatio = 1 - activityRatio
  4796. // activityLoops = totalLoops * activityRatio
  4797. // combatLoops = totalLoops * combatRatio
  4798. // fights = combatLoops / combatSpeed
  4799. // actions = activityLoops / activitySpeed
  4800. // encounterChance = fights / (fights + actions)
  4801. const combatRatio = combatEstimation.speed / (activityEstimation.speed * (1 / monsterChance + combatEstimation.speed / activityEstimation.speed - 1));
  4802. const activityRatio = 1 - combatRatio;
  4803.  
  4804. const survivalChance = estimatorCombat.getSurvivalChance(combatEstimation.player, combatEstimation.monster, combatEstimation.speed, 1);
  4805.  
  4806. const exp = activityEstimation.exp * activityRatio;
  4807. const drops = {};
  4808. merge(drops, activityEstimation.drops, activityRatio);
  4809. merge(drops, combatEstimation.drops, combatRatio);
  4810. const ingredients = {};
  4811. merge(ingredients, activityEstimation.ingredients, activityRatio);
  4812. merge(ingredients, combatEstimation.ingredients, combatRatio);
  4813. const equipments = {};
  4814. merge(equipments, activityEstimation.equipments, activityRatio);
  4815. merge(equipments, combatEstimation.equipments, combatRatio);
  4816.  
  4817. return {
  4818. type: 'OUTSKIRTS',
  4819. skill: skillId,
  4820. speed: activityEstimation.speed,
  4821. productionSpeed: activityEstimation.productionSpeed,
  4822. exp,
  4823. drops,
  4824. ingredients,
  4825. equipments,
  4826. player: combatEstimation.player,
  4827. monster: combatEstimation.monster,
  4828. survivalChance
  4829. };
  4830. } finally {
  4831. statsStore.update(new Set());
  4832. }
  4833. }
  4834.  
  4835. function merge(target, source, ratio) {
  4836. for(const key in source) {
  4837. target[key] = (target[key] || 0) + source[key] * ratio;
  4838. }
  4839. }
  4840.  
  4841. return exports;
  4842.  
  4843.  
  4844.  
  4845. }
  4846. );
  4847. // guildSorts
  4848. window.moduleRegistry.add('guildSorts', (events, elementWatcher, util, elementCreator, configuration, colorMapper) => {
  4849.  
  4850. let enabled = false;
  4851.  
  4852. function initialise() {
  4853. configuration.registerCheckbox({
  4854. category: 'UI Features',
  4855. key: 'guild-sorts',
  4856. name: 'Guild sorts',
  4857. default: true,
  4858. handler: handleConfigStateChange
  4859. });
  4860. elementCreator.addStyles(styles);
  4861. events.register('page', setup);
  4862. }
  4863.  
  4864. function handleConfigStateChange(state) {
  4865. enabled = state;
  4866. }
  4867.  
  4868. async function setup() {
  4869. if(!enabled) {
  4870. return;
  4871. }
  4872. try {
  4873. await elementWatcher.exists('.card > .row');
  4874. if(events.getLast('page').type !== 'guild') {
  4875. return;
  4876. }
  4877. await addAdditionGuildSortButtons();
  4878. setupGuildMenuButtons();
  4879. } catch(e) {}
  4880. }
  4881.  
  4882. function setupGuildMenuButtons() {
  4883. $(`button > div.name:contains('Members')`).parent().on('click', async function () {
  4884. await util.sleep(50);
  4885. await addAdditionGuildSortButtons();
  4886. });
  4887. }
  4888.  
  4889. async function addAdditionGuildSortButtons() {
  4890. await elementWatcher.exists('div.sort');
  4891. const orginalButtonGroup = $('div.sort').find('div.container');
  4892.  
  4893. // rename daily to daily xp
  4894. $(`button:contains('Daily')`).text('Daily XP');
  4895. // fix text on 2 lines
  4896. $('div.sort').find('button').addClass('overrideFlex');
  4897. // attach clear custom to game own sorts
  4898. $('div.sort').find('button').on('click', function() {
  4899. clearCustomActiveButtons()
  4900. });
  4901.  
  4902. const customButtonGroup = $('<div/>')
  4903. .addClass('customButtonGroup')
  4904. .addClass('alignButtonGroupLeft')
  4905. .attr('id', 'guildSortButtonGroup')
  4906. .append(
  4907. $('<button/>')
  4908. .attr('type', 'button')
  4909. .addClass('customButtonGroupButton')
  4910. .addClass('customSortByLevel')
  4911. .text('Level')
  4912. .click(sortByLevel)
  4913. )
  4914. .append(
  4915. $('<button/>')
  4916. .attr('type', 'button')
  4917. .addClass('customButtonGroupButton')
  4918. .addClass('customSortByIdle')
  4919. .text('Idle')
  4920. .click(sortByIdle)
  4921. )
  4922. .append(
  4923. $('<button/>')
  4924. .attr('type', 'button')
  4925. .addClass('customButtonGroupButton')
  4926. .addClass('customSortByTotalXP')
  4927. .text('Total XP')
  4928. .click(sortByXp)
  4929. );
  4930.  
  4931. customButtonGroup.insertAfter(orginalButtonGroup);
  4932. }
  4933.  
  4934. function clearCustomActiveButtons() {
  4935. $('.customButtonGroupButton').removeClass('custom-sort-active');
  4936. }
  4937.  
  4938. function clearActiveButtons() {
  4939. $('div.sort').find('button').removeClass('sort-active');
  4940. }
  4941.  
  4942. function sortByXp() {
  4943. $(`button:contains('Date')`).trigger('click');
  4944.  
  4945. clearCustomActiveButtons();
  4946. clearActiveButtons();
  4947. $('.customSortByTotalXP').addClass('custom-sort-active');
  4948.  
  4949. const parent = $('div.sort').parent();
  4950. sortElements({
  4951. elements: parent.find('button.row'),
  4952. extractor: a => util.parseNumber($(a).find('div.amount').text()),
  4953. sorter: (a,b) => b-a,
  4954. target: parent
  4955. });
  4956. }
  4957.  
  4958. function sortByIdle() {
  4959. // make sure the last contributed time is visible
  4960. if(
  4961. !$(`div.sort button:contains('Date')`).hasClass('sort-active') &&
  4962. !$(`button:contains('Daily XP')`).hasClass('sort-active')
  4963. ) {
  4964. $(`button:contains('Date')`).trigger('click');
  4965. }
  4966.  
  4967. clearCustomActiveButtons();
  4968. clearActiveButtons();
  4969. $('.customSortByIdle').addClass('custom-sort-active');
  4970.  
  4971. const parent = $('div.sort').parent();
  4972. sortElements({
  4973. elements: parent.find('button.row'),
  4974. extractor: a => util.parseDuration($(a).find('div.time').text()),
  4975. sorter: (a,b) => b-a,
  4976. target: parent
  4977. });
  4978. }
  4979.  
  4980. function sortByLevel() {
  4981. clearCustomActiveButtons();
  4982. clearActiveButtons();
  4983. $('.customSortByLevel').addClass('custom-sort-active');
  4984.  
  4985. const parent = $('div.sort').parent();
  4986. sortElements({
  4987. elements: parent.find('button.row'),
  4988. extractor: a => util.parseNumber($(a).find('div.level').text().replace('Lv. ', '')),
  4989. sorter: (a,b) => b-a,
  4990. target: parent
  4991. });
  4992. }
  4993.  
  4994. // sorts a list of `elements` according to the extracted property from `extractor`,
  4995. // sorts them using `sorter`, and appends them to the `target`
  4996. // elements is a jquery list
  4997. // target is a jquery element
  4998. // { elements, target, extractor, sorter }
  4999. function sortElements(config) {
  5000. const list = config.elements.get().map(element => ({
  5001. element,
  5002. value: config.extractor(element)
  5003. }));
  5004. list.sort((a,b) => config.sorter(a.value, b.value));
  5005. for(const item of list) {
  5006. config.target.append(item.element);
  5007. }
  5008. }
  5009.  
  5010. const styles = `
  5011. .alignButtonGroupLeft {
  5012. margin-right: auto;
  5013. margin-left: 8px;
  5014. }
  5015. .customButtonGroup {
  5016. display: flex;
  5017. align-items: center;
  5018. border-radius: 4px;
  5019. box-shadow: 0 1px 2px #0003;
  5020. border: 1px solid #263849;
  5021. overflow: hidden;
  5022. }
  5023. .customButtonGroupButton {
  5024. padding: 4px var(--gap);
  5025. flex: none !important;
  5026. text-align: center;
  5027. justify-content: center;
  5028. background-color: ${colorMapper('componentRegular')};
  5029. }
  5030. .customButtonGroupButton:not(:first-of-type) {
  5031. border-left: 1px solid #263849;
  5032. }
  5033. .overrideFlex {
  5034. flex: none !important
  5035. }
  5036. .custom-sort-active {
  5037. background-color: ${colorMapper('componentLight')};
  5038. }
  5039. `;
  5040.  
  5041. initialise();
  5042. }
  5043. );
  5044. // idleBeep
  5045. window.moduleRegistry.add('idleBeep', (configuration, events, util) => {
  5046.  
  5047. const audio = new Audio('data:audio/mpeg;base64,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');
  5048. const sleepAmount = 2000;
  5049. let enabled = false;
  5050. let started = false;
  5051.  
  5052. function initialise() {
  5053. configuration.registerCheckbox({
  5054. category: 'Other',
  5055. key: 'idle-beep-enabled',
  5056. name: 'Idle beep',
  5057. default: false,
  5058. handler: handleConfigStateChange
  5059. });
  5060. events.register('xhr', handleXhr);
  5061. }
  5062.  
  5063. function handleConfigStateChange(state, name) {
  5064. enabled = state;
  5065. }
  5066.  
  5067. async function handleXhr(xhr) {
  5068. if(!enabled) {
  5069. return;
  5070. }
  5071. if(xhr.url.endsWith('startAction')) {
  5072. started = true;
  5073. }
  5074. if(xhr.url.endsWith('stopAction')) {
  5075. started = false;
  5076. console.debug(`Triggering beep in ${sleepAmount}ms`);
  5077. await util.sleep(sleepAmount);
  5078. beep();
  5079. }
  5080. }
  5081.  
  5082. function beep() {
  5083. if(!started) {
  5084. audio.play();
  5085. }
  5086. }
  5087.  
  5088. initialise();
  5089.  
  5090. }
  5091. );
  5092. // itemHover
  5093. window.moduleRegistry.add('itemHover', (configuration, itemCache, util) => {
  5094.  
  5095. let enabled = false;
  5096. let entered = false;
  5097. let element;
  5098. const converters = {
  5099. SPEED: a => a/2,
  5100. DURATION: a => util.secondsToDuration(a/10)
  5101. }
  5102.  
  5103. function initialise() {
  5104. configuration.registerCheckbox({
  5105. category: 'UI Features',
  5106. key: 'item-hover',
  5107. name: 'Item hover info',
  5108. default: true,
  5109. handler: handleConfigStateChange
  5110. });
  5111. setup();
  5112. $(document).on('mouseenter', 'div.image > img', handleMouseEnter);
  5113. $(document).on('mouseleave', 'div.image > img', handleMouseLeave);
  5114. $(document).on('click', 'div.image > img', handleMouseLeave);
  5115. }
  5116.  
  5117. function handleConfigStateChange(state) {
  5118. enabled = state;
  5119. }
  5120.  
  5121. function handleMouseEnter(event) {
  5122. if(!enabled || entered || !itemCache.byId) {
  5123. return;
  5124. }
  5125. entered = true;
  5126. const name = $(event.relatedTarget).find('.name').text();
  5127. const nameMatch = itemCache.byName[name];
  5128. if(nameMatch) {
  5129. return show(nameMatch);
  5130. }
  5131.  
  5132. const parts = event.target.src.split('/');
  5133. const lastPart = parts[parts.length-1];
  5134. const imageMatch = itemCache.byImage[lastPart];
  5135. if(imageMatch) {
  5136. return show(imageMatch);
  5137. }
  5138. }
  5139.  
  5140. function handleMouseLeave(event) {
  5141. if(!enabled || !itemCache.byId) {
  5142. return;
  5143. }
  5144. entered = false;
  5145. hide();
  5146. }
  5147.  
  5148. function show(item) {
  5149. element.find('.image').attr('src', `/assets/${item.image}`);
  5150. element.find('.name').text(item.name);
  5151. for(const attribute of itemCache.attributes) {
  5152. let value = item.attributes[attribute.technicalName];
  5153. if(value && converters[attribute.technicalName]) {
  5154. value = converters[attribute.technicalName](value);
  5155. }
  5156. if(value && Number.isInteger(value)) {
  5157. value = util.formatNumber(value);
  5158. }
  5159. updateRow(attribute.technicalName, value);
  5160. }
  5161. element.show();
  5162. }
  5163.  
  5164. function updateRow(name, value) {
  5165. if(!value) {
  5166. element.find(`.${name}-row`).hide();
  5167. } else {
  5168. element.find(`.${name}`).text(value);
  5169. element.find(`.${name}-row`).show();
  5170. }
  5171. }
  5172.  
  5173. function hide() {
  5174. element.hide();
  5175. }
  5176.  
  5177. function setup() {
  5178. const attributesHtml = itemCache.attributes
  5179. .map(a => `<div class='${a.technicalName}-row'><img src='${a.image}'/><span>${a.name}</span><span class='${a.technicalName}'/></div>`)
  5180. .join('');
  5181. $('head').append(`
  5182. <style>
  5183. #custom-item-hover {
  5184. position: fixed;
  5185. right: .5em;
  5186. top: .5em;
  5187. display: flex;
  5188. font-family: Jost,Helvetica Neue,Arial,sans-serif;
  5189. flex-direction: column;
  5190. white-space: nowrap;
  5191. z-index: 1;
  5192. background-color: black;
  5193. padding: .4rem;
  5194. border: 1px solid #3e3e3e;
  5195. border-radius: .4em;
  5196. gap: .4em;
  5197. }
  5198. #custom-item-hover > div {
  5199. display: flex;
  5200. gap: .4em;
  5201. }
  5202. #custom-item-hover > div > *:last-child {
  5203. margin-left: auto;
  5204. }
  5205. #custom-item-hover img {
  5206. width: 24px;
  5207. height: 24px;
  5208. image-rendering: auto;
  5209. }
  5210. #custom-item-hover img.pixelated {
  5211. image-rendering: pixelated;
  5212. }
  5213. </style>
  5214. `);
  5215. element = $(`
  5216. <div id='custom-item-hover' style='display:none'>
  5217. <div>
  5218. <img class='image pixelated'/>
  5219. <span class='name'/>
  5220. </div>
  5221. ${attributesHtml}
  5222. </div>
  5223. `);
  5224. $('body').append(element);
  5225. }
  5226.  
  5227. initialise();
  5228.  
  5229. }
  5230. );
  5231. // marketCompetition
  5232. window.moduleRegistry.add('marketCompetition', (configuration, events, toast, util, elementCreator, colorMapper) => {
  5233.  
  5234. let enabled = false;
  5235.  
  5236. function initialise() {
  5237. configuration.registerCheckbox({
  5238. category: 'Data',
  5239. key: 'market-competition',
  5240. name: 'Market competition indicator',
  5241. default: false,
  5242. handler: handleConfigStateChange
  5243. });
  5244. events.register('state-market', handleMarketData);
  5245. elementCreator.addStyles(styles);
  5246. }
  5247.  
  5248. function handleConfigStateChange(state) {
  5249. enabled = state;
  5250. }
  5251.  
  5252. function handleMarketData(marketData) {
  5253. if(!enabled || marketData.lastType !== 'OWN') {
  5254. return;
  5255. }
  5256. const page = events.getLast('page');
  5257. if(page.type !== 'market') {
  5258. return;
  5259. }
  5260. showToasts(marketData);
  5261. showCircles(marketData);
  5262. }
  5263.  
  5264. function showToasts(marketData) {
  5265. if(!marketData.SELL) {
  5266. toast.create({
  5267. text: 'Missing "Buy" listing data for the competition checker'
  5268. });
  5269. }
  5270. if(!marketData.BUY) {
  5271. toast.create({
  5272. text: 'Missing "Orders" listing data for the competition checker'
  5273. });
  5274. }
  5275. }
  5276.  
  5277. function showCircles(marketData) {
  5278. $('.market-competition').remove();
  5279. for(const listing of marketData.OWN) {
  5280. if(!marketData[listing.type]) {
  5281. continue;
  5282. }
  5283. const matching = marketData[listing.type].filter(a => !a.isOwn && a.item === listing.item);
  5284. const same = matching.filter(a => a.price === listing.price);
  5285. const better = matching.filter(a =>
  5286. (listing.type === 'SELL' && a.price < listing.price) ||
  5287. (listing.type === 'BUY' && a.price > listing.price)
  5288. );
  5289. if(!same.length && !better.length) {
  5290. continue;
  5291. }
  5292. const color = better.length ? 'danger' : 'warning';
  5293. const text = better.concat(same)
  5294. .map(a => `${util.formatNumber(a.amount)} @ ${util.formatNumber(a.price)}`)
  5295. .join(' / ');
  5296. listing.element.find('.cost').before(`<div class='market-competition market-competition-${color}' title='${text}'></div>`);
  5297. }
  5298. }
  5299.  
  5300. const styles = `
  5301. .market-competition {
  5302. width: 16px;
  5303. height: 16px;
  5304. border-radius: 50%;
  5305. }
  5306.  
  5307. .market-competition-warning {
  5308. background-color: ${colorMapper('warning')}
  5309. }
  5310.  
  5311. .market-competition-danger {
  5312. background-color: ${colorMapper('danger')}
  5313. }
  5314. `;
  5315.  
  5316. initialise();
  5317.  
  5318. }
  5319. );
  5320. // marketFilter
  5321. window.moduleRegistry.add('marketFilter', (configuration, localDatabase, events, components, elementWatcher, Promise, itemCache, dropCache, marketReader, elementCreator) => {
  5322.  
  5323. const STORE_NAME = 'market-filters';
  5324. const TYPE_TO_ITEM = {
  5325. 'Food': itemCache.byName['Health'].id,
  5326. 'Charcoal': itemCache.byName['Charcoal'].id,
  5327. 'Compost': itemCache.byName['Compost'].id,
  5328. 'Arcane Powder': itemCache.byName['Arcane Powder'].id,
  5329. 'Pet Snacks': itemCache.byName['Pet Snacks'].id,
  5330. };
  5331. let savedFilters = [];
  5332. let enabled = false;
  5333. let currentFilter = {
  5334. type: 'None',
  5335. amount: 0,
  5336. key: 'SELL-None'
  5337. };
  5338. let pageInitialised = false;
  5339. let listingsUpdatePromise = null;
  5340.  
  5341. async function initialise() {
  5342. configuration.registerCheckbox({
  5343. category: 'Data',
  5344. key: 'market-filter',
  5345. name: 'Market filter',
  5346. default: true,
  5347. handler: handleConfigStateChange
  5348. });
  5349. events.register('page', update);
  5350. events.register('state-market', update);
  5351.  
  5352. savedFilters = await localDatabase.getAllEntries(STORE_NAME);
  5353.  
  5354. // detect elements changing
  5355.  
  5356. // clear filters when searching yourself
  5357. $(document).on('click', 'market-listings-component .search > .clear-button', clearFilter);
  5358. $(document).on('input', 'market-listings-component .search > input', clearFilter);
  5359.  
  5360. // Buy tab -> trigger update
  5361. $(document).on('click', 'market-listings-component .card > .tabs > :nth-child(1)', function() {
  5362. showComponent();
  5363. marketReader.trigger();
  5364. });
  5365. $(document).on('click', 'market-listings-component .card > .tabs > :nth-child(2)', function() {
  5366. showComponent();
  5367. marketReader.trigger();
  5368. });
  5369. $(document).on('click', 'market-listings-component .card > .tabs > :nth-child(3)', function() {
  5370. hideComponent();
  5371. marketReader.trigger();
  5372. });
  5373.  
  5374. elementCreator.addStyles(`
  5375. .greenOutline {
  5376. outline: 2px solid rgb(83, 189, 115) !important;
  5377. }
  5378. `);
  5379.  
  5380. // on save hover, highlight saved fields
  5381. $(document).on('mouseenter mouseleave click', '.saveFilterHoverTrigger', function(e) {
  5382. switch(e.type) {
  5383. case 'mouseenter':
  5384. if(currentFilter.type === 'None') {
  5385. return $('.saveFilterHover.search').addClass('greenOutline');
  5386. }
  5387. return $('.saveFilterHover:not(.search)').addClass('greenOutline');
  5388. case 'mouseleave':
  5389. case 'click':
  5390. return $('.saveFilterHover').removeClass('greenOutline');
  5391. }
  5392. });
  5393. }
  5394.  
  5395. function handleConfigStateChange(state) {
  5396. enabled = state;
  5397. }
  5398.  
  5399. function update() {
  5400. if(!enabled) {
  5401. return;
  5402. }
  5403. if(events.getLast('page')?.type !== 'market') {
  5404. pageInitialised = false;
  5405. return;
  5406. }
  5407. initialisePage();
  5408. $('market-listings-component .search').addClass('saveFilterHover');
  5409. syncListingsView();
  5410. }
  5411.  
  5412. async function initialisePage() {
  5413. if(pageInitialised) {
  5414. return;
  5415. }
  5416. clearFilter();
  5417. try {
  5418. await elementWatcher.childAddedContinuous('market-listings-component .card', () => {
  5419. if(listingsUpdatePromise) {
  5420. listingsUpdatePromise.resolve();
  5421. listingsUpdatePromise = null;
  5422. }
  5423. });
  5424. pageInitialised = true;
  5425. } catch(error) {
  5426. console.warn(`Could probably not detect the market listing component, cause : ${error}`);
  5427. }
  5428. }
  5429.  
  5430. async function clearFilter() {
  5431. await applyFilter({
  5432. type: 'None',
  5433. amount: 0
  5434. });
  5435. syncCustomView();
  5436. }
  5437.  
  5438. async function applyFilter(filter) {
  5439. Object.assign(currentFilter, {search:null}, filter);
  5440. currentFilter.key = `${currentFilter.listingType}-${currentFilter.type}`;
  5441. if(currentFilter.type && currentFilter.type !== 'None') {
  5442. await clearSearch();
  5443. }
  5444. syncListingsView();
  5445. }
  5446.  
  5447. async function clearSearch() {
  5448. if(!$('market-listings-component .search > input').val()) {
  5449. return;
  5450. }
  5451. listingsUpdatePromise = new Promise.Expiring(5000, 'marketFilter - clearSearch');
  5452. setSearch('');
  5453. await listingsUpdatePromise;
  5454. marketReader.trigger();
  5455. }
  5456.  
  5457. function setSearch(value) {
  5458. const searchReference = $('market-listings-component .search > input');
  5459. searchReference.val(value);
  5460. searchReference[0].dispatchEvent(new Event('input'));
  5461. }
  5462.  
  5463. async function saveFilter() {
  5464. let filter = structuredClone(currentFilter);
  5465. if(currentFilter.type === 'None') {
  5466. filter.search = $('market-listings-component .search > input').val();
  5467. if(!filter.search) {
  5468. return;
  5469. }
  5470. }
  5471. if(filter.search) {
  5472. filter.key = `SEARCH-${filter.search}`;
  5473. } else {
  5474. filter.key = `${filter.type}-${filter.amount}`;
  5475. }
  5476. if(!savedFilters.find(a => a.key === filter.key)) {
  5477. localDatabase.saveEntry(STORE_NAME, filter);
  5478. savedFilters.push(filter);
  5479. }
  5480. componentBlueprint.selectedTabIndex = 0;
  5481. syncCustomView();
  5482. }
  5483.  
  5484. async function removeFilter(filter) {
  5485. localDatabase.removeEntry(STORE_NAME, filter.key);
  5486. savedFilters = savedFilters.filter(a => a.key !== filter.key);
  5487. syncCustomView();
  5488. }
  5489.  
  5490. function syncListingsView() {
  5491. const marketData = events.getLast('state-market');
  5492. if(!marketData) {
  5493. return;
  5494. }
  5495. // do nothing on own listings tab
  5496. if(marketData.lastType === 'OWN') {
  5497. resetListingsView(marketData);
  5498. return;
  5499. }
  5500. // search
  5501. if(currentFilter.search) {
  5502. resetListingsView(marketData);
  5503. setSearch(currentFilter.search);
  5504. return;
  5505. }
  5506. // no type
  5507. if(currentFilter.type === 'None') {
  5508. resetListingsView(marketData);
  5509. return;
  5510. }
  5511. // type
  5512. const itemId = TYPE_TO_ITEM[currentFilter.type];
  5513. const conversionsByItem = dropCache.conversionMappings[itemId].reduce((a,b) => (a[b.from] = b, a), {});
  5514. let matchingListings = marketData.last.filter(listing => listing.item in conversionsByItem);
  5515. for(const listing of matchingListings) {
  5516. listing.ratio = listing.price / conversionsByItem[listing.item].amount;
  5517. }
  5518. matchingListings.sort((a,b) => (a.type === 'BUY' ? 1 : -1) * (b.ratio - a.ratio));
  5519. if(currentFilter.amount) {
  5520. matchingListings = matchingListings.slice(0, currentFilter.amount);
  5521. }
  5522. for(const listing of marketData.last) {
  5523. if(matchingListings.includes(listing)) {
  5524. listing.element.show();
  5525. if(!listing.element.find('.ratio').length) {
  5526. listing.element.find('.amount').after(`<div class='ratio'>(${listing.ratio.toFixed(2)})</div>`);
  5527. }
  5528. } else {
  5529. listing.element.hide();
  5530. }
  5531. }
  5532. }
  5533.  
  5534. function resetListingsView(marketData) {
  5535. for(const element of marketData.last.map(a => a.element)) {
  5536. element.find('.ratio').remove();
  5537. element.show();
  5538. }
  5539. }
  5540.  
  5541. function syncCustomView() {
  5542. for(const option of components.search(componentBlueprint, 'filterDropdown').options) {
  5543. option.selected = option.value === currentFilter.type;
  5544. }
  5545. components.search(componentBlueprint, 'amountInput').value = currentFilter.amount;
  5546. components.search(componentBlueprint, 'savedFiltersTab').hidden = !savedFilters.length;
  5547. if(!savedFilters.length) {
  5548. componentBlueprint.selectedTabIndex = 1;
  5549. }
  5550. const savedFiltersSegment = components.search(componentBlueprint, 'savedFiltersSegment');
  5551. savedFiltersSegment.rows = [];
  5552. for(const savedFilter of savedFilters) {
  5553. let text = `Type : ${savedFilter.type}`;
  5554. if(savedFilter.amount) {
  5555. text = `Type : ${savedFilter.amount} x ${savedFilter.type}`;
  5556. }
  5557. if(savedFilter.search) {
  5558. text = `Search : ${savedFilter.search}`;
  5559. }
  5560. savedFiltersSegment.rows.push({
  5561. type: 'buttons',
  5562. buttons: [{
  5563. text: text,
  5564. size: 3,
  5565. color: 'primary',
  5566. action: async function() {
  5567. await applyFilter(savedFilter);
  5568. syncCustomView();
  5569. }
  5570. },{
  5571. text: 'Remove',
  5572. color: 'danger',
  5573. action: removeFilter.bind(null,savedFilter)
  5574. }]
  5575. });
  5576. }
  5577. showComponent();
  5578. }
  5579.  
  5580. function hideComponent() {
  5581. components.removeComponent(componentBlueprint);
  5582. }
  5583.  
  5584. function showComponent() {
  5585. componentBlueprint.prepend = screen.width < 750;
  5586. components.addComponent(componentBlueprint);
  5587. }
  5588.  
  5589. const componentBlueprint = {
  5590. componentId : 'marketFilterComponent',
  5591. dependsOn: 'market-page',
  5592. parent : 'market-listings-component > .groups > :last-child',
  5593. prepend: false,
  5594. selectedTabIndex : 0,
  5595. tabs : [{
  5596. id: 'savedFiltersTab',
  5597. title : 'Saved filters',
  5598. hidden: true,
  5599. rows: [{
  5600. type: 'segment',
  5601. id: 'savedFiltersSegment',
  5602. rows: []
  5603. }, {
  5604. type: 'buttons',
  5605. buttons: [{
  5606. text: 'Clear filter',
  5607. color: 'warning',
  5608. action: async function() {
  5609. await clearFilter();
  5610. await clearSearch();
  5611. }
  5612. }]
  5613. }]
  5614. }, {
  5615. title : 'Filter',
  5616. rows: [{
  5617. type: 'dropdown',
  5618. id: 'filterDropdown',
  5619. action: type => applyFilter({type}),
  5620. class: 'saveFilterHover',
  5621. options: [{
  5622. text: 'None',
  5623. value: 'None',
  5624. selected: false
  5625. }].concat(Object.keys(TYPE_TO_ITEM).map(a => ({
  5626. text: a,
  5627. value: a,
  5628. selected: false
  5629. })))
  5630. }, {
  5631. type: 'input',
  5632. id: 'amountInput',
  5633. name: 'Amount',
  5634. value: '',
  5635. inputType: 'number',
  5636. action: amount => applyFilter({amount:+amount}),
  5637. class: 'saveFilterHover'
  5638. }, {
  5639. type: 'buttons',
  5640. buttons: [{
  5641. text: 'Save filter',
  5642. action: saveFilter,
  5643. color: 'success',
  5644. class: 'saveFilterHoverTrigger'
  5645. }]
  5646. }, {
  5647. type: 'buttons',
  5648. buttons: [{
  5649. text: 'Clear filter',
  5650. color: 'warning',
  5651. action: async function() {
  5652. await clearFilter();
  5653. await clearSearch();
  5654. }
  5655. }]
  5656. }]
  5657. }]
  5658. };
  5659.  
  5660. initialise();
  5661.  
  5662. }
  5663. );
  5664. // petHighlighter
  5665. window.moduleRegistry.add('petHighlighter', (events) => {
  5666.  
  5667. const exports = {
  5668. highlight
  5669. };
  5670.  
  5671. let currentColor = null;
  5672. let currentNames = null;
  5673.  
  5674. function initialise() {
  5675. events.register('page', update);
  5676. events.register('state-pet', update);
  5677. }
  5678.  
  5679. function highlight(color, names) {
  5680. currentColor = color;
  5681. currentNames = names;
  5682. }
  5683.  
  5684. function update() {
  5685. if(!currentColor || !currentNames || !currentNames.length) {
  5686. return;
  5687. }
  5688. const page = events.getLast('page');
  5689. if(page?.type === 'taming' && page.menu === 'pets') {
  5690. events.getLast('state-pet')
  5691. .filter(pet => currentNames.includes(pet.name) && pet.element)
  5692. .forEach(pet => {
  5693. $(pet.element).css('box-shadow', `inset 0px 0px 8px 0px ${currentColor}`)
  5694. });
  5695. }
  5696. }
  5697.  
  5698. initialise();
  5699.  
  5700. return exports;
  5701.  
  5702. }
  5703. );
  5704. // petRenamer
  5705. window.moduleRegistry.add('petRenamer', (configuration, events, petUtil, elementCreator, toast) => {
  5706.  
  5707. let enabled = false;
  5708. let lastSeenPet;
  5709. let pasteButton;
  5710.  
  5711. function initialise() {
  5712. configuration.registerCheckbox({
  5713. category: 'Pets',
  5714. key: 'pet-rename',
  5715. name: 'Name suggestions',
  5716. default: false,
  5717. handler: handleConfigStateChange
  5718. });
  5719. events.register('reader-pet', handlePetReader);
  5720. $(document).on('click', 'modal-component .header .heading', onRename);
  5721. pasteButton = elementCreator.getButton('Paste encoded name', pasteName);
  5722. }
  5723.  
  5724. function handleConfigStateChange(state, name) {
  5725. enabled = state;
  5726. }
  5727.  
  5728. function handlePetReader(event) {
  5729. if(event.type === 'single') {
  5730. lastSeenPet = event.value;
  5731. }
  5732. }
  5733.  
  5734. function onRename() {
  5735. if(!enabled) {
  5736. return;
  5737. }
  5738. const page = events.getLast('page');
  5739. if(!page || page.type !== 'taming') {
  5740. return;
  5741. }
  5742. $('modal-component .header > .name').append(pasteButton);
  5743. }
  5744.  
  5745. function pasteName() {
  5746. const text = petUtil.petToText(lastSeenPet);
  5747. const input = $('modal-component input');
  5748. input.val(text);
  5749. input[0].dispatchEvent(new Event('input'));
  5750. toast.create({
  5751. text: 'Pasted encoded name',
  5752. image: 'https://img.icons8.com/?size=48&id=22244'
  5753. });
  5754. }
  5755.  
  5756. initialise();
  5757.  
  5758. });
  5759. // petStatHighlighter
  5760. window.moduleRegistry.add('petStatHighlighter', (configuration, events, util, colorMapper, petCache, petPassiveCache, petUtil) => {
  5761.  
  5762. let enabled = false;
  5763. const stats = petUtil.STATS_BASE;
  5764. const passiveStats = util.distinct(petPassiveCache.list.map(a => a.stats.name));
  5765. let highestValues = null;
  5766.  
  5767. function initialise() {
  5768. configuration.registerCheckbox({
  5769. category: 'Pets',
  5770. key: 'pet-stat-highlighter',
  5771. name: 'Highlight best stats [needs stat redesign]',
  5772. default: false,
  5773. handler: handleConfigStateChange
  5774. });
  5775. events.register('redesign-pet', renderMain);
  5776. events.register('reader-pet', renderSingle);
  5777. }
  5778.  
  5779. function handleConfigStateChange(state, name) {
  5780. enabled = state;
  5781. }
  5782.  
  5783. function renderMain(pets) {
  5784. if(!enabled || !pets.length) {
  5785. return;
  5786. }
  5787. highestValues = getHighestValuesByFamily(pets);
  5788. const color = colorMapper('success');
  5789. for(const pet of pets) {
  5790. const tags = $(pet.element).find('.tags');
  5791. highlight(pet, color, tags);
  5792. }
  5793. }
  5794.  
  5795. function renderSingle(event) {
  5796. if(!enabled || event.type !== 'single') {
  5797. return;
  5798. }
  5799. const pets = events.getLast('redesign-pet').slice(0);
  5800. pets.push(event.value);
  5801. highestValues = getHighestValuesByFamily(pets);
  5802. const color = colorMapper('success');
  5803. highlight(event.value, color, $(event.modal));
  5804. }
  5805.  
  5806. function highlight(pet, color, root) {
  5807. for(const stat of stats) {
  5808. if(pet[stat] === highestValues[pet.family][stat]) {
  5809. root.find(`.stat-${stat}`).css('box-shadow', `inset 0px 0px 8px 0px ${color}`);
  5810. } else {
  5811. root.find(`.stat-${stat}`).css('box-shadow', '');
  5812. }
  5813. }
  5814. for(const id of pet.passives) {
  5815. const passive = petPassiveCache.byId[id].stats;
  5816. if(passive.value === highestValues[pet.family][passive.name]) {
  5817. root.find(`.passive-${passive.name}`).css('box-shadow', `inset 0px 0px 8px 0px ${color}`);
  5818. } else {
  5819. root.find(`.passive-${passive.name}`).css('box-shadow', '');
  5820. }
  5821. }
  5822. }
  5823.  
  5824. function getHighestValuesByFamily(pets) {
  5825. const result = {};
  5826. for(const pet of pets) {
  5827. pet.family = petCache.byId[pet.species].family;
  5828. }
  5829. const families = util.distinct(pets.map(pet => pet.family));
  5830. for(const family of families) {
  5831. result[family] = {};
  5832. for(const stat of stats) {
  5833. result[family][stat] = pets
  5834. .filter(pet => pet.family === family)
  5835. .map(pet => pet[stat])
  5836. .sort((a,b) => b-a)[0] || 0;
  5837. }
  5838. for(const stat of passiveStats) {
  5839. result[family][stat] = pets
  5840. .filter(pet => pet.family === family)
  5841. .flatMap(pet => pet.passives)
  5842. .map(id => petPassiveCache.byId[id])
  5843. .filter(passive => passive.stats.name === stat)
  5844. .map(passive => passive.stats.value)
  5845. .sort((a,b) => b-a)[0] || 0;
  5846. }
  5847. }
  5848. return result;
  5849. }
  5850.  
  5851. initialise();
  5852.  
  5853. }
  5854. );
  5855. // petStatRedesign
  5856. window.moduleRegistry.add('petStatRedesign', (configuration, events, elementCreator, petTraitCache, petPassiveCache, colorMapper, petUtil) => {
  5857.  
  5858. let enabled = false;
  5859. const emitEvent = events.emit.bind(null, 'redesign-pet');
  5860.  
  5861. function initialise() {
  5862. configuration.registerCheckbox({
  5863. category: 'Pets',
  5864. key: 'pet-stat-redesign',
  5865. name: 'Stat redesign',
  5866. default: true,
  5867. handler: handleConfigStateChange
  5868. });
  5869. events.register('state-pet', update);
  5870. }
  5871.  
  5872. function handleConfigStateChange(state, name) {
  5873. enabled = state;
  5874. }
  5875.  
  5876. function update(state) {
  5877. if(!enabled) {
  5878. return;
  5879. }
  5880. let changed = false;
  5881. for(const pet of state.filter(pet => pet.default)) {
  5882. renderDefault(pet);
  5883. }
  5884. for(const pet of state.filter(pet => !pet.default && pet.duplicate)) {
  5885. renderDuplicate(pet);
  5886. }
  5887. const pets = state.filter(pet => !pet.default && !pet.duplicate && pet.parsed);
  5888. for(const pet of pets) {
  5889. if(renderParsed(pet)) {
  5890. changed = true;
  5891. }
  5892. }
  5893. if(changed) {
  5894. emitEvent(pets);
  5895. }
  5896. }
  5897.  
  5898. function renderDefault(pet) {
  5899. const tags = $(pet.element).find('.tags');
  5900. if(tags.find('.tag-default').length) {
  5901. return false;
  5902. }
  5903. const color = colorMapper('warning');
  5904. const tag = elementCreator.getTag('Default name', undefined, 'tag-default')
  5905. .css('box-shadow', `inset 0px 0px 8px 0px ${color}`);
  5906. tags.append(tag);
  5907. return true;
  5908. }
  5909.  
  5910. function renderDuplicate(pet) {
  5911. const tags = $(pet.element).find('.tags');
  5912. if(tags.find('.tag-duplicate').length) {
  5913. return false;
  5914. }
  5915. const color = colorMapper('warning');
  5916. const tag = elementCreator.getTag('Duplicate name', undefined, 'tag-duplicate')
  5917. .css('box-shadow', `inset 0px 0px 8px 0px ${color}`);
  5918. tags.append(tag);
  5919. return true;
  5920. }
  5921.  
  5922. function renderParsed(pet) {
  5923. const tags = $(pet.element).find('.tags');
  5924. if(tags.find('.stat-health').length) {
  5925. return false;
  5926. }
  5927. tags.empty();
  5928. const table = $(`<div style='display:inline-grid;grid-template-rows:1fr 1fr;grid-auto-flow:column'></div>`);
  5929. tags.append(table);
  5930. // traits
  5931. const traits = petTraitCache.byId[pet.traits];
  5932. if(traits.attack) {
  5933. table.append(elementCreator.getTag('', petUtil.IMAGES.attack));
  5934. }
  5935. if(traits.specialAttack) {
  5936. table.append(elementCreator.getTag('', petUtil.IMAGES.specialAttack));
  5937. }
  5938. if(traits.defense) {
  5939. table.append(elementCreator.getTag('', petUtil.IMAGES.defense));
  5940. }
  5941. if(traits.specialDefense) {
  5942. table.append(elementCreator.getTag('', petUtil.IMAGES.specialDefense));
  5943. }
  5944. // spacing
  5945. table.append(`<div style='padding:5px'></div>`);
  5946. table.append(`<div style='padding:5px'></div>`);
  5947. // stats
  5948. table.append(elementCreator.getTag(`${pet.health}%`, petUtil.IMAGES.health, 'stat-health'));
  5949. table.append(elementCreator.getTag(`${pet.speed}%`, petUtil.IMAGES.speed, 'stat-speed'));
  5950. table.append(elementCreator.getTag(`${pet.attack}%`, petUtil.IMAGES.attack, 'stat-attack'));
  5951. table.append(elementCreator.getTag(`${pet.specialAttack}%`, petUtil.IMAGES.specialAttack, 'stat-specialAttack'));
  5952. table.append(elementCreator.getTag(`${pet.defense}%`, petUtil.IMAGES.defense, 'stat-defense'));
  5953. table.append(elementCreator.getTag(`${pet.specialDefense}%`, petUtil.IMAGES.specialDefense, 'stat-specialDefense'));
  5954. // spacing
  5955. table.append(`<div style='padding:5px'></div>`);
  5956. table.append(`<div style='padding:5px'></div>`);
  5957. // passives
  5958. for(const id of pet.passives) {
  5959. const passive = petPassiveCache.byId[id];
  5960. table.append(elementCreator.getTag(passive.name, null, `passive-${passive.stats.name}`));
  5961. }
  5962. return true;
  5963. }
  5964.  
  5965. function render(pet) {
  5966. if(!pet.parsed && !pet.duplicate) {
  5967. return;
  5968. }
  5969. if(pet.duplicate || pet.default) {
  5970. return false;
  5971. }
  5972. const tags = $(pet.element).find('.tags');
  5973. if(tags.find('.stat-health').length) {
  5974. return false;
  5975. }
  5976. tags.empty();
  5977. const table = $(`<div style='display:inline-grid;grid-template-rows:1fr 1fr;grid-auto-flow:column'></div>`);
  5978. tags.append(table);
  5979. if(!pet.parsed) {
  5980. return;
  5981. }
  5982. // traits
  5983. const traits = petTraitCache.byId[pet.traits];
  5984. if(traits.hasAttack) {
  5985. table.append(elementCreator.getTag('', petUtil.IMAGES.attack));
  5986. }
  5987. if(traits.hasSpecialAttack) {
  5988. table.append(elementCreator.getTag('', petUtil.IMAGES.specialAttack));
  5989. }
  5990. if(traits.hasDefense) {
  5991. table.append(elementCreator.getTag('', petUtil.IMAGES.defense));
  5992. }
  5993. if(traits.hasSpecialDefense) {
  5994. table.append(elementCreator.getTag('', petUtil.IMAGES.specialDefense));
  5995. }
  5996. // spacing
  5997. table.append(`<div style='padding:5px'></div>`);
  5998. table.append(`<div style='padding:5px'></div>`);
  5999. // stats
  6000. table.append(elementCreator.getTag(`${pet.health}%`, petUtil.IMAGES.health, 'stat-health'));
  6001. table.append(elementCreator.getTag(`${pet.speed}%`, petUtil.IMAGES.speed, 'stat-speed'));
  6002. table.append(elementCreator.getTag(`${pet.attack}%`, petUtil.IMAGES.attack, 'stat-attack'));
  6003. table.append(elementCreator.getTag(`${pet.specialAttack}%`, petUtil.IMAGES.specialAttack, 'stat-specialAttack'));
  6004. table.append(elementCreator.getTag(`${pet.defense}%`, petUtil.IMAGES.defense, 'stat-defense'));
  6005. table.append(elementCreator.getTag(`${pet.specialDefense}%`, petUtil.IMAGES.specialDefense, 'stat-specialDefense'));
  6006. // spacing
  6007. table.append(`<div style='padding:5px'></div>`);
  6008. table.append(`<div style='padding:5px'></div>`);
  6009. // passives
  6010. for(const id of pet.passives) {
  6011. const passive = petPassiveCache.byId[id];
  6012. table.append(elementCreator.getTag(passive.name, null, `passive-${passive.stats.name}`));
  6013. }
  6014. return true;
  6015. }
  6016.  
  6017. initialise();
  6018.  
  6019. });
  6020. // recipeClickthrough
  6021. window.moduleRegistry.add('recipeClickthrough', (recipeCache, configuration, util) => {
  6022.  
  6023. let enabled = false;
  6024.  
  6025. function initialise() {
  6026. configuration.registerCheckbox({
  6027. category: 'UI Features',
  6028. key: 'recipe-click',
  6029. name: 'Recipe clickthrough',
  6030. default: true,
  6031. handler: handleConfigStateChange
  6032. });
  6033. $(document).on('click', 'div.image > img', handleClick);
  6034. }
  6035.  
  6036. function handleConfigStateChange(state) {
  6037. enabled = state;
  6038. }
  6039.  
  6040. function handleClick(event) {
  6041. if(!enabled) {
  6042. return;
  6043. }
  6044. if($(event.currentTarget).closest('button').length) {
  6045. return;
  6046. }
  6047. event.stopPropagation();
  6048. const name = $(event.relatedTarget).find('.name').text();
  6049. const nameMatch = recipeCache.byName[name];
  6050. if(nameMatch) {
  6051. return followRecipe(nameMatch);
  6052. }
  6053.  
  6054. const parts = event.target.src.split('/');
  6055. const lastPart = parts[parts.length-1];
  6056. const imageMatch = recipeCache.byImage[lastPart];
  6057. if(imageMatch) {
  6058. return followRecipe(imageMatch);
  6059. }
  6060. }
  6061.  
  6062. function followRecipe(recipe) {
  6063. util.goToPage(recipe.url);
  6064. }
  6065.  
  6066. initialise();
  6067.  
  6068. }
  6069. );
  6070. // syncTracker
  6071. window.moduleRegistry.add('syncTracker', (events, localDatabase, pages, components, util, toast, elementWatcher, debugService) => {
  6072.  
  6073. const STORE_NAME = 'sync-tracking';
  6074. const PAGE_NAME = 'Sync State';
  6075. const TOAST_SUCCESS_TIME = 1000*60*5; // 5 minutes
  6076. const TOAST_WARN_TIME = 1000*60*60*24*3; // 3 days
  6077. const TOAST_REWARN_TIME = 1000*60*60*4; // 4 hours
  6078.  
  6079. const sources = {
  6080. inventory: {
  6081. name: 'Inventory',
  6082. event: 'reader-inventory',
  6083. page: 'inventory'
  6084. },
  6085. 'equipment-equipment': {
  6086. name: 'Equipment',
  6087. event: 'reader-equipment-equipment',
  6088. page: 'equipment'
  6089. },
  6090. 'equipment-runes': {
  6091. name: 'Runes',
  6092. event: 'reader-equipment-runes',
  6093. page: 'equipment',
  6094. element: 'equipment-page .categories button:contains("Runes")'
  6095. },
  6096. 'equipment-tomes': {
  6097. name: 'Tomes',
  6098. event: 'reader-equipment-tomes',
  6099. page: 'equipment',
  6100. element: 'equipment-page .categories button:contains("Tomes")'
  6101. },
  6102. structures: {
  6103. name: 'Buildings',
  6104. event: 'reader-structures',
  6105. page: 'house/build/2'
  6106. },
  6107. enhancements: {
  6108. name: 'Building enhancements',
  6109. event: 'reader-enhancements',
  6110. page: 'house/enhance/2'
  6111. },
  6112. 'structures-guild': {
  6113. name: 'Guild buildings',
  6114. event: 'reader-structures-guild',
  6115. page: 'guild',
  6116. element: 'guild-page button:contains("Buildings")'
  6117. }
  6118. };
  6119.  
  6120. let autoVisiting = false;
  6121.  
  6122. async function initialise() {
  6123. await loadSavedData();
  6124. for(const key of Object.keys(sources)) {
  6125. events.register(sources[key].event, handleReader.bind(null, key));
  6126. }
  6127. await pages.register({
  6128. category: 'Misc',
  6129. name: PAGE_NAME,
  6130. image: 'https://img.icons8.com/?size=48&id=1ODJ62iG96gX&format=png',
  6131. columns: '3',
  6132. render: renderPage
  6133. });
  6134. pages.show(PAGE_NAME);
  6135. setInterval(update, 1000);
  6136. }
  6137.  
  6138. async function loadSavedData() {
  6139. const entries = await localDatabase.getAllEntries(STORE_NAME);
  6140. for(const entry of entries) {
  6141. if(!sources[entry.key]) {
  6142. continue;
  6143. }
  6144. sources[entry.key].lastSeen = entry.value.time;
  6145. events.emit(`reader-${entry.key}`, {
  6146. type: 'cache',
  6147. value: entry.value.value
  6148. });
  6149. }
  6150. }
  6151.  
  6152. function handleReader(key, event) {
  6153. if(event.type !== 'full') {
  6154. return;
  6155. }
  6156. const time = Date.now();
  6157. let newData = false;
  6158. if(!sources[key].lastSeen || sources[key].lastSeen + TOAST_SUCCESS_TIME < time) {
  6159. newData = true;
  6160. }
  6161. sources[key].lastSeen = time;
  6162. sources[key].notified = false;
  6163. localDatabase.saveEntry(STORE_NAME, {
  6164. key: key,
  6165. value: {
  6166. time,
  6167. value: event.value
  6168. }
  6169. });
  6170. if(newData) {
  6171. toast.create({
  6172. text: `${sources[key].name} synced`,
  6173. image: 'https://img.icons8.com/?size=48&id=1ODJ62iG96gX&format=png'
  6174. });
  6175. if(autoVisiting) {
  6176. triggerAutoVisitor();
  6177. }
  6178. }
  6179. }
  6180.  
  6181. function update() {
  6182. pages.requestRender(PAGE_NAME);
  6183. const time = Date.now();
  6184. for(const source of Object.values(sources)) {
  6185. if(source.lastSeen && source.lastSeen + TOAST_WARN_TIME >= time) {
  6186. continue;
  6187. }
  6188. if(source.notified && source.notified + TOAST_REWARN_TIME >= time) {
  6189. continue;
  6190. }
  6191. toast.create({
  6192. text: `${source.name} needs a sync`,
  6193. image: 'https://img.icons8.com/?size=48&id=1ODJ62iG96gX&format=png',
  6194. time: 5000
  6195. });
  6196. source.notified = time;
  6197. }
  6198. }
  6199.  
  6200. async function visit(source) {
  6201. if(!source.page) {
  6202. return;
  6203. }
  6204. util.goToPage(source.page);
  6205. if(source.element) {
  6206. await elementWatcher.exists(source.element);
  6207. $(source.element).click();
  6208. }
  6209. }
  6210.  
  6211. function startAutoVisiting() {
  6212. autoVisiting = true;
  6213. triggerAutoVisitor();
  6214. }
  6215.  
  6216. const stopAutoVisiting = util.debounce(function() {
  6217. autoVisiting = false;
  6218. pages.open(PAGE_NAME);
  6219. toast.create({
  6220. text: `Auto sync finished`,
  6221. image: 'https://img.icons8.com/?size=48&id=1ODJ62iG96gX&format=png'
  6222. });
  6223. }, 1500);
  6224.  
  6225. function triggerAutoVisitor() {
  6226. try {
  6227. const time = Date.now();
  6228. for(const source of Object.values(sources)) {
  6229. let secondsAgo = (time - source.lastSeen) / 1000;
  6230. if(source.page && (!source.lastSeen || secondsAgo >= 60*60)) {
  6231. visit(source);
  6232. return;
  6233. }
  6234. }
  6235. } finally {
  6236. stopAutoVisiting();
  6237. }
  6238. }
  6239.  
  6240. function renderPage() {
  6241. components.addComponent(autoVisitBlueprint);
  6242. const header = components.search(sourceBlueprint, 'header');
  6243. const item = components.search(sourceBlueprint, 'item');
  6244. const buttons = components.search(sourceBlueprint, 'buttons');
  6245. const time = Date.now();
  6246. for(const source of Object.values(sources)) {
  6247. sourceBlueprint.componentId = `syncTrackerSourceComponent_${source.name}`;
  6248. header.title = source.name;
  6249. let secondsAgo = (time - source.lastSeen) / 1000;
  6250. if(!secondsAgo) {
  6251. secondsAgo = Number.MAX_VALUE;
  6252. }
  6253. item.value = util.secondsToDuration(secondsAgo);
  6254. buttons.hidden = secondsAgo < 60*60;
  6255. buttons.buttons[0].action = visit.bind(null, source);
  6256. components.addComponent(sourceBlueprint);
  6257. }
  6258. }
  6259.  
  6260. const autoVisitBlueprint = {
  6261. componentId: 'syncTrackerAutoVisitComponent',
  6262. dependsOn: 'custom-page',
  6263. parent: '.column0',
  6264. selectedTabIndex: 0,
  6265. tabs: [
  6266. {
  6267. rows: [
  6268. {
  6269. type: 'buttons',
  6270. buttons: [
  6271. {
  6272. text: 'Auto sync',
  6273. color: 'primary',
  6274. action: startAutoVisiting
  6275. }
  6276. ]
  6277. },
  6278. {
  6279. type: 'buttons',
  6280. buttons: [
  6281. {
  6282. text: 'Submit debug info',
  6283. color: 'primary',
  6284. action: debugService.submit
  6285. }
  6286. ]
  6287. }
  6288. ]
  6289. }
  6290. ]
  6291. };
  6292.  
  6293. const sourceBlueprint = {
  6294. componentId: 'syncTrackerSourceComponent',
  6295. dependsOn: 'custom-page',
  6296. parent: '.column0',
  6297. selectedTabIndex: 0,
  6298. tabs: [
  6299. {
  6300. rows: [
  6301. {
  6302. type: 'header',
  6303. id: 'header',
  6304. title: '',
  6305. centered: true
  6306. }, {
  6307. type: 'item',
  6308. id: 'item',
  6309. name: 'Last detected',
  6310. value: ''
  6311. }, {
  6312. type: 'buttons',
  6313. id: 'buttons',
  6314. buttons: [
  6315. {
  6316. text: 'Visit',
  6317. color: 'danger',
  6318. action: undefined
  6319. }
  6320. ]
  6321. }
  6322. ]
  6323. },
  6324. ]
  6325. };
  6326.  
  6327. initialise();
  6328.  
  6329. }
  6330. );
  6331. // ui
  6332. window.moduleRegistry.add('ui', (configuration) => {
  6333.  
  6334. const id = crypto.randomUUID();
  6335. const sections = [
  6336. 'challenges-page',
  6337. 'changelog-page',
  6338. 'daily-quest-page',
  6339. 'equipment-page',
  6340. 'guild-page',
  6341. 'home-page',
  6342. 'leaderboards-page',
  6343. 'market-page',
  6344. 'merchant-page',
  6345. 'quests-page',
  6346. 'settings-page',
  6347. 'skill-page',
  6348. 'upgrade-page',
  6349. 'taming-page'
  6350. ].join(', ');
  6351. const selector = `:is(${sections})`;
  6352.  
  6353. function initialise() {
  6354. configuration.registerCheckbox({
  6355. category: 'UI Features',
  6356. key: 'ui-changes',
  6357. name: 'UI changes',
  6358. default: true,
  6359. handler: handleConfigStateChange
  6360. });
  6361. }
  6362.  
  6363. function handleConfigStateChange(state) {
  6364. if(state) {
  6365. add();
  6366. } else {
  6367. remove();
  6368. }
  6369. }
  6370.  
  6371. function add() {
  6372. document.documentElement.style.setProperty('--gap', '8px');
  6373. const element = $(`
  6374. <style>
  6375. ${selector} :not(.multi-row) > :is(
  6376. button.item,
  6377. button.row,
  6378. button.socket-button,
  6379. button.level-button,
  6380. div.item,
  6381. div.row
  6382. ) {
  6383. padding: 2px 6px !important;
  6384. min-height: 0 !important;
  6385. }
  6386.  
  6387. ${selector} :not(.multi-row) > :is(
  6388. button.item div.image,
  6389. button.row div.image,
  6390. div.item div.image,
  6391. div.item div.placeholder-image,
  6392. div.row div.image
  6393. ) {
  6394. height: 32px !important;
  6395. width: 32px !important;
  6396. min-height: 0 !important;
  6397. min-width: 0 !important;
  6398. }
  6399.  
  6400. ${selector} div.lock {
  6401. height: unset !important;
  6402. padding: 0 !important;
  6403. }
  6404.  
  6405. action-component div.body > div.image,
  6406. produce-component div.body > div.image,
  6407. daily-quest-page div.body > div.image {
  6408. height: 48px !important;
  6409. width: 48px !important;
  6410. }
  6411.  
  6412. div.progress div.body {
  6413. padding: 8px !important;
  6414. }
  6415.  
  6416. action-component div.bars {
  6417. padding: 0 !important;
  6418. }
  6419.  
  6420. equipment-component button {
  6421. padding: 0 !important;
  6422. }
  6423.  
  6424. inventory-page .items {
  6425. grid-gap: 0 !important;
  6426. }
  6427.  
  6428. div.scroll.custom-scrollbar .header,
  6429. div.scroll.custom-scrollbar button {
  6430. height: 28px !important;
  6431. }
  6432.  
  6433. div.scroll.custom-scrollbar img {
  6434. height: 16px !important;
  6435. width: 16px !important;
  6436. }
  6437.  
  6438. .scroll {
  6439. overflow-y: auto !important;
  6440. }
  6441. .scroll {
  6442. -ms-overflow-style: none; /* Internet Explorer 10+ */
  6443. scrollbar-width: none; /* Firefox */
  6444. }
  6445. .scroll::-webkit-scrollbar {
  6446. display: none; /* Safari and Chrome */
  6447. }
  6448. </style>
  6449. `).attr('id', id);
  6450. window.$('head').append(element);
  6451. }
  6452.  
  6453. function remove() {
  6454. document.documentElement.style.removeProperty('--gap');
  6455. $(`#${id}`).remove();
  6456. }
  6457.  
  6458. initialise();
  6459.  
  6460. }
  6461. );
  6462. // versionWarning
  6463. window.moduleRegistry.add('versionWarning', (request, toast) => {
  6464.  
  6465. function initialise() {
  6466. setInterval(run, 1000 * 60 * 5);
  6467. run();
  6468. }
  6469.  
  6470. async function run() {
  6471. const version = await request.getVersion();
  6472. if(!window.PANCAKE_VERSION || version === window.PANCAKE_VERSION) {
  6473. return;
  6474. }
  6475. toast.create({
  6476. 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`,
  6477. image: 'https://img.icons8.com/?size=48&id=iAqIpjeFjcYz&format=png',
  6478. time: 5000
  6479. });
  6480. }
  6481.  
  6482. initialise();
  6483.  
  6484. }
  6485. );
  6486. // abstractStateStore
  6487. window.moduleRegistry.add('abstractStateStore', (events, util) => {
  6488.  
  6489. const SOURCES = [
  6490. 'inventory',
  6491. 'equipment-runes',
  6492. 'equipment-tomes',
  6493. 'structures',
  6494. 'enhancements',
  6495. 'structures-guild'
  6496. ];
  6497.  
  6498. const stateBySource = {};
  6499.  
  6500. function initialise() {
  6501. for(const source of SOURCES) {
  6502. stateBySource[source] = {};
  6503. events.register(`reader-${source}`, handleReader.bind(null, source));
  6504. }
  6505. }
  6506.  
  6507. function handleReader(source, event) {
  6508. let updated = false;
  6509. if(event.type === 'full' || event.type === 'cache') {
  6510. if(util.compareObjects(stateBySource[source], event.value)) {
  6511. return;
  6512. }
  6513. updated = true;
  6514. stateBySource[source] = event.value;
  6515. }
  6516. if(event.type === 'partial') {
  6517. for(const key of Object.keys(event.value)) {
  6518. if(stateBySource[source][key] === event.value[key]) {
  6519. continue;
  6520. }
  6521. updated = true;
  6522. stateBySource[source][key] = event.value[key];
  6523. }
  6524. }
  6525. if(updated) {
  6526. events.emit(`state-${source}`, stateBySource[source]);
  6527. }
  6528. }
  6529.  
  6530. initialise();
  6531.  
  6532. }
  6533. );
  6534. // configurationStore
  6535. window.moduleRegistry.add('configurationStore', (Promise, localConfigurationStore, _remoteConfigurationStore) => {
  6536.  
  6537. const initialised = new Promise.Expiring(2000, 'configurationStore');
  6538. let configs = null;
  6539.  
  6540. const exports = {
  6541. save,
  6542. getConfigs
  6543. };
  6544.  
  6545. const configurationStore = _remoteConfigurationStore || localConfigurationStore;
  6546.  
  6547. async function initialise() {
  6548. configs = await configurationStore.load();
  6549. for(const key in configs) {
  6550. configs[key] = JSON.parse(configs[key]);
  6551. }
  6552. initialised.resolve(exports);
  6553. }
  6554.  
  6555. async function save(key, value) {
  6556. await configurationStore.save(key, value);
  6557. configs[key] = value;
  6558. }
  6559.  
  6560. function getConfigs() {
  6561. return configs;
  6562. }
  6563.  
  6564. initialise();
  6565.  
  6566. return initialised;
  6567.  
  6568. }
  6569. );
  6570. // equipmentStateStore
  6571. window.moduleRegistry.add('equipmentStateStore', (events, util, itemCache) => {
  6572.  
  6573. let state = {};
  6574.  
  6575. function initialise() {
  6576. events.register('reader-equipment-equipment', handleEquipmentReader);
  6577. }
  6578.  
  6579. function handleEquipmentReader(event) {
  6580. let updated = false;
  6581. if(event.type === 'full' || event.type === 'cache') {
  6582. if(util.compareObjects(state, event.value)) {
  6583. return;
  6584. }
  6585. updated = true;
  6586. state = event.value;
  6587. }
  6588. if(event.type === 'partial') {
  6589. for(const key of Object.keys(event.value)) {
  6590. if(state[key] === event.value[key]) {
  6591. continue;
  6592. }
  6593. updated = true;
  6594. // remove items of similar type
  6595. for(const itemType in itemCache.specialIds) {
  6596. if(Array.isArray(itemCache.specialIds[itemType]) && itemCache.specialIds[itemType].includes(+key)) {
  6597. for(const itemId of itemCache.specialIds[itemType]) {
  6598. delete state[itemId];
  6599. }
  6600. }
  6601. }
  6602. state[key] = event.value[key];
  6603. }
  6604. }
  6605. if(updated) {
  6606. events.emit('state-equipment-equipment', state);
  6607. }
  6608. }
  6609.  
  6610. initialise();
  6611.  
  6612. }
  6613. );
  6614. // expStateStore
  6615. window.moduleRegistry.add('expStateStore', (events, util) => {
  6616.  
  6617. const emitEvent = events.emit.bind(null, 'state-exp');
  6618. const state = {};
  6619.  
  6620. function initialise() {
  6621. events.register('reader-exp', handleExpReader);
  6622. }
  6623.  
  6624. function handleExpReader(event) {
  6625. let updated = false;
  6626. for(const skill of event) {
  6627. if(!state[skill.id]) {
  6628. state[skill.id] = {
  6629. id: skill.id,
  6630. exp: 0,
  6631. level: 1
  6632. };
  6633. }
  6634. if(skill.exp > state[skill.id].exp) {
  6635. updated = true;
  6636. state[skill.id].exp = skill.exp;
  6637. state[skill.id].level = util.expToLevel(skill.exp);
  6638. }
  6639. }
  6640. if(updated) {
  6641. emitEvent(state);
  6642. }
  6643. }
  6644.  
  6645. initialise();
  6646.  
  6647. }
  6648. );
  6649. // localConfigurationStore
  6650. window.moduleRegistry.add('localConfigurationStore', (localDatabase) => {
  6651.  
  6652. const exports = {
  6653. load,
  6654. save
  6655. };
  6656.  
  6657. const STORE_NAME = 'settings';
  6658.  
  6659. async function load() {
  6660. const entries = await localDatabase.getAllEntries(STORE_NAME);
  6661. const configurations = {};
  6662. for(const entry of entries) {
  6663. configurations[entry.key] = entry.value;
  6664. }
  6665. return configurations;
  6666. }
  6667.  
  6668. async function save(key, value) {
  6669. await localDatabase.saveEntry(STORE_NAME, {key, value});
  6670. }
  6671.  
  6672. return exports;
  6673.  
  6674. }
  6675. );
  6676. // marketStore
  6677. window.moduleRegistry.add('marketStore', (events) => {
  6678.  
  6679. const emitEvent = events.emit.bind(null, 'state-market');
  6680. let state = {};
  6681.  
  6682. function initialise() {
  6683. events.register('page', handlePage);
  6684. events.register('reader-market', handleMarketReader);
  6685. }
  6686.  
  6687. function handlePage(event) {
  6688. if(event.type == 'market') {
  6689. state = {};
  6690. }
  6691. }
  6692.  
  6693. function handleMarketReader(event) {
  6694. state[event.type] = event.listings;
  6695. state.lastType = event.type;
  6696. state.last = event.listings;
  6697. emitEvent(state);
  6698. }
  6699.  
  6700. initialise();
  6701.  
  6702. }
  6703. );
  6704. // petStateStore
  6705. window.moduleRegistry.add('petStateStore', (events, petUtil, util, localDatabase, petCache) => {
  6706.  
  6707. const STORE_NAME = 'various';
  6708. const KEY_NAME = 'pets';
  6709. let state = [];
  6710.  
  6711. async function initialise() {
  6712. await loadSavedData();
  6713. events.register('page', handlePage);
  6714. events.register('reader-pet', handlePetReader);
  6715. }
  6716.  
  6717. async function loadSavedData() {
  6718. const entries = await localDatabase.getAllEntries(STORE_NAME);
  6719. const entry = entries.find(entry => entry.key === KEY_NAME);
  6720. if(entry) {
  6721. state = entry.value;
  6722. }
  6723. }
  6724.  
  6725. function handlePage(page) {
  6726. if(page.type === 'taming' && page.menu === 'pets') {
  6727. emitEvent(state);
  6728. }
  6729. }
  6730.  
  6731. function handlePetReader(event) {
  6732. let updated = false;
  6733. if(event.type === 'list') {
  6734. const duplicateNames = new Set(util.getDuplicates(event.value.map(a => a.name)));
  6735. const defaultNames = new Set(petCache.list.map(a => a.name));
  6736. const newState = event.value.map(pet => {
  6737. pet.duplicate = duplicateNames.has(pet.name);
  6738. pet.default = defaultNames.has(pet.name);
  6739. if(pet.duplicate || pet.default) {
  6740. return pet;
  6741. }
  6742. const match = find(pet);
  6743. if(match) {
  6744. delete pet.parsed;
  6745. Object.assign(match, pet);
  6746. return match;
  6747. }
  6748. updated = true;
  6749. if(petUtil.isEncodedPetName(pet.name)) {
  6750. Object.assign(pet, petUtil.textToPet(pet.name));
  6751. }
  6752. return pet;
  6753. });
  6754. if(state.length !== newState.length) {
  6755. updated = true;
  6756. }
  6757. state = newState;
  6758. } else if(event.type === 'single') {
  6759. const match = find(event.value);
  6760. if(match && !match.duplicate && !match.default && !match.parsed) {
  6761. Object.assign(match, event.value);
  6762. updated = true;
  6763. }
  6764. }
  6765. if(updated) {
  6766. emitEvent(state);
  6767. }
  6768. }
  6769.  
  6770. function find(pet) {
  6771. return state.find(pet2 => pet2.name === pet.name);
  6772. }
  6773.  
  6774. async function emitEvent(state) {
  6775. const savedState = state.map(pet => Object.assign({}, pet));
  6776. for(const pet of savedState) {
  6777. delete pet.element;
  6778. }
  6779. await localDatabase.saveEntry(STORE_NAME, {
  6780. key: KEY_NAME,
  6781. value: savedState
  6782. });
  6783. events.emit('state-pet', state);
  6784. }
  6785.  
  6786. initialise();
  6787.  
  6788. }
  6789. );
  6790. // statsStore
  6791. window.moduleRegistry.add('statsStore', (events, util, skillCache, itemCache, structuresCache, statNameCache) => {
  6792.  
  6793. const emitEvent = events.emit.bind(null, 'state-stats');
  6794.  
  6795. const exports = {
  6796. get,
  6797. getLevel,
  6798. getInventoryItem,
  6799. getEquipmentItem,
  6800. getManyEquipmentItems,
  6801. getWeapon,
  6802. getAttackStyle,
  6803. update
  6804. };
  6805.  
  6806. let exp = {};
  6807. let inventory = {};
  6808. let tomes = {};
  6809. let equipment = {};
  6810. let runes = {};
  6811. let structures = {};
  6812. let enhancements = {};
  6813. let guildStructures = {};
  6814. let various = {};
  6815.  
  6816. let stats;
  6817.  
  6818. function initialise() {
  6819. let _update = util.debounce(update, 200);
  6820. events.register('state-exp', event => (exp = event, _update()));
  6821. events.register('state-inventory', event => (inventory = event, _update()));
  6822. events.register('state-equipment-tomes', event => (tomes = event, _update()));
  6823. events.register('state-equipment-equipment', event => (equipment = event, _update()));
  6824. events.register('state-equipment-runes', event => (runes = event, _update()));
  6825. events.register('state-structures', event => (structures = event, _update()));
  6826. events.register('state-enhancements', event => (enhancements = event, _update()));
  6827. events.register('state-structures-guild', event => (guildStructures = event, _update()));
  6828. events.register('state-various', event => (various = event, _update()));
  6829. }
  6830.  
  6831. function get(stat, skill) {
  6832. if(!stat) {
  6833. return stats;
  6834. }
  6835. statNameCache.validate(stat);
  6836. let value = 0;
  6837. if(stats && stats.global[stat]) {
  6838. value += stats.global[stat] || 0;
  6839. }
  6840. if(Number.isInteger(skill)) {
  6841. skill = skillCache.byId[skill]?.technicalName;
  6842. }
  6843. if(stats && stats.bySkill[stat] && stats.bySkill[stat][skill]) {
  6844. value += stats.bySkill[stat][skill];
  6845. }
  6846. return value;
  6847. }
  6848.  
  6849. function getLevel(skillId) {
  6850. return exp[skillId] || {
  6851. id: skillId,
  6852. exp: 0,
  6853. level: 1
  6854. };
  6855. }
  6856.  
  6857. function getInventoryItem(itemId) {
  6858. return inventory[itemId] || 0;
  6859. }
  6860.  
  6861. function getEquipmentItem(itemId) {
  6862. return equipment[itemId] || tomes[itemId] || runes[itemId] || 0;
  6863. }
  6864.  
  6865. function getManyEquipmentItems(ids) {
  6866. return ids.map(id => ({
  6867. id,
  6868. amount: getEquipmentItem(id)
  6869. })).filter(a => a.amount);
  6870. }
  6871.  
  6872. function getWeapon() {
  6873. return stats.weapon;
  6874. }
  6875.  
  6876. function getAttackStyle() {
  6877. return stats.attackStyle;
  6878. }
  6879.  
  6880. function update(excludedItemIds) {
  6881. reset();
  6882. processExp();
  6883. processTomes();
  6884. processEquipment(excludedItemIds);
  6885. processRunes();
  6886. processStructures();
  6887. processEnhancements();
  6888. processGuildStructures();
  6889. processVarious();
  6890. cleanup();
  6891. if(!excludedItemIds) {
  6892. emitEvent(stats);
  6893. }
  6894. }
  6895.  
  6896. function reset() {
  6897. stats = {
  6898. weapon: null,
  6899. attackStyle: null,
  6900. bySkill: {},
  6901. global: {}
  6902. };
  6903. }
  6904.  
  6905. function processExp() {
  6906. for(const id in exp) {
  6907. const skill = skillCache.byId[id];
  6908. addStats({
  6909. bySkill: {
  6910. EFFICIENCY : {
  6911. [skill.technicalName]: 0.25
  6912. }
  6913. }
  6914. }, exp[id].level, 4);
  6915. if(skill.displayName === 'Ranged') {
  6916. addStats({
  6917. global: {
  6918. AMMO_PRESERVATION_CHANCE : 0.5
  6919. }
  6920. }, exp[id].level, 2);
  6921. }
  6922. }
  6923. }
  6924.  
  6925. // first tomes, then equipments
  6926. // because we need to know the potion effect multiplier first
  6927. function processTomes() {
  6928. for(const id in tomes) {
  6929. const item = itemCache.byId[id];
  6930. if(!item) {
  6931. continue;
  6932. }
  6933. addStats(item.stats);
  6934. }
  6935. }
  6936.  
  6937. function processEquipment(excludedItemIds) {
  6938. let arrow;
  6939. let bow;
  6940. const potionMultiplier = get('INCREASED_POTION_EFFECT');
  6941. for(const id in equipment) {
  6942. if(equipment[id] <= 0) {
  6943. continue;
  6944. }
  6945. if(excludedItemIds && excludedItemIds.has(+id)) {
  6946. continue;
  6947. }
  6948. const item = itemCache.byId[id];
  6949. if(!item) {
  6950. continue;
  6951. }
  6952. if(item.stats.global.ATTACK_SPEED) {
  6953. stats.weapon = item;
  6954. stats.attackStyle = item.skill;
  6955. }
  6956. if(item.name.endsWith('Arrow')) {
  6957. arrow = item;
  6958. addStats({
  6959. global: {
  6960. AMMO_PRESERVATION_CHANCE : -0.5
  6961. }
  6962. }, util.tierToLevel(item.tier), 2);
  6963. continue;
  6964. }
  6965. if(item.name.endsWith('Bow')) {
  6966. bow = item;
  6967. }
  6968. let multiplier = 1;
  6969. let accuracy = 2;
  6970. if(potionMultiplier && /(Potion|Mix)$/.exec(item.name)) {
  6971. multiplier = 1 + potionMultiplier / 100;
  6972. accuracy = 10;
  6973. }
  6974. if(item.name.endsWith('Rune')) {
  6975. multiplier = equipment[id];
  6976. accuracy = 10;
  6977. }
  6978. addStats(item.stats, multiplier, accuracy);
  6979. }
  6980. if(bow && arrow) {
  6981. addStats(arrow.stats);
  6982. }
  6983. }
  6984. function processRunes() {
  6985. for(const id in runes) {
  6986. const item = itemCache.byId[id];
  6987. if(!item) {
  6988. continue;
  6989. }
  6990. addStats(item.stats, runes[id]);
  6991. }
  6992. }
  6993.  
  6994. function processStructures() {
  6995. for(const id in structures) {
  6996. const structure = structuresCache.byId[id];
  6997. if(!structure) {
  6998. continue;
  6999. }
  7000. addStats(structure.regular, structures[id] + 2/3);
  7001. }
  7002. }
  7003.  
  7004. function processEnhancements() {
  7005. for(const id in enhancements) {
  7006. const structure = structuresCache.byId[id];
  7007. if(!structure) {
  7008. continue;
  7009. }
  7010. addStats(structure.enhance, enhancements[id]);
  7011. }
  7012. }
  7013.  
  7014. function processGuildStructures() {
  7015. for(const id in guildStructures) {
  7016. const structure = structuresCache.byId[id];
  7017. if(!structure) {
  7018. continue;
  7019. }
  7020. addStats(structure.regular, guildStructures[id]);
  7021. }
  7022. }
  7023.  
  7024. function processVarious() {
  7025. if(various.maxAmount) {
  7026. const stats = {
  7027. bySkill: {
  7028. MAX_AMOUNT: {}
  7029. }
  7030. };
  7031. for(const skillId in various.maxAmount) {
  7032. const skill = skillCache.byId[skillId];
  7033. if(various.maxAmount[skillId]) {
  7034. stats.bySkill.MAX_AMOUNT[skill.technicalName] = various.maxAmount[skillId];
  7035. }
  7036. }
  7037. addStats(stats);
  7038. }
  7039. }
  7040.  
  7041. function cleanup() {
  7042. // base
  7043. addStats({
  7044. global: {
  7045. HEALTH: 10,
  7046. AMMO_PRESERVATION_CHANCE : 65
  7047. }
  7048. });
  7049. // fallback
  7050. if(!stats.weapon) {
  7051. stats.weapon = null;
  7052. stats.attackStyle = '';
  7053. stats.global.ATTACK_SPEED = 3;
  7054. }
  7055. // health percent
  7056. const healthPercent = get('HEALTH_PERCENT');
  7057. if(healthPercent) {
  7058. const health = get('HEALTH');
  7059. addStats({
  7060. global: {
  7061. HEALTH : Math.floor(healthPercent * health / 100)
  7062. }
  7063. })
  7064. }
  7065. // damage percent
  7066. const damagePercent = get('DAMAGE_PERCENT');
  7067. if(damagePercent) {
  7068. const damage = get('DAMAGE');
  7069. addStats({
  7070. global: {
  7071. DAMAGE : Math.floor(damagePercent * damage / 100)
  7072. }
  7073. })
  7074. }
  7075. // bonus level efficiency
  7076. if(stats.bySkill['BONUS_LEVEL']) {
  7077. for(const skill in stats.bySkill['BONUS_LEVEL']) {
  7078. addStats({
  7079. bySkill: {
  7080. EFFICIENCY: {
  7081. [skill]: 0.25
  7082. }
  7083. }
  7084. }, Math.round(stats.bySkill['BONUS_LEVEL'][skill]), 4);
  7085. }
  7086. }
  7087. // clamping
  7088. if(stats.global['AMMO_PRESERVATION_CHANCE'] < 65) {
  7089. stats.global['AMMO_PRESERVATION_CHANCE'] = 65;
  7090. }
  7091. if(stats.global['AMMO_PRESERVATION_CHANCE'] > 80) {
  7092. stats.global['AMMO_PRESERVATION_CHANCE'] = 80;
  7093. }
  7094. }
  7095.  
  7096. function addStats(newStats, multiplier = 1, accuracy = 1) {
  7097. if(newStats.global) {
  7098. for(const stat in newStats.global) {
  7099. if(!stats.global[stat]) {
  7100. stats.global[stat] = 0;
  7101. }
  7102. stats.global[stat] += Math.round(accuracy * multiplier * newStats.global[stat]) / accuracy;
  7103. }
  7104. }
  7105. if(newStats.bySkill) {
  7106. for(const stat in newStats.bySkill) {
  7107. if(!stats.bySkill[stat]) {
  7108. stats.bySkill[stat] = {};
  7109. }
  7110. for(const skill in newStats.bySkill[stat]) {
  7111. if(!stats.bySkill[stat][skill]) {
  7112. stats.bySkill[stat][skill] = 0;
  7113. }
  7114. stats.bySkill[stat][skill] += Math.round(accuracy * multiplier * newStats.bySkill[stat][skill]) / accuracy;
  7115. }
  7116. }
  7117. }
  7118. }
  7119.  
  7120. initialise();
  7121.  
  7122. return exports;
  7123.  
  7124. }
  7125. );
  7126. // variousStateStore
  7127. window.moduleRegistry.add('variousStateStore', (events, skillCache) => {
  7128.  
  7129. const emitEvent = events.emit.bind(null, 'state-various');
  7130. const state = {};
  7131.  
  7132. function initialise() {
  7133. events.register('reader-various', handleReader);
  7134. }
  7135.  
  7136. function handleReader(event) {
  7137. const updated = merge(state, event);
  7138. if(updated) {
  7139. emitEvent(state);
  7140. }
  7141. }
  7142.  
  7143. function merge(target, source) {
  7144. let updated = false;
  7145. for(const key in source) {
  7146. if(!(key in target)) {
  7147. target[key] = source[key];
  7148. updated = true;
  7149. continue;
  7150. }
  7151. if(typeof target[key] === 'object' && typeof source[key] === 'object') {
  7152. updated |= merge(target[key], source[key]);
  7153. continue;
  7154. }
  7155. if(target[key] !== source[key]) {
  7156. target[key] = source[key];
  7157. updated = true;
  7158. continue;
  7159. }
  7160. }
  7161. return updated;
  7162. }
  7163.  
  7164. initialise();
  7165.  
  7166. }
  7167. );
  7168. // actionCache
  7169. window.moduleRegistry.add('actionCache', (request, Promise) => {
  7170.  
  7171. const initialised = new Promise.Expiring(2000, 'actionCache');
  7172.  
  7173. const exports = {
  7174. list: [],
  7175. byId: {},
  7176. byName: {}
  7177. };
  7178.  
  7179. async function tryInitialise() {
  7180. try {
  7181. await initialise();
  7182. initialised.resolve(exports);
  7183. } catch(e) {
  7184. initialised.reject(e);
  7185. }
  7186. }
  7187.  
  7188. async function initialise() {
  7189. const actions = await request.listActions();
  7190. for(const action of actions) {
  7191. exports.list.push(action);
  7192. exports.byId[action.id] = action;
  7193. exports.byName[action.name] = action;
  7194. }
  7195. }
  7196.  
  7197. tryInitialise();
  7198.  
  7199. return initialised;
  7200.  
  7201. }
  7202. );
  7203. // dropCache
  7204. window.moduleRegistry.add('dropCache', (request, Promise, itemCache, actionCache, skillCache, ingredientCache) => {
  7205.  
  7206. const initialised = new Promise.Expiring(2000, 'dropCache');
  7207.  
  7208. const exports = {
  7209. list: [],
  7210. byAction: {},
  7211. byItem: {},
  7212. boneCarveMappings: null,
  7213. lowerGatherMappings: null,
  7214. conversionMappings: null
  7215. };
  7216.  
  7217. Object.defineProperty(Array.prototype, '_groupBy', {
  7218. enumerable: false,
  7219. value: function(selector) {
  7220. return Object.values(this.reduce(function(rv, x) {
  7221. (rv[selector(x)] = rv[selector(x)] || []).push(x);
  7222. return rv;
  7223. }, {}));
  7224. }
  7225. });
  7226.  
  7227. Object.defineProperty(Array.prototype, '_distinct', {
  7228. enumerable: false,
  7229. value: function(selector) {
  7230. return [...new Set(this)];
  7231. }
  7232. });
  7233.  
  7234. async function tryInitialise() {
  7235. try {
  7236. await initialise();
  7237. initialised.resolve(exports);
  7238. } catch(e) {
  7239. initialised.reject(e);
  7240. }
  7241. }
  7242.  
  7243. async function initialise() {
  7244. const drops = await request.listDrops();
  7245. for(const drop of drops) {
  7246. exports.list.push(drop);
  7247. if(!exports.byAction[drop.action]) {
  7248. exports.byAction[drop.action] = [];
  7249. }
  7250. exports.byAction[drop.action].push(drop);
  7251. if(!exports.byItem[drop.item]) {
  7252. exports.byItem[drop.item] = [];
  7253. }
  7254. exports.byItem[drop.item].push(drop);
  7255. }
  7256. extractBoneCarvings();
  7257. extractLowerGathers();
  7258. extractConversions();
  7259. }
  7260.  
  7261. // I'm sorry for what follows
  7262. function extractBoneCarvings() {
  7263. let name;
  7264. exports.boneCarveMappings = exports.list
  7265. // filtering
  7266. .filter(drop => drop.type === 'GUARANTEED')
  7267. .filter(drop => (name = itemCache.byId[drop.item].name, name.endsWith('Bone') || name.endsWith('Fang')))
  7268. .filter(drop => actionCache.byId[drop.action].skill === 'Combat')
  7269. // sort
  7270. .sort((a,b) => actionCache.byId[a.action].level - actionCache.byId[b.action].level)
  7271. // per level
  7272. ._groupBy(drop => actionCache.byId[drop.action].level)
  7273. .map(a => a[0].item)
  7274. .map((item,i,all) => ({
  7275. from: item,
  7276. to: [].concat([all[i-1]]).concat([all[i-2]]).filter(a => a)
  7277. }))
  7278. .reduce((a,b) => (a[b.from] = b.to, a), {});
  7279. }
  7280.  
  7281. function extractLowerGathers() {
  7282. exports.lowerGatherMappings = exports.list
  7283. // filtering
  7284. .filter(drop => drop.type === 'REGULAR')
  7285. .filter(drop => skillCache.byName[actionCache.byId[drop.action].skill].type === 'Gathering')
  7286. // sort
  7287. .sort((a,b) => actionCache.byId[a.action].level - actionCache.byId[b.action].level)
  7288. // per action, the highest chance drop
  7289. ._groupBy(drop => drop.action)
  7290. .map(a => a.reduce((a,b) => a.chance >= b.chance ? a : b))
  7291. // per skill, and for farming,
  7292. ._groupBy(drop => {
  7293. const action = actionCache.byId[drop.action];
  7294. let skill = action.skill
  7295. if(skill === 'Farming') {
  7296. // add flower or vegetable suffix
  7297. skill += `-${action.image.split('/')[1].split('-')[0]}`;
  7298. }
  7299. return skill;
  7300. })
  7301. .flatMap(a => a
  7302. ._groupBy(drop => actionCache.byId[drop.action].level)
  7303. .map(b => b.map(drop => drop.item)._distinct())
  7304. .flatMap((b,i,all) => b.map(item => ({
  7305. from: item,
  7306. to: [].concat(all[i-1]).concat(all[i-2]).filter(a => a)
  7307. })))
  7308. )
  7309. .reduce((a,b) => (a[b.from] = b.to, a), {});
  7310. }
  7311.  
  7312. function extractConversions() {
  7313. exports.conversionMappings = exports.list
  7314. .filter(a => actionCache.byId[a.action].type === 'CONVERSION')
  7315. .map(drop => ({
  7316. from: ingredientCache.byAction[drop.action][0].item,
  7317. to: drop.item,
  7318. amount: drop.amount
  7319. }))
  7320. ._groupBy(a => a.to)
  7321. .reduce((a,b) => (a[b[0].to] = b, a), {});
  7322. }
  7323.  
  7324. tryInitialise();
  7325.  
  7326. return initialised;
  7327.  
  7328. }
  7329. );
  7330. // expeditionCache
  7331. window.moduleRegistry.add('expeditionCache', (request, Promise) => {
  7332.  
  7333. const initialised = new Promise.Expiring(2000, 'expeditionCache');
  7334.  
  7335. const exports = {
  7336. list: [],
  7337. byId: {},
  7338. byName: {},
  7339. byTier: {}
  7340. };
  7341.  
  7342. async function tryInitialise() {
  7343. try {
  7344. await initialise();
  7345. initialised.resolve(exports);
  7346. } catch(e) {
  7347. initialised.reject(e);
  7348. }
  7349. }
  7350.  
  7351. async function initialise() {
  7352. const expeditions = await request.listExpeditions();
  7353. for(const expedition of expeditions) {
  7354. exports.list.push(expedition);
  7355. exports.byId[expedition.id] = expedition;
  7356. exports.byName[expedition.name] = expedition;
  7357. exports.byTier[expedition.tier] = expedition;
  7358. }
  7359. }
  7360.  
  7361. tryInitialise();
  7362.  
  7363. return initialised;
  7364.  
  7365. }
  7366. );
  7367. // expeditionDropCache
  7368. window.moduleRegistry.add('expeditionDropCache', (request, Promise) => {
  7369.  
  7370. const initialised = new Promise.Expiring(2000, 'expeditionDropCache');
  7371.  
  7372. const exports = {
  7373. list: [],
  7374. byExpedition: {},
  7375. byItem: {}
  7376. };
  7377.  
  7378. async function tryInitialise() {
  7379. try {
  7380. await initialise();
  7381. initialised.resolve(exports);
  7382. } catch(e) {
  7383. initialised.reject(e);
  7384. }
  7385. }
  7386.  
  7387. async function initialise() {
  7388. const drops = await request.listExpeditionDrops();
  7389. for(const drop of drops) {
  7390. exports.list.push(drop);
  7391. if(!exports.byExpedition[drop.expedition]) {
  7392. exports.byExpedition[drop.expedition] = [];
  7393. }
  7394. exports.byExpedition[drop.expedition].push(drop);
  7395. if(!exports.byItem[drop.item]) {
  7396. exports.byItem[drop.item] = [];
  7397. }
  7398. exports.byItem[drop.item].push(drop);
  7399. }
  7400. }
  7401.  
  7402. tryInitialise();
  7403.  
  7404. return initialised;
  7405.  
  7406. }
  7407. );
  7408. // ingredientCache
  7409. window.moduleRegistry.add('ingredientCache', (request, Promise) => {
  7410.  
  7411. const initialised = new Promise.Expiring(2000, 'ingredientCache');
  7412.  
  7413. const exports = {
  7414. list: [],
  7415. byAction: {},
  7416. byItem: {}
  7417. };
  7418.  
  7419. async function tryInitialise() {
  7420. try {
  7421. await initialise();
  7422. initialised.resolve(exports);
  7423. } catch(e) {
  7424. initialised.reject(e);
  7425. }
  7426. }
  7427.  
  7428. async function initialise() {
  7429. const ingredients = await request.listIngredients();
  7430. for(const ingredient of ingredients) {
  7431. if(!exports.byAction[ingredient.action]) {
  7432. exports.byAction[ingredient.action] = [];
  7433. }
  7434. exports.byAction[ingredient.action].push(ingredient);
  7435. if(!exports.byItem[ingredient.item]) {
  7436. exports.byItem[ingredient.item] = [];
  7437. }
  7438. exports.byItem[ingredient.item].push(ingredient);
  7439. }
  7440. }
  7441.  
  7442. tryInitialise();
  7443.  
  7444. return initialised;
  7445.  
  7446. }
  7447. );
  7448. // itemCache
  7449. window.moduleRegistry.add('itemCache', (request, Promise) => {
  7450.  
  7451. const initialised = new Promise.Expiring(2000, 'itemCache');
  7452.  
  7453. const exports = {
  7454. list: [],
  7455. byId: {},
  7456. byName: {},
  7457. byImage: {},
  7458. attributes: null,
  7459. specialIds: {
  7460. coins: null,
  7461. mainHand: null,
  7462. offHand: null,
  7463. helmet: null,
  7464. body: null,
  7465. gloves: null,
  7466. boots: null,
  7467. amulet: null,
  7468. ring: null,
  7469. bracelet: null,
  7470. hatchet: null,
  7471. pickaxe: null,
  7472. spade: null,
  7473. rod: null,
  7474. dagger: null,
  7475. telescope: null,
  7476. lantern: null,
  7477. food: null,
  7478. ammo: null,
  7479. gatheringPotion: null,
  7480. craftingPotion: null,
  7481. combatPotion: null,
  7482. dungeonMap: null,
  7483. woodcuttingRune: null,
  7484. miningRune: null,
  7485. farmingRune: null,
  7486. fishingRune: null,
  7487. gatheringRune: null,
  7488. oneHandedRune: null,
  7489. twoHandedRune: null,
  7490. rangedRune: null,
  7491. defenseRune: null,
  7492. utilityRune: null,
  7493. savageLootingTome: null,
  7494. bountifulHarvestTome: null,
  7495. opulentCraftingTome: null,
  7496. eternalLifeTome: null,
  7497. insatiablePowerTome: null,
  7498. potentConcoctionTome: null,
  7499. }
  7500. };
  7501.  
  7502. async function tryInitialise() {
  7503. try {
  7504. await initialise();
  7505. initialised.resolve(exports);
  7506. } catch(e) {
  7507. initialised.reject(e);
  7508. }
  7509. }
  7510.  
  7511. async function initialise() {
  7512. const enrichedItems = await request.listItems();
  7513. for(const enrichedItem of enrichedItems) {
  7514. const item = Object.assign(enrichedItem.item, enrichedItem);
  7515. delete item.item;
  7516. exports.list.push(item);
  7517. exports.byId[item.id] = item;
  7518. exports.byName[item.name] = item;
  7519. const lastPart = item.image.split('/').at(-1);
  7520. if(exports.byImage[lastPart]) {
  7521. exports.byImage[lastPart].duplicate = true;
  7522. } else {
  7523. exports.byImage[lastPart] = item;
  7524. }
  7525. if(!item.attributes) {
  7526. item.attributes = {};
  7527. }
  7528. if(item.charcoal) {
  7529. item.attributes.CHARCOAL = item.charcoal;
  7530. }
  7531. if(item.compost) {
  7532. item.attributes.COMPOST = item.compost;
  7533. }
  7534. if(item.arcanePowder) {
  7535. item.attributes.ARCANE_POWDER = item.arcanePowder;
  7536. }
  7537. if(item.petSnacks) {
  7538. item.attributes.PET_SNACKS = item.petSnacks;
  7539. }
  7540. if(item.attributes.ATTACK_SPEED) {
  7541. item.attributes.ATTACK_SPEED /= 2;
  7542. }
  7543. for(const stat in item.stats.bySkill) {
  7544. if(item.stats.bySkill[stat].All) {
  7545. item.stats.global[stat] = item.stats.bySkill[stat].All;
  7546. delete item.stats.bySkill[stat].All;
  7547. if(!Object.keys(item.stats.bySkill[stat]).length) {
  7548. delete item.stats.bySkill[stat];
  7549. }
  7550. }
  7551. }
  7552. }
  7553. for(const image of Object.keys(exports.byImage)) {
  7554. if(exports.byImage[image].duplicate) {
  7555. delete exports.byImage[image];
  7556. }
  7557. }
  7558. exports.attributes = await request.listItemAttributes();
  7559. exports.attributes.push({
  7560. technicalName: 'CHARCOAL',
  7561. name: 'Charcoal',
  7562. image: '/assets/items/charcoal.png'
  7563. },{
  7564. technicalName: 'COMPOST',
  7565. name: 'Compost',
  7566. image: '/assets/misc/compost.png'
  7567. },{
  7568. technicalName: 'ARCANE_POWDER',
  7569. name: 'Arcane Powder',
  7570. image: '/assets/misc/arcane-powder.png'
  7571. },{
  7572. technicalName: 'PET_SNACKS',
  7573. name: 'Pet Snacks',
  7574. image: '/assets/misc/pet-snacks.png'
  7575. });
  7576. const potions = exports.list.filter(a => /(Potion|Mix)$/.exec(a.name));
  7577. // we do not cover any event items
  7578. exports.specialIds.coins = exports.byName['Coins'].id;
  7579. exports.specialIds.mainHand = getAllIdsEnding('Sword', 'Hammer', 'Spear', 'Scythe', 'Bow', 'Boomerang');
  7580. exports.specialIds.offHand = getAllIdsEnding('Shield');
  7581. exports.specialIds.helmet = getAllIdsEnding('Helmet');
  7582. exports.specialIds.body = getAllIdsEnding('Body');
  7583. exports.specialIds.gloves = getAllIdsEnding('Gloves');
  7584. exports.specialIds.boots = getAllIdsEnding('Boots');
  7585. exports.specialIds.amulet = getAllIdsEnding('Amulet');
  7586. exports.specialIds.ring = getAllIdsEnding('Ring');
  7587. exports.specialIds.bracelet = getAllIdsEnding('Bracelet');
  7588. exports.specialIds.hatchet = getAllIdsEnding('Hatchet');
  7589. exports.specialIds.pickaxe = getAllIdsEnding('Pickaxe');
  7590. exports.specialIds.spade = getAllIdsEnding('Spade');
  7591. exports.specialIds.rod = getAllIdsEnding('Rod');
  7592. exports.specialIds.dagger = getAllIdsEnding('Dagger');
  7593. exports.specialIds.telescope = getAllIdsEnding('Telescope');
  7594. exports.specialIds.lantern = getAllIdsEnding('Lantern');
  7595. exports.specialIds.food = exports.list.filter(a => a.stats.global.HEAL).map(a => a.id);
  7596. exports.specialIds.ammo = getAllIdsEnding('Arrow');
  7597. exports.specialIds.gatheringPotion = potions.filter(a => a.name.includes('Gather')).map(a => a.id);
  7598. exports.specialIds.craftingPotion = potions.filter(a => a.name.includes('Craft') || a.name.includes('Preservation')).map(a => a.id);
  7599. exports.specialIds.combatPotion = potions.filter(a => !a.name.includes('Gather') && !a.name.includes('Craft') && !a.name.includes('Preservation')).map(a => a.id);
  7600. exports.specialIds.dungeonMap = getAllIdsStarting('Dungeon Map');
  7601. exports.specialIds.woodcuttingRune = getAllIdsEnding('Woodcutting Rune');
  7602. exports.specialIds.miningRune = getAllIdsEnding('Mining Rune');
  7603. exports.specialIds.farmingRune = getAllIdsEnding('Farming Rune');
  7604. exports.specialIds.fishingRune = getAllIdsEnding('Fishing Rune');
  7605. exports.specialIds.gatheringRune = [
  7606. ...exports.specialIds.woodcuttingRune,
  7607. ...exports.specialIds.miningRune,
  7608. ...exports.specialIds.farmingRune,
  7609. ...exports.specialIds.fishingRune
  7610. ];
  7611. exports.specialIds.oneHandedRune = getAllIdsEnding('One-handed Rune');
  7612. exports.specialIds.twoHandedRune = getAllIdsEnding('Two-handed Rune');
  7613. exports.specialIds.rangedRune = getAllIdsEnding('Ranged Rune');
  7614. exports.specialIds.defenseRune = getAllIdsEnding('Defense Rune');
  7615. exports.specialIds.utilityRune = getAllIdsEnding('Crit Rune', 'Damage Rune', 'Block Rune', 'Stun Rune', 'Bleed Rune', 'Parry Rune');
  7616. exports.specialIds.savageLootingTome = getAllIdsStarting('Savage Looting Tome');
  7617. exports.specialIds.bountifulHarvestTome = getAllIdsStarting('Bountiful Harvest Tome');
  7618. exports.specialIds.opulentCraftingTome = getAllIdsStarting('Opulent Crafting Tome');
  7619. exports.specialIds.eternalLifeTome = getAllIdsStarting('Eternal Life Tome');
  7620. exports.specialIds.insatiablePowerTome = getAllIdsStarting('Insatiable Power Tome');
  7621. exports.specialIds.potentConcoctionTome = getAllIdsStarting('Potent Concoction Tome');
  7622. }
  7623.  
  7624. function getAllIdsEnding() {
  7625. const suffixes = Array.prototype.slice.call(arguments);
  7626. return exports.list.filter(a => new RegExp(`(${suffixes.join('|')})$`).exec(a.name)).map(a => a.id);
  7627. }
  7628.  
  7629. function getAllIdsStarting() {
  7630. const prefixes = Array.prototype.slice.call(arguments);
  7631. return exports.list.filter(a => new RegExp(`^(${prefixes.join('|')})`).exec(a.name)).map(a => a.id);
  7632. }
  7633.  
  7634. tryInitialise();
  7635.  
  7636. return initialised;
  7637.  
  7638. }
  7639. );
  7640. // monsterCache
  7641. window.moduleRegistry.add('monsterCache', (request, Promise) => {
  7642.  
  7643. const initialised = new Promise.Expiring(2000, 'monsterCache');
  7644.  
  7645. const exports = {
  7646. list: [],
  7647. byId: {},
  7648. byName: {}
  7649. };
  7650.  
  7651. async function tryInitialise() {
  7652. try {
  7653. await initialise();
  7654. initialised.resolve(exports);
  7655. } catch(e) {
  7656. initialised.reject(e);
  7657. }
  7658. }
  7659.  
  7660. async function initialise() {
  7661. const monsters = await request.listMonsters();
  7662. for(const monster of monsters) {
  7663. exports.list.push(monster);
  7664. exports.byId[monster.id] = monster;
  7665. exports.byName[monster.name] = monster;
  7666. }
  7667. }
  7668.  
  7669. tryInitialise();
  7670.  
  7671. return initialised;
  7672.  
  7673. }
  7674. );
  7675. // petCache
  7676. window.moduleRegistry.add('petCache', (request, Promise) => {
  7677.  
  7678. const initialised = new Promise.Expiring(2000, 'petCache');
  7679.  
  7680. const exports = {
  7681. list: [],
  7682. byId: {},
  7683. byName: {},
  7684. byImage: {},
  7685. idToIndex: {}
  7686. };
  7687.  
  7688. async function tryInitialise() {
  7689. try {
  7690. await initialise();
  7691. initialised.resolve(exports);
  7692. } catch(e) {
  7693. initialised.reject(e);
  7694. }
  7695. }
  7696.  
  7697. async function initialise() {
  7698. const pets = await request.listPets();
  7699. for(const pet of pets) {
  7700. exports.list.push(pet);
  7701. exports.byId[pet.id] = pet;
  7702. exports.byName[pet.name] = pet;
  7703. exports.idToIndex[pet.id] = exports.list.length-1;
  7704. const lastPart = pet.image.split('/').at(-1);
  7705. exports.byImage[lastPart] = pet;
  7706. pet.abilities = [{
  7707. [pet.abilityName1]: pet.abilityValue1
  7708. }];
  7709. if(pet.abilityName2) {
  7710. pet.abilities.push({
  7711. [pet.abilityName2]: pet.abilityValue2
  7712. });
  7713. }
  7714. delete pet.abilityName1;
  7715. delete pet.abilityValue1;
  7716. delete pet.abilityName2;
  7717. delete pet.abilityValue2;
  7718. }
  7719. }
  7720.  
  7721. tryInitialise();
  7722.  
  7723. return initialised;
  7724.  
  7725. }
  7726. );
  7727. // petPassiveCache
  7728. window.moduleRegistry.add('petPassiveCache', (request, Promise) => {
  7729.  
  7730. const initialised = new Promise.Expiring(2000, 'petPassiveCache');
  7731.  
  7732. const exports = {
  7733. list: [],
  7734. byId: {},
  7735. byName: {},
  7736. idToIndex: {}
  7737. };
  7738.  
  7739. async function tryInitialise() {
  7740. try {
  7741. await initialise();
  7742. initialised.resolve(exports);
  7743. } catch(e) {
  7744. initialised.reject(e);
  7745. }
  7746. }
  7747.  
  7748. async function initialise() {
  7749. const petPassives = await request.listPetPassives();
  7750. for(const petPassive of petPassives) {
  7751. exports.list.push(petPassive);
  7752. exports.byId[petPassive.id] = petPassive;
  7753. exports.byName[petPassive.name] = petPassive;
  7754. exports.idToIndex[petPassive.id] = exports.list.length-1;
  7755. petPassive.stats = {
  7756. name: petPassive.statName,
  7757. value: petPassive.statValue
  7758. };
  7759. delete petPassive.statName;
  7760. delete petPassive.statValue;
  7761. }
  7762. }
  7763.  
  7764. tryInitialise();
  7765.  
  7766. return initialised;
  7767.  
  7768. }
  7769. );
  7770. // petTraitCache
  7771. window.moduleRegistry.add('petTraitCache', () => {
  7772.  
  7773. const exports = {
  7774. list: [],
  7775. byId: {},
  7776. byName: {},
  7777. idToIndex: {}
  7778. };
  7779.  
  7780. function initialise() {
  7781. const traits = ['Attack & Defense', 'Attack & Special Def', 'Special Atk & Defense', 'Special Atk & Special Def'];
  7782. for(const trait of traits) {
  7783. const value = {
  7784. id: exports.list.length,
  7785. name: trait,
  7786. attack: trait.startsWith('Attack'),
  7787. defense: trait.endsWith('Defense'),
  7788. specialAttack: trait.startsWith('Special Atk'),
  7789. specialDefense: trait.endsWith('Special Def')
  7790. };
  7791. exports.list.push(value);
  7792. exports.byId[value.id] = value;
  7793. exports.byName[value.name] = value;
  7794. exports.idToIndex[value.id] = exports.list.length-1;
  7795. }
  7796. }
  7797.  
  7798. initialise();
  7799.  
  7800. return exports;
  7801.  
  7802. }
  7803. );
  7804. // recipeCache
  7805. window.moduleRegistry.add('recipeCache', (request, Promise) => {
  7806.  
  7807. const initialised = new Promise.Expiring(2000, 'recipeCache');
  7808.  
  7809. const exports = {
  7810. list: [],
  7811. byId: {},
  7812. byName: {},
  7813. byImage: {}
  7814. };
  7815.  
  7816. async function tryInitialise() {
  7817. try {
  7818. await initialise();
  7819. initialised.resolve(exports);
  7820. } catch(e) {
  7821. initialised.reject(e);
  7822. }
  7823. }
  7824.  
  7825. async function initialise() {
  7826. exports.list = await request.listRecipes();
  7827. for(const recipe of exports.list) {
  7828. exports.byId[recipe.id] = recipe;
  7829. exports.byName[recipe.name] = recipe;
  7830. const lastPart = recipe.image.split('/').at(-1);
  7831. exports.byImage[lastPart] = recipe;
  7832. }
  7833. }
  7834.  
  7835. tryInitialise();
  7836.  
  7837. return initialised;
  7838.  
  7839. }
  7840. );
  7841. // skillCache
  7842. window.moduleRegistry.add('skillCache', (request, Promise) => {
  7843.  
  7844. const initialised = new Promise.Expiring(2000, 'skillCache');
  7845.  
  7846. const exports = {
  7847. list: [],
  7848. byId: {},
  7849. byName: {},
  7850. byTechnicalName: {},
  7851. };
  7852.  
  7853. async function tryInitialise() {
  7854. try {
  7855. await initialise();
  7856. initialised.resolve(exports);
  7857. } catch(e) {
  7858. initialised.reject(e);
  7859. }
  7860. }
  7861.  
  7862. async function initialise() {
  7863. const skills = await request.listSkills();
  7864. for(const skill of skills) {
  7865. exports.list.push(skill);
  7866. exports.byId[skill.id] = skill;
  7867. exports.byName[skill.displayName] = skill;
  7868. exports.byTechnicalName[skill.technicalName] = skill;
  7869. }
  7870. }
  7871.  
  7872. tryInitialise();
  7873.  
  7874. return initialised;
  7875.  
  7876. }
  7877. );
  7878. // statNameCache
  7879. window.moduleRegistry.add('statNameCache', () => {
  7880.  
  7881. const exports = {
  7882. validate
  7883. };
  7884.  
  7885. const statNames = new Set([
  7886. // ITEM_STAT_ATTRIBUTE
  7887. 'AMMO_PRESERVATION_CHANCE',
  7888. 'ATTACK_SPEED',
  7889. 'BONUS_LEVEL',
  7890. 'COIN_SNATCH',
  7891. 'COMBAT_EXP',
  7892. 'DOUBLE_EXP',
  7893. 'DOUBLE_DROP',
  7894. 'EFFICIENCY',
  7895. 'LOWER_TIER_CHANCE',
  7896. 'MERCHANT_SELL_CHANCE',
  7897. 'PRESERVATION',
  7898. 'SKILL_SPEED',
  7899. // ITEM_ATTRIBUTE
  7900. 'ARMOUR',
  7901. 'BLEED_CHANCE',
  7902. 'BLOCK_CHANCE',
  7903. 'CARVE_CHANCE',
  7904. 'COIN_SNATCH',
  7905. 'COMBAT_EXP',
  7906. 'CRIT_CHANCE',
  7907. 'DAMAGE',
  7908. 'DAMAGE_PERCENT',
  7909. 'DAMAGE_RANGE',
  7910. 'DECREASED_POTION_DURATION',
  7911. 'DUNGEON_DAMAGE',
  7912. 'FOOD_EFFECT',
  7913. 'FOOD_PRESERVATION_CHANCE',
  7914. 'HEAL',
  7915. 'HEALTH',
  7916. 'HEALTH_PERCENT',
  7917. 'INCREASED_POTION_EFFECT',
  7918. 'MAP_FIND_CHANCE',
  7919. 'PARRY_CHANCE',
  7920. 'PASSIVE_FOOD_CONSUMPTION',
  7921. 'REVIVE_TIME',
  7922. 'STUN_CHANCE',
  7923. 'DUNGEON_TIME',
  7924. // FRONTEND ONLY
  7925. 'MAX_AMOUNT'
  7926. ]);
  7927.  
  7928. function validate(name) {
  7929. if(!statNames.has(name)) {
  7930. throw `Unsupported stat usage : ${name}`;
  7931. }
  7932. }
  7933.  
  7934. return exports;
  7935.  
  7936. });
  7937. // structuresCache
  7938. window.moduleRegistry.add('structuresCache', (request, Promise) => {
  7939.  
  7940. const initialised = new Promise.Expiring(2000, 'structuresCache');
  7941.  
  7942. const exports = {
  7943. list: [],
  7944. byId: {},
  7945. byName: {}
  7946. };
  7947.  
  7948. async function tryInitialise() {
  7949. try {
  7950. await initialise();
  7951. initialised.resolve(exports);
  7952. } catch(e) {
  7953. initialised.reject(e);
  7954. }
  7955. }
  7956.  
  7957. async function initialise() {
  7958. const structures = await request.listStructures();
  7959. for(const structure of structures) {
  7960. exports.list.push(structure);
  7961. exports.byId[structure.id] = structure;
  7962. exports.byName[structure.name] = structure;
  7963. }
  7964. }
  7965.  
  7966. tryInitialise();
  7967.  
  7968. return initialised;
  7969.  
  7970. }
  7971. );
  7972. window.moduleRegistry.build();