Elethor Combat Calculator

Calculate combat profit with uniform midas/extractor options.

当前为 2025-03-19 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Elethor Combat Calculator
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.11
  5. // @description Calculate combat profit with uniform midas/extractor options.
  6. // @author Eugene
  7. // @match https://elethor.com/*
  8. // @grant GM_xmlhttpRequest
  9. // @esversion 11
  10. // @license GPL-3.0-or-later
  11. // ==/UserScript==
  12.  
  13. /*
  14. * This program is free software: you can redistribute it and/or modify
  15. * it under the terms of the GNU General Public License as published by
  16. * the Free Software Foundation, either version 3 of the License, or
  17. * (at your option) any later version.
  18. *
  19. * This program is distributed in the hope that it will be useful,
  20. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  21. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  22. * GNU General Public License for more details.
  23. *
  24. * You should have received a copy of the GNU General Public License
  25. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  26. */
  27.  
  28. (function() {
  29. 'use strict';
  30.  
  31. const monsterData = {
  32. 'Apex Nassul': { mobDrop: 'Ether Flux', itemId: 381, baseMobDropChance: 40.5, baseGoldDropChance: 100 },
  33. 'Scarlet Merchant': { mobDrop: null, itemId: null, baseMobDropChance: 0, baseGoldDropChance: 100 },
  34. 'Voidkin Artificier': { mobDrop: 'Void Artifact', itemId: 385, baseMobDropChance: 100, baseGoldDropChance: 100 },
  35. 'Voidstalker': { mobDrop: 'Tattered Cowl', itemId: 386, baseMobDropChance: 40.5, baseGoldDropChance: 100 },
  36. 'Tunnel Ambusher': { mobDrop: 'Carapace Segment', itemId: 387, baseMobDropChance: 40.5, baseGoldDropChance: 100 },
  37. 'Elite Guard Broodmother': { mobDrop: 'Elite Guard Insignia', itemId: 388, baseMobDropChance: 40.5, baseGoldDropChance: 100 }
  38. };
  39.  
  40. function createButton() {
  41. const button = document.createElement('button');
  42. button.textContent = 'Combat Calculator';
  43. button.style.cssText = "position: fixed; top: 20px; right: 20px; background-color: #4CAF50; color: white; padding: 10px 20px; border: none; border-radius: 5px; cursor: pointer; z-index: 1000;";
  44. document.body.appendChild(button);
  45. button.addEventListener('click', toggleCalculator);
  46. }
  47.  
  48. function createCalculatorUI() {
  49. const container = document.createElement('div');
  50. container.id = 'combatCalculator';
  51. container.style.cssText = "display: none; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background-color: black; color: white; padding: 20px; border-radius: 10px; box-shadow: 0 0 10px rgba(255,255,255,0.5); z-index: 1001; max-height: 80vh; overflow-y: auto; min-width: 800px;";
  52.  
  53. const title = document.createElement('h2');
  54. title.textContent = 'Combat Calculator';
  55. title.style.textAlign = 'center';
  56.  
  57. const subtitle = document.createElement('h3');
  58. subtitle.style.textAlign = 'center';
  59. subtitle.innerHTML = 'Made by <a href="https://elethor.com/profile/49979" target="_blank">Eugene</a>';
  60.  
  61. container.appendChild(title);
  62. container.appendChild(subtitle);
  63.  
  64. // -- Uniform Settings Container --
  65. const uniformContainer = document.createElement('div');
  66. uniformContainer.id = 'uniformContainer';
  67. uniformContainer.style.cssText = "margin: 10px 0; padding: 10px; border: 1px solid white;";
  68.  
  69. // Uniform Midas Checkbox
  70. const uniformMidasLabel = document.createElement('label');
  71. uniformMidasLabel.style.marginRight = '20px';
  72. const uniformMidasCheckbox = document.createElement('input');
  73. uniformMidasCheckbox.type = 'checkbox';
  74. uniformMidasCheckbox.id = 'uniformMidasCheckbox';
  75. uniformMidasLabel.appendChild(uniformMidasCheckbox);
  76. uniformMidasLabel.appendChild(document.createTextNode(' Uniform Midas'));
  77. uniformMidasCheckbox.addEventListener('change', function() {
  78. document.querySelectorAll('.midas-input').forEach(input => {
  79. input.disabled = this.checked;
  80. input.parentElement.classList.toggle('disabled-input', this.checked);
  81. });
  82. });
  83. uniformContainer.appendChild(uniformMidasLabel);
  84.  
  85. // Uniform Extractor Checkbox
  86. const uniformExtractorLabel = document.createElement('label');
  87. const uniformExtractorCheckbox = document.createElement('input');
  88. uniformExtractorCheckbox.type = 'checkbox';
  89. uniformExtractorCheckbox.id = 'uniformExtractorCheckbox';
  90. uniformExtractorLabel.appendChild(uniformExtractorCheckbox);
  91. uniformExtractorLabel.appendChild(document.createTextNode(' Uniform Extractor'));
  92. uniformExtractorCheckbox.addEventListener('change', function() {
  93. document.querySelectorAll('.extractor-input').forEach(input => {
  94. input.disabled = this.checked;
  95. input.parentElement.classList.toggle('disabled-input', this.checked);
  96. });
  97. });
  98. uniformContainer.appendChild(uniformExtractorLabel);
  99.  
  100. // Global Midas Input (hidden by default)
  101. const globalMidasContainer = document.createElement('div');
  102. globalMidasContainer.id = 'globalMidasContainer';
  103. globalMidasContainer.style.cssText = "display: none; margin-top: 10px;";
  104. const globalMidasLabel = document.createElement('label');
  105. globalMidasLabel.textContent = 'Global Midas %: ';
  106. const globalMidasInput = document.createElement('input');
  107. globalMidasInput.type = 'number';
  108. globalMidasInput.id = 'globalMidasInput';
  109. globalMidasInput.min = '0';
  110. globalMidasInput.style.cssText = "background-color: white; color: black;";
  111. globalMidasContainer.appendChild(globalMidasLabel);
  112. globalMidasContainer.appendChild(globalMidasInput);
  113. uniformContainer.appendChild(globalMidasContainer);
  114.  
  115. // Global Extractor Input (hidden by default)
  116. const globalExtractorContainer = document.createElement('div');
  117. globalExtractorContainer.id = 'globalExtractorContainer';
  118. globalExtractorContainer.style.cssText = "display: none; margin-top: 10px;";
  119. const globalExtractorLabel = document.createElement('label');
  120. globalExtractorLabel.textContent = 'Global Extractor %: ';
  121. const globalExtractorInput = document.createElement('input');
  122. globalExtractorInput.type = 'number';
  123. globalExtractorInput.id = 'globalExtractorInput';
  124. globalExtractorInput.min = '0';
  125. globalExtractorInput.style.cssText = "background-color: white; color: black;";
  126. globalExtractorContainer.appendChild(globalExtractorLabel);
  127. globalExtractorContainer.appendChild(globalExtractorInput);
  128. uniformContainer.appendChild(globalExtractorContainer);
  129.  
  130. // Event listeners for uniform checkboxes
  131. uniformMidasCheckbox.addEventListener('change', function() {
  132. if (this.checked) {
  133. globalMidasContainer.style.display = 'block';
  134. // Disable individual midas inputs
  135. document.querySelectorAll('.midas-input').forEach(input => {
  136. input.disabled = true;
  137. input.style.backgroundColor = '#f0f0f0';
  138. });
  139. } else {
  140. globalMidasContainer.style.display = 'none';
  141. // Enable individual midas inputs
  142. document.querySelectorAll('.midas-input').forEach(input => {
  143. input.disabled = false;
  144. input.style.backgroundColor = 'white';
  145. });
  146. }
  147. });
  148.  
  149. uniformExtractorCheckbox.addEventListener('change', function() {
  150. if (this.checked) {
  151. globalExtractorContainer.style.display = 'block';
  152. // Disable individual extractor inputs
  153. document.querySelectorAll('.extractor-input').forEach(input => {
  154. input.disabled = true;
  155. input.style.backgroundColor = '#f0f0f0';
  156. });
  157. } else {
  158. globalExtractorContainer.style.display = 'none';
  159. document.querySelectorAll('.extractor-input').forEach(input => {
  160. input.disabled = false;
  161. input.style.backgroundColor = 'white';
  162. });
  163. }
  164. });
  165.  
  166. container.appendChild(uniformContainer);
  167. // -------------------------------
  168.  
  169. const monsterList = document.createElement('div');
  170. monsterList.style.marginTop = '20px';
  171.  
  172. for (const monster in monsterData) {
  173. const row = createMonsterRow(monster, monsterData[monster]);
  174. monsterList.appendChild(row);
  175. }
  176.  
  177. const calculateButton = document.createElement('button');
  178. calculateButton.textContent = 'Calculate';
  179. calculateButton.style.cssText = "background-color: #4CAF50; color: white; padding: 10px 20px; border: none; border-radius: 5px; cursor: pointer; margin-top: 20px; width: 100%;";
  180. calculateButton.addEventListener('click', calculateGpA);
  181.  
  182. container.appendChild(monsterList);
  183. container.appendChild(calculateButton);
  184. document.body.appendChild(container);
  185.  
  186. // Add overlay
  187. const overlay = document.createElement('div');
  188. overlay.id = 'calculatorOverlay';
  189. overlay.style.cssText = "display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.5); z-index: 1000;";
  190. overlay.addEventListener('click', toggleCalculator);
  191. document.body.appendChild(overlay);
  192. }
  193.  
  194. function createMonsterRow(monsterName, data) {
  195. const row = document.createElement('div');
  196. // Add a class to easily identify monster rows
  197. row.classList.add('monster-row');
  198. row.style.cssText = "display: grid; grid-template-columns: 200px repeat(6, 1fr) 100px; gap: 10px; margin-bottom: 15px; align-items: center; color: white;";
  199.  
  200. // Monster name
  201. const nameLabel = document.createElement('div');
  202. nameLabel.textContent = monsterName;
  203. row.appendChild(nameLabel);
  204.  
  205. // Tier input
  206. const tierInput = document.createElement('input');
  207. tierInput.type = 'number';
  208. tierInput.min = '0';
  209. tierInput.max = '10000';
  210. tierInput.placeholder = 'Tier';
  211. tierInput.className = 'tier-input';
  212. tierInput.style.backgroundColor = 'white';
  213. tierInput.style.color = 'black';
  214. row.appendChild(tierInput);
  215.  
  216. // Win rate input
  217. const winRateInput = document.createElement('input');
  218. winRateInput.type = 'number';
  219. winRateInput.min = '0';
  220. winRateInput.max = '100';
  221. winRateInput.placeholder = 'Win Rate %';
  222. winRateInput.className = 'winrate-input';
  223. winRateInput.style.backgroundColor = 'white';
  224. winRateInput.style.color = 'black';
  225. row.appendChild(winRateInput);
  226.  
  227. // Price radio buttons
  228. const priceContainer = document.createElement('div');
  229. priceContainer.style.cssText = "display: flex; align-items: center; gap: 10px;";
  230. const priceLabel = document.createElement('span');
  231. priceLabel.textContent = 'Price: ';
  232. priceContainer.appendChild(priceLabel);
  233.  
  234. const priceTypes = ['Buy price', 'Sell price', 'Manual'];
  235. priceTypes.forEach((type, index) => {
  236. const radioContainer = document.createElement('div');
  237. radioContainer.style.cssText = "display: flex; align-items: center; gap: 4px;";
  238.  
  239. const radio = document.createElement('input');
  240. radio.type = 'radio';
  241. radio.name = `price-${monsterName}`;
  242. radio.value = type.toLowerCase();
  243. radio.id = `${type.toLowerCase()}-${monsterName}`;
  244. radio.style.margin = '0';
  245. if (index === 0) radio.checked = true;
  246.  
  247. const label = document.createElement('label');
  248. label.htmlFor = radio.id;
  249. label.textContent = type;
  250. label.style.margin = '0';
  251.  
  252. radioContainer.appendChild(radio);
  253. radioContainer.appendChild(label);
  254. priceContainer.appendChild(radioContainer);
  255. });
  256.  
  257. // Disable price options for Scarlet Merchant
  258. if (monsterName === 'Scarlet Merchant') {
  259. priceContainer.querySelectorAll('input[type="radio"]').forEach(radio => {
  260. radio.disabled = true;
  261. });
  262. }
  263.  
  264. row.appendChild(priceContainer);
  265.  
  266. // Price input - always visible and persistent
  267. const priceInput = document.createElement('input');
  268. priceInput.type = 'number';
  269. priceInput.min = '0';
  270. priceInput.placeholder = monsterName === 'Scarlet Merchant' ? 'No mob drops' : 'Price';
  271. priceInput.className = 'price-input';
  272. priceInput.dataset.monster = monsterName;
  273. priceInput.style.backgroundColor = monsterName === 'Scarlet Merchant' ? '#f0f0f0' : 'white';
  274. priceInput.style.color = 'black';
  275. if (monsterName === 'Scarlet Merchant') {
  276. priceInput.disabled = true;
  277. }
  278. row.appendChild(priceInput);
  279.  
  280. // Load saved values from localStorage if available
  281. const savedManualPrice = localStorage.getItem(`manual-price-${monsterName}`);
  282. const savedTier = localStorage.getItem(`tier-${monsterName}`);
  283. const savedWinRate = localStorage.getItem(`winrate-${monsterName}`);
  284.  
  285. if (savedManualPrice) {
  286. priceInput.value = savedManualPrice;
  287. }
  288. if (savedTier) {
  289. tierInput.value = savedTier;
  290. }
  291. if (savedWinRate) {
  292. winRateInput.value = savedWinRate;
  293. }
  294.  
  295. // Save tier and winrate to localStorage when they change
  296. tierInput.addEventListener('input', () => {
  297. localStorage.setItem(`tier-${monsterName}`, tierInput.value);
  298. });
  299.  
  300. winRateInput.addEventListener('input', () => {
  301. localStorage.setItem(`winrate-${monsterName}`, winRateInput.value);
  302. });
  303.  
  304. // Save manual price to localStorage when it changes
  305. priceInput.addEventListener('input', () => {
  306. if (priceContainer.querySelector('input[value="manual"]:checked')) {
  307. localStorage.setItem(`manual-price-${monsterName}`, priceInput.value);
  308. }
  309. });
  310.  
  311. // Handle price input behavior based on radio selection
  312. priceContainer.querySelectorAll('input[type="radio"]').forEach(radio => {
  313. // Load saved price type selection from localStorage
  314. const savedPriceType = localStorage.getItem(`price-type-${monsterName}`);
  315. if (savedPriceType && radio.value === savedPriceType) {
  316. radio.checked = true;
  317. }
  318.  
  319. radio.addEventListener('change', () => {
  320. // Save price type selection to localStorage
  321. localStorage.setItem(`price-type-${monsterName}`, radio.value);
  322.  
  323. if (radio.value === 'manual') {
  324. priceInput.disabled = false;
  325. priceInput.style.backgroundColor = 'white';
  326. priceInput.placeholder = 'Enter price';
  327. // Restore saved manual price if available
  328. const savedPrice = localStorage.getItem(`manual-price-${monsterName}`);
  329. if (savedPrice) {
  330. priceInput.value = savedPrice;
  331. }
  332. } else {
  333. priceInput.disabled = true;
  334. priceInput.style.backgroundColor = '#f0f0f0';
  335. priceInput.placeholder = 'Fetching price...';
  336. // If monster has an item, fetch the price immediately
  337. if (data.itemId) {
  338. fetchMarketData(data.itemId, radio.value === 'buy price' ? 'buy' : 'sell')
  339. .then(price => {
  340. priceInput.value = price;
  341. })
  342. .catch(error => {
  343. console.error(`Error fetching price for ${monsterName}:`, error);
  344. priceInput.placeholder = 'Error fetching price';
  345. });
  346. }
  347. }
  348. });
  349. });
  350.  
  351. // Set initial state based on saved or default selection
  352. const selectedRadio = priceContainer.querySelector('input[type="radio"]:checked');
  353. if (selectedRadio.value === 'manual') {
  354. priceInput.disabled = false;
  355. priceInput.style.backgroundColor = 'white';
  356. priceInput.placeholder = 'Enter price';
  357. } else {
  358. priceInput.disabled = true;
  359. priceInput.style.backgroundColor = '#f0f0f0';
  360. priceInput.placeholder = 'Fetching price...';
  361. // If monster has an item, fetch the price immediately
  362. if (data.itemId) {
  363. fetchMarketData(data.itemId, selectedRadio.value === 'buy price' ? 'buy' : 'sell')
  364. .then(price => {
  365. priceInput.value = price;
  366. })
  367. .catch(error => {
  368. console.error(`Error fetching price for ${monsterName}:`, error);
  369. priceInput.placeholder = 'Error fetching price';
  370. });
  371. }
  372. }
  373.  
  374. // Midas percentage input
  375. const midasInput = document.createElement('input');
  376. midasInput.type = 'number';
  377. midasInput.min = '0';
  378. midasInput.placeholder = 'Midas %';
  379. midasInput.className = 'midas-input';
  380. midasInput.style.backgroundColor = 'white';
  381. midasInput.style.color = 'black';
  382. row.appendChild(midasInput);
  383.  
  384. // Load saved Midas value
  385. const savedMidas = localStorage.getItem(`midas-${monsterName}`);
  386. if (savedMidas) {
  387. midasInput.value = savedMidas;
  388. }
  389.  
  390. // Save Midas value when it changes
  391. midasInput.addEventListener('input', () => {
  392. localStorage.setItem(`midas-${monsterName}`, midasInput.value);
  393. });
  394.  
  395. // Extractor percentage input
  396. const extractorInput = document.createElement('input');
  397. extractorInput.type = 'number';
  398. extractorInput.min = '0';
  399. extractorInput.placeholder = 'Extractor %';
  400. extractorInput.className = 'extractor-input';
  401. extractorInput.style.backgroundColor = 'white';
  402. extractorInput.style.color = 'black';
  403. row.appendChild(extractorInput);
  404.  
  405. // Load saved Extractor value
  406. const savedExtractor = localStorage.getItem(`extractor-${monsterName}`);
  407. if (savedExtractor) {
  408. extractorInput.value = savedExtractor;
  409. }
  410.  
  411. // Save Extractor value when it changes
  412. extractorInput.addEventListener('input', () => {
  413. localStorage.setItem(`extractor-${monsterName}`, extractorInput.value);
  414. });
  415.  
  416. // GpA result
  417. const gpaResult = document.createElement('div');
  418. gpaResult.className = 'gpa-result';
  419. row.appendChild(gpaResult);
  420.  
  421. return row;
  422. }
  423.  
  424. async function fetchMarketData(itemId, priceType) {
  425. return new Promise((resolve, reject) => {
  426. GM_xmlhttpRequest({
  427. method: 'GET',
  428. url: `https://elethor.com/game/market/listings?itemId=${itemId}`,
  429. onload: function(response) {
  430. try {
  431. const data = JSON.parse(response.responseText);
  432. if (!data || !Array.isArray(data)) {
  433. throw new Error('Invalid market data received');
  434. }
  435.  
  436. const relevantListings = data.filter(listing => listing.type === priceType);
  437. if (relevantListings.length === 0) {
  438. throw new Error(`No ${priceType} listings found`);
  439. }
  440.  
  441. if (priceType === 'sell') {
  442. resolve(Math.min(...relevantListings.map(listing => listing.price)));
  443. } else {
  444. resolve(Math.max(...relevantListings.map(listing => listing.price)));
  445. }
  446. } catch (error) {
  447. reject(error);
  448. }
  449. },
  450. onerror: function(error) {
  451. reject(error);
  452. }
  453. });
  454. });
  455. }
  456.  
  457. function calculateTierMultiplier(tier, isGold, isScarletMerchant) {
  458. if (isGold) {
  459. if (isScarletMerchant) {
  460. return 1 + (tier * 0.2);
  461. } else {
  462. return 1 + (tier * 0.15);
  463. }
  464. } else {
  465. return 1 + (tier * 0.75);
  466. }
  467. }
  468.  
  469. async function calculateGpA() {
  470. // Use the specific class to get monster rows
  471. const rows = document.querySelectorAll('.monster-row');
  472. let maxGpA = -1;
  473. let maxGpARow = null;
  474.  
  475. // Reset previous results
  476. document.querySelectorAll('.gpa-result').forEach(result => {
  477. result.textContent = '';
  478. result.style.color = '';
  479. });
  480.  
  481. // First refresh all market prices
  482. for (const row of rows) {
  483. const monsterName = row.querySelector('div').textContent;
  484. const monsterInfo = monsterData[monsterName];
  485. const selectedPriceType = row.querySelector('input[type="radio"]:checked')?.value;
  486. const priceInput = row.querySelector('.price-input');
  487.  
  488. if (monsterInfo.itemId && selectedPriceType !== 'manual') {
  489. try {
  490. const price = await fetchMarketData(monsterInfo.itemId, selectedPriceType === 'buy price' ? 'buy' : 'sell');
  491. priceInput.value = price;
  492. } catch (error) {
  493. console.error(`Error refreshing price for ${monsterName}:`, error);
  494. }
  495. }
  496. }
  497.  
  498. for (const row of rows) {
  499. const monsterName = row.querySelector('div').textContent;
  500. const monsterInfo = monsterData[monsterName];
  501.  
  502. const tier = parseFloat(row.querySelector('.tier-input').value) || 0;
  503. const winRate = parseFloat(row.querySelector('.winrate-input').value) || 0;
  504. // Get individual values first…
  505. let midasPercentage = parseFloat(row.querySelector('.midas-input').value) || 0;
  506. let extractorPercentage = parseFloat(row.querySelector('.extractor-input').value) || 0;
  507.  
  508. // Override with global values if uniform mode is enabled
  509. const uniformMidas = document.getElementById('uniformMidasCheckbox').checked;
  510. const uniformExtractor = document.getElementById('uniformExtractorCheckbox').checked;
  511. if (uniformMidas) {
  512. midasPercentage = parseFloat(document.getElementById('globalMidasInput').value) || 0;
  513. }
  514. if (uniformExtractor) {
  515. extractorPercentage = parseFloat(document.getElementById('globalExtractorInput').value) || 0;
  516. }
  517.  
  518. let mobDropPrice = 0;
  519. const selectedPriceType = row.querySelector('input[type="radio"]:checked')?.value;
  520.  
  521. if (monsterInfo.itemId && selectedPriceType !== 'manual') {
  522. try {
  523. mobDropPrice = await fetchMarketData(monsterInfo.itemId, selectedPriceType === 'buy price' ? 'buy' : 'sell');
  524. } catch (error) {
  525. console.error(`Error fetching price for ${monsterName}:`, error);
  526. }
  527. } else if (selectedPriceType === 'manual') {
  528. mobDropPrice = parseFloat(row.querySelector('.price-input').value) || 0;
  529. }
  530.  
  531. const mobDropMultiplier = calculateTierMultiplier(tier, false, false);
  532. const goldMultiplier = calculateTierMultiplier(tier, true, monsterName === 'Scarlet Merchant');
  533.  
  534. const gpa = (
  535. ((winRate / 100) * monsterInfo.baseMobDropChance * mobDropMultiplier / 100) * (1 + extractorPercentage / 100) * mobDropPrice +
  536. ((winRate / 100) * monsterInfo.baseGoldDropChance * goldMultiplier / 100) * (1 + midasPercentage / 100)
  537. );
  538.  
  539. const gpaResult = row.querySelector('.gpa-result');
  540. gpaResult.textContent = gpa.toFixed(2);
  541.  
  542. if (gpa > maxGpA) {
  543. maxGpA = gpa;
  544. maxGpARow = row;
  545. }
  546. }
  547.  
  548. if (maxGpARow) {
  549. const checkmark = '✓';
  550. maxGpARow.querySelector('.gpa-result').style.color = '#4CAF50';
  551. maxGpARow.querySelector('.gpa-result').textContent += ` ${checkmark}`;
  552. }
  553. }
  554.  
  555. function toggleCalculator() {
  556. const calculator = document.getElementById('combatCalculator');
  557. const overlay = document.getElementById('calculatorOverlay');
  558. const isVisible = calculator.style.display === 'block';
  559.  
  560. calculator.style.display = isVisible ? 'none' : 'block';
  561. overlay.style.display = isVisible ? 'none' : 'block';
  562. }
  563.  
  564. // Initialize the calculator
  565. createButton();
  566. createCalculatorUI();
  567. })();