// ==UserScript==
// @name Elethor Stats Comparison
// @namespace http://tampermonkey.net/
// @version 1.0
// @description Spire simulator
// @author Eugene
// @match https://elethor.com/*
// @grant none
// @license GPL-3.0-or-later
// ==/UserScript==
/*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
(function() {
'use strict';
const stats = [
'MIN DAMAGE',
'MAX DAMAGE',
'HEALTH',
'BREACH',
'BARRIER',
'CRIT CHANCE',
'CRIT DAMAGE',
'BLOCK CHANCE',
'BLOCK AMOUNT'
];
let simulationLogs = []; // Store combat log elements for each simulation
// Object to store selected modifiers
let selectedModifiers = {};
// Functions to handle localStorage
function saveToLocalStorage(key, data) {
try {
localStorage.setItem(key, JSON.stringify(data));
} catch (error) {
console.error('Error saving to localStorage:', error);
}
}
function loadFromLocalStorage(key) {
try {
const data = localStorage.getItem(key);
return data ? JSON.parse(data) : null;
} catch (error) {
console.error('Error loading from localStorage:', error);
return null;
}
}
// Function to calculate final stats after confirm modifiers is pressed.
// It processes modifiers in the following order for each stat:
// 1. Add up all Base [stat] modifiers (floored) and add to starting value.
// 2. Apply Base [firstStat] MINUS [secondStat] modifiers if applicable.
// 3. Sum all Increased [stat] modifiers, floor that sum, and then multiply by (1 + (floored sum)/100).
// 4. Floor the final result.
function calculateTotalStats() {
// Create a copy of userStats (as numbers) so we can work with each key
const totals = {};
for (const key in userStats) {
totals[key] = parseFloat(userStats[key]) || 0;
}
console.log('Starting calculation with base stats:', {...totals});
console.log('Processing modifiers:', Object.keys(selectedModifiers).length, selectedModifiers);
// --- Step 1: Process Base [stat] modifiers (excluding MINUS) ---
Object.values(selectedModifiers).forEach(modifier => {
if (modifier.name.startsWith('Base ') && !modifier.name.includes('MINUS')) {
// Determine the stat key from the modifier
const modKey = modifier.name.replace('Base ', '').toLowerCase().replace(/ /g, '_');
const addValue = Math.floor(parseFloat(modifier.value));
console.log(`Applying Base modifier: ${modifier.name} (${addValue}) from ${modifier.tree}:${modifier.anchor}`);
if (modKey === 'damage') {
// For damage, add to both min_damage and max_damage
if (totals['min_damage'] !== undefined) {
totals['min_damage'] += addValue;
}
if (totals['max_damage'] !== undefined) {
totals['max_damage'] += addValue;
}
} else if (modKey === 'block') {
// For Base Block, assume it applies to block_amount
if (totals['block_amount'] !== undefined) {
totals['block_amount'] += addValue;
}
} else {
// Otherwise, add to the matching stat if it exists
if (totals[modKey] !== undefined) {
totals[modKey] += addValue;
}
}
}
});
// --- Step 2: Apply Base [stat1] MINUS [stat2] Modifiers ---
Object.values(selectedModifiers).forEach(modifier => {
if (/^Base\s+/i.test(modifier.name) && /MINUS/i.test(modifier.name)) {
const modStr = modifier.name.replace(/^Base\s+/i, '');
const parts = modStr.split(/\s+MINUS\s+/i);
if (parts.length !== 2) return;
const stat1 = parts[0].toLowerCase().replace(/ /g, '_');
const stat2 = parts[1].toLowerCase().replace(/ /g, '_');
const modValue = Math.floor(parseFloat(modifier.value));
console.log(`Applying MINUS modifier: ${modifier.name} (${modValue}) from ${modifier.tree}:${modifier.anchor}`);
if (stat1 === 'breach' && stat2 === 'barrier') {
if (totals['breach'] !== undefined) totals['breach'] += modValue;
if (totals['barrier'] !== undefined) totals['barrier'] -= modValue;
} else if (stat1 === 'barrier' && stat2 === 'breach') {
if (totals['breach'] !== undefined) totals['breach'] -= modValue;
if (totals['barrier'] !== undefined) totals['barrier'] += modValue;
} else if (stat1 === 'health' && stat2 === 'damage') {
if (totals['health'] !== undefined) totals['health'] += modValue;
if (totals['min_damage'] !== undefined) totals['min_damage'] -= Math.floor(modValue / 3);
if (totals['max_damage'] !== undefined) totals['max_damage'] -= Math.floor(modValue / 3);
} else if (stat1 === 'damage' && stat2 === 'health') {
if (totals['min_damage'] !== undefined) totals['min_damage'] += modValue;
if (totals['max_damage'] !== undefined) totals['max_damage'] += modValue;
if (totals['health'] !== undefined) totals['health'] -= Math.floor(modValue * 3);
}
}
});
// --- Step 3: Process Increased [stat] modifiers ---
const incSums = {}; // e.g., incSums['health'] = total increased percentage (before dividing by 100)
Object.values(selectedModifiers).forEach(modifier => {
if (/^Increase(d)?\s+/.test(modifier.name)) {
const statKey = modifier.name.replace(/^Increase(d)?\s+/, '').toLowerCase().replace(/ /g, '_');
// Sum raw values
incSums[statKey] = (incSums[statKey] || 0) + parseFloat(modifier.value);
console.log(`Adding Increased modifier: ${modifier.name} (${modifier.value}) from ${modifier.tree}:${modifier.anchor}`);
}
});
// Now apply increased modifiers for each stat
for (const stat in incSums) {
// Floor the sum for the increased modifiers
const totalIncrease = Math.floor(incSums[stat]);
// Compute multiplier
const multiplier = 1 + totalIncrease / 100;
console.log(`Applying Increased modifier for ${stat}: +${totalIncrease}% (x${multiplier.toFixed(2)})`);
// Special case: if stat is "damage" then apply to both min_damage and max_damage
if (stat === 'damage') {
if (totals['min_damage'] !== undefined) {
totals['min_damage'] = Math.floor(totals['min_damage'] * multiplier);
}
if (totals['max_damage'] !== undefined) {
totals['max_damage'] = Math.floor(totals['max_damage'] * multiplier);
}
} else if (stat === 'block') {
if (totals['block_amount'] !== undefined) {
totals['block_amount'] = Math.floor(totals['block_amount'] * multiplier);
}
} else {
if (totals[stat] !== undefined) {
totals[stat] = Math.floor(totals[stat] * multiplier);
}
}
}
console.log('Final calculated stats:', {...totals});
// Return the totals as strings (if needed)
const finalTotals = {};
for (const key in totals) {
finalTotals[key] = totals[key].toString();
}
return finalTotals;
}
function logSelectedModifiers() {
console.log('Currently selected modifiers:');
Object.entries(selectedModifiers).forEach(([key, mod]) => {
console.log(`${key}: ${mod.name} (${mod.value})`);
});
}
// Object to store all stat values
let monsterStats = {};
let modifiersData = {};
let userStats = {};
let autoMonsterStats = false;
let currentFloor = 1;
// We'll also use this container for simulation results.
let simulationResultsContainer = null;
function calculateDamageMultiplier(breach, barrier) {
if (breach > barrier) {
return 4 * (2 / Math.PI) * Math.atan((breach - barrier) / Math.sqrt(breach * barrier)) + 1;
} else {
return Math.max(0.25, (2 / Math.PI) * Math.atan((breach - barrier) / Math.sqrt(breach * barrier)) + 1);
}
}
function calculateDamage(attacker, defender) {
// If attacker is the player, use totalUserStats
if (attacker === userStats) {
attacker = calculateTotalStats();
}
const baseDamage = (parseFloat(attacker.min_damage) + parseFloat(attacker.max_damage)) / 2;
const multiplier = calculateDamageMultiplier(parseFloat(attacker.breach), parseFloat(defender.barrier));
let damage = baseDamage * multiplier;
const critRoll = Math.random() * 100;
if (critRoll <= parseFloat(attacker.crit_chance)) {
damage = damage * (parseFloat(attacker.crit_damage) / 100);
}
const blockRoll = Math.random() * 100;
if (blockRoll <= parseFloat(defender.block_chance)) {
damage = Math.max(0, damage - parseFloat(defender.block_amount));
}
return Math.round(damage);
}
const simulationLogOverlay = document.createElement('div');
simulationLogOverlay.style.cssText = `
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(22, 28, 36, 0.95);
color: #ecf0f1;
padding: 40px 20px 20px 20px;
overflow-y: auto;
display: none;
z-index: 10000;
box-sizing: border-box;
`;
simulationLogOverlay.id = 'simulation-log-overlay';
const backButton = document.createElement('button');
backButton.textContent = 'Back to results';
backButton.style.cssText = `
position: absolute;
top: 10px;
right: 10px;
padding: 5px 10px;
background: #e67e22;
border: none;
border-radius: 4px;
color: white;
cursor: pointer;
`;
backButton.addEventListener('click', () => {
simulationLogOverlay.style.display = 'none';
simulationResultsContainer.style.display = 'block';
});
simulationLogOverlay.appendChild(backButton);
// Modified simulateCombat that returns the combat log element.
function simulateCombat() {
const combatLog = document.createElement('div');
combatLog.style.cssText = `
margin-top: 20px;
padding: 10px;
background: rgba(44, 62, 80, 0.95);
border-radius: 4px;
max-height: 400px;
overflow-y: auto;
font-family: monospace;
line-height: 1.4;
`;
// Get the total stats with all modifiers applied
const totalUserStats = calculateTotalStats();
let userHealth = parseFloat(totalUserStats.health);
let monsterHealth = parseFloat(monsterStats.health);
let round = 1;
let monsterDefeated = false;
function logAttack(attacker, defender, isUser) {
const baseDamage = (parseFloat(attacker.min_damage) + parseFloat(attacker.max_damage)) / 2;
const multiplier = calculateDamageMultiplier(parseFloat(attacker.breach), parseFloat(defender.barrier));
let damage = baseDamage * multiplier;
const critRoll = Math.random() * 100;
const isCrit = critRoll <= parseFloat(attacker.crit_chance);
if (isCrit) {
damage = damage * (parseFloat(attacker.crit_damage) / 100);
}
const blockRoll = Math.random() * 100;
const isBlocked = blockRoll <= parseFloat(defender.block_chance);
const originalDamage = damage;
if (isBlocked) {
damage = Math.max(0, damage - parseFloat(defender.block_amount));
}
const color = isUser ? '#2ecc71' : '#e74c3c';
const attacker_name = isUser ? 'You' : 'Monster';
const defender_name = isUser ? 'Monster' : 'You';
let log = `<div style="color: ${color}; margin-bottom: 10px;">`;
log += `<div><b>Round ${round} - ${attacker_name}'s Attack</b></div>`;
log += `<div>Base Damage: ${Math.round(baseDamage)} (${attacker.min_damage}-${attacker.max_damage})</div>`;
log += `<div>Breach vs Barrier: ${attacker.breach} vs ${defender.barrier} (x${multiplier.toFixed(2)})</div>`;
if (isCrit) {
log += `<div>Critical Hit! (${attacker.crit_chance}% chance, +${attacker.crit_damage}% damage)</div>`;
}
if (isBlocked) {
log += `<div>Attack Blocked! (${defender.block_chance}% chance, ${defender.block_amount} blocked)</div>`;
log += `<div>Damage Reduced: ${Math.round(originalDamage)} → ${Math.round(damage)}</div>`;
}
const finalDamage = Math.round(damage);
const newHealth = isUser ? Math.max(0, monsterHealth - finalDamage) : Math.max(0, userHealth - finalDamage);
log += `<div><b>Final Damage: ${finalDamage}</b></div>`;
log += `<div>${defender_name}'s Health: ${isUser ? monsterHealth : userHealth} → ${newHealth}</div>`;
log += '</div>';
combatLog.innerHTML += log;
return finalDamage;
}
// Use totalUserStats for the player's attacks and defense
while (userHealth > 0) {
const userDamage = logAttack(totalUserStats, monsterStats, true);
monsterHealth -= userDamage;
if (monsterHealth <= 0) {
monsterDefeated = true;
break;
}
const monsterDamage = logAttack(monsterStats, totalUserStats, false);
userHealth -= monsterDamage;
round++;
}
const winner = monsterDefeated ? 'You win!' : 'Monster wins!';
combatLog.innerHTML += `<div style="color: #f1c40f; margin-top: 10px; font-weight: bold; font-size: 1.2em;">${winner}</div>`;
return combatLog;
}
// Function to fetch modifiers data from API
async function fetchModifiers() {
try {
const response = await fetch('https://elethor.com/game/neo-spire/modifiers');
const data = await response.json();
return data.modifiers || {};
} catch (error) {
console.error('Error fetching modifiers:', error);
return null;
}
}
// Function to create modifiers list (unchanged)
// Modify the createModifiersList function to add Deselect buttons
function createModifiersList(modifiers) {
const savedModifiers = loadFromLocalStorage('selectedModifiers') || {};
selectedModifiers = savedModifiers;
const container = document.createElement('div');
container.style.cssText = `
display: flex;
gap: 20px;
padding: 10px;
font-family: Arial, sans-serif;
color: #ecf0f1;
max-height: 80vh;
overflow-y: auto;
width: 600px;
`;
['Defense', 'Offense'].forEach(tree => {
const treeData = modifiers[tree];
if (!treeData) return;
const treeSection = document.createElement('div');
treeSection.style.cssText = `flex: 1; min-width: 0;`;
treeSection.innerHTML = `<h3 style="color: #3498db; margin: 10px 0;">${tree}</h3>`;
Object.entries(treeData).forEach(([anchor, options]) => {
const anchorSection = document.createElement('div');
anchorSection.style.cssText = `
margin-bottom: 15px;
padding: 8px;
background: rgba(44, 62, 80, 0.5);
border-radius: 4px;
`;
// Create header container for anchor name and deselect button
const headerContainer = document.createElement('div');
headerContainer.style.cssText = `
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 5px;
`;
// Add anchor name
const anchorName = document.createElement('div');
anchorName.style.cssText = `color: #f1c40f;`;
anchorName.textContent = anchor;
headerContainer.appendChild(anchorName);
// Add deselect button
const deselectButton = document.createElement('button');
deselectButton.textContent = 'Deselect';
deselectButton.style.cssText = `
padding: 2px 6px;
background: #e74c3c;
border: none;
border-radius: 3px;
color: white;
cursor: pointer;
font-size: 0.8em;
transition: background 0.2s;
`;
deselectButton.addEventListener('mouseenter', () => { deselectButton.style.background = '#c0392b'; });
deselectButton.addEventListener('mouseleave', () => { deselectButton.style.background = '#e74c3c'; });
// Add click event for deselect button
deselectButton.addEventListener('click', () => {
// Deselect all options in this anchor
optionsContainer.querySelectorAll('div').forEach(div => {
div.style.backgroundColor = '';
div.style.color = '#ecf0f1';
});
// Remove this anchor's selection from selectedModifiers
const compositeKey = `${tree}:${anchor}`;
if (selectedModifiers[compositeKey]) {
delete selectedModifiers[compositeKey];
console.log(`Deselected modifier at ${compositeKey}`);
console.log('Remaining selected modifiers:', {...selectedModifiers});
saveToLocalStorage('selectedModifiers', selectedModifiers);
}
});
headerContainer.appendChild(deselectButton);
anchorSection.appendChild(headerContainer);
const optionsContainer = document.createElement('div');
optionsContainer.style.cssText = `
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 5px;
`;
options.forEach(option => {
const optionDiv = document.createElement('div');
optionDiv.style.cssText = `
padding: 5px;
border-left: 2px solid #3498db;
padding-left: 8px;
flex: 1;
min-width: 150px;
max-width: calc(50% - 5px);
cursor: pointer;
transition: background-color 0.3s;
`;
optionDiv.dataset.tree = tree;
optionDiv.dataset.anchor = anchor;
optionDiv.dataset.optionIndex = options.indexOf(option);
// Modified click event handler for modifiers
optionDiv.addEventListener('click', () => {
// Only unselect options in CURRENT anchor and tree
optionsContainer.querySelectorAll('div').forEach(div => {
div.style.backgroundColor = '';
div.style.color = '#ecf0f1';
});
optionDiv.style.backgroundColor = '#2980b9';
optionDiv.style.color = '#fff';
// Use a composite key that includes both tree and anchor
const compositeKey = `${tree}:${anchor}`;
selectedModifiers[compositeKey] = { ...option, tree: tree, anchor: anchor };
console.log('Selected modifier:', compositeKey, selectedModifiers[compositeKey]);
console.log('All selected modifiers:', {...selectedModifiers});
saveToLocalStorage('selectedModifiers', selectedModifiers);
});
optionDiv.innerHTML = `
<div style="font-weight: bold;">${option.name}</div>
<div style="color: #95a5a6; font-size: 0.9em;">Cost: ${option.cost} | Value: ${option.value}</div>
`;
optionsContainer.appendChild(optionDiv);
});
anchorSection.appendChild(optionsContainer);
treeSection.appendChild(anchorSection);
});
container.appendChild(treeSection);
});
// At the end of createModifiersList, highlight previously selected options
Object.entries(selectedModifiers).forEach(([compositeKey, modData]) => {
const [tree, anchor] = compositeKey.split(':');
const optionIndex = modData.optionIndex;
// Find and highlight the selected option
const options = container.querySelectorAll(`div[data-tree="${tree}"][data-anchor="${anchor}"]`);
options.forEach(option => {
if (parseInt(option.dataset.optionIndex) === optionIndex) {
option.style.backgroundColor = '#2980b9';
option.style.color = '#fff';
}
});
});
return container;
}
// Function to fetch monster stats from API
async function fetchMonsterStats(floor) {
try {
const response = await fetch(`https://elethor.com/game/neo-spire/${floor}`);
const data = await response.json();
if (data && data.floor && data.floor.stats) {
return data.floor.stats;
} else {
console.error('Invalid data format from API');
return null;
}
} catch (error) {
console.error('Error fetching monster stats:', error);
return null;
}
}
// Function to update monster stat inputs
function updateMonsterStatInputs(stats) {
if (!stats) return;
document.querySelectorAll('input[data-stat]').forEach(input => {
if (input.dataset.type === 'monster') {
const statKey = input.dataset.stat.toLowerCase().replace(/ /g, '_');
if (stats[statKey] !== undefined) {
input.value = stats[statKey];
const event = new Event('input', { bubbles: true });
input.dispatchEvent(event);
}
}
});
}
// Define updateTotalStats in a scope accessible to event listeners
function updateTotalStats() {
const totalStats = calculateTotalStats();
document.querySelectorAll('span[data-stat]').forEach(span => {
const stat = span.dataset.stat.toLowerCase().replace(/ /g, '_');
span.textContent = totalStats[stat] || '-';
});
}
// Create stats table and simulation controls
function createStatsTable() {
const tableContainer = document.createElement('div');
tableContainer.style.cssText = `
padding: 10px;
font-family: Arial, sans-serif;
color: #ecf0f1;
flex: 1;
`;
const table = document.createElement('table');
table.style.cssText = `
border-collapse: collapse;
width: 100%;
`;
// Auto monster stats controls (unchanged)
const autoStatsContainer = document.createElement('div');
autoStatsContainer.style.cssText = `
display: flex;
align-items: center;
margin-bottom: 10px;
padding: 8px;
background: rgba(44, 62, 80, 0.5);
border-radius: 4px;
`;
const autoStatsCheckbox = document.createElement('input');
autoStatsCheckbox.type = 'checkbox';
autoStatsCheckbox.id = 'auto-monster-stats';
autoStatsCheckbox.style.cssText = `margin-right: 8px;`;
const autoStatsLabel = document.createElement('label');
autoStatsLabel.htmlFor = 'auto-monster-stats';
autoStatsLabel.textContent = 'Auto Monster Stats';
autoStatsLabel.style.cssText = `margin-right: 15px; color: #3498db;`;
const floorLabel = document.createElement('label');
floorLabel.htmlFor = 'floor-input';
floorLabel.textContent = 'Floor:';
floorLabel.style.cssText = `margin-right: 8px; color: #3498db;`;
const floorInput = document.createElement('input');
floorInput.type = 'number';
floorInput.id = 'floor-input';
floorInput.min = '1';
floorInput.value = '1';
floorInput.style.cssText = `
width: 60px;
background: #2c3e50;
border: 1px solid #34495e;
color: #ecf0f1;
padding: 4px;
border-radius: 3px;
`;
const fetchButton = document.createElement('button');
fetchButton.textContent = 'Fetch';
fetchButton.style.cssText = `
margin-left: 8px;
padding: 4px 8px;
background: #3498db;
border: none;
border-radius: 4px;
color: white;
cursor: pointer;
font-weight: bold;
transition: background 0.3s;
`;
fetchButton.addEventListener('mouseenter', () => { fetchButton.style.background = '#2980b9'; });
fetchButton.addEventListener('mouseleave', () => { fetchButton.style.background = '#3498db'; });
autoStatsCheckbox.addEventListener('change', async (e) => {
autoMonsterStats = e.target.checked;
document.querySelectorAll('input[data-type="monster"]').forEach(input => {
input.disabled = autoMonsterStats;
input.style.opacity = autoMonsterStats ? '0.5' : '1';
});
if (autoMonsterStats) {
currentFloor = parseInt(floorInput.value) || 1;
const stats = await fetchMonsterStats(currentFloor);
if (stats) { updateMonsterStatInputs(stats); }
}
});
floorInput.addEventListener('input', (e) => { currentFloor = parseInt(e.target.value) || 1; });
fetchButton.addEventListener('click', async () => {
if (autoMonsterStats) {
currentFloor = parseInt(floorInput.value) || 1;
const stats = await fetchMonsterStats(currentFloor);
if (stats) { updateMonsterStatInputs(stats); }
}
});
autoStatsContainer.appendChild(autoStatsCheckbox);
autoStatsContainer.appendChild(autoStatsLabel);
autoStatsContainer.appendChild(floorLabel);
autoStatsContainer.appendChild(floorInput);
autoStatsContainer.appendChild(fetchButton);
tableContainer.appendChild(autoStatsContainer);
// Add "Number of simulations" input above simulate combat
const simulationControls = document.createElement('div');
simulationControls.style.cssText = 'margin-bottom: 10px; display: flex; align-items: center; gap: 10px;';
const numSimLabel = document.createElement('label');
numSimLabel.htmlFor = 'num-simulations';
numSimLabel.textContent = 'Number of simulations:';
numSimLabel.style.cssText = 'color: #3498db;';
simulationControls.appendChild(numSimLabel);
const numSimInput = document.createElement('input');
numSimInput.type = 'number';
numSimInput.id = 'num-simulations';
numSimInput.value = '10';
numSimInput.style.cssText = `
width: 60px;
background: #2c3e50;
border: 1px solid #34495e;
color: #ecf0f1;
padding: 4px;
border-radius: 3px;
`;
simulationControls.appendChild(numSimInput);
tableContainer.appendChild(simulationControls);
// Create header row for the stat table
const header = document.createElement('tr');
['STAT', 'MONSTER', 'YOU', 'TOTAL YOU'].forEach(text => {
const th = document.createElement('th');
th.textContent = text;
th.style.cssText = `
padding: 8px;
text-align: left;
border-bottom: 2px solid #34495e;
color: #3498db;
`;
header.appendChild(th);
});
table.appendChild(header);
// Create stat rows
stats.forEach(stat => {
const row = document.createElement('tr');
const nameCell = document.createElement('td');
nameCell.textContent = stat;
nameCell.style.cssText = `padding: 8px; border-bottom: 1px solid #34495e;`;
const monsterCell = document.createElement('td');
const monsterInput = document.createElement('input');
monsterInput.type = 'text';
monsterInput.dataset.stat = stat;
monsterInput.dataset.type = 'monster';
monsterInput.addEventListener('input', (e) => {
const value = e.target.value;
const statKey = stat.toLowerCase().replace(/ /g, '_');
monsterStats[statKey] = value;
saveToLocalStorage('monsterStats', monsterStats); // Save monster stats
console.log('Monster stats updated:', monsterStats);
});
monsterInput.style.cssText = `
width: 80px;
background: #2c3e50;
border: 1px solid #34495e;
color: #ecf0f1;
padding: 4px;
border-radius: 3px;
`;
monsterCell.appendChild(monsterInput);
monsterCell.style.cssText = `padding: 8px; border-bottom: 1px solid #34495e;`;
const yourCell = document.createElement('td');
const yourInput = document.createElement('input');
yourInput.type = 'text';
yourInput.dataset.stat = stat;
yourInput.addEventListener('input', (e) => {
const value = e.target.value;
const statKey = stat.toLowerCase().replace(/ /g, '_');
userStats[statKey] = value;
saveToLocalStorage('userStats', userStats); // Save user stats
console.log('User stats updated:', userStats);
});
yourInput.style.cssText = `
width: 80px;
background: #2c3e50;
border: 1px solid #34495e;
color: #ecf0f1;
padding: 4px;
border-radius: 3px;
`;
yourCell.appendChild(yourInput);
yourCell.style.cssText = `padding: 8px; border-bottom: 1px solid #34495e;`;
row.appendChild(nameCell);
row.appendChild(monsterCell);
row.appendChild(yourCell);
const totalCell = document.createElement('td');
const totalSpan = document.createElement('span');
totalSpan.dataset.stat = stat;
totalSpan.textContent = '-';
totalSpan.style.cssText = `color: #2ecc71; font-weight: bold;`;
totalCell.appendChild(totalSpan);
totalCell.style.cssText = `padding: 8px; border-bottom: 1px solid #34495e;`;
row.appendChild(totalCell);
row.addEventListener('mouseenter', () => { row.style.backgroundColor = 'rgba(52, 73, 94, 0.5)'; });
row.addEventListener('mouseleave', () => { row.style.backgroundColor = ''; });
table.appendChild(row);
});
tableContainer.appendChild(table);
// Add simulate button and modify its event listener for multiple simulations
const simulateButton = document.createElement('button');
simulateButton.textContent = 'Simulate Combat';
simulateButton.style.cssText = `
margin-top: 10px;
padding: 8px 16px;
background: #2ecc71;
border: none;
border-radius: 4px;
color: white;
cursor: pointer;
width: 100%;
font-weight: bold;
transition: background 0.3s;
`;
simulateButton.addEventListener('mouseenter', () => { simulateButton.style.background = '#27ae60'; });
simulateButton.addEventListener('mouseleave', () => { simulateButton.style.background = '#2ecc71'; });
simulateButton.addEventListener('click', () => {
// Clear previous simulation results and logs
simulationResultsContainer.innerHTML = '';
simulationLogs = []; // reset stored logs
// Calculate total stats before running simulations
const totalUserStats = calculateTotalStats();
const numSims = parseInt(document.getElementById('num-simulations').value) || 1;
for (let i = 0; i < numSims; i++) {
const combatLog = simulateCombat();
// Initially hide each combat log (they will be shown in the overlay)
combatLog.style.display = 'none';
combatLog.id = `combat-log-${i}`;
simulationLogs.push(combatLog);
// Extract final result text from combat log
const finalLine = combatLog.querySelector('div[style*="color: #f1c40f"]');
const resultText = finalLine ? finalLine.textContent : 'Unknown';
// Create simulation result entry
const simResultDiv = document.createElement('div');
simResultDiv.style.cssText = 'margin-bottom: 10px; padding: 5px; background: rgba(44, 62, 80, 0.5); border-radius: 4px;';
simResultDiv.innerHTML = `<strong>Simulation ${i + 1}:</strong> ${resultText}`;
const logsButton = document.createElement('button');
logsButton.textContent = 'Logs';
logsButton.style.cssText = 'margin-left: 10px; padding: 2px 6px;';
logsButton.addEventListener('click', () => {
// When clicked, hide the results container and show the overlay with the combat log
simulationResultsContainer.style.display = 'none';
// Clear any previous content except the back button in the overlay
simulationLogOverlay.innerHTML = '';
simulationLogOverlay.appendChild(backButton);
simulationLogOverlay.appendChild(simulationLogs[i]);
simulationLogs[i].style.display = 'block';
simulationLogOverlay.style.display = 'block';
});
simResultDiv.appendChild(logsButton);
simulationResultsContainer.appendChild(simResultDiv);
}
});
tableContainer.appendChild(simulateButton);
// Add confirm modifiers button (unchanged)
const confirmButton = document.createElement('button');
confirmButton.textContent = 'Confirm Modifiers';
confirmButton.style.cssText = `
margin-top: 10px;
margin-bottom: 10px;
padding: 8px 16px;
background: #e67e22;
border: none;
border-radius: 4px;
color: white;
cursor: pointer;
width: 100%;
font-weight: bold;
transition: background 0.3s;
`;
confirmButton.addEventListener('mouseenter', () => { confirmButton.style.background = '#d35400'; });
confirmButton.addEventListener('mouseleave', () => { confirmButton.style.background = '#e67e22'; });
confirmButton.addEventListener('click', () => {
logSelectedModifiers();
updateTotalStats();
});
tableContainer.appendChild(confirmButton);
// Add drag functionality for the table container
let isDragging = false, currentX, currentY, initialX, initialY, xOffset = 0, yOffset = 0;
tableContainer.addEventListener('mousedown', dragStart);
document.addEventListener('mousemove', drag);
document.addEventListener('mouseup', dragEnd);
function dragStart(e) {
initialX = e.clientX - xOffset;
initialY = e.clientY - yOffset;
if (e.target === tableContainer) { isDragging = true; }
}
function drag(e) {
if (isDragging) {
e.preventDefault();
currentX = e.clientX - initialX;
currentY = e.clientY - initialY;
xOffset = currentX;
yOffset = currentY;
setTranslate(currentX, currentY, tableContainer);
}
}
function setTranslate(xPos, yPos, el) {
el.style.transform = `translate3d(${xPos}px, ${yPos}px, 0)`;
}
function dragEnd() {
initialX = currentX;
initialY = currentY;
isDragging = false;
}
document.body.appendChild(tableContainer);
}
// Main container creation on page load
window.addEventListener('load', async () => {
const mainContainer = document.createElement('div');
// Load saved stats from localStorage
const savedUserStats = loadFromLocalStorage('userStats') || {};
const savedMonsterStats = loadFromLocalStorage('monsterStats') || {};
userStats = savedUserStats;
monsterStats = savedMonsterStats;
mainContainer.style.cssText = `
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(22, 28, 36, 0.95);
z-index: 9999;
box-shadow: 0 0 10px rgba(0,0,0,0.5);
overflow: auto;
`;
// Create content container with side-by-side layout
const contentContainer = document.createElement('div');
contentContainer.style.cssText = `
display: flex;
flex-direction: row;
gap: 20px;
padding: 20px;
`;
// Create simulation results container (to be shown on the left)
simulationResultsContainer = document.createElement('div');
simulationResultsContainer.id = 'simulation-results';
simulationResultsContainer.style.cssText = `
flex: 1;
background: rgba(22, 28, 36, 0.95);
color: #ecf0f1;
padding: 10px;
border: 1px solid #2c3e50;
border-radius: 4px;
max-height: 400px;
overflow-y: auto;
`;
contentContainer.appendChild(simulationResultsContainer);
// Create and append stats table
createStatsTable();
// After table creation, populate input fields with saved values
document.querySelectorAll('input[data-stat]').forEach(input => {
const statKey = input.dataset.stat.toLowerCase().replace(/ /g, '_');
if (input.dataset.type === 'monster' && monsterStats[statKey] !== undefined) {
input.value = monsterStats[statKey];
const event = new Event('input', { bubbles: true });
input.dispatchEvent(event); // Ensure any dependent calculations trigger
}
if (input.dataset.type === 'user' && userStats[statKey] !== undefined) {
input.value = userStats[statKey];
const event = new Event('input', { bubbles: true });
input.dispatchEvent(event);
}
});
const statsTable = document.querySelector('div[style*="font-family: Arial"]');
if (statsTable) {
contentContainer.appendChild(statsTable);
}
// Create and append modifiers list
const modifiers = await fetchModifiers();
if (modifiers) {
modifiersData = modifiers;
const modifiersList = createModifiersList(modifiers);
contentContainer.appendChild(modifiersList);
}
mainContainer.appendChild(contentContainer);
document.body.appendChild(simulationLogOverlay);
document.body.appendChild(mainContainer);
});
})();