Jump to content

MediaWiki:Common.js

From FuturePath

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/* Any JavaScript here will be loaded for all users on every page load. */
$('#mw-mywiki-test').html('<html><script src="http://ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.min.js" type="text/javascript"></script><script src="http://dev.d20futurepath.com/roller.js" type="text/javascript"></script><body><p>Please input the dice IE: d20+d4:</p><input id="dice"><button type="button" onclick="myFunction()">Submit</button><p id="diceRoll"></p></body></html>');
/**
 * dice_calc_wiki_ui.js
 *
 * MediaWiki Interactive DC Probability Calculator Widget
 *
 * Embed this file into MediaWiki:Common.js or a MediaWiki Gadget.
 * To display the calculator on any wiki page, add:
 *   <div id="dc-calc-widget"></div>
 */

(function () {
    // =========================================================================
    // 1. Core Calculation Engine & Pre-Populated Data
    // =========================================================================
    const EXPLODABLE_DICE = new Set([2, 4, 6, 8, 10, 12]);
    const DEFAULT_DCS = [5, 10, 15, 20, 25, 30, 35, 40, 45];

    const Classicd20 = [
        'd20+0', 'd20+1', 'd20+2', 'd20+3', 'd20+4', 'd20+5',
        'd20+6', 'd20+7', 'd20+8', 'd20+9', 'd20+10', 'd20+11',
        'd20+12', 'd20+13', 'd20+14', 'd20+15', 'd20+16', 'd20+17',
        'd20+18', 'd20+19', 'd20+20'
    ];

    const SkillRanks = [
        'd20+0', 'd20+d2', 'd20+2d2', 'd20+2d2+1', 'd20+d4+d2+1', 'd20+2d4+1', 'd20+2d4+2',
        'd20+d6+d4+2', 'd20+2d6+2', 'd20+2d6+3', 'd20+d8+d6+3', 'd20+2d8+3', 'd20+2d8+4',
        'd20+d10+d8+4', 'd20+2d10+4', 'd20+2d10+5', 'd20+d12+d10+5', 'd20+2d12+5', 'd20+2d12+6',
        'd20+2d6+d12+6', 'd20+2d6+2d6+6'
    ];

    const AdvantageDieRanks = [
        'd20+0', 'd20+d2+1', 'd20+2d2+1', 'd20+2d2+2', 'd20+d4+d2+2', 'd20+2d4+2', 'd20+2d4+3',
        'd20+d6+d4+3', 'd20+2d6+3', 'd20+2d6+4', 'd20+d8+d6+4', 'd20+2d8+4', 'd20+2d8+5',
        'd20+d10+d8+5', 'd20+2d10+5', 'd20+2d10+6', 'd20+d12+d10+6', 'd20+2d12+6', 'd20+2d12+7',
        'd20+2d6+d12+7', 'd20+2d6+2d6+7'
    ];

    const PathAdvantageDieProgression = [
        'd20+0',
        'd20+d2+1',
        'd20+2d2+1',
        'd20+2d2+2',
        'd20+d4+d2+2',
        'd20+d4+d2+2',
        'd20+2d4+2',
        'd20+2d4+3',
        'd20+d6+d4+3',
        'd20+2d6+3',
        'd20+2d6+4',
        'd20+2d6+4',
        'd20+d8+d6+4',
        'd20+2d8+4',
        'd20+2d8+5',
        'd20+d10+d8+5',
        'd20+2d10+5',
        'd20+2d10+5',
        'd20+2d10+6',
        'd20+d12+d10+6',
        'd20+2d12+6'
    ];

    const PRESETS = {
        'Classicd20': Classicd20,
        'SkillRanks': SkillRanks,
        'AdvantageDieRanks': AdvantageDieRanks,
        'PathAdvantageDieProgression': PathAdvantageDieProgression
    };

    function parseRollGroup(exprStr, defaultMod = 0, explosions = 0, luck = false, unlucky = false) {
        const token = exprStr.trim();
        if (!token) throw new Error("Empty roll expression.");

        const regex = /([+-]?)\s*(?:(\d*)d(\d+)|(\d+))/gi;
        const matches = [];
        let m;
        while ((m = regex.exec(token)) !== null) matches.push(m);
        if (matches.length === 0) throw new Error("Invalid dice expression.");

        const diceSides = [];
        const diceTokens = [];
        let hasInlineMod = false;
        let inlineMod = 0;

        for (const match of matches) {
            const sign = match[1];
            const countStr = match[2];
            const sidesStr = match[3];
            const numStr = match[4];
            const isNegative = (sign === '-');

            if (sidesStr !== undefined && sidesStr !== null) {
                const count = countStr ? parseInt(countStr, 10) : 1;
                const sides = parseInt(sidesStr, 10);
                for (let i = 0; i < count; i++) diceSides.push(sides);
                let dieName = count > 1 ? `${count}d${sides}` : `d${sides}`;
                if (sides === 20 && luck) dieName += "^";
                else if (sides === 20 && unlucky) dieName += "v";
                else if (explosions > 0 && EXPLODABLE_DICE.has(sides)) dieName += "*";
                diceTokens.push(dieName);
            } else if (numStr !== undefined && numStr !== null) {
                hasInlineMod = true;
                const val = parseInt(numStr, 10);
                inlineMod += isNegative ? -val : val;
            }
        }
        if (diceSides.length === 0 && !hasInlineMod) throw new Error("No dice found.");

        const finalMod = inlineMod + defaultMod;
        const dicePart = diceTokens.join("+");
        let canonicalExpr = dicePart;
        if (dicePart.length === 0) {
            canonicalExpr = finalMod >= 0 ? `+${finalMod}` : `${finalMod}`;
        } else if (finalMod > 0) {
            canonicalExpr += `+${finalMod}`;
        } else if (finalMod < 0) {
            canonicalExpr += `${finalMod}`;
        }

        return { diceSides, diceExpr: canonicalExpr, mod: finalMod };
    }

    function computeDieTransitions(sides, e = 0) {
        if (e <= 0 || !EXPLODABLE_DICE.has(sides)) {
            const trans = [];
            const prob = 1.0 / sides;
            for (let face = 1; face <= sides; face++) {
                trans.push({ val: face, remE: 0, p: prob });
            }
            return trans;
        }

        const trans = [];
        const probPerFace = 1.0 / sides;
        for (let face = 1; face < sides; face++) {
            trans.push({ val: face, remE: e, p: probPerFace });
        }
        const subTrans = computeDieTransitions(sides, e - 1);
        for (let i = 0; i < subTrans.length; i++) {
            const sub = subTrans[i];
            trans.push({
                val: sides + sub.val,
                remE: sub.remE,
                p: probPerFace * sub.p
            });
        }
        return trans;
    }

    function computeSingleDiePmf(sides, explosions = 0, luck = false, unlucky = false) {
        return computePmf([sides], explosions, luck, unlucky);
    }

    function computePmf(dice, explosions = 0, luck = false, unlucky = false) {
        const explodable = [];
        const nonExplodable = [];
        for (const sides of dice) {
            if (EXPLODABLE_DICE.has(sides)) {
                explodable.push(sides);
            } else {
                nonExplodable.push(sides);
            }
        }

        // Sort explodable dice descending by sides (highest die gets explosion priority)
        explodable.sort((a, b) => b - a);

        // State: "currentSum,remainingExplosions" -> probability
        let state = new Map();
        state.set(`0,${explosions}`, 1.0);

        for (const sides of explodable) {
            const nextState = new Map();
            const transitionsCache = new Map();

            for (const [stateKey, p1] of state.entries()) {
                const commaIdx = stateKey.indexOf(',');
                const curSum = parseInt(stateKey.slice(0, commaIdx), 10);
                const remE = parseInt(stateKey.slice(commaIdx + 1), 10);

                let trans = transitionsCache.get(remE);
                if (!trans) {
                    trans = computeDieTransitions(sides, remE);
                    transitionsCache.set(remE, trans);
                }

                for (let i = 0; i < trans.length; i++) {
                    const tr = trans[i];
                    const nextSum = curSum + tr.val;
                    const nextKey = `${nextSum},${tr.remE}`;
                    nextState.set(nextKey, (nextState.get(nextKey) || 0.0) + p1 * tr.p);
                }
            }
            state = nextState;
        }

        // Marginalize out remaining explosions
        let pmf = {};
        for (const [stateKey, p] of state.entries()) {
            const commaIdx = stateKey.indexOf(',');
            const curSum = parseInt(stateKey.slice(0, commaIdx), 10);
            pmf[curSum] = (pmf[curSum] || 0.0) + p;
        }

        // Convolve with non-explodable dice
        for (const sides of nonExplodable) {
            let diePmf = {};
            if (sides === 20 && luck) {
                for (let face = 1; face <= 20; face++) {
                    diePmf[face] = (2 * face - 1) / 400.0;
                }
            } else if (sides === 20 && unlucky) {
                for (let face = 1; face <= 20; face++) {
                    diePmf[face] = (41 - 2 * face) / 400.0;
                }
            } else {
                const prob = 1.0 / sides;
                for (let face = 1; face <= sides; face++) {
                    diePmf[face] = prob;
                }
            }

            const newPmf = {};
            for (const [curSumStr, p1] of Object.entries(pmf)) {
                const curSum = parseInt(curSumStr, 10);
                for (const [dieValStr, p2] of Object.entries(diePmf)) {
                    const dieVal = parseInt(dieValStr, 10);
                    const sum = curSum + dieVal;
                    newPmf[sum] = (newPmf[sum] || 0.0) + p1 * p2;
                }
            }
            pmf = newPmf;
        }

        return pmf;
    }

    function calculateProbabilities(diceStr, defaultMod, customDcStr, explosions, luck = false, unlucky = false) {
        const customDcs = customDcStr.split(',')
            .map(s => s.trim())
            .filter(s => s !== '' && !isNaN(parseInt(s, 10)))
            .map(s => parseInt(s, 10));

        const dcs = Array.from(new Set([...DEFAULT_DCS, ...customDcs])).sort((a, b) => a - b);
        const rollTokens = diceStr.split(/\s+/).filter(Boolean);
        const rows = [];

        for (const token of rollTokens) {
            const group = parseRollGroup(token, defaultMod, explosions, luck, unlucky);
            const pmf = computePmf(group.diceSides, explosions, luck, unlucky);
            const probs = {};
            for (const dc of dcs) {
                let pSuccess = 0.0;
                for (const [sumVal, p] of Object.entries(pmf)) {
                    if (parseInt(sumVal, 10) + group.mod >= dc) pSuccess += p;
                }
                probs[dc] = pSuccess;
            }
            rows.push({ expr: group.diceExpr, probs });
        }
        return { dcs, rows };
    }

    // =========================================================================
    // 2. UI Builder & Event Handling (Theme-Aware)
    // =========================================================================
    const CSS_STYLES = `
        .dc-calc-container {
            --dc-card-bg: #f8f9fa;
            --dc-card-border: #c8ccd1;
            --dc-text: #202122;
            --dc-subtext: #54595d;
            --dc-input-bg: #ffffff;
            --dc-input-border: #a2a9b1;
            --dc-input-text: #202122;
            --dc-table-hdr-bg: #eaecf0;
            --dc-table-hdr-text: #202122;
            --dc-table-border: #a2a9b1;
            --dc-table-bg: #ffffff;
            --dc-btn-calc: #3366cc;
            --dc-btn-calc-hover: #2a4b8d;
            --dc-btn-copy: #2ea44f;
            --dc-btn-copy-hover: #22863a;
            --dc-link: #3366cc;
            --dc-error: #d33;
            --dc-success: #2ea44f;
            --dc-textarea-bg: #ffffff;
            background: var(--dc-card-bg);
            border: 1px solid var(--dc-card-border);
            color: var(--dc-text);
            border-radius: 4px;
            padding: 15px;
            margin: 15px 0;
            max-width: 1100px;
            width: 100%;
            box-sizing: border-box;
            font-family: sans-serif;
            transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease;
        }

        /* MediaWiki Vector 2022 Night mode & Dark Mode Gadget/Skin Classes */
        html.skin-theme-clientpref-night .dc-calc-container,
        html.dark-mode .dc-calc-container,
        body.skin-night .dc-calc-container,
        body.theme-dark .dc-calc-container,
        html.client-dark-mode .dc-calc-container {
            --dc-card-bg: #202122;
            --dc-card-border: #54595d;
            --dc-text: #f8f9fa;
            --dc-subtext: #a2a9b1;
            --dc-input-bg: #27292d;
            --dc-input-border: #72777d;
            --dc-input-text: #f8f9fa;
            --dc-table-hdr-bg: #2e3034;
            --dc-table-hdr-text: #ffffff;
            --dc-table-border: #54595d;
            --dc-table-bg: #202122;
            --dc-btn-calc: #447ff5;
            --dc-btn-calc-hover: #3366cc;
            --dc-btn-copy: #34b359;
            --dc-btn-copy-hover: #2ea44f;
            --dc-link: #6699ff;
            --dc-error: #ff6b6b;
            --dc-success: #34b359;
            --dc-textarea-bg: #27292d;
        }

        /* OS automatic Dark mode (when MediaWiki client preference is 'os' or unset) */
        @media (prefers-color-scheme: dark) {
            html.skin-theme-clientpref-os .dc-calc-container,
            html:not(.skin-theme-clientpref-day):not(.skin-theme-clientpref-night) .dc-calc-container {
                --dc-card-bg: #202122;
                --dc-card-border: #54595d;
                --dc-text: #f8f9fa;
                --dc-subtext: #a2a9b1;
                --dc-input-bg: #27292d;
                --dc-input-border: #72777d;
                --dc-input-text: #f8f9fa;
                --dc-table-hdr-bg: #2e3034;
                --dc-table-hdr-text: #ffffff;
                --dc-table-border: #54595d;
                --dc-table-bg: #202122;
                --dc-btn-calc: #447ff5;
                --dc-btn-calc-hover: #3366cc;
                --dc-btn-copy: #34b359;
                --dc-btn-copy-hover: #2ea44f;
                --dc-link: #6699ff;
                --dc-error: #ff6b6b;
                --dc-success: #34b359;
                --dc-textarea-bg: #27292d;
            }
        }

        .dc-calc-input {
            background: var(--dc-input-bg) !important;
            border: 1px solid var(--dc-input-border) !important;
            color: var(--dc-input-text) !important;
            border-radius: 3px;
            padding: 6px;
            font-size: 0.9em;
        }
        .dc-calc-input:disabled {
            opacity: 0.55;
            cursor: not-allowed;
        }
        .dc-calc-table {
            width: 100%;
            border-collapse: collapse;
            text-align: center;
            font-size: 0.9em;
            margin-top: 10px;
        }
        .dc-calc-table th {
            background: var(--dc-table-hdr-bg);
            color: var(--dc-table-hdr-text);
            border: 1px solid var(--dc-table-border);
            padding: 6px 8px;
        }
        .dc-calc-table td {
            background: var(--dc-table-bg);
            color: var(--dc-text);
            border: 1px solid var(--dc-table-border);
            padding: 6px 8px;
        }
        .dc-calc-btn-calc {
            background: var(--dc-btn-calc);
            color: #ffffff;
            border: none;
            padding: 8px 16px;
            border-radius: 4px;
            cursor: pointer;
            font-weight: bold;
        }
        .dc-calc-btn-calc:hover {
            background: var(--dc-btn-calc-hover);
        }
        .dc-calc-btn-copy {
            background: var(--dc-btn-copy);
            color: #ffffff;
            border: none;
            padding: 8px 16px;
            border-radius: 4px;
            cursor: pointer;
            font-weight: bold;
            margin-left: 8px;
        }
        .dc-calc-btn-copy:hover {
            background: var(--dc-btn-copy-hover);
        }
    `;

    function injectStyles() {
        if (!document.getElementById('dc-calc-injected-styles')) {
            const style = document.createElement('style');
            style.id = 'dc-calc-injected-styles';
            style.textContent = CSS_STYLES;
            document.head.appendChild(style);
        }
    }

    function initDcCalcWidget() {
        const container = document.getElementById('dc-calc-widget');
        if (!container || container.dataset.loaded) return;
        container.dataset.loaded = "true";

        injectStyles();

        container.innerHTML = `
            <div class="dc-calc-container">
                <div style="display: flex; flex-wrap: wrap; gap: 12px; align-items: flex-end; margin-bottom: 15px;">
                    <div>
                        <label style="display: block; font-weight: bold; margin-bottom: 4px;">Preset:</label>
                        <select id="dc-select-preset" class="dc-calc-input" style="width: 195px;">
                            <option value="custom">Custom Input</option>
                            <option value="Classicd20">Classic d20 (0-20)</option>
                            <option value="SkillRanks">Skill Ranks (0-20)</option>
                            <option value="AdvantageDieRanks">Advantage Die (0-20)</option>
                            <option value="PathAdvantageDieProgression">Path Advantage Die (0-20)</option>
                        </select>
                    </div>
                    <div>
                        <label style="display: block; font-weight: bold; margin-bottom: 4px;">Dice Expression(s):</label>
                        <input id="dc-input-dice" class="dc-calc-input" type="text" value="d20+5 d20+2d6+1" style="width: 200px;" placeholder="e.g. d20+5 d20+2d6">
                    </div>
                    <div>
                        <label style="display: block; font-weight: bold; margin-bottom: 4px;">Modifier:</label>
                        <input id="dc-input-mod" class="dc-calc-input" type="number" value="0" style="width: 70px;">
                    </div>
                    <div>
                        <label style="display: block; font-weight: bold; margin-bottom: 4px;">Exploding Dice:</label>
                        <select id="dc-select-exploding" class="dc-calc-input">
                            <option value="0">Off (0)</option>
                            <option value="1">1 Explosion</option>
                            <option value="2">2 Explosions</option>
                            <option value="3">3 Explosions</option>
                            <option value="4">4 Explosions</option>
                            <option value="5">5 Explosions</option>
                            <option value="6">6 Explosions</option>
                            <option value="100">Infinity (∞)</option>
                        </select>
                    </div>
                    <div>
                        <label style="display: block; font-weight: bold; margin-bottom: 4px;">Custom DC(s):</label>
                        <input id="dc-input-custom-dc" class="dc-calc-input" type="text" value="" style="width: 110px;" placeholder="e.g. 17, 22">
                    </div>
                    <div style="display: flex; align-items: center; padding-bottom: 6px;">
                        <label style="font-weight: bold; cursor: pointer; color: var(--dc-text); display: flex; align-items: center; gap: 4px;">
                            <input id="dc-check-row-count" type="checkbox" checked> Row #
                        </label>
                    </div>
                    <div style="display: flex; align-items: center; gap: 6px; padding-bottom: 6px;">
                        <label style="font-weight: bold; cursor: pointer; color: var(--dc-text); display: flex; align-items: center; margin: 0;">
                            <input id="dc-check-luck" type="checkbox" style="cursor: pointer; margin: 0;">
                        </label>
                        <select id="dc-select-luck-mode" class="dc-calc-input" style="height: 28px; padding: 2px 8px; font-size: 0.85em; font-weight: bold; cursor: pointer;" disabled>
                            <option value="luck">Luck</option>
                            <option value="unlucky">Unlucky</option>
                        </select>
                    </div>
                    <div style="display: flex; gap: 8px; align-items: center;">
                        <button id="dc-btn-calc" class="dc-calc-btn-calc">Calculate</button>
                        <button id="dc-btn-copy" class="dc-calc-btn-copy" style="margin-left: 0;">Copy WikiText</button>
                        <span id="dc-copy-status" style="color: var(--dc-success); font-weight: bold; display: none;">Copied!</span>
                    </div>
                </div>

                <div id="dc-preview-container" style="overflow-x: auto; margin-top: 10px;"></div>

                <details style="margin-top: 12px;">
                    <summary style="cursor: pointer; color: var(--dc-link); font-size: 0.9em; font-weight: 500;">View Generated WikiText</summary>
                    <textarea id="dc-wikitext-output" class="dc-calc-input" readonly style="width: 100%; height: 90px; margin-top: 5px; font-family: monospace; font-size: 0.85em;"></textarea>
                </details>
            </div>
        `;

        function render() {
            try {
                const diceStr = document.getElementById('dc-input-dice').value;
                const defaultMod = parseInt(document.getElementById('dc-input-mod').value, 10) || 0;
                const exploding = parseInt(document.getElementById('dc-select-exploding').value, 10) || 0;
                const customDcStr = document.getElementById('dc-input-custom-dc').value;
                const luckChecked = document.getElementById('dc-check-luck').checked;
                const luckMode = document.getElementById('dc-select-luck-mode').value;
                const luck = luckChecked && luckMode === 'luck';
                const unlucky = luckChecked && luckMode === 'unlucky';
                const showRowCount = document.getElementById('dc-check-row-count').checked;

                const { dcs, rows } = calculateProbabilities(diceStr, defaultMod, customDcStr, exploding, luck, unlucky);

                // Build WikiText Output
                const wikiHeaders = [];
                if (showRowCount) wikiHeaders.push("#");
                wikiHeaders.push("Dice");
                wikiHeaders.push(...dcs.map(dc => `DC ${dc}`));

                const wikiHeader = `{| class="wikitable"\n! ` + wikiHeaders.join(' || ');
                const wikiRows = rows.map((r, i) => {
                    const rowCells = [];
                    if (showRowCount) rowCells.push(String(i));
                    rowCells.push(r.expr);
                    rowCells.push(...dcs.map(dc => `${(r.probs[dc] * 100).toFixed(2)}%`));
                    return `|-\n| ` + rowCells.join(' || ');
                }).join('\n');
                const fullWikiText = `${wikiHeader}\n${wikiRows}\n|}`;
                document.getElementById('dc-wikitext-output').value = fullWikiText;

                // Build HTML Live Preview Table
                let html = '<table class="dc-calc-table">';
                html += '<tr>' + (showRowCount ? '<th>#</th>' : '') + '<th>Dice</th>' + dcs.map(dc => `<th>DC ${dc}</th>`).join('') + '</tr>';
                for (let i = 0; i < rows.length; i++) {
                    const r = rows[i];
                    html += '<tr>' + (showRowCount ? `<td style="color: var(--dc-subtext); font-size: 0.85em;">${i}</td>` : '') + `<td style="font-weight: bold;">${r.expr}</td>` + dcs.map(dc => `<td>${(r.probs[dc] * 100).toFixed(2)}%</td>`).join('') + '</tr>';
                }
                html += '</table>';
                document.getElementById('dc-preview-container').innerHTML = html;
            } catch (err) {
                document.getElementById('dc-preview-container').innerHTML = `<div style="color: var(--dc-error); font-weight: bold;">Error: ${err.message}</div>`;
            }
        }

        document.getElementById('dc-select-preset').addEventListener('change', function () {
            const val = this.value;
            if (val !== 'custom' && PRESETS[val]) {
                document.getElementById('dc-input-dice').value = PRESETS[val].join(' ');
                render();
            }
        });

        document.getElementById('dc-input-dice').addEventListener('input', function () {
            const currentVal = this.value.trim();
            const matchedKey = Object.keys(PRESETS).find(k => PRESETS[k].join(' ') === currentVal);
            document.getElementById('dc-select-preset').value = matchedKey || 'custom';
        });

        document.getElementById('dc-check-row-count').addEventListener('change', render);
        document.getElementById('dc-check-luck').addEventListener('change', function () {
            const selectMode = document.getElementById('dc-select-luck-mode');
            selectMode.disabled = !this.checked;
            if (this.checked && !selectMode.value) {
                selectMode.value = 'luck';
            }
            render();
        });
        document.getElementById('dc-select-luck-mode').addEventListener('change', render);
        document.getElementById('dc-select-exploding').addEventListener('change', render);
        document.getElementById('dc-input-mod').addEventListener('input', render);
        document.getElementById('dc-input-custom-dc').addEventListener('input', render);

        document.getElementById('dc-btn-calc').addEventListener('click', render);
        document.getElementById('dc-btn-copy').addEventListener('click', function () {
            const textarea = document.getElementById('dc-wikitext-output');
            textarea.select();
            navigator.clipboard.writeText(textarea.value);
            const status = document.getElementById('dc-copy-status');
            status.style.display = 'inline';
            setTimeout(() => { status.style.display = 'none'; }, 2000);
        });

        // Trigger initial calculation
        render();
    }

    // Initialize when DOM / MediaWiki is ready
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initDcCalcWidget);
    } else {
        initDcCalcWidget();
    }
})();
/**
 * ability_score_calc_wiki_ui.js
 *
 * MediaWiki Interactive Ability Theme & Score Calculator Widget
 *
 * Supports 3 distribution methods:
 * 1. Standard Spread (Default 15, 14, 13, 12, 10, 8 with Low/High fantasy options)
 * 2. Dice Roller (Low / Standard / High / Epic Fantasy via FuturePathAPI /tasks/roll/character/<level>)
 * 3. Point Buy (Low / Standard / High / Epic fantasy budgets, scores 7-18 with official costs)
 *
 * Embed this script into MediaWiki:Common.js or a MediaWiki Gadget.
 * To display the widget on any wiki page, add:
 *   <div id="ability-score-calc-widget"></div>
 */

(function () {
    'use strict';

    // =========================================================================
    // 1. Constants & Core Data
    // =========================================================================
    const CORE_ATTRIBUTES = [
        { key: 'STR', name: 'Strength', desc: 'Physical power and melee prowess' },
        { key: 'DEX', name: 'Dexterity', desc: 'Agility, reflexes, and ranged accuracy' },
        { key: 'CON', name: 'Constitution', desc: 'Health, stamina, and hit point bonus' },
        { key: 'INT', name: 'Intelligence', desc: 'Knowledge, reasoning, and skill points' },
        { key: 'WIS', name: 'Wisdom', desc: 'Perception, insight, and willpower' },
        { key: 'CHA', name: 'Charisma', desc: 'Leadership, persuasiveness, and force of personality' }
    ];

    const SPREADS = {
        'standard': {
            name: 'Standard Fantasy (15, 14, 13, 12, 10, 8)',
            scores: [15, 14, 13, 12, 10, 8]
        },
        'low': {
            name: 'Low Fantasy (14, 12, 10, 10, 8, 8)',
            scores: [14, 12, 10, 10, 8, 8]
        },
        'high': {
            name: 'High Fantasy (19, 18, 16, 13, 12, 10)',
            scores: [19, 18, 16, 13, 12, 10]
        }
    };

    const POINT_BUY_BUDGETS = {
        'low': { name: 'Low Fantasy', points: 10 },
        'standard': { name: 'Standard Fantasy', points: 15 },
        'high': { name: 'High Fantasy', points: 20 },
        'epic': { name: 'Epic Fantasy', points: 25 }
    };

    const POINT_BUY_COSTS = {
        7: -4,
        8: -2,
        9: -1,
        10: 0,
        11: 1,
        12: 2,
        13: 3,
        14: 5,
        15: 7,
        16: 10,
        17: 13,
        18: 17
    };

    const ROLLER_CONFIGS = {
        'low': {
            name: 'Low Fantasy',
            apiLevel: 'low',
            desc: '3d6 (reroll total <= 7), 6 rolls',
            diceCount: 3,
            sides: 6,
            dropLowest: 0,
            rerollTotal: 7,
            totalRolls: 6,
            dropLowestTotal: 0
        },
        'standard': {
            name: 'Standard Fantasy',
            apiLevel: 'normal',
            desc: '4d6 drop lowest 1 (reroll total <= 8), 6 rolls',
            diceCount: 4,
            sides: 6,
            dropLowest: 1,
            rerollTotal: 8,
            totalRolls: 6,
            dropLowestTotal: 0
        },
        'high': {
            name: 'High Fantasy',
            apiLevel: 'high',
            desc: '4d6 drop lowest 1 (reroll total <= 9), 7 rolls drop lowest',
            diceCount: 4,
            sides: 6,
            dropLowest: 1,
            rerollTotal: 9,
            totalRolls: 7,
            dropLowestTotal: 1
        },
        'epic': {
            name: 'Epic Fantasy',
            apiLevel: 'epic',
            desc: '5d6 drop lowest 2 (reroll total <= 10), 7 rolls drop lowest',
            diceCount: 5,
            sides: 6,
            dropLowest: 2,
            rerollTotal: 10,
            totalRolls: 7,
            dropLowestTotal: 1
        }
    };

    // =========================================================================
    // 2. Calculation Helpers
    // =========================================================================
    function calculateModifier(score) {
        if (score === null || score === undefined || isNaN(score)) return 0;
        return Math.floor((score - 10) / 2);
    }

    function formatModifier(mod) {
        if (mod > 0) return `+${mod}`;
        return `${mod}`;
    }

    // Client-side dice rolling fallback replicating FuturePathAPI/Rolling.py
    function rollSingleDie(sides) {
        return Math.floor(Math.random() * sides) + 1;
    }

    function rollScore(diceCount, sides, dropLowest, rerollTotal) {
        for (let attempt = 0; attempt < 100; attempt++) {
            const dice = [];
            for (let i = 0; i < diceCount; i++) {
                dice.push(rollSingleDie(sides));
            }
            const sorted = [...dice].sort((a, b) => a - b);
            const kept = sorted.slice(dropLowest);
            const total = kept.reduce((a, b) => a + b, 0);
            if (total > rerollTotal) {
                return { total, dice, kept };
            }
        }
        return { total: rerollTotal + 1, dice: [], kept: [] };
    }

    function rollCharacterLocally(config) {
        const rolls = [];
        for (let i = 0; i < config.totalRolls; i++) {
            rolls.push(rollScore(config.diceCount, config.sides, config.dropLowest, config.rerollTotal));
        }
        if (config.dropLowestTotal > 0) {
            let minIndex = 0;
            for (let i = 1; i < rolls.length; i++) {
                if (rolls[i].total < rolls[minIndex].total) {
                    minIndex = i;
                }
            }
            rolls.splice(minIndex, 1);
        }
        return rolls.map(r => r.total);
    }

    // =========================================================================
    // 3. Theme-Aware CSS Styles
    // =========================================================================
    const CSS_STYLES = `
        .asc-container {
            --asc-card-bg: #f8f9fa;
            --asc-card-border: #c8ccd1;
            --asc-text: #202122;
            --asc-subtext: #54595d;
            --asc-input-bg: #ffffff;
            --asc-input-border: #a2a9b1;
            --asc-input-text: #202122;
            --asc-table-hdr-bg: #eaecf0;
            --asc-table-hdr-text: #202122;
            --asc-table-border: #a2a9b1;
            --asc-table-bg: #ffffff;
            --asc-btn-primary: #3366cc;
            --asc-btn-primary-hover: #2a4b8d;
            --asc-btn-copy: #2ea44f;
            --asc-btn-copy-hover: #22863a;
            --asc-btn-secondary: #eaecf0;
            --asc-btn-secondary-hover: #dadde3;
            --asc-btn-secondary-text: #202122;
            --asc-tab-active-border: #3366cc;
            --asc-tab-active-bg: #ffffff;
            --asc-badge-bg: #eaecf0;
            --asc-badge-text: #202122;
            --asc-badge-active-bg: #3366cc;
            --asc-badge-active-text: #ffffff;
            --asc-link: #3366cc;
            --asc-error: #d33;
            --asc-success: #2ea44f;
            --asc-warning: #b35900;
            --asc-highlight-bg: #eaf3ff;
            background: var(--asc-card-bg);
            border: 1px solid var(--asc-card-border);
            color: var(--asc-text);
            border-radius: 6px;
            padding: 16px;
            margin: 15px 0;
            max-width: 1100px;
            width: 100%;
            box-sizing: border-box;
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
            transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease;
        }

        /* MediaWiki Vector 2022 Night mode & Dark Mode Gadget/Skin Classes */
        html.skin-theme-clientpref-night .asc-container,
        html.dark-mode .asc-container,
        body.skin-night .asc-container,
        body.theme-dark .asc-container,
        html.client-dark-mode .asc-container {
            --asc-card-bg: #202122;
            --asc-card-border: #54595d;
            --asc-text: #f8f9fa;
            --asc-subtext: #a2a9b1;
            --asc-input-bg: #27292d;
            --asc-input-border: #72777d;
            --asc-input-text: #f8f9fa;
            --asc-table-hdr-bg: #2e3034;
            --asc-table-hdr-text: #ffffff;
            --asc-table-border: #54595d;
            --asc-table-bg: #202122;
            --asc-btn-primary: #447ff5;
            --asc-btn-primary-hover: #3366cc;
            --asc-btn-copy: #34b359;
            --asc-btn-copy-hover: #2ea44f;
            --asc-btn-secondary: #2e3034;
            --asc-btn-secondary-hover: #3a3d42;
            --asc-btn-secondary-text: #f8f9fa;
            --asc-tab-active-border: #447ff5;
            --asc-tab-active-bg: #27292d;
            --asc-badge-bg: #2e3034;
            --asc-badge-text: #f8f9fa;
            --asc-badge-active-bg: #447ff5;
            --asc-badge-active-text: #ffffff;
            --asc-link: #6699ff;
            --asc-error: #ff6b6b;
            --asc-success: #34b359;
            --asc-warning: #ffaa33;
            --asc-highlight-bg: #1c2b42;
        }

        /* OS automatic Dark mode */
        @media (prefers-color-scheme: dark) {
            html.skin-theme-clientpref-os .asc-container,
            html:not(.skin-theme-clientpref-day):not(.skin-theme-clientpref-night) .asc-container {
                --asc-card-bg: #202122;
                --asc-card-border: #54595d;
                --asc-text: #f8f9fa;
                --asc-subtext: #a2a9b1;
                --asc-input-bg: #27292d;
                --asc-input-border: #72777d;
                --asc-input-text: #f8f9fa;
                --asc-table-hdr-bg: #2e3034;
                --asc-table-hdr-text: #ffffff;
                --asc-table-border: #54595d;
                --asc-table-bg: #202122;
                --asc-btn-primary: #447ff5;
                --asc-btn-primary-hover: #3366cc;
                --asc-btn-copy: #34b359;
                --asc-btn-copy-hover: #2ea44f;
                --asc-btn-secondary: #2e3034;
                --asc-btn-secondary-hover: #3a3d42;
                --asc-btn-secondary-text: #f8f9fa;
                --asc-tab-active-border: #447ff5;
                --asc-tab-active-bg: #27292d;
                --asc-badge-bg: #2e3034;
                --asc-badge-text: #f8f9fa;
                --asc-badge-active-bg: #447ff5;
                --asc-badge-active-text: #ffffff;
                --asc-link: #6699ff;
                --asc-error: #ff6b6b;
                --asc-success: #34b359;
                --asc-warning: #ffaa33;
                --asc-highlight-bg: #1c2b42;
            }
        }

        .asc-tabs {
            display: flex;
            border-bottom: 2px solid var(--asc-card-border);
            gap: 6px;
            margin-bottom: 16px;
            overflow-x: auto;
        }
        .asc-tab-btn {
            background: transparent;
            border: none;
            border-bottom: 3px solid transparent;
            color: var(--asc-subtext);
            padding: 10px 18px;
            font-size: 0.95em;
            font-weight: 600;
            cursor: pointer;
            transition: all 0.2s ease;
            white-space: nowrap;
            border-top-left-radius: 4px;
            border-top-right-radius: 4px;
        }
        .asc-tab-btn:hover {
            color: var(--asc-text);
            background: var(--asc-btn-secondary);
        }
        .asc-tab-btn.active {
            color: var(--asc-btn-primary);
            border-bottom: 3px solid var(--asc-tab-active-border);
            background: var(--asc-tab-active-bg);
        }

        .asc-tab-content {
            display: none;
        }
        .asc-tab-content.active {
            display: block;
        }

        .asc-control-row {
            display: flex;
            flex-wrap: wrap;
            gap: 12px;
            align-items: center;
            margin-bottom: 15px;
        }

        .asc-input {
            background: var(--asc-input-bg) !important;
            border: 1px solid var(--asc-input-border) !important;
            color: var(--asc-input-text) !important;
            border-radius: 4px;
            padding: 6px 10px;
            font-size: 0.9em;
            box-sizing: border-box;
        }

        .asc-btn {
            border: none;
            padding: 7px 14px;
            border-radius: 4px;
            cursor: pointer;
            font-weight: 600;
            font-size: 0.9em;
            transition: background 0.15s ease;
            display: inline-flex;
            align-items: center;
            gap: 6px;
        }
        .asc-btn-primary {
            background: var(--asc-btn-primary);
            color: #ffffff !important;
        }
        .asc-btn-primary:hover {
            background: var(--asc-btn-primary-hover);
        }
        .asc-btn-copy {
            background: var(--asc-btn-copy);
            color: #ffffff !important;
        }
        .asc-btn-copy:hover {
            background: var(--asc-btn-copy-hover);
        }
        .asc-btn-secondary {
            background: var(--asc-btn-secondary);
            color: var(--asc-btn-secondary-text) !important;
            border: 1px solid var(--asc-card-border);
        }
        .asc-btn-secondary:hover {
            background: var(--asc-btn-secondary-hover);
        }

        .asc-stepper-btn {
            width: 30px;
            height: 30px;
            border-radius: 4px;
            border: 1px solid var(--asc-input-border);
            background: var(--asc-btn-secondary);
            color: var(--asc-text);
            font-weight: bold;
            font-size: 1.1em;
            cursor: pointer;
            display: inline-flex;
            align-items: center;
            justify-content: center;
            transition: background 0.15s ease;
        }
        .asc-stepper-btn:hover:not(:disabled) {
            background: var(--asc-btn-primary);
            color: #ffffff;
            border-color: var(--asc-btn-primary);
        }
        .asc-stepper-btn:disabled {
            opacity: 0.4;
            cursor: not-allowed;
        }

        .asc-badge {
            display: inline-block;
            padding: 3px 8px;
            border-radius: 4px;
            font-size: 0.85em;
            font-weight: 600;
            background: var(--asc-badge-bg);
            color: var(--asc-badge-text);
        }
        .asc-badge-pool {
            font-size: 1em;
            padding: 4px 10px;
            cursor: default;
            border: 1px solid var(--asc-card-border);
        }
        .asc-badge-pool.assigned {
            opacity: 0.45;
            text-decoration: line-through;
        }

        .asc-attr-grid {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(290px, 1fr));
            gap: 12px;
            margin-bottom: 16px;
        }
        .asc-attr-card {
            background: var(--asc-card-bg);
            border: 1px solid var(--asc-card-border);
            border-radius: 6px;
            padding: 12px;
            display: flex;
            justify-content: space-between;
            align-items: center;
            transition: border-color 0.2s ease;
        }
        .asc-attr-card:hover {
            border-color: var(--asc-btn-primary);
        }
        .asc-attr-info {
            display: flex;
            flex-direction: column;
            gap: 2px;
        }
        .asc-attr-title {
            font-weight: bold;
            font-size: 0.95em;
        }
        .asc-attr-desc {
            font-size: 0.78em;
            color: var(--asc-subtext);
        }

        .asc-table {
            width: 100%;
            border-collapse: collapse;
            text-align: center;
            font-size: 0.9em;
            margin-top: 10px;
        }
        .asc-table th {
            background: var(--asc-table-hdr-bg);
            color: var(--asc-table-hdr-text);
            border: 1px solid var(--asc-table-border);
            padding: 6px 10px;
        }
        .asc-table td {
            background: var(--asc-table-bg);
            color: var(--asc-text);
            border: 1px solid var(--asc-table-border);
            padding: 6px 10px;
        }

        .asc-budget-bar {
            display: flex;
            flex-wrap: wrap;
            gap: 16px;
            align-items: center;
            background: var(--asc-highlight-bg);
            border: 1px solid var(--asc-card-border);
            border-radius: 6px;
            padding: 10px 16px;
            margin-bottom: 14px;
        }
        .asc-budget-item {
            display: flex;
            align-items: center;
            gap: 6px;
            font-size: 0.9em;
        }

        .asc-status-text {
            font-weight: bold;
            font-size: 0.85em;
            margin-left: 8px;
        }
    `;

    function injectStyles() {
        if (!document.getElementById('asc-calc-injected-styles')) {
            const style = document.createElement('style');
            style.id = 'asc-calc-injected-styles';
            style.textContent = CSS_STYLES;
            document.head.appendChild(style);
        }
    }

    // =========================================================================
    // 4. Widget Initialization & Logic
    // =========================================================================
    function initAbilityScoreWidget() {
        const container = document.getElementById('ability-score-calc-widget') ||
                          document.getElementById('ability-theme-calc-widget') ||
                          document.querySelector('.ability-score-calc-widget');
        if (!container || container.dataset.loaded) return;
        container.dataset.loaded = 'true';

        injectStyles();

        // ---------------------------------------------------------------------
        // State Management
        // ---------------------------------------------------------------------
        const state = {
            activeTab: 'spread',
            spread: {
                presetKey: 'standard',
                assignments: { STR: 15, DEX: 14, CON: 13, INT: 12, WIS: 10, CHA: 8 }
            },
            roller: {
                fantasyLevel: 'standard',
                rolledScores: [],
                assignmentMode: 'order',
                assignments: { STR: null, DEX: null, CON: null, INT: null, WIS: null, CHA: null },
                source: ''
            },
            pointbuy: {
                budgetKey: 'standard',
                customBudget: 15,
                scores: { STR: 10, DEX: 10, CON: 10, INT: 10, WIS: 10, CHA: 10 }
            }
        };

        // ---------------------------------------------------------------------
        // Render Shell
        // ---------------------------------------------------------------------
        container.innerHTML = `
            <div class="asc-container">
                <div class="asc-tabs">
                    <button class="asc-tab-btn active" data-tab="spread">📋 Standard Spread</button>
                    <button class="asc-tab-btn" data-tab="roller">🎲 Dice Roller</button>
                    <button class="asc-tab-btn" data-tab="pointbuy">⚖️ Point Buy</button>
                </div>

                <!-- TAB 1: STANDARD SPREAD -->
                <div id="asc-tab-spread" class="asc-tab-content active">
                    <div class="asc-control-row">
                        <div>
                            <label style="font-weight: bold; margin-right: 6px;">Campaign Spread:</label>
                            <select id="asc-spread-preset" class="asc-input">
                                <option value="standard" selected>Standard Fantasy (15, 14, 13, 12, 10, 8)</option>
                                <option value="low">Low Fantasy (14, 12, 10, 10, 8, 8)</option>
                                <option value="high">High Fantasy (19, 18, 16, 13, 12, 10)</option>
                            </select>
                        </div>
                        <button id="asc-spread-btn-auto" class="asc-btn asc-btn-secondary" title="Assign in standard order">Auto-Assign</button>
                        <button id="asc-spread-btn-clear" class="asc-btn asc-btn-secondary" title="Clear all assignments">Clear</button>
                    </div>

                    <div style="margin-bottom: 12px; display: flex; align-items: center; gap: 8px; flex-wrap: wrap;">
                        <span style="font-weight: 600; font-size: 0.9em; color: var(--asc-subtext);">Available Scores:</span>
                        <div id="asc-spread-pool" style="display: flex; gap: 6px; flex-wrap: wrap;"></div>
                    </div>

                    <div id="asc-spread-grid" class="asc-attr-grid"></div>

                    <div style="display: flex; gap: 8px; align-items: center; margin-top: 10px; margin-bottom: 10px;">
                        <button id="asc-spread-btn-copy-wiki" class="asc-btn asc-btn-copy">Copy WikiText</button>
                        <button id="asc-spread-btn-copy-text" class="asc-btn asc-btn-secondary">Copy Summary</button>
                        <span id="asc-spread-copy-status" class="asc-status-text" style="color: var(--asc-success); display: none;">Copied!</span>
                    </div>

                    <div id="asc-spread-preview"></div>

                    <details style="margin-top: 10px;">
                        <summary style="cursor: pointer; color: var(--asc-link); font-size: 0.88em; font-weight: 500;">View Generated WikiText</summary>
                        <textarea id="asc-spread-wikitext" class="asc-input" readonly style="width: 100%; height: 85px; margin-top: 5px; font-family: monospace; font-size: 0.85em;"></textarea>
                    </details>
                </div>

                <!-- TAB 2: DICE ROLLER -->
                <div id="asc-tab-roller" class="asc-tab-content">
                    <div class="asc-control-row">
                        <div>
                            <label style="font-weight: bold; margin-right: 6px;">Fantasy Level:</label>
                            <select id="asc-roller-level" class="asc-input">
                                <option value="standard" selected>Standard Fantasy (4d6 drop 1, reroll &le; 8)</option>
                                <option value="low">Low Fantasy (3d6, reroll &le; 7)</option>
                                <option value="high">High Fantasy (4d6 drop 1, 7 rolls drop lowest)</option>
                                <option value="epic">Epic Fantasy (5d6 drop 2, 7 rolls drop lowest)</option>
                            </select>
                        </div>
                        <button id="asc-roller-btn-roll" class="asc-btn asc-btn-primary">🎲 Roll Scores</button>
                        <span id="asc-roller-source-badge" class="asc-badge" style="display: none;"></span>
                    </div>

                    <div id="asc-roller-results-bar" style="margin-bottom: 14px; display: none;">
                        <div style="display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 8px;">
                            <span style="font-weight: 600; font-size: 0.9em; color: var(--asc-subtext);">Rolled Scores:</span>
                            <div id="asc-roller-scores-pool" style="display: flex; gap: 6px; flex-wrap: wrap;"></div>
                        </div>
                        <div style="display: flex; gap: 12px; align-items: center; font-size: 0.88em;">
                            <label style="font-weight: 600; cursor: pointer;">
                                <input type="radio" name="asc-roller-mode" value="order" checked> Assign in Order (STR &rarr; CHA)
                            </label>
                            <label style="font-weight: 600; cursor: pointer;">
                                <input type="radio" name="asc-roller-mode" value="custom"> Custom Assign
                            </label>
                        </div>
                    </div>

                    <div id="asc-roller-grid" class="asc-attr-grid"></div>

                    <div style="display: flex; gap: 8px; align-items: center; margin-top: 10px; margin-bottom: 10px;">
                        <button id="asc-roller-btn-copy-wiki" class="asc-btn asc-btn-copy" style="display: none;">Copy WikiText</button>
                        <button id="asc-roller-btn-copy-text" class="asc-btn asc-btn-secondary" style="display: none;">Copy Summary</button>
                        <span id="asc-roller-copy-status" class="asc-status-text" style="color: var(--asc-success); display: none;">Copied!</span>
                    </div>

                    <div id="asc-roller-preview"></div>

                    <details id="asc-roller-details" style="margin-top: 10px; display: none;">
                        <summary style="cursor: pointer; color: var(--asc-link); font-size: 0.88em; font-weight: 500;">View Generated WikiText</summary>
                        <textarea id="asc-roller-wikitext" class="asc-input" readonly style="width: 100%; height: 85px; margin-top: 5px; font-family: monospace; font-size: 0.85em;"></textarea>
                    </details>
                </div>

                <!-- TAB 3: POINT BUY -->
                <div id="asc-tab-pointbuy" class="asc-tab-content">
                    <div class="asc-control-row">
                        <div>
                            <label style="font-weight: bold; margin-right: 6px;">Campaign Type:</label>
                            <select id="asc-pb-budget-select" class="asc-input">
                                <option value="low">Low Fantasy (10 Points)</option>
                                <option value="standard" selected>Standard Fantasy (15 Points)</option>
                                <option value="high">High Fantasy (20 Points)</option>
                                <option value="epic">Epic Fantasy (25 Points)</option>
                                <option value="custom">Custom Points</option>
                            </select>
                        </div>
                        <div id="asc-pb-custom-wrap" style="display: none; align-items: center; gap: 6px;">
                            <label style="font-weight: bold;">Points:</label>
                            <input id="asc-pb-custom-points" class="asc-input" type="number" value="15" min="0" max="100" style="width: 65px;">
                        </div>
                        <button id="asc-pb-btn-reset" class="asc-btn asc-btn-secondary" title="Reset all abilities to 10">Reset to 10s</button>
                    </div>

                    <div class="asc-budget-bar">
                        <div class="asc-budget-item">
                            <span style="color: var(--asc-subtext);">Total Budget:</span>
                            <span id="asc-pb-budget-total" style="font-weight: bold;">15</span>
                        </div>
                        <div class="asc-budget-item">
                            <span style="color: var(--asc-subtext);">Points Spent:</span>
                            <span id="asc-pb-budget-spent" style="font-weight: bold;">0</span>
                        </div>
                        <div class="asc-budget-item">
                            <span style="color: var(--asc-subtext);">Remaining:</span>
                            <span id="asc-pb-budget-remaining" class="asc-badge" style="font-size: 0.95em; font-weight: bold;">15</span>
                        </div>
                        <div id="asc-pb-budget-message" style="font-size: 0.85em; font-weight: 600;"></div>
                    </div>

                    <div id="asc-pb-grid" class="asc-attr-grid"></div>

                    <div style="display: flex; gap: 8px; align-items: center; margin-top: 10px; margin-bottom: 10px;">
                        <button id="asc-pb-btn-copy-wiki" class="asc-btn asc-btn-copy">Copy WikiText</button>
                        <button id="asc-pb-btn-copy-text" class="asc-btn asc-btn-secondary">Copy Summary</button>
                        <span id="asc-pb-copy-status" class="asc-status-text" style="color: var(--asc-success); display: none;">Copied!</span>
                    </div>

                    <div id="asc-pb-preview"></div>

                    <details style="margin-top: 10px;">
                        <summary style="cursor: pointer; color: var(--asc-link); font-size: 0.88em; font-weight: 500;">View Ability Score Cost Table</summary>
                        <div style="margin-top: 8px; overflow-x: auto;">
                            <table class="asc-table" style="max-width: 450px;">
                                <tr><th>Score</th><th>Point Cost</th><th>Modifier</th></tr>
                                <tr><td>7</td><td>-4 (gains 4)</td><td>-2</td></tr>
                                <tr><td>8</td><td>-2 (gains 2)</td><td>-1</td></tr>
                                <tr><td>9</td><td>-1 (gains 1)</td><td>-1</td></tr>
                                <tr><td>10</td><td>0 (base)</td><td>0</td></tr>
                                <tr><td>11</td><td>1</td><td>0</td></tr>
                                <tr><td>12</td><td>2</td><td>+1</td></tr>
                                <tr><td>13</td><td>3</td><td>+1</td></tr>
                                <tr><td>14</td><td>5</td><td>+2</td></tr>
                                <tr><td>15</td><td>7</td><td>+2</td></tr>
                                <tr><td>16</td><td>10</td><td>+3</td></tr>
                                <tr><td>17</td><td>13</td><td>+3</td></tr>
                                <tr><td>18</td><td>17</td><td>+4</td></tr>
                            </table>
                        </div>
                    </details>

                    <details style="margin-top: 10px;">
                        <summary style="cursor: pointer; color: var(--asc-link); font-size: 0.88em; font-weight: 500;">View Generated WikiText</summary>
                        <textarea id="asc-pb-wikitext" class="asc-input" readonly style="width: 100%; height: 85px; margin-top: 5px; font-family: monospace; font-size: 0.85em;"></textarea>
                    </details>
                </div>
            </div>
        `;

        // ---------------------------------------------------------------------
        // Tab Navigation
        // ---------------------------------------------------------------------
        const tabButtons = container.querySelectorAll('.asc-tab-btn');
        tabButtons.forEach(btn => {
            btn.addEventListener('click', function () {
                const targetTab = this.dataset.tab;
                state.activeTab = targetTab;
                tabButtons.forEach(b => b.classList.remove('active'));
                this.classList.add('active');

                container.querySelectorAll('.asc-tab-content').forEach(pane => {
                    pane.classList.remove('active');
                });
                const targetPane = document.getElementById(`asc-tab-${targetTab}`);
                if (targetPane) targetPane.classList.add('active');
            });
        });

        // ---------------------------------------------------------------------
        // Table & Output Builders
        // ---------------------------------------------------------------------
        function buildWikiText(scoresMap, titleNote = '') {
            const lines = ['{| class="wikitable"'];
            if (titleNote) lines.push(`|+ ${titleNote}`);
            lines.push('! Ability Theme !! Score !! Modifier');
            for (const attr of CORE_ATTRIBUTES) {
                const score = scoresMap[attr.key];
                const scoreStr = (score !== null && score !== undefined) ? String(score) : '—';
                const mod = (score !== null && score !== undefined) ? formatModifier(calculateModifier(score)) : '—';
                lines.push(`|-\n| ${attr.name} (${attr.key}) || ${scoreStr} || ${mod}`);
            }
            lines.push('|}');
            return lines.join('\n');
        }

        function buildSummaryText(scoresMap) {
            return CORE_ATTRIBUTES.map(attr => {
                const score = scoresMap[attr.key];
                if (score === null || score === undefined) return `${attr.key}: —`;
                const mod = formatModifier(calculateModifier(score));
                return `${attr.key}: ${score} (${mod})`;
            }).join(' | ');
        }

        function buildPreviewTable(scoresMap) {
            let html = '<table class="asc-table">';
            html += '<tr><th>Ability Theme</th><th>Score</th><th>Modifier</th></tr>';
            for (const attr of CORE_ATTRIBUTES) {
                const score = scoresMap[attr.key];
                const scoreStr = (score !== null && score !== undefined) ? `<strong>${score}</strong>` : '<em>Unassigned</em>';
                const modStr = (score !== null && score !== undefined) ? formatModifier(calculateModifier(score)) : '—';
                html += `<tr><td style="text-align: left; font-weight: 600;">${attr.name} (${attr.key})</td><td>${scoreStr}</td><td>${modStr}</td></tr>`;
            }
            html += '</table>';
            return html;
        }

        function setupClipboardCopy(buttonId, textGetter, statusId) {
            const btn = document.getElementById(buttonId);
            const status = document.getElementById(statusId);
            if (!btn) return;
            btn.addEventListener('click', function () {
                const text = textGetter();
                navigator.clipboard.writeText(text).then(() => {
                    if (status) {
                        status.style.display = 'inline';
                        setTimeout(() => { status.style.display = 'none'; }, 2000);
                    }
                }).catch(() => {
                    alert('Could not copy to clipboard. Please select and copy from the text box below.');
                });
            });
        }

        // =====================================================================
        // TAB 1: Standard Spread Logic
        // =====================================================================
        function renderSpreadTab() {
            const preset = SPREADS[state.spread.presetKey];
            const spreadScores = [...preset.scores];

            // Render Available Pool
            const poolEl = document.getElementById('asc-spread-pool');
            poolEl.innerHTML = '';
            const assignedVals = Object.values(state.spread.assignments).filter(v => v !== null);
            const tempAssigned = [...assignedVals];

            spreadScores.forEach((score) => {
                const badge = document.createElement('span');
                badge.className = 'asc-badge asc-badge-pool';
                badge.textContent = score;
                const foundIdx = tempAssigned.indexOf(score);
                if (foundIdx !== -1) {
                    badge.classList.add('assigned');
                    tempAssigned.splice(foundIdx, 1);
                }
                poolEl.appendChild(badge);
            });

            // Render Attribute Grid
            const gridEl = document.getElementById('asc-spread-grid');
            gridEl.innerHTML = '';

            CORE_ATTRIBUTES.forEach(attr => {
                const currentScore = state.spread.assignments[attr.key];
                const card = document.createElement('div');
                card.className = 'asc-attr-card';

                const info = document.createElement('div');
                info.className = 'asc-attr-info';
                info.innerHTML = `
                    <div class="asc-attr-title">${attr.name} (${attr.key})</div>
                    <div class="asc-attr-desc">${attr.desc}</div>
                `;

                const controls = document.createElement('div');
                controls.style.display = 'flex';
                controls.style.alignItems = 'center';
                controls.style.gap = '8px';

                const select = document.createElement('select');
                select.className = 'asc-input';
                select.style.minWidth = '75px';
                select.innerHTML = '<option value="">--</option>';

                spreadScores.forEach((s) => {
                    const opt = document.createElement('option');
                    opt.value = String(s);
                    opt.textContent = String(s);
                    select.appendChild(opt);
                });

                if (currentScore !== null && currentScore !== undefined) {
                    select.value = String(currentScore);
                } else {
                    select.value = '';
                }

                select.addEventListener('change', function () {
                    const newScore = this.value ? parseInt(this.value, 10) : null;
                    if (newScore !== null) {
                        const currentUsage = Object.values(state.spread.assignments).filter(v => v === newScore).length;
                        const maxAllowed = spreadScores.filter(v => v === newScore).length;
                        if (currentUsage >= maxAllowed) {
                            for (const k of Object.keys(state.spread.assignments)) {
                                if (k !== attr.key && state.spread.assignments[k] === newScore) {
                                    state.spread.assignments[k] = currentScore;
                                    break;
                                }
                            }
                        }
                    }
                    state.spread.assignments[attr.key] = newScore;
                    renderSpreadTab();
                });

                const modBadge = document.createElement('span');
                modBadge.className = 'asc-badge';
                modBadge.style.minWidth = '30px';
                modBadge.style.textAlign = 'center';
                modBadge.textContent = currentScore !== null ? formatModifier(calculateModifier(currentScore)) : '—';
                if (currentScore !== null && calculateModifier(currentScore) > 0) {
                    modBadge.style.background = 'var(--asc-highlight-bg)';
                    modBadge.style.color = 'var(--asc-btn-primary)';
                }

                controls.appendChild(select);
                controls.appendChild(modBadge);
                card.appendChild(info);
                card.appendChild(controls);
                gridEl.appendChild(card);
            });

            const wikiText = buildWikiText(state.spread.assignments, `Table: ${preset.name} Ability Scores`);
            document.getElementById('asc-spread-wikitext').value = wikiText;
            document.getElementById('asc-spread-preview').innerHTML = buildPreviewTable(state.spread.assignments);
        }

        document.getElementById('asc-spread-preset').addEventListener('change', function () {
            state.spread.presetKey = this.value;
            const scores = SPREADS[this.value].scores;
            CORE_ATTRIBUTES.forEach((attr, idx) => {
                state.spread.assignments[attr.key] = scores[idx] !== undefined ? scores[idx] : null;
            });
            renderSpreadTab();
        });

        document.getElementById('asc-spread-btn-auto').addEventListener('click', function () {
            const scores = SPREADS[state.spread.presetKey].scores;
            CORE_ATTRIBUTES.forEach((attr, idx) => {
                state.spread.assignments[attr.key] = scores[idx] !== undefined ? scores[idx] : null;
            });
            renderSpreadTab();
        });

        document.getElementById('asc-spread-btn-clear').addEventListener('click', function () {
            CORE_ATTRIBUTES.forEach(attr => {
                state.spread.assignments[attr.key] = null;
            });
            renderSpreadTab();
        });

        setupClipboardCopy('asc-spread-btn-copy-wiki', () => document.getElementById('asc-spread-wikitext').value, 'asc-spread-copy-status');
        setupClipboardCopy('asc-spread-btn-copy-text', () => buildSummaryText(state.spread.assignments), 'asc-spread-copy-status');

        // =====================================================================
        // TAB 2: Dice Roller Logic
        // =====================================================================
        async function rollAbilityStats() {
            const levelKey = document.getElementById('asc-roller-level').value;
            state.roller.fantasyLevel = levelKey;
            const config = ROLLER_CONFIGS[levelKey];

            const rollBtn = document.getElementById('asc-roller-btn-roll');
            rollBtn.disabled = true;
            rollBtn.textContent = '⏳ Rolling...';

            let finalScores = [];
            let sourceLabel = '';

            try {
                const apiBase = (window.FUTUREPATH_API_BASE !== undefined) ? window.FUTUREPATH_API_BASE : '';
                const url = `${apiBase}/tasks/roll/character/${config.apiLevel}`;
                const response = await fetch(url, {
                    headers: { 'Accept': 'application/json' }
                });

                if (response.ok) {
                    const data = await response.json();
                    if (data && data.Rolls && Array.isArray(data.Rolls) && data.Rolls.length >= 6) {
                        finalScores = data.Rolls.map(r => r.Total);
                        sourceLabel = `API Endpoint (/tasks/roll/character/${config.apiLevel})`;
                    }
                }
            } catch (err) {
                // Fallback on network/CORS error
            }

            if (finalScores.length < 6) {
                finalScores = rollCharacterLocally(config);
                sourceLabel = 'Local FuturePath Engine (Client)';
            }

            state.roller.rolledScores = finalScores;
            state.roller.source = sourceLabel;

            if (state.roller.assignmentMode === 'order') {
                CORE_ATTRIBUTES.forEach((attr, idx) => {
                    state.roller.assignments[attr.key] = finalScores[idx] !== undefined ? finalScores[idx] : null;
                });
            }

            rollBtn.disabled = false;
            rollBtn.textContent = '🎲 Roll Scores';

            renderRollerTab();
        }

        function renderRollerTab() {
            const hasRolls = state.roller.rolledScores && state.roller.rolledScores.length > 0;
            const resultsBar = document.getElementById('asc-roller-results-bar');
            const sourceBadge = document.getElementById('asc-roller-source-badge');
            const copyWikiBtn = document.getElementById('asc-roller-btn-copy-wiki');
            const copyTextBtn = document.getElementById('asc-roller-btn-copy-text');
            const detailsEl = document.getElementById('asc-roller-details');

            if (!hasRolls) {
                resultsBar.style.display = 'none';
                sourceBadge.style.display = 'none';
                copyWikiBtn.style.display = 'none';
                copyTextBtn.style.display = 'none';
                detailsEl.style.display = 'none';
                document.getElementById('asc-roller-grid').innerHTML = `
                    <div style="grid-column: 1 / -1; padding: 25px; text-align: center; color: var(--asc-subtext); border: 1px dashed var(--asc-card-border); border-radius: 6px;">
                        Select a Fantasy level above and click <strong>Roll Scores</strong> to generate character stats using the FuturePath rules.
                    </div>
                `;
                document.getElementById('asc-roller-preview').innerHTML = '';
                return;
            }

            resultsBar.style.display = 'block';
            sourceBadge.style.display = 'inline-block';
            sourceBadge.textContent = state.roller.source;
            copyWikiBtn.style.display = 'inline-flex';
            copyTextBtn.style.display = 'inline-flex';
            detailsEl.style.display = 'block';

            const poolEl = document.getElementById('asc-roller-scores-pool');
            poolEl.innerHTML = '';
            const tempAssigned = Object.values(state.roller.assignments).filter(v => v !== null);

            state.roller.rolledScores.forEach(score => {
                const badge = document.createElement('span');
                badge.className = 'asc-badge asc-badge-pool';
                badge.textContent = score;
                const foundIdx = tempAssigned.indexOf(score);
                if (foundIdx !== -1) {
                    badge.classList.add('assigned');
                    tempAssigned.splice(foundIdx, 1);
                }
                poolEl.appendChild(badge);
            });

            const gridEl = document.getElementById('asc-roller-grid');
            gridEl.innerHTML = '';

            CORE_ATTRIBUTES.forEach((attr) => {
                const currentScore = state.roller.assignments[attr.key];
                const card = document.createElement('div');
                card.className = 'asc-attr-card';

                const info = document.createElement('div');
                info.className = 'asc-attr-info';
                info.innerHTML = `
                    <div class="asc-attr-title">${attr.name} (${attr.key})</div>
                    <div class="asc-attr-desc">${attr.desc}</div>
                `;

                const controls = document.createElement('div');
                controls.style.display = 'flex';
                controls.style.alignItems = 'center';
                controls.style.gap = '8px';

                if (state.roller.assignmentMode === 'order') {
                    const scoreDisplay = document.createElement('span');
                    scoreDisplay.style.fontWeight = 'bold';
                    scoreDisplay.style.fontSize = '1.05em';
                    scoreDisplay.textContent = currentScore !== null ? String(currentScore) : '—';
                    controls.appendChild(scoreDisplay);
                } else {
                    const select = document.createElement('select');
                    select.className = 'asc-input';
                    select.style.minWidth = '75px';
                    select.innerHTML = '<option value="">--</option>';

                    state.roller.rolledScores.forEach(s => {
                        const opt = document.createElement('option');
                        opt.value = String(s);
                        opt.textContent = String(s);
                        select.appendChild(opt);
                    });

                    select.value = currentScore !== null ? String(currentScore) : '';
                    select.addEventListener('change', function () {
                        const newScore = this.value ? parseInt(this.value, 10) : null;
                        if (newScore !== null) {
                            const currentUsage = Object.values(state.roller.assignments).filter(v => v === newScore).length;
                            const maxAllowed = state.roller.rolledScores.filter(v => v === newScore).length;
                            if (currentUsage >= maxAllowed) {
                                for (const k of Object.keys(state.roller.assignments)) {
                                    if (k !== attr.key && state.roller.assignments[k] === newScore) {
                                        state.roller.assignments[k] = currentScore;
                                        break;
                                    }
                                }
                            }
                        }
                        state.roller.assignments[attr.key] = newScore;
                        renderRollerTab();
                    });
                    controls.appendChild(select);
                }

                const modBadge = document.createElement('span');
                modBadge.className = 'asc-badge';
                modBadge.style.minWidth = '30px';
                modBadge.style.textAlign = 'center';
                modBadge.textContent = currentScore !== null ? formatModifier(calculateModifier(currentScore)) : '—';
                if (currentScore !== null && calculateModifier(currentScore) > 0) {
                    modBadge.style.background = 'var(--asc-highlight-bg)';
                    modBadge.style.color = 'var(--asc-btn-primary)';
                }

                controls.appendChild(modBadge);
                card.appendChild(info);
                card.appendChild(controls);
                gridEl.appendChild(card);
            });

            const config = ROLLER_CONFIGS[state.roller.fantasyLevel];
            const wikiText = buildWikiText(state.roller.assignments, `Table: ${config.name} Rolled Ability Scores`);
            document.getElementById('asc-roller-wikitext').value = wikiText;
            document.getElementById('asc-roller-preview').innerHTML = buildPreviewTable(state.roller.assignments);
        }

        document.getElementById('asc-roller-btn-roll').addEventListener('click', rollAbilityStats);

        const modeRadios = container.querySelectorAll('input[name="asc-roller-mode"]');
        modeRadios.forEach(radio => {
            radio.addEventListener('change', function () {
                state.roller.assignmentMode = this.value;
                if (this.value === 'order' && state.roller.rolledScores.length > 0) {
                    CORE_ATTRIBUTES.forEach((attr, idx) => {
                        state.roller.assignments[attr.key] = state.roller.rolledScores[idx] || null;
                    });
                }
                renderRollerTab();
            });
        });

        setupClipboardCopy('asc-roller-btn-copy-wiki', () => document.getElementById('asc-roller-wikitext').value, 'asc-roller-copy-status');
        setupClipboardCopy('asc-roller-btn-copy-text', () => buildSummaryText(state.roller.assignments), 'asc-roller-copy-status');

        // =====================================================================
        // TAB 3: Point Buy Logic
        // =====================================================================
        function getPointBuyBudget() {
            if (state.pointbuy.budgetKey === 'custom') {
                return parseInt(document.getElementById('asc-pb-custom-points').value, 10) || 0;
            }
            return POINT_BUY_BUDGETS[state.pointbuy.budgetKey].points;
        }

        function calculatePointsSpent() {
            let total = 0;
            for (const attr of CORE_ATTRIBUTES) {
                const score = state.pointbuy.scores[attr.key] || 10;
                total += (POINT_BUY_COSTS[score] !== undefined ? POINT_BUY_COSTS[score] : 0);
            }
            return total;
        }

        function renderPointBuyTab() {
            const budgetTotal = getPointBuyBudget();
            const spent = calculatePointsSpent();
            const remaining = budgetTotal - spent;

            document.getElementById('asc-pb-budget-total').textContent = String(budgetTotal);
            document.getElementById('asc-pb-budget-spent').textContent = String(spent);

            const remBadge = document.getElementById('asc-pb-budget-remaining');
            remBadge.textContent = String(remaining);

            const msgEl = document.getElementById('asc-pb-budget-message');
            if (remaining === 0) {
                remBadge.style.background = 'var(--asc-success)';
                remBadge.style.color = '#ffffff';
                msgEl.style.color = 'var(--asc-success)';
                msgEl.textContent = '✓ Points perfectly allocated!';
            } else if (remaining > 0) {
                remBadge.style.background = 'var(--asc-btn-primary)';
                remBadge.style.color = '#ffffff';
                msgEl.style.color = 'var(--asc-subtext)';
                msgEl.textContent = `${remaining} point${remaining === 1 ? '' : 's'} available to spend`;
            } else {
                remBadge.style.background = 'var(--asc-error)';
                remBadge.style.color = '#ffffff';
                msgEl.style.color = 'var(--asc-error)';
                msgEl.textContent = `⚠ Over budget by ${Math.abs(remaining)} point${Math.abs(remaining) === 1 ? '' : 's'}!`;
            }

            const gridEl = document.getElementById('asc-pb-grid');
            gridEl.innerHTML = '';

            CORE_ATTRIBUTES.forEach(attr => {
                const currentScore = state.pointbuy.scores[attr.key];
                const currentCost = POINT_BUY_COSTS[currentScore] || 0;
                const mod = calculateModifier(currentScore);

                const card = document.createElement('div');
                card.className = 'asc-attr-card';

                const info = document.createElement('div');
                info.className = 'asc-attr-info';
                info.innerHTML = `
                    <div class="asc-attr-title">${attr.name} (${attr.key})</div>
                    <div class="asc-attr-desc">${attr.desc}</div>
                `;

                const controls = document.createElement('div');
                controls.style.display = 'flex';
                controls.style.alignItems = 'center';
                controls.style.gap = '6px';

                const btnMinus = document.createElement('button');
                btnMinus.className = 'asc-stepper-btn';
                btnMinus.textContent = '−';
                btnMinus.title = 'Decrease score';
                btnMinus.disabled = (currentScore <= 7);
                btnMinus.addEventListener('click', function () {
                    if (state.pointbuy.scores[attr.key] > 7) {
                        state.pointbuy.scores[attr.key] -= 1;
                        renderPointBuyTab();
                    }
                });

                const scoreBox = document.createElement('span');
                scoreBox.style.display = 'inline-block';
                scoreBox.style.minWidth = '28px';
                scoreBox.style.textAlign = 'center';
                scoreBox.style.fontWeight = 'bold';
                scoreBox.style.fontSize = '1.1em';
                scoreBox.textContent = String(currentScore);

                const btnPlus = document.createElement('button');
                btnPlus.className = 'asc-stepper-btn';
                btnPlus.textContent = '+';
                btnPlus.title = 'Increase score';
                btnPlus.disabled = (currentScore >= 18);
                btnPlus.addEventListener('click', function () {
                    if (state.pointbuy.scores[attr.key] < 18) {
                        state.pointbuy.scores[attr.key] += 1;
                        renderPointBuyTab();
                    }
                });

                const costBadge = document.createElement('span');
                costBadge.className = 'asc-badge';
                costBadge.style.fontSize = '0.8em';
                costBadge.style.minWidth = '52px';
                costBadge.style.textAlign = 'center';
                costBadge.textContent = `${currentCost >= 0 ? '+' : ''}${currentCost} pts`;

                const modBadge = document.createElement('span');
                modBadge.className = 'asc-badge';
                modBadge.style.minWidth = '28px';
                modBadge.style.textAlign = 'center';
                modBadge.textContent = formatModifier(mod);
                if (mod > 0) {
                    modBadge.style.background = 'var(--asc-highlight-bg)';
                    modBadge.style.color = 'var(--asc-btn-primary)';
                }

                controls.appendChild(btnMinus);
                controls.appendChild(scoreBox);
                controls.appendChild(btnPlus);
                controls.appendChild(costBadge);
                controls.appendChild(modBadge);

                card.appendChild(info);
                card.appendChild(controls);
                gridEl.appendChild(card);
            });

            const campaignName = state.pointbuy.budgetKey === 'custom'
                ? `Custom (${budgetTotal} pts)`
                : POINT_BUY_BUDGETS[state.pointbuy.budgetKey].name;
            const wikiText = buildWikiText(state.pointbuy.scores, `Table: Point Buy (${campaignName}) Ability Scores`);
            document.getElementById('asc-pb-wikitext').value = wikiText;
            document.getElementById('asc-pb-preview').innerHTML = buildPreviewTable(state.pointbuy.scores);
        }

        document.getElementById('asc-pb-budget-select').addEventListener('change', function () {
            state.pointbuy.budgetKey = this.value;
            const customWrap = document.getElementById('asc-pb-custom-wrap');
            customWrap.style.display = (this.value === 'custom') ? 'inline-flex' : 'none';
            renderPointBuyTab();
        });

        document.getElementById('asc-pb-custom-points').addEventListener('input', renderPointBuyTab);

        document.getElementById('asc-pb-btn-reset').addEventListener('click', function () {
            CORE_ATTRIBUTES.forEach(attr => {
                state.pointbuy.scores[attr.key] = 10;
            });
            renderPointBuyTab();
        });

        setupClipboardCopy('asc-pb-btn-copy-wiki', () => document.getElementById('asc-pb-wikitext').value, 'asc-pb-copy-status');
        setupClipboardCopy('asc-pb-btn-copy-text', () => buildSummaryText(state.pointbuy.scores), 'asc-pb-copy-status');

        // Initial Renders
        renderSpreadTab();
        renderRollerTab();
        renderPointBuyTab();
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initAbilityScoreWidget);
    } else {
        initAbilityScoreWidget();
    }
})();

/**
 * spaceship_damage_calc_wiki_ui.js
 *
 * MediaWiki Interactive Spaceship Battle Damage Probability Calculator Widget
 *
 * Calculates d10 Battle Pool probability distributions against Spaceship
 * Battle Damage Difficulty Checks (BD-DC: Structure, Shields).
 * Supports Ship Weapons re-rolls, Ship Sensor modifiers, Officer ability boosts,
 * and Sensor Officer Critical Scan mechanics.
 *
 * Embed this script into MediaWiki:Common.js or a MediaWiki Gadget.
 * To display the widget on any wiki page, add:
 *   <div id="spaceship-damage-calc-widget"></div>
 *   (or <div id="bd-dc-calc-widget"></div>)
 */

(function () {
    'use strict';

    // =========================================================================
    // 1. Core Mathematical & Calculation Engine
    // =========================================================================

    // Precompute Pascal triangle for combinations nCr up to n = 100
    const MAX_COMB_N = 100;
    const BINOM_TABLE = [];
    for (let n = 0; n <= MAX_COMB_N; n++) {
        BINOM_TABLE[n] = new Array(n + 1).fill(0);
        BINOM_TABLE[n][0] = 1;
        BINOM_TABLE[n][n] = 1;
        for (let k = 1; k < n; k++) {
            BINOM_TABLE[n][k] = BINOM_TABLE[n - 1][k - 1] + BINOM_TABLE[n - 1][k];
        }
    }

    function getCombination(n, k) {
        if (k < 0 || k > n) return 0;
        if (n <= MAX_COMB_N) return BINOM_TABLE[n][k];
        let res = 1;
        for (let i = 1; i <= k; i++) {
            res = (res * (n - k + i)) / i;
        }
        return res;
    }

    /**
     * Compute dynamic programming table for Weapons modifier re-rolls.
     * State: D[k][r][e]
     * k = active successful dice eligible to be re-rolled (0..maxK)
     * r = remaining re-rolls allowed (0..R)
     * e = additional successes achieved (0..r)
     */
    function computeRerollDP(maxK, R, pSuccess) {
        const D = [];
        for (let k = 0; k <= maxK; k++) {
            D[k] = [];
            for (let r = 0; r <= R; r++) {
                D[k][r] = new Array(r + 1).fill(0.0);
            }
        }
        for (let k = 0; k <= maxK; k++) D[k][0][0] = 1.0;
        for (let r = 0; r <= R; r++) D[0][r][0] = 1.0;

        for (let r = 1; r <= R; r++) {
            for (let k = 1; k <= maxK; k++) {
                D[k][r][0] = (1.0 - pSuccess) * D[k - 1][r - 1][0];
                for (let e = 1; e <= r; e++) {
                    const term1 = pSuccess * D[k][r - 1][e - 1];
                    const term2 = (e <= r - 1) ? (1.0 - pSuccess) * D[k - 1][r - 1][e] : 0.0;
                    D[k][r][e] = term1 + term2;
                }
            }
        }
        return D;
    }

    /**
     * Calculate damage statistics for a given pool size and BD-DC (X, Y).
     */
    function calculateDamageStats(N, W, S, sEnabled, WO, woEnabled, SO, soEnabled, X, Y) {
        function runSimulation(critScan) {
            const sMod = sEnabled ? S : 0;
            const R = Math.max(0, W);

            // Single die face probabilities
            const faceProbs = new Array(11).fill(0);
            if (!critScan) {
                for (let f = 1; f <= 10; f++) faceProbs[f] = 0.1;
            } else {
                // Sensor Officer Crit: re-roll any 1s once
                faceProbs[1] = 0.01;
                for (let f = 2; f <= 10; f++) faceProbs[f] = 0.11;
            }

            const woBonus = (woEnabled && WO > 0) ? WO : 0;
            const soBonus = (soEnabled && SO > 0) ? SO : 0;

            const bHigh = Math.max(woBonus, soBonus);
            const bLow = Math.min(woBonus, soBonus);
            const bothActive = (woBonus > 0 && soBonus > 0);
            const singleActive = (woBonus > 0) !== (soBonus > 0);

            // Partition single-die faces into 4 mutually exclusive boost categories:
            // Cat 0: Natural Success (f + sMod >= Y)
            // Cat 1: Boostable by either officer
            // Cat 2: Boostable only by officer with higher bonus
            // Cat 3: Cannot be boosted to a success
            const catProbs = [0.0, 0.0, 0.0, 0.0];
            for (let f = 1; f <= 10; f++) {
                const p = faceProbs[f];
                if (f + sMod >= Y) {
                    catProbs[0] += p;
                } else if (bothActive) {
                    if (f + sMod + bLow >= Y) {
                        catProbs[1] += p;
                    } else if (f + sMod + bHigh >= Y) {
                        catProbs[2] += p;
                    } else {
                        catProbs[3] += p;
                    }
                } else if (singleActive) {
                    if (f + sMod + bHigh >= Y) {
                        catProbs[2] += p;
                    } else {
                        catProbs[3] += p;
                    }
                } else {
                    catProbs[3] += p;
                }
            }

            const pRerollSuccess = catProbs[0];
            const maxK = Math.max(R, N);
            const rerollDP = computeRerollDP(maxK, R, pRerollSuccess);

            // Probability mass function of total successful d10s
            const finalPmf = {};

            for (let c0 = 0; c0 <= N; c0++) {
                const p0 = (catProbs[0] > 0 || c0 === 0) ? Math.pow(catProbs[0], c0) : 0;
                if (p0 === 0 && c0 > 0) continue;
                const comb0 = getCombination(N, c0);

                for (let c1 = 0; c1 <= N - c0; c1++) {
                    const p1 = (catProbs[1] > 0 || c1 === 0) ? Math.pow(catProbs[1], c1) : 0;
                    if (p1 === 0 && c1 > 0) continue;
                    const comb1 = getCombination(N - c0, c1);

                    for (let c2 = 0; c2 <= N - c0 - c1; c2++) {
                        const c3 = N - c0 - c1 - c2;
                        const p2 = (catProbs[2] > 0 || c2 === 0) ? Math.pow(catProbs[2], c2) : 0;
                        if (p2 === 0 && c2 > 0) continue;
                        const p3 = (catProbs[3] > 0 || c3 === 0) ? Math.pow(catProbs[3], c3) : 0;
                        if (p3 === 0 && c3 > 0) continue;

                        const comb2 = getCombination(N - c0 - c1, c2);
                        const pPart = comb0 * p0 * comb1 * p1 * comb2 * p2 * p3;
                        if (pPart === 0) continue;

                        // Optimal officer boost assignment
                        let boostCnt = 0;
                        if (bothActive) {
                            boostCnt = (c2 >= 1) ? (1 + Math.min(1, c1)) : Math.min(2, c1);
                        } else if (singleActive) {
                            boostCnt = Math.min(1, c2);
                        }

                        const kNat = c0;
                        const kCapped = Math.min(kNat, maxK);

                        // Convolve with re-roll extra successes
                        for (let e = 0; e <= R; e++) {
                            const pExtra = rerollDP[kCapped][R][e];
                            if (pExtra === 0) continue;
                            const totalSucc = kNat + boostCnt + e;
                            finalPmf[totalSucc] = (finalPmf[totalSucc] || 0.0) + pPart * pExtra;
                        }
                    }
                }
            }

            let pSucc = 0.0;
            let avgSucc = 0.0;
            let pCritHit = 0.0;

            for (const [sStr, p] of Object.entries(finalPmf)) {
                const sCount = parseInt(sStr, 10);
                if (!critScan) {
                    if (sCount >= X) pSucc += p;
                    avgSucc += sCount * p;
                } else {
                    // Under SO Crit Scan, successful d10s count twice
                    const effectiveCount = sCount * 2;
                    if (effectiveCount >= X) pSucc += p;
                    // Critical hit happens when exceeding 2x the BD-DC (>= 2X + 1)
                    if (effectiveCount >= 2 * X + 1) pCritHit += p;
                    avgSucc += sCount * p;
                }
            }

            return { pSucc, avgSucc, pCritHit };
        }

        const standard = runSimulation(false);
        const critical = runSimulation(true);

        return {
            successProb: standard.pSucc,
            avgSuccesses: standard.avgSucc,
            soCritSuccessProb: critical.pSucc,
            soCritWoCritHitProb: critical.pCritHit
        };
    }

    function parsePoolSizes(str) {
        const tokens = str.split(/[\s,;]+/).map(s => s.trim()).filter(Boolean);
        const pools = [];
        for (const t of tokens) {
            const val = parseInt(t, 10);
            if (!isNaN(val) && val >= 1 && val <= 50) {
                pools.push(val);
            }
        }
        return pools.length > 0 ? Array.from(new Set(pools)) : [4];
    }

    function parseBDDCs(str) {
        const list = [];
        const regex = /(\d+)\s*[,/]\s*(\d+)/g;
        let m;
        while ((m = regex.exec(str)) !== null) {
            const x = parseInt(m[1], 10);
            const y = parseInt(m[2], 10);
            if (x >= 1 && y >= 1) {
                list.push({ x, y, label: `${x},${y}` });
            }
        }
        if (list.length === 0) {
            const nums = str.split(/\s+/).map(s => parseInt(s, 10)).filter(n => !isNaN(n));
            for (let i = 0; i + 1 < nums.length; i += 2) {
                list.push({ x: nums[i], y: nums[i + 1], label: `${nums[i]},${nums[i + 1]}` });
            }
        }
        return list.length > 0 ? list : [{ x: 4, y: 4, label: '4,4' }];
    }

    // =========================================================================
    // 2. Presets & Theme-Aware CSS Styles
    // =========================================================================

    const PRESETS = {
        'default': { pool: '4', bddc: '4,4', weaponsMod: 1, sensorMod: 0, sensorEnabled: true, woBonus: 2, woEnabled: true, soBonus: 2, soEnabled: true },
        'standard6': { pool: '6', bddc: '4,4', weaponsMod: 2, sensorMod: 1, sensorEnabled: true, woBonus: 2, woEnabled: true, soBonus: 2, soEnabled: true },
        'destroyer': { pool: '6', bddc: '5,6', weaponsMod: 2, sensorMod: 1, sensorEnabled: true, woBonus: 3, woEnabled: true, soBonus: 2, soEnabled: true },
        'cruiser': { pool: '8', bddc: '6,6', weaponsMod: 3, sensorMod: 2, sensorEnabled: true, woBonus: 3, woEnabled: true, soBonus: 3, soEnabled: true },
        'dreadnought': { pool: '10', bddc: '7,8', weaponsMod: 4, sensorMod: 3, sensorEnabled: true, woBonus: 4, woEnabled: true, soBonus: 4, soEnabled: true }
    };

    const CSS_STYLES = `
        .sdc-container {
            --sdc-card-bg: #f8f9fa;
            --sdc-card-border: #c8ccd1;
            --sdc-text: #202122;
            --sdc-subtext: #54595d;
            --sdc-input-bg: #ffffff;
            --sdc-input-border: #a2a9b1;
            --sdc-input-text: #202122;
            --sdc-table-hdr-bg: #eaecf0;
            --sdc-table-hdr-text: #202122;
            --sdc-table-border: #a2a9b1;
            --sdc-table-bg: #ffffff;
            --sdc-table-alt-bg: #f8f9fa;
            --sdc-btn-calc: #3366cc;
            --sdc-btn-calc-hover: #2a4b8d;
            --sdc-btn-copy: #2ea44f;
            --sdc-btn-copy-hover: #22863a;
            --sdc-link: #3366cc;
            --sdc-error: #d33;
            --sdc-success: #2ea44f;
            --sdc-crit: #b35900;
            --sdc-highlight-bg: #eaf3ff;
            background: var(--sdc-card-bg);
            border: 1px solid var(--sdc-card-border);
            color: var(--sdc-text);
            border-radius: 6px;
            padding: 16px;
            margin: 15px 0;
            max-width: 1100px;
            width: 100%;
            box-sizing: border-box;
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
            transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease;
        }

        /* MediaWiki Vector 2022 Night mode & Dark Mode Gadget/Skin Classes */
        html.skin-theme-clientpref-night .sdc-container,
        html.dark-mode .sdc-container,
        body.skin-night .sdc-container,
        body.theme-dark .sdc-container,
        html.client-dark-mode .sdc-container {
            --sdc-card-bg: #202122;
            --sdc-card-border: #54595d;
            --sdc-text: #f8f9fa;
            --sdc-subtext: #a2a9b1;
            --sdc-input-bg: #27292d;
            --sdc-input-border: #72777d;
            --sdc-input-text: #f8f9fa;
            --sdc-table-hdr-bg: #2e3034;
            --sdc-table-hdr-text: #ffffff;
            --sdc-table-border: #54595d;
            --sdc-table-bg: #202122;
            --sdc-table-alt-bg: #27292d;
            --sdc-btn-calc: #447ff5;
            --sdc-btn-calc-hover: #3366cc;
            --sdc-btn-copy: #34b359;
            --sdc-btn-copy-hover: #2ea44f;
            --sdc-link: #6699ff;
            --sdc-error: #ff6b6b;
            --sdc-success: #34b359;
            --sdc-crit: #f59f00;
            --sdc-highlight-bg: #2b384e;
        }

        /* OS automatic Dark mode */
        @media (prefers-color-scheme: dark) {
            html.skin-theme-clientpref-os .sdc-container,
            html:not(.skin-theme-clientpref-day):not(.skin-theme-clientpref-night) .sdc-container {
                --sdc-card-bg: #202122;
                --sdc-card-border: #54595d;
                --sdc-text: #f8f9fa;
                --sdc-subtext: #a2a9b1;
                --sdc-input-bg: #27292d;
                --sdc-input-border: #72777d;
                --sdc-input-text: #f8f9fa;
                --sdc-table-hdr-bg: #2e3034;
                --sdc-table-hdr-text: #ffffff;
                --sdc-table-border: #54595d;
                --sdc-table-bg: #202122;
                --sdc-table-alt-bg: #27292d;
                --sdc-btn-calc: #447ff5;
                --sdc-btn-calc-hover: #3366cc;
                --sdc-btn-copy: #34b359;
                --sdc-btn-copy-hover: #2ea44f;
                --sdc-link: #6699ff;
                --sdc-error: #ff6b6b;
                --sdc-success: #34b359;
                --sdc-crit: #f59f00;
                --sdc-highlight-bg: #2b384e;
            }
        }

        .sdc-input {
            background: var(--sdc-input-bg) !important;
            border: 1px solid var(--sdc-input-border) !important;
            color: var(--sdc-input-text) !important;
            border-radius: 4px;
            padding: 6px 8px;
            font-size: 0.9em;
            box-sizing: border-box;
        }
        .sdc-input:disabled {
            opacity: 0.55;
            cursor: not-allowed;
        }
        .sdc-btn-calc {
            background: var(--sdc-btn-calc);
            color: #ffffff;
            border: none;
            padding: 8px 16px;
            border-radius: 4px;
            cursor: pointer;
            font-weight: bold;
            font-size: 0.9em;
            transition: background 0.15s ease;
        }
        .sdc-btn-calc:hover {
            background: var(--sdc-btn-calc-hover);
        }
        .sdc-btn-copy {
            background: var(--sdc-btn-copy);
            color: #ffffff;
            border: none;
            padding: 8px 16px;
            border-radius: 4px;
            cursor: pointer;
            font-weight: bold;
            font-size: 0.9em;
            transition: background 0.15s ease;
        }
        .sdc-btn-copy:hover {
            background: var(--sdc-btn-copy-hover);
        }
        .sdc-table {
            width: 100%;
            border-collapse: collapse;
            text-align: center;
            font-size: 0.9em;
            margin-top: 12px;
        }
        .sdc-table th {
            background: var(--sdc-table-hdr-bg);
            color: var(--sdc-table-hdr-text);
            border: 1px solid var(--sdc-table-border);
            padding: 8px 10px;
            font-weight: 600;
        }
        .sdc-table td {
            background: var(--sdc-table-bg);
            color: var(--sdc-text);
            border: 1px solid var(--sdc-table-border);
            padding: 7px 10px;
        }
        .sdc-table tr:nth-child(even) td {
            background: var(--sdc-table-alt-bg);
        }
    `;

    function injectStyles() {
        if (!document.getElementById('sdc-calc-injected-styles')) {
            const style = document.createElement('style');
            style.id = 'sdc-calc-injected-styles';
            style.textContent = CSS_STYLES;
            document.head.appendChild(style);
        }
    }

    // =========================================================================
    // 3. UI Builder & Event Handling
    // =========================================================================

    function initSpaceshipDamageCalcWidget() {
        const container = document.getElementById('spaceship-damage-calc-widget') ||
                          document.getElementById('bd-dc-calc-widget') ||
                          document.getElementById('spaceship-combat-calc-widget') ||
                          document.querySelector('.spaceship-damage-calc-widget');

        if (!container || container.dataset.loaded) return;
        container.dataset.loaded = 'true';

        injectStyles();

        container.innerHTML = `
            <div class="sdc-container">
                <div style="margin-bottom: 12px; border-bottom: 1px solid var(--sdc-card-border); padding-bottom: 8px;">
                    <h3 style="margin: 0 0 4px 0; font-size: 1.15em; color: var(--sdc-text);">🚀 Spaceship Combat: Battle Damage Calculator</h3>
                    <div style="font-size: 0.85em; color: var(--sdc-subtext);">
                        Calculates d10 Battle Pool probability distributions against Spaceship Battle Damage Difficulty Checks (BD-DC: Structure, Shields).
                    </div>
                </div>

                <!-- Input Controls Row 1 -->
                <div style="display: flex; flex-wrap: wrap; gap: 14px; align-items: flex-end; margin-bottom: 14px;">
                    <div>
                        <label style="display: block; font-weight: bold; margin-bottom: 4px; font-size: 0.88em;">Preset:</label>
                        <select id="sdc-select-preset" class="sdc-input" style="width: 215px;">
                            <option value="custom">Custom Configuration</option>
                            <option value="default" selected>Default (Pool 4 vs BD-DC 4,4)</option>
                            <option value="standard6">Standard Ship (Pool 6 vs 4,4)</option>
                            <option value="destroyer">Destroyer vs Shields (Pool 6 vs 5,6)</option>
                            <option value="cruiser">Cruiser vs Armor (Pool 8 vs 6,6)</option>
                            <option value="dreadnought">Dreadnought vs Fortress (Pool 10 vs 7,8)</option>
                        </select>
                    </div>
                    <div>
                        <label style="display: block; font-weight: bold; margin-bottom: 4px; font-size: 0.88em;">Battle Pool (d10s):</label>
                        <input id="sdc-input-pool" class="sdc-input" type="text" value="4" style="width: 120px;" placeholder="e.g. 4 or 4, 6">
                    </div>
                    <div>
                        <label style="display: block; font-weight: bold; margin-bottom: 4px; font-size: 0.88em;">BD-DC (Req, Threshold):</label>
                        <input id="sdc-input-bddc" class="sdc-input" type="text" value="4,4" style="width: 160px;" placeholder="e.g. 4,4 or 4,4; 5,6">
                    </div>
                    <div>
                        <label style="display: block; font-weight: bold; margin-bottom: 4px; font-size: 0.88em;">Ship Weapons Mod (Re-rolls):</label>
                        <input id="sdc-input-weapons-mod" class="sdc-input" type="number" value="1" min="0" max="20" style="width: 80px;">
                    </div>
                </div>

                <!-- Input Controls Row 2 -->
                <div style="display: flex; flex-wrap: wrap; gap: 16px; align-items: flex-end; margin-bottom: 14px;">
                    <div>
                        <label style="display: block; font-weight: bold; margin-bottom: 4px; font-size: 0.88em;">Ship Sensor Mod:</label>
                        <div style="display: flex; align-items: center; gap: 8px;">
                            <input id="sdc-input-sensor-mod" class="sdc-input" type="number" value="0" min="-10" max="20" style="width: 70px;">
                            <label style="font-weight: 500; font-size: 0.88em; cursor: pointer; display: flex; align-items: center; gap: 4px;">
                                <input id="sdc-check-sensor-mod" type="checkbox" checked> Add to d10s
                            </label>
                        </div>
                    </div>

                    <div>
                        <label style="display: block; font-weight: bold; margin-bottom: 4px; font-size: 0.88em;">Weapons Officer Mod Bonus:</label>
                        <div style="display: flex; align-items: center; gap: 8px;">
                            <input id="sdc-input-wo-bonus" class="sdc-input" type="number" value="2" min="0" max="20" style="width: 70px;">
                            <label style="font-weight: 500; font-size: 0.88em; cursor: pointer; display: flex; align-items: center; gap: 4px;">
                                <input id="sdc-check-wo-bonus" type="checkbox" checked> Include Boost
                            </label>
                        </div>
                    </div>

                    <div>
                        <label style="display: block; font-weight: bold; margin-bottom: 4px; font-size: 0.88em;">Sensor Officer Mod Bonus:</label>
                        <div style="display: flex; align-items: center; gap: 8px;">
                            <input id="sdc-input-so-bonus" class="sdc-input" type="number" value="2" min="0" max="20" style="width: 70px;">
                            <label style="font-weight: 500; font-size: 0.88em; cursor: pointer; display: flex; align-items: center; gap: 4px;">
                                <input id="sdc-check-so-bonus" type="checkbox" checked> Include Boost
                            </label>
                        </div>
                    </div>

                    <div style="display: flex; align-items: center; padding-bottom: 6px;">
                        <label style="font-weight: bold; font-size: 0.88em; cursor: pointer; display: flex; align-items: center; gap: 4px;">
                            <input id="sdc-check-row-count" type="checkbox" checked> Row #
                        </label>
                    </div>

                    <div style="display: flex; gap: 8px; align-items: center;">
                        <button id="sdc-btn-calc" class="sdc-btn-calc">Calculate</button>
                        <button id="sdc-btn-copy" class="sdc-btn-copy">Copy WikiText</button>
                        <span id="sdc-copy-status" style="color: var(--sdc-success); font-weight: bold; font-size: 0.9em; display: none;">Copied!</span>
                    </div>
                </div>

                <!-- HTML Live Preview Table -->
                <div id="sdc-preview-container" style="overflow-x: auto; margin-top: 10px;"></div>

                <!-- Collapsible WikiText Output -->
                <details style="margin-top: 14px;">
                    <summary style="cursor: pointer; color: var(--sdc-link); font-size: 0.9em; font-weight: 500;">View Generated WikiText</summary>
                    <textarea id="sdc-wikitext-output" class="sdc-input" readonly style="width: 100%; height: 95px; margin-top: 6px; font-family: monospace; font-size: 0.85em;"></textarea>
                </details>
            </div>
        `;

        function render() {
            try {
                const poolStr = document.getElementById('sdc-input-pool').value;
                const bddcStr = document.getElementById('sdc-input-bddc').value;
                const weaponsMod = parseInt(document.getElementById('sdc-input-weapons-mod').value, 10) || 0;
                const sensorMod = parseInt(document.getElementById('sdc-input-sensor-mod').value, 10) || 0;
                const sensorEnabled = document.getElementById('sdc-check-sensor-mod').checked;
                const woBonus = parseInt(document.getElementById('sdc-input-wo-bonus').value, 10) || 0;
                const woEnabled = document.getElementById('sdc-check-wo-bonus').checked;
                const soBonus = parseInt(document.getElementById('sdc-input-so-bonus').value, 10) || 0;
                const soEnabled = document.getElementById('sdc-check-so-bonus').checked;
                const showRowCount = document.getElementById('sdc-check-row-count').checked;

                const pools = parsePoolSizes(poolStr);
                const bddcs = parseBDDCs(bddcStr);

                const rows = [];
                for (const pool of pools) {
                    for (const bddc of bddcs) {
                        const stats = calculateDamageStats(
                            pool,
                            weaponsMod,
                            sensorMod,
                            sensorEnabled,
                            woBonus,
                            woEnabled,
                            soBonus,
                            soEnabled,
                            bddc.x,
                            bddc.y
                        );
                        rows.push({
                            pool,
                            bddc,
                            stats
                        });
                    }
                }

                // Generate WikiText
                const wikiHeaders = [];
                if (showRowCount) wikiHeaders.push('#');
                wikiHeaders.push('Battle Pool');
                wikiHeaders.push('BD-DC');
                wikiHeaders.push('Success %');
                wikiHeaders.push('Avg. Successful d10s');
                wikiHeaders.push('SO Crit: Normal Success %');
                wikiHeaders.push('SO Crit: WO Critical Hit %');

                const wikiHeader = '{| class="wikitable"\n! ' + wikiHeaders.join(' || ');
                const wikiRows = rows.map((r, i) => {
                    const cells = [];
                    if (showRowCount) cells.push(String(i + 1));
                    cells.push(`${r.pool} d10`);
                    cells.push(r.bddc.label);
                    cells.push(`${(r.stats.successProb * 100).toFixed(2)}%`);
                    cells.push(r.stats.avgSuccesses.toFixed(2));
                    cells.push(`${(r.stats.soCritSuccessProb * 100).toFixed(2)}%`);
                    cells.push(`${(r.stats.soCritWoCritHitProb * 100).toFixed(2)}%`);
                    return '|-\n| ' + cells.join(' || ');
                }).join('\n');
                const fullWikiText = `${wikiHeader}\n${wikiRows}\n|}`;
                document.getElementById('sdc-wikitext-output').value = fullWikiText;

                // Generate Live HTML Preview Table
                let html = '<table class="sdc-table">';
                html += '<thead><tr>';
                if (showRowCount) html += '<th style="width: 40px;">#</th>';
                html += '<th>Battle Pool</th>';
                html += '<th>BD-DC</th>';
                html += '<th title="Column 1: The % chance of a success against the BD-DC">Success %</th>';
                html += '<th title="Column 2: The average number of successfully rolled d10s">Avg. Successful d10s</th>';
                html += '<th title="Column 3: The % chance of a normal success when the Sensor Officer gets a Critical success on a Scan">SO Crit: Normal Success %</th>';
                html += '<th title="Column 4: The % chance of the Weapon\'s Officer getting a Critical hit against the BD-DC assuming the Sensor Officer gets a Critical success on a Scan">SO Crit: WO Critical Hit %</th>';
                html += '</tr></thead><tbody>';

                for (let i = 0; i < rows.length; i++) {
                    const r = rows[i];
                    html += '<tr>';
                    if (showRowCount) {
                        html += `<td style="color: var(--sdc-subtext); font-size: 0.85em;">${i + 1}</td>`;
                    }
                    html += `<td style="font-weight: bold;">${r.pool} d10</td>`;
                    html += `<td style="font-weight: bold; color: var(--sdc-link);">${r.bddc.label}</td>`;
                    html += `<td style="font-weight: 600;">${(r.stats.successProb * 100).toFixed(2)}%</td>`;
                    html += `<td>${r.stats.avgSuccesses.toFixed(2)}</td>`;
                    html += `<td style="font-weight: 600; color: var(--sdc-success);">${(r.stats.soCritSuccessProb * 100).toFixed(2)}%</td>`;
                    html += `<td style="font-weight: 600; color: var(--sdc-crit);">${(r.stats.soCritWoCritHitProb * 100).toFixed(2)}%</td>`;
                    html += '</tr>';
                }
                html += '</tbody></table>';
                document.getElementById('sdc-preview-container').innerHTML = html;

            } catch (err) {
                document.getElementById('sdc-preview-container').innerHTML =
                    `<div style="color: var(--sdc-error); font-weight: bold; padding: 10px;">Error: ${err.message}</div>`;
            }
        }

        // Preset Dropdown Handler
        document.getElementById('sdc-select-preset').addEventListener('change', function () {
            const val = this.value;
            if (val !== 'custom' && PRESETS[val]) {
                const p = PRESETS[val];
                document.getElementById('sdc-input-pool').value = p.pool;
                document.getElementById('sdc-input-bddc').value = p.bddc;
                document.getElementById('sdc-input-weapons-mod').value = p.weaponsMod;
                document.getElementById('sdc-input-sensor-mod').value = p.sensorMod;
                document.getElementById('sdc-check-sensor-mod').checked = p.sensorEnabled;
                document.getElementById('sdc-input-wo-bonus').value = p.woBonus;
                document.getElementById('sdc-check-wo-bonus').checked = p.woEnabled;
                document.getElementById('sdc-input-so-bonus').value = p.soBonus;
                document.getElementById('sdc-check-so-bonus').checked = p.soEnabled;
                render();
            }
        });

        // Detect manual changes to switch Preset to 'custom'
        const customInputs = [
            'sdc-input-pool', 'sdc-input-bddc', 'sdc-input-weapons-mod',
            'sdc-input-sensor-mod', 'sdc-input-wo-bonus', 'sdc-input-so-bonus'
        ];
        customInputs.forEach(id => {
            const el = document.getElementById(id);
            if (el) {
                el.addEventListener('input', function () {
                    document.getElementById('sdc-select-preset').value = 'custom';
                    render();
                });
            }
        });

        const customChecks = ['sdc-check-sensor-mod', 'sdc-check-wo-bonus', 'sdc-check-so-bonus', 'sdc-check-row-count'];
        customChecks.forEach(id => {
            const el = document.getElementById(id);
            if (el) {
                el.addEventListener('change', function () {
                    if (id !== 'sdc-check-row-count') {
                        document.getElementById('sdc-select-preset').value = 'custom';
                    }
                    render();
                });
            }
        });

        document.getElementById('sdc-btn-calc').addEventListener('click', render);

        document.getElementById('sdc-btn-copy').addEventListener('click', function () {
            const textarea = document.getElementById('sdc-wikitext-output');
            textarea.select();
            if (navigator.clipboard && navigator.clipboard.writeText) {
                navigator.clipboard.writeText(textarea.value);
            } else {
                document.execCommand('copy');
            }
            const status = document.getElementById('sdc-copy-status');
            status.style.display = 'inline';
            setTimeout(() => { status.style.display = 'none'; }, 2000);
        });

        // Trigger initial calculation
        render();
    }

    // Initialize when DOM / MediaWiki is ready
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initSpaceshipDamageCalcWidget);
    } else {
        initSpaceshipDamageCalcWidget();
    }
})();