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 - DEBUG)
// @namespace http://tampermonkey.net/
// @version 28.5
// @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/
// @connect gist.githubusercontent.com
// @connect www.milkywayidle.com
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// ==/UserScript==
(function() {
'use strict';
console.log("JIGS (Jigglymoose's Intelligent Gear Simulator) v28.5-debug 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 VARIABLES ---
let HOUSE_RECIPES = {};
let ITEM_ID_TO_NAME_MAP = {};
let SPELL_BOOK_XP = {};
let SIMULATOR_TO_MARKET_MAP = {};
let ABILITY_XP_LEVELS = [];
let baselineDps = 0;
let baselineProfit = 0;
let baselineExp = 0;
let baselineEph = 0;
let marketData = null;
let isBatchRunning = false;
let detailedResults = [];
const skillKeywords = ["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 = { 'Zone': 'selectZone', 'Difficulty': 'selectDifficulty', 'Duration': 'inputSimulationTime' };
let houseKeywords = [];
// --- 1. UI & STYLES ---
const controlsPanel = document.createElement('div');
controlsPanel.id = 'batch-panel';
controlsPanel.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="export-csv-button" disabled>Export to CSV</button>
<button id="capture-setup-button" disabled>Capture Setup</button>
<button id="import-triggers-button" disabled>Import Triggers</button>
<button id="update-baseline-button" disabled>Update Baseline</button>
<div id="baseline-container">
<label for="baseline-dps-input">Baseline DPS</label><input type="text" id="baseline-dps-input" value="0">
<label for="baseline-profit-input">Profit/Day</label><input type="text" id="baseline-profit-input" value="0">
<label for="baseline-exp-input">Exp/Hour</label><input type="text" id="baseline-exp-input" value="0">
<label for="baseline-eph-input">Baseline EPH</label><input type="text" id="baseline-eph-input" value="0">
</div>
<button id="run-batch-button" disabled>Run Simulations</button>
<button id="reset-button" disabled>Reset to Baseline</button>
<button id="stop-batch-button" style="display: none;">Stop</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">
<div id="jigs-player-select-container"></div>
<details id="sim-settings-group" open><summary>Simulation Settings</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>
`;
document.body.appendChild(controlsPanel);
const resultsPanel = document.createElement('div');
resultsPanel.id = 'jigs-results-panel';
resultsPanel.innerHTML = `
<div id="jigs-results-header"><span>Results</span><button id="results-toggle">-</button></div>
<div id="jigs-results-content">
<div id="column-toggle-container">
Show Columns:
<label><input type="checkbox" class="column-toggle" data-col="ttp-col" checked> Time to Purchase</label>
<label><input type="checkbox" class="column-toggle" data-col="dps-col" checked> DPS</label>
<label><input type="checkbox" class="column-toggle" data-col="profit-col" checked> Profit</label>
<label><input type="checkbox" class="column-toggle" data-col="exp-col" checked> Experience</label>
<label><input type="checkbox" class="column-toggle" data-col="eph-col" checked> EPH</label>
</div>
<div id="batch-results-container">
<table id="batch-results-table">
<thead>
<tr>
<th class="upgrade-col" data-sort-key="upgrade" title="The specific upgrade being tested">Upgrade</th>
<th class="cost-col" data-sort-key="cost" title="The net cost of the upgrade.
Formula: (New Item Buy Price - Old Item Sell Price)">Upgrade Cost</th>
<th class="ttp-col" data-sort-key="timeToPurchase" title="Estimated time to afford this upgrade.
Formula: (Upgrade Cost / Baseline Profit Per Day)">Time to Purchase</th>
<th class="dps-col" data-sort-key="dpsChange" title="The raw DPS increase from this change.
Formula: New DPS - Baseline DPS">DPS Change</th>
<th class="dps-col" data-sort-key="percentChange" title="The percentage of DPS gained.
Formula: (DPS Change / Baseline DPS) * 100">% DPS Change</th>
<th class="dps-col" data-sort-key="costPerDps" title="Gold cost for every 0.01% increase in total DPS. Lower is better!
Formula: (Upgrade Cost / % DPS Change) * 0.01">Gold per 0.01% DPS</th>
<th class="profit-col" data-sort-key="profitChange" title="The raw profit increase from this change.
Formula: New Profit - Baseline Profit">Profit Change</th>
<th class="profit-col" data-sort-key="percentProfitChange" title="The percentage of profit gained.
Formula: (Profit Change / Baseline Profit) * 100">% Profit Change</th>
<th class="profit-col" data-sort-key="costPerProfit" title="Gold cost for every 0.01% increase in total Profit. Lower is better!
Formula: (Upgrade Cost / % Profit Change) * 0.01">Gold per 0.01% Profit</th>
<th class="exp-col" data-sort-key="expChange" title="The raw experience per hour increase from this change.
Formula: New Exp/Hr - Baseline Exp/Hr">Exp/Hr Change</th>
<th class="exp-col" data-sort-key="percentExpChange" title="The percentage of experience per hour gained.
Formula: (Exp Change / Baseline Exp) * 100">% Exp/Hr Change</th>
<th class="exp-col" data-sort-key="costPerExp" title="Gold cost for every 0.01% increase in total Exp/Hr. Lower is better!
Formula: (Upgrade Cost / % Exp Change) * 0.01">Gold per 0.01% Exp/Hr</th>
<th class="eph-col" data-sort-key="ephChange" title="The raw EPH increase from this change.
Formula: New EPH - Baseline EPH">EPH Change</th>
<th class="eph-col" data-sort-key="percentEphChange" title="The percentage of EPH gained.
Formula: (EPH Change / Baseline EPH) * 100">% EPH Change</th>
<th class="eph-col" data-sort-key="costPerEph" title="Gold cost for every 0.01% increase in total EPH. Lower is better!
Formula: (Upgrade Cost / % EPH Change) * 0.01">Gold per 0.01% EPH</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
`;
document.body.appendChild(resultsPanel);
GM_addStyle(`
#batch-panel { position: fixed; bottom: 10px; right: 10px; width: 600px; 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; }
#jigs-results-panel { position: fixed; bottom: 10px; right: 620px; width: 1050px; max-height: 90vh; background-color: #2c2c2c; border: 1px solid #444; border-radius: 5px; color: #eee; z-index: 9998; font-family: sans-serif; display: flex; flex-direction: column; }
#jigs-results-header { background-color: #333; padding: 8px; cursor: pointer; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #444; }
#jigs-results-header span { font-weight: bold; }
#results-toggle { background: #555; border: 1px solid #777; color: white; border-radius: 3px; cursor: pointer; margin-left: 5px; }
#jigs-results-content { padding: 10px; display: flex; flex-direction: column; overflow-y: auto; }
#jigs-results-content.hidden { display: none; }
#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(4, 1fr); grid-template-rows: auto auto; gap: 8px; margin-bottom: 10px; }
#stop-batch-button { background-color: #c9302c; grid-column: 3 / 4; grid-row: 2/3;}
#controls-grid button { width: 100%; padding: 8px; color: white; border: none; border-radius: 4px; cursor: pointer; }
#export-csv-button { grid-column: 1 / 2; grid-row: 1 / 2; background-color: #5bc0de; }
#capture-setup-button { grid-column: 2 / 3; grid-row: 1 / 2; background-color: #337ab7; }
#import-triggers-button { grid-column: 3 / 4; grid-row: 1 / 2; background-color: #6f42c1; }
#update-baseline-button { grid-column: 4 / 5; grid-row: 1 / 2; background-color: #f44336; }
#baseline-container { grid-column: 1 / 3; grid-row: 2 / 3; display: grid; grid-template-columns: auto 1fr; gap: 4px 8px; align-items: center; font-size: 0.9em; color: #ccc; }
#baseline-container input { background-color: #1e1e1e; color: #ddd; border: 1px solid #555; width: 100%; box-sizing: border-box; text-align: right; padding: 2px 4px; }
#run-batch-button { grid-column: 3 / 4; grid-row: 2 / 3; background-color: #4CAF50; }
#reset-button { grid-column: 4 / 5; 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, #import-triggers-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; }
#jigs-player-select-container { display: grid; grid-template-columns: 100px 1fr; align-items: center; margin-bottom: 10px; gap: 5px; padding-bottom: 10px; border-bottom: 1px solid #444;}
summary { font-weight: bold; cursor: pointer; padding: 4px; background-color: #333; margin-bottom: 5px; }
details { border-left: 1px solid #444; padding-left: 10px; margin-bottom: 5px;}
.batch-input-row { display: grid; grid-template-columns: 100px 1fr auto; align-items: center; margin-bottom: 5px; gap: 5px; }
.batch-input-row-equip { display: grid; grid-template-columns: 60px 1fr 80px auto; grid-template-rows: auto auto; align-items: center; margin-bottom: 10px; row-gap: 5px; column-gap: 5px; }
.batch-input-row-ability { display: grid; grid-template-columns: 60px 1fr 80px auto; 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; grid-row: 1; }
.batch-input-row select, .batch-input-row input, .batch-input-row-equip > select, .batch-input-row-equip > input[type=number], .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; }
.price-info-container { grid-column: 2 / 4; grid-row: 2 / 3; display: flex; align-items: center; gap: 10px; }
.jigs-price-override { width: 90px !important; background-color: #1e1e1e; color: #ddd; border: 1px solid #555; box-sizing: border-box; text-align: right; padding: 2px 4px; }
.constant-checkbox-container { display: flex; align-items: center; justify-content: center; padding: 0 5px; grid-row: 1; grid-column: 4; }
.jigs-constant-checkbox { title: 'Add as Constant'; }
.market-indicators { flex-grow: 1; display: flex; flex-wrap: wrap; gap: 4px; }
.market-dot { background-color: #555; color: #ddd; font-size: 0.8em; padding: 1px 5px; border-radius: 4px; cursor: pointer; border: 1px solid #777; }
.market-dot:hover { background-color: #777; border-color: #999; }
#house-grid-container { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 10px; }
.house-grid-item { display: grid; grid-template-columns: 1fr auto; grid-template-rows: auto auto; gap: 4px 8px; }
.house-grid-item label { grid-column: 1 / -1; font-size: 0.8em; margin-bottom: 4px; height: 2.5em; overflow: hidden; text-align: center; }
.house-grid-item input { grid-column: 1 / 2; width: 100%; text-align: center; }
.house-grid-item .constant-checkbox-container { grid-column: 2 / 3; grid-row: 2 / 3; }
#column-toggle-container { margin: 5px 0 10px 0; display: flex; gap: 15px; flex-wrap: wrap; justify-content: center; font-size: 0.9em; }
#batch-results-container { margin-top: 10px; max-height: 80vh; overflow-y: auto; }
#batch-results-table { width: 100%; border-collapse: collapse; }
#batch-results-table td.best-upgrade { background-color: #28a745 !important; color: #fff !important; }
#batch-results-table td.worst-upgrade { background-color: #dc3545 !important; color: #fff !important; }
#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; position: sticky; top: 0; z-index: 1; }
#batch-results-table th:hover { background-color: #444; }
.sorted-asc::after { content: ' ▲'; }
.sorted-desc::after { content: ' ▼'; }
.trigger-container { margin-left: 20px; margin-bottom: 10px; padding-top: 5px; border-top: 1px solid #444; }
.trigger-row, .trigger-range-row { display: grid; grid-template-columns: 60px 1fr 1fr 1fr 1fr; gap: 5px; align-items: center; }
.trigger-row label { font-size: 0.9em; font-style: italic; }
.trigger-range-row { margin-top: 5px; grid-template-columns: 60px auto 1fr auto 1fr; }
.trigger-range-row .jigs-range-label, .trigger-range-row .jigs-increment-label { font-size: 0.8em; font-style: italic; text-align: right; padding-right: 5px; }
.trigger-row select, .trigger-row input, .trigger-range-row input { background-color: #1e1e1e; color: #ddd; border: 1px solid #555; width: 100%; box-sizing: border-box; }
`);
// --- 3. HELPER FUNCTIONS ---
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'), };
function createTriggerRow(type, index) {
const container = document.createElement('div');
container.className = 'trigger-container';
const mainRow = document.createElement('div');
mainRow.className = 'trigger-row';
mainRow.dataset.triggerType = type;
mainRow.dataset.triggerIndex = index;
mainRow.innerHTML = `
<label>Trigger:</label>
<select class="jigs-trigger-dependency" data-field="dependency" title="Condition 1"><option value=""></option></select>
<select class="jigs-trigger-condition" data-field="condition" title="Condition 2"><option value=""></option></select>
<select class="jigs-trigger-comparator" data-field="comparator" title="Condition 3"><option value=""></option></select>
<input type="number" class="jigs-trigger-value" data-field="value" placeholder="Value" title="Condition 4">
`;
const rangeRow = document.createElement('div');
rangeRow.className = 'trigger-range-row';
rangeRow.innerHTML = `
<label></label> <label class="jigs-range-label">Range:</label>
<input type="text" class="jigs-trigger-range" placeholder="e.g., 2000-2500" data-original-value="">
<label class="jigs-increment-label">Increment:</label>
<input type="number" class="jigs-trigger-increment" placeholder="e.g., 100" data-original-value="">
`;
container.appendChild(mainRow);
container.appendChild(rangeRow);
return container;
}
function createConstantCheckbox() {
const container = document.createElement('div');
container.className = 'constant-checkbox-container';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.className = 'jigs-constant-checkbox';
checkbox.title = 'Add as Constant';
container.appendChild(checkbox);
return container;
}
function parseGold(value) { if (typeof value !== 'string') return NaN; value = value.toLowerCase().trim(); const num = parseFloat(value.replace(/,/g, '')); let result = num; if (value.endsWith('b')) { result = num * 1e9; } else if (value.endsWith('m')) { result = num * 1e6; } else if (value.endsWith('k')) { result = num * 1e3; } return result; }
function updateBaselinesFromInputs() { console.log("JIGS DEBUG: Reading baseline values from input fields."); const dpsStr = document.getElementById('baseline-dps-input').value; const profitStr = document.getElementById('baseline-profit-input').value; const expStr = document.getElementById('baseline-exp-input').value; const ephStr = document.getElementById('baseline-eph-input').value; baselineDps = parseGold(dpsStr) || 0; baselineProfit = parseGold(profitStr) || 0; baselineExp = parseFloat(expStr.replace(/,/g, '')) || 0; baselineEph = parseFloat(ephStr.replace(/,/g, '')) || 0; console.log(`JIGS DEBUG: Baselines updated to DPS: ${baselineDps} (from '${dpsStr}'), Profit: ${baselineProfit} (from '${profitStr}'), Exp: ${baselineExp} (from '${expStr}'), EPH: ${baselineEph} (from '${ephStr}')`); }
function createNumberInput(name, value, min, max, isHouse = false, withCheckbox = true) {
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);
if (withCheckbox) {
container.appendChild(createConstantCheckbox());
} else {
container.style.gridTemplateColumns = '100px 1fr';
}
return container;
}
function createSelect(name, value, options, withCheckbox = true) {
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);
if (withCheckbox) {
row.appendChild(createConstantCheckbox());
} else {
row.style.gridTemplateColumns = '100px 1fr';
}
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`;
const priceInfoContainer = document.createElement('div');
priceInfoContainer.className = 'price-info-container';
const priceOverrideInput = document.createElement('input');
priceOverrideInput.type = 'text';
priceOverrideInput.className = 'jigs-price-override';
priceOverrideInput.placeholder = 'Auto Price';
priceOverrideInput.title = 'Manual price override. Use "k", "m", "b". Clears on item/enh change.';
const indicatorContainer = document.createElement('div');
indicatorContainer.className = 'market-indicators';
priceInfoContainer.appendChild(indicatorContainer);
priceInfoContainer.appendChild(priceOverrideInput);
row.appendChild(label);
row.appendChild(itemSelect);
row.appendChild(enhInput);
row.appendChild(createConstantCheckbox());
row.appendChild(priceInfoContainer);
itemSelect.addEventListener('change', () => {
updateMarketIndicators(row);
updatePriceOverrideField(row);
});
enhInput.addEventListener('change', () => updatePriceOverrideField(row));
return row;
}
function createAbilityRow(name, itemValue, itemOptions, lvlValue) {
const row = document.createElement('div');
row.className = 'batch-input-row-ability';
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);
row.appendChild(createConstantCheckbox());
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 && 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);
let costPerProfitText = formatGold(result.costPerProfit);
let costPerExpText = formatGold(result.costPerExp);
let costPerEphText = formatGold(result.costPerEph);
if (result.cost === Infinity) {
costText = 'No Seller';
costPerDpsText = 'N/A';
costPerProfitText = 'N/A';
costPerExpText = 'N/A';
costPerEphText = '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.timeToPurchase = isFinite(result.timeToPurchase) ? result.timeToPurchase : Infinity;
row.dataset.dpsChange = result.dps;
row.dataset.percentChange = isFinite(result.percent) ? result.percent : Infinity;
row.dataset.costPerDps = result.costPerDps === 'Free' ? 0 : (isFinite(result.costPerDps) ? result.costPerDps : Infinity);
row.dataset.profitChange = result.profitChange;
row.dataset.percentProfitChange = isFinite(result.percentProfitChange) ? result.percentProfitChange : Infinity;
row.dataset.costPerProfit = result.costPerProfit === 'Free' ? 0 : (isFinite(result.costPerProfit) ? result.costPerProfit : Infinity);
row.dataset.expChange = result.expChange;
row.dataset.percentExpChange = isFinite(result.percentExpChange) ? result.percentExpChange : Infinity;
row.dataset.costPerExp = result.costPerExp === 'Free' ? 0 : (isFinite(result.costPerExp) ? result.costPerExp : Infinity);
row.dataset.ephChange = result.ephChange;
row.dataset.percentEphChange = isFinite(result.percentEphChange) ? result.percentEphChange : Infinity;
row.dataset.costPerEph = result.costPerEph === 'Free' ? 0 : (isFinite(result.costPerEph) ? result.costPerEph : Infinity);
row.innerHTML = ` <td class="upgrade-col">${upgradeText}</td>
<td class="cost-col">${costText}</td>
<td class="ttp-col">${formatTime(result.timeToPurchase)}</td>
<td class="dps-col">${result.dps > 0 ? '+' : ''}${result.dps.toFixed(2)}</td>
<td class="dps-col">${isFinite(result.percent) ? result.percent.toFixed(2)+'%' : '∞'}</td>
<td class="dps-col">${costPerDpsText}</td>
<td class="profit-col">${result.profitChange > 0 ? '+' : ''}${formatGold(result.profitChange)}</td>
<td class="profit-col">${isFinite(result.percentProfitChange) ? result.percentProfitChange.toFixed(2)+'%' : '∞'}</td>
<td class="profit-col">${costPerProfitText}</td>
<td class="exp-col">${result.expChange > 0 ? '+' : ''}${result.expChange.toLocaleString()}</td>
<td class="exp-col">${isFinite(result.percentExpChange) ? result.percentExpChange.toFixed(2)+'%' : '∞'}</td>
<td class="exp-col">${costPerExpText}</td>
<td class="eph-col">${result.ephChange > 0 ? '+' : ''}${result.ephChange.toFixed(2)}</td>
<td class="eph-col">${isFinite(result.percentEphChange) ? result.percentEphChange.toFixed(2)+'%' : '∞'}</td>
<td class="eph-col">${costPerEphText}</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`; }
function getProfitValue() {
const durationInput = document.getElementById('inputSimulationTime');
const durationHours = durationInput ? parseFloat(durationInput.value) : 24;
if (isNaN(durationHours) || durationHours <= 0) { return null; }
const durationInDays = durationHours / 24;
const newProfitElement = document.getElementById('noRngProfitPreview');
if (newProfitElement) {
const profitString = newProfitElement.textContent.replace(/,/g, '').trim();
const totalProfit = parseFloat(profitString);
if (!isNaN(totalProfit)) {
return totalProfit / durationInDays;
}
}
const profitElement = document.querySelector('div[i18n-id="i18n-realDailyProfitTitle"]');
if (!profitElement) { return null; }
let profitString = '';
if (profitElement.hasAttribute('i18n-data')) {
profitString = profitElement.getAttribute('i18n-data');
} else {
profitString = profitElement.textContent;
}
profitString = (profitString.split(':')[1] || '').replace(/,/g, '').trim();
if (profitString === '') { return null; }
const profitValue = parseFloat(profitString);
return isNaN(profitValue) ? null : profitValue;
}
function getExpValue() { const allTotalLabels = Array.from(document.querySelectorAll('div')).filter(el => el.textContent.trim() === 'Total'); const totalLabelElement = allTotalLabels.find(el => el.parentElement.classList.contains('row') && el.nextElementSibling); if (!totalLabelElement) { return null; } const expElement = totalLabelElement.nextElementSibling; if (expElement) { const expString = expElement.textContent.trim().replace(/,/g, ''); const expValue = parseFloat(expString); return isNaN(expValue) ? null : expValue; } return null; }
function getEphValue() { const ephLabel = document.querySelector('div[data-i18n="common:simulationResults.encounters"]'); if (!ephLabel) { return null; } const ephElement = ephLabel.nextElementSibling; if (ephElement) { const ephString = ephElement.textContent.trim().replace(/,/g, ''); const ephValue = parseFloat(ephString); return isNaN(ephValue) ? null : ephValue; } return null; }
function updateColumnVisibility() { document.querySelectorAll('.column-toggle').forEach(checkbox => { const columnClass = checkbox.dataset.col; const isVisible = checkbox.checked; document.querySelectorAll(`.${columnClass}`).forEach(el => el.style.display = isVisible ? '' : 'none'); }); }
function formatTime(days) { if (!isFinite(days) || days === Infinity) { return 'Never'; } if (days <= 0) { return 'Free'; } const hours = days * 24; if (hours < 1) { const minutes = hours * 60; return `${minutes.toFixed(0)} min`; } if (days < 1) { return `${hours.toFixed(1)} hrs`; } const months = days / 30.44; if (months >= 1) { return `${months.toFixed(1)} mon`; } return `${days.toFixed(1)} days`; }
function switchPlayerAndCapture(playerName) { const playerTabs = Array.from(document.querySelectorAll('a[id^="player"][id$="-tab"]')); const targetTab = playerTabs.find(tab => tab.textContent.trim() === playerName); if (targetTab) { statusDiv.textContent = `Switching to ${playerName}...`; targetTab.click(); setTimeout(() => { buildInputsUI(); }, 500); } else { statusDiv.textContent = `Error: Could not find player ${playerName}.`; } }
function populatePlayerDropdown() {
const container = document.getElementById('jigs-player-select-container');
if (!container) return;
container.innerHTML = '';
const playerTabs = Array.from(document.querySelectorAll('a[id^="player"][id$="-tab"]'));
if (playerTabs.length > 1) {
const playerNames = playerTabs.map(tab => tab.textContent.trim());
const activeTab = playerTabs.find(tab => tab.classList.contains('active'));
const currentPlayer = activeTab ? activeTab.textContent.trim() : playerNames[0];
const playerSelectRow = createSelect('Select Player', currentPlayer, playerNames, false);
const selectEl = playerSelectRow.querySelector('select');
if (selectEl) selectEl.id = 'jigs-player-select';
container.append(...playerSelectRow.childNodes);
container.className = 'batch-input-row';
container.style.gridTemplateColumns = '100px 1fr';
} else {
container.className = '';
}
}
function highlightResults() {
const rows = document.querySelectorAll('#batch-results-table tbody tr');
if (rows.length < 2) { return; }
document.querySelectorAll('#batch-results-table td').forEach(td => {
td.classList.remove('best-upgrade', 'worst-upgrade');
});
const metrics = [
{ key: 'costPerDps', colIndex: 5 },
{ key: 'costPerProfit', colIndex: 8 },
{ key: 'costPerExp', colIndex: 11 },
{ key: 'costPerEph', colIndex: 14 }
];
for (const metric of metrics) {
let values = [];
for (const row of rows) {
const valStr = row.dataset[metric.key];
const val = parseFloat(valStr);
if (isFinite(val)) {
values.push({ value: val, row: row });
}
}
if (values.length < 2) { continue; }
values.sort((a, b) => a.value - b.value);
const minVal = values[0].value;
const maxVal = values[values.length - 1].value;
if (minVal === maxVal) { continue; }
const minRow = values[0].row;
const maxRow = values[values.length - 1].row;
const minCell = minRow.children[metric.colIndex];
const maxCell = maxRow.children[metric.colIndex];
if (minCell) minCell.classList.add('best-upgrade');
if (maxCell) maxCell.classList.add('worst-upgrade');
}
}
function updateMarketIndicators(equipmentRow) {
if (!marketData) return;
const itemSelect = equipmentRow.querySelector('select');
const enhInput = equipmentRow.querySelector('input[type="number"]');
const indicatorContainer = equipmentRow.querySelector('.market-indicators');
if (!itemSelect || !enhInput || !indicatorContainer) return;
indicatorContainer.innerHTML = '';
const simItemName = itemSelect.value;
if (simItemName === 'Empty') return;
const marketItemName = SIMULATOR_TO_MARKET_MAP[simItemName] || simItemName;
const itemKeyBase = marketItemName.replace(/'/g, '').toLowerCase();
for (let i = 1; i <= 20; i++) {
const marketKey = `${itemKeyBase} +${i}`;
const priceData = marketData[marketKey];
if (priceData && priceData.seller && priceData.seller !== -1) {
const dot = document.createElement('div');
dot.className = 'market-dot';
dot.textContent = `+${i}`;
dot.title = `+${i}: ${formatGold(priceData.seller)}`;
dot.addEventListener('click', () => {
enhInput.value = i;
enhInput.dispatchEvent(new Event('change', { bubbles: true }));
});
indicatorContainer.appendChild(dot);
}
}
}
function updatePriceOverrideField(equipmentRow) {
if (!marketData) return;
const itemSelect = equipmentRow.querySelector('select');
const enhInput = equipmentRow.querySelector('input[type="number"]');
const priceInput = equipmentRow.querySelector('.jigs-price-override');
if (!itemSelect || !enhInput || !priceInput) return;
const simItemName = itemSelect.value;
const enhLevel = enhInput.value;
priceInput.value = '';
if (simItemName === 'Empty' || enhLevel === '0') return;
const marketItemName = SIMULATOR_TO_MARKET_MAP[simItemName] || simItemName;
const itemKeyBase = marketItemName.replace(/'/g, '').toLowerCase();
const marketKey = enhLevel === '0' ? itemKeyBase : `${itemKeyBase} +${enhLevel}`;
const priceData = marketData[marketKey];
if (priceData && priceData.seller && priceData.seller !== -1) {
priceInput.value = formatGold(priceData.seller);
}
}
// --- 4. CORE LOGIC ---
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.replace(/'/g, '').toLowerCase()] = { buyer: prices.b, seller: prices.a }; } } resolve(marketData); } catch (e) { resolve(null); } } else { resolve(null); } }, onerror: function() { resolve(null); } }); }); }
async function runSimulation(progressCallback) { return new Promise((resolve) => { let progressWatcher, dpsWatcher; const cleanup = () => { clearInterval(progressWatcher); clearInterval(dpsWatcher); }; 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;
document.getElementById('import-triggers-button').disabled = isRunning;
if (!isRunning) {
document.getElementById('jigs-progress-container').style.display = 'none';
}
}
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.'; }
async function buildInputsUI() {
statusDiv.textContent = 'Status: Reading data and fetching market prices...';
await fetchMarketData();
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;
document.getElementById('import-triggers-button').disabled = true;
const excludedRooms = [ "Shed", "Dairy Barn", "Garden", "Forge", "Workshop", "Sewing Parlor", "Kitchen", "Brewery", "Laboratory", "Observatory", "Log Shed" ];
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 = [];
populatePlayerDropdown();
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; const equipmentRow = createEquipmentRow(name, itemValue, itemOptions, enhValue); groupContainers.equipment.appendChild(equipmentRow); updateMarketIndicators(equipmentRow); updatePriceOverrideField(equipmentRow); itemsFound++; } });
for (let i = 0; i < 5; i++) { const abilitySelect = document.getElementById(`selectAbility_${i}`); const levelInput = document.getElementById(`inputAbilityLevel_${i}`); if (abilitySelect && levelInput) { const name = `Ability ${i + 1}`; 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(name, itemValue, itemOptions, lvlValue)); groupContainers.abilities.appendChild(createTriggerRow('ability', i)); itemsFound++; } }
document.querySelectorAll('select[id^="selectFood_"], select[id^="selectDrink_"]').forEach((el, i) => { const isFood = el.id.includes('Food'); const type = isFood ? 'food' : 'drink'; const indexInType = i % 3; const name = `${type} ${indexInType + 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)); groupContainers.food.appendChild(createTriggerRow(type, indexInType)); 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();
if (excludedRooms.includes(name)) return;
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);
const selectEl = createSelect(name, currentValue, options, false);
groupContainers.sim.appendChild(selectEl);
} else {
const numInput = createNumberInput(name, pageEl.value, pageEl.min, pageEl.max, false, false);
groupContainers.sim.appendChild(numInput);
}
itemsFound++;
}
}
const multInput = createNumberInput('Multiplier', currentMultiplier, 1, 100, false, false);
groupContainers.sim.appendChild(multInput);
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;
document.getElementById('import-triggers-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() {
console.log("JIGS DEBUG: updateBaseline started.");
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');
console.log("JIGS DEBUG: Applying sim setting changes before baseline run.");
simSettings.forEach(uiEl => {
if (!uiEl.dataset.name) return;
if (uiEl.value !== uiEl.dataset.originalValue) {
const pageEl = findPageElementByName(uiEl.dataset.name);
if (pageEl) {
console.log(`JIGS DEBUG: Changing '${uiEl.dataset.name}' on page to '${uiEl.value}'`);
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;
console.log(`JIGS DEBUG: Running baseline simulation with multiplier: ${multiplier}`);
const simResult = await runSimulationMultiple(multiplier, progress => { jigsProgressBar.style.width = `${progress}%`; });
const newBaseline = simResult.averageDps;
console.log(`JIGS DEBUG: Baseline sim complete. Result:`, simResult);
if (!isNaN(newBaseline)) {
baselineDps = newBaseline;
baselineProfit = getProfitValue() || 0;
baselineExp = getExpValue() || 0;
baselineEph = getEphValue() || 0;
console.log(`JIGS DEBUG: New baselines set -> DPS: ${baselineDps}, Profit: ${baselineProfit}, Exp: ${baselineExp}, EPH: ${baselineEph}`);
document.getElementById('baseline-dps-input').value = baselineDps.toFixed(2);
document.getElementById('baseline-profit-input').value = formatGold(baselineProfit);
document.getElementById('baseline-exp-input').value = baselineExp.toLocaleString();
document.getElementById('baseline-eph-input').value = baselineEph.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.';
console.error("JIGS DEBUG: Failed to update baseline; simulation returned NaN.");
}
} finally {
setRunningState(false);
}
}
async function applyTriggerChanges(triggerData) {
const type = triggerData.type;
const index = triggerData.index;
const typeCap = type.charAt(0).toUpperCase() + type.slice(1);
const buttonId = `button${typeCap}Trigger_${index}`;
const triggerButton = document.getElementById(buttonId);
if (!triggerButton) {
console.error(`JIGS: Could not find trigger button ${buttonId} to apply changes.`);
return;
}
triggerButton.click();
await new Promise(r => setTimeout(r, 100));
const modal = document.getElementById('triggerModal');
if (!modal || !modal.classList.contains('show')) {
// If modal fails to open, try to find a close button just in case one is lingering, then throw
const closeBtn = document.querySelector('button.btn-close[data-bs-dismiss="modal"]');
if(closeBtn) closeBtn.click();
throw new Error(`Modal for ${buttonId} did not open for applying trigger changes.`);
}
const setPageSelect = (selectElement, textToFind) => {
const option = Array.from(selectElement.options).find(o => o.text === textToFind);
if (option) {
selectElement.value = option.value;
} else if (textToFind === "") {
selectElement.value = "";
}
selectElement.dispatchEvent(new Event('change', { bubbles: true }));
};
setPageSelect(document.getElementById('selectTriggerDependency_0'), triggerData.dependency);
setPageSelect(document.getElementById('selectTriggerCondition_0'), triggerData.condition);
setPageSelect(document.getElementById('selectTriggerComparator_0'), triggerData.comparator);
const valInput = document.getElementById('inputTriggerValue_0');
valInput.value = triggerData.value;
valInput.dispatchEvent(new Event('input', { bubbles: true }));
valInput.dispatchEvent(new Event('change', { bubbles: true }));
document.getElementById('buttonTriggerModalSave').click();
await new Promise(r => setTimeout(r, 100));
}
async function startBatch() {
console.log("JIGS DEBUG: startBatch started.");
updateBaselinesFromInputs();
let allPageInputs = new Map();
let lastModifiedTriggers = [];
setRunningState(true);
try {
await fetchMarketData();
if (!isBatchRunning) { statusDiv.textContent = 'Status: Stopped by user.'; return; }
if (!marketData) { console.error("JIGS DEBUG: Market data not available."); return; }
if (baselineDps === 0) { statusDiv.textContent = 'Error: Please set a baseline first.'; console.warn("JIGS DEBUG: baselineDps is 0."); 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:not([type=checkbox]), #batch-inputs-container select").forEach(el => {
const triggerContainer = el.closest('.trigger-container');
if (el.id !== 'jigs-player-select' && !el.classList.contains('jigs-price-override') && !triggerContainer && !el.disabled && el.dataset.name && el.value !== el.dataset.originalValue) {
const row = el.closest('.batch-input-row, .batch-input-row-equip, .batch-input-row-ability, .house-grid-item');
const isConstant = row.querySelector('.jigs-constant-checkbox')?.checked || false;
allChanges.push({ element: el, name: el.dataset.name, value: el.value, originalValue: el.dataset.originalValue, isConstant: isConstant });
}
});
let allTriggerChanges = [];
document.querySelectorAll('.trigger-container').forEach(container => {
const mainRow = container.querySelector('.trigger-row');
const rangeRow = container.querySelector('.trigger-range-row');
const depEl = mainRow.querySelector('.jigs-trigger-dependency');
const condEl = mainRow.querySelector('.jigs-trigger-condition');
const compEl = mainRow.querySelector('.jigs-trigger-comparator');
const valEl = mainRow.querySelector('.jigs-trigger-value');
const rangeEl = rangeRow.querySelector('.jigs-trigger-range');
const incEl = rangeRow.querySelector('.jigs-trigger-increment');
const hasChanged = depEl.value !== depEl.dataset.originalValue ||
condEl.value !== condEl.dataset.originalValue ||
compEl.value !== compEl.dataset.originalValue ||
valEl.value !== valEl.dataset.originalValue ||
rangeEl.value !== rangeEl.dataset.originalValue ||
incEl.value !== incEl.dataset.originalValue;
if (hasChanged) {
allTriggerChanges.push({
type: mainRow.dataset.triggerType,
index: mainRow.dataset.triggerIndex,
isConstant: false,
data: { dependency: depEl.value, condition: condEl.value, comparator: compEl.value, value: valEl.value },
range: rangeEl.value.trim(),
increment: incEl.value.trim()
});
}
});
const groupAllChanges = (itemChanges, triggerChanges) => {
const upgrades = [];
const processed = new Set();
const nameMap = new Map(itemChanges.map(c => [c.name, c]));
for (const change of itemChanges) {
if (processed.has(change.name)) continue;
let baseName = change.name.replace(' Enhancement', '').replace(' Level', '');
if (processed.has(baseName)) continue;
processed.add(baseName);
let combinedUpgrade = { name: baseName, isConstant: change.isConstant };
const mainChange = nameMap.get(baseName);
const enhChange = nameMap.get(`${baseName} Enhancement`);
const lvlChange = nameMap.get(`${baseName} Level`);
if (mainChange) { combinedUpgrade = { ...combinedUpgrade, ...mainChange }; }
if (enhChange) { combinedUpgrade.enhancement = enhChange; }
if (lvlChange) { combinedUpgrade.level = lvlChange; }
if (baseName.startsWith('Ability') || baseName.startsWith('Food') || baseName.startsWith('Drink')) {
const type = baseName.startsWith('Ability') ? 'ability' : (baseName.startsWith('Food') ? 'food' : 'drink');
const index = parseInt(baseName.match(/\d+/)[0]) - 1;
const triggerIndex = triggerChanges.findIndex(t => t.type === type && t.index == index);
if (triggerIndex > -1) {
combinedUpgrade.triggerChange = triggerChanges[triggerIndex];
triggerChanges.splice(triggerIndex, 1);
}
}
upgrades.push(combinedUpgrade);
}
for (const trigger of triggerChanges) {
upgrades.push({ name: `Trigger ${trigger.type} ${parseInt(trigger.index) + 1}`, isTriggerOnly: true, triggerChange: trigger, isConstant: trigger.isConstant });
}
return upgrades;
};
const allGroupedChanges = groupAllChanges(allChanges, allTriggerChanges);
const constantUpgrades = allGroupedChanges.filter(c => c.isConstant);
const individualUpgrades = allGroupedChanges.filter(c => !c.isConstant);
const simulationsToRun = [];
for (const individual of individualUpgrades) {
const tc = individual.triggerChange;
const hasRange = tc && tc.range && tc.increment;
if (hasRange) {
const [startStr, endStr] = tc.range.split('-').map(s => s.trim());
const start = parseInt(startStr);
const end = parseInt(endStr);
const increment = parseInt(tc.increment);
if (!isNaN(start) && !isNaN(end) && !isNaN(increment) && increment > 0 && end >= start) {
for (let value = start; value <= end; value += increment) {
const simUpgrade = JSON.parse(JSON.stringify(individual));
simUpgrade.triggerChange.data.value = value;
simUpgrade.customLabel = `${simUpgrade.isTriggerOnly ? simUpgrade.name : (simUpgrade.name + ': ' + simUpgrade.originalValue + ' -> ' + simUpgrade.value)} (Trigger Val: ${value})`;
simulationsToRun.push({ upgrades: [...constantUpgrades, simUpgrade] });
}
} else {
simulationsToRun.push({ upgrades: [...constantUpgrades, individual] });
}
} else {
simulationsToRun.push({ upgrades: [...constantUpgrades, individual] });
}
}
if (individualUpgrades.length === 0 && constantUpgrades.length > 0) {
simulationsToRun.push({ upgrades: constantUpgrades });
}
console.log(`JIGS DEBUG: Found ${simulationsToRun.length} simulations to run.`);
if (simulationsToRun.length === 0) { statusDiv.textContent = 'Error: No changes to simulate.'; return; }
document.querySelectorAll("#batch-inputs-container input:not(.jigs-constant-checkbox), #batch-inputs-container select").forEach(uiEl => {
if (!uiEl.dataset.name) return;
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 }); }
});
const multiplier = parseInt(document.querySelector('#sim-settings-group [data-name="Multiplier"]').value) || 1;
let simsCompleted = 0;
for (const simulation of simulationsToRun) {
if (!isBatchRunning) break;
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 })); }});
for (const triggerToReset of lastModifiedTriggers) {
const type = triggerToReset.type;
const index = triggerToReset.index;
const triggerRow = document.querySelector(`.trigger-row[data-trigger-type="${type}"][data-trigger-index="${index}"]`);
if (triggerRow) {
const originalState = {
type: type, index: index,
dependency: triggerRow.querySelector('.jigs-trigger-dependency').dataset.originalValue,
condition: triggerRow.querySelector('.jigs-trigger-condition').dataset.originalValue,
comparator: triggerRow.querySelector('.jigs-trigger-comparator').dataset.originalValue,
value: triggerRow.querySelector('.jigs-trigger-value').dataset.originalValue
};
await applyTriggerChanges(originalState);
}
}
lastModifiedTriggers = [];
await new Promise(r => setTimeout(r, 50));
let upgradeLabelParts = []; let totalCost = 0; let totalBooks = 0;
for (const upgrade of simulation.upgrades) {
let partLabel = upgrade.customLabel || '';
if (!partLabel) {
if (!upgrade.isTriggerOnly) {
const baseName = upgrade.name;
if (upgrade.value !== upgrade.originalValue) { partLabel = `${baseName}: ${upgrade.originalValue} -> ${upgrade.value}`; } else { partLabel = baseName; }
if (upgrade.enhancement) { partLabel += ` & Enh: ${upgrade.enhancement.originalValue} -> ${upgrade.enhancement.value}`; }
if (upgrade.level) { partLabel += ` & Lvl: ${upgrade.level.originalValue} -> ${upgrade.level.value}`; }
}
if (upgrade.triggerChange) {
let triggerLabel = `${upgrade.isTriggerOnly ? upgrade.name : ''} Trigger Change`;
partLabel = partLabel ? `${partLabel} & ${triggerLabel}` : triggerLabel;
}
}
if (!upgrade.isTriggerOnly) {
if (upgrade.value) { 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 })); }}
if (upgrade.enhancement) { const enh = upgrade.enhancement; const enhInput = allPageInputs.get(enh.name); if(enhInput && enhInput.el) { enhInput.el.value = enh.value; enhInput.el.dispatchEvent(new Event('change', { bubbles: true })); } }
if (upgrade.level) { const lvl = upgrade.level; const lvlInput = allPageInputs.get(lvl.name); if(lvlInput && lvlInput.el) { lvlInput.el.value = lvl.value; lvlInput.el.dispatchEvent(new Event('change', { bubbles: true })); } }
}
if (upgrade.triggerChange) {
const tc = upgrade.triggerChange;
await applyTriggerChanges({ ...tc.data, type: tc.type, index: tc.index });
lastModifiedTriggers.push(tc);
}
upgradeLabelParts.push(partLabel);
}
const finalLabel = upgradeLabelParts.filter(p => p).join(' & ');
statusDiv.textContent = `Status: Simulating (${simsCompleted + 1}/${simulationsToRun.length}): ${finalLabel}`;
const simResult = await runSimulationMultiple(multiplier, progress => { const totalProgress = ((simsCompleted + (progress / 100)) / simulationsToRun.length) * 100; jigsProgressBar.style.width = `${totalProgress}%`; });
const newDps = simResult.averageDps; const newProfit = getProfitValue() || 0; const newExp = getExpValue() || 0; const newEph = getEphValue() || 0;
simsCompleted++;
if (isNaN(newDps)) { console.error(`JIGS DEBUG: DPS is NaN for this upgrade. Skipping.`); continue; }
for (const upgrade of simulation.upgrades) {
if (upgrade.isTriggerOnly) continue;
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++) { if (!isFinite(cost)) break; const recipe = roomRecipes[i + 1]; if (recipe) { cost += recipe.gold; for (const materialId in recipe.materials) { const materialName = ITEM_ID_TO_NAME_MAP[materialId]; if (!materialName) { cost = Infinity; break; } const materialKey = materialName.replace(/'/g, '').toLowerCase(); const price = marketData[materialKey]?.seller === -1 ? Infinity : marketData[materialKey]?.seller || Infinity; if (price === Infinity) { cost = Infinity; break; } cost += price * recipe.materials[materialId]; } } } }
} else if (skillKeywords.includes(upgrade.name)) { cost = 0;
} else if (equipmentKeywords.includes(upgrade.name)) {
const baseName = upgrade.name;
const upgradeRow = Array.from(document.querySelectorAll('.batch-input-row-equip')).find(r => r.querySelector(`[data-name="${baseName}"]`));
const priceOverrideInput = upgradeRow ? upgradeRow.querySelector('.jigs-price-override') : null;
let newPrice = Infinity;
if (priceOverrideInput && priceOverrideInput.value.trim() !== '') { const parsedPrice = parseGold(priceOverrideInput.value); if (isFinite(parsedPrice)) { newPrice = parsedPrice; }
} else {
let newSimName = upgrade.value || allPageInputs.get(baseName).originalValue;
const newMarketName = SIMULATOR_TO_MARKET_MAP[newSimName] || newSimName;
const newEnh = upgrade.enhancement ? upgrade.enhancement.value : (upgradeRow ? upgradeRow.querySelector('input[type="number"]').value : 0);
const newKey = newEnh == 0 ? newMarketName.replace(/'/g, '').toLowerCase() : `${newMarketName.replace(/'/g, '').toLowerCase()} +${newEnh}`;
const newPriceRaw = marketData[newKey]?.seller;
newPrice = (newPriceRaw === undefined || newPriceRaw === -1) ? Infinity : newPriceRaw;
}
let oldSimName = upgrade.originalValue;
const oldMarketName = SIMULATOR_TO_MARKET_MAP[oldSimName] || oldSimName;
const oldEnh = upgrade.enhancement ? upgrade.enhancement.originalValue : allPageInputs.get(`${baseName} Enhancement`)?.originalValue || 0;
const oldKey = oldEnh == 0 ? oldMarketName.replace(/'/g, '').toLowerCase() : `${oldMarketName.replace(/'/g, '').toLowerCase()} +${oldEnh}`;
const oldPriceRaw = marketData[oldKey]?.buyer;
const oldPrice = (oldPriceRaw === undefined || oldPriceRaw === -1) ? 0 : oldPriceRaw;
cost = newPrice - (oldMarketName === 'Empty' ? 0 : oldPrice);
} else if (upgrade.name.startsWith('Ability')) {
const baseName = upgrade.name;
const abilityName = upgrade.value || allPageInputs.get(baseName).originalValue;
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 materialKey = marketItemName.replace(/'/g, '').toLowerCase(); const priceOfThisBook = marketData[materialKey]?.seller === -1 ? Infinity : marketData[materialKey]?.seller || Infinity;
const startLvl = upgrade.level ? upgrade.level.originalValue : allPageInputs.get(`${baseName} Level`)?.originalValue;
const endLvl = upgrade.level ? upgrade.level.value : allPageInputs.get(`${baseName} Level`)?.originalValue;
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) { cost = Infinity; }
if (isFinite(cost)) { totalCost += cost; } else { totalCost = Infinity; }
totalBooks += booksNeeded;
}
const dpsGain = newDps - baselineDps; const percentChange = (baselineDps > 0) ? (dpsGain / baselineDps) * 100 : (dpsGain > 0 ? Infinity : 0);
const profitChange = newProfit - baselineProfit; const percentProfitChange = (baselineProfit > 0) ? (profitChange / baselineProfit) * 100 : (profitChange > 0 ? Infinity : 0);
const expChange = newExp - baselineExp; const percentExpChange = (baselineExp > 0) ? (expChange / baselineExp) * 100 : (expChange > 0 ? Infinity : 0);
const ephChange = newEph - baselineEph; const percentEphChange = (baselineEph > 0) ? (ephChange / baselineEph) * 100 : (ephChange > 0 ? Infinity : 0);
const costPerPercent = (percentChange > 0 && isFinite(totalCost) && totalCost !== 0) ? (totalCost / percentChange) * 0.01 : (totalCost === 0 && dpsGain > 0 ? "Free" : "N/A");
const costPerProfitPercent = (percentProfitChange > 0 && isFinite(totalCost) && totalCost !== 0) ? (totalCost / percentProfitChange) * 0.01 : (totalCost === 0 && profitChange > 0 ? "Free" : "N/A");
const costPerExpPercent = (percentExpChange > 0 && isFinite(totalCost) && totalCost !== 0) ? (totalCost / percentExpChange) * 0.01 : (totalCost === 0 && expChange > 0 ? "Free" : "N/A");
const costPerEphPercent = (percentEphChange > 0 && isFinite(totalCost) && totalCost !== 0) ? (totalCost / percentEphChange) * 0.01 : (totalCost === 0 && ephChange > 0 ? "Free" : "N/A");
const timeToPurchaseDays = (baselineProfit > 0 && isFinite(totalCost)) ? (totalCost / baselineProfit) : Infinity;
const resultData = { upgrade: finalLabel, cost: totalCost, timeToPurchase: timeToPurchaseDays, dps: dpsGain, percent: percentChange, costPerDps: costPerPercent, books: totalBooks, averageDps: newDps, individualRuns: simResult.individualRuns, profitChange: profitChange, percentProfitChange: percentProfitChange, costPerProfit: costPerProfitPercent, expChange: expChange, percentExpChange: percentExpChange, costPerExp: costPerExpPercent, ephChange: ephChange, percentEphChange: percentEphChange, costPerEph: costPerEphPercent };
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 }));
}
});
for (const triggerToReset of lastModifiedTriggers) {
const type = triggerToReset.type;
const index = triggerToReset.index;
const triggerRow = document.querySelector(`.trigger-container [data-trigger-type="${type}"][data-trigger-index="${index}"]`);
if (triggerRow) {
const originalState = {
type: type, index: index,
dependency: triggerRow.querySelector('.jigs-trigger-dependency').dataset.originalValue,
condition: triggerRow.querySelector('.jigs-trigger-condition').dataset.originalValue,
comparator: triggerRow.querySelector('.jigs-trigger-comparator').dataset.originalValue,
value: triggerRow.querySelector('.jigs-trigger-value').dataset.originalValue
};
await applyTriggerChanges(originalState);
}
}
if (isBatchRunning) { statusDiv.textContent = 'Status: Done!'; document.getElementById('export-csv-button').disabled = false; }
setRunningState(false);
highlightResults();
updateColumnVisibility();
}
}
async function importTriggers() {
console.log("JIGS DEBUG: Starting trigger import.");
statusDiv.textContent = 'Status: Importing triggers...';
setRunningState(true);
try {
const itemTypes = [
{ type: 'ability', count: 5, prefix: 'buttonAbilityTrigger_' },
{ type: 'food', count: 3, prefix: 'buttonFoodTrigger_' },
{ type: 'drink', count: 3, prefix: 'buttonDrinkTrigger_' }
];
for (const item of itemTypes) {
for (let i = 0; i < item.count; i++) {
if (!isBatchRunning) throw new Error('User stopped');
const buttonId = `${item.prefix}${i}`;
const triggerButton = document.getElementById(buttonId);
const jigsTriggerContainer = document.querySelector(`.trigger-container [data-trigger-type="${item.type}"][data-trigger-index="${i}"]`)?.closest('.trigger-container');
if (!triggerButton || !jigsTriggerContainer) continue;
statusDiv.textContent = `Importing for ${item.type} ${i + 1}...`;
triggerButton.click();
await new Promise(r => setTimeout(r, 250));
const modal = document.getElementById('triggerModal');
if (!modal || !modal.classList.contains('show')) {
console.warn(`JIGS: Modal for ${buttonId} did not open.`);
const closeBtn = document.querySelector('#triggerModal button.btn-close');
if (closeBtn) closeBtn.click();
await new Promise(r => setTimeout(r, 250));
continue;
}
const scrapeAndSetSelect = (pageSelect, jigsSelect) => {
jigsSelect.innerHTML = '';
const selectedText = pageSelect.selectedOptions.length > 0 ? pageSelect.selectedOptions[0].text : '';
for (const option of pageSelect.options) {
jigsSelect.add(new Option(option.text, option.text));
}
jigsSelect.value = selectedText;
jigsSelect.dataset.originalValue = selectedText;
};
scrapeAndSetSelect(document.getElementById('selectTriggerDependency_0'), jigsTriggerContainer.querySelector('.jigs-trigger-dependency'));
scrapeAndSetSelect(document.getElementById('selectTriggerCondition_0'), jigsTriggerContainer.querySelector('.jigs-trigger-condition'));
scrapeAndSetSelect(document.getElementById('selectTriggerComparator_0'), jigsTriggerContainer.querySelector('.jigs-trigger-comparator'));
const valEl = document.getElementById('inputTriggerValue_0');
const jigsVal = jigsTriggerContainer.querySelector('.jigs-trigger-value');
jigsVal.value = valEl.value;
jigsVal.dataset.originalValue = valEl.value;
const rangeEl = jigsTriggerContainer.querySelector('.jigs-trigger-range');
rangeEl.value = '';
rangeEl.dataset.originalValue = '';
const incEl = jigsTriggerContainer.querySelector('.jigs-trigger-increment');
incEl.value = '';
incEl.dataset.originalValue = '';
document.querySelector('#triggerModal button.btn-close').click();
await new Promise(r => setTimeout(r, 250));
}
}
statusDiv.textContent = 'Status: Trigger import complete.';
} catch (e) {
if (e.message === 'User stopped') {
statusDiv.textContent = 'Status: Trigger import stopped by user.';
} else {
console.error('JIGS: Error during trigger import.', e);
statusDiv.textContent = 'Error during trigger import. Check console.';
}
} finally {
setRunningState(false);
}
}
// --- 5. INITIALIZATION ---
async function initializeScript() {
console.log("JIGS DEBUG: Step 1 - initializeScript started.");
if (!await fetchJigsData()) {
statusDiv.textContent = 'Error: Could not load critical JIGS data. The script cannot continue.';
return;
}
console.log("JIGS DEBUG: Step 2 - fetchJigsData successful.");
const controlsPanel = document.getElementById('batch-panel');
controlsPanel.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...'; const stopSimButton = document.getElementById('buttonStopSimulation'); if (stopSimButton) { stopSimButton.click(); } break;
case 'capture-setup-button': buildInputsUI(); break;
case 'update-baseline-button': updateBaseline(); break;
case 'export-csv-button': exportResultsToCSV(); break;
case 'reset-button': resetInputsToBaseline(); break;
case 'import-triggers-button': importTriggers(); break;
}
});
const resultsPanel = document.getElementById('jigs-results-panel');
resultsPanel.addEventListener('click', (event) => {
const button = event.target.closest('button');
if (!button) return;
if (button.id === 'results-toggle') {
document.getElementById('jigs-results-content').classList.toggle('hidden');
}
});
controlsPanel.addEventListener('change', async (event) => {
if (event.target.id === 'jigs-player-select') {
const selectedPlayerName = event.target.value;
switchPlayerAndCapture(selectedPlayerName);
}
});
const columnToggleContainer = document.getElementById('column-toggle-container');
if (columnToggleContainer) {
columnToggleContainer.addEventListener('change', updateColumnVisibility);
}
document.getElementById('baseline-dps-input').addEventListener('change', updateBaselinesFromInputs);
document.getElementById('baseline-profit-input').addEventListener('change', updateBaselinesFromInputs);
document.getElementById('baseline-exp-input').addEventListener('change', updateBaselinesFromInputs);
document.getElementById('baseline-eph-input').addEventListener('change', updateBaselinesFromInputs);
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! Waiting for player names and initial sim...';
const resultsContainer = document.getElementById('simulationResultTotalDamageDone');
if (resultsContainer) resultsContainer.innerHTML = '';
const baselineObserver = new MutationObserver(() => {
const dpsVal = getDpsValue();
if (dpsVal) {
baselineObserver.disconnect();
baselineDps = parseFloat(dpsVal.replace(/,/g, ''));
baselineProfit = getProfitValue() || 0;
baselineExp = getExpValue() || 0;
baselineEph = getEphValue() || 0;
document.getElementById('baseline-dps-input').value = baselineDps.toFixed(2);
document.getElementById('baseline-profit-input').value = formatGold(baselineProfit);
document.getElementById('baseline-exp-input').value = baselineExp.toLocaleString();
document.getElementById('baseline-eph-input').value = baselineEph.toFixed(2);
}
});
baselineObserver.observe(document.body, { childList: true, subtree: true });
let uiBuildAttempt = 0;
const uiBuilderInterval = setInterval(() => {
uiBuildAttempt++;
const playerTabs = Array.from(document.querySelectorAll('a[id^="player"][id$="-tab"]'));
const namesAreLoaded = playerTabs.length > 0 && playerTabs.some(tab => !tab.textContent.trim().startsWith('Player '));
if (namesAreLoaded) {
clearInterval(uiBuilderInterval);
buildInputsUI();
} else if (uiBuildAttempt > 50) {
clearInterval(uiBuilderInterval);
statusDiv.textContent = 'Status: Could not detect player names. Please use Capture Setup.';
}
}, 200);
});
}
}, 500);
updateColumnVisibility();
}
initializeScript();
})();