Module:TalentCalculator: Difference between revisions

From August Wiki
Jump to navigation Jump to search
Replaced content with "-- Talent Calculator for MediaWiki local p = {} -- [Keep all your existing code for talentTrees, generateTalents(), etc.] -- Main rendering function function p.renderTalentCalculator(frame) -- [Keep the existing function content] -- Return just the JSON data and a div with ID return { html = '<div id="talent-calculator-container">Loading talent calculator...</div>', talentsData = mw.text.jsonEncode(talents), treeConfig = mw...."
Tag: Replaced
No edit summary
Line 1: Line 1:
-- Talent Calculator for MediaWiki
-- Talent Calculator for MediaWiki
-- Configuration
local p = {}
local p = {}


-- [Keep all your existing code for talentTrees, generateTalents(), etc.]
-- Talent Tree Configuration
local talentTrees = {
    pvm = {
        name = "PvM Talents",
        tiers = 5,
        color = "#c41e3a", -- Red theme for combat
        talents_per_tier = 5,
        tier_point_requirements = {0, 5, 10, 15, 20} -- Points needed in previous tiers
    },
    skilling = {
        name = "Skilling Talents",
        tiers = 5,
        color = "#228b22", -- Green theme for skilling
        talents_per_tier = 5,
        tier_point_requirements = {0, 5, 10, 15, 20}
    },
    utility = {
        name = "Utility Talents",
        tiers = 5,
        color = "#4169e1", -- Blue theme for utility
        talents_per_tier = 5,
        tier_point_requirements = {0, 5, 10, 15, 20}
    }
}
 
-- Dummy talents data generator
local function generateTalents()
    local talents = {}
   
    for treeId, tree in pairs(talentTrees) do
        talents[treeId] = {}
        for tier = 1, tree.tiers do
            for talentNum = 1, tree.talents_per_tier do
                local talentId = treeId .. "_" .. tier .. "_" .. talentNum
                talents[treeId][talentId] = {
                    name = "Talent " .. tier .. "-" .. talentNum,
                    description = "This is a tier " .. tier .. " " .. treeId .. " talent.",
                    tier = tier,
                    maxPoints = math.min(tier * 2, 10), -- Max points scales with tier, capped at 10
                    cost = tier, -- Cost equal to tier number
                    icon = "talent_" .. treeId .. "_" .. tier .. "_" .. talentNum .. ".png"
                }
            end
        end
    end
   
    return talents
end
 
local talents = generateTalents()
 
-- Helper function to render a single talent tree
function renderTalentTree(treeId, tree)
    local html = [[
<div class="talent-tree" style="border-color: ]] .. tree.color .. [[">
    <div class="talent-tree-header" style="color: ]] .. tree.color .. [[">]] .. tree.name .. [[</div>
]]
 
    -- Render each tier
    for tier = 1, tree.tiers do
        html = html .. [[
    <div class="talent-tier]] .. (tier > 1 and " talent-tier-disabled" or "") .. [[" data-tree="]] .. treeId .. [[" data-tier="]] .. tier .. [[">
        <div class="talent-tier-header" style="background-color: ]] .. tree.color .. [[30">Tier ]] .. tier .. [[ (Requires ]] .. tree.tier_point_requirements[tier] .. [[ points)</div>
        <div class="talents-container">
]]
 
        -- Add talents for this tier
        for i = 1, tree.talents_per_tier do
            local talentId = treeId .. "_" .. tier .. "_" .. i
            local talent = talents[treeId][talentId]
           
            html = html .. [[
            <div class="talent"
                data-talent-id="]] .. talentId .. [["
                data-tree-id="]] .. treeId .. [["
                data-tier="]] .. tier .. [["
                data-max-points="]] .. talent.maxPoints .. [[">
                <div class="talent-icon">]] .. tier .. "-" .. i .. [[</div>
                <div class="talent-points"><span id="]] .. talentId .. [[-points">0</span>/]] .. talent.maxPoints .. [[</div>
            </div>
]]
        end
       
        html = html .. [[
        </div>
    </div>
]]
    end
   
    html = html .. [[
</div>
]]
 
    return html
end


-- Main rendering function
-- Main rendering function
function p.renderTalentCalculator(frame)
function p.renderTalentCalculator(frame)
     -- [Keep the existing function content]
     local talentState = {
        pvm = {},
        skilling = {},
        utility = {},
        pointsSpent = {
            pvm = 0,
            skilling = 0,
            utility = 0
        },
        totalPoints = 0
    }
      
      
     -- Return just the JSON data and a div with ID
     -- Base HTML and CSS
     return {
     local html = [[
        html = '<div id="talent-calculator-container">Loading talent calculator...</div>',
<style>
         talentsData = mw.text.jsonEncode(talents),
    .talent-calculator {
         treeConfig = mw.text.jsonEncode(talentTrees)
        display: flex;
        flex-direction: column;
        font-family: Arial, sans-serif;
         max-width: 900px;
         margin: 0 auto;
     }
     }
   
    .talent-trees {
        display: flex;
        flex-wrap: wrap;
        gap: 20px;
        justify-content: center;
    }
   
    .talent-tree {
        border: 1px solid #ccc;
        border-radius: 8px;
        padding: 15px;
        width: 280px;
    }
   
    .talent-tree-header {
        text-align: center;
        font-weight: bold;
        font-size: 1.2em;
        padding-bottom: 10px;
        border-bottom: 1px solid #eee;
        margin-bottom: 10px;
    }
   
    .talent-tier {
        margin-bottom: 15px;
        padding: 5px;
        border-radius: 5px;
        background-color: rgba(0,0,0,0.05);
    }
   
    .talent-tier-header {
        font-weight: bold;
        margin-bottom: 5px;
        padding: 3px;
        border-radius: 3px;
    }
   
    .talent-tier-disabled {
        opacity: 0.5;
        pointer-events: none;
    }
   
    .talents-container {
        display: flex;
        flex-wrap: wrap;
        gap: 5px;
    }
   
    .talent {
        display: flex;
        flex-direction: column;
        align-items: center;
        width: 50px;
        cursor: pointer;
        padding: 3px;
        border-radius: 5px;
        transition: background-color 0.2s;
    }
   
    .talent:hover {
        background-color: rgba(0,0,0,0.1);
    }
   
    .talent-icon {
        width: 40px;
        height: 40px;
        background-color: #eee;
        border-radius: 5px;
        display: flex;
        justify-content: center;
        align-items: center;
        font-size: 0.8em;
        margin-bottom: 3px;
    }
   
    .talent-points {
        font-size: 0.9em;
    }
   
    .controls {
        display: flex;
        justify-content: space-between;
        margin-top: 20px;
        padding: 15px;
        background-color: #f5f5f5;
        border-radius: 8px;
    }
   
    .points-display {
        font-weight: bold;
    }
   
    .import-export {
        display: flex;
        gap: 10px;
    }
   
    .button {
        padding: 5px 10px;
        border: none;
        border-radius: 4px;
        background-color: #4CAF50;
        color: white;
        cursor: pointer;
    }
   
    .button:hover {
        background-color: #45a049;
    }
   
    .talent-tooltip {
        position: absolute;
        display: none;
        background-color: #333;
        color: #fff;
        padding: 10px;
        border-radius: 5px;
        max-width: 200px;
        z-index: 100;
    }
</style>
<div class="talent-calculator">
    <div class="talent-trees">
]]
    -- Render each talent tree
    for treeId, tree in pairs(talentTrees) do
        html = html .. renderTalentTree(treeId, tree)
    end
   
    -- Add controls section
    html = html .. [[
    </div>
   
    <div class="controls">
        <div class="points-display">
            <span id="points-spent">Total Points Spent: 0/100</span>
            <div>
                <span id="points-pvm">PvM: 0 points</span> |
                <span id="points-skilling">Skilling: 0 points</span> |
                <span id="points-utility">Utility: 0 points</span>
            </div>
        </div>
        <div class="import-export">
            <button class="button" id="export-build">Export Build</button>
            <button class="button" id="import-build">Import Build</button>
            <button class="button" id="reset-build">Reset</button>
        </div>
    </div>
   
    <div class="talent-tooltip" id="talent-tooltip"></div>
   
    <div id="modal" style="display:none; position:fixed; top:0; left:0; width:100%; height:100%; background-color:rgba(0,0,0,0.5); z-index:1000;">
        <div style="position:relative; margin:10% auto; padding:20px; width:50%; background-color:white; border-radius:5px;">
            <span id="close-modal" style="position:absolute; top:10px; right:15px; cursor:pointer; font-size:20px;">&times;</span>
            <h3 id="modal-title">Import/Export Build</h3>
            <p>Copy this code to share your build:</p>
            <textarea id="build-code" style="width:100%; height:100px;"></textarea>
            <button id="modal-button" class="button" style="margin-top:10px;">Copy to Clipboard</button>
        </div>
    </div>
   
    <script>
        // Talent calculator functionality
        (function() {
            let talentState = {
                pvm: {},
                skilling: {},
                utility: {},
                pointsSpent: {
                    pvm: 0,
                    skilling: 0,
                    utility: 0
                },
                totalPoints: 0,
                maxPoints: 100
            };
           
            const treeRequirements = {
                pvm: [0, 5, 10, 15, 20],
                skilling: [0, 5, 10, 15, 20],
                utility: [0, 5, 10, 15, 20]
            };
           
            const talents = ]].. mw.text.jsonEncode(talents) .. [[;
           
            // Initialize talent state
            Object.keys(talents).forEach(treeId => {
                Object.keys(talents[treeId]).forEach(talentId => {
                    talentState[treeId][talentId] = 0;
                });
            });
           
            // Add event listeners to talents
            document.querySelectorAll('.talent').forEach(talent => {
                talent.addEventListener('click', handleTalentClick);
                talent.addEventListener('mouseover', showTooltip);
                talent.addEventListener('mouseout', hideTooltip);
            });
           
            // Handle talent click
            function handleTalentClick(event) {
                const talentElement = event.currentTarget;
                const talentId = talentElement.getAttribute('data-talent-id');
                const treeId = talentElement.getAttribute('data-tree-id');
                const tier = parseInt(talentElement.getAttribute('data-tier'));
                const maxPoints = parseInt(talentElement.getAttribute('data-max-points'));
                const rightClick = event.button === 2 || event.ctrlKey;
               
                // Check if tier is unlocked
                if (!isTierUnlocked(treeId, tier)) return;
               
                // Add or remove points
                if (rightClick) {
                    // Remove point
                    if (talentState[treeId][talentId] > 0) {
                        // Check if this would break tier requirements for talents in higher tiers
                        if (!canRemovePoint(treeId, tier)) {
                            alert("Cannot remove point - talents in higher tiers depend on this tier's total");
                            return;
                        }
                       
                        talentState[treeId][talentId]--;
                        talentState.pointsSpent[treeId]--;
                        talentState.totalPoints--;
                    }
                } else {
                    // Add point
                    if (talentState[treeId][talentId] < maxPoints && talentState.totalPoints < talentState.maxPoints) {
                        talentState[treeId][talentId]++;
                        talentState.pointsSpent[treeId]++;
                        talentState.totalPoints++;
                    }
                }
               
                // Update UI
                updateTalentPoint(talentId, talentState[treeId][talentId]);
                updatePointsDisplay();
                updateTierAvailability();
            }
           
            // Check if a tier is unlocked based on points spent in previous tiers
            function isTierUnlocked(treeId, tier) {
                if (tier === 1) return true;
                return talentState.pointsSpent[treeId] >= treeRequirements[treeId][tier-1];
            }
           
            // Check if removing a point will break tier requirements
            function canRemovePoint(treeId, tier) {
                // Check if any talents are allocated in higher tiers
                for (let t = tier + 1; t <= 5; t++) {
                    // Find if any talents in this tier have points
                    const talentsInTier = Object.keys(talents[treeId]).filter(id =>
                        talents[treeId][id].tier === t && talentState[treeId][id] > 0
                    );
                   
                    if (talentsInTier.length > 0 &&
                        talentState.pointsSpent[treeId] - 1 < treeRequirements[treeId][t-1]) {
                        return false;
                    }
                }
                return true;
            }
           
            // Update talent point display
            function updateTalentPoint(talentId, points) {
                const pointsElement = document.querySelector(`#${talentId}-points`);
                if (pointsElement) {
                    pointsElement.textContent = points;
                }
            }
           
            // Update the overall points display
            function updatePointsDisplay() {
                document.getElementById('points-spent').textContent =
                    `Total Points Spent: ${talentState.totalPoints}/${talentState.maxPoints}`;
                document.getElementById('points-pvm').textContent =
                    `PvM: ${talentState.pointsSpent.pvm} points`;
                document.getElementById('points-skilling').textContent =
                    `Skilling: ${talentState.pointsSpent.skilling} points`;
                document.getElementById('points-utility').textContent =
                    `Utility: ${talentState.pointsSpent.utility} points`;
            }
           
            // Update tier availability based on points spent
            function updateTierAvailability() {
                Object.keys(talents).forEach(treeId => {
                    for (let tier = 2; tier <= 5; tier++) {
                        const tierElement = document.querySelector(`.talent-tier[data-tree="${treeId}"][data-tier="${tier}"]`);
                        if (isTierUnlocked(treeId, tier)) {
                            tierElement.classList.remove('talent-tier-disabled');
                        } else {
                            tierElement.classList.add('talent-tier-disabled');
                        }
                    }
                });
            }
           
            // Show tooltip on hover
            function showTooltip(event) {
                const talentElement = event.currentTarget;
                const talentId = talentElement.getAttribute('data-talent-id');
                const treeId = talentElement.getAttribute('data-tree-id');
                const talent = talents[treeId][talentId];
               
                const tooltip = document.getElementById('talent-tooltip');
                tooltip.innerHTML = `
                    <strong>${talent.name}</strong>
                    <div>${talent.description}</div>
                    <div>Tier: ${talent.tier}</div>
                    <div>Points: ${talentState[treeId][talentId]}/${talent.maxPoints}</div>
                    <div>Cost: ${talent.cost} per point</div>
                `;
               
                tooltip.style.display = 'block';
                tooltip.style.left = (event.pageX + 10) + 'px';
                tooltip.style.top = (event.pageY + 10) + 'px';
            }
           
            // Hide tooltip
            function hideTooltip() {
                document.getElementById('talent-tooltip').style.display = 'none';
            }
           
            // Export build
            document.getElementById('export-build').addEventListener('click', function() {
                const modal = document.getElementById('modal');
                const buildCode = document.getElementById('build-code');
                const modalTitle = document.getElementById('modal-title');
                const modalButton = document.getElementById('modal-button');
               
                modalTitle.textContent = 'Export Build';
                modalButton.textContent = 'Copy to Clipboard';
               
                // Generate export code - simple base64 encoding of JSON state
                const exportData = {
                    pvm: talentState.pvm,
                    skilling: talentState.skilling,
                    utility: talentState.utility
                };
               
                const exportString = btoa(JSON.stringify(exportData));
                buildCode.value = exportString;
               
                modal.style.display = 'block';
                buildCode.select();
               
                modalButton.onclick = function() {
                    buildCode.select();
                    document.execCommand('copy');
                    alert('Build code copied to clipboard!');
                };
            });
           
            // Import build
            document.getElementById('import-build').addEventListener('click', function() {
                const modal = document.getElementById('modal');
                const buildCode = document.getElementById('build-code');
                const modalTitle = document.getElementById('modal-title');
                const modalButton = document.getElementById('modal-button');
               
                modalTitle.textContent = 'Import Build';
                modalButton.textContent = 'Import';
                buildCode.value = '';
               
                modal.style.display = 'block';
               
                modalButton.onclick = function() {
                    try {
                        const importData = JSON.parse(atob(buildCode.value));
                       
                        // Reset current state
                        resetBuild();
                       
                        // Apply imported state
                        Object.keys(importData).forEach(treeId => {
                            Object.keys(importData[treeId]).forEach(talentId => {
                                const points = importData[treeId][talentId];
                                talentState[treeId][talentId] = points;
                                talentState.pointsSpent[treeId] += points;
                                talentState.totalPoints += points;
                                updateTalentPoint(talentId, points);
                            });
                        });
                       
                        updatePointsDisplay();
                        updateTierAvailability();
                        modal.style.display = 'none';
                    } catch (e) {
                        alert('Invalid build code. Please try again with a correct code.');
                    }
                };
            });
           
            // Reset build
            document.getElementById('reset-build').addEventListener('click', resetBuild);
           
            function resetBuild() {
                Object.keys(talents).forEach(treeId => {
                    Object.keys(talents[treeId]).forEach(talentId => {
                        talentState[treeId][talentId] = 0;
                        updateTalentPoint(talentId, 0);
                    });
                    talentState.pointsSpent[treeId] = 0;
                });
               
                talentState.totalPoints = 0;
                updatePointsDisplay();
                updateTierAvailability();
            }
           
            // Close modal
            document.getElementById('close-modal').addEventListener('click', function() {
                document.getElementById('modal').style.display = 'none';
            });
           
            // Initialize tier availability
            updateTierAvailability();
           
            // Prevent context menu on talents to use right-click for point removal
            document.querySelectorAll('.talent').forEach(talent => {
                talent.addEventListener('contextmenu', function(e) {
                    e.preventDefault();
                    handleTalentClick({...e, button: 2, currentTarget: talent});
                    return false;
                });
            });
        })();
    </script>
</div>
]]
    return html
end
end


-- Modified main function
-- Main entry function
function p.main(frame)
function p.main(frame)
     local data = p.renderTalentCalculator(frame)
     return mw.html.raw(p.renderTalentCalculator(frame))
   
    -- Create a template call to handle the HTML rendering
    return frame:expandTemplate{
        title = 'TalentCalculatorTemplate',
        args = {
            html = data.html,
            talentsData = data.talentsData,
            treeConfig = data.treeConfig
        }
    }
end
end


return p
return p

Revision as of 13:00, 16 May 2025

Documentation for this module may be created at Module:TalentCalculator/doc

-- Talent Calculator for MediaWiki
-- Configuration
local p = {}

-- Talent Tree Configuration
local talentTrees = {
    pvm = {
        name = "PvM Talents",
        tiers = 5,
        color = "#c41e3a", -- Red theme for combat
        talents_per_tier = 5,
        tier_point_requirements = {0, 5, 10, 15, 20} -- Points needed in previous tiers
    },
    skilling = {
        name = "Skilling Talents",
        tiers = 5,
        color = "#228b22", -- Green theme for skilling
        talents_per_tier = 5,
        tier_point_requirements = {0, 5, 10, 15, 20}
    },
    utility = {
        name = "Utility Talents",
        tiers = 5, 
        color = "#4169e1", -- Blue theme for utility
        talents_per_tier = 5,
        tier_point_requirements = {0, 5, 10, 15, 20}
    }
}

-- Dummy talents data generator
local function generateTalents()
    local talents = {}
    
    for treeId, tree in pairs(talentTrees) do
        talents[treeId] = {}
        for tier = 1, tree.tiers do
            for talentNum = 1, tree.talents_per_tier do
                local talentId = treeId .. "_" .. tier .. "_" .. talentNum
                talents[treeId][talentId] = {
                    name = "Talent " .. tier .. "-" .. talentNum,
                    description = "This is a tier " .. tier .. " " .. treeId .. " talent.",
                    tier = tier,
                    maxPoints = math.min(tier * 2, 10), -- Max points scales with tier, capped at 10
                    cost = tier, -- Cost equal to tier number
                    icon = "talent_" .. treeId .. "_" .. tier .. "_" .. talentNum .. ".png"
                }
            end
        end
    end
    
    return talents
end

local talents = generateTalents()

-- Helper function to render a single talent tree
function renderTalentTree(treeId, tree)
    local html = [[
<div class="talent-tree" style="border-color: ]] .. tree.color .. [[">
    <div class="talent-tree-header" style="color: ]] .. tree.color .. [[">]] .. tree.name .. [[</div>
]]

    -- Render each tier
    for tier = 1, tree.tiers do
        html = html .. [[
    <div class="talent-tier]] .. (tier > 1 and " talent-tier-disabled" or "") .. [[" data-tree="]] .. treeId .. [[" data-tier="]] .. tier .. [[">
        <div class="talent-tier-header" style="background-color: ]] .. tree.color .. [[30">Tier ]] .. tier .. [[ (Requires ]] .. tree.tier_point_requirements[tier] .. [[ points)</div>
        <div class="talents-container">
]]

        -- Add talents for this tier
        for i = 1, tree.talents_per_tier do
            local talentId = treeId .. "_" .. tier .. "_" .. i
            local talent = talents[treeId][talentId]
            
            html = html .. [[
            <div class="talent" 
                data-talent-id="]] .. talentId .. [[" 
                data-tree-id="]] .. treeId .. [[" 
                data-tier="]] .. tier .. [[" 
                data-max-points="]] .. talent.maxPoints .. [[">
                <div class="talent-icon">]] .. tier .. "-" .. i .. [[</div>
                <div class="talent-points"><span id="]] .. talentId .. [[-points">0</span>/]] .. talent.maxPoints .. [[</div>
            </div>
]]
        end
        
        html = html .. [[
        </div>
    </div>
]]
    end
    
    html = html .. [[
</div>
]]

    return html
end

-- Main rendering function
function p.renderTalentCalculator(frame)
    local talentState = {
        pvm = {},
        skilling = {},
        utility = {},
        pointsSpent = {
            pvm = 0,
            skilling = 0,
            utility = 0
        },
        totalPoints = 0
    }
    
    -- Base HTML and CSS
    local html = [[
<style>
    .talent-calculator {
        display: flex;
        flex-direction: column;
        font-family: Arial, sans-serif;
        max-width: 900px;
        margin: 0 auto;
    }
    
    .talent-trees {
        display: flex;
        flex-wrap: wrap;
        gap: 20px;
        justify-content: center;
    }
    
    .talent-tree {
        border: 1px solid #ccc;
        border-radius: 8px;
        padding: 15px;
        width: 280px;
    }
    
    .talent-tree-header {
        text-align: center;
        font-weight: bold;
        font-size: 1.2em;
        padding-bottom: 10px;
        border-bottom: 1px solid #eee;
        margin-bottom: 10px;
    }
    
    .talent-tier {
        margin-bottom: 15px;
        padding: 5px;
        border-radius: 5px;
        background-color: rgba(0,0,0,0.05);
    }
    
    .talent-tier-header {
        font-weight: bold;
        margin-bottom: 5px;
        padding: 3px;
        border-radius: 3px;
    }
    
    .talent-tier-disabled {
        opacity: 0.5;
        pointer-events: none;
    }
    
    .talents-container {
        display: flex;
        flex-wrap: wrap;
        gap: 5px;
    }
    
    .talent {
        display: flex;
        flex-direction: column;
        align-items: center;
        width: 50px;
        cursor: pointer;
        padding: 3px;
        border-radius: 5px;
        transition: background-color 0.2s;
    }
    
    .talent:hover {
        background-color: rgba(0,0,0,0.1);
    }
    
    .talent-icon {
        width: 40px;
        height: 40px;
        background-color: #eee;
        border-radius: 5px;
        display: flex;
        justify-content: center;
        align-items: center;
        font-size: 0.8em;
        margin-bottom: 3px;
    }
    
    .talent-points {
        font-size: 0.9em;
    }
    
    .controls {
        display: flex;
        justify-content: space-between;
        margin-top: 20px;
        padding: 15px;
        background-color: #f5f5f5;
        border-radius: 8px;
    }
    
    .points-display {
        font-weight: bold;
    }
    
    .import-export {
        display: flex;
        gap: 10px;
    }
    
    .button {
        padding: 5px 10px;
        border: none;
        border-radius: 4px;
        background-color: #4CAF50;
        color: white;
        cursor: pointer;
    }
    
    .button:hover {
        background-color: #45a049;
    }
    
    .talent-tooltip {
        position: absolute;
        display: none;
        background-color: #333;
        color: #fff;
        padding: 10px;
        border-radius: 5px;
        max-width: 200px;
        z-index: 100;
    }
</style>

<div class="talent-calculator">
    <div class="talent-trees">
]]

    -- Render each talent tree
    for treeId, tree in pairs(talentTrees) do
        html = html .. renderTalentTree(treeId, tree)
    end
    
    -- Add controls section
    html = html .. [[
    </div>
    
    <div class="controls">
        <div class="points-display">
            <span id="points-spent">Total Points Spent: 0/100</span>
            <div>
                <span id="points-pvm">PvM: 0 points</span> | 
                <span id="points-skilling">Skilling: 0 points</span> | 
                <span id="points-utility">Utility: 0 points</span>
            </div>
        </div>
        <div class="import-export">
            <button class="button" id="export-build">Export Build</button>
            <button class="button" id="import-build">Import Build</button>
            <button class="button" id="reset-build">Reset</button>
        </div>
    </div>
    
    <div class="talent-tooltip" id="talent-tooltip"></div>
    
    <div id="modal" style="display:none; position:fixed; top:0; left:0; width:100%; height:100%; background-color:rgba(0,0,0,0.5); z-index:1000;">
        <div style="position:relative; margin:10% auto; padding:20px; width:50%; background-color:white; border-radius:5px;">
            <span id="close-modal" style="position:absolute; top:10px; right:15px; cursor:pointer; font-size:20px;">&times;</span>
            <h3 id="modal-title">Import/Export Build</h3>
            <p>Copy this code to share your build:</p>
            <textarea id="build-code" style="width:100%; height:100px;"></textarea>
            <button id="modal-button" class="button" style="margin-top:10px;">Copy to Clipboard</button>
        </div>
    </div>
    
    <script>
        // Talent calculator functionality
        (function() {
            let talentState = {
                pvm: {},
                skilling: {},
                utility: {},
                pointsSpent: {
                    pvm: 0,
                    skilling: 0,
                    utility: 0
                },
                totalPoints: 0,
                maxPoints: 100
            };
            
            const treeRequirements = {
                pvm: [0, 5, 10, 15, 20],
                skilling: [0, 5, 10, 15, 20],
                utility: [0, 5, 10, 15, 20]
            };
            
            const talents = ]].. mw.text.jsonEncode(talents) .. [[;
            
            // Initialize talent state
            Object.keys(talents).forEach(treeId => {
                Object.keys(talents[treeId]).forEach(talentId => {
                    talentState[treeId][talentId] = 0;
                });
            });
            
            // Add event listeners to talents
            document.querySelectorAll('.talent').forEach(talent => {
                talent.addEventListener('click', handleTalentClick);
                talent.addEventListener('mouseover', showTooltip);
                talent.addEventListener('mouseout', hideTooltip);
            });
            
            // Handle talent click
            function handleTalentClick(event) {
                const talentElement = event.currentTarget;
                const talentId = talentElement.getAttribute('data-talent-id');
                const treeId = talentElement.getAttribute('data-tree-id');
                const tier = parseInt(talentElement.getAttribute('data-tier'));
                const maxPoints = parseInt(talentElement.getAttribute('data-max-points'));
                const rightClick = event.button === 2 || event.ctrlKey;
                
                // Check if tier is unlocked
                if (!isTierUnlocked(treeId, tier)) return;
                
                // Add or remove points
                if (rightClick) {
                    // Remove point
                    if (talentState[treeId][talentId] > 0) {
                        // Check if this would break tier requirements for talents in higher tiers
                        if (!canRemovePoint(treeId, tier)) {
                            alert("Cannot remove point - talents in higher tiers depend on this tier's total");
                            return;
                        }
                        
                        talentState[treeId][talentId]--;
                        talentState.pointsSpent[treeId]--;
                        talentState.totalPoints--;
                    }
                } else {
                    // Add point
                    if (talentState[treeId][talentId] < maxPoints && talentState.totalPoints < talentState.maxPoints) {
                        talentState[treeId][talentId]++;
                        talentState.pointsSpent[treeId]++;
                        talentState.totalPoints++;
                    }
                }
                
                // Update UI
                updateTalentPoint(talentId, talentState[treeId][talentId]);
                updatePointsDisplay();
                updateTierAvailability();
            }
            
            // Check if a tier is unlocked based on points spent in previous tiers
            function isTierUnlocked(treeId, tier) {
                if (tier === 1) return true;
                return talentState.pointsSpent[treeId] >= treeRequirements[treeId][tier-1];
            }
            
            // Check if removing a point will break tier requirements
            function canRemovePoint(treeId, tier) {
                // Check if any talents are allocated in higher tiers
                for (let t = tier + 1; t <= 5; t++) {
                    // Find if any talents in this tier have points
                    const talentsInTier = Object.keys(talents[treeId]).filter(id => 
                        talents[treeId][id].tier === t && talentState[treeId][id] > 0
                    );
                    
                    if (talentsInTier.length > 0 && 
                        talentState.pointsSpent[treeId] - 1 < treeRequirements[treeId][t-1]) {
                        return false;
                    }
                }
                return true;
            }
            
            // Update talent point display
            function updateTalentPoint(talentId, points) {
                const pointsElement = document.querySelector(`#${talentId}-points`);
                if (pointsElement) {
                    pointsElement.textContent = points;
                }
            }
            
            // Update the overall points display
            function updatePointsDisplay() {
                document.getElementById('points-spent').textContent = 
                    `Total Points Spent: ${talentState.totalPoints}/${talentState.maxPoints}`;
                document.getElementById('points-pvm').textContent = 
                    `PvM: ${talentState.pointsSpent.pvm} points`;
                document.getElementById('points-skilling').textContent = 
                    `Skilling: ${talentState.pointsSpent.skilling} points`;
                document.getElementById('points-utility').textContent = 
                    `Utility: ${talentState.pointsSpent.utility} points`;
            }
            
            // Update tier availability based on points spent
            function updateTierAvailability() {
                Object.keys(talents).forEach(treeId => {
                    for (let tier = 2; tier <= 5; tier++) {
                        const tierElement = document.querySelector(`.talent-tier[data-tree="${treeId}"][data-tier="${tier}"]`);
                        if (isTierUnlocked(treeId, tier)) {
                            tierElement.classList.remove('talent-tier-disabled');
                        } else {
                            tierElement.classList.add('talent-tier-disabled');
                        }
                    }
                });
            }
            
            // Show tooltip on hover
            function showTooltip(event) {
                const talentElement = event.currentTarget;
                const talentId = talentElement.getAttribute('data-talent-id');
                const treeId = talentElement.getAttribute('data-tree-id');
                const talent = talents[treeId][talentId];
                
                const tooltip = document.getElementById('talent-tooltip');
                tooltip.innerHTML = `
                    <strong>${talent.name}</strong>
                    <div>${talent.description}</div>
                    <div>Tier: ${talent.tier}</div>
                    <div>Points: ${talentState[treeId][talentId]}/${talent.maxPoints}</div>
                    <div>Cost: ${talent.cost} per point</div>
                `;
                
                tooltip.style.display = 'block';
                tooltip.style.left = (event.pageX + 10) + 'px';
                tooltip.style.top = (event.pageY + 10) + 'px';
            }
            
            // Hide tooltip
            function hideTooltip() {
                document.getElementById('talent-tooltip').style.display = 'none';
            }
            
            // Export build
            document.getElementById('export-build').addEventListener('click', function() {
                const modal = document.getElementById('modal');
                const buildCode = document.getElementById('build-code');
                const modalTitle = document.getElementById('modal-title');
                const modalButton = document.getElementById('modal-button');
                
                modalTitle.textContent = 'Export Build';
                modalButton.textContent = 'Copy to Clipboard';
                
                // Generate export code - simple base64 encoding of JSON state
                const exportData = {
                    pvm: talentState.pvm,
                    skilling: talentState.skilling,
                    utility: talentState.utility
                };
                
                const exportString = btoa(JSON.stringify(exportData));
                buildCode.value = exportString;
                
                modal.style.display = 'block';
                buildCode.select();
                
                modalButton.onclick = function() {
                    buildCode.select();
                    document.execCommand('copy');
                    alert('Build code copied to clipboard!');
                };
            });
            
            // Import build
            document.getElementById('import-build').addEventListener('click', function() {
                const modal = document.getElementById('modal');
                const buildCode = document.getElementById('build-code');
                const modalTitle = document.getElementById('modal-title');
                const modalButton = document.getElementById('modal-button');
                
                modalTitle.textContent = 'Import Build';
                modalButton.textContent = 'Import';
                buildCode.value = '';
                
                modal.style.display = 'block';
                
                modalButton.onclick = function() {
                    try {
                        const importData = JSON.parse(atob(buildCode.value));
                        
                        // Reset current state
                        resetBuild();
                        
                        // Apply imported state
                        Object.keys(importData).forEach(treeId => {
                            Object.keys(importData[treeId]).forEach(talentId => {
                                const points = importData[treeId][talentId];
                                talentState[treeId][talentId] = points;
                                talentState.pointsSpent[treeId] += points;
                                talentState.totalPoints += points;
                                updateTalentPoint(talentId, points);
                            });
                        });
                        
                        updatePointsDisplay();
                        updateTierAvailability();
                        modal.style.display = 'none';
                    } catch (e) {
                        alert('Invalid build code. Please try again with a correct code.');
                    }
                };
            });
            
            // Reset build
            document.getElementById('reset-build').addEventListener('click', resetBuild);
            
            function resetBuild() {
                Object.keys(talents).forEach(treeId => {
                    Object.keys(talents[treeId]).forEach(talentId => {
                        talentState[treeId][talentId] = 0;
                        updateTalentPoint(talentId, 0);
                    });
                    talentState.pointsSpent[treeId] = 0;
                });
                
                talentState.totalPoints = 0;
                updatePointsDisplay();
                updateTierAvailability();
            }
            
            // Close modal
            document.getElementById('close-modal').addEventListener('click', function() {
                document.getElementById('modal').style.display = 'none';
            });
            
            // Initialize tier availability
            updateTierAvailability();
            
            // Prevent context menu on talents to use right-click for point removal
            document.querySelectorAll('.talent').forEach(talent => {
                talent.addEventListener('contextmenu', function(e) {
                    e.preventDefault();
                    handleTalentClick({...e, button: 2, currentTarget: talent});
                    return false;
                });
            });
        })();
    </script>
</div>
]]

    return html
end

-- Main entry function
function p.main(frame)
    return mw.html.raw(p.renderTalentCalculator(frame))
end

return p