Elethor Spire Simulator

Spire simulator

  1. // ==UserScript==
  2. // @name Elethor Spire Simulator
  3. // @namespace http://tampermonkey.net/
  4. // @version 2.2
  5. // @description Spire simulator
  6. // @author Eugene
  7. // @match https://elethor.com/*
  8. // @grant none
  9. // @license GPL-3.0-or-later
  10. // ==/UserScript==
  11. /*
  12. * This program is free software: you can redistribute it and/or modify
  13. * it under the terms of the GNU General Public License as published by
  14. * the Free Software Foundation, either version 3 of the License, or
  15. * (at your option) any later version.
  16. *
  17. * This program is distributed in the hope that it will be useful,
  18. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  19. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  20. * GNU General Public License for more details.
  21. *
  22. * You should have received a copy of the GNU General Public License
  23. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  24. */
  25. (function() {
  26. 'use strict';
  27.  
  28. const stats = [
  29. 'MIN DAMAGE',
  30. 'MAX DAMAGE',
  31. 'HEALTH',
  32. 'BREACH',
  33. 'BARRIER',
  34. 'CRIT CHANCE',
  35. 'CRIT DAMAGE',
  36. 'BLOCK CHANCE',
  37. 'BLOCK AMOUNT'
  38. ];
  39. let simulationLogs = [];
  40. let monsterWins = 0;
  41. let userWins = 0;
  42. let selectedModifiers = {};
  43.  
  44. function saveToLocalStorage(key, data) {
  45. try {
  46. localStorage.setItem(key, JSON.stringify(data));
  47. } catch (error) {
  48. console.error('Error saving to localStorage:', error);
  49. }
  50. }
  51.  
  52. function loadFromLocalStorage(key) {
  53. try {
  54. const data = localStorage.getItem(key);
  55. return data ? JSON.parse(data) : null;
  56. } catch (error) {
  57. console.error('Error loading from localStorage:', error);
  58. return null;
  59. }
  60. }
  61.  
  62. function calculateTotalStats() {
  63.  
  64. const totals = {};
  65. for (const key in userStats) {
  66. totals[key] = parseFloat(userStats[key]) || 0;
  67. }
  68.  
  69. console.log('Starting calculation with base stats:', {...totals});
  70. console.log('Processing modifiers:', Object.keys(selectedModifiers).length, selectedModifiers);
  71.  
  72. const baseModifiers = {};
  73. const percentageStats = ['crit_chance', 'block_chance'];
  74.  
  75. Object.values(selectedModifiers).forEach(modifier => {
  76. if (modifier.name.startsWith('Base ') && !modifier.name.includes('MINUS')) {
  77. const words = modifier.name.split(' '); // Split into words
  78. let modKey = modifier.name.replace('Base ', '').toLowerCase().replace(/ /g, '_');
  79.  
  80. // If the second word is "Crit" and the third word is NOT "Chance", set it to crit_chance
  81. if (words.length >= 2 && words[1] === 'Crit' && words[2] !== 'Chance') {
  82. modKey = 'crit_damage';
  83. }
  84. const modValue = parseFloat(modifier.value);
  85.  
  86. if (!baseModifiers[modKey]) baseModifiers[modKey] = [];
  87.  
  88. if (percentageStats.includes(modKey)) {
  89. baseModifiers[modKey].push(Math.floor(modValue));
  90. } else {
  91.  
  92. baseModifiers[modKey].push(Math.floor(modValue));
  93. }
  94.  
  95. console.log(`Adding Base modifier: ${modifier.name} (${modValue}) from ${modifier.tree}:${modifier.anchor}`);
  96. }
  97. });
  98.  
  99. for (const [key, values] of Object.entries(baseModifiers)) {
  100.  
  101. if (percentageStats.includes(key)) {
  102. const totalValue = values.reduce((sum, val) => sum + val, 0);
  103. console.log(`Applying summed Base ${key} (percentage stat): ${totalValue}`);
  104.  
  105. if (totals[key] !== undefined) {
  106. totals[key] += totalValue;
  107. }
  108. } else {
  109.  
  110. const totalValue = Math.floor(values.reduce((sum, val) => sum + val, 0));
  111. console.log(`Applying summed Base ${key}: ${totalValue}`);
  112.  
  113. if (key === 'damage') {
  114.  
  115. if (totals['min_damage'] !== undefined) {
  116. totals['min_damage'] += totalValue;
  117. }
  118. if (totals['max_damage'] !== undefined) {
  119. totals['max_damage'] += totalValue;
  120. }
  121. } else if (key === 'block') {
  122.  
  123. if (totals['block_amount'] !== undefined) {
  124. totals['block_amount'] += totalValue;
  125. }
  126. } else {
  127.  
  128. if (totals[key] !== undefined) {
  129. totals[key] += totalValue;
  130. }
  131. }
  132. }
  133. }
  134.  
  135. const baseMinusModifiers = {
  136. 'breach_barrier': [],
  137. 'barrier_breach': [],
  138. 'health_damage': [],
  139. 'damage_health': []
  140. };
  141.  
  142. Object.values(selectedModifiers).forEach(modifier => {
  143. if (/^Base\s+/i.test(modifier.name) && /MINUS/i.test(modifier.name)) {
  144. const modStr = modifier.name.replace(/^Base\s+/i, '');
  145. const parts = modStr.split(/\s+MINUS\s+/i);
  146. if (parts.length !== 2) return;
  147.  
  148. const stat1 = parts[0].toLowerCase().replace(/ /g, '_');
  149. const stat2 = parts[1].toLowerCase().replace(/ /g, '_');
  150. const modValue = parseFloat(modifier.value);
  151.  
  152. const typeKey = `${stat1}_${stat2}`;
  153. if (baseMinusModifiers[typeKey] !== undefined) {
  154. baseMinusModifiers[typeKey].push({
  155. value: modValue,
  156. source: `${modifier.tree}:${modifier.anchor}`
  157. });
  158. console.log(`Collecting MINUS modifier: ${modifier.name} (${modValue}) from ${modifier.tree}:${modifier.anchor}`);
  159. }
  160. }
  161. });
  162.  
  163. if (baseMinusModifiers['breach_barrier'].length > 0) {
  164. const values = baseMinusModifiers['breach_barrier'].map(mod => Math.floor(mod.value));
  165. const totalValue = values.reduce((sum, val) => sum + val, 0);
  166. console.log(`Applying BREACH MINUS BARRIER: ${values.join(' + ')} = ${totalValue}`);
  167.  
  168. if (totals['breach'] !== undefined) totals['breach'] += totalValue;
  169. if (totals['barrier'] !== undefined) totals['barrier'] -= totalValue;
  170. }
  171.  
  172. if (baseMinusModifiers['barrier_breach'].length > 0) {
  173. const values = baseMinusModifiers['barrier_breach'].map(mod => Math.floor(mod.value));
  174. const totalValue = values.reduce((sum, val) => sum + val, 0);
  175. console.log(`Applying BARRIER MINUS BREACH: ${values.join(' + ')} = ${totalValue}`);
  176.  
  177. if (totals['breach'] !== undefined) totals['breach'] -= totalValue;
  178. if (totals['barrier'] !== undefined) totals['barrier'] += totalValue;
  179. }
  180.  
  181. if (baseMinusModifiers['health_damage'].length > 0) {
  182. const values = baseMinusModifiers['health_damage'].map(mod => Math.floor(mod.value));
  183. const totalValue = values.reduce((sum, val) => sum + val, 0);
  184. console.log(`Applying HEALTH MINUS DAMAGE: ${values.join(' + ')} = ${totalValue}`);
  185.  
  186. if (totals['health'] !== undefined) totals['health'] += totalValue;
  187. if (totals['min_damage'] !== undefined) totals['min_damage'] -= Math.floor(totalValue / 3);
  188. if (totals['max_damage'] !== undefined) totals['max_damage'] -= Math.floor(totalValue / 3);
  189. }
  190.  
  191. if (baseMinusModifiers['damage_health'].length > 0) {
  192. const values = baseMinusModifiers['damage_health'].map(mod => Math.floor(mod.value));
  193. const totalValue = values.reduce((sum, val) => sum + val, 0);
  194. console.log(`Applying DAMAGE MINUS HEALTH: ${values.join(' + ')} = ${totalValue}`);
  195.  
  196. if (totals['min_damage'] !== undefined) totals['min_damage'] += totalValue;
  197. if (totals['max_damage'] !== undefined) totals['max_damage'] += totalValue;
  198. if (totals['health'] !== undefined) totals['health'] -= Math.floor(totalValue * 3);
  199. }
  200. for (const key in totals) {
  201. if (totals[key] < 0) {
  202. console.log(`Fixing negative stat: ${key} from ${totals[key]} to 0`);
  203. totals[key] = 0;
  204. }
  205. }
  206.  
  207. const incSums = {};
  208.  
  209. Object.values(selectedModifiers).forEach(modifier => {
  210. if (/^Increase(d)?\s+/.test(modifier.name)) {
  211. const statKey = modifier.name.replace(/^Increase(d)?\s+/, '').toLowerCase().replace(/ /g, '_');
  212.  
  213. if (!incSums[statKey]) incSums[statKey] = 0;
  214.  
  215. const value = parseFloat(modifier.value);
  216. incSums[statKey] += value;
  217. console.log(`Adding Increased modifier: ${modifier.name} (${value}) from ${modifier.tree}:${modifier.anchor}`);
  218. }
  219. });
  220. for (const stat in incSums) {
  221.  
  222. const totalIncrease = incSums[stat];
  223.  
  224. const multiplier = 1 + totalIncrease / 100;
  225. console.log(`Applying Increased modifier for ${stat}: +${totalIncrease}% (x${multiplier.toFixed(2)})`);
  226.  
  227. if (stat === 'damage') {
  228. if (totals['min_damage'] !== undefined) {
  229.  
  230. totals['min_damage'] = Math.floor(totals['min_damage'] * multiplier);
  231. }
  232. if (totals['max_damage'] !== undefined) {
  233. totals['max_damage'] = Math.floor(totals['max_damage'] * multiplier);
  234. }
  235. } else if (stat === 'block') {
  236. if (totals['block_amount'] !== undefined) {
  237. totals['block_amount'] = Math.floor(totals['block_amount'] * multiplier);
  238. }
  239. } else {
  240. if (totals[stat] !== undefined) {
  241. totals[stat] = Math.floor(totals[stat] * multiplier);
  242. }
  243. }
  244. }
  245.  
  246. console.log('Final calculated stats:', {...totals});
  247.  
  248. const finalTotals = {};
  249. for (const key in totals) {
  250. finalTotals[key] = totals[key].toString();
  251. }
  252. return finalTotals;
  253. }
  254.  
  255. function logSelectedModifiers() {
  256. console.log('Currently selected modifiers:');
  257. Object.entries(selectedModifiers).forEach(([key, mod]) => {
  258. console.log(`${key}: ${mod.name} (${mod.value})`);
  259. });
  260. }
  261.  
  262. let monsterStats = {};
  263. let modifiersData = {};
  264. let userStats = {
  265. min_damage: 90,
  266. max_damage: 110,
  267. health: 800,
  268. breach: 120,
  269. barrier: 120,
  270. crit_chance: 5,
  271. crit_damage: 100,
  272. block_chance: 5,
  273. block_amount: 10
  274. };
  275. let autoMonsterStats = false;
  276. let currentFloor = 1;
  277.  
  278. let simulationResultsContainer = null;
  279.  
  280. function calculateDamageMultiplier(breach, barrier) {
  281. if (breach > barrier) {
  282. return 4 * (2 / Math.PI) * Math.atan((breach - barrier) / Math.sqrt(breach * barrier)) + 1;
  283. } else {
  284. return Math.max(0.25, (2 / Math.PI) * Math.atan((breach - barrier) / Math.sqrt(breach * barrier)) + 1);
  285. }
  286. }
  287.  
  288. function calculateDamage(attacker, defender) {
  289.  
  290. if (attacker === userStats) {
  291. attacker = calculateTotalStats();
  292. }
  293.  
  294. const baseDamage = (parseFloat(attacker.min_damage) + parseFloat(attacker.max_damage)) / 2;
  295. const multiplier = calculateDamageMultiplier(parseFloat(attacker.breach), parseFloat(defender.barrier));
  296. let damage = baseDamage * multiplier;
  297.  
  298. const critRoll = Math.random() * 100;
  299. if (critRoll <= parseFloat(attacker.crit_chance)) {
  300. damage = damage * (parseFloat(attacker.crit_damage) / 100);
  301. }
  302. const blockRoll = Math.random() * 100;
  303. if (blockRoll <= parseFloat(defender.block_chance)) {
  304. damage = Math.max(0, damage - parseFloat(defender.block_amount));
  305. }
  306.  
  307. return Math.round(damage);
  308. }
  309. const simulationLogOverlay = document.createElement('div');
  310. simulationLogOverlay.style.cssText = `
  311. position: absolute;
  312. top: 0;
  313. left: 0;
  314. right: 0;
  315. bottom: 0;
  316. background: rgba(22, 28, 36, 0.95);
  317. color: #ecf0f1;
  318. padding: 40px 20px 20px 20px;
  319. overflow-y: auto;
  320. display: none;
  321. z-index: 10000;
  322. box-sizing: border-box;
  323. `;
  324. simulationLogOverlay.id = 'simulation-log-overlay';
  325. const backButton = document.createElement('button');
  326. backButton.textContent = 'Back to results';
  327. backButton.style.cssText = `
  328. position: absolute;
  329. top: 10px;
  330. right: 10px;
  331. padding: 5px 10px;
  332. background: #e67e22;
  333. border: none;
  334. border-radius: 4px;
  335. color: white;
  336. cursor: pointer;
  337. `;
  338. backButton.addEventListener('click', () => {
  339. simulationLogOverlay.style.display = 'none';
  340. simulationResultsContainer.style.display = 'block';
  341. });
  342. simulationLogOverlay.appendChild(backButton);
  343.  
  344. function simulateCombat() {
  345. const startTime = performance.now();
  346.  
  347. const combatLog = document.createElement('div');
  348. combatLog.style.cssText = `
  349. margin-top: 20px;
  350. padding: 10px;
  351. background: rgba(44, 62, 80, 0.95);
  352. border-radius: 4px;
  353. max-height: 400px;
  354. overflow-y: auto;
  355. font-family: monospace;
  356. line-height: 1.4;
  357. `;
  358.  
  359. const totalUserStats = calculateTotalStats();
  360.  
  361. let userHealth = parseFloat(totalUserStats.health);
  362. let monsterHealth = parseFloat(monsterStats.health);
  363. let round = 1;
  364. let monsterDefeated = false;
  365.  
  366. function logAttack(attacker, defender, isUser) {
  367. const baseDamage = (parseFloat(attacker.min_damage) + parseFloat(attacker.max_damage)) / 2;
  368. const multiplier = calculateDamageMultiplier(parseFloat(attacker.breach), parseFloat(defender.barrier));
  369. let damage = baseDamage * multiplier;
  370.  
  371. const critRoll = Math.random() * 100;
  372. const isCrit = critRoll <= parseFloat(attacker.crit_chance);
  373. if (isCrit) {
  374. damage = damage * (parseFloat(attacker.crit_damage) / 100);
  375. }
  376. const blockRoll = Math.random() * 100;
  377. const isBlocked = blockRoll <= parseFloat(defender.block_chance);
  378. const originalDamage = damage;
  379. if (isBlocked) {
  380. damage = Math.max(0, damage - parseFloat(defender.block_amount));
  381. }
  382.  
  383. const color = isUser ? '#2ecc71' : '#e74c3c';
  384. const attacker_name = isUser ? 'You' : 'Monster';
  385. const defender_name = isUser ? 'Monster' : 'You';
  386.  
  387. let log = `<div style="color: ${color}; margin-bottom: 10px;">`;
  388. log += `<div><b>Round ${round} - ${attacker_name}'s Attack</b></div>`;
  389. log += `<div>Base Damage: ${Math.round(baseDamage)} (${attacker.min_damage}-${attacker.max_damage})</div>`;
  390. log += `<div>Breach vs Barrier: ${attacker.breach} vs ${defender.barrier} (x${multiplier.toFixed(2)})</div>`;
  391.  
  392. if (isCrit) {
  393. log += `<div>Critical Hit! (${attacker.crit_chance}% chance, +${attacker.crit_damage}% damage)</div>`;
  394. }
  395. if (isBlocked) {
  396. log += `<div>Attack Blocked! (${defender.block_chance}% chance, ${defender.block_amount} blocked)</div>`;
  397. log += `<div>Damage Reduced: ${Math.round(originalDamage)} ${Math.round(damage)}</div>`;
  398. }
  399.  
  400. const finalDamage = Math.round(damage);
  401. const newHealth = isUser ? Math.max(0, monsterHealth - finalDamage) : Math.max(0, userHealth - finalDamage);
  402. log += `<div><b>Final Damage: ${finalDamage}</b></div>`;
  403. log += `<div>${defender_name}'s Health: ${isUser ? monsterHealth : userHealth} ${newHealth}</div>`;
  404. log += '</div>';
  405.  
  406. combatLog.innerHTML += log;
  407. return finalDamage;
  408. }
  409.  
  410. while (userHealth > 0) {
  411. const userDamage = logAttack(totalUserStats, monsterStats, true);
  412. monsterHealth -= userDamage;
  413.  
  414. if (monsterHealth <= 0) {
  415. monsterDefeated = true;
  416. break;
  417. }
  418.  
  419. const monsterDamage = logAttack(monsterStats, totalUserStats, false);
  420. userHealth -= monsterDamage;
  421.  
  422. round++;
  423. }
  424.  
  425. const winner = monsterDefeated ? 'You win!' : 'Monster wins!';
  426. combatLog.innerHTML += `<div style="color: #f1c40f; margin-top: 10px; font-weight: bold; font-size: 1.2em;">${winner}</div>`;
  427. const endTime = performance.now();
  428. console.log(`Single simulation time: ${(endTime - startTime).toFixed(6)} ms`);
  429.  
  430. return combatLog;
  431. }
  432.  
  433. async function fetchModifiers() {
  434. try {
  435. const response = await fetch('https://elethor.com/game/neo-spire/modifiers');
  436. const data = await response.json();
  437. return data.modifiers || {};
  438. } catch (error) {
  439. console.error('Error fetching modifiers:', error);
  440. return null;
  441. }
  442. }
  443.  
  444. function createModifiersList(modifiers) {
  445. selectedModifiers = {};
  446. selectedModifiers = loadFromLocalStorage('selectedModifiers') || {};
  447.  
  448. const container = document.createElement('div');
  449.  
  450. container.style.cssText = `
  451. display: flex;
  452. gap: 20px;
  453. padding: 10px;
  454. font-family: Arial, sans-serif;
  455. color: #ecf0f1;
  456. max-height: 80vh;
  457. overflow-y: auto;
  458. width: 600px;
  459. `;
  460.  
  461. ['Defense', 'Offense'].forEach(tree => {
  462. const treeData = modifiers[tree];
  463. if (!treeData) return;
  464.  
  465. const treeSection = document.createElement('div');
  466. treeSection.style.cssText = `flex: 1; min-width: 0;`;
  467. treeSection.innerHTML = `<h3 style="color: #3498db; margin: 10px 0;">${tree}</h3>`;
  468.  
  469. Object.entries(treeData).forEach(([anchor, options]) => {
  470. const anchorSection = document.createElement('div');
  471. anchorSection.style.cssText = `
  472. margin-bottom: 15px;
  473. padding: 8px;
  474. background: rgba(44, 62, 80, 0.5);
  475. border-radius: 4px;
  476. `;
  477.  
  478. const headerContainer = document.createElement('div');
  479. headerContainer.style.cssText = `
  480. display: flex;
  481. justify-content: space-between;
  482. align-items: center;
  483. margin-bottom: 5px;
  484. `;
  485.  
  486. const anchorName = document.createElement('div');
  487. anchorName.style.cssText = `color: #f1c40f;`;
  488. anchorName.textContent = anchor;
  489. headerContainer.appendChild(anchorName);
  490.  
  491. const deselectButton = document.createElement('button');
  492. deselectButton.textContent = 'Deselect';
  493. deselectButton.style.cssText = `
  494. padding: 2px 6px;
  495. background: #e74c3c;
  496. border: none;
  497. border-radius: 3px;
  498. color: white;
  499. cursor: pointer;
  500. font-size: 0.8em;
  501. transition: background 0.2s;
  502. `;
  503. deselectButton.addEventListener('mouseenter', () => { deselectButton.style.background = '#c0392b'; });
  504. deselectButton.addEventListener('mouseleave', () => { deselectButton.style.background = '#e74c3c'; });
  505.  
  506. deselectButton.addEventListener('click', () => {
  507.  
  508. optionsContainer.querySelectorAll('input[type="checkbox"]').forEach(checkbox => {
  509. checkbox.checked = false;
  510. });
  511.  
  512. const compositeKey = `${tree}:${anchor}`;
  513. if (selectedModifiers[compositeKey]) {
  514. delete selectedModifiers[compositeKey];
  515. console.log(`Deselected modifier at ${compositeKey}`);
  516. console.log('Remaining selected modifiers:', {...selectedModifiers});
  517.  
  518. saveToLocalStorage('selectedModifiers', selectedModifiers);
  519. }
  520. });
  521. headerContainer.appendChild(deselectButton);
  522. anchorSection.appendChild(headerContainer);
  523.  
  524. const optionsContainer = document.createElement('div');
  525. optionsContainer.style.cssText = `
  526. display: flex;
  527. flex-wrap: wrap;
  528. gap: 10px;
  529. margin-top: 5px;
  530. `;
  531.  
  532. const radioGroupName = `${tree.toLowerCase()}-${anchor.replace(/\s+/g, '-').toLowerCase()}`;
  533.  
  534. options.forEach(option => {
  535. const optionDiv = document.createElement('div');
  536. optionDiv.style.cssText = `
  537. padding: 5px;
  538. border-left: 2px solid #3498db;
  539. padding-left: 8px;
  540. flex: 1;
  541. min-width: 150px;
  542. max-width: calc(50% - 5px);
  543. cursor: pointer;
  544. transition: background-color 0.3s;
  545. display: flex;
  546. align-items: center;
  547. `;
  548.  
  549. const checkbox = document.createElement('input');
  550. checkbox.type = 'checkbox';
  551. checkbox.style.marginRight = '8px';
  552. checkbox.dataset.tree = tree;
  553. checkbox.dataset.anchor = anchor;
  554. checkbox.dataset.optionIndex = options.indexOf(option);
  555.  
  556. checkbox.addEventListener('change', (e) => {
  557.  
  558. if (e.target.checked) {
  559. optionsContainer.querySelectorAll('input[type="checkbox"]').forEach(cb => {
  560. if (cb !== e.target) {
  561. cb.checked = false;
  562. }
  563. });
  564.  
  565. const compositeKey = `${tree}:${anchor}`;
  566. selectedModifiers[compositeKey] = { ...option, tree: tree, anchor: anchor };
  567.  
  568. optionsContainer.querySelectorAll('div').forEach(div => {
  569. div.style.backgroundColor = '';
  570. });
  571. optionDiv.style.backgroundColor = '#2980b9';
  572.  
  573. console.log('Selected modifier:', compositeKey, selectedModifiers[compositeKey]);
  574. } else {
  575.  
  576. const compositeKey = `${tree}:${anchor}`;
  577. if (selectedModifiers[compositeKey]) {
  578. delete selectedModifiers[compositeKey];
  579. optionDiv.style.backgroundColor = '';
  580. console.log(`Deselected modifier at ${compositeKey}`);
  581. }
  582. }
  583.  
  584. saveToLocalStorage('selectedModifiers', selectedModifiers);
  585.  
  586. console.log('All selected modifiers:', {...selectedModifiers});
  587. });
  588. optionDiv.appendChild(checkbox);
  589.  
  590. const textContainer = document.createElement('div');
  591. textContainer.style.cssText = 'flex: 1;';
  592. textContainer.innerHTML = `
  593. <div style="font-weight: bold;">${option.name}</div>
  594. <div style="color: #95a5a6; font-size: 0.9em;">Cost: ${option.cost} | Value: ${option.value}</div>
  595. `;
  596.  
  597. optionDiv.appendChild(textContainer);
  598.  
  599. optionDiv.addEventListener('click', (e) => {
  600.  
  601. if (e.target !== checkbox) {
  602. checkbox.checked = !checkbox.checked;
  603.  
  604. const changeEvent = new Event('change', { bubbles: true });
  605. checkbox.dispatchEvent(changeEvent);
  606. }
  607. });
  608.  
  609. optionsContainer.appendChild(optionDiv);
  610. });
  611.  
  612. anchorSection.appendChild(optionsContainer);
  613. treeSection.appendChild(anchorSection);
  614. });
  615.  
  616. container.appendChild(treeSection);
  617. });
  618.  
  619. const savedModifiers = loadFromLocalStorage('selectedModifiers') || {};
  620. Object.entries(savedModifiers).forEach(([compositeKey, modData]) => {
  621. const [tree, anchor] = compositeKey.split(':');
  622.  
  623. const options = modifiers[tree]?.[anchor] || [];
  624. const matchingOption = options.find(opt =>
  625. opt.name === modData.name &&
  626. opt.value === modData.value
  627. );
  628.  
  629. if (matchingOption) {
  630. const optionIndex = options.indexOf(matchingOption);
  631.  
  632. const checkbox = container.querySelector(
  633. `input[data-tree="${tree}"][data-anchor="${anchor}"][data-option-index="${optionIndex}"]`
  634. );
  635.  
  636. if (checkbox) {
  637. checkbox.checked = true;
  638.  
  639. const parentDiv = checkbox.closest('div');
  640. if (parentDiv) {
  641. parentDiv.style.backgroundColor = '#2980b9';
  642. }
  643.  
  644. selectedModifiers[compositeKey] = {
  645. ...matchingOption,
  646. tree: tree,
  647. anchor: anchor
  648. };
  649. }
  650. }
  651. });
  652.  
  653. return container;
  654. }
  655.  
  656. async function fetchMonsterStats(floor) {
  657. try {
  658. const response = await fetch(`https://elethor.com/game/neo-spire/${floor}`);
  659. const data = await response.json();
  660. if (data && data.floor && data.floor.stats) {
  661. return data.floor.stats;
  662. } else {
  663. console.error('Invalid data format from API');
  664. return null;
  665. }
  666. } catch (error) {
  667. console.error('Error fetching monster stats:', error);
  668. return null;
  669. }
  670. }
  671.  
  672.  
  673. function updateMonsterStatInputs(stats) {
  674. if (!stats) return;
  675. document.querySelectorAll('input[data-stat]').forEach(input => {
  676. if (input.dataset.type === 'monster') {
  677. const statKey = input.dataset.stat.toLowerCase().replace(/ /g, '_');
  678. if (stats[statKey] !== undefined) {
  679. input.value = stats[statKey];
  680. const event = new Event('input', { bubbles: true });
  681. input.dispatchEvent(event);
  682. }
  683. }
  684. });
  685. }
  686. function updateStatInputs() {
  687. document.querySelectorAll('input[data-stat]').forEach(input => {
  688. const statKey = input.dataset.stat.toLowerCase().replace(/ /g, '_');
  689.  
  690. if (input.dataset.type === 'monster') {
  691. if (monsterStats[statKey] !== undefined) {
  692. input.value = monsterStats[statKey];
  693. const event = new Event('input', { bubbles: true });
  694. input.dispatchEvent(event);
  695. }
  696. } else if (input.dataset.type === 'user') { // Update "You" stats
  697. if (userStats[statKey] !== undefined) {
  698. input.value = userStats[statKey];
  699. const event = new Event('input', { bubbles: true });
  700. input.dispatchEvent(event);
  701. }
  702. }
  703. });
  704. }
  705.  
  706.  
  707. function updateTotalStats() {
  708. const totalStats = calculateTotalStats();
  709. document.querySelectorAll('span[data-stat]').forEach(span => {
  710. const stat = span.dataset.stat.toLowerCase().replace(/ /g, '_');
  711. span.textContent = totalStats[stat] || '-';
  712. });
  713. }
  714.  
  715.  
  716. function createStatsTable() {
  717. const tableContainer = document.createElement('div');
  718. tableContainer.style.cssText = `
  719. padding: 10px;
  720. font-family: Arial, sans-serif;
  721. color: #ecf0f1;
  722. flex: 1;
  723. `;
  724. const table = document.createElement('table');
  725. table.style.cssText = `
  726. border-collapse: collapse;
  727. width: 100%;
  728. `;
  729.  
  730. const autoStatsContainer = document.createElement('div');
  731. autoStatsContainer.style.cssText = `
  732. display: flex;
  733. align-items: center;
  734. margin-bottom: 10px;
  735. padding: 8px;
  736. background: rgba(44, 62, 80, 0.5);
  737. border-radius: 4px;
  738. `;
  739. const autoStatsCheckbox = document.createElement('input');
  740. autoStatsCheckbox.type = 'checkbox';
  741. autoStatsCheckbox.id = 'auto-monster-stats';
  742. autoStatsCheckbox.style.cssText = `margin-right: 8px;`;
  743. const autoStatsLabel = document.createElement('label');
  744. autoStatsLabel.htmlFor = 'auto-monster-stats';
  745. autoStatsLabel.textContent = 'Auto Monster Stats';
  746. autoStatsLabel.style.cssText = `margin-right: 15px; color: #3498db;`;
  747. const floorLabel = document.createElement('label');
  748. floorLabel.htmlFor = 'floor-input';
  749. floorLabel.textContent = 'Floor:';
  750. floorLabel.style.cssText = `margin-right: 8px; color: #3498db;`;
  751. const floorInput = document.createElement('input');
  752. floorInput.type = 'number';
  753. floorInput.id = 'floor-input';
  754. floorInput.min = '1';
  755. floorInput.value = '1';
  756. floorInput.style.cssText = `
  757. width: 60px;
  758. background: #2c3e50;
  759. border: 1px solid #34495e;
  760. color: #ecf0f1;
  761. padding: 4px;
  762. border-radius: 3px;
  763. `;
  764. const fetchButton = document.createElement('button');
  765. fetchButton.textContent = 'Fetch';
  766. fetchButton.style.cssText = `
  767. margin-left: 8px;
  768. padding: 4px 8px;
  769. background: #3498db;
  770. border: none;
  771. border-radius: 4px;
  772. color: white;
  773. cursor: pointer;
  774. font-weight: bold;
  775. transition: background 0.3s;
  776. `;
  777. fetchButton.addEventListener('mouseenter', () => { fetchButton.style.background = '#2980b9'; });
  778. fetchButton.addEventListener('mouseleave', () => { fetchButton.style.background = '#3498db'; });
  779. autoStatsCheckbox.addEventListener('change', async (e) => {
  780. autoMonsterStats = e.target.checked;
  781. document.querySelectorAll('input[data-type="monster"]').forEach(input => {
  782. input.disabled = autoMonsterStats;
  783. input.style.opacity = autoMonsterStats ? '0.5' : '1';
  784. });
  785. if (autoMonsterStats) {
  786. currentFloor = parseInt(floorInput.value) || 1;
  787. const stats = await fetchMonsterStats(currentFloor);
  788. if (stats) { updateMonsterStatInputs(stats); }
  789. }
  790. });
  791. floorInput.addEventListener('input', (e) => { currentFloor = parseInt(e.target.value) || 1; });
  792. fetchButton.addEventListener('click', async () => {
  793. if (autoMonsterStats) {
  794. currentFloor = parseInt(floorInput.value) || 1;
  795. const stats = await fetchMonsterStats(currentFloor);
  796. if (stats) { updateMonsterStatInputs(stats); }
  797. }
  798. });
  799. autoStatsContainer.appendChild(autoStatsCheckbox);
  800. autoStatsContainer.appendChild(autoStatsLabel);
  801. autoStatsContainer.appendChild(floorLabel);
  802. autoStatsContainer.appendChild(floorInput);
  803. autoStatsContainer.appendChild(fetchButton);
  804. tableContainer.appendChild(autoStatsContainer);
  805.  
  806. const simulationControls = document.createElement('div');
  807. simulationControls.style.cssText = 'margin-bottom: 10px; display: flex; align-items: center; gap: 10px;';
  808. const numSimLabel = document.createElement('label');
  809. numSimLabel.htmlFor = 'num-simulations';
  810. numSimLabel.textContent = 'Number of simulations:';
  811. numSimLabel.style.cssText = 'color: #3498db;';
  812. simulationControls.appendChild(numSimLabel);
  813. const numSimInput = document.createElement('input');
  814. numSimInput.type = 'number';
  815. numSimInput.id = 'num-simulations';
  816. numSimInput.value = '10';
  817. numSimInput.style.cssText = `
  818. width: 60px;
  819. background: #2c3e50;
  820. border: 1px solid #34495e;
  821. color: #ecf0f1;
  822. padding: 4px;
  823. border-radius: 3px;
  824. `;
  825. simulationControls.appendChild(numSimInput);
  826. tableContainer.appendChild(simulationControls);
  827.  
  828. const header = document.createElement('tr');
  829. ['STAT', 'MONSTER', 'YOU', 'TOTAL YOU'].forEach(text => {
  830. const th = document.createElement('th');
  831. th.textContent = text;
  832. th.style.cssText = `
  833. padding: 8px;
  834. text-align: left;
  835. border-bottom: 2px solid #34495e;
  836. color: #3498db;
  837. `;
  838. header.appendChild(th);
  839. });
  840. table.appendChild(header);
  841.  
  842. stats.forEach(stat => {
  843. const row = document.createElement('tr');
  844. const nameCell = document.createElement('td');
  845. nameCell.textContent = stat;
  846. nameCell.style.cssText = `padding: 8px; border-bottom: 1px solid #34495e;`;
  847. const monsterCell = document.createElement('td');
  848. const monsterInput = document.createElement('input');
  849. monsterInput.type = 'text';
  850. monsterInput.dataset.stat = stat;
  851. monsterInput.dataset.type = 'monster';
  852. monsterInput.addEventListener('input', (e) => {
  853. const value = e.target.value;
  854. const statKey = stat.toLowerCase().replace(/ /g, '_');
  855. monsterStats[statKey] = value;
  856.  
  857. saveToLocalStorage('monsterStats', monsterStats);
  858. console.log('Monster stats updated:', monsterStats);
  859. });
  860.  
  861. monsterInput.style.cssText = `
  862. width: 80px;
  863. background: #2c3e50;
  864. border: 1px solid #34495e;
  865. color: #ecf0f1;
  866. padding: 4px;
  867. border-radius: 3px;
  868. `;
  869. monsterCell.appendChild(monsterInput);
  870. monsterCell.style.cssText = `padding: 8px; border-bottom: 1px solid #34495e;`;
  871. const yourCell = document.createElement('td');
  872. const yourInput = document.createElement('input');
  873. yourInput.type = 'text';
  874. yourInput.dataset.stat = stat;
  875. yourInput.addEventListener('input', (e) => {
  876. const value = e.target.value;
  877. const statKey = stat.toLowerCase().replace(/ /g, '_');
  878. userStats[statKey] = value;
  879.  
  880. console.log('User stats updated:', userStats);
  881. });
  882. yourInput.style.cssText = `
  883. width: 80px;
  884. background: #2c3e50;
  885. border: 1px solid #34495e;
  886. color: #ecf0f1;
  887. padding: 4px;
  888. border-radius: 3px;
  889. `;
  890. yourCell.appendChild(yourInput);
  891. yourCell.style.cssText = `padding: 8px; border-bottom: 1px solid #34495e;`;
  892. row.appendChild(nameCell);
  893. row.appendChild(monsterCell);
  894. row.appendChild(yourCell);
  895. const totalCell = document.createElement('td');
  896. const totalSpan = document.createElement('span');
  897. totalSpan.dataset.stat = stat;
  898. totalSpan.textContent = '-';
  899. totalSpan.style.cssText = `color: #2ecc71; font-weight: bold;`;
  900. totalCell.appendChild(totalSpan);
  901. totalCell.style.cssText = `padding: 8px; border-bottom: 1px solid #34495e;`;
  902. row.appendChild(totalCell);
  903. row.addEventListener('mouseenter', () => { row.style.backgroundColor = 'rgba(52, 73, 94, 0.5)'; });
  904. row.addEventListener('mouseleave', () => { row.style.backgroundColor = ''; });
  905. table.appendChild(row);
  906. });
  907. tableContainer.appendChild(table);
  908.  
  909. const simulateButton = document.createElement('button');
  910. simulateButton.textContent = 'Simulate Combat';
  911. simulateButton.style.cssText = `
  912. margin-top: 10px;
  913. padding: 8px 16px;
  914. background: #2ecc71;
  915. border: none;
  916. border-radius: 4px;
  917. color: white;
  918. cursor: pointer;
  919. width: 100%;
  920. font-weight: bold;
  921. transition: background 0.3s;
  922. `;
  923. simulateButton.addEventListener('mouseenter', () => { simulateButton.style.background = '#27ae60'; });
  924. simulateButton.addEventListener('mouseleave', () => { simulateButton.style.background = '#2ecc71'; });
  925. simulateButton.addEventListener('click', () => {
  926.  
  927. simulationResultsContainer.innerHTML = '';
  928. simulationLogs = [];
  929. monsterWins = 0;
  930. userWins = 0;
  931. const totalUserStats = calculateTotalStats();
  932.  
  933. const numSims = parseInt(document.getElementById('num-simulations').value) || 1;
  934. for (let i = 0; i < numSims; i++) {
  935. const combatLog = simulateCombat();
  936.  
  937. combatLog.style.display = 'none';
  938. combatLog.id = `combat-log-${i}`;
  939. simulationLogs.push(combatLog);
  940.  
  941. const finalLine = combatLog.querySelector('div[style*="color: #f1c40f"]');
  942. const resultText = finalLine ? finalLine.textContent : 'Unknown';
  943. if (resultText === 'You win!') {
  944. userWins++;
  945. } else if (resultText === 'Monster wins!') {
  946. monsterWins++;
  947. }
  948. const simResultDiv = document.createElement('div');
  949. simResultDiv.style.cssText = 'margin-bottom: 10px; padding: 5px; background: rgba(44, 62, 80, 0.5); border-radius: 4px;';
  950. simResultDiv.innerHTML = `<strong>Simulation ${i + 1}:</strong> ${resultText}`;
  951. const logsButton = document.createElement('button');
  952. logsButton.textContent = 'Logs';
  953. logsButton.style.cssText = 'margin-left: 10px; padding: 2px 6px;';
  954. logsButton.addEventListener('click', () => {
  955. simulationResultsContainer.style.display = 'none';
  956. simulationLogOverlay.innerHTML = '';
  957. simulationLogOverlay.appendChild(backButton);
  958. simulationLogOverlay.appendChild(simulationLogs[i]);
  959. simulationLogs[i].style.display = 'block';
  960. simulationLogOverlay.style.display = 'block';
  961. });
  962. simResultDiv.appendChild(logsButton);
  963. simulationResultsContainer.appendChild(simResultDiv);
  964. }
  965. const statsDiv = document.createElement('div');
  966. statsDiv.style.cssText = `
  967. margin-bottom: 15px;
  968. padding: 10px;
  969. background: rgba(44, 62, 80, 0.8);
  970. border-radius: 4px;
  971. font-weight: bold;
  972. display: flex;
  973. justify-content: space-between;
  974. `;
  975.  
  976. const winrate = (userWins / numSims * 100).toFixed(2);
  977.  
  978. statsDiv.innerHTML = `
  979. <div style="color: #e74c3c;">Monster Wins: ${monsterWins}</div>
  980. <div style="color: #2ecc71;">Your Wins: ${userWins}</div>
  981. <div style="color: #f1c40f;">Winrate: ${winrate}%</div>
  982. `;
  983.  
  984.  
  985. simulationResultsContainer.insertBefore(statsDiv, simulationResultsContainer.firstChild);
  986. });
  987. tableContainer.appendChild(simulateButton);
  988.  
  989. const confirmButton = document.createElement('button');
  990. confirmButton.textContent = 'Confirm Modifiers';
  991. confirmButton.style.cssText = `
  992. margin-top: 10px;
  993. margin-bottom: 10px;
  994. padding: 8px 16px;
  995. background: #e67e22;
  996. border: none;
  997. border-radius: 4px;
  998. color: white;
  999. cursor: pointer;
  1000. width: 100%;
  1001. font-weight: bold;
  1002. transition: background 0.3s;
  1003. `;
  1004. confirmButton.addEventListener('mouseenter', () => { confirmButton.style.background = '#d35400'; });
  1005. confirmButton.addEventListener('mouseleave', () => { confirmButton.style.background = '#e67e22'; });
  1006. confirmButton.addEventListener('click', () => {
  1007. logSelectedModifiers();
  1008. updateTotalStats();
  1009. });
  1010. tableContainer.appendChild(confirmButton);
  1011.  
  1012. let isDragging = false, currentX, currentY, initialX, initialY, xOffset = 0, yOffset = 0;
  1013. tableContainer.addEventListener('mousedown', dragStart);
  1014. document.addEventListener('mousemove', drag);
  1015. document.addEventListener('mouseup', dragEnd);
  1016. function dragStart(e) {
  1017. initialX = e.clientX - xOffset;
  1018. initialY = e.clientY - yOffset;
  1019. if (e.target === tableContainer) { isDragging = true; }
  1020. }
  1021. function drag(e) {
  1022. if (isDragging) {
  1023. e.preventDefault();
  1024. currentX = e.clientX - initialX;
  1025. currentY = e.clientY - initialY;
  1026. xOffset = currentX;
  1027. yOffset = currentY;
  1028. setTranslate(currentX, currentY, tableContainer);
  1029. }
  1030. }
  1031. function setTranslate(xPos, yPos, el) {
  1032. el.style.transform = `translate3d(${xPos}px, ${yPos}px, 0)`;
  1033. }
  1034. function dragEnd() {
  1035. initialX = currentX;
  1036. initialY = currentY;
  1037. isDragging = false;
  1038. }
  1039.  
  1040. document.body.appendChild(tableContainer);
  1041. }
  1042.  
  1043. window.addEventListener('load', async () => {
  1044. const mainContainer = document.createElement('div');
  1045. const closeButton = document.createElement('button');
  1046. closeButton.textContent = 'X';
  1047. closeButton.style.cssText = `
  1048. position: absolute;
  1049. top: 10px;
  1050. right: 10px;
  1051. padding: 5px 10px;
  1052. background: #e74c3c;
  1053. border: none;
  1054. border-radius: 4px;
  1055. color: white;
  1056. cursor: pointer;
  1057. font-weight: bold;
  1058. z-index: 10001;
  1059. `;
  1060. closeButton.addEventListener('click', () => {
  1061. mainContainer.style.display = 'none';
  1062. showOverlayButton.style.display = 'block';
  1063. });
  1064.  
  1065. mainContainer.appendChild(closeButton);
  1066. const showOverlayButton = document.createElement('button');
  1067. showOverlayButton.textContent = 'Open Spire Simulator';
  1068. showOverlayButton.style.cssText = `
  1069. position: fixed;
  1070. top: 10px;
  1071. right: 10px;
  1072. padding: 5px 10px;
  1073. background: #2ecc71;
  1074. border: none;
  1075. border-radius: 4px;
  1076. color: white;
  1077. cursor: pointer;
  1078. font-weight: bold;
  1079. z-index: 9998;
  1080. display: none;
  1081. `;
  1082. showOverlayButton.addEventListener('click', () => {
  1083. mainContainer.style.display = 'block';
  1084. showOverlayButton.style.display = 'none';
  1085. });
  1086. document.body.appendChild(showOverlayButton);
  1087.  
  1088. const savedUserStats = {};
  1089. const savedMonsterStats = loadFromLocalStorage('monsterStats') || {};
  1090.  
  1091. userStats = savedUserStats;
  1092. monsterStats = savedMonsterStats;
  1093.  
  1094. mainContainer.style.cssText = `
  1095. position: fixed;
  1096. top: 0;
  1097. left: 0;
  1098. right: 0;
  1099. bottom: 0;
  1100. background: rgba(22, 28, 36, 0.95);
  1101. z-index: 9999;
  1102. box-shadow: 0 0 10px rgba(0,0,0,0.5);
  1103. overflow: auto;
  1104. `;
  1105.  
  1106. const contentContainer = document.createElement('div');
  1107. contentContainer.style.cssText = `
  1108. display: flex;
  1109. flex-direction: row;
  1110. gap: 20px;
  1111. padding: 20px;
  1112. `;
  1113.  
  1114. simulationResultsContainer = document.createElement('div');
  1115. simulationResultsContainer.id = 'simulation-results';
  1116. simulationResultsContainer.style.cssText = `
  1117. flex: 1;
  1118. background: rgba(22, 28, 36, 0.95);
  1119. color: #ecf0f1;
  1120. padding: 10px;
  1121. border: 1px solid #2c3e50;
  1122. border-radius: 4px;
  1123. max-height: 400px;
  1124. overflow-y: auto;
  1125. `;
  1126. contentContainer.appendChild(simulationResultsContainer);
  1127.  
  1128. createStatsTable();
  1129. document.querySelectorAll('input[data-stat]').forEach(input => {
  1130. const statKey = input.dataset.stat.toLowerCase().replace(/ /g, '_');
  1131.  
  1132. if (input.dataset.type === 'monster' && monsterStats[statKey] !== undefined) {
  1133. input.value = monsterStats[statKey];
  1134. const event = new Event('input', { bubbles: true });
  1135. input.dispatchEvent(event);
  1136. }
  1137.  
  1138. if (input.dataset.type === 'user' && userStats[statKey] !== undefined) {
  1139. input.value = userStats[statKey];
  1140. const event = new Event('input', { bubbles: true });
  1141. input.dispatchEvent(event);
  1142. }
  1143. });
  1144.  
  1145. const statsTable = document.querySelector('div[style*="font-family: Arial"]');
  1146. if (statsTable) {
  1147. contentContainer.appendChild(statsTable);
  1148. }
  1149.  
  1150. const modifiers = await fetchModifiers();
  1151. if (modifiers) {
  1152. modifiersData = modifiers;
  1153. const modifiersList = createModifiersList(modifiers);
  1154. contentContainer.appendChild(modifiersList);
  1155. }
  1156.  
  1157. mainContainer.appendChild(contentContainer);
  1158.  
  1159. document.body.appendChild(simulationLogOverlay);
  1160.  
  1161. document.body.appendChild(mainContainer);
  1162.  
  1163. });
  1164. })();