JIGS (Jigglymoose's Intelligent Gear Simulator)

Automates running multiple simulations on the MWI Combat Simulator with a dynamic, grouped UI and cost-analysis.

当前为 2025-09-23 提交的版本,查看 最新版本

// ==UserScript==
// @name         JIGS (Jigglymoose's Intelligent Gear Simulator)
// @namespace    http://tampermonkey.net/
// @version      17.1
// @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';
    /* global bootstrap */ // Informs the linter that 'bootstrap' is a global variable provided by the page.

    console.log("JIGS (Jigglymoose's Intelligent Gear Simulator) v17.1 Loaded");

    // --- CONFIGURATION ---
    const XP_PER_SPELLBOOK = 25;
    const MARKET_API_URL = 'https://www.milkywayidle.com/game_data/marketplace.json';

    // --- 1. UI & STYLES ---
    const panel = document.createElement('div');
    panel.id = 'batch-panel';
    panel.innerHTML = `
        <div id="batch-header"><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="refresh-data-button" disabled>Refresh Data</button>
                <span id="baseline-display">Baseline DPS: --</span>
                <button id="update-baseline-button" disabled>Update Baseline</button>
            </div>
            <div id="batch-status">Status: Please import a character...</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">Upgrade</th><th data-sort-key="dpsChange">DPS Change</th><th data-sort-key="percentChange">% Change</th><th data-sort-key="costPerDps">Gold per 0.01% DPS</th></tr></thead><tbody></tbody></table>
            </div>
            <button id="jigs-open-debug-button" title="Open Debug Log" style="display: none;">⚙️</button>
        </div>
        <div class="modal" id="jigs-debug-modal" tabindex="-1">
            <div class="modal-dialog modal-lg">
                <div class="modal-content">
                    <div class="modal-header">
                        <h5 class="modal-title">JIGS Debug Output</h5>
                        <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
                    </div>
                    <div class="modal-body">
                        <pre id="jigs-debug-output" style="width: 100%; white-space: pre-wrap; word-break: break-all; max-height: 60vh; overflow-y: auto;"></pre>
                    </div>
                </div>
            </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; }
        #jigs-open-debug-button { position: absolute; bottom: 5px; right: 5px; z-index: 100; background: #555; border: 1px solid #777; color: white; border-radius: 3px; cursor: pointer; padding: 1px 6px; font-size: 1.1em; }
        #controls-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 10px; }
        #controls-grid button { width: 100%; padding: 8px; color: white; border: none; border-radius: 4px; cursor: pointer; }
        #baseline-display { grid-column: 1 / 2; text-align: center; align-self: center; font-size: 0.9em; color: #ccc; }
        #run-batch-button { background-color: #4CAF50; } #refresh-data-button { background-color: #008CBA; } #update-baseline-button { background-color: #f44336; }
        #run-batch-button:disabled, #refresh-data-button:disabled, #update-baseline-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, #jigs-debug-output { background-color: #1e1e1e; color: #ddd; border: 1px solid #555; width: 100%; box-sizing: border-box; font-family: monospace; }
        #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; }
        #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: ' ▼'; }
        .debug-log-entry { display: block; } .debug-normal { color: #bbbbbb; } .debug-success { color: #4CAF50; } .debug-warn { color: #FFC107; } .debug-error { color: #F44336; font-weight: bold; }
    `);

    // --- 2. GET ELEMENTS & DEFINE DATA ---
    const statusDiv = document.getElementById('batch-status');
    const runButton = document.getElementById('run-batch-button');
    const refreshButton = document.getElementById('refresh-data-button');
    const updateBaselineButton = document.getElementById('update-baseline-button');
    const openDebugButton = document.getElementById('jigs-open-debug-button');
    const baselineDisplay = document.getElementById('baseline-display');
    const jigsProgressContainer = document.getElementById('jigs-progress-container');
    const jigsProgressBar = document.getElementById('jigs-progress-bar');
    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 cheapestSpellBookPrice = Infinity;
    let debugLog = [];
    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 logDebug(message, level = 'normal') { debugLog.push({ message: `[${new Date().toLocaleTimeString()}] ${message}`, level }); }
    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 => { 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(); row.dataset.upgrade = result.upgrade; row.dataset.dpsChange = result.dps; row.dataset.percentChange = result.percent; row.dataset.costPerDps = isFinite(result.costPerDps) ? result.costPerDps : Infinity; row.innerHTML = `<td>${result.upgrade}</td><td>${result.dps > 0 ? '+' : ''}${result.dps.toFixed(2)}</td><td>${result.percent.toFixed(2)}%</td><td>${formatGold(result.costPerDps)}</td>`; }
    function formatGold(value) { if (value === 'N/A' || value === 'Free' || !isFinite(value)) return value; if (value < 1000) return Math.round(value).toLocaleString(); if (value < 1000000) return `${(value / 1000).toFixed(1)}k`; return `${(value / 1000000).toFixed(2)}M`; }
    function convertApiIdToItemName(apiId) { if (!apiId.includes('/')) return apiId; let name = apiId.split('/').pop(); return name.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()); }
    function calculateXpNeeded(startLvl, endLvl) { let totalXp = 0; for (let i = Number(startLvl); i < Number(endLvl); i++) { totalXp += Math.floor(100 * Math.pow(1.1, i - 1)); } return totalXp; }
    
    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 = convertApiIdToItemName(itemId); const itemEnhancements = rawMarketData[itemId]; for (const enhancementLevel in itemEnhancements) { const prices = itemEnhancements[enhancementLevel]; const fullName = enhancementLevel === "0" ? itemName : `${itemName} +${enhancementLevel}`; marketData[fullName] = { buyer: prices.b, seller: prices.a }; } } cheapestSpellBookPrice = Infinity; for (const itemName in marketData) { if (itemName.startsWith('Spell Book:')) { const price = marketData[itemName].seller; if (price > -1 && price < cheapestSpellBookPrice) { cheapestSpellBookPrice = price; } } } 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); }, 15000); const setupButton = document.getElementById('buttonSimulationSetup'); if (!setupButton) { cleanup(); resolve(NaN); return; } const startButton = document.getElementById('buttonStartSimulation'); if (!startButton) { cleanup(); resolve(NaN); return; } const resultsContainer = document.getElementById('simulationResultTotalDamageDone'); if (resultsContainer) resultsContainer.innerHTML = ''; setupButton.click(); setTimeout(() => { 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); }); }

    // --- 4. CORE LOGIC ---
    async function buildInputsUI() {
        statusDiv.textContent = 'Status: Reading data from page...';
        runButton.disabled = true; refreshButton.disabled = true; updateBaselineButton.disabled = true; openDebugButton.disabled = true;
        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 = [];
        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++;
            }
        }
        if (itemsFound > 0) { statusDiv.textContent = 'Status: Idle.'; runButton.disabled = false; updateBaselineButton.disabled = false; openDebugButton.disabled = false; }
        else { statusDiv.textContent = 'Status: No data found. Import & click Refresh.'; }
        refreshButton.disabled = false;
    }

    async function updateBaseline() {
        runButton.disabled = true; refreshButton.disabled = true; updateBaselineButton.disabled = true; openDebugButton.disabled = true;
        statusDiv.textContent = 'Status: Applying settings and updating baseline...';
        jigsProgressContainer.style.display = 'block';
        jigsProgressBar.style.width = '0%';
        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 newBaseline = await runSimulation(progress => { jigsProgressBar.style.width = `${progress}%`; });
        if (!isNaN(newBaseline)) { baselineDps = newBaseline; baselineDisplay.textContent = `Baseline DPS: ${baselineDps.toFixed(2)}`; simSettings.forEach(uiEl => { uiEl.dataset.originalValue = uiEl.value; }); statusDiv.textContent = 'Status: Baseline updated.'; }
        else { statusDiv.textContent = 'Error: Failed to update baseline. Try again.'; }
        jigsProgressContainer.style.display = 'none';
        runButton.disabled = false; refreshButton.disabled = false; updateBaselineButton.disabled = false; openDebugButton.disabled = false;
    }

    async function startBatch() {
        runButton.disabled = true; refreshButton.disabled = true; updateBaselineButton.disabled = true; openDebugButton.disabled = true;
        await fetchMarketData();
        if (!marketData) { runButton.disabled = false; refreshButton.disabled = false; updateBaselineButton.disabled = false; openDebugButton.disabled = false; return; }
        if (baselineDps === 0) { statusDiv.textContent = 'Error: Please set a baseline first.'; return; }
        
        jigsProgressContainer.style.display = 'block';
        jigsProgressBar.style.width = '0%';
        document.querySelector('#batch-results-table tbody').innerHTML = '';
        
        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]);
        const upgradeChanges = upgrades.filter(u => !specialIdMap[u.name]);
        if (upgradeChanges.length === 0 && settingChanges.length === 0) { statusDiv.textContent = 'Error: No changes.'; runButton.disabled = false; refreshButton.disabled = false; updateBaselineButton.disabled = false; openDebugButton.disabled = false; jigsProgressContainer.style.display = 'none'; return; }
        
        let simsCompleted = 0; let totalSims = upgradeChanges.length + (settingChanges.length > 0 ? 1 : 0);
        let currentBaselineDps = baselineDps;
        const allPageInputs = new Map();
        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 newBaseline = await runSimulation(progress => updateOverallProgress(simsCompleted, progress));
            if (isNaN(newBaseline)) { statusDiv.textContent = 'Error: Could not get baseline for new settings.'; jigsProgressContainer.style.display = 'none'; runButton.disabled = false; refreshButton.disabled = false; updateBaselineButton.disabled = false; openDebugButton.disabled = false; return; }
            currentBaselineDps = newBaseline; baselineDps = newBaseline; baselineDisplay.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.'; jigsProgressContainer.style.display = 'none'; runButton.disabled = false; refreshButton.disabled = false; updateBaselineButton.disabled = false; openDebugButton.disabled = false; return; }
        
        for (const upgrade of upgradeChanges) {
            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 newDps = await runSimulation(progress => updateOverallProgress(simsCompleted, progress));
            simsCompleted++;
            if (isNaN(newDps)) { console.error(`JIGS: Failed to get DPS for upgrade: ${upgrade.name}`); continue; }
            
            const dpsGain = newDps - currentBaselineDps;
            const percentChange = (dpsGain / currentBaselineDps) * 100;
            
            let cost = 0;
            if (skillKeywords.includes(upgrade.name) || houseKeywords.includes(upgrade.name)) { cost = 0; }
            else if (equipmentKeywords.includes(upgrade.name) || upgrade.isEnhancementOnly) {
                const originalItemUI = document.querySelector(`#batch-inputs-container [data-name="${upgrade.name}"]`);
                const oldItemName = upgrade.isEnhancementOnly ? originalItemUI.dataset.originalValue : upgrade.originalValue;
                const newItemName = upgrade.value || originalItemUI.value;
                const oldEnhInput = allPageInputs.get(`${upgrade.name} Enhancement`);
                const oldEnh = upgrade.enhancement ? upgrade.enhancement.originalValue : oldEnhInput?.originalValue || 0;
                const newEnh = upgrade.enhancement?.value || oldEnhInput?.originalValue || 0;
                const oldPrice = marketData[`${oldItemName} +${oldEnh}`]?.buyer || 0;
                const newPrice = marketData[`${newItemName} +${newEnh}`]?.seller || Infinity;
                cost = newPrice - (oldItemName === 'Empty' ? 0 : oldPrice);
            } else if (upgrade.name.startsWith('Ability') || upgrade.isLevelOnly) {
                const lvlInput = allPageInputs.get(`${upgrade.name.replace(' Level', '')} Level`);
                const startLvl = upgrade.level ? upgrade.level.originalValue : lvlInput?.originalValue;
                const endLvl = upgrade.level?.value || lvlInput?.originalValue;
                if(Number(endLvl) > Number(startLvl)) { const xpNeeded = calculateXpNeeded(startLvl, endLvl); cost = Math.ceil(xpNeeded / XP_PER_SPELLBOOK) * cheapestSpellBookPrice; }
            }
            const costPerPercent = (percentChange > 0 && cost !== 0) ? (cost / percentChange) * 0.01 : (cost === 0 && dpsGain > 0 ? "Free" : "N/A");
            
            addResultRow({ upgrade: upgradeLabelParts.join(' & '), dps: dpsGain, percent: percentChange, costPerDps: costPerPercent });
        }
        
        allPageInputs.forEach(input => { 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 })); });
        
        statusDiv.textContent = 'Status: Done!';
        jigsProgressContainer.style.display = 'none';
        runButton.disabled = false; refreshButton.disabled = false; updateBaselineButton.disabled = false; openDebugButton.disabled = false;
    }

    async function showDebugModal() {
        debugLog = [];
        logDebug("--- JIGS DEBUGGER ---");
        
        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 }; } });
        logDebug(`Detected ${Object.keys(allChanges).length} raw changes.`);
        
        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);
        }
        logDebug(`Bundled into ${upgrades.length} upgrade objects.`);

        const settingChanges = upgrades.filter(u => specialIdMap[u.name]);
        const upgradeChanges = upgrades.filter(u => !specialIdMap[u.name]);
        
        await fetchMarketData();
        
        let debugHtml = debugLog.map(log => `<span class="debug-log-entry debug-${log.level}">${log.message}</span>`).join('');
        debugHtml += `<span class="debug-log-entry debug-normal">\n== Detected Setting Changes: ${settingChanges.length} ==</span>`;
        debugHtml += `<span class="debug-log-entry debug-normal">${JSON.stringify(settingChanges, null, 2)}</span>`;
        debugHtml += `<span class="debug-log-entry debug-normal">\n== Detected Upgrade Changes (Bundled): ${upgradeChanges.length} ==</span>`;
        debugHtml += `<span class="debug-log-entry debug-normal">${JSON.stringify(upgradeChanges, null, 2)}</span>`;

        document.getElementById('jigs-debug-output').innerHTML = debugHtml;
        new bootstrap.Modal(document.getElementById('jigs-debug-modal')).show();
    }


    // --- 5. INITIALIZATION ---
    function initializeScript() {
        const batchContent = document.getElementById('batch-content');
        const batchToggle = document.getElementById('batch-toggle');
        batchToggle.addEventListener('click', () => batchContent.classList.toggle('hidden'));
        runButton.addEventListener('click', startBatch);
        refreshButton.addEventListener('click', buildInputsUI);
        updateBaselineButton.addEventListener('click', updateBaseline);
        openDebugButton.addEventListener('click', showDebugModal);
        document.querySelector('#batch-results-table thead').addEventListener('click', (event) => { const headerCell = event.target.closest('th'); if (!headerCell) return; const sortKey = headerCell.dataset.sortKey; if (!sortKey) 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'); } });
        
        const findButtonInterval = setInterval(() => {
            const importButton = Array.from(document.querySelectorAll('button')).find(btn => btn.textContent.includes('Import solo/group'));
            if (importButton) {
                clearInterval(findButtonInterval);
                statusDiv.textContent = 'Status: Ready to import.';
                refreshButton.disabled = false;
                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, ''));
                            baselineDisplay.textContent = `Baseline DPS: ${baselineDps.toFixed(2)}`;
                            setTimeout(buildInputsUI, 200);
                        }
                    });
                    initialSimObserver.observe(document.body, { childList: true, subtree: true });
                });
            }
        }, 500);
    }
    initializeScript();

})();