Automates running multiple simulations on the MWI Combat Simulator with a dynamic, grouped UI and cost-analysis.
当前为
// ==UserScript==
// @name JIGS (Jigglymoose's Intelligent Gear Simulator)
// @namespace http://tampermonkey.net/
// @version 23.3
// @description Automates running multiple simulations on the MWI Combat Simulator with a dynamic, grouped UI and cost-analysis.
// @author Gemini & Jigglymoose
// @license MIT
// @match https://shykai.github.io/MWICombatSimulatorTest/dist/
// @match https://shykai.github.io/MWICombatSimulator/dist/
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// ==/UserScript==
(function() {
'use strict';
console.log("JIGS (Jigglymoose's Intelligent Gear Simulator) v23.3 Loaded");
// --- CONFIGURATION ---
const MARKET_API_URL = 'https://www.milkywayidle.com/game_data/marketplace.json';
const JIGS_DATA_URL = 'https://gist.githubusercontent.com/JigglyMoose/79db9d275a73a26dec30305865692525/raw/2a527e359bb442b0f5972cbf3a92387774e3339c/jigs_data.json';
// Data will be loaded into these variables
let HOUSE_RECIPES = {};
let ITEM_ID_TO_NAME_MAP = {};
let SPELL_BOOK_XP = {};
let SIMULATOR_TO_MARKET_MAP = {};
let ABILITY_XP_LEVELS = [];
// --- 1. UI & STYLES ---
const panel = document.createElement('div');
panel.id = 'batch-panel';
panel.innerHTML = `
<div id="batch-header" title="Jigglymoose's Intelligent Gear Simulator"><span>JIGS</span><button id="batch-toggle">-</button></div>
<div id="batch-content">
<div id="controls-grid">
<button id="run-batch-button" disabled>Run Simulations</button>
<button id="stop-batch-button" style="display: none;">Stop</button>
<button id="capture-setup-button" disabled>Capture Setup</button>
<button id="update-baseline-button" disabled>Update Baseline</button>
<span id="baseline-display">Baseline DPS: --</span>
<button id="export-csv-button" disabled>Export to CSV</button>
<button id="reset-button" disabled>Reset to Baseline</button>
</div>
<div id="batch-status">Status: Loading game data...</div>
<div id="jigs-progress-container" style="display: none;">
<div id="jigs-progress-bar"></div>
</div>
<div id="batch-inputs-container">
<details id="sim-settings-group" open><summary>Simulation Settings (Constant for Batch)</summary></details>
<details id="skills-group" open><summary>Skills</summary></details>
<details id="equipment-group" open><summary>Equipment</summary></details>
<details id="abilities-group" open><summary>Abilities</summary></details>
<details id="food-drink-group" open><summary>Food & Drink</summary></details>
<details id="house-group" open><summary>House</summary><div id="house-grid-container"></div></details>
</div>
<div id="batch-results-container">
<table id="batch-results-table">
<thead>
<tr>
<th data-sort-key="upgrade" title="The specific upgrade being tested">Upgrade</th>
<th data-sort-key="cost" title="The total cost of the upgrade in gold, based on market prices">Upgrade Cost</th>
<th data-sort-key="dpsChange" title="The raw DPS increase (+) or decrease (-) from this single change">DPS Change</th>
<th data-sort-key="percentChange" title="The percentage of DPS gained from this single change">% Change</th>
<th data-sort-key="costPerDps" title="Lower is better! This shows the gold cost for every 0.01% increase in total DPS.">Gold per 0.01% DPS</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
`;
document.body.appendChild(panel);
GM_addStyle(`
#batch-panel { position: fixed; bottom: 10px; right: 10px; width: 550px; max-height: 90vh; background-color: #2c2c2c; border: 1px solid #444; border-radius: 5px; color: #eee; z-index: 9999; font-family: sans-serif; display: flex; flex-direction: column; }
#batch-header { background-color: #333; padding: 8px; cursor: pointer; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #444; }
#batch-header span { font-weight: bold; }
#batch-toggle { background: #555; border: 1px solid #777; color: white; border-radius: 3px; cursor: pointer; margin-left: 5px; }
#batch-content { padding: 10px; display: flex; flex-direction: column; overflow-y: auto; position: relative; }
#batch-content.hidden { display: none; }
#controls-grid { display: grid; grid-template-columns: repeat(3, 1fr); grid-template-rows: auto auto; gap: 8px; margin-bottom: 10px; }
#run-stop-container { grid-column: 1 / 2; grid-row: 1 / 2; display: contents; }
#stop-batch-button { background-color: #c9302c; }
#controls-grid button { width: 100%; padding: 8px; color: white; border: none; border-radius: 4px; cursor: pointer; }
#run-batch-button { grid-column: 1 / 2; grid-row: 1 / 2; background-color: #4CAF50; }
#capture-setup-button { grid-column: 2 / 3; grid-row: 1 / 2; background-color: #337ab7; }
#update-baseline-button { grid-column: 3 / 4; grid-row: 1 / 2; background-color: #f44336; }
#baseline-display { grid-column: 1 / 2; grid-row: 2 / 3; text-align: center; align-self: center; font-size: 0.9em; color: #ccc; }
#export-csv-button { grid-column: 2 / 3; grid-row: 2 / 3; background-color: #5bc0de; }
#reset-button { grid-column: 3 / 4; grid-row: 2 / 3; background-color: #f0ad4e; }
#run-batch-button:disabled, #capture-setup-button:disabled, #update-baseline-button:disabled, #export-csv-button:disabled, #reset-button:disabled { background-color: #555; cursor: not-allowed; }
#batch-status { margin-bottom: 5px; font-style: italic; color: #aaa; text-align: center; }
#jigs-progress-container { width: 100%; background-color: #555; border-radius: 5px; height: 10px; margin-bottom: 10px; border: 1px solid #333; }
#jigs-progress-bar { width: 0%; height: 100%; background-color: #4CAF50; border-radius: 5px; transition: width 0.1s linear; }
#batch-inputs-container { display: flex; flex-direction: column; gap: 5px; max-height: 40vh; overflow-y: auto; border: 1px solid #444; padding: 10px; margin-bottom: 10px; }
summary { font-weight: bold; cursor: pointer; padding: 4px; background-color: #333; margin-bottom: 5px; }
#sim-settings-group summary { font-size: 0.9em; }
details { border-left: 1px solid #444; padding-left: 10px; margin-bottom: 5px;}
.batch-input-row { display: grid; grid-template-columns: 100px 1fr; align-items: center; margin-bottom: 5px; gap: 5px; }
.batch-input-row-equip, .batch-input-row-ability { display: grid; grid-template-columns: 60px 1fr 80px; align-items: center; margin-bottom: 5px; gap: 5px; }
.batch-input-row label, .batch-input-row-equip label, .batch-input-row-ability label { font-size: 0.9em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.batch-input-row select, .batch-input-row input, .batch-input-row-equip select, .batch-input-row-equip input, .batch-input-row-ability select, .batch-input-row-ability input { background-color: #1e1e1e; color: #ddd; border: 1px solid #555; width: 100%; box-sizing: border-box; }
#house-grid-container { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
.house-grid-item { display: flex; flex-direction: column; align-items: center; }
.house-grid-item label { font-size: 0.8em; margin-bottom: 2px; }
.house-grid-item input { width: 90%; text-align: center; }
#batch-results-container { margin-top: 10px; max-height: 30vh; overflow-y: auto; }
#batch-results-table { width: 100%; border-collapse: collapse; }
#batch-results-table th, #batch-results-table td { border: 1px solid #444; padding: 5px; text-align: left; font-size: 0.9em; }
#batch-results-table th { background-color: #333; cursor: pointer; }
#batch-results-table th:hover { background-color: #444; }
.sorted-asc::after { content: ' ▲'; }
.sorted-desc::after { content: ' ▼'; }
`);
// --- 2. GET ELEMENTS & DEFINE DATA ---
const statusDiv = document.getElementById('batch-status');
const groupContainers = { skills: document.querySelector('#skills-group'), house: document.querySelector('#house-grid-container'), abilities: document.querySelector('#abilities-group'), equipment: document.querySelector('#equipment-group'), food: document.querySelector('#food-drink-group'), sim: document.querySelector('#sim-settings-group'), };
let baselineDps = 0;
let marketData = null;
let isBatchRunning = false;
let itemIdToNameMap = {};
let detailedResults = [];
const skillKeywords = ["Combat", "Stamina", "Intelligence", "Attack", "Melee", "Defense", "Ranged", "Magic"];
const equipmentKeywords = ["Head", "Necklace", "Earrings", "Body", "Legs", "Feet", "Hands", "Ring", "Main Hand", "Off Hand", "Pouch", "Back", "Charm"];
const specialIdMap = { 'Select Zone': 'selectZone', 'Difficulty': 'selectDifficulty', 'Duration': 'inputSimulationTime' };
let houseKeywords = [];
// --- 3. HELPER FUNCTIONS ---
function createNumberInput(name, value, min, max, isHouse = false) { const container = document.createElement('div'); container.className = isHouse ? 'house-grid-item' : 'batch-input-row'; const label = document.createElement('label'); label.textContent = name; label.title = name; const input = document.createElement('input'); input.type = 'number'; input.value = value; input.min = min ?? 1; input.max = max ?? 400; input.dataset.originalValue = value; input.dataset.name = name; container.appendChild(label); container.appendChild(input); return container; }
function createSelect(name, value, options) { const row = document.createElement('div'); row.className = 'batch-input-row'; const label = document.createElement('label'); label.textContent = name; label.title = name; const select = document.createElement('select'); select.dataset.originalValue = value; select.dataset.name = name; options.forEach(opt => { const option = document.createElement('option'); option.value = opt; option.textContent = opt; select.appendChild(option); }); select.value = value; row.appendChild(label); row.appendChild(select); return row; }
function createEquipmentRow(name, itemValue, itemOptions, enhValue) { const row = document.createElement('div'); row.className = 'batch-input-row-equip'; const label = document.createElement('label'); label.textContent = name; label.title = name; const itemSelect = document.createElement('select'); itemSelect.dataset.originalValue = itemValue; itemSelect.dataset.name = name; itemOptions.forEach(opt => { const option = document.createElement('option'); option.value = opt; option.textContent = opt; itemSelect.appendChild(option); }); itemSelect.value = itemValue; const enhInput = document.createElement('input'); enhInput.type = 'number'; enhInput.value = enhValue; enhInput.min = 0; enhInput.max = 20; enhInput.dataset.originalValue = enhValue; enhInput.dataset.name = `${name} Enhancement`; row.appendChild(label); row.appendChild(itemSelect); row.appendChild(enhInput); return row; }
function createAbilityRow(index, itemValue, itemOptions, lvlValue) { const row = document.createElement('div'); row.className = 'batch-input-row-ability'; const name = `Ability ${index + 1}`; const label = document.createElement('label'); label.textContent = name; label.title = name; const itemSelect = document.createElement('select'); itemSelect.dataset.originalValue = itemValue; itemSelect.dataset.name = name; itemOptions.forEach(opt => { if (opt !== 'Promote') { const option = document.createElement('option'); option.value = opt; option.textContent = opt; itemSelect.appendChild(option); } }); itemSelect.value = itemValue; const lvlInput = document.createElement('input'); lvlInput.type = 'number'; lvlInput.value = lvlValue; lvlInput.min = 1; lvlInput.max = 200; lvlInput.dataset.originalValue = lvlValue; lvlInput.dataset.name = `${name} Level`; row.appendChild(label); row.appendChild(itemSelect); row.appendChild(lvlInput); return row; }
function findPageElementByName(name, tag = 'input, select') { if (specialIdMap[name]) { return document.getElementById(specialIdMap[name]); } const labels = Array.from(document.querySelectorAll('label')); const targetLabel = labels.find(l => l.textContent.trim().toLowerCase() === name.toLowerCase()); if (!targetLabel) return null; const parentRow = targetLabel.closest('.row'); if (parentRow) { return parentRow.querySelector(tag); } return null; }
function getDpsValue() { const resultsContainer = document.getElementById('simulationResultTotalDamageDone'); if (!resultsContainer || !resultsContainer.hasChildNodes()) { return null; } const totalLabelElement = Array.from(resultsContainer.querySelectorAll('div.col-md-5')).find(el => el.textContent.trim() === 'Total'); if (!totalLabelElement) { return null; } const dpsElement = totalLabelElement.nextElementSibling?.nextElementSibling; if (dpsElement) { return dpsElement.textContent.trim(); } return null; }
function addResultRow(result) { const resultsTbody = document.querySelector('#batch-results-table tbody'); const row = resultsTbody.insertRow(); let costText = formatGold(result.cost); let costPerDpsText = formatGold(result.costPerDps); if (result.cost === Infinity) { costText = 'No Seller'; costPerDpsText = 'N/A'; } let upgradeText = result.upgrade; if (result.books > 0) { upgradeText += ` (${result.books.toLocaleString()} books)`; } row.dataset.upgrade = result.upgrade; row.dataset.cost = isFinite(result.cost) ? result.cost : Infinity; row.dataset.dpsChange = result.dps; row.dataset.percentChange = result.percent; row.dataset.costPerDps = isFinite(result.costPerDps) ? result.costPerDps : Infinity; row.innerHTML = `<td>${upgradeText}</td><td>${costText}</td><td>${result.dps > 0 ? '+' : ''}${result.dps.toFixed(2)}</td><td>${result.percent.toFixed(2)}%</td><td>${costPerDpsText}</td>`; }
function formatGold(value) { if (value === 'N/A' || value === 'Free') return value; if (!isFinite(value) || value === Infinity) return "N/A"; if (value < 1000) return Math.round(value).toLocaleString(); if (value < 1000000) return `${(value / 1000).toFixed(1)}k`; return `${(value / 1000000).toFixed(2)}M`; }
async function fetchJigsData() { return new Promise((resolve) => { GM_xmlhttpRequest({ method: "GET", url: JIGS_DATA_URL, onload: function(response) { if (response.status === 200) { try { const data = JSON.parse(response.responseText); HOUSE_RECIPES = data.recipes; ITEM_ID_TO_NAME_MAP = data.itemMap; SPELL_BOOK_XP = data.spellBookXp; SIMULATOR_TO_MARKET_MAP = data.refinedMap; ABILITY_XP_LEVELS = data.abilityXp; resolve(true); } catch(e) { console.error("JIGS: Failed to parse JIGS data.", e); resolve(false); } } else { console.error("JIGS: Failed to fetch JIGS data.", response.status); resolve(false); } }, onerror: function() { console.error("JIGS: Error fetching JIGS data."); resolve(false); } }); }); }
async function fetchMarketData() { if (marketData) { return marketData; } return new Promise((resolve) => { GM_xmlhttpRequest({ method: "GET", url: MARKET_API_URL, onload: function(response) { if (response.status === 200) { try { const responseObject = JSON.parse(response.responseText); const rawMarketData = responseObject.marketData; if (typeof rawMarketData !== 'object' || rawMarketData === null) { resolve(null); return; } marketData = {}; for (const itemId in rawMarketData) { const itemName = ITEM_ID_TO_NAME_MAP[itemId]; if (!itemName) continue; const itemEnhancements = rawMarketData[itemId]; for (const enhancementLevel in itemEnhancements) { const prices = itemEnhancements[enhancementLevel]; const fullName = enhancementLevel === "0" ? itemName : `${itemName} +${enhancementLevel}`; marketData[fullName.toLowerCase()] = { buyer: prices.b, seller: prices.a }; } } statusDiv.textContent = 'Status: Market data loaded.'; resolve(marketData); } catch (e) { resolve(null); } } else { resolve(null); } }, onerror: function() { resolve(null); } }); }); }
async function runSimulation(progressCallback) { return new Promise((resolve) => { let timeout, progressWatcher, dpsWatcher; const cleanup = () => { clearTimeout(timeout); clearInterval(progressWatcher); clearInterval(dpsWatcher); }; timeout = setTimeout(() => { cleanup(); resolve(NaN); }, 120000); const setupButton = document.getElementById('buttonSimulationSetup'); if (!setupButton) { cleanup(); resolve(NaN); return; } setupButton.click(); setTimeout(() => { const startButton = document.getElementById('buttonStartSimulation'); if (!startButton) { cleanup(); resolve(NaN); return; } const resultsContainer = document.getElementById('simulationResultTotalDamageDone'); if (resultsContainer) resultsContainer.innerHTML = ''; startButton.click(); progressWatcher = setInterval(() => { const progressBar = document.getElementById('simulationProgressBar'); if (progressBar) { const progress = parseInt(progressBar.textContent) || 0; if (progressCallback) progressCallback(progress); } if (progressBar && progressBar.textContent.includes('100%')) { clearInterval(progressWatcher); dpsWatcher = setInterval(() => { const dpsVal = getDpsValue(); if (dpsVal) { cleanup(); resolve(parseFloat(dpsVal.replace(/,/g, ''))); } }, 100); } }, 200); }, 300); }); }
async function runSimulationMultiple(multiplier, progressCallback) { let totalDps = 0; let successfulRuns = 0; let allDpsResults = []; for (let i = 0; i < multiplier; i++) { if (!isBatchRunning) break; const singleRunProgress = (progress) => { if (progressCallback) { const overallProgress = ((i * 100) + progress) / multiplier; progressCallback(overallProgress); } }; const dps = await runSimulation(singleRunProgress); if (!isNaN(dps)) { totalDps += dps; successfulRuns++; allDpsResults.push(dps); } else { console.error(`JIGS: Simulation run ${i + 1} of ${multiplier} failed.`); } } if (successfulRuns === 0) { return { averageDps: NaN, individualRuns: allDpsResults }; } return { averageDps: totalDps / successfulRuns, individualRuns: allDpsResults }; }
function setRunningState(isRunning) {
isBatchRunning = isRunning;
document.getElementById('run-batch-button').style.display = isRunning ? 'none' : 'block';
document.getElementById('stop-batch-button').style.display = isRunning ? 'block' : 'none';
document.getElementById('capture-setup-button').disabled = isRunning;
document.getElementById('update-baseline-button').disabled = isRunning;
document.getElementById('export-csv-button').disabled = isRunning;
document.getElementById('reset-button').disabled = isRunning;
if (!isRunning) {
document.getElementById('jigs-progress-container').style.display = 'none';
}
}
function exportResultsToCSV() {
if (detailedResults.length === 0) {
alert('No results to export. Please run a simulation first.');
return;
}
let csvContent = "";
const maxRuns = Math.max(...detailedResults.map(r => r.individualRuns.length));
let headers = ["Upgrade", "Cost", "DPS Change", "% Change", "Gold per 0.01% DPS", "Books Needed", "Average DPS"];
for(let i=1; i<=maxRuns; i++) { headers.push(`Run ${i}`); }
csvContent += headers.join(',') + "\r\n";
detailedResults.forEach(result => {
let row = [
`"${result.upgrade.replace(/"/g, '""')}"`,
isFinite(result.cost) ? result.cost : '"N/A"',
result.dps.toFixed(2),
result.percent.toFixed(2),
isFinite(result.costPerDps) ? result.costPerDps : '"N/A"',
result.books,
result.averageDps.toFixed(2)
];
for(let i=0; i<maxRuns; i++) {
row.push(result.individualRuns[i] ? result.individualRuns[i].toFixed(2) : '');
}
csvContent += row.join(',') + "\r\n";
});
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement("a");
const url = URL.createObjectURL(blob);
link.setAttribute("href", url);
link.setAttribute("download", "jigs_results.csv");
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
function resetInputsToBaseline() {
document.querySelectorAll('#batch-inputs-container input, #batch-inputs-container select').forEach(el => {
if (el.dataset.originalValue !== undefined) {
el.value = el.dataset.originalValue;
}
});
statusDiv.textContent = 'Status: All inputs reset to baseline.';
}
// --- 4. CORE LOGIC ---
async function buildInputsUI() {
statusDiv.textContent = 'Status: Reading data from page...';
document.getElementById('run-batch-button').disabled = true;
document.getElementById('capture-setup-button').disabled = true;
document.getElementById('update-baseline-button').disabled = true;
document.getElementById('reset-button').disabled = true;
const currentMultiplier = document.querySelector('#sim-settings-group [data-name="Multiplier"]')?.value || 1;
Object.values(groupContainers).forEach(c => { if(c.id !== 'house-grid-container') c.innerHTML = `<summary>${c.querySelector('summary').textContent}</summary>`; else c.innerHTML = ''; });
let itemsFound = 0;
houseKeywords = [];
itemIdToNameMap = {...ITEM_ID_TO_NAME_MAP};
skillKeywords.forEach(name => { const pageEl = findPageElementByName(name); if (pageEl) { groupContainers.skills.appendChild(createNumberInput(name, pageEl.value, pageEl.min, pageEl.max)); itemsFound++; } });
equipmentKeywords.forEach(name => { const itemSelect = findPageElementByName(name, 'select'); const enhInput = findPageElementByName(name, 'input'); if (itemSelect && enhInput) { const itemValue = itemSelect.options[itemSelect.selectedIndex].text; const itemOptions = Array.from(itemSelect.options).map(opt => opt.text); const enhValue = enhInput.value; groupContainers.equipment.appendChild(createEquipmentRow(name, itemValue, itemOptions, enhValue)); itemsFound++; } });
for (let i = 0; i < 5; i++) { const abilitySelect = document.getElementById(`selectAbility_${i}`); const levelInput = document.getElementById(`inputAbilityLevel_${i}`); if (abilitySelect && levelInput) { const itemValue = abilitySelect.options[abilitySelect.selectedIndex].text; const itemOptions = Array.from(abilitySelect.options).map(opt => opt.text); const lvlValue = levelInput.value; groupContainers.abilities.appendChild(createAbilityRow(i, itemValue, itemOptions, lvlValue)); itemsFound++; } }
document.querySelectorAll('select[id^="selectFood_"], select[id^="selectDrink_"]').forEach((el, i) => { const type = el.id.includes('Food') ? 'Food' : 'Drink'; const name = `${type} ${i % 3 + 1}`; const currentValue = el.options[el.selectedIndex].text; const options = Array.from(el.options).map(opt => opt.text); groupContainers.food.appendChild(createSelect(name, currentValue, options)); itemsFound++; });
document.querySelectorAll('#houseRoomsList .row').forEach(row => { const labelEl = row.querySelector('div[data-i18n]'); const inputEl = row.querySelector('input'); if (labelEl && inputEl) { const name = labelEl.textContent.trim(); houseKeywords.push(name); groupContainers.house.appendChild(createNumberInput(name, inputEl.value, inputEl.min, inputEl.max, true)); itemsFound++; } });
for (const name of Object.keys(specialIdMap)) {
const pageEl = findPageElementByName(name);
if (pageEl) {
if (pageEl.tagName === 'SELECT') { const currentValue = pageEl.options[pageEl.selectedIndex].text; const options = Array.from(pageEl.options).map(opt => opt.text); groupContainers.sim.appendChild(createSelect(name, currentValue, options)); }
else { groupContainers.sim.appendChild(createNumberInput(name, pageEl.value, pageEl.min, pageEl.max)); }
itemsFound++;
}
}
groupContainers.sim.appendChild(createNumberInput('Multiplier', currentMultiplier, 1, 100));
if (itemsFound > 0) {
statusDiv.textContent = 'Status: Idle.';
document.getElementById('run-batch-button').disabled = false;
document.getElementById('update-baseline-button').disabled = false;
document.getElementById('reset-button').disabled = false;
} else {
statusDiv.textContent = 'Status: No data found. Import or use Capture Setup.';
}
document.getElementById('capture-setup-button').disabled = false;
}
async function updateBaseline() {
setRunningState(true);
const jigsProgressContainer = document.getElementById('jigs-progress-container');
const jigsProgressBar = document.getElementById('jigs-progress-bar');
statusDiv.textContent = 'Status: Applying settings and updating baseline...';
jigsProgressContainer.style.display = 'block';
jigsProgressBar.style.width = '0%';
try {
const simSettings = document.querySelectorAll('#sim-settings-group select, #sim-settings-group input');
simSettings.forEach(uiEl => { if (uiEl.value !== uiEl.dataset.originalValue) { const pageEl = findPageElementByName(uiEl.dataset.name); if (pageEl) { if (pageEl.tagName === 'SELECT') { const opt = Array.from(pageEl.options).find(o => o.text === uiEl.value); if (opt) pageEl.value = opt.value; } else { pageEl.value = uiEl.value; } pageEl.dispatchEvent(new Event('change', { bubbles: true })); pageEl.dispatchEvent(new Event('input', { bubbles: true })); } } });
const multiplier = parseInt(document.querySelector('#sim-settings-group [data-name="Multiplier"]').value) || 1;
const simResult = await runSimulationMultiple(multiplier, progress => { jigsProgressBar.style.width = `${progress}%`; });
const newBaseline = simResult.averageDps;
if (!isNaN(newBaseline)) { baselineDps = newBaseline; document.getElementById('baseline-display').textContent = `Baseline DPS: ${baselineDps.toFixed(2)}`; simSettings.forEach(uiEl => { uiEl.dataset.originalValue = uiEl.value; }); statusDiv.textContent = 'Status: Baseline updated.'; }
else if (isBatchRunning) { statusDiv.textContent = 'Error: Failed to update baseline. Try again.'; }
} finally {
setRunningState(false);
}
}
async function startBatch() {
let allPageInputs = new Map();
setRunningState(true);
try {
await fetchMarketData();
if (!isBatchRunning) {
statusDiv.textContent = 'Status: Stopped by user.';
return;
}
if (!marketData) {
return;
}
if (baselineDps === 0) {
statusDiv.textContent = 'Error: Please set a baseline first.';
return;
}
const jigsProgressContainer = document.getElementById('jigs-progress-container');
const jigsProgressBar = document.getElementById('jigs-progress-bar');
jigsProgressContainer.style.display = 'block';
jigsProgressBar.style.width = '0%';
document.querySelector('#batch-results-table tbody').innerHTML = '';
detailedResults = [];
document.getElementById('export-csv-button').disabled = true;
let allChanges = {};
document.querySelectorAll('#batch-inputs-container input, #batch-inputs-container select').forEach(el => {
if (!el.disabled && el.value !== el.dataset.originalValue) {
allChanges[el.dataset.name] = {
value: el.value,
originalValue: el.dataset.originalValue
};
}
});
const upgrades = [];
const processed = new Set();
for (const name in allChanges) {
if (processed.has(name)) continue;
let combinedUpgrade = {
name,
...allChanges[name]
};
let enhName = `${name} Enhancement`;
let lvlName = `${name} Level`;
if (allChanges[enhName]) {
combinedUpgrade.enhancement = allChanges[enhName];
processed.add(enhName);
} else if (allChanges[lvlName]) {
combinedUpgrade.level = allChanges[lvlName];
processed.add(lvlName);
} else if (name.includes('Enhancement')) {
let base = name.replace(' Enhancement', '');
if (!allChanges[base]) {
combinedUpgrade = {
name: base,
isEnhancementOnly: true,
enhancement: allChanges[name]
};
}
} else if (name.includes('Level')) {
let base = name.replace(' Level', '');
if (!allChanges[base]) {
combinedUpgrade = {
name: base,
isLevelOnly: true,
level: allChanges[name]
};
}
}
upgrades.push(combinedUpgrade);
processed.add(name);
}
const settingChanges = upgrades.filter(u => specialIdMap[u.name] || u.name === 'Multiplier');
const upgradeChanges = upgrades.filter(u => !settingChanges.includes(u));
if (upgradeChanges.length === 0 && settingChanges.length === 0) {
statusDiv.textContent = 'Error: No changes.';
return;
}
let simsCompleted = 0;
let totalSims = upgradeChanges.length + (settingChanges.length > 0 ? 1 : 0);
let currentBaselineDps = baselineDps;
const updateOverallProgress = (simIndex, currentSimProgress) => {
const totalProgress = ((simIndex + (currentSimProgress / 100)) / totalSims) * 100;
jigsProgressBar.style.width = `${totalProgress}%`;
};
document.querySelectorAll('#batch-inputs-container input, #batch-inputs-container select').forEach(uiEl => {
const name = uiEl.dataset.name.replace(' Enhancement', '').replace(' Level', '').trim();
const tag = uiEl.dataset.name.includes('Enhancement') || uiEl.dataset.name.includes('Level') ? 'input' : 'select, input';
let el;
if (uiEl.dataset.name.startsWith('Ability')) {
const index = parseInt(uiEl.dataset.name.match(/\d+/)[0]) - 1;
const idPart = uiEl.dataset.name.includes('Level') ? 'inputAbilityLevel' : 'selectAbility';
el = document.getElementById(`${idPart}_${index}`);
} else if (uiEl.dataset.name.startsWith('Food') || uiEl.dataset.name.startsWith('Drink')) {
const type = uiEl.dataset.name.startsWith('Food') ? 'Food' : 'Drink';
const index = parseInt(uiEl.dataset.name.slice(-1)) - 1;
el = document.querySelector(`#select${type}_${index}`);
} else if (houseKeywords.includes(name)) {
const allRoomLabels = document.querySelectorAll('#houseRoomsList div[data-i18n]');
const targetLabel = Array.from(allRoomLabels).find(div => div.textContent.trim() === name);
if (targetLabel) {
el = targetLabel.closest('.row').querySelector('input');
}
} else {
el = findPageElementByName(name, tag);
}
if (el) {
const selectedOption = Array.from(el.options || []).find(o => o.selected);
const originalValue = uiEl.tagName === 'SELECT' ? (selectedOption ? selectedOption.text : '') : el.value;
allPageInputs.set(uiEl.dataset.name, {
el: el,
originalValue: originalValue
});
}
});
if (settingChanges.length > 0) {
statusDiv.textContent = 'Status: Applying settings & running new baseline...';
settingChanges.forEach(change => {
const input = allPageInputs.get(change.name);
if (input && input.el) {
const el = input.el;
if (el.tagName === 'SELECT') {
const opt = Array.from(el.options).find(o => o.text === change.value);
if (opt) el.value = opt.value;
} else {
el.value = change.value;
}
el.dispatchEvent(new Event('change', {
bubbles: true
}));
}
});
const multiplier = parseInt(document.querySelector('#sim-settings-group [data-name="Multiplier"]').value) || 1;
const simResult = await runSimulationMultiple(multiplier, progress => updateOverallProgress(simsCompleted, progress));
const newBaseline = simResult.averageDps;
if (!isBatchRunning) {
statusDiv.textContent = 'Status: Stopped by user.';
return;
}
if (isNaN(newBaseline)) {
statusDiv.textContent = 'Error: Could not get baseline for new settings.';
return;
}
currentBaselineDps = newBaseline;
baselineDps = newBaseline;
document.getElementById('baseline-display').textContent = `Baseline DPS: ${baselineDps.toFixed(2)}`;
document.querySelectorAll('#sim-settings-group select, #sim-settings-group input').forEach(uiEl => {
uiEl.dataset.originalValue = uiEl.value;
});
simsCompleted++;
}
if (upgradeChanges.length === 0) {
statusDiv.textContent = 'Status: Settings updated.';
return;
}
const multiplier = parseInt(document.querySelector('#sim-settings-group [data-name="Multiplier"]').value) || 1;
for (const upgrade of upgradeChanges) {
if (!isBatchRunning) break;
allPageInputs.forEach(input => {
if (input.el && !specialIdMap[input.el.id]) {
const el = input.el;
if (el.tagName === 'SELECT') {
const opt = Array.from(el.options).find(o => o.text === input.originalValue);
if (opt) el.value = opt.value;
} else {
el.value = input.originalValue;
}
el.dispatchEvent(new Event('change', {
bubbles: true
}));
}
});
await new Promise(r => setTimeout(r, 50));
let upgradeLabelParts = [];
if (upgrade.isEnhancementOnly) {
const enhChange = upgrade.enhancement;
const itemName = allPageInputs.get(upgrade.name)?.originalValue || upgrade.name;
upgradeLabelParts.push(`${itemName} Enhancement: ${enhChange.originalValue} -> ${enhChange.value}`);
const input = allPageInputs.get(enhChange.name);
if (input && input.el) {
input.el.value = enhChange.value;
input.el.dispatchEvent(new Event('change', {
bubbles: true
}));
}
} else if (upgrade.isLevelOnly) {
const lvlChange = upgrade.level;
const itemName = allPageInputs.get(upgrade.name)?.originalValue || upgrade.name;
upgradeLabelParts.push(`${itemName} Level: ${lvlChange.originalValue} -> ${lvlChange.value}`);
const input = allPageInputs.get(lvlChange.name);
if (input && input.el) {
input.el.value = lvlChange.value;
input.el.dispatchEvent(new Event('change', {
bubbles: true
}));
}
} else {
const mainInput = allPageInputs.get(upgrade.name);
if (mainInput && mainInput.el) {
const el = mainInput.el;
if (el.tagName === 'SELECT') {
const opt = Array.from(el.options).find(o => o.text === upgrade.value);
if (opt) el.value = opt.value;
} else {
el.value = upgrade.value;
}
el.dispatchEvent(new Event('change', {
bubbles: true
}));
upgradeLabelParts.push(`${upgrade.name}: ${upgrade.originalValue} -> ${upgrade.value}`);
}
if (upgrade.enhancement) {
const enhInput = allPageInputs.get(`${upgrade.name} Enhancement`);
if (enhInput && enhInput.el) {
enhInput.el.value = upgrade.enhancement.value;
enhInput.el.dispatchEvent(new Event('change', {
bubbles: true
}));
}
upgradeLabelParts.push(`Enhancement: ${upgrade.enhancement.originalValue} -> ${upgrade.enhancement.value}`);
}
if (upgrade.level) {
const lvlInput = allPageInputs.get(`${upgrade.name} Level`);
if (lvlInput && lvlInput.el) {
lvlInput.el.value = upgrade.level.value;
lvlInput.el.dispatchEvent(new Event('change', {
bubbles: true
}));
}
upgradeLabelParts.push(`Level: ${upgrade.level.originalValue} -> ${upgrade.level.value}`);
}
}
statusDiv.textContent = `Status: Simulating (${upgradeChanges.indexOf(upgrade)+1}/${upgradeChanges.length}): ${upgradeLabelParts.join(' & ')}`;
const simResult = await runSimulationMultiple(multiplier, progress => updateOverallProgress(simsCompleted, progress));
const newDps = simResult.averageDps;
simsCompleted++;
if (isNaN(newDps)) {
console.error(`JIGS: Failed to get DPS for upgrade: ${upgrade.name}`);
continue;
}
const dpsGain = newDps - currentBaselineDps;
const percentChange = (currentBaselineDps > 0) ? (dpsGain / currentBaselineDps) * 100 : 0;
let cost = 0;
let booksNeeded = 0;
try {
if (houseKeywords.includes(upgrade.name)) {
const startLvl = parseInt(upgrade.originalValue);
const endLvl = parseInt(upgrade.value);
const roomRecipes = HOUSE_RECIPES[upgrade.name];
if (roomRecipes) {
for (let i = startLvl; i < endLvl; i++) {
const recipe = roomRecipes[i + 1];
if (recipe) {
cost += recipe.gold;
for (const materialId in recipe.materials) {
const materialName = itemIdToNameMap[materialId];
const price = marketData[materialName.toLowerCase()]?.seller === -1 ? Infinity : marketData[materialName.toLowerCase()]?.seller || Infinity;
if (!materialName || price === Infinity) {
cost = Infinity;
break;
}
cost += price * recipe.materials[materialId];
}
}
if (!isFinite(cost)) break;
}
}
} else if (skillKeywords.includes(upgrade.name)) {
cost = 0;
} else if (equipmentKeywords.includes(upgrade.name) || upgrade.isEnhancementOnly) {
const originalItemUI = document.querySelector(`#batch-inputs-container [data-name="${upgrade.name}"]`);
let oldSimName = upgrade.isEnhancementOnly ? originalItemUI.dataset.originalValue : upgrade.originalValue;
let newSimName = upgrade.value || originalItemUI.value;
const oldMarketName = SIMULATOR_TO_MARKET_MAP[oldSimName] || oldSimName;
const newMarketName = SIMULATOR_TO_MARKET_MAP[newSimName] || newSimName;
const enhJigsInput = document.querySelector(`#batch-inputs-container [data-name="${upgrade.name} Enhancement"]`);
const oldEnh = enhJigsInput?.dataset.originalValue || 0;
const newEnh = enhJigsInput?.value || 0;
const oldKey = oldEnh == 0 ? oldMarketName.toLowerCase() : `${oldMarketName.toLowerCase()} +${oldEnh}`;
const newKey = newEnh == 0 ? newMarketName.toLowerCase() : `${newMarketName.toLowerCase()} +${newEnh}`;
const oldPriceRaw = marketData[oldKey]?.buyer;
const newPriceRaw = marketData[newKey]?.seller;
const oldPrice = (oldPriceRaw === undefined || oldPriceRaw === -1) ? 0 : oldPriceRaw;
const newPrice = (newPriceRaw === undefined || newPriceRaw === -1) ? Infinity : newPriceRaw;
cost = newPrice - (oldMarketName === 'Empty' ? 0 : oldPrice);
} else if (upgrade.name.startsWith('Ability') || upgrade.isLevelOnly) {
const abilityName = upgrade.isLevelOnly ? allPageInputs.get(upgrade.name).originalValue : upgrade.value;
const marketItemName = abilityName;
const correctlyCasedKey = Object.keys(SPELL_BOOK_XP).find(k => k.toLowerCase() === marketItemName.toLowerCase());
const xpPerBook = correctlyCasedKey ? SPELL_BOOK_XP[correctlyCasedKey] : undefined;
if (abilityName === 'Empty' || xpPerBook === undefined) {
cost = 0;
} else {
const priceOfThisBook = marketData[marketItemName.toLowerCase()]?.seller === -1 ? Infinity : marketData[marketItemName.toLowerCase()]?.seller || Infinity;
const lvlInput = allPageInputs.get(`${upgrade.name.replace(' Level', '')} Level`);
const startLvl = upgrade.level ? upgrade.level.originalValue : lvlInput?.originalValue;
const endLvl = upgrade.level ? upgrade.level.value : lvlInput?.el.value;
if (Number(endLvl) > Number(startLvl)) {
const startXp = ABILITY_XP_LEVELS[startLvl] || 0;
const endXp = ABILITY_XP_LEVELS[endLvl] || 0;
const xpNeeded = endXp - startXp;
booksNeeded = Math.ceil(xpNeeded / xpPerBook);
cost = booksNeeded * priceOfThisBook;
}
}
}
} catch (e) {
console.error("JIGS Error during cost calculation:", e);
cost = 'N/A';
}
const costPerPercent = (percentChange > 0 && isFinite(cost) && cost !== 0) ? (cost / percentChange) * 0.01 : (cost === 0 && dpsGain > 0 ? "Free" : "N/A");
const resultData = {
upgrade: upgradeLabelParts.join(' & '),
cost: cost,
dps: dpsGain,
percent: percentChange,
costPerDps: costPerPercent,
books: booksNeeded,
averageDps: newDps,
individualRuns: simResult.individualRuns
};
addResultRow(resultData);
detailedResults.push(resultData);
}
} finally {
allPageInputs.forEach(input => {
if (input.el) {
const el = input.el;
if (el.tagName === 'SELECT') {
const opt = Array.from(el.options).find(o => o.text === input.originalValue);
if (opt) el.value = opt.value;
} else {
el.value = input.originalValue;
}
el.dispatchEvent(new Event('change', {
bubbles: true
}));
}
});
if (isBatchRunning) {
statusDiv.textContent = 'Status: Done!';
document.getElementById('export-csv-button').disabled = false;
}
setRunningState(false);
}
}
// --- 5. INITIALIZATION ---
async function initializeScript() {
if (!await fetchJigsData()) {
statusDiv.textContent = 'Error: Could not load critical JIGS data. The script cannot continue.';
return;
}
const panel = document.getElementById('batch-panel');
panel.addEventListener('click', (event) => {
const button = event.target.closest('button');
if (!button) return;
switch (button.id) {
case 'batch-toggle':
document.getElementById('batch-content').classList.toggle('hidden');
break;
case 'run-batch-button':
startBatch();
break;
case 'stop-batch-button':
isBatchRunning = false;
statusDiv.textContent = 'Status: Stopping...';
break;
case 'capture-setup-button':
buildInputsUI();
break;
case 'update-baseline-button':
updateBaseline();
break;
case 'export-csv-button':
exportResultsToCSV();
break;
case 'reset-button':
resetInputsToBaseline();
break;
}
});
document.querySelector('#batch-results-table thead').addEventListener('click', (event) => { const headerCell = event.target.closest('th'); if (!headerCell) return; const sortKey = headerCell.dataset.sortKey; if (!headerCell) return; const tbody = headerCell.closest('table').querySelector('tbody'); const rows = Array.from(tbody.querySelectorAll('tr')); const isDesc = headerCell.classList.contains('sorted-desc'); const direction = isDesc ? 1 : -1; rows.sort((a, b) => { const valA = a.dataset[sortKey]; const valB = b.dataset[sortKey]; const numA = parseFloat(valA); const numB = parseFloat(valB); if (!isNaN(numA) && !isNaN(numB)) { return (numA - numB) * direction; } return valA.localeCompare(valB) * direction; }); tbody.innerHTML = ''; rows.forEach(row => tbody.appendChild(row)); headerCell.parentElement.querySelectorAll('th').forEach(th => th.classList.remove('sorted-asc', 'sorted-desc')); if(direction === 1) { headerCell.classList.add('sorted-asc'); } else { headerCell.classList.add('sorted-desc'); } });
statusDiv.textContent = 'Status: Ready. Please import a character.';
document.getElementById('capture-setup-button').disabled = false;
const findButtonInterval = setInterval(() => {
const importButton = Array.from(document.querySelectorAll('button')).find(btn => btn.textContent.includes('Import solo/group'));
if (importButton) {
clearInterval(findButtonInterval);
importButton.addEventListener('click', () => {
statusDiv.textContent = 'Import initiated! Simulating...';
const resultsContainer = document.getElementById('simulationResultTotalDamageDone');
if(resultsContainer) resultsContainer.innerHTML = '';
const initialSimObserver = new MutationObserver(() => {
const dpsVal = getDpsValue();
if (dpsVal) {
initialSimObserver.disconnect();
baselineDps = parseFloat(dpsVal.replace(/,/g, ''));
document.getElementById('baseline-display').textContent = `Baseline DPS: ${baselineDps.toFixed(2)}`;
setTimeout(buildInputsUI, 200);
}
});
initialSimObserver.observe(document.body, { childList: true, subtree: true });
});
}
}, 500);
}
window.addEventListener('load', initializeScript);
})();