Ironwood RPG - Pancake-Scripts

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

当前为 2024-01-13 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Ironwood RPG - Pancake-Scripts
  3. // @namespace http://tampermonkey.net/
  4. // @version 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. // @run-at document-body
  11. // @require https://code.jquery.com/jquery-3.6.4.min.js
  12. // @require https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.0/chart.umd.js
  13. // ==/UserScript==
  14.  
  15. window.PANCAKE_ROOT = 'https://iwrpg.vectordungeon.com';
  16. window.PANCAKE_VERSION = '4.0';
  17. (() => {
  18.  
  19. if(window.moduleRegistry) {
  20. return;
  21. }
  22.  
  23. window.moduleRegistry = {
  24. add,
  25. get,
  26. build
  27. };
  28.  
  29. const modules = {};
  30.  
  31. function add(name, initialiser) {
  32. modules[name] = createModule(name, initialiser);
  33. }
  34.  
  35. function get(name) {
  36. return modules[name] || null;
  37. }
  38.  
  39. async function build() {
  40. for(const module of Object.values(modules)) {
  41. await buildModule(module);
  42. }
  43. }
  44.  
  45. function createModule(name, initialiser) {
  46. const dependencies = extractParametersFromFunction(initialiser).map(dependency => {
  47. const name = dependency.replaceAll('_', '');
  48. const module = get(name);
  49. const optional = dependency.startsWith('_');
  50. return { name, module, optional };
  51. });
  52. const module = {
  53. name,
  54. initialiser,
  55. dependencies
  56. };
  57. for(const other of Object.values(modules)) {
  58. for(const dependency of other.dependencies) {
  59. if(dependency.name === name) {
  60. dependency.module = module;
  61. }
  62. }
  63. }
  64. return module;
  65. }
  66.  
  67. async function buildModule(module, partial, chain) {
  68. if(module.built) {
  69. return true;
  70. }
  71.  
  72. chain = chain || [];
  73. if(chain.includes(module.name)) {
  74. chain.push(module.name);
  75. throw `Circular dependency in chain : ${chain.join(' -> ')}`;
  76. }
  77. chain.push(module.name);
  78.  
  79. for(const dependency of module.dependencies) {
  80. if(!dependency.module) {
  81. if(partial) {
  82. return false;
  83. }
  84. if(dependency.optional) {
  85. continue;
  86. }
  87. throw `Unresolved dependency : ${dependency.name}`;
  88. }
  89. const built = await buildModule(dependency.module, partial, chain);
  90. if(!built) {
  91. return false;
  92. }
  93. }
  94.  
  95. const parameters = module.dependencies.map(a => a.module?.reference);
  96. try {
  97. module.reference = await module.initialiser.apply(null, parameters);
  98. } catch(e) {
  99. console.error(`Failed building ${module.name}`, e);
  100. return false;
  101. }
  102. module.built = true;
  103.  
  104. chain.pop();
  105. return true;
  106. }
  107.  
  108. function extractParametersFromFunction(fn) {
  109. const PARAMETER_NAMES = /([^\s,]+)/g;
  110. var fnStr = fn.toString();
  111. var result = fnStr.slice(fnStr.indexOf('(')+1, fnStr.indexOf(')')).match(PARAMETER_NAMES);
  112. return result || [];
  113. }
  114.  
  115. })();
  116. // colorMapper
  117. window.moduleRegistry.add('colorMapper', () => {
  118.  
  119. const colorMappings = {
  120. // https://colorswall.com/palette/3
  121. primary: '#0275d8',
  122. success: '#5cb85c',
  123. info: '#5bc0de',
  124. warning: '#f0ad4e',
  125. danger: '#d9534f',
  126. inverse: '#292b2c',
  127. // component styling
  128. componentLight: '#393532',
  129. componentRegular: '#28211b',
  130. componentDark: '#211a12'
  131. };
  132.  
  133. function mapColor(color) {
  134. return colorMappings[color] || color;
  135. }
  136.  
  137. return mapColor;
  138.  
  139. }
  140. );
  141. // components
  142. window.moduleRegistry.add('components', (elementWatcher, colorMapper, elementCreator) => {
  143.  
  144. const exports = {
  145. addComponent,
  146. removeComponent,
  147. search
  148. }
  149.  
  150. const $ = window.$;
  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.  
  165. function initialise() {
  166. elementCreator.addStyles(styles);
  167. }
  168.  
  169. function removeComponent(blueprint) {
  170. $(`#${blueprint.componentId}`).remove();
  171. }
  172.  
  173. async function addComponent(blueprint) {
  174. if($(blueprint.dependsOn).length) {
  175. actualAddComponent(blueprint);
  176. return;
  177. }
  178. await elementWatcher.exists(blueprint.dependsOn);
  179. actualAddComponent(blueprint);
  180. }
  181.  
  182. function actualAddComponent(blueprint) {
  183. $(`#${blueprint.componentId}`).remove();
  184. const component =
  185. $('<div/>')
  186. .addClass('customComponent')
  187. .attr('id', blueprint.componentId);
  188. if(blueprint.onClick) {
  189. component
  190. .click(blueprint.onClick)
  191. .css('cursor', 'pointer');
  192. }
  193.  
  194. // TABS
  195. const theTabs = createTab(blueprint);
  196. component.append(theTabs);
  197.  
  198. // PAGE
  199. const selectedTabBlueprint = blueprint.tabs[blueprint.selectedTabIndex] || blueprint.tabs[0];
  200. selectedTabBlueprint.rows.forEach((rowBlueprint, index) => {
  201. component.append(createRow(rowBlueprint));
  202. });
  203.  
  204. if(blueprint.prepend) {
  205. $(`${blueprint.parent}`).prepend(component);
  206. } else {
  207. $(`${blueprint.parent}`).append(component);
  208. }
  209. }
  210.  
  211. function createTab(blueprint) {
  212. if(!blueprint.selectedTabIndex) {
  213. blueprint.selectedTabIndex = 0;
  214. }
  215. if(blueprint.tabs.length === 1) {
  216. return;
  217. }
  218. const tabContainer = $('<div/>').addClass('tabs');
  219. blueprint.tabs.forEach((element, index) => {
  220. if(element.hidden) {
  221. return;
  222. }
  223. const tab = $('<button/>')
  224. .attr('type', 'button')
  225. .addClass('tabButton')
  226. .text(element.title)
  227. .click(changeTab.bind(null, blueprint, index));
  228. if(blueprint.selectedTabIndex !== index) {
  229. tab.addClass('tabButtonInactive')
  230. }
  231. if(index !== 0) {
  232. tab.addClass('lineLeft')
  233. }
  234. tabContainer.append(tab);
  235. });
  236. return tabContainer;
  237. }
  238.  
  239. function createRow(rowBlueprint) {
  240. if(!rowTypeMappings[rowBlueprint.type]) {
  241. console.warn(`Skipping unknown row type in blueprint: ${rowBlueprint.type}`, rowBlueprint);
  242. return;
  243. }
  244. if(rowBlueprint.hidden) {
  245. return;
  246. }
  247. return rowTypeMappings[rowBlueprint.type](rowBlueprint);
  248. }
  249.  
  250. function createRow_Item(itemBlueprint) {
  251. const parentRow = $('<div/>').addClass('customRow');
  252. if(itemBlueprint.image) {
  253. parentRow.append(createImage(itemBlueprint));
  254. }
  255. if(itemBlueprint?.name) {
  256. parentRow
  257. .append(
  258. $('<div/>')
  259. .addClass('myItemName name')
  260. .text(itemBlueprint.name)
  261. );
  262. }
  263. parentRow // always added because it spreads pushes name left and value right !
  264. .append(
  265. $('<div/>')
  266. .addClass('myItemValue')
  267. .text(itemBlueprint?.extra || '')
  268. );
  269. if(itemBlueprint?.value) {
  270. parentRow
  271. .append(
  272. $('<div/>')
  273. .addClass('myItemWorth')
  274. .text(itemBlueprint.value)
  275. )
  276. }
  277. return parentRow;
  278. }
  279.  
  280. function createRow_Input(inputBlueprint) {
  281. const parentRow = $('<div/>').addClass('customRow');
  282. if(inputBlueprint.text) {
  283. parentRow
  284. .append(
  285. $('<div/>')
  286. .addClass('myItemInputText')
  287. .addClass(inputBlueprint.class || '')
  288. .text(inputBlueprint.text)
  289. .css('flex', `${inputBlueprint.layout?.split('/')[0] || 1}`)
  290. )
  291. }
  292. parentRow
  293. .append(
  294. $('<input/>')
  295. .attr('id', inputBlueprint.id)
  296. .addClass('myItemInput')
  297. .addClass(inputBlueprint.class || '')
  298. .attr('type', inputBlueprint.inputType || 'text')
  299. .attr('placeholder', inputBlueprint.name)
  300. .attr('value', inputBlueprint.value || '')
  301. .css('flex', `${inputBlueprint.layout?.split('/')[1] || 1}`)
  302. .keyup(inputDelay(function(e) {
  303. inputBlueprint.value = e.target.value;
  304. inputBlueprint.action(inputBlueprint.value);
  305. }, inputBlueprint.delay || 0))
  306. )
  307. return parentRow;
  308. }
  309.  
  310. function createRow_Break(breakBlueprint) {
  311. const parentRow = $('<div/>').addClass('customRow');
  312. parentRow.append('<br/>');
  313. return parentRow;
  314. }
  315.  
  316. function createRow_Button(buttonBlueprint) {
  317. const parentRow = $('<div/>').addClass('customRow');
  318. for(const button of buttonBlueprint.buttons) {
  319. parentRow
  320. .append(
  321. $(`<button class='myButton'>${button.text}</button>`)
  322. .css('background-color', button.disabled ? '#ffffff0a' : colorMapper(button.color || 'primary'))
  323. .css('flex', `${button.size || 1} 1 0`)
  324. .prop('disabled', !!button.disabled)
  325. .addClass(button.class || '')
  326. .click(button.action)
  327. );
  328. }
  329. return parentRow;
  330. }
  331.  
  332. function createRow_Select(selectBlueprint) {
  333. const parentRow = $('<div/>').addClass('customRow');
  334. const select = $('<select/>')
  335. .addClass('myItemSelect')
  336. .addClass(selectBlueprint.class || '')
  337. .change(inputDelay(function(e) {
  338. for(const option of selectBlueprint.options) {
  339. option.selected = this.value === option.value;
  340. }
  341. selectBlueprint.action(this.value);
  342. }, selectBlueprint.delay || 0));
  343. for(const option of selectBlueprint.options) {
  344. select.append(`<option value='${option.value}' ${option.selected ? 'selected' : ''}>${option.text}</option>`);
  345. }
  346. parentRow.append(select);
  347. return parentRow;
  348. }
  349.  
  350. function createRow_Header(headerBlueprint) {
  351. const parentRow =
  352. $('<div/>')
  353. .addClass('myHeader lineTop')
  354. if(headerBlueprint.image) {
  355. parentRow.append(createImage(headerBlueprint));
  356. }
  357. parentRow.append(
  358. $('<div/>')
  359. .addClass('myName')
  360. .text(headerBlueprint.title)
  361. )
  362. if(headerBlueprint.action) {
  363. parentRow
  364. .append(
  365. $('<button/>')
  366. .addClass('myHeaderAction')
  367. .text(headerBlueprint.name)
  368. .attr('type', 'button')
  369. .css('background-color', colorMapper(headerBlueprint.color || 'success'))
  370. .click(headerBlueprint.action)
  371. )
  372. } else if(headerBlueprint.textRight) {
  373. parentRow.append(
  374. $('<div/>')
  375. .addClass('level')
  376. .text(headerBlueprint.title)
  377. .css('margin-left', 'auto')
  378. .html(headerBlueprint.textRight)
  379. )
  380. }
  381. if(headerBlueprint.centered) {
  382. parentRow.css('justify-content', 'center');
  383. }
  384. return parentRow;
  385. }
  386.  
  387. function createRow_Checkbox(checkboxBlueprint) {
  388. 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>`;
  389. 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>`;
  390.  
  391. const buttonInnerHTML = checkboxBlueprint.checked ? checked_true : checked_false;
  392.  
  393. const parentRow = $('<div/>').addClass('customRow')
  394. .append(
  395. $('<div/>')
  396. .addClass('customCheckBoxText')
  397. .text(checkboxBlueprint?.text || '')
  398. )
  399. .append(
  400. $('<div/>')
  401. .addClass('customCheckboxCheckbox')
  402. .append(
  403. $(`<button>${buttonInnerHTML}</button>`)
  404. .html(buttonInnerHTML)
  405. .click(() => {
  406. checkboxBlueprint.checked = !checkboxBlueprint.checked;
  407. checkboxBlueprint.action(checkboxBlueprint.checked);
  408. })
  409. )
  410.  
  411. );
  412.  
  413. return parentRow;
  414. }
  415.  
  416. function createRow_Segment(segmentBlueprint) {
  417. if(segmentBlueprint.hidden) {
  418. return;
  419. }
  420. return segmentBlueprint.rows.flatMap(createRow);
  421. }
  422.  
  423. function createRow_Progress(progressBlueprint) {
  424. const parentRow = $('<div/>').addClass('customRow');
  425. const up = progressBlueprint.numerator;
  426. const down = progressBlueprint.denominator;
  427. parentRow.append(
  428. $('<div/>')
  429. .addClass('myBar')
  430. .append(
  431. $('<div/>')
  432. .css('height', '100%')
  433. .css('width', progressBlueprint.progressPercent + '%')
  434. .css('background-color', colorMapper(progressBlueprint.color || 'rgb(122, 118, 118)'))
  435. )
  436. );
  437. parentRow.append(
  438. $('<div/>')
  439. .addClass('myPercent')
  440. .text(progressBlueprint.progressPercent + '%')
  441. )
  442. parentRow.append(
  443. $('<div/>')
  444. .css('margin-left', 'auto')
  445. .text(progressBlueprint.progressText)
  446. )
  447. return parentRow;
  448. }
  449.  
  450. function createRow_Chart(chartBlueprint) {
  451. const parentRow = $('<div/>')
  452. .addClass('lineTop')
  453. .append(
  454. $('<canvas/>')
  455. .attr('id', chartBlueprint.chartId)
  456. );
  457. return parentRow;
  458. }
  459.  
  460. function createRow_List(listBlueprint) {
  461. const parentRow = $('<div/>').addClass('customRow');
  462. parentRow // always added because it spreads pushes name left and value right !
  463. .append(
  464. $('<ul/>')
  465. .addClass('myListDescription')
  466. .append(...listBlueprint.entries.map(entry =>
  467. $('<li/>')
  468. .addClass('myListLine')
  469. .text(entry)
  470. ))
  471. );
  472. return parentRow;
  473. }
  474. function createImage(blueprint) {
  475. return $('<div/>')
  476. .addClass('myItemImage image')
  477. .append(
  478. $('<img/>')
  479. .attr('src', `${blueprint.image}`)
  480. .css('filter', `${blueprint.imageFilter}`)
  481. .css('image-rendering', blueprint.imagePixelated ? 'pixelated' : 'auto')
  482. )
  483. }
  484.  
  485. function changeTab(blueprint, index) {
  486. blueprint.selectedTabIndex = index;
  487. addComponent(blueprint);
  488. }
  489.  
  490. function inputDelay(callback, ms) {
  491. var timer = 0;
  492. return function() {
  493. var context = this, args = arguments;
  494. window.clearTimeout(timer);
  495. timer = window.setTimeout(function() {
  496. callback.apply(context, args);
  497. }, ms || 0);
  498. };
  499. }
  500.  
  501. function search(blueprint, query) {
  502. if(!blueprint.idMappings) {
  503. generateIdMappings(blueprint);
  504. }
  505. if(!blueprint.idMappings[query]) {
  506. throw `Could not find id ${query} in blueprint ${blueprint.componentId}`;
  507. }
  508. return blueprint.idMappings[query];
  509. }
  510.  
  511. function generateIdMappings(blueprint) {
  512. blueprint.idMappings = {};
  513. for(const tab of blueprint.tabs) {
  514. addIdMapping(blueprint, tab);
  515. for(const row of tab.rows) {
  516. addIdMapping(blueprint, row);
  517. }
  518. }
  519. }
  520.  
  521. function addIdMapping(blueprint, element) {
  522. if(element.id) {
  523. if(blueprint.idMappings[element.id]) {
  524. throw `Detected duplicate id ${element.id} in blueprint ${blueprint.componentId}`;
  525. }
  526. blueprint.idMappings[element.id] = element;
  527. }
  528. let subelements = null;
  529. if(element.type === 'segment') {
  530. subelements = element.rows;
  531. }
  532. if(element.type === 'buttons') {
  533. subelements = element.buttons;
  534. }
  535. if(subelements) {
  536. for(const subelement of subelements) {
  537. addIdMapping(blueprint, subelement);
  538. }
  539. }
  540. }
  541.  
  542. const styles = `
  543. :root {
  544. --background-color: ${colorMapper('componentRegular')};
  545. --border-color: ${colorMapper('componentLight')};
  546. --darker-color: ${colorMapper('componentDark')};
  547. }
  548. .customComponent {
  549. margin-top: var(--gap);
  550. background-color: var(--background-color);
  551. box-shadow: 0 6px 12px -6px #0006;
  552. border-radius: 4px;
  553. width: 100%;
  554. }
  555. .myHeader {
  556. display: flex;
  557. align-items: center;
  558. padding: 12px var(--gap);
  559. gap: var(--gap);
  560. }
  561. .myName {
  562. font-weight: 600;
  563. letter-spacing: .25px;
  564. }
  565. .myHeaderAction{
  566. margin: 0px 0px 0px auto;
  567. border: 1px solid var(--border-color);
  568. border-radius: 4px;
  569. padding: 0px 5px;
  570. }
  571. .customRow {
  572. display: flex;
  573. justify-content: center;
  574. align-items: center;
  575. border-top: 1px solid var(--border-color);
  576. /*padding: 5px 12px 5px 6px;*/
  577. min-height: 0px;
  578. min-width: 0px;
  579. gap: var(--margin);
  580. padding: calc(var(--gap) / 2) var(--gap);
  581. }
  582. .myItemImage {
  583. position: relative;
  584. display: flex;
  585. align-items: center;
  586. justify-content: center;
  587. height: 24px;
  588. width: 24px;
  589. min-height: 0px;
  590. min-width: 0px;
  591. }
  592. .myItemImage > img {
  593. max-width: 100%;
  594. max-height: 100%;
  595. width: 100%;
  596. height: 100%;
  597. }
  598. .myItemValue {
  599. display: flex;
  600. align-items: center;
  601. flex: 1;
  602. color: #aaa;
  603. }
  604. .myItemInputText {
  605. height: 40px;
  606. width: 100%;
  607. display: flex;
  608. align-items: center;
  609. padding: 12px var(--gap);
  610. }
  611. .myItemInput {
  612. height: 40px;
  613. width: 100%;
  614. background-color: #ffffff0a;
  615. padding: 0 12px;
  616. text-align: center;
  617. border-radius: 4px;
  618. border: 1px solid var(--border-color);
  619. }
  620. .myItemSelect {
  621. height: 40px;
  622. width: 100%;
  623. background-color: #ffffff0a;
  624. padding: 0 12px;
  625. text-align: center;
  626. border-radius: 4px;
  627. border: 1px solid var(--border-color);
  628. }
  629. .myItemSelect > option {
  630. background-color: var(--darker-color);
  631. }
  632. .myButton {
  633. flex: 1;
  634. display: flex;
  635. align-items: center;
  636. justify-content: center;
  637. border-radius: 4px;
  638. height: 40px;
  639. font-weight: 600;
  640. letter-spacing: .25px;
  641. }
  642. .myButton[disabled] {
  643. pointer-events: none;
  644. }
  645. .sort {
  646. padding: 12px var(--gap);
  647. border-top: 1px solid var(--border-color);
  648. display: flex;
  649. align-items: center;
  650. justify-content: space-between;
  651. }
  652. .sortButtonContainer {
  653. display: flex;
  654. align-items: center;
  655. border-radius: 4px;
  656. box-shadow: 0 1px 2px #0003;
  657. border: 1px solid var(--border-color);
  658. overflow: hidden;
  659. }
  660. .sortButton {
  661. display: flex;
  662. border: none;
  663. background: transparent;
  664. font-family: inherit;
  665. font-size: inherit;
  666. line-height: 1.5;
  667. font-weight: inherit;
  668. color: inherit;
  669. resize: none;
  670. text-transform: inherit;
  671. letter-spacing: inherit;
  672. cursor: pointer;
  673. padding: 4px var(--gap);
  674. flex: 1;
  675. text-align: center;
  676. justify-content: center;
  677. background-color: var(--darker-color);
  678. }
  679. .tabs {
  680. display: flex;
  681. align-items: center;
  682. overflow: hidden;
  683. border-radius: inherit;
  684. }
  685. .tabButton {
  686. border: none;
  687. border-radius: 0px !important;
  688. background: transparent;
  689. font-family: inherit;
  690. font-size: inherit;
  691. line-height: 1.5;
  692. color: inherit;
  693. resize: none;
  694. text-transform: inherit;
  695. cursor: pointer;
  696. flex: 1;
  697. display: flex;
  698. align-items: center;
  699. justify-content: center;
  700. height: 48px;
  701. font-weight: 600;
  702. letter-spacing: .25px;
  703. padding: 0 var(--gap);
  704. border-radius: 4px 0 0;
  705. }
  706. .tabButtonInactive{
  707. background-color: var(--darker-color);
  708. }
  709. .lineRight {
  710. border-right: 1px solid var(--border-color);
  711. }
  712. .lineLeft {
  713. border-left: 1px solid var(--border-color);
  714. }
  715. .lineTop {
  716. border-top: 1px solid var(--border-color);
  717. }
  718. .customCheckBoxText {
  719. flex: 1;
  720. color: #aaa
  721. }
  722. .customCheckboxCheckbox {
  723. display: flex;
  724. justify-content: flex-end;
  725. min-width: 32px;
  726. margin-left: var(--margin);
  727. }
  728. .customCheckBoxEnabled {
  729. color: #53bd73
  730. }
  731. .customCheckBoxDisabled {
  732. color: #aaa
  733. }
  734. .myBar {
  735. height: 12px;
  736. flex: 1;
  737. background-color: #ffffff0a;
  738. overflow: hidden;
  739. max-width: 50%;
  740. border-radius: 999px;
  741. }
  742. .myPercent {
  743. margin-left: var(--margin);
  744. margin-right: var(--margin);
  745. color: #aaa;
  746. }
  747. .myListDescription {
  748. list-style: disc;
  749. width: 100%;
  750. }
  751. .myListLine {
  752. margin-left: 20px;
  753. }
  754. `;
  755.  
  756. initialise();
  757.  
  758. return exports;
  759. }
  760. );
  761. // configuration
  762. window.moduleRegistry.add('configuration', (Promise, localConfigurationStore, _remoteConfigurationStore) => {
  763.  
  764. const loaded = new Promise.Deferred();
  765. const configurationStore = _remoteConfigurationStore || localConfigurationStore;
  766.  
  767. const exports = {
  768. registerCheckbox,
  769. registerInput,
  770. registerDropdown,
  771. registerJson,
  772. items: []
  773. };
  774.  
  775. async function initialise() {
  776. const configs = await configurationStore.load();
  777. loaded.resolve(configs);
  778. }
  779.  
  780. const CHECKBOX_KEYS = ['category', 'key', 'name', 'default', 'handler'];
  781. function registerCheckbox(item) {
  782. validate(item, CHECKBOX_KEYS);
  783. return register(Object.assign(item, {
  784. type: 'checkbox'
  785. }));
  786. }
  787.  
  788. const INPUT_KEYS = ['category', 'key', 'name', 'default', 'inputType', 'handler'];
  789. function registerInput(item) {
  790. validate(item, INPUT_KEYS);
  791. return register(Object.assign(item, {
  792. type: 'input'
  793. }));
  794. }
  795.  
  796. const DROPDOWN_KEYS = ['category', 'key', 'name', 'options', 'default', 'handler'];
  797. function registerDropdown(item) {
  798. validate(item, DROPDOWN_KEYS);
  799. return register(Object.assign(item, {
  800. type: 'dropdown'
  801. }));
  802. }
  803.  
  804. const JSON_KEYS = ['key', 'default', 'handler'];
  805. function registerJson(item) {
  806. validate(item, JSON_KEYS);
  807. return register(Object.assign(item, {
  808. type: 'json'
  809. }));
  810. }
  811.  
  812. function register(item) {
  813. const handler = item.handler;
  814. item.handler = (value, isInitial) => {
  815. item.value = value;
  816. handler(value, item.key, isInitial);
  817. if(!isInitial) {
  818. save(item, value);
  819. }
  820. }
  821. loaded.then(configs => {
  822. let value;
  823. if(item.key in configs) {
  824. value = JSON.parse(configs[item.key]);
  825. } else {
  826. value = item.default;
  827. }
  828. item.handler(value, true);
  829. });
  830. exports.items.push(item);
  831. return item;
  832. }
  833.  
  834. async function save(item, value) {
  835. if(item.type === 'toggle') {
  836. value = !!value;
  837. }
  838. if(item.type === 'input' || item.type === 'json') {
  839. value = JSON.stringify(value);
  840. }
  841. await configurationStore.save(item.key, value);
  842. }
  843.  
  844. function validate(item, keys) {
  845. for(const key of keys) {
  846. if(!(key in item)) {
  847. throw `Missing ${key} while registering a configuration item`;
  848. }
  849. }
  850. }
  851.  
  852. initialise();
  853.  
  854. return exports;
  855.  
  856. }
  857. );
  858. // Distribution
  859. window.moduleRegistry.add('Distribution', () => {
  860.  
  861. class Distribution {
  862.  
  863. #map = new Map();
  864.  
  865. constructor(initial) {
  866. if(initial) {
  867. this.add(initial, 1);
  868. }
  869. }
  870.  
  871. add(value, probability) {
  872. if(this.#map.has(value)) {
  873. this.#map.set(value, this.#map.get(value) + probability);
  874. } else {
  875. this.#map.set(value, probability);
  876. }
  877. }
  878.  
  879. addDistribution(other, weight) {
  880. other.#map.forEach((probability, value) => {
  881. this.add(value, probability * weight);
  882. });
  883. }
  884.  
  885. convolution(other, multiplier) {
  886. const old = this.#map;
  887. this.#map = new Map();
  888. old.forEach((probability, value) => {
  889. other.#map.forEach((probability2, value2) => {
  890. this.add(multiplier(value, value2), probability * probability2);
  891. });
  892. });
  893. }
  894.  
  895. convolutionWithGenerator(generator, multiplier) {
  896. const result = new Distribution();
  897. this.#map.forEach((probability, value) => {
  898. const other = generator(value);
  899. other.#map.forEach((probability2, value2) => {
  900. result.add(multiplier(value, value2), probability * probability2);
  901. });
  902. });
  903. return result;
  904. }
  905.  
  906. count() {
  907. return this.#map.size;
  908. }
  909.  
  910. average() {
  911. let result = 0;
  912. this.#map.forEach((probability, value) => {
  913. result += value * probability;
  914. });
  915. return result;
  916. }
  917.  
  918. sum() {
  919. let result = 0;
  920. this.#map.forEach(probability => {
  921. result += probability;
  922. });
  923. return result;
  924. }
  925.  
  926. min() {
  927. return Array.from(this.#map, ([k, v]) => k).reduce((a,b) => Math.min(a,b), Infinity);
  928. }
  929.  
  930. max() {
  931. return Array.from(this.#map, ([k, v]) => k).reduce((a,b) => Math.max(a,b), -Infinity);
  932. }
  933.  
  934. variance() {
  935. let result = 0;
  936. const average = this.average();
  937. this.#map.forEach((probability, value) => {
  938. const dist = average - value;
  939. result += dist * dist * probability;
  940. });
  941. return result;
  942. }
  943.  
  944. normalize() {
  945. const sum = this.sum();
  946. this.#map = new Map(Array.from(this.#map, ([k, v]) => [k, v / sum]));
  947. }
  948.  
  949. expectedRollsUntill(limit) {
  950. const x = (this.count() - 1) / 2.0;
  951. const y = x * (x + 1) * (2 * x + 1) / 6;
  952. const z = 2*y / this.variance();
  953. const average = this.average();
  954. const a = y + average * (average - 1) * z / 2;
  955. const b = z * average * average;
  956. return limit / average + a / b;
  957. }
  958.  
  959. clone() {
  960. const result = new Distribution();
  961. result.#map = new Map(this.#map);
  962. return result;
  963. }
  964.  
  965. getLeftTail(rolls, cutoff) {
  966. const mean = rolls * this.average();
  967. const variance = rolls * this.variance();
  968. const stdev = Math.sqrt(variance);
  969. return Distribution.cdf(cutoff, mean, stdev);
  970. }
  971.  
  972. getRightTail(rolls, cutoff) {
  973. return 1 - this.getLeftTail(rolls, cutoff);
  974. }
  975.  
  976. getRange(rolls, left, right) {
  977. return 1 - this.getLeftTail(rolls, left) - this.getRightTail(rolls, right);
  978. }
  979.  
  980. getMeanLeftTail(rolls, cutoff) {
  981. return this.getMeanRange(rolls, -Infinity, cutoff);
  982. }
  983.  
  984. getMeanRightTail(rolls, cutoff) {
  985. return this.getMeanRange(rolls, cutoff, Infinity);
  986. }
  987.  
  988. getMeanRange(rolls, left, right) {
  989. const mean = rolls * this.average();
  990. const variance = rolls * this.variance();
  991. const stdev = Math.sqrt(variance);
  992. const alpha = (left - mean) / stdev;
  993. const beta = (right - mean) / stdev;
  994. const c = Distribution.pdf(beta) - Distribution.pdf(alpha);
  995. const d = Distribution.cdf(beta, 0, 1) - Distribution.cdf(alpha, 0, 1);
  996. if(!c || !d) {
  997. return (left + right) / 2;
  998. }
  999. return mean - stdev * c / d;
  1000. }
  1001.  
  1002. toChart(other) {
  1003. if(other) {
  1004. const min = Math.min(this.min(), other.min());
  1005. const max = Math.max(this.max(), other.max());
  1006. for(let i=min;i<=max;i++) {
  1007. if(!this.#map.has(i)) {
  1008. this.#map.set(i, 0);
  1009. }
  1010. }
  1011. }
  1012. const result = Array.from(this.#map, ([k, v]) => ({x:k,y:v}));
  1013. result.sort((a,b) => a.x - b.x);
  1014. return result;
  1015. }
  1016.  
  1017. redistribute(value, exceptions) {
  1018. // redistributes this single value across all others, except the exceptions
  1019. const probability = this.#map.get(value);
  1020. if(!probability) {
  1021. return;
  1022. }
  1023. this.#map.delete(value);
  1024.  
  1025. let sum = 0;
  1026. this.#map.forEach((p, v) => {
  1027. if(!exceptions.includes(v)) {
  1028. sum += p;
  1029. }
  1030. });
  1031. this.#map.forEach((p, v) => {
  1032. if(!exceptions.includes(v)) {
  1033. this.#map.set(v, p + probability*p/sum);
  1034. }
  1035. });
  1036. }
  1037.  
  1038. };
  1039.  
  1040. Distribution.getRandomChance = function(probability) {
  1041. const result = new Distribution();
  1042. result.add(true, probability);
  1043. result.add(false, 1-probability);
  1044. return result;
  1045. };
  1046.  
  1047. // probability density function -> probability mass function
  1048. Distribution.getRandomOutcomeFloored = function(min, max) {
  1049. const result = new Distribution();
  1050. const rangeMult = 1 / (max - min);
  1051. for(let value=Math.floor(min); value<max; value++) {
  1052. let lower = value;
  1053. let upper = value + 1;
  1054. if(lower < min) {
  1055. lower = min;
  1056. }
  1057. if(upper > max) {
  1058. upper = max;
  1059. }
  1060. result.add(value, (upper - lower) * rangeMult);
  1061. }
  1062. return result;
  1063. };
  1064.  
  1065. Distribution.getRandomOutcomeRounded = function(min, max) {
  1066. return Distribution.getRandomOutcomeFloored(min + 0.5, max + 0.5);
  1067. }
  1068.  
  1069. // Cumulative Distribution Function
  1070. // https://stackoverflow.com/a/59217784
  1071. Distribution.cdf = function(value, mean, std) {
  1072. const z = (value - mean) / std;
  1073. const t = 1 / (1 + .2315419 * Math.abs(z));
  1074. const d =.3989423 * Math.exp( -z * z / 2);
  1075. let prob = d * t * (.3193815 + t * ( -.3565638 + t * (1.781478 + t * (-1.821256 + t * 1.330274))));
  1076. if(z > 0 ) {
  1077. prob = 1 - prob;
  1078. }
  1079. return prob
  1080. };
  1081.  
  1082. Distribution.pdf = function(zScore) {
  1083. return (Math.E ** (-zScore*zScore/2)) / Math.sqrt(2 * Math.PI);
  1084. };
  1085.  
  1086. return Distribution;
  1087.  
  1088. }
  1089. );
  1090. // elementCreator
  1091. window.moduleRegistry.add('elementCreator', () => {
  1092.  
  1093. const exports = {
  1094. addStyles
  1095. };
  1096.  
  1097. function addStyles(css) {
  1098. const head = document.getElementsByTagName('head')[0]
  1099. if(!head) {
  1100. console.error('Could not add styles, missing head');
  1101. return;
  1102. }
  1103. const style = document.createElement('style');
  1104. style.type = 'text/css';
  1105. style.innerHTML = css;
  1106. head.appendChild(style);
  1107. }
  1108.  
  1109. return exports;
  1110.  
  1111. }
  1112. );
  1113. // elementWatcher
  1114. window.moduleRegistry.add('elementWatcher', (Promise) => {
  1115.  
  1116. const exports = {
  1117. exists,
  1118. childAdded,
  1119. childAddedContinuous
  1120. }
  1121.  
  1122. const $ = window.$;
  1123.  
  1124. async function exists(selector, delay, timeout, inverted) {
  1125. delay = delay !== undefined ? delay : 10;
  1126. timeout = timeout !== undefined ? timeout : 5000;
  1127. const promiseWrapper = new Promise.Checking(() => {
  1128. let result = $(selector)[0];
  1129. return inverted ? !result : result;
  1130. }, delay, timeout);
  1131. return promiseWrapper;
  1132. }
  1133.  
  1134. async function childAdded(selector) {
  1135. const promiseWrapper = new Promise.Expiring(5000);
  1136.  
  1137. try {
  1138. const parent = await exists(selector);
  1139. const observer = new MutationObserver(function(mutations, observer) {
  1140. for(const mutation of mutations) {
  1141. if(mutation.addedNodes?.length) {
  1142. observer.disconnect();
  1143. promiseWrapper.resolve();
  1144. }
  1145. }
  1146. });
  1147. observer.observe(parent, { childList: true });
  1148. } catch(error) {
  1149. promiseWrapper.reject(error);
  1150. }
  1151.  
  1152. return promiseWrapper;
  1153. }
  1154.  
  1155. async function childAddedContinuous(selector, callback) {
  1156. const parent = await exists(selector);
  1157. const observer = new MutationObserver(function(mutations, observer) {
  1158. for(const mutation of mutations) {
  1159. if(mutation.addedNodes?.length) {
  1160. callback();
  1161. }
  1162. }
  1163. });
  1164. observer.observe(parent, { childList: true });
  1165. }
  1166.  
  1167. return exports;
  1168.  
  1169. }
  1170. );
  1171. // events
  1172. window.moduleRegistry.add('events', () => {
  1173.  
  1174. const exports = {
  1175. register,
  1176. emit,
  1177. getLast
  1178. };
  1179.  
  1180. const handlers = {};
  1181. const lastCache = {};
  1182.  
  1183. function register(name, handler) {
  1184. if(!handlers[name]) {
  1185. handlers[name] = [];
  1186. }
  1187. handlers[name].push(handler);
  1188. if(lastCache[name]) {
  1189. handle(handler, lastCache[name]);
  1190. }
  1191. }
  1192.  
  1193. // options = { skipCache }
  1194. function emit(name, data, options) {
  1195. if(!options?.skipCache) {
  1196. lastCache[name] = data;
  1197. }
  1198. if(!handlers[name]) {
  1199. return;
  1200. }
  1201. for(const handler of handlers[name]) {
  1202. handle(handler, data);
  1203. }
  1204. }
  1205.  
  1206. function handle(handler, data) {
  1207. try {
  1208. handler(data);
  1209. } catch(e) {
  1210. console.error('Something went wrong', e);
  1211. }
  1212. }
  1213.  
  1214. function getLast(name) {
  1215. return lastCache[name];
  1216. }
  1217.  
  1218. return exports;
  1219.  
  1220. }
  1221. );
  1222. // interceptor
  1223. window.moduleRegistry.add('interceptor', (events) => {
  1224.  
  1225. function initialise() {
  1226. registerInterceptorUrlChange();
  1227. events.emit('url', window.location.href);
  1228. }
  1229.  
  1230. function registerInterceptorUrlChange() {
  1231. const pushState = history.pushState;
  1232. history.pushState = function() {
  1233. pushState.apply(history, arguments);
  1234. console.debug(`Detected page ${arguments[2]}`);
  1235. events.emit('url', arguments[2]);
  1236. };
  1237. const replaceState = history.replaceState;
  1238. history.replaceState = function() {
  1239. replaceState.apply(history, arguments);
  1240. console.debug(`Detected page ${arguments[2]}`);
  1241. events.emit('url', arguments[2]);
  1242. }
  1243. }
  1244.  
  1245. initialise();
  1246.  
  1247. }
  1248. );
  1249. // itemUtil
  1250. window.moduleRegistry.add('itemUtil', (util, itemCache) => {
  1251.  
  1252. const exports = {
  1253. extractItem
  1254. };
  1255.  
  1256. function extractItem(element, target, ignoreMissing) {
  1257. element = $(element);
  1258. const name = element.find('.name').text();
  1259. let item = itemCache.byName[name];
  1260. if(!item) {
  1261. const src = element.find('img').attr('src');
  1262. if(src) {
  1263. const image = src.split('/').at(-1);
  1264. item = itemCache.byImage[image];
  1265. }
  1266. }
  1267. if(!item) {
  1268. if(!ignoreMissing) {
  1269. console.warn(`Could not find item with name [${name}]`);
  1270. }
  1271. return false;
  1272. }
  1273. let amount = 1;
  1274. let amountElements = element.find('.amount, .value');
  1275. if(amountElements.length) {
  1276. amount = amountElements.text();
  1277. if(!amount) {
  1278. return false;
  1279. }
  1280. if(amount.includes(' / ')) {
  1281. amount = amount.split(' / ')[0];
  1282. }
  1283. amount = util.parseNumber(amount);
  1284. }
  1285. let uses = element.find('.uses, .use').text();
  1286. if(uses && !uses.endsWith('HP')) {
  1287. amount += util.parseNumber(uses);
  1288. }
  1289. target[item.id] = (target[item.id] || 0) + amount;
  1290. return item;
  1291. }
  1292.  
  1293. return exports;
  1294.  
  1295. }
  1296. );
  1297. // localDatabase
  1298. window.moduleRegistry.add('localDatabase', (Promise) => {
  1299.  
  1300. const exports = {
  1301. getAllEntries,
  1302. saveEntry
  1303. }
  1304.  
  1305. const initialised = new Promise.Expiring(2000);
  1306. let database = null;
  1307.  
  1308. const databaseName = 'PancakeScripts';
  1309.  
  1310. function initialise() {
  1311. const request = window.indexedDB.open(databaseName, 2);
  1312. request.onsuccess = function(event) {
  1313. database = this.result;
  1314. initialised.resolve(exports);
  1315. };
  1316. request.onerror = function(event) {
  1317. console.error(`Failed creating IndexedDB : ${event.target.errorCode}`);
  1318. };
  1319. request.onupgradeneeded = function(event) {
  1320. const db = event.target.result;
  1321. if(event.oldVersion <= 0) {
  1322. console.debug('Creating IndexedDB');
  1323. const settingsStore = db.createObjectStore('settings', { keyPath: 'key' });
  1324. settingsStore.createIndex('key', 'key', { unique: true });
  1325. }
  1326. if(event.oldVersion <= 1) {
  1327. const syncTrackingStore = db.createObjectStore('sync-tracking', { keyPath: 'key' });
  1328. syncTrackingStore.createIndex('key', 'key', { unique: true });
  1329. }
  1330. };
  1331. }
  1332.  
  1333. async function getAllEntries(storeName) {
  1334. const result = new Promise.Expiring(1000);
  1335. const entries = [];
  1336. const store = database.transaction(storeName, 'readonly').objectStore(storeName);
  1337. const request = store.openCursor();
  1338. request.onsuccess = function(event) {
  1339. const cursor = event.target.result;
  1340. if(cursor) {
  1341. entries.push(cursor.value);
  1342. cursor.continue();
  1343. } else {
  1344. result.resolve(entries);
  1345. }
  1346. };
  1347. request.onerror = function(event) {
  1348. result.reject(event.error);
  1349. };
  1350. return result;
  1351. }
  1352.  
  1353. async function saveEntry(storeName, entry) {
  1354. const result = new Promise.Expiring(1000);
  1355. const store = database.transaction(storeName, 'readwrite').objectStore(storeName);
  1356. const request = store.put(entry);
  1357. request.onsuccess = function(event) {
  1358. result.resolve();
  1359. };
  1360. request.onerror = function(event) {
  1361. result.reject(event.error);
  1362. };
  1363. return result;
  1364. }
  1365.  
  1366. initialise();
  1367.  
  1368. return initialised;
  1369.  
  1370. }
  1371. );
  1372. // pageDetector
  1373. window.moduleRegistry.add('pageDetector', (events) => {
  1374.  
  1375. const registerUrlHandler = events.register.bind(null, 'url');
  1376. const emitEvent = events.emit.bind(null, 'page');
  1377.  
  1378. async function initialise() {
  1379. registerUrlHandler(handleUrl);
  1380. }
  1381.  
  1382. function handleUrl(url) {
  1383. let result = null;
  1384. const parts = url.split('/');
  1385. if(url.includes('/skill/') && url.includes('/action/')) {
  1386. result = {
  1387. type: 'action',
  1388. skill: +parts[parts.length-3],
  1389. action: +parts[parts.length-1]
  1390. };
  1391. } else if(url.includes('house/build')) {
  1392. result = {
  1393. type: 'structure',
  1394. structure: +parts[parts.length-1]
  1395. };
  1396. } else if(url.includes('house/enhance')) {
  1397. result = {
  1398. type: 'enhancement',
  1399. structure: +parts[parts.length-1]
  1400. };
  1401. } else if(url.includes('house/produce')) {
  1402. result = {
  1403. type: 'automation',
  1404. structure: +parts[parts.length-2],
  1405. action: +parts[parts.length-1]
  1406. };
  1407. } else {
  1408. result = {
  1409. type: parts.pop()
  1410. };
  1411. }
  1412. emitEvent(result);
  1413. }
  1414.  
  1415. initialise();
  1416.  
  1417. }
  1418. );
  1419. // pages
  1420. window.moduleRegistry.add('pages', (elementWatcher, events, colorMapper, util, skillCache, elementCreator) => {
  1421.  
  1422. const registerPageHandler = events.register.bind(null, 'page');
  1423. const getLastPage = events.getLast.bind(null, 'page');
  1424.  
  1425. const exports = {
  1426. register,
  1427. requestRender,
  1428. show,
  1429. hide,
  1430. open: visitPage
  1431. }
  1432.  
  1433. const pages = [];
  1434.  
  1435. function initialise() {
  1436. registerPageHandler(handlePage);
  1437. elementCreator.addStyles(styles);
  1438. }
  1439.  
  1440. function handlePage(page) {
  1441. // handle navigating away
  1442. if(!pages.some(p => p.path === page.type)) {
  1443. $('custom-page').remove();
  1444. $('nav-component > div.nav > div.scroll > button')
  1445. .removeClass('customActiveLink');
  1446. $('header-component div.wrapper > div.image > img')
  1447. .css('image-rendering', '');
  1448. headerPageNameChangeBugFix(page);
  1449. }
  1450. }
  1451.  
  1452. async function register(page) {
  1453. if(pages.some(p => p.name === page.name)) {
  1454. console.error(`Custom page already registered : ${page.name}`);
  1455. return;
  1456. }
  1457. page.path = page.name.toLowerCase().replaceAll(' ', '-');
  1458. page.class = `customMenuButton_${page.path}`;
  1459. page.image = page.image || 'https://ironwoodrpg.com/assets/misc/settings.png';
  1460. page.category = page.category?.toUpperCase() || 'MISC';
  1461. page.columns = page.columns || 1;
  1462. pages.push(page);
  1463. console.debug('Registered pages', pages);
  1464. await setupNavigation(page);
  1465. }
  1466.  
  1467. function show(name) {
  1468. const page = pages.find(p => p.name === name)
  1469. if(!page) {
  1470. console.error(`Could not find page : ${name}`);
  1471. return;
  1472. }
  1473. $(`.${page.class}`).show();
  1474. }
  1475.  
  1476. function hide(name) {
  1477. const page = pages.find(p => p.name === name)
  1478. if(!page) {
  1479. console.error(`Could not find page : ${name}`);
  1480. return;
  1481. }
  1482. $(`.${page.class}`).hide();
  1483. }
  1484.  
  1485. function requestRender(name) {
  1486. const page = pages.find(p => p.name === name)
  1487. if(!page) {
  1488. console.error(`Could not find page : ${name}`);
  1489. return;
  1490. }
  1491. if(getLastPage()?.type === page.path) {
  1492. render(page);
  1493. }
  1494. }
  1495.  
  1496. function render(page) {
  1497. $('.customComponent').remove();
  1498. page.render();
  1499. }
  1500.  
  1501. async function setupNavigation(page) {
  1502. await elementWatcher.exists('div.nav > div.scroll');
  1503. // MENU HEADER / CATEGORY
  1504. let menuHeader = $(`nav-component > div.nav > div.scroll > div.header:contains('${page.category}'), div.customMenuHeader:contains('${page.category}')`);
  1505. if(!menuHeader.length) {
  1506. menuHeader = createMenuHeader(page.category);
  1507. }
  1508. // MENU BUTTON / PAGE LINK
  1509. const menuButton = createMenuButton(page)
  1510. // POSITIONING
  1511. if(page.after) {
  1512. $(`nav-component button:contains('${page.after}')`).after(menuButton);
  1513. } else {
  1514. menuHeader.after(menuButton);
  1515. }
  1516. }
  1517.  
  1518. function createMenuHeader(text) {
  1519. const menuHeader =
  1520. $('<div/>')
  1521. .addClass('header customMenuHeader')
  1522. .append(
  1523. $('<div/>')
  1524. .addClass('customMenuHeaderText')
  1525. .text(text)
  1526. );
  1527. $('nav-component > div.nav > div.scroll')
  1528. .prepend(menuHeader);
  1529. return menuHeader;
  1530. }
  1531.  
  1532. function createMenuButton(page) {
  1533. const menuButton =
  1534. $('<button/>')
  1535. .attr('type', 'button')
  1536. .addClass(`customMenuButton ${page.class}`)
  1537. .css('display', 'none')
  1538. .click(() => visitPage(page.name))
  1539. .append(
  1540. $('<img/>')
  1541. .addClass('customMenuButtonImage')
  1542. .attr('src', page.image)
  1543. .css('image-rendering', page.imagePixelated ? 'pixelated' : 'auto')
  1544. )
  1545. .append(
  1546. $('<div/>')
  1547. .addClass('customMenuButtonText')
  1548. .text(page.name)
  1549. );
  1550. return menuButton;
  1551. }
  1552.  
  1553. async function visitPage(name) {
  1554. const page = pages.find(p => p.name === name);
  1555. if($('custom-page').length) {
  1556. $('custom-page').remove();
  1557. } else {
  1558. await setupEmptyPage();
  1559. }
  1560. createPage(page.columns);
  1561. updatePageHeader(page);
  1562. updateActivePageInNav(page.name);
  1563. history.pushState({}, '', page.path);
  1564. page.render();
  1565. }
  1566.  
  1567. async function setupEmptyPage() {
  1568. util.goToPage('settings');
  1569. await elementWatcher.exists('settings-page');
  1570. $('settings-page').remove();
  1571. }
  1572.  
  1573. function createPage(columnCount) {
  1574. const custompage = $('<custom-page/>');
  1575. const columns = $('<div/>')
  1576. .addClass('customGroups');
  1577. for(let columnIndex = 0; columnIndex < columnCount; columnIndex++) {
  1578. columns.append(
  1579. $('<div/>')
  1580. .addClass('customGroup')
  1581. .addClass(`column${columnIndex}`)
  1582. )
  1583. };
  1584. custompage.append(columns);
  1585. $('div.padding > div.wrapper > router-outlet').after(custompage);
  1586. }
  1587.  
  1588. function updatePageHeader(page) {
  1589. $('header-component div.wrapper > div.image > img')
  1590. .attr('src', page.image)
  1591. .css('image-rendering', page.imagePixelated ? 'pixelated' : 'auto');
  1592. $('header-component div.wrapper > div.title').text(page.name);
  1593. }
  1594.  
  1595. function updateActivePageInNav(name) {
  1596. //Set other pages as inactive
  1597. $(`nav-component > div.nav > div.scroll > button`)
  1598. .removeClass('active-link')
  1599. .removeClass('customActiveLink');
  1600. //Set this page as active
  1601. $(`nav-component > div.nav > div.scroll > button > div.customMenuButtonText:contains('${name}')`)
  1602. .parent()
  1603. .addClass('customActiveLink');
  1604. }
  1605.  
  1606. // hacky shit, idk why angular stops updating page header title ???
  1607. async function headerPageNameChangeBugFix(page) {
  1608. await elementWatcher.exists('nav-component > div.nav');
  1609. let headerName = null;
  1610. if(page.type === 'action') {
  1611. headerName = skillCache.byId[page.skill].name;
  1612. } else if(page.type === 'structure') {
  1613. headerName = 'House';
  1614. } else if(page.type === 'enhancement') {
  1615. headerName = 'House';
  1616. } else if(page.type === 'automation') {
  1617. headerName = 'House';
  1618. } else {
  1619. headerName = page.type;
  1620. headerName = headerName.charAt(0).toUpperCase() + headerName.slice(1);
  1621. }
  1622. $('header-component div.wrapper > div.title').text(headerName);
  1623. }
  1624.  
  1625. const styles = `
  1626. :root {
  1627. --background-color: ${colorMapper('componentRegular')};
  1628. --border-color: ${colorMapper('componentLight')};
  1629. --darker-color: ${colorMapper('componentDark')};
  1630. }
  1631. .customMenuHeader {
  1632. height: 56px;
  1633. display: flex;
  1634. align-items: center;
  1635. padding: 0 24px;
  1636. color: #aaa;
  1637. font-size: .875rem;
  1638. font-weight: 600;
  1639. letter-spacing: 1px;
  1640. text-transform: uppercase;
  1641. border-bottom: 1px solid var(--border-color);
  1642. background-color: var(--background-color);
  1643. }
  1644. .customMenuHeaderText {
  1645. flex: 1;
  1646. }
  1647. .customMenuButton {
  1648. border: none;
  1649. background: transparent;
  1650. font-family: inherit;
  1651. font-size: inherit;
  1652. line-height: 1.5;
  1653. font-weight: inherit;
  1654. color: inherit;
  1655. resize: none;
  1656. text-transform: inherit;
  1657. letter-spacing: inherit;
  1658. cursor: pointer;
  1659. height: 56px;
  1660. display: flex;
  1661. align-items: center;
  1662. padding: 0 24px;
  1663. border-bottom: 1px solid var(--border-color);
  1664. width: 100%;
  1665. text-align: left;
  1666. position: relative;
  1667. background-color: var(--background-color);
  1668. }
  1669. .customMenuButtonImage {
  1670. max-width: 100%;
  1671. max-height: 100%;
  1672. height: 32px;
  1673. width: 32px;
  1674. }
  1675. .customMenuButtonText {
  1676. margin-left: var(--margin);
  1677. flex: 1;
  1678. }
  1679. .customGroups {
  1680. display: flex;
  1681. gap: var(--gap);
  1682. flex-wrap: wrap;
  1683. }
  1684. .customGroup {
  1685. flex: 1;
  1686. min-width: 360px;
  1687. }
  1688. .customActiveLink {
  1689. background-color: var(--darker-color);
  1690. }
  1691. `;
  1692.  
  1693. initialise();
  1694.  
  1695. return exports
  1696. }
  1697. );
  1698. // Promise
  1699. window.moduleRegistry.add('Promise', () => {
  1700.  
  1701. class Deferred {
  1702. #promise;
  1703. resolve;
  1704. reject;
  1705. constructor() {
  1706. this.#promise = new Promise((resolve, reject) => {
  1707. this.resolve = resolve;
  1708. this.reject = reject;
  1709. }).catch(error => {
  1710. if(error) {
  1711. console.warn(error);
  1712. }
  1713. throw error;
  1714. });
  1715. }
  1716.  
  1717. then() {
  1718. this.#promise.then.apply(this.#promise, arguments);
  1719. return this;
  1720. }
  1721.  
  1722. catch() {
  1723. this.#promise.catch.apply(this.#promise, arguments);
  1724. return this;
  1725. }
  1726.  
  1727. finally() {
  1728. this.#promise.finally.apply(this.#promise, arguments);
  1729. return this;
  1730. }
  1731. }
  1732.  
  1733. class Delayed extends Deferred {
  1734. constructor(timeout) {
  1735. super();
  1736. const timeoutReference = window.setTimeout(() => {
  1737. this.resolve();
  1738. }, timeout);
  1739. this.finally(() => {
  1740. window.clearTimeout(timeoutReference)
  1741. });
  1742. }
  1743. }
  1744.  
  1745. class Expiring extends Deferred {
  1746. constructor(timeout) {
  1747. super();
  1748. if(timeout <= 0) {
  1749. return;
  1750. }
  1751. const timeoutReference = window.setTimeout(() => {
  1752. this.reject(`Timed out after ${timeout} ms`);
  1753. }, timeout);
  1754. this.finally(() => {
  1755. window.clearTimeout(timeoutReference)
  1756. });
  1757. }
  1758. }
  1759.  
  1760. class Checking extends Expiring {
  1761. #checker;
  1762. constructor(checker, interval, timeout) {
  1763. super(timeout);
  1764. this.#checker = checker;
  1765. this.#check();
  1766. const intervalReference = window.setInterval(this.#check.bind(this), interval);
  1767. this.finally(() => {
  1768. window.clearInterval(intervalReference)
  1769. });
  1770. }
  1771. #check() {
  1772. const checkResult = this.#checker();
  1773. if(!checkResult) {
  1774. return;
  1775. }
  1776. this.resolve(checkResult);
  1777. }
  1778. }
  1779.  
  1780. return {
  1781. Deferred,
  1782. Delayed,
  1783. Expiring,
  1784. Checking
  1785. };
  1786.  
  1787. }
  1788. );
  1789. // request
  1790. window.moduleRegistry.add('request', () => {
  1791.  
  1792. async function request(url, body, headers) {
  1793. if(!headers) {
  1794. headers = {};
  1795. }
  1796. headers['Content-Type'] = 'application/json';
  1797. const method = body ? 'POST' : 'GET';
  1798. try {
  1799. if(body) {
  1800. body = JSON.stringify(body);
  1801. }
  1802. const fetchResponse = await fetch(`${window.PANCAKE_ROOT}/${url}`, {method, headers, body});
  1803. if(fetchResponse.status !== 200) {
  1804. console.error(await fetchResponse.text());
  1805. return;
  1806. }
  1807. try {
  1808. const contentType = fetchResponse.headers.get('Content-Type');
  1809. if(contentType.startsWith('text/plain')) {
  1810. return await fetchResponse.text();
  1811. } else if(contentType.startsWith('application/json')) {
  1812. return await fetchResponse.json();
  1813. } else {
  1814. console.error(`Unknown content type : ${contentType}`);
  1815. }
  1816. } catch(e) {
  1817. if(body) {
  1818. return 'OK';
  1819. }
  1820. }
  1821. } catch(e) {
  1822. console.error(e);
  1823. }
  1824. }
  1825.  
  1826. // alphabetical
  1827.  
  1828. request.listActions = () => request('public/list/action');
  1829. request.listDrops = () => request('public/list/drop');
  1830. request.listItems = () => request('public/list/item');
  1831. request.listItemAttributes = () => request('public/list/itemAttribute');
  1832. request.listIngredients = () => request('public/list/ingredient');
  1833. request.listMonsters = () => request('public/list/monster');
  1834. request.listRecipes = () => request('public/list/recipe');
  1835. request.listSkills = () => request('public/list/skill');
  1836. request.listStructures = () => request('public/list/structure');
  1837.  
  1838. request.getMarketConversion = () => request('public/market/conversions');
  1839.  
  1840. request.getChangelogs = () => request('public/settings/changelog');
  1841. request.getVersion = () => request('public/settings/version');
  1842.  
  1843. return request;
  1844.  
  1845. }
  1846. );
  1847. // toast
  1848. window.moduleRegistry.add('toast', (util, elementCreator) => {
  1849.  
  1850. const exports = {
  1851. create
  1852. };
  1853.  
  1854. function initialise() {
  1855. elementCreator.addStyles(styles);
  1856. }
  1857.  
  1858. // text, time, image
  1859. async function create(config) {
  1860. config.time ||= 2000;
  1861. config.image ||= 'https://ironwoodrpg.com/assets/misc/quests.png';
  1862. const notificationId = `customNotification_${Math.floor(Date.now() * Math.random())}`
  1863. const notificationDiv =
  1864. $('<div/>')
  1865. .addClass('customNotification')
  1866. .attr('id', notificationId)
  1867. .append(
  1868. $('<div/>')
  1869. .addClass('customNotificationImageDiv')
  1870. .append(
  1871. $('<img/>')
  1872. .addClass('customNotificationImage')
  1873. .attr('src', config.image)
  1874. )
  1875. )
  1876. .append(
  1877. $('<div/>')
  1878. .addClass('customNotificationDetails')
  1879. .html(config.text)
  1880. );
  1881. $('div.notifications').append(notificationDiv);
  1882. await util.sleep(config.time);
  1883. $(`#${notificationId}`).fadeOut('slow', () => {
  1884. $(`#${notificationId}`).remove();
  1885. });
  1886. }
  1887.  
  1888. const styles = `
  1889. .customNotification {
  1890. padding: 8px 16px 8px 12px;
  1891. border-radius: 4px;
  1892. backdrop-filter: blur(8px);
  1893. background: rgba(255,255,255,.15);
  1894. box-shadow: 0 8px 16px -4px #00000080;
  1895. display: flex;
  1896. align-items: center;
  1897. min-height: 48px;
  1898. margin-top: 12px;
  1899. pointer-events: all;
  1900. }
  1901. .customNotificationImageDiv {
  1902. display: flex;
  1903. align-items: center;
  1904. justify-content: center;
  1905. width: 32px;
  1906. height: 32px;
  1907. }
  1908. .customNotificationImage {
  1909. filter: drop-shadow(0px 8px 4px rgba(0,0,0,.1));
  1910. image-rendering: auto;
  1911. }
  1912. .customNotificationDetails {
  1913. margin-left: 8px;
  1914. text-align: center;
  1915. }
  1916. `;
  1917.  
  1918. initialise();
  1919.  
  1920. return exports;
  1921. }
  1922. );
  1923. // util
  1924. window.moduleRegistry.add('util', () => {
  1925.  
  1926. const exports = {
  1927. levelToExp,
  1928. expToLevel,
  1929. expToVirtualLevel,
  1930. expToCurrentExp,
  1931. expToNextLevel,
  1932. expToNextTier,
  1933. formatNumber,
  1934. parseNumber,
  1935. secondsToDuration,
  1936. parseDuration,
  1937. divmod,
  1938. sleep,
  1939. goToPage,
  1940. compareObjects,
  1941. debounce
  1942. };
  1943.  
  1944. function levelToExp(level) {
  1945. if(level === 1) {
  1946. return 0;
  1947. }
  1948. return Math.floor(Math.pow(level, 3.5) * 6 / 5);
  1949. }
  1950.  
  1951. function expToLevel(exp) {
  1952. return Math.min(100, expToVirtualLevel(exp));
  1953. }
  1954.  
  1955. function expToVirtualLevel(exp) {
  1956. let level = Math.pow((exp + 1) * 5 / 6, 1 / 3.5);
  1957. level = Math.floor(level);
  1958. level = Math.max(1, level);
  1959. return level;
  1960. }
  1961.  
  1962. function expToCurrentExp(exp) {
  1963. const level = expToLevel(exp);
  1964. return exp - levelToExp(level);
  1965. }
  1966.  
  1967. function expToNextLevel(exp) {
  1968. const level = expToLevel(exp);
  1969. return levelToExp(level + 1) - exp;
  1970. }
  1971.  
  1972. function expToNextTier(exp) {
  1973. const level = expToLevel(exp);
  1974. let target = 10;
  1975. while(target <= level) {
  1976. target += 15;
  1977. }
  1978. return levelToExp(target) - exp;
  1979. }
  1980.  
  1981. function formatNumber(number) {
  1982. return number.toLocaleString(undefined, {maximumFractionDigits:2});
  1983. }
  1984.  
  1985. function parseNumber(text) {
  1986. if(!text) {
  1987. return 0;
  1988. }
  1989. const regexMatch = /\d+.*/.exec(text);
  1990. if(!regexMatch) {
  1991. return 0;
  1992. }
  1993. text = regexMatch[0];
  1994. text = text.replaceAll(/,/g, '');
  1995. let multiplier = 1;
  1996. if(text.endsWith('%')) {
  1997. multiplier = 1 / 100;
  1998. }
  1999. if(text.endsWith('K')) {
  2000. multiplier = 1_000;
  2001. }
  2002. if(text.endsWith('M')) {
  2003. multiplier = 1_000_000;
  2004. }
  2005. return (parseFloat(text) || 0) * multiplier;
  2006. }
  2007.  
  2008. function secondsToDuration(seconds) {
  2009. seconds = Math.floor(seconds);
  2010. if(seconds > 60 * 60 * 24 * 100) {
  2011. // > 100 days
  2012. return 'A very long time';
  2013. }
  2014.  
  2015. var [minutes, seconds] = divmod(seconds, 60);
  2016. var [hours, minutes] = divmod(minutes, 60);
  2017. var [days, hours] = divmod(hours, 24);
  2018.  
  2019. seconds = `${seconds}`.padStart(2, '0');
  2020. minutes = `${minutes}`.padStart(2, '0');
  2021. hours = `${hours}`.padStart(2, '0');
  2022. days = `${days}`.padStart(2, '0');
  2023.  
  2024. let result = '';
  2025. if(result || +days) {
  2026. result += `${days}d `;
  2027. }
  2028. if(result || +hours) {
  2029. result += `${hours}h `;
  2030. }
  2031. if(result || +minutes) {
  2032. result += `${minutes}m `;
  2033. }
  2034. result += `${seconds}s`;
  2035.  
  2036. return result;
  2037. }
  2038.  
  2039. function parseDuration(duration) {
  2040. const parts = duration.split(' ');
  2041. let seconds = 0;
  2042. for(const part of parts) {
  2043. const value = parseFloat(part);
  2044. if(part.endsWith('m')) {
  2045. seconds += value * 60;
  2046. } else if(part.endsWith('h')) {
  2047. seconds += value * 60 * 60;
  2048. } else if(part.endsWith('d')) {
  2049. seconds += value * 60 * 60 * 24;
  2050. } else {
  2051. console.warn(`Unexpected duration being parsed : ${part}`);
  2052. }
  2053. }
  2054. return seconds;
  2055. }
  2056.  
  2057. function divmod(x, y) {
  2058. return [Math.floor(x / y), x % y];
  2059. }
  2060.  
  2061. function goToPage(page) {
  2062. window.history.pushState({}, '', page);
  2063. window.history.pushState({}, '', page);
  2064. window.history.back();
  2065. }
  2066.  
  2067. async function sleep(millis) {
  2068. await new Promise(r => window.setTimeout(r, millis));
  2069. }
  2070.  
  2071. function compareObjects(object1, object2) {
  2072. const keys1 = Object.keys(object1);
  2073. const keys2 = Object.keys(object2);
  2074. if(keys1.length !== keys2.length) {
  2075. return false;
  2076. }
  2077. keys1.sort();
  2078. keys2.sort();
  2079. for(let i=0;i<keys1.length;i++) {
  2080. if(keys1[i] !== keys2[i]) {
  2081. return false;
  2082. }
  2083. if(object1[keys1[i]] !== object2[keys2[i]]) {
  2084. return false;
  2085. }
  2086. }
  2087. return true;
  2088. }
  2089.  
  2090. function debounce(callback, delay) {
  2091. let timer;
  2092. return function() {
  2093. clearTimeout(timer);
  2094. timer = setTimeout(() => {
  2095. callback();
  2096. }, delay);
  2097. }
  2098. }
  2099.  
  2100. return exports;
  2101.  
  2102. }
  2103. );
  2104. // enhancementsReader
  2105. window.moduleRegistry.add('enhancementsReader', (events, util) => {
  2106.  
  2107. const emitEvent = events.emit.bind(null, 'reader-enhancements');
  2108.  
  2109. let currentPage;
  2110.  
  2111. function initialise() {
  2112. events.register('page', handlePage);
  2113. window.setInterval(update, 1000);
  2114. }
  2115.  
  2116. function handlePage(page) {
  2117. currentPage = page;
  2118. update();
  2119. }
  2120.  
  2121. function update() {
  2122. if(!currentPage) {
  2123. return;
  2124. }
  2125. if(currentPage.type === 'enhancement' && $('home-page .categories .category-active').text() === 'Enhance') {
  2126. readEnhancementsScreen();
  2127. }
  2128. }
  2129.  
  2130. function readEnhancementsScreen() {
  2131. const enhancements = {};
  2132. $('home-page .categories + .card button').each((i,element) => {
  2133. element = $(element);
  2134. const name = element.find('.name').text();
  2135. const level = util.parseNumber(element.find('.level').text());
  2136. enhancements[name] = level;
  2137. });
  2138. emitEvent({
  2139. type: 'full',
  2140. value: enhancements
  2141. });
  2142. }
  2143.  
  2144. initialise();
  2145.  
  2146. }
  2147. );
  2148. // equipmentReader
  2149. window.moduleRegistry.add('equipmentReader', (events, itemCache, util, itemUtil) => {
  2150.  
  2151. let currentPage;
  2152.  
  2153. function initialise() {
  2154. events.register('page', handlePage);
  2155. window.setInterval(update, 1000);
  2156. }
  2157.  
  2158. function handlePage(page) {
  2159. currentPage = page;
  2160. update();
  2161. }
  2162.  
  2163. function update() {
  2164. if(!currentPage) {
  2165. return;
  2166. }
  2167. if(currentPage.type === 'equipment') {
  2168. readEquipmentScreen();
  2169. }
  2170. if(currentPage.type === 'action') {
  2171. readActionScreen();
  2172. }
  2173. }
  2174.  
  2175. function readEquipmentScreen() {
  2176. const equipment = {};
  2177. const activeTab = $('equipment-page .categories button[disabled]').text().toLowerCase();
  2178. $('equipment-page .header + .items > .item > .description').parent().each((i,element) => {
  2179. itemUtil.extractItem(element, equipment);
  2180. });
  2181. events.emit(`reader-equipment-${activeTab}`, {
  2182. type: 'full',
  2183. value: equipment
  2184. });
  2185. }
  2186.  
  2187. function readActionScreen() {
  2188. const equipment = {};
  2189. $('skill-page .header > .name:contains("Consumables")').closest('.card').find('button > .name:not(.placeholder)').parent().each((i,element) => {
  2190. itemUtil.extractItem(element, equipment);
  2191. });
  2192. events.emit('reader-equipment-equipment', {
  2193. type: 'partial',
  2194. value: equipment
  2195. });
  2196. }
  2197.  
  2198. initialise();
  2199.  
  2200. }
  2201. );
  2202. // expReader
  2203. window.moduleRegistry.add('expReader', (events, skillCache, util) => {
  2204.  
  2205. const emitEvent = events.emit.bind(null, 'reader-exp');
  2206.  
  2207. let currentPage;
  2208.  
  2209. function initialise() {
  2210. events.register('page', handlePage);
  2211. window.setInterval(update, 1000);
  2212. }
  2213.  
  2214. function handlePage(page) {
  2215. currentPage = page;
  2216. update();
  2217. }
  2218.  
  2219. function update() {
  2220. if(!currentPage) {
  2221. return;
  2222. }
  2223. if(currentPage.type === 'action') {
  2224. readActionScreen(currentPage.skill);
  2225. }
  2226. readSidebar();
  2227. }
  2228.  
  2229. function readActionScreen(id) {
  2230. const text = $('skill-page .header > .name:contains("Stats")')
  2231. .closest('.card')
  2232. .find('.row > .name:contains("Total"):contains("XP")')
  2233. .closest('.row')
  2234. .find('.value')
  2235. .text();
  2236. const exp = util.parseNumber(text);
  2237. emitEvent([{ id, exp }]);
  2238. }
  2239.  
  2240. function readSidebar() {
  2241. const levels = [];
  2242. $('nav-component button.skill').each((i,element) => {
  2243. element = $(element);
  2244. const name = element.find('.name').text();
  2245. const id = skillCache.byName[name].id;
  2246. const level = +(/\d+/.exec(element.find('.level').text())?.[0]);
  2247. const exp = util.levelToExp(level);
  2248. levels.push({ id, exp });
  2249. });
  2250. emitEvent(levels);
  2251. }
  2252.  
  2253. initialise();
  2254.  
  2255. }
  2256. );
  2257. // guildStructuresReader
  2258. window.moduleRegistry.add('guildStructuresReader', (events, util) => {
  2259.  
  2260. const emitEvent = events.emit.bind(null, 'reader-structures-guild');
  2261.  
  2262. let currentPage;
  2263.  
  2264. function initialise() {
  2265. events.register('page', handlePage);
  2266. window.setInterval(update, 1000);
  2267. }
  2268.  
  2269. function handlePage(page) {
  2270. currentPage = page;
  2271. update();
  2272. }
  2273.  
  2274. function update() {
  2275. if(!currentPage) {
  2276. return;
  2277. }
  2278. if(currentPage.type === 'guild' && $('guild-page .tracker + div button.row-active').text() === 'Buildings') {
  2279. readGuildStructuresScreen();
  2280. }
  2281. }
  2282.  
  2283. function readGuildStructuresScreen() {
  2284. const structures = {};
  2285. $('guild-page .card').first().find('button').each((i,element) => {
  2286. element = $(element);
  2287. const name = element.find('.name').text();
  2288. const level = util.parseNumber(element.find('.amount').text());
  2289. structures[name] = level;
  2290. });
  2291. emitEvent({
  2292. type: 'full',
  2293. value: structures
  2294. });
  2295. }
  2296.  
  2297. initialise();
  2298.  
  2299. }
  2300. );
  2301. // inventoryReader
  2302. window.moduleRegistry.add('inventoryReader', (events, itemCache, util, itemUtil) => {
  2303.  
  2304. const emitEvent = events.emit.bind(null, 'reader-inventory');
  2305.  
  2306. let currentPage;
  2307.  
  2308. function initialise() {
  2309. events.register('page', handlePage);
  2310. window.setInterval(update, 1000);
  2311. }
  2312.  
  2313. function handlePage(page) {
  2314. currentPage = page;
  2315. update();
  2316. }
  2317.  
  2318. function update() {
  2319. if(!currentPage) {
  2320. return;
  2321. }
  2322. if(currentPage.type === 'inventory') {
  2323. readInventoryScreen();
  2324. }
  2325. if(currentPage.type === 'action') {
  2326. readActionScreen();
  2327. }
  2328. }
  2329.  
  2330. function readInventoryScreen() {
  2331. const inventory = {};
  2332. $('inventory-page .items > .item').each((i,element) => {
  2333. itemUtil.extractItem(element, inventory, true);
  2334. });
  2335. emitEvent({
  2336. type: 'full',
  2337. value: inventory
  2338. });
  2339. }
  2340.  
  2341. function readActionScreen() {
  2342. const inventory = {};
  2343. $('skill-page .header > .name:contains("Materials")').closest('.card').find('.row').each((i,element) => {
  2344. itemUtil.extractItem(element, inventory);
  2345. });
  2346. emitEvent({
  2347. type: 'partial',
  2348. value: inventory
  2349. });
  2350. }
  2351.  
  2352. initialise();
  2353.  
  2354. }
  2355. );
  2356. // structuresReader
  2357. window.moduleRegistry.add('structuresReader', (events, util) => {
  2358.  
  2359. const emitEvent = events.emit.bind(null, 'reader-structures');
  2360.  
  2361. let currentPage;
  2362.  
  2363. function initialise() {
  2364. events.register('page', handlePage);
  2365. window.setInterval(update, 1000);
  2366. }
  2367.  
  2368. function handlePage(page) {
  2369. currentPage = page;
  2370. update();
  2371. }
  2372.  
  2373. function update() {
  2374. if(!currentPage) {
  2375. return;
  2376. }
  2377. if(currentPage.type === 'structure' && $('home-page .categories .category-active').text() === 'Build') {
  2378. readStructuresScreen();
  2379. }
  2380. }
  2381.  
  2382. function readStructuresScreen() {
  2383. const structures = {};
  2384. $('home-page .categories + .card button').each((i,element) => {
  2385. element = $(element);
  2386. const name = element.find('.name').text();
  2387. const level = util.parseNumber(element.find('.level').text());
  2388. structures[name] = level;
  2389. });
  2390. emitEvent({
  2391. type: 'full',
  2392. value: structures
  2393. });
  2394. }
  2395.  
  2396. initialise();
  2397.  
  2398. }
  2399. );
  2400. // authToast
  2401. window.moduleRegistry.add('authToast', (toast) => {
  2402.  
  2403. function initialise() {
  2404. toast.create({
  2405. text: 'Pancake-Scripts initialised!',
  2406. image: 'https://img.icons8.com/?size=48&id=1ODJ62iG96gX&format=png'
  2407. });
  2408. }
  2409.  
  2410. initialise();
  2411.  
  2412. }
  2413. );
  2414. // changelog
  2415. window.moduleRegistry.add('changelog', (Promise, pages, components, request, util, configuration) => {
  2416.  
  2417. const PAGE_NAME = 'Plugin changelog';
  2418. const loaded = new Promise.Deferred();
  2419.  
  2420. let changelogs = null;
  2421.  
  2422. async function initialise() {
  2423. await pages.register({
  2424. category: 'Skills',
  2425. after: 'Changelog',
  2426. name: PAGE_NAME,
  2427. image: 'https://ironwoodrpg.com/assets/misc/changelog.png',
  2428. render: renderPage
  2429. });
  2430. configuration.registerCheckbox({
  2431. category: 'Pages',
  2432. key: 'changelog-enabled',
  2433. name: 'Changelog',
  2434. default: true,
  2435. handler: handleConfigStateChange
  2436. });
  2437. load();
  2438. }
  2439.  
  2440. function handleConfigStateChange(state, name) {
  2441. if(state) {
  2442. pages.show(PAGE_NAME);
  2443. } else {
  2444. pages.hide(PAGE_NAME);
  2445. }
  2446. }
  2447.  
  2448. async function load() {
  2449. changelogs = await request.getChangelogs();
  2450. loaded.resolve();
  2451. }
  2452.  
  2453. async function renderPage() {
  2454. await loaded;
  2455. const header = components.search(componentBlueprint, 'header');
  2456. const list = components.search(componentBlueprint, 'list');
  2457. for(const index in changelogs) {
  2458. componentBlueprint.componentId = `changelogComponent_${index}`;
  2459. header.title = changelogs[index].title;
  2460. header.textRight = new Date(changelogs[index].time).toLocaleDateString();
  2461. list.entries = changelogs[index].entries;
  2462. components.addComponent(componentBlueprint);
  2463. }
  2464. }
  2465.  
  2466. const componentBlueprint = {
  2467. componentId: 'changelogComponent',
  2468. dependsOn: 'custom-page',
  2469. parent: '.column0',
  2470. selectedTabIndex: 0,
  2471. tabs: [{
  2472. title: 'tab',
  2473. rows: [{
  2474. id: 'header',
  2475. type: 'header',
  2476. title: '',
  2477. textRight: ''
  2478. },{
  2479. id: 'list',
  2480. type: 'list',
  2481. entries: []
  2482. }]
  2483. }]
  2484. };
  2485.  
  2486. initialise();
  2487.  
  2488. }
  2489. );
  2490. // configurationPage
  2491. window.moduleRegistry.add('configurationPage', (pages, components, elementWatcher, configuration, elementCreator) => {
  2492.  
  2493. const PAGE_NAME = 'Configuration';
  2494.  
  2495. async function initialise() {
  2496. await pages.register({
  2497. category: 'Misc',
  2498. after: 'Settings',
  2499. name: PAGE_NAME,
  2500. image: 'https://cdn-icons-png.flaticon.com/512/3953/3953226.png',
  2501. columns: '2',
  2502. render: renderPage
  2503. });
  2504. elementCreator.addStyles(styles);
  2505. pages.show(PAGE_NAME);
  2506. }
  2507.  
  2508. function generateBlueprint() {
  2509. const categories = {};
  2510. for(const item of configuration.items) {
  2511. if(!categories[item.category]) {
  2512. categories[item.category] = {
  2513. name: item.category,
  2514. items: []
  2515. }
  2516. }
  2517. categories[item.category].items.push(item);
  2518. }
  2519. const blueprints = [];
  2520. let column = 1;
  2521. for(const category in categories) {
  2522. column = 1 - column;
  2523. const rows = [{
  2524. type: 'header',
  2525. title: category,
  2526. centered: true
  2527. }];
  2528. rows.push(...categories[category].items.flatMap(createRows));
  2529. blueprints.push({
  2530. componentId: `configurationComponent_${category}`,
  2531. dependsOn: 'custom-page',
  2532. parent: `.column${column}`,
  2533. selectedTabIndex: 0,
  2534. tabs: [{
  2535. rows: rows
  2536. }]
  2537. });
  2538. }
  2539. return blueprints;
  2540. }
  2541.  
  2542. function createRows(item) {
  2543. switch(item.type) {
  2544. case 'checkbox': return createRows_Checkbox(item);
  2545. case 'input': return createRows_Input(item);
  2546. case 'dropdown': return createRows_Dropdown(item);
  2547. case 'json': break;
  2548. default: throw `Unknown configuration type : ${item.type}`;
  2549. }
  2550. }
  2551.  
  2552. function createRows_Checkbox(item) {
  2553. return [{
  2554. type: 'checkbox',
  2555. text: item.name,
  2556. checked: item.value,
  2557. delay: 500,
  2558. action: (value) => {
  2559. item.handler(value);
  2560. pages.requestRender(PAGE_NAME);
  2561. }
  2562. }]
  2563. }
  2564.  
  2565. function createRows_Input(item) {
  2566. const value = item.value || item.default;
  2567. return [{
  2568. type: 'item',
  2569. name: item.name
  2570. },{
  2571. type: 'input',
  2572. name: item.name,
  2573. value: value,
  2574. inputType: item.inputType,
  2575. delay: 500,
  2576. action: (value) => {
  2577. item.handler(value);
  2578. }
  2579. }]
  2580. }
  2581.  
  2582. function createRows_Dropdown(item) {
  2583. const value = item.value || item.default;
  2584. const options = item.options.map(option => ({
  2585. text: option,
  2586. value: option,
  2587. selected: option === value
  2588. }));
  2589. return [{
  2590. type: 'item',
  2591. name: item.name
  2592. },{
  2593. type: 'dropdown',
  2594. options: options,
  2595. delay: 500,
  2596. action: (value) => {
  2597. item.handler(value);
  2598. }
  2599. }]
  2600. }
  2601.  
  2602. function renderPage() {
  2603. const blueprints = generateBlueprint();
  2604. for(const blueprint of blueprints) {
  2605. components.addComponent(blueprint);
  2606. }
  2607. }
  2608.  
  2609. const styles = `
  2610. .modifiedHeight {
  2611. height: 28px;
  2612. }
  2613. `;
  2614.  
  2615. initialise();
  2616. }
  2617. );
  2618. // estimator
  2619. window.moduleRegistry.add('estimator', (configuration, events, skillCache, actionCache, itemCache, estimatorActivity, estimatorCombat, estimatorOutskirts, components, util, statsStore) => {
  2620.  
  2621. let enabled = false;
  2622.  
  2623. function initialise() {
  2624. configuration.registerCheckbox({
  2625. category: 'Other',
  2626. key: 'estimations',
  2627. name: 'Estimations',
  2628. default: true,
  2629. handler: handleConfigStateChange
  2630. });
  2631. events.register('page', update);
  2632. events.register('state-stats', update);
  2633. }
  2634.  
  2635. function handleConfigStateChange(state) {
  2636. enabled = state;
  2637. }
  2638.  
  2639. function update() {
  2640. if(!enabled) {
  2641. return;
  2642. }
  2643. const page = events.getLast('page');
  2644. const stats = events.getLast('state-stats');
  2645. if(!page || !stats || page.type !== 'action') {
  2646. return;
  2647. }
  2648. const skill = skillCache.byId[page.skill];
  2649. const action = actionCache.byId[page.action];
  2650. let estimation;
  2651. if(action.type === 'OUTSKIRTS') {
  2652. estimation = estimatorOutskirts.get(page.skill, page.action);
  2653. } else if(skill.type === 'Gathering' || skill.type === 'Crafting') {
  2654. estimation = estimatorActivity.get(page.skill, page.action);
  2655. } else if(skill.type === 'Combat') {
  2656. estimation = estimatorCombat.get(page.skill, page.action);
  2657. }
  2658. if(estimation) {
  2659. enrichTimings(estimation);
  2660. enrichValues(estimation);
  2661. render(estimation);
  2662. }
  2663. }
  2664.  
  2665. function enrichTimings(estimation) {
  2666. const inventory = Object.entries(estimation.ingredients).map(([id,amount]) => ({
  2667. id,
  2668. stored: statsStore.getInventoryItem(id),
  2669. secondsLeft: statsStore.getInventoryItem(id) * 3600 / amount
  2670. })).reduce((a,b) => (a[b.id] = b, a), {});
  2671. const equipment = Object.entries(estimation.equipments).map(([id,amount]) => ({
  2672. id,
  2673. stored: statsStore.getEquipmentItem(id),
  2674. secondsLeft: statsStore.getEquipmentItem(id) * 3600 / amount
  2675. })).reduce((a,b) => (a[b.id] = b, a), {});
  2676. const levelState = statsStore.getLevel(estimation.skill);
  2677. estimation.timings = {
  2678. inventory,
  2679. equipment,
  2680. finished: Math.min(...Object.values(inventory).concat(Object.values(equipment)).map(a => a.secondsLeft)),
  2681. level: levelState.level === 100 ? 0 : util.expToNextLevel(levelState.level) * 3600 / estimation.exp,
  2682. tier: levelState.level === 100 ? 0 : util.expToNextTier(levelState.level) * 3600 / estimation.exp,
  2683. };
  2684. }
  2685.  
  2686. function enrichValues(estimation) {
  2687. estimation.values = {
  2688. drop: getSellPrice(estimation.drops),
  2689. ingredient: getSellPrice(estimation.ingredients),
  2690. equipment: getSellPrice(estimation.equipments),
  2691. net: 0
  2692. };
  2693. estimation.values.net = estimation.values.drop - estimation.values.ingredient - estimation.values.equipment;
  2694. }
  2695.  
  2696. function getSellPrice(object) {
  2697. return Object.entries(object)
  2698. .map(a => a[1] * itemCache.byId[a[0]].attributes.SELL_PRICE)
  2699. .filter(a => a)
  2700. .reduce((a,b) => a+b, 0);
  2701. }
  2702.  
  2703. function render(estimation) {
  2704. components.search(componentBlueprint, 'speed').value
  2705. = util.formatNumber(estimation.speed/10) + ' s';
  2706. components.search(componentBlueprint, 'exp').hidden
  2707. = estimation.exp === 0;
  2708. components.search(componentBlueprint, 'exp').value
  2709. = util.formatNumber(estimation.exp);
  2710. components.search(componentBlueprint, 'survivalChance').hidden
  2711. = estimation.type === 'ACTIVITY';
  2712. components.search(componentBlueprint, 'survivalChance').value
  2713. = util.formatNumber(estimation.survivalChance * 100) + ' %';
  2714. components.search(componentBlueprint, 'finishedTime').value
  2715. = util.secondsToDuration(estimation.timings.finished);
  2716. components.search(componentBlueprint, 'levelTime').hidden
  2717. = estimation.exp === 0 || estimation.timings.level === 0;
  2718. components.search(componentBlueprint, 'levelTime').value
  2719. = util.secondsToDuration(estimation.timings.level);
  2720. components.search(componentBlueprint, 'tierTime').hidden
  2721. = estimation.exp === 0 || estimation.timings.tier === 0;
  2722. components.search(componentBlueprint, 'tierTime').value
  2723. = util.secondsToDuration(estimation.timings.tier);
  2724. components.search(componentBlueprint, 'dropValue').hidden
  2725. = estimation.values.drop === 0;
  2726. components.search(componentBlueprint, 'dropValue').value
  2727. = util.formatNumber(estimation.values.drop);
  2728. components.search(componentBlueprint, 'ingredientValue').hidden
  2729. = estimation.values.ingredient === 0;
  2730. components.search(componentBlueprint, 'ingredientValue').value
  2731. = util.formatNumber(estimation.values.ingredient);
  2732. components.search(componentBlueprint, 'equipmentValue').hidden
  2733. = estimation.values.equipment === 0;
  2734. components.search(componentBlueprint, 'equipmentValue').value
  2735. = util.formatNumber(estimation.values.equipment);
  2736. components.search(componentBlueprint, 'netValue').hidden
  2737. = estimation.values.net === 0;
  2738. components.search(componentBlueprint, 'netValue').value
  2739. = util.formatNumber(estimation.values.net);
  2740. components.search(componentBlueprint, 'tabTime').hidden
  2741. = (estimation.timings.inventory.length + estimation.timings.equipment.length) === 0;
  2742.  
  2743. const dropRows = components.search(componentBlueprint, 'dropRows');
  2744. const ingredientRows = components.search(componentBlueprint, 'ingredientRows');
  2745. const timeRows = components.search(componentBlueprint, 'timeRows');
  2746. dropRows.rows = [];
  2747. ingredientRows.rows = [];
  2748. timeRows.rows = [];
  2749. for(const id in estimation.drops) {
  2750. const item = itemCache.byId[id];
  2751. dropRows.rows.push({
  2752. type: 'item',
  2753. image: `/assets/${item.image}`,
  2754. imagePixelated: true,
  2755. name: item.name,
  2756. value: util.formatNumber(estimation.drops[id]) + ' / hour'
  2757. });
  2758. }
  2759. for(const id in estimation.ingredients) {
  2760. const item = itemCache.byId[id];
  2761. const timing = estimation.timings.inventory[id];
  2762. ingredientRows.rows.push({
  2763. type: 'item',
  2764. image: `/assets/${item.image}`,
  2765. imagePixelated: true,
  2766. name: item.name,
  2767. value: util.formatNumber(estimation.ingredients[id]) + ' / hour'
  2768. });
  2769. timeRows.rows.push({
  2770. type: 'item',
  2771. image: `/assets/${item.image}`,
  2772. imagePixelated: true,
  2773. name: `${item.name} [${util.formatNumber(timing.stored)}]`,
  2774. value: util.secondsToDuration(timing.secondsLeft)
  2775. });
  2776. }
  2777. for(const id in estimation.equipments) {
  2778. const item = itemCache.byId[id];
  2779. const timing = estimation.timings.equipment[id];
  2780. ingredientRows.rows.push({
  2781. type: 'item',
  2782. image: `/assets/${item.image}`,
  2783. imagePixelated: true,
  2784. name: item.name,
  2785. value: util.formatNumber(estimation.equipments[id]) + ' / hour'
  2786. });
  2787. timeRows.rows.push({
  2788. type: 'item',
  2789. image: `/assets/${item.image}`,
  2790. imagePixelated: true,
  2791. name: `${item.name} [${util.formatNumber(timing.stored)}]`,
  2792. value: util.secondsToDuration(timing.secondsLeft)
  2793. });
  2794. }
  2795.  
  2796. components.addComponent(componentBlueprint);
  2797. }
  2798.  
  2799. const componentBlueprint = {
  2800. componentId: 'estimatorComponent',
  2801. dependsOn: 'skill-page',
  2802. parent: 'actions-component',
  2803. selectedTabIndex: 0,
  2804. tabs: [{
  2805. title: 'Overview',
  2806. rows: [{
  2807. type: 'item',
  2808. id: 'speed',
  2809. name: 'Time per action',
  2810. image: 'https://cdn-icons-png.flaticon.com/512/3563/3563395.png',
  2811. value: ''
  2812. },{
  2813. type: 'item',
  2814. id: 'exp',
  2815. name: 'Exp/hour',
  2816. image: 'https://cdn-icons-png.flaticon.com/512/616/616490.png',
  2817. value: ''
  2818. },{
  2819. type: 'item',
  2820. id: 'survivalChance',
  2821. name: 'Survival chance',
  2822. image: 'https://cdn-icons-png.flaticon.com/512/3004/3004458.png',
  2823. value: ''
  2824. },{
  2825. type: 'item',
  2826. id: 'finishedTime',
  2827. name: 'Finished',
  2828. image: 'https://cdn-icons-png.flaticon.com/512/1505/1505471.png',
  2829. value: ''
  2830. },{
  2831. type: 'item',
  2832. id: 'levelTime',
  2833. name: 'Level up',
  2834. image: 'https://cdn-icons-png.flaticon.com/512/4614/4614145.png',
  2835. value: ''
  2836. },{
  2837. type: 'item',
  2838. id: 'tierTime',
  2839. name: 'Tier up',
  2840. image: 'https://cdn-icons-png.flaticon.com/512/4789/4789514.png',
  2841. value: ''
  2842. },{
  2843. type: 'item',
  2844. id: 'dropValue',
  2845. name: 'Gold/hour (loot)',
  2846. image: 'https://cdn-icons-png.flaticon.com/512/9028/9028024.png',
  2847. imageFilter: 'invert(100%) sepia(47%) saturate(3361%) hue-rotate(313deg) brightness(106%) contrast(108%)',
  2848. value: ''
  2849. },{
  2850. type: 'item',
  2851. id: 'ingredientValue',
  2852. name: 'Gold/hour (materials)',
  2853. image: 'https://cdn-icons-png.flaticon.com/512/9028/9028031.png',
  2854. imageFilter: 'invert(100%) sepia(47%) saturate(3361%) hue-rotate(313deg) brightness(106%) contrast(108%)',
  2855. value: ''
  2856. },{
  2857. type: 'item',
  2858. id: 'equipmentValue',
  2859. name: 'Gold/hour (equipments)',
  2860. image: 'https://cdn-icons-png.flaticon.com/512/9028/9028031.png',
  2861. imageFilter: 'invert(100%) sepia(47%) saturate(3361%) hue-rotate(313deg) brightness(106%) contrast(108%)',
  2862. value: ''
  2863. },{
  2864. type: 'item',
  2865. id: 'netValue',
  2866. name: 'Gold/hour (total)',
  2867. image: 'https://cdn-icons-png.flaticon.com/512/11937/11937869.png',
  2868. imageFilter: 'invert(100%) sepia(47%) saturate(3361%) hue-rotate(313deg) brightness(106%) contrast(108%)',
  2869. value: ''
  2870. }]
  2871. },{
  2872. title: 'Items',
  2873. rows: [{
  2874. type: 'header',
  2875. title: 'Produced'
  2876. },{
  2877. type: 'segment',
  2878. id: 'dropRows',
  2879. rows: []
  2880. },{
  2881. type: 'header',
  2882. title: 'Consumed'
  2883. },{
  2884. type: 'segment',
  2885. id: 'ingredientRows',
  2886. rows: []
  2887. }]
  2888. },{
  2889. title: 'Time',
  2890. id: 'tabTime',
  2891. rows: [{
  2892. type: 'segment',
  2893. id: 'timeRows',
  2894. rows: []
  2895. }]
  2896. }]
  2897. };
  2898.  
  2899. initialise();
  2900.  
  2901. }
  2902. );
  2903. // estimatorAction
  2904. window.moduleRegistry.add('estimatorAction', (dropCache, actionCache, ingredientCache, skillCache, itemCache, statsStore) => {
  2905.  
  2906. const LOOPS_PER_HOUR = 10 * 60 * 60; // 1 second = 10 loops
  2907. const LOOPS_PER_FOOD = 150;
  2908.  
  2909. const exports = {
  2910. LOOPS_PER_HOUR,
  2911. LOOPS_PER_FOOD,
  2912. getDrops,
  2913. getIngredients,
  2914. getEquipmentUses
  2915. };
  2916.  
  2917. function getDrops(skillId, actionId, isCombat, multiplier = 1) {
  2918. const drops = dropCache.byAction[actionId];
  2919. if(!drops) {
  2920. return [];
  2921. }
  2922. const hasFailDrops = !!drops.find(a => a.type === 'FAILED');
  2923. const hasMonsterDrops = !!drops.find(a => a.type === 'MONSTER');
  2924. const successChance = hasFailDrops ? getSuccessChance(skillId, actionId) / 100 : 1;
  2925. return drops.map(drop => {
  2926. let amount = (1 + drop.amount) / 2 * multiplier * drop.chance;
  2927. if(drop.type !== 'MONSTER' && isCombat && hasMonsterDrops) {
  2928. amount = 0;
  2929. } else if(drop.type === 'MONSTER' && !isCombat) {
  2930. amount = 0;
  2931. } else if(drop.type === 'FAILED') {
  2932. amount *= 1 - successChance;
  2933. } else {
  2934. amount *= successChance;
  2935. }
  2936. if(amount) {
  2937. return {
  2938. id: drop.item,
  2939. amount
  2940. };
  2941. }
  2942. })
  2943. .filter(a => a)
  2944. .reduce((a,b) => (a[b.id] = b.amount, a), {});
  2945. }
  2946.  
  2947. function getSuccessChance(skillId, actionId) {
  2948. const action = actionCache.byId[actionId];
  2949. const level = statsStore.getLevel(skillId).level;
  2950. return Math.min(95, 80 + level - action.level) + Math.floor(level / 20);
  2951. }
  2952.  
  2953. function getIngredients(actionId, multiplier = 1) {
  2954. const ingredients = ingredientCache.byAction[actionId];
  2955. if(!ingredients) {
  2956. return [];
  2957. }
  2958. return ingredients.map(ingredient => ({
  2959. id: ingredient.item,
  2960. amount: ingredient.amount * multiplier
  2961. }))
  2962. .reduce((a,b) => (a[b.id] = b.amount, a), {});
  2963. }
  2964.  
  2965. function getEquipmentUses(skillId, actionId, isCombat = false, foodPerHour = 0) {
  2966. const skill = skillCache.byId[skillId];
  2967. const action = actionCache.byId[actionId];
  2968. const result = {};
  2969. const potionMultiplier = 1 + statsStore.get('DECREASED_POTION_DURATION') / 100;
  2970. if(isCombat) {
  2971. if(action.type !== 'OUTSKIRTS') {
  2972. // combat potions
  2973. statsStore.getManyEquipmentItems(itemCache.specialIds.potionCombat)
  2974. .forEach(a => result[a.id] = 20 * potionMultiplier);
  2975. }
  2976. if(action.type === 'DUNGEON') {
  2977. // dungeon map
  2978. statsStore.getManyEquipmentItems(itemCache.specialIds.map)
  2979. .forEach(a => result[a.id] = 3);
  2980. }
  2981. if(foodPerHour && action.type !== 'OUTSKIRTS' && statsStore.get('HEAL')) {
  2982. // active food
  2983. statsStore.getManyEquipmentItems(itemCache.specialIds.food)
  2984. .forEach(a => result[a.id] = foodPerHour);
  2985. }
  2986. if(statsStore.getAttackStyle() === 'Ranged') {
  2987. // ammo
  2988. const attacksPerHour = LOOPS_PER_HOUR / 5 / statsStore.get('ATTACK_SPEED');
  2989. const ammoPerHour = attacksPerHour * (1 - statsStore.get('AMMO_PRESERVATION_CHANCE') / 100);
  2990. statsStore.getManyEquipmentItems(itemCache.specialIds.arrow)
  2991. .forEach(a => result[a.id] = ammoPerHour);
  2992. }
  2993. } else {
  2994. if(skill.type === 'Gathering') {
  2995. // gathering potions
  2996. statsStore.getManyEquipmentItems(itemCache.specialIds.potionGathering)
  2997. .forEach(a => result[a.id] = 20 * potionMultiplier);
  2998. }
  2999. if(skill.type === 'Crafting') {
  3000. // crafting potions
  3001. statsStore.getManyEquipmentItems(itemCache.specialIds.potionCrafting)
  3002. .forEach(a => result[a.id] = 20 * potionMultiplier);
  3003. }
  3004. }
  3005. if(statsStore.get('PASSIVE_FOOD_CONSUMPTION') && statsStore.get('HEAL')) {
  3006. // passive food
  3007. statsStore.getManyEquipmentItems(itemCache.specialIds.food)
  3008. .forEach(a => result[a.id] = statsStore.get('PASSIVE_FOOD_CONSUMPTION')* 3600 / 5 / statsStore.get('HEAL'));
  3009. }
  3010. return result;
  3011. }
  3012.  
  3013. return exports;
  3014.  
  3015. }
  3016. );
  3017. // estimatorActivity
  3018. window.moduleRegistry.add('estimatorActivity', (skillCache, actionCache, estimatorAction, statsStore, itemCache, dropCache) => {
  3019.  
  3020. const exports = {
  3021. get
  3022. };
  3023.  
  3024. function get(skillId, actionId) {
  3025. const skill = skillCache.byId[skillId];
  3026. const action = actionCache.byId[actionId];
  3027. const speed = getSpeed(skill, action);
  3028. const actionCount = estimatorAction.LOOPS_PER_HOUR / speed;
  3029. const actualActionCount = actionCount * (1 + statsStore.get('EFFICIENCY', skill.technicalName) / 100);
  3030. const dropCount = actualActionCount * (1 + statsStore.get('DOUBLE_DROP', skill.technicalName) / 100);
  3031. const ingredientCount = actualActionCount * (1 - statsStore.get('PRESERVATION', skill.technicalName) / 100);
  3032. const exp = actualActionCount * action.exp * (1 + statsStore.get('DOUBLE_EXP', skill.technicalName) / 100);
  3033. const drops = estimatorAction.getDrops(skillId, actionId, false, dropCount);
  3034. const ingredients = estimatorAction.getIngredients(actionId, ingredientCount);
  3035. const equipments = estimatorAction.getEquipmentUses(skillId, actionId);
  3036.  
  3037. let statLowerTierChance;
  3038. if(skill.type === 'Gathering' && (statLowerTierChance = statsStore.get('LOWER_TIER_CHANCE', skill.technicalName) / 100)) {
  3039. for(const item in drops) {
  3040. const mappings = dropCache.lowerGatherMappings[item];
  3041. if(mappings) {
  3042. for(const other of mappings) {
  3043. drops[other] = (drops[other] || 0) + statLowerTierChance * drops[item] / mappings.length;
  3044. }
  3045. drops[item] *= 1 - statLowerTierChance;
  3046. }
  3047. }
  3048. }
  3049.  
  3050. let statMerchantSellChance;
  3051. if(skill.type === 'Crafting' && (statMerchantSellChance = statsStore.get('MERCHANT_SELL_CHANCE', skill.technicalName) / 100)) {
  3052. for(const item in drops) {
  3053. drops[itemCache.specialIds.coins] = (drops[itemCache.specialIds.coins] || 0) + 2 * statMerchantSellChance * drops[item] * itemCache.byId[item].attributes.SELL_PRICE;
  3054. drops[item] *= 1 - statMerchantSellChance;
  3055. }
  3056. }
  3057.  
  3058. return {
  3059. type: 'ACTIVITY',
  3060. skill: skillId,
  3061. speed,
  3062. exp,
  3063. drops,
  3064. ingredients,
  3065. equipments
  3066. };
  3067. }
  3068.  
  3069. function getSpeed(skill, action) {
  3070. const speedBonus = statsStore.get('SKILL_SPEED', skill.technicalName);
  3071. return Math.round(action.speed * 1000 / (100 + speedBonus)) + 1;
  3072. }
  3073.  
  3074. return exports;
  3075.  
  3076. }
  3077. );
  3078. // estimatorCombat
  3079. window.moduleRegistry.add('estimatorCombat', (skillCache, actionCache, monsterCache, itemCache, dropCache, statsStore, Distribution, estimatorAction) => {
  3080.  
  3081. const exports = {
  3082. get,
  3083. getDamageDistributions,
  3084. getSurvivalChance
  3085. };
  3086.  
  3087. function get(skillId, actionId) {
  3088. const skill = skillCache.byId[skillId];
  3089. const action = actionCache.byId[actionId];
  3090. const monsterIds = action.monster ? [action.monster] : action.monsterGroup;
  3091. const playerStats = getPlayerStats();
  3092. const sampleMonsterStats = getMonsterStats(monsterIds[Math.floor(monsterIds.length / 2)]);
  3093. playerStats.damage_ = new Distribution();
  3094. sampleMonsterStats.damage_ = new Distribution();
  3095. for(const monsterId of monsterIds) {
  3096. const monsterStats = getMonsterStats(monsterId);
  3097. let damage_ = getInternalDamageDistribution(playerStats, monsterStats, monsterIds.length > 1);
  3098. const weight = damage_.expectedRollsUntill(monsterStats.health);
  3099. playerStats.damage_.addDistribution(damage_, weight);
  3100. damage_ = getInternalDamageDistribution(monsterStats, playerStats, monsterIds.length > 1);
  3101. sampleMonsterStats.damage_.addDistribution(damage_, weight);
  3102. }
  3103. playerStats.damage_.normalize();
  3104. sampleMonsterStats.damage_.normalize();
  3105.  
  3106. const loopsPerKill = playerStats.attackSpeed * playerStats.damage_.expectedRollsUntill(sampleMonsterStats.health) * 10 + 5;
  3107. const actionCount = estimatorAction.LOOPS_PER_HOUR / loopsPerKill;
  3108. const efficiency = 1 + statsStore.get('EFFICIENCY', skill.technicalName) / 100;
  3109. const actualActionCount = actionCount * efficiency;
  3110. const dropCount = actualActionCount * (1 + statsStore.get('DOUBLE_DROP', skill.technicalName) / 100);
  3111. const attacksReceivedPerHour = estimatorAction.LOOPS_PER_HOUR / 10 / sampleMonsterStats.attackSpeed;
  3112. const healPerFood = statsStore.get('HEAL') * (1 + statsStore.get('FOOD_EFFECT') / 100);
  3113. const damagePerHour = attacksReceivedPerHour * sampleMonsterStats.damage_.average();
  3114. const foodPerHour = damagePerHour / healPerFood * (1 - statsStore.get('FOOD_PRESERVATION_CHANCE') / 100);
  3115.  
  3116. let exp = estimatorAction.LOOPS_PER_HOUR * action.exp / 1000;
  3117. exp *= efficiency;
  3118. exp *= 1 + statsStore.get('DOUBLE_EXP', skill.technicalName) / 100;
  3119. exp *= 1 + statsStore.get('COMBAT_EXP', skill.technicalName) / 100;
  3120. const drops = estimatorAction.getDrops(skillId, actionId, true, dropCount);
  3121. const equipments = estimatorAction.getEquipmentUses(skillId, actionId, true, foodPerHour);
  3122. const survivalChance = getSurvivalChance(playerStats, sampleMonsterStats, loopsPerKill);
  3123.  
  3124. let statCoinSnatch;
  3125. if(statCoinSnatch = statsStore.get('COIN_SNATCH')) {
  3126. const attacksPerHour = estimatorAction.LOOPS_PER_HOUR / 10 / playerStats.attackSpeed;
  3127. const coinsPerHour = (statCoinSnatch + 1) / 2 * attacksPerHour;
  3128. drops[itemCache.specialIds.coins] = (drops[itemCache.specialIds.coins] || 0) + coinsPerHour;
  3129. }
  3130.  
  3131. let statCarveChance = 0.1;
  3132. if(action.type !== 'OUTSKIRTS' && (statCarveChance = statsStore.get('CARVE_CHANCE') / 100)) {
  3133. const boneDrop = dropCache.byAction[actionId].find(a => a.chance === 1);
  3134. const boneDropCount = drops[boneDrop.item];
  3135. const coinDrop = dropCache.byAction[actionId].find(a => a.item === itemCache.specialIds.coins);
  3136. const averageAmount = (1 + coinDrop.amount) / 2;
  3137. drops[itemCache.specialIds.coins] -= statCarveChance * coinDrop.chance * averageAmount / 2 * boneDropCount;
  3138. const mappings = dropCache.boneCarveMappings[boneDrop.item];
  3139. for(const other of mappings) {
  3140. drops[other] = (drops[other] || 0) + statCarveChance * coinDrop.chance * boneDropCount / mappings.length;
  3141. }
  3142. }
  3143.  
  3144. return {
  3145. type: 'COMBAT',
  3146. skill: skillId,
  3147. speed: loopsPerKill,
  3148. exp,
  3149. drops,
  3150. ingredients: {},
  3151. equipments,
  3152. player: playerStats,
  3153. monster: sampleMonsterStats,
  3154. survivalChance
  3155. };
  3156. }
  3157.  
  3158. function getPlayerStats() {
  3159. const attackStyle = statsStore.getAttackStyle();
  3160. const attackSkill = skillCache.byTechnicalName[attackStyle];
  3161. const attackLevel = statsStore.getLevel(attackSkill.id).level;
  3162. const defenseLevel = statsStore.getLevel(8).level;
  3163. return {
  3164. isPlayer: true,
  3165. attackStyle,
  3166. attackSpeed: statsStore.get('ATTACK_SPEED'),
  3167. damage: statsStore.get('DAMAGE'),
  3168. armour: statsStore.get('ARMOUR'),
  3169. health: statsStore.get('HEALTH'),
  3170. blockChance: statsStore.get('BLOCK_CHANCE')/100,
  3171. critChance: statsStore.get('CRIT_CHANCE')/100,
  3172. stunChance: statsStore.get('STUN_CHANCE')/100,
  3173. parryChance: statsStore.get('PARRY_CHANCE')/100,
  3174. bleedChance: statsStore.get('BLEED_CHANCE')/100,
  3175. damageRange: (75 + statsStore.get('DAMAGE_RANGE'))/100,
  3176. dungeonDamage: 1 + statsStore.get('DUNGEON_DAMAGE')/100,
  3177. attackLevel,
  3178. defenseLevel
  3179. };
  3180. }
  3181.  
  3182. function getMonsterStats(monsterId) {
  3183. const monster = monsterCache.byId[monsterId];
  3184. return {
  3185. isPlayer: false,
  3186. attackStyle: monster.attackStyle,
  3187. attackSpeed: monster.speed,
  3188. damage: monster.attack,
  3189. armour: monster.armour,
  3190. health: monster.health,
  3191. blockChance: 0,
  3192. critChance: 0,
  3193. stunChance: 0,
  3194. parryChance: 0,
  3195. bleedChance: 0,
  3196. damageRange: 0.75,
  3197. dungeonDamage: 0,
  3198. attackLevel: monster.level,
  3199. defenseLevel: monster.level
  3200. };
  3201. }
  3202.  
  3203. function getInternalDamageDistribution(attacker, defender, isDungeon) {
  3204. let damage = attacker.damage;
  3205. damage *= getDamageTriangleModifier(attacker, defender);
  3206. damage *= getDamageScalingRatio(attacker, defender);
  3207. damage *= getDamageArmourRatio(attacker, defender);
  3208. damage *= !isDungeon ? 1 : attacker.dungeonDamage;
  3209.  
  3210. const maxDamage_ = new Distribution(damage);
  3211. // crit
  3212. if(attacker.critChance) {
  3213. maxDamage_.convolution(
  3214. Distribution.getRandomChance(attacker.critChance),
  3215. (dmg, crit) => dmg * (crit ? 1.5 : 1)
  3216. );
  3217. }
  3218. // damage range
  3219. const result = maxDamage_.convolutionWithGenerator(
  3220. dmg => Distribution.getRandomOutcomeRounded(dmg * attacker.damageRange, dmg),
  3221. (dmg, randomDamage) => randomDamage
  3222. );
  3223. // block
  3224. if(defender.blockChance) {
  3225. result.convolution(
  3226. Distribution.getRandomChance(defender.blockChance),
  3227. (dmg, blocked) => blocked ? 0 : dmg
  3228. );
  3229. }
  3230. // stun
  3231. if(defender.stunChance) {
  3232. let stunChance = defender.stunChance;
  3233. // only when defender accurate
  3234. stunChance *= getAccuracy(defender, attacker);
  3235. // can also happen on defender parries
  3236. stunChance *= 1 + defender.parryChance;
  3237. // modifier based on speed
  3238. stunChance *= attacker.attackSpeed / defender.attackSpeed;
  3239. // convert to actual stunned percentage
  3240. const stunnedPercentage = stunChance * 2.5 / attacker.attackSpeed;
  3241. result.convolution(
  3242. Distribution.getRandomChance(stunnedPercentage),
  3243. (dmg, stunned) => stunned ? 0 : dmg
  3244. );
  3245. }
  3246. // accuracy
  3247. const accuracy = getAccuracy(attacker, defender);
  3248. result.convolution(
  3249. Distribution.getRandomChance(accuracy),
  3250. (dmg, accurate) => accurate ? dmg : 0
  3251. );
  3252. // === special effects ===
  3253. const intermediateClone_ = result.clone();
  3254. // parry attacker - deal back 25% of a regular attack
  3255. if(attacker.parryChance) {
  3256. let parryChance = attacker.parryChance;
  3257. if(attacker.attackSpeed < defender.attackSpeed) {
  3258. parryChance *= attacker.attackSpeed / defender.attackSpeed;
  3259. }
  3260. const parried_ = intermediateClone_.clone();
  3261. parried_.convolution(
  3262. Distribution.getRandomChance(parryChance),
  3263. (dmg, parried) => parried ? Math.round(dmg/4.0) : 0
  3264. );
  3265. result.convolution(
  3266. parried_,
  3267. (dmg, extra) => dmg + extra
  3268. );
  3269. if(attacker.attackSpeed > defender.attackSpeed) {
  3270. // we can parry multiple times during one turn
  3271. parryChance *= (attacker.attackSpeed - defender.attackSpeed) / attacker.attackSpeed;
  3272. parried_.convolution(
  3273. Distribution.getRandomChance(parryChance),
  3274. (dmg, parried) => parried ? dmg : 0
  3275. );
  3276. result.convolution(
  3277. parried_,
  3278. (dmg, extra) => dmg + extra
  3279. );
  3280. }
  3281. }
  3282. // parry defender - deal 50% of a regular attack
  3283. if(defender.parryChance) {
  3284. result.convolution(
  3285. Distribution.getRandomChance(defender.parryChance),
  3286. (dmg, parried) => parried ? Math.round(dmg/2) : dmg
  3287. );
  3288. }
  3289. // bleed - 50% of damage over 3 seconds (assuming to be within one attack round)
  3290. if(attacker.bleedChance) {
  3291. const bleed_ = intermediateClone_.clone();
  3292. bleed_.convolution(
  3293. Distribution.getRandomChance(attacker.bleedChance),
  3294. (dmg, bleed) => bleed ? 5 * Math.round(dmg/10) : 0
  3295. );
  3296. result.convolution(
  3297. bleed_,
  3298. (dmg, extra) => dmg + extra
  3299. );
  3300. }
  3301. return result;
  3302. }
  3303.  
  3304. function getDamageTriangleModifier(attacker, defender) {
  3305. if(!attacker.attackStyle || !defender.attackStyle) {
  3306. return 1.0;
  3307. }
  3308. if(attacker.attackStyle === defender.attackStyle) {
  3309. return 1.0;
  3310. }
  3311. if(attacker.attackStyle === 'OneHanded' && defender.attackStyle === 'Ranged') {
  3312. return 1.1;
  3313. }
  3314. if(attacker.attackStyle === 'Ranged' && defender.attackStyle === 'TwoHanded') {
  3315. return 1.1;
  3316. }
  3317. if(attacker.attackStyle === 'TwoHanded' && defender.attackStyle === 'OneHanded') {
  3318. return 1.1;
  3319. }
  3320. return 0.9;
  3321. }
  3322.  
  3323. function getDamageScalingRatio(attacker, defender) {
  3324. const ratio = attacker.attackLevel / defender.defenseLevel;
  3325. if(attacker.isPlayer) {
  3326. return Math.min(1, ratio);
  3327. }
  3328. return Math.max(1, ratio);
  3329. }
  3330.  
  3331. function getDamageArmourRatio(attacker, defender) {
  3332. if(!defender.armour) {
  3333. return 1;
  3334. }
  3335. const scale = 25 + Math.min(70, (defender.armour - 25) * 50 / 105);
  3336. return (100 - scale) / 100;
  3337. }
  3338.  
  3339. function getAccuracy(attacker, defender) {
  3340. let accuracy = 75 + (attacker.attackLevel - defender.defenseLevel) / 2.0;
  3341. accuracy = Math.max(25, accuracy);
  3342. accuracy = Math.min(95, accuracy);
  3343. return accuracy / 100;
  3344. }
  3345.  
  3346. function getDamageDistributions(monsterId) {
  3347. const playerStats = getPlayerStats();
  3348. const monsterStats = getMonsterStats(monsterId);
  3349. const playerDamage_ = getInternalDamageDistribution(playerStats, monsterStats);
  3350. const monsterDamage_ = getInternalDamageDistribution(monsterStats, playerStats);
  3351. playerDamage_.normalize();
  3352. monsterDamage_.normalize();
  3353. return [playerDamage_, monsterDamage_];
  3354. }
  3355.  
  3356. function getSurvivalChance(player, monster, loopsPerFight, fights = 10, applyCringeMultiplier = false) {
  3357. const loopsPerAttack = monster.attackSpeed * 10;
  3358. let attacksPerFight = loopsPerFight / loopsPerAttack;
  3359. if(fights === 1 && applyCringeMultiplier) {
  3360. const playerLoopsPerAttack = player.attackSpeed * 10;
  3361. const playerAttacksPerFight = loopsPerFight / playerLoopsPerAttack;
  3362. const cringeMultiplier = Math.min(1.4, Math.max(1, 1.4 - playerAttacksPerFight / 50));
  3363. attacksPerFight *= cringeMultiplier;
  3364. }
  3365. const foodPerAttack = loopsPerAttack / estimatorAction.LOOPS_PER_FOOD;
  3366. const healPerFood = statsStore.get('HEAL') * (1 + statsStore.get('FOOD_EFFECT') / 100);
  3367. const healPerAttack = Math.round(healPerFood * foodPerAttack);
  3368. const healPerFight = healPerAttack * attacksPerFight;
  3369. let deathChance = 0;
  3370. let scenarioChance = 1;
  3371. let health = player.health;
  3372. for(let i=0;i<fights;i++) {
  3373. const currentDeathChance = monster.damage_.getRightTail(attacksPerFight, health + healPerFight);
  3374. deathChance += currentDeathChance * scenarioChance;
  3375. scenarioChance *= 1 - currentDeathChance;
  3376. const damage = monster.damage_.getMeanRange(attacksPerFight, healPerFight, health + healPerFight);
  3377. health -= damage - healPerFight;
  3378. if(isNaN(health) || health === Infinity || health === -Infinity) {
  3379. // TODO NaN / Infinity result from above?
  3380. break;
  3381. }
  3382. }
  3383. const cringeCutoff = 0.10;
  3384. if(fights === 1 && !applyCringeMultiplier && deathChance < cringeCutoff) {
  3385. const other = getSurvivalChance(player, monster, loopsPerFight, fights, true);
  3386. const avg = (1 - deathChance + other) / 2;
  3387. if(avg > 1 - cringeCutoff / 2) {
  3388. return avg;
  3389. }
  3390. }
  3391. return 1 - deathChance;
  3392. }
  3393.  
  3394. return exports;
  3395.  
  3396. }
  3397. );
  3398. // estimatorOutskirts
  3399. window.moduleRegistry.add('estimatorOutskirts', (actionCache, itemCache, statsStore, estimatorActivity, estimatorCombat) => {
  3400.  
  3401. const exports = {
  3402. get
  3403. };
  3404.  
  3405. function get(skillId, actionId) {
  3406. try {
  3407. const action = actionCache.byId[actionId];
  3408. const excludedItemIds = itemCache.specialIds.food.concat(itemCache.specialIds.potionCombat);
  3409. statsStore.update(new Set(excludedItemIds));
  3410.  
  3411. const activityEstimation = estimatorActivity.get(skillId, actionId);
  3412. const combatEstimation = estimatorCombat.get(skillId, actionId);
  3413. const monsterChance = (1000 - action.outskirtsMonsterChance) / 1000;
  3414.  
  3415. // Axioms:
  3416. // combatRatio = 1 - activityRatio
  3417. // activityLoops = totalLoops * activityRatio
  3418. // combatLoops = totalLoops * combatRatio
  3419. // fights = combatLoops / combatSpeed
  3420. // actions = activityLoops / activitySpeed
  3421. // encounterChance = fights / (fights + actions)
  3422. const combatRatio = combatEstimation.speed / (activityEstimation.speed * (1 / monsterChance + combatEstimation.speed / activityEstimation.speed - 1));
  3423. const activityRatio = 1 - combatRatio;
  3424.  
  3425. const survivalChance = estimatorCombat.getSurvivalChance(combatEstimation.player, combatEstimation.monster, combatEstimation.speed, 1);
  3426.  
  3427. const exp = activityEstimation.exp * activityRatio;
  3428. const drops = {};
  3429. merge(drops, activityEstimation.drops, activityRatio);
  3430. merge(drops, combatEstimation.drops, combatRatio);
  3431. const ingredients = {};
  3432. merge(ingredients, activityEstimation.ingredients, activityRatio);
  3433. merge(ingredients, combatEstimation.ingredients, combatRatio);
  3434. const equipments = {};
  3435. merge(equipments, activityEstimation.equipments, activityRatio);
  3436. merge(equipments, combatEstimation.equipments, combatRatio);
  3437.  
  3438. return {
  3439. type: 'OUTSKIRTS',
  3440. skill: skillId,
  3441. speed: activityEstimation.speed,
  3442. exp,
  3443. drops,
  3444. ingredients,
  3445. equipments,
  3446. player: combatEstimation.player,
  3447. monster: combatEstimation.monster,
  3448. survivalChance
  3449. };
  3450. } finally {
  3451. statsStore.update(new Set());
  3452. }
  3453. }
  3454.  
  3455. function merge(target, source, ratio) {
  3456. for(const key in source) {
  3457. target[key] = (target[key] || 0) + source[key] * ratio;
  3458. }
  3459. }
  3460.  
  3461. return exports;
  3462.  
  3463.  
  3464.  
  3465. }
  3466. );
  3467. // idleBeep
  3468. window.moduleRegistry.add('idleBeep', (configuration, events, util) => {
  3469.  
  3470. const audio = new Audio('data:audio/mpeg;base64,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');
  3471. const sleepAmount = 2000;
  3472. let enabled = false;
  3473. let started = false;
  3474.  
  3475. function initialise() {
  3476. configuration.registerCheckbox({
  3477. category: 'Other',
  3478. key: 'idle-beep-enabled',
  3479. name: 'Idle beep',
  3480. default: false,
  3481. handler: handleConfigStateChange
  3482. });
  3483. events.register('xhr', handleXhr);
  3484. }
  3485.  
  3486. function handleConfigStateChange(state, name) {
  3487. enabled = state;
  3488. }
  3489.  
  3490. async function handleXhr(xhr) {
  3491. if(!enabled) {
  3492. return;
  3493. }
  3494. if(xhr.url.endsWith('startAction')) {
  3495. started = true;
  3496. }
  3497. if(xhr.url.endsWith('stopAction')) {
  3498. started = false;
  3499. console.debug(`Triggering beep in ${sleepAmount}ms`);
  3500. await util.sleep(sleepAmount);
  3501. beep();
  3502. }
  3503. }
  3504.  
  3505. function beep() {
  3506. if(!started) {
  3507. audio.play();
  3508. }
  3509. }
  3510.  
  3511. initialise();
  3512.  
  3513. }
  3514. );
  3515. // itemHover
  3516. window.moduleRegistry.add('itemHover', (configuration, itemCache, util) => {
  3517.  
  3518. let enabled = false;
  3519. let entered = false;
  3520. let element;
  3521. const converters = {
  3522. SPEED: a => a/2,
  3523. DURATION: a => util.secondsToDuration(a/10)
  3524. }
  3525.  
  3526. function initialise() {
  3527. configuration.registerCheckbox({
  3528. category: 'UI Features',
  3529. key: 'item-hover',
  3530. name: 'Item hover info',
  3531. default: true,
  3532. handler: handleConfigStateChange
  3533. });
  3534. setup();
  3535. $(document).on('mouseenter', 'div.image > img', handleMouseEnter);
  3536. $(document).on('mouseleave', 'div.image > img', handleMouseLeave);
  3537. $(document).on('click', 'div.image > img', handleMouseLeave);
  3538. }
  3539.  
  3540. function handleConfigStateChange(state) {
  3541. enabled = state;
  3542. }
  3543.  
  3544. function handleMouseEnter(event) {
  3545. if(!enabled || entered || !itemCache.byId) {
  3546. return;
  3547. }
  3548. entered = true;
  3549. const name = $(event.relatedTarget).find('.name').text();
  3550. const nameMatch = itemCache.byName[name];
  3551. if(nameMatch) {
  3552. return show(nameMatch);
  3553. }
  3554.  
  3555. const parts = event.target.src.split('/');
  3556. const lastPart = parts[parts.length-1];
  3557. const imageMatch = itemCache.byImage[lastPart];
  3558. if(imageMatch) {
  3559. return show(imageMatch);
  3560. }
  3561. }
  3562.  
  3563. function handleMouseLeave(event) {
  3564. if(!enabled || !itemCache.byId) {
  3565. return;
  3566. }
  3567. entered = false;
  3568. hide();
  3569. }
  3570.  
  3571. function show(item) {
  3572. element.find('.image').attr('src', `/assets/${item.image}`);
  3573. element.find('.name').text(item.name);
  3574. for(const attribute of itemCache.attributes) {
  3575. let value = item.attributes[attribute.technicalName];
  3576. if(value && converters[attribute.technicalName]) {
  3577. value = converters[attribute.technicalName](value);
  3578. }
  3579. updateRow(attribute.technicalName, value);
  3580. }
  3581. element.show();
  3582. }
  3583.  
  3584. function updateRow(name, value) {
  3585. if(!value) {
  3586. element.find(`.${name}-row`).hide();
  3587. } else {
  3588. element.find(`.${name}`).text(value);
  3589. element.find(`.${name}-row`).show();
  3590. }
  3591. }
  3592.  
  3593. function hide() {
  3594. element.hide();
  3595. }
  3596.  
  3597. function setup() {
  3598. const attributesHtml = itemCache.attributes
  3599. .map(a => `<div class='${a.technicalName}-row'><img src='${a.image}'/><span>${a.name}</span><span class='${a.technicalName}'/></div>`)
  3600. .join('');
  3601. $('head').append(`
  3602. <style>
  3603. #custom-item-hover {
  3604. position: fixed;
  3605. right: .5em;
  3606. top: .5em;
  3607. display: flex;
  3608. font-family: Jost,Helvetica Neue,Arial,sans-serif;
  3609. flex-direction: column;
  3610. white-space: nowrap;
  3611. z-index: 1;
  3612. background-color: black;
  3613. padding: .4rem;
  3614. border: 1px solid #3e3e3e;
  3615. border-radius: .4em;
  3616. gap: .4em;
  3617. }
  3618. #custom-item-hover > div {
  3619. display: flex;
  3620. gap: .4em;
  3621. }
  3622. #custom-item-hover > div > *:last-child {
  3623. margin-left: auto;
  3624. }
  3625. #custom-item-hover img {
  3626. width: 24px;
  3627. height: 24px;
  3628. image-rendering: auto;
  3629. }
  3630. #custom-item-hover img.pixelated {
  3631. image-rendering: pixelated;
  3632. }
  3633. </style>
  3634. `);
  3635. element = $(`
  3636. <div id='custom-item-hover' style='display:none'>
  3637. <div>
  3638. <img class='image pixelated'/>
  3639. <span class='name'/>
  3640. </div>
  3641. ${attributesHtml}
  3642. </div>
  3643. `);
  3644. $('body').append(element);
  3645. }
  3646.  
  3647. initialise();
  3648.  
  3649. }
  3650. );
  3651. // recipeClickthrough
  3652. window.moduleRegistry.add('recipeClickthrough', (recipeCache, configuration, util) => {
  3653.  
  3654. let enabled = false;
  3655.  
  3656. function initialise() {
  3657. configuration.registerCheckbox({
  3658. category: 'UI Features',
  3659. key: 'recipe-click',
  3660. name: 'Recipe clickthrough',
  3661. default: true,
  3662. handler: handleConfigStateChange
  3663. });
  3664. $(document).on('click', 'div.image > img', handleClick);
  3665. }
  3666.  
  3667. function handleConfigStateChange(state) {
  3668. enabled = state;
  3669. }
  3670.  
  3671. function handleClick(event) {
  3672. if(!enabled) {
  3673. return;
  3674. }
  3675. if($(event.currentTarget).closest('button').length) {
  3676. return;
  3677. }
  3678. event.stopPropagation();
  3679. const name = $(event.relatedTarget).find('.name').text();
  3680. const nameMatch = recipeCache.byName[name];
  3681. if(nameMatch) {
  3682. return followRecipe(nameMatch);
  3683. }
  3684.  
  3685. const parts = event.target.src.split('/');
  3686. const lastPart = parts[parts.length-1];
  3687. const imageMatch = recipeCache.byImage[lastPart];
  3688. if(imageMatch) {
  3689. return followRecipe(imageMatch);
  3690. }
  3691. }
  3692.  
  3693. function followRecipe(recipe) {
  3694. util.goToPage(recipe.url);
  3695. }
  3696.  
  3697. initialise();
  3698.  
  3699. }
  3700. );
  3701. // syncTracker
  3702. window.moduleRegistry.add('syncTracker', (events, localDatabase, pages, components, util, toast, elementWatcher) => {
  3703.  
  3704. const STORE_NAME = 'sync-tracking';
  3705. const PAGE_NAME = 'Sync State';
  3706. const TOAST_SUCCESS_TIME = 1000*60*5;
  3707. const TOAST_WARN_TIME = 1000*60*60*4;
  3708. const TOAST_REWARN_TIME = 1000*60*60;
  3709.  
  3710. const sources = {
  3711. inventory: {
  3712. name: 'Inventory',
  3713. event: 'reader-inventory',
  3714. page: 'inventory'
  3715. },
  3716. 'equipment-equipment': {
  3717. name: 'Equipment',
  3718. event: 'reader-equipment-equipment',
  3719. page: 'equipment'
  3720. },
  3721. 'equipment-runes': {
  3722. name: 'Runes',
  3723. event: 'reader-equipment-runes',
  3724. page: 'equipment',
  3725. element: 'equipment-page .categories button:contains("Runes")'
  3726. },
  3727. 'equipment-tomes': {
  3728. name: 'Tomes',
  3729. event: 'reader-equipment-tomes',
  3730. page: 'equipment',
  3731. element: 'equipment-page .categories button:contains("Tomes")'
  3732. },
  3733. structures: {
  3734. name: 'Buildings',
  3735. event: 'reader-structures',
  3736. page: 'house/build/2'
  3737. },
  3738. enhancements: {
  3739. name: 'Building enhancements',
  3740. event: 'reader-enhancements',
  3741. page: 'house/enhance/2'
  3742. },
  3743. 'structures-guild': {
  3744. name: 'Guild buildings',
  3745. event: 'reader-structures-guild',
  3746. page: 'guild',
  3747. element: 'guild-page button:contains("Buildings")'
  3748. }
  3749. };
  3750.  
  3751. let autoVisiting = false;
  3752.  
  3753. async function initialise() {
  3754. await loadSavedData();
  3755. for(const key of Object.keys(sources)) {
  3756. events.register(sources[key].event, handleReader.bind(null, key));
  3757. }
  3758. await pages.register({
  3759. category: 'Misc',
  3760. name: PAGE_NAME,
  3761. image: 'https://img.icons8.com/?size=48&id=1ODJ62iG96gX&format=png',
  3762. columns: '3',
  3763. render: renderPage
  3764. });
  3765. pages.show(PAGE_NAME);
  3766. setInterval(update, 1000);
  3767. }
  3768.  
  3769. async function loadSavedData() {
  3770. const entries = await localDatabase.getAllEntries(STORE_NAME);
  3771. for(const entry of entries) {
  3772. if(!sources[entry.key]) {
  3773. sources[entry.key] = {};
  3774. }
  3775. sources[entry.key].lastSeen = entry.value.time;
  3776. events.emit(`reader-${entry.key}`, {
  3777. type: 'cache',
  3778. value: entry.value.value
  3779. });
  3780. }
  3781. }
  3782.  
  3783. function handleReader(key, event) {
  3784. if(event.type !== 'full') {
  3785. return;
  3786. }
  3787. const time = Date.now();
  3788. let newData = false;
  3789. if(!sources[key].lastSeen || sources[key].lastSeen + TOAST_SUCCESS_TIME < time) {
  3790. newData = true;
  3791. }
  3792. sources[key].lastSeen = time;
  3793. sources[key].notified = false;
  3794. localDatabase.saveEntry(STORE_NAME, {
  3795. key: key,
  3796. value: {
  3797. time,
  3798. value: event.value
  3799. }
  3800. });
  3801. if(newData) {
  3802. toast.create({
  3803. text: `${sources[key].name} synced`,
  3804. image: 'https://img.icons8.com/?size=48&id=1ODJ62iG96gX&format=png'
  3805. });
  3806. if(autoVisiting) {
  3807. triggerAutoVisitor();
  3808. }
  3809. }
  3810. }
  3811.  
  3812. function update() {
  3813. pages.requestRender(PAGE_NAME);
  3814. const time = Date.now();
  3815. for(const source of Object.values(sources)) {
  3816. if(source.lastSeen && source.lastSeen + TOAST_WARN_TIME >= time) {
  3817. continue;
  3818. }
  3819. if(source.notified && source.notified + TOAST_REWARN_TIME >= time) {
  3820. continue;
  3821. }
  3822. toast.create({
  3823. text: `${source.name} needs a sync`,
  3824. image: 'https://img.icons8.com/?size=48&id=1ODJ62iG96gX&format=png',
  3825. time: 5000
  3826. });
  3827. source.notified = time;
  3828. }
  3829. }
  3830.  
  3831. async function visit(source) {
  3832. if(!source.page) {
  3833. return;
  3834. }
  3835. util.goToPage(source.page);
  3836. if(source.element) {
  3837. await elementWatcher.exists(source.element);
  3838. $(source.element).click();
  3839. }
  3840. }
  3841.  
  3842. function startAutoVisiting() {
  3843. autoVisiting = true;
  3844. triggerAutoVisitor();
  3845. }
  3846.  
  3847. const stopAutoVisiting = util.debounce(function() {
  3848. autoVisiting = false;
  3849. pages.open(PAGE_NAME);
  3850. toast.create({
  3851. text: `Auto sync finished`,
  3852. image: 'https://img.icons8.com/?size=48&id=1ODJ62iG96gX&format=png'
  3853. });
  3854. }, 1500);
  3855.  
  3856. function triggerAutoVisitor() {
  3857. try {
  3858. const time = Date.now();
  3859. for(const source of Object.values(sources)) {
  3860. let secondsAgo = (time - source.lastSeen) / 1000;
  3861. if(source.page && (!source.lastSeen || secondsAgo >= 60*60)) {
  3862. visit(source);
  3863. return;
  3864. }
  3865. }
  3866. } finally {
  3867. stopAutoVisiting();
  3868. }
  3869. }
  3870.  
  3871. function renderPage() {
  3872. components.addComponent(autoVisitBlueprint);
  3873. const header = components.search(sourceBlueprint, 'header');
  3874. const item = components.search(sourceBlueprint, 'item');
  3875. const buttons = components.search(sourceBlueprint, 'buttons');
  3876. const time = Date.now();
  3877. for(const source of Object.values(sources)) {
  3878. sourceBlueprint.componentId = `syncTrackerSourceComponent_${source.name}`;
  3879. header.title = source.name;
  3880. let secondsAgo = (time - source.lastSeen) / 1000;
  3881. if(!secondsAgo) {
  3882. secondsAgo = Number.MAX_VALUE;
  3883. }
  3884. item.value = util.secondsToDuration(secondsAgo);
  3885. buttons.hidden = secondsAgo < 60*60;
  3886. buttons.buttons[0].action = visit.bind(null, source);
  3887. components.addComponent(sourceBlueprint);
  3888. }
  3889. }
  3890.  
  3891. const autoVisitBlueprint = {
  3892. componentId: 'syncTrackerAutoVisitComponent',
  3893. dependsOn: 'custom-page',
  3894. parent: '.column0',
  3895. selectedTabIndex: 0,
  3896. tabs: [
  3897. {
  3898. rows: [
  3899. {
  3900. type: 'buttons',
  3901. buttons: [
  3902. {
  3903. text: 'Auto sync',
  3904. color: 'primary',
  3905. action: startAutoVisiting
  3906. }
  3907. ]
  3908. }
  3909. ]
  3910. }
  3911. ]
  3912. };
  3913.  
  3914. const sourceBlueprint = {
  3915. componentId: 'syncTrackerSourceComponent',
  3916. dependsOn: 'custom-page',
  3917. parent: '.column0',
  3918. selectedTabIndex: 0,
  3919. tabs: [
  3920. {
  3921. rows: [
  3922. {
  3923. type: 'header',
  3924. id: 'header',
  3925. title: '',
  3926. centered: true
  3927. }, {
  3928. type: 'item',
  3929. id: 'item',
  3930. name: 'Last detected',
  3931. value: ''
  3932. }, {
  3933. type: 'buttons',
  3934. id: 'buttons',
  3935. buttons: [
  3936. {
  3937. text: 'Visit',
  3938. color: 'danger',
  3939. action: undefined
  3940. }
  3941. ]
  3942. }
  3943. ]
  3944. },
  3945. ]
  3946. };
  3947.  
  3948. initialise();
  3949.  
  3950. }
  3951. );
  3952. // ui
  3953. window.moduleRegistry.add('ui', (configuration) => {
  3954.  
  3955. const id = crypto.randomUUID();
  3956. const sections = [
  3957. 'challenges-page',
  3958. 'changelog-page',
  3959. 'daily-quest-page',
  3960. 'equipment-page',
  3961. 'guild-page',
  3962. 'home-page',
  3963. 'leaderboards-page',
  3964. 'market-page',
  3965. 'merchant-page',
  3966. 'quests-page',
  3967. 'settings-page',
  3968. 'skill-page',
  3969. 'upgrade-page'
  3970. ].join(', ');
  3971. const selector = `:is(${sections})`;
  3972. let gap
  3973.  
  3974. function initialise() {
  3975. configuration.registerCheckbox({
  3976. category: 'UI Features',
  3977. key: 'ui-changes',
  3978. name: 'UI changes',
  3979. default: false,
  3980. handler: handleConfigStateChange
  3981. });
  3982. }
  3983.  
  3984. function handleConfigStateChange(state) {
  3985. if(state) {
  3986. add();
  3987. } else {
  3988. remove();
  3989. }
  3990. }
  3991.  
  3992. function add() {
  3993. document.documentElement.style.setProperty('--gap', '8px');
  3994. const element = $(`
  3995. <style>
  3996. ${selector} :not(.multi-row) > :is(
  3997. button.item,
  3998. button.row,
  3999. button.socket-button,
  4000. button.level-button,
  4001. div.item,
  4002. div.row
  4003. ) {
  4004. padding: 2px 6px !important;
  4005. min-height: 0 !important;
  4006. }
  4007.  
  4008. ${selector} :not(.multi-row) > :is(
  4009. button.item div.image,
  4010. button.row div.image,
  4011. div.item div.image,
  4012. div.item div.placeholder-image,
  4013. div.row div.image
  4014. ) {
  4015. height: 32px !important;
  4016. width: 32px !important;
  4017. min-height: 0 !important;
  4018. min-width: 0 !important;
  4019. }
  4020.  
  4021. ${selector} div.lock {
  4022. height: unset !important;
  4023. padding: 0 !important;
  4024. }
  4025.  
  4026. action-component div.body > div.image,
  4027. produce-component div.body > div.image,
  4028. daily-quest-page div.body > div.image {
  4029. height: 48px !important;
  4030. width: 48px !important;
  4031. }
  4032.  
  4033. div.progress div.body {
  4034. padding: 8px !important;
  4035. }
  4036.  
  4037. action-component div.bars {
  4038. padding: 0 !important;
  4039. }
  4040.  
  4041. equipment-component button {
  4042. padding: 0 !important;
  4043. }
  4044.  
  4045. inventory-page .items {
  4046. grid-gap: 0 !important;
  4047. }
  4048.  
  4049. div.scroll.custom-scrollbar .header,
  4050. div.scroll.custom-scrollbar button {
  4051. height: 28px !important;
  4052. }
  4053.  
  4054. div.scroll.custom-scrollbar img {
  4055. height: 16px !important;
  4056. width: 16px !important;
  4057. }
  4058.  
  4059. .scroll {
  4060. overflow-y: auto !important;
  4061. }
  4062. .scroll {
  4063. -ms-overflow-style: none; /* Internet Explorer 10+ */
  4064. scrollbar-width: none; /* Firefox */
  4065. }
  4066. .scroll::-webkit-scrollbar {
  4067. display: none; /* Safari and Chrome */
  4068. }
  4069. </style>
  4070. `).attr('id', id);
  4071. window.$('head').append(element);
  4072. }
  4073.  
  4074. function remove() {
  4075. document.documentElement.style.removeProperty('--gap');
  4076. $(`#${id}`).remove();
  4077. }
  4078.  
  4079. initialise();
  4080.  
  4081. }
  4082. );
  4083. // versionWarning
  4084. window.moduleRegistry.add('versionWarning', (request, toast) => {
  4085.  
  4086. function initialise() {
  4087. setInterval(run, 1000 * 60 * 5);
  4088. }
  4089.  
  4090. async function run() {
  4091. const version = await request.getVersion();
  4092. if(!window.PANCAKE_VERSION || version === window.PANCAKE_VERSION) {
  4093. return;
  4094. }
  4095. toast.create({
  4096. 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`,
  4097. image: 'https://img.icons8.com/?size=48&id=iAqIpjeFjcYz&format=png',
  4098. time: 5000
  4099. });
  4100. }
  4101.  
  4102. initialise();
  4103.  
  4104. }
  4105. );
  4106. // abstractStateStore
  4107. window.moduleRegistry.add('abstractStateStore', (events, util) => {
  4108.  
  4109. const SOURCES = [
  4110. 'inventory',
  4111. 'equipment-equipment',
  4112. 'equipment-runes',
  4113. 'equipment-tomes',
  4114. 'structures',
  4115. 'enhancements',
  4116. 'structures-guild'
  4117. ];
  4118.  
  4119. const stateBySource = {};
  4120.  
  4121. function initialise() {
  4122. for(const source of SOURCES) {
  4123. stateBySource[source] = {};
  4124. events.register(`reader-${source}`, handleReader.bind(null, source));
  4125. }
  4126. }
  4127.  
  4128. function handleReader(source, event) {
  4129. let updated = false;
  4130. if(event.type === 'full' || event.type === 'cache') {
  4131. if(util.compareObjects(stateBySource[source], event.value)) {
  4132. return;
  4133. }
  4134. updated = true;
  4135. stateBySource[source] = event.value;
  4136. }
  4137. if(event.type === 'partial') {
  4138. for(const key of Object.keys(event.value)) {
  4139. if(stateBySource[source][key] === event.value[key]) {
  4140. continue;
  4141. }
  4142. updated = true;
  4143. stateBySource[source][key] = event.value[key];
  4144. }
  4145. }
  4146. if(updated) {
  4147. events.emit(`state-${source}`, stateBySource[source]);
  4148. }
  4149. }
  4150.  
  4151. initialise();
  4152.  
  4153. }
  4154. );
  4155. // expStateStore
  4156. window.moduleRegistry.add('expStateStore', (events, util) => {
  4157.  
  4158. const emitEvent = events.emit.bind(null, 'state-exp');
  4159. const state = {};
  4160.  
  4161. function initialise() {
  4162. events.register('reader-exp', handleExpReader);
  4163. }
  4164.  
  4165. function handleExpReader(event) {
  4166. let updated = false;
  4167. for(const skill of event) {
  4168. if(!state[skill.id]) {
  4169. state[skill.id] = {
  4170. id: skill.id,
  4171. exp: 0,
  4172. level: 1,
  4173. virtualLevel: 1
  4174. };
  4175. }
  4176. if(skill.exp > state[skill.id].exp) {
  4177. updated = true;
  4178. state[skill.id].exp = skill.exp;
  4179. state[skill.id].level = util.expToLevel(skill.exp);
  4180. state[skill.id].virtualLevel = util.expToVirtualLevel(skill.exp);
  4181. }
  4182. }
  4183. if(updated) {
  4184. emitEvent(state);
  4185. }
  4186. }
  4187.  
  4188. initialise();
  4189.  
  4190. }
  4191. );
  4192. // localConfigurationStore
  4193. window.moduleRegistry.add('localConfigurationStore', (localDatabase) => {
  4194.  
  4195. const exports = {
  4196. load,
  4197. save
  4198. };
  4199.  
  4200. const STORE_NAME = 'settings';
  4201.  
  4202. async function load() {
  4203. const entries = await localDatabase.getAllEntries(STORE_NAME);
  4204. const configurations = {};
  4205. for(const entry of entries) {
  4206. configurations[entry.key] = entry.value;
  4207. }
  4208. return configurations;
  4209. }
  4210.  
  4211. async function save(key, value) {
  4212. await localDatabase.saveEntry(STORE_NAME, {key, value});
  4213. }
  4214.  
  4215. return exports;
  4216.  
  4217. }
  4218. );
  4219. // statsStore
  4220. window.moduleRegistry.add('statsStore', (events, util, skillCache, itemCache, structuresCache, statNameCache) => {
  4221.  
  4222. const emitEvent = events.emit.bind(null, 'state-stats');
  4223.  
  4224. const exports = {
  4225. get,
  4226. getLevel,
  4227. getInventoryItem,
  4228. getEquipmentItem,
  4229. getManyEquipmentItems,
  4230. getAttackStyle,
  4231. update
  4232. };
  4233.  
  4234. let exp = {};
  4235. let inventory = {};
  4236. let tomes = {};
  4237. let equipment = {};
  4238. let runes = {};
  4239. let structures = {};
  4240. let enhancements = {};
  4241. let guildStructures = {};
  4242.  
  4243. let stats;
  4244.  
  4245. function initialise() {
  4246. let _update = util.debounce(update, 200);
  4247. events.register('state-exp', event => (exp = event, _update()));
  4248. events.register('state-inventory', event => (inventory = event, _update()));
  4249. events.register('state-equipment-tomes', event => (tomes = event, _update()));
  4250. events.register('state-equipment-equipment', event => (equipment = event, _update()));
  4251. events.register('state-equipment-runes', event => (runes = event, _update()));
  4252. events.register('state-structures', event => (structures = event, _update()));
  4253. events.register('state-enhancements', event => (enhancements = event, _update()));
  4254. events.register('state-structures-guild', event => (guildStructures = event, _update()));
  4255. }
  4256.  
  4257. function get(stat, skill) {
  4258. if(!stat) {
  4259. return stats;
  4260. }
  4261. statNameCache.validate(stat);
  4262. let value = 0;
  4263. if(stats && stats.global[stat]) {
  4264. value += stats.global[stat] || 0;
  4265. }
  4266. if(stats && stats.bySkill[stat] && stats.bySkill[stat][skill]) {
  4267. value += stats.bySkill[stat][skill];
  4268. }
  4269. return value;
  4270. }
  4271.  
  4272. function getLevel(skillId) {
  4273. return exp[skillId] || {
  4274. id: skillId,
  4275. exp: 0,
  4276. level: 1,
  4277. virtualLevel: 1
  4278. };
  4279. }
  4280.  
  4281. function getInventoryItem(itemId) {
  4282. return inventory[itemId] || 0;
  4283. }
  4284.  
  4285. function getEquipmentItem(itemId) {
  4286. return equipment[itemId] || tomes[itemId] || runes[itemId] || 0;
  4287. }
  4288.  
  4289. function getManyEquipmentItems(ids) {
  4290. return ids.map(id => ({
  4291. id,
  4292. amount: getEquipmentItem(id)
  4293. })).filter(a => a.amount);
  4294. }
  4295.  
  4296. function getAttackStyle() {
  4297. return stats.attackStyle;
  4298. }
  4299.  
  4300. function update(excludedItemIds) {
  4301. reset();
  4302. processExp();
  4303. processTomes();
  4304. processEquipment(excludedItemIds);
  4305. processRunes();
  4306. processStructures();
  4307. processEnhancements();
  4308. processGuildStructures();
  4309. cleanup();
  4310. if(!excludedItemIds) {
  4311. emitEvent(stats);
  4312. }
  4313. }
  4314.  
  4315. function reset() {
  4316. stats = {
  4317. attackStyle: null,
  4318. bySkill: {},
  4319. global: {}
  4320. };
  4321. }
  4322.  
  4323. function processExp() {
  4324. for(const id in exp) {
  4325. const skill = skillCache.byId[id];
  4326. addStats({
  4327. bySkill: {
  4328. EFFICIENCY : {
  4329. [skill.technicalName]: 0.25
  4330. }
  4331. }
  4332. }, exp[id].level);
  4333. if(skill.displayName === 'Ranged') {
  4334. addStats({
  4335. global: {
  4336. AMMO_PRESERVATION_CHANCE : 0.25
  4337. }
  4338. }, exp[id].level);
  4339. }
  4340. }
  4341. }
  4342.  
  4343. // first tomes, then equipments
  4344. // because we need to know the potion effect multiplier first
  4345. function processTomes() {
  4346. for(const id in tomes) {
  4347. const item = itemCache.byId[id];
  4348. if(!item) {
  4349. continue;
  4350. }
  4351. addStats(item.stats);
  4352. }
  4353. }
  4354.  
  4355. function processEquipment(excludedItemIds) {
  4356. let arrow;
  4357. let bow;
  4358. const potionMultiplier = get('INCREASED_POTION_EFFECT');
  4359. for(const id in equipment) {
  4360. if(equipment[id] <= 0) {
  4361. continue;
  4362. }
  4363. if(excludedItemIds && excludedItemIds.has(+id)) {
  4364. continue;
  4365. }
  4366. const item = itemCache.byId[id];
  4367. if(!item) {
  4368. continue;
  4369. }
  4370. if(item.stats.global.ATTACK_SPEED) {
  4371. stats.attackStyle = item.skill;
  4372. }
  4373. if(item.name.endsWith('Arrow')) {
  4374. arrow = item;
  4375. continue;
  4376. }
  4377. if(item.name.endsWith('Bow')) {
  4378. bow = item;
  4379. }
  4380. let multiplier = 1;
  4381. let accuracy = 2;
  4382. if(potionMultiplier && item.name.endsWith('Potion')) {
  4383. multiplier = 1 + potionMultiplier / 100;
  4384. accuracy = 10;
  4385. }
  4386. if(item.name.endsWith('Rune')) {
  4387. multiplier = equipment[id];
  4388. accuracy = 10;
  4389. }
  4390. addStats(item.stats, multiplier, accuracy);
  4391. }
  4392. if(bow && arrow) {
  4393. addStats(arrow.stats);
  4394. }
  4395. }
  4396. function processRunes() {
  4397. for(const id in runes) {
  4398. const item = itemCache.byId[id];
  4399. if(!item) {
  4400. continue;
  4401. }
  4402. addStats(item.stats, runes[id]);
  4403. }
  4404. }
  4405.  
  4406. function processStructures() {
  4407. for(const name in structures) {
  4408. const structure = structuresCache.byName[name];
  4409. if(!structure) {
  4410. continue;
  4411. }
  4412. addStats(structure.regular, structures[name] + 2/3);
  4413. }
  4414. }
  4415.  
  4416. function processEnhancements() {
  4417. for(const name in enhancements) {
  4418. const structure = structuresCache.byName[name];
  4419. if(!structure) {
  4420. continue;
  4421. }
  4422. addStats(structure.enhance, enhancements[name]);
  4423. }
  4424. }
  4425.  
  4426. function processGuildStructures() {
  4427. for(const name in guildStructures) {
  4428. const structure = structuresCache.byName[name];
  4429. if(!structure) {
  4430. continue;
  4431. }
  4432. addStats(structure.regular, guildStructures[name]);
  4433. }
  4434. }
  4435.  
  4436. function cleanup() {
  4437. // base
  4438. addStats({
  4439. global: {
  4440. HEALTH: 10,
  4441. AMMO_PRESERVATION_CHANCE : 55
  4442. }
  4443. });
  4444. // fallback
  4445. if(!stats.attackStyle) {
  4446. stats.attackStyle = 'OneHanded';
  4447. }
  4448. if(!stats.global.ATTACK_SPEED) {
  4449. stats.global.ATTACK_SPEED = 3;
  4450. stats.attackStyle = '';
  4451. }
  4452. // health percent
  4453. const healthPercent = get('HEALTH_PERCENT');
  4454. if(healthPercent) {
  4455. const health = get('HEALTH');
  4456. addStats({
  4457. global: {
  4458. HEALTH : Math.floor(healthPercent * health / 100)
  4459. }
  4460. })
  4461. }
  4462. // damage percent
  4463. const damagePercent = get('DAMAGE_PERCENT');
  4464. if(damagePercent) {
  4465. const damage = get('DAMAGE');
  4466. addStats({
  4467. global: {
  4468. DAMAGE : Math.floor(damagePercent * damage / 100)
  4469. }
  4470. })
  4471. }
  4472. // bonus level efficiency
  4473. if(stats.bySkill['BONUS_LEVEL']) {
  4474. for(const skill in stats.bySkill['BONUS_LEVEL']) {
  4475. addStats({
  4476. bySkill: {
  4477. EFFICIENCY: {
  4478. [skill]: 0.25
  4479. }
  4480. }
  4481. }, Math.round(stats.bySkill['BONUS_LEVEL'][skill]), 4);
  4482. }
  4483. }
  4484. }
  4485.  
  4486. function addStats(newStats, multiplier = 1, accuracy = 1) {
  4487. if(newStats.global) {
  4488. for(const stat in newStats.global) {
  4489. if(!stats.global[stat]) {
  4490. stats.global[stat] = 0;
  4491. }
  4492. stats.global[stat] += Math.round(accuracy * multiplier * newStats.global[stat]) / accuracy;
  4493. }
  4494. }
  4495. if(newStats.bySkill) {
  4496. for(const stat in newStats.bySkill) {
  4497. if(!stats.bySkill[stat]) {
  4498. stats.bySkill[stat] = {};
  4499. }
  4500. for(const skill in newStats.bySkill[stat]) {
  4501. if(!stats.bySkill[stat][skill]) {
  4502. stats.bySkill[stat][skill] = 0;
  4503. }
  4504. stats.bySkill[stat][skill] += Math.round(accuracy * multiplier * newStats.bySkill[stat][skill]) / accuracy;
  4505. }
  4506. }
  4507. }
  4508. }
  4509.  
  4510. initialise();
  4511.  
  4512. return exports;
  4513.  
  4514. }
  4515. );
  4516. // actionCache
  4517. window.moduleRegistry.add('actionCache', (request, Promise) => {
  4518.  
  4519. const initialised = new Promise.Expiring(2000);
  4520.  
  4521. const exports = {
  4522. list: [],
  4523. byId: null,
  4524. byName: null
  4525. };
  4526.  
  4527. async function initialise() {
  4528. const actions = await request.listActions();
  4529. exports.byId = {};
  4530. exports.byName = {};
  4531. for(const action of actions) {
  4532. exports.list.push(action);
  4533. exports.byId[action.id] = action;
  4534. exports.byName[action.name] = action;
  4535. }
  4536. initialised.resolve(exports);
  4537. }
  4538.  
  4539. initialise();
  4540.  
  4541. return initialised;
  4542.  
  4543. }
  4544. );
  4545. // dropCache
  4546. window.moduleRegistry.add('dropCache', (request, Promise, itemCache, actionCache, skillCache) => {
  4547.  
  4548. const initialised = new Promise.Expiring(2000);
  4549.  
  4550. const exports = {
  4551. list: [],
  4552. byAction: null,
  4553. byItem: null,
  4554. boneCarveMappings: null,
  4555. lowerGatherMappings: null
  4556. };
  4557.  
  4558. Object.defineProperty(Array.prototype, '_groupBy', {
  4559. enumerable: false,
  4560. value: function(selector) {
  4561. return Object.values(this.reduce(function(rv, x) {
  4562. (rv[selector(x)] = rv[selector(x)] || []).push(x);
  4563. return rv;
  4564. }, {}));
  4565. }
  4566. });
  4567.  
  4568. Object.defineProperty(Array.prototype, '_distinct', {
  4569. enumerable: false,
  4570. value: function(selector) {
  4571. return [...new Set(this)];
  4572. }
  4573. });
  4574.  
  4575. async function initialise() {
  4576. const drops = await request.listDrops();
  4577. exports.byAction = {};
  4578. exports.byItem = {};
  4579. for(const drop of drops) {
  4580. exports.list.push(drop);
  4581. if(!exports.byAction[drop.action]) {
  4582. exports.byAction[drop.action] = [];
  4583. }
  4584. exports.byAction[drop.action].push(drop);
  4585. if(!exports.byItem[drop.item]) {
  4586. exports.byItem[drop.item] = [];
  4587. }
  4588. exports.byItem[drop.item].push(drop);
  4589. }
  4590. extractBoneCarvings();
  4591. extractLowerGathers();
  4592. initialised.resolve(exports);
  4593. }
  4594.  
  4595. // I'm sorry for what follows
  4596. function extractBoneCarvings() {
  4597. let name;
  4598. exports.boneCarveMappings = exports.list
  4599. // filtering
  4600. .filter(drop => drop.type === 'GUARANTEED')
  4601. .filter(drop => (name = itemCache.byId[drop.item].name, name.endsWith('Bone') || name.endsWith('Fang')))
  4602. .filter(drop => actionCache.byId[drop.action].skill === 'Combat')
  4603. // sort
  4604. .sort((a,b) => actionCache.byId[a.action].level - actionCache.byId[b.action].level)
  4605. // per level
  4606. ._groupBy(drop => actionCache.byId[drop.action].level)
  4607. .map(a => a[0].item)
  4608. .map((item,i,all) => ({
  4609. from: item,
  4610. to: [].concat([all[i-1]]).concat([all[i-2]]).filter(a => a)
  4611. }))
  4612. .reduce((a,b) => (a[b.from] = b.to, a), {});
  4613. }
  4614.  
  4615. function extractLowerGathers() {
  4616. exports.lowerGatherMappings = exports.list
  4617. // filtering
  4618. .filter(drop => drop.type === 'REGULAR')
  4619. .filter(drop => skillCache.byName[actionCache.byId[drop.action].skill].type === 'Gathering')
  4620. // sort
  4621. .sort((a,b) => actionCache.byId[a.action].level - actionCache.byId[b.action].level)
  4622. // per action, the highest chance drop
  4623. ._groupBy(drop => drop.action)
  4624. .map(a => a.reduce((a,b) => a.chance >= b.chance ? a : b))
  4625. // per skill
  4626. ._groupBy(drop => actionCache.byId[drop.action].skill)
  4627. .flatMap(a => a
  4628. ._groupBy(drop => actionCache.byId[drop.action].level)
  4629. .map(b => b.map(drop => drop.item)._distinct())
  4630. .flatMap((b,i,all) => b.map(item => ({
  4631. from: item,
  4632. to: [].concat(all[i-1]).concat(all[i-2]).filter(a => a)
  4633. })))
  4634. )
  4635. .reduce((a,b) => (a[b.from] = b.to, a), {});
  4636. }
  4637.  
  4638. initialise();
  4639.  
  4640. return initialised;
  4641.  
  4642. }
  4643. );
  4644. // ingredientCache
  4645. window.moduleRegistry.add('ingredientCache', (request, Promise) => {
  4646.  
  4647. const initialised = new Promise.Expiring(2000);
  4648.  
  4649. const exports = {
  4650. list: [],
  4651. byAction: null,
  4652. byItem: null
  4653. };
  4654.  
  4655. async function initialise() {
  4656. const ingredients = await request.listIngredients();
  4657. exports.byAction = {};
  4658. exports.byItem = {};
  4659. for(const ingredient of ingredients) {
  4660. if(!exports.byAction[ingredient.action]) {
  4661. exports.byAction[ingredient.action] = [];
  4662. }
  4663. exports.byAction[ingredient.action].push(ingredient);
  4664. if(!exports.byItem[ingredient.item]) {
  4665. exports.byItem[ingredient.item] = [];
  4666. }
  4667. exports.byItem[ingredient.item].push(ingredient);
  4668. }
  4669. initialised.resolve(exports);
  4670. }
  4671.  
  4672. initialise();
  4673.  
  4674. return initialised;
  4675.  
  4676. }
  4677. );
  4678. // itemCache
  4679. window.moduleRegistry.add('itemCache', (request, Promise) => {
  4680.  
  4681. const initialised = new Promise.Expiring(2000);
  4682.  
  4683. const exports = {
  4684. list: [],
  4685. byId: null,
  4686. byName: null,
  4687. byImage: null,
  4688. attributes: null,
  4689. specialIds: {
  4690. coins: null,
  4691. food: null,
  4692. arrow: null,
  4693. map: null,
  4694. runeGathering: null,
  4695. potionCombat: null,
  4696. potionGathering: null,
  4697. potionCrafting: null,
  4698. }
  4699. };
  4700.  
  4701. async function initialise() {
  4702. const enrichedItems = await request.listItems();
  4703. exports.byId = {};
  4704. exports.byName = {};
  4705. exports.byImage = {};
  4706. for(const enrichedItem of enrichedItems) {
  4707. const item = Object.assign(enrichedItem.item, enrichedItem);
  4708. delete item.item;
  4709. exports.list.push(item);
  4710. exports.byId[item.id] = item;
  4711. exports.byName[item.name] = item;
  4712. const lastPart = item.image.split('/').at(-1);
  4713. if(exports.byImage[lastPart]) {
  4714. exports.byImage[lastPart].duplicate = true;
  4715. } else {
  4716. exports.byImage[lastPart] = item;
  4717. }
  4718. if(!item.attributes) {
  4719. item.attributes = {};
  4720. }
  4721. if(item.charcoal) {
  4722. item.attributes.CHARCOAL = item.charcoal;
  4723. }
  4724. if(item.compost) {
  4725. item.attributes.COMPOST = item.compost;
  4726. }
  4727. if(item.attributes.ATTACK_SPEED) {
  4728. item.attributes.ATTACK_SPEED /= 2;
  4729. }
  4730. for(const stat in item.stats.bySkill) {
  4731. if(item.stats.bySkill[stat].All) {
  4732. item.stats.global[stat] = item.stats.bySkill[stat].All;
  4733. delete item.stats.bySkill[stat].All;
  4734. if(!Object.keys(item.stats.bySkill[stat]).length) {
  4735. delete item.stats.bySkill[stat];
  4736. }
  4737. }
  4738. }
  4739. }
  4740. for(const image of Object.keys(exports.byImage)) {
  4741. if(exports.byImage[image].duplicate) {
  4742. delete exports.byImage[image];
  4743. }
  4744. }
  4745. exports.attributes = await request.listItemAttributes();
  4746. exports.attributes.push({
  4747. technicalName: 'CHARCOAL',
  4748. name: 'Charcoal',
  4749. image: '/assets/items/charcoal.png'
  4750. },{
  4751. technicalName: 'COMPOST',
  4752. name: 'Compost',
  4753. image: '/assets/misc/compost.png'
  4754. });
  4755. exports.specialIds.coins = exports.byName['Coins'].id;
  4756. exports.specialIds.food = exports.list.filter(a => /^Cooked|Pie$/.exec(a.name)).map(a => a.id);
  4757. exports.specialIds.arrow = exports.list.filter(a => /Arrow$/.exec(a.name)).map(a => a.id);
  4758. exports.specialIds.map = exports.list.filter(a => /Map \d+$/.exec(a.name)).map(a => a.id);
  4759. exports.specialIds.potionCombat = exports.list.filter(a => /(Combat|Health).*Potion$/.exec(a.name)).map(a => a.id);
  4760. exports.specialIds.potionGathering = exports.list.filter(a => /Gather.*Potion$/.exec(a.name)).map(a => a.id);
  4761. exports.specialIds.potionCrafting = exports.list.filter(a => /(Craft|Preservation).*Potion$/.exec(a.name)).map(a => a.id);
  4762. exports.specialIds.runeGathering = exports.list.filter(a => /(Woodcutting|Mining|Farming|Fishing) Rune$/.exec(a.name)).map(a => a.id);
  4763. initialised.resolve(exports);
  4764. }
  4765.  
  4766. initialise();
  4767.  
  4768. return initialised;
  4769.  
  4770. }
  4771. );
  4772. // monsterCache
  4773. window.moduleRegistry.add('monsterCache', (request, Promise) => {
  4774.  
  4775. const initialised = new Promise.Expiring(2000);
  4776.  
  4777. const exports = {
  4778. list: [],
  4779. byId: null,
  4780. byName: null
  4781. };
  4782.  
  4783. async function initialise() {
  4784. const monsters = await request.listMonsters();
  4785. exports.byId = {};
  4786. exports.byName = {};
  4787. for(const monster of monsters) {
  4788. exports.list.push(monster);
  4789. exports.byId[monster.id] = monster;
  4790. exports.byName[monster.name] = monster;
  4791. }
  4792. initialised.resolve(exports);
  4793. }
  4794.  
  4795. initialise();
  4796.  
  4797. return initialised;
  4798.  
  4799. }
  4800. );
  4801. // recipeCache
  4802. window.moduleRegistry.add('recipeCache', (request, Promise) => {
  4803.  
  4804. const initialised = new Promise.Expiring(2000);
  4805.  
  4806. const exports = {
  4807. list: [],
  4808. byName: null,
  4809. byImage: null
  4810. };
  4811.  
  4812. async function initialise() {
  4813. const recipes = await request.listRecipes();
  4814. exports.byName = {};
  4815. exports.byImage = {};
  4816. for(const recipe of recipes) {
  4817. if(!exports.byName[recipe.name]) {
  4818. exports.byName[recipe.name] = recipe;
  4819. }
  4820. const lastPart = recipe.image.split('/').at(-1);
  4821. if(!exports.byImage[lastPart]) {
  4822. exports.byImage[lastPart] = recipe;
  4823. }
  4824. }
  4825. initialised.resolve(exports);
  4826. }
  4827.  
  4828. initialise();
  4829.  
  4830. return initialised;
  4831.  
  4832. }
  4833. );
  4834. // skillCache
  4835. window.moduleRegistry.add('skillCache', (request, Promise) => {
  4836.  
  4837. const initialised = new Promise.Expiring(2000);
  4838.  
  4839. const exports = {
  4840. list: [],
  4841. byId: null,
  4842. byName: null,
  4843. byTechnicalName: null,
  4844. };
  4845.  
  4846. async function initialise() {
  4847. const skills = await request.listSkills();
  4848. exports.byId = {};
  4849. exports.byName = {};
  4850. exports.byTechnicalName = {};
  4851. for(const skill of skills) {
  4852. exports.list.push(skill);
  4853. exports.byId[skill.id] = skill;
  4854. exports.byName[skill.displayName] = skill;
  4855. exports.byTechnicalName[skill.technicalName] = skill;
  4856. }
  4857. initialised.resolve(exports);
  4858. }
  4859.  
  4860. initialise();
  4861.  
  4862. return initialised;
  4863.  
  4864. }
  4865. );
  4866. // statNameCache
  4867. window.moduleRegistry.add('statNameCache', () => {
  4868.  
  4869. const exports = {
  4870. validate
  4871. };
  4872.  
  4873. const statNames = new Set([
  4874. // ITEM_STAT_ATTRIBUTE
  4875. 'AMMO_PRESERVATION_CHANCE',
  4876. 'ATTACK_SPEED',
  4877. 'BONUS_LEVEL',
  4878. 'COIN_SNATCH',
  4879. 'COMBAT_EXP',
  4880. 'DOUBLE_EXP',
  4881. 'DOUBLE_DROP',
  4882. 'EFFICIENCY',
  4883. 'LOWER_TIER_CHANCE',
  4884. 'MERCHANT_SELL_CHANCE',
  4885. 'PRESERVATION',
  4886. 'SKILL_SPEED',
  4887. // ITEM_ATTRIBUTE
  4888. 'ARMOUR',
  4889. 'BLEED_CHANCE',
  4890. 'BLOCK_CHANCE',
  4891. 'CARVE_CHANCE',
  4892. 'COIN_SNATCH',
  4893. 'COMBAT_EXP',
  4894. 'CRIT_CHANCE',
  4895. 'DAMAGE',
  4896. 'DAMAGE_PERCENT',
  4897. 'DAMAGE_RANGE',
  4898. 'DECREASED_POTION_DURATION',
  4899. 'DUNGEON_DAMAGE',
  4900. 'FOOD_EFFECT',
  4901. 'FOOD_PRESERVATION_CHANCE',
  4902. 'HEAL',
  4903. 'HEALTH',
  4904. 'HEALTH_PERCENT',
  4905. 'INCREASED_POTION_EFFECT',
  4906. 'MAP_FIND_CHANCE',
  4907. 'PARRY_CHANCE',
  4908. 'PASSIVE_FOOD_CONSUMPTION',
  4909. 'REVIVE_TIME',
  4910. 'STUN_CHANCE'
  4911. ]);
  4912.  
  4913. function validate(name) {
  4914. if(!statNames.has(name)) {
  4915. throw `Unsupported stat usage : ${name}`;
  4916. }
  4917. }
  4918.  
  4919. return exports;
  4920.  
  4921. });
  4922. // structuresCache
  4923. window.moduleRegistry.add('structuresCache', (request, Promise) => {
  4924.  
  4925. const initialised = new Promise.Expiring(2000);
  4926.  
  4927. const exports = {
  4928. list: [],
  4929. byName: null
  4930. };
  4931.  
  4932. async function initialise() {
  4933. const enrichedStructures = await request.listStructures();
  4934. exports.byName = {};
  4935. for(const enrichedStructure of enrichedStructures) {
  4936. exports.list.push(enrichedStructure);
  4937. exports.byName[enrichedStructure.name] = enrichedStructure;
  4938. }
  4939. initialised.resolve(exports);
  4940. }
  4941.  
  4942. initialise();
  4943.  
  4944. return initialised;
  4945.  
  4946. }
  4947. );
  4948. window.moduleRegistry.build();