Module:TalentCalculator: Difference between revisions

From August Wiki
Jump to navigation Jump to search
No edit summary
No edit summary
 
(2 intermediate revisions by the same user not shown)
Line 1: Line 1:
-- Talent Calculator for MediaWiki
-- Configuration
local p = {}
local p = {}


-- Talent Tree Configuration
-- Configuration for the talent trees
local talentTrees = {
local talentConfig = {
     pvm = {
     trees = {
         name = "PvM Talents",
         {
        tiers = 5,
            id = "pvm",
        color = "#c41e3a", -- Red theme for combat
            name = "PvM Talents",
        talents_per_tier = 5,
            color = "#CF4040", -- Red theme
         tier_point_requirements = {0, 5, 10, 15, 20} -- Points needed in previous tiers
            tiers = 5
    },
        },
    skilling = {
         {
        name = "Skilling Talents",
            id = "skilling",
        tiers = 5,
            name = "Skilling Talents",
        color = "#228b22", -- Green theme for skilling
            color = "#40CF40", -- Green theme
        talents_per_tier = 5,
            tiers = 5
         tier_point_requirements = {0, 5, 10, 15, 20}
        },
        {
            id = "utility",
            name = "Utility Talents",
            color = "#4040CF", -- Blue theme
            tiers = 5
         }
     },
     },
     utility = {
     -- Dummy talents for each tier and tree
        name = "Utility Talents",
    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
-- Initialize dummy talents
local function generateTalents()
for _, tree in ipairs(talentConfig.trees) do
    local talents = {}
    for tier = 1, 5 do
   
        for talent = 1, 5 do
    for treeId, tree in pairs(talentTrees) do
            local talentId = tree.id .. "_t" .. tier .. "_" .. talent
        talents[treeId] = {}
            local displayName = tree.name:gsub(" Talents", "") .. " " .. tier .. "." .. talent
        for tier = 1, tree.tiers do
           
            for talentNum = 1, tree.talents_per_tier do
            table.insert(talentConfig.talents, {
                local talentId = treeId .. "_" .. tier .. "_" .. talentNum
                id = talentId,
                talents[treeId][talentId] = {
                name = displayName,
                    name = "Talent " .. tier .. "-" .. talentNum,
                tree = tree.id,
                    description = "This is a tier " .. tier .. " " .. treeId .. " talent.",
                tier = tier,
                    tier = tier,
                maxRank = math.min(10, tier * 2), -- Scale max rank with tier (capped at 10)
                    maxPoints = math.min(tier * 2, 10), -- Max points scales with tier, capped at 10
                cost = tier, -- Cost equal to tier number
                    cost = tier, -- Cost equal to tier number
                description = "This is a dummy " .. displayName .. " talent. Each point increases effectiveness by 5%."
                    icon = "talent_" .. treeId .. "_" .. tier .. "_" .. talentNum .. ".png"
            })
                }
            end
         end
         end
     end
     end
   
    return talents
end
end


local talents = generateTalents()
-- Main function to render the talent calculator
function p.renderCalculator(frame)
    local args = frame.args
    local parent = frame:getParent()
 
    if parent then
        args = parent.args
    end


-- Helper function to render a single talent tree
    -- Optional parameters
function renderTalentTree(treeId, tree)
    local maxPoints = tonumber(args.maxPoints or "51") -- Default max points: 51
    local importString = args.import or ""
   
    -- Generate JavaScript and CSS
    local js = p.generateJavaScript(maxPoints)
    local css = p.generateCSS()
   
    -- Start building the HTML
     local html = [[
     local html = [[
<div class="talent-tree" style="border-color: ]] .. tree.color .. [[">
<div class="talent-calculator-wrapper">
     <div class="talent-tree-header" style="color: ]] .. tree.color .. [[">]] .. tree.name .. [[</div>
     <style>]] .. css .. [[</style>
]]
      
 
     <div class="talent-calculator-header">
     -- Render each tier
         <h2>Talent Calculator</h2>
    for tier = 1, tree.tiers do
         <div class="points-counter">
        html = html .. [[
             <span id="spent-points">0</span>/<span id="max-points">]] .. maxPoints .. [[</span> Points Spent
     <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>
     </div>
     </div>
]]
   
    <div class="talent-calculator-trees">]]
   
    -- Generate HTML for each talent tree
    for _, tree in ipairs(talentConfig.trees) do
        html = html .. p.renderTalentTree(tree)
     end
     end
      
      
     html = html .. [[
     html = html .. [[
    </div>
   
    <div class="talent-calculator-controls">
        <div class="import-export">
            <input type="text" id="build-string" placeholder="Paste build string here..." value="]] .. importString .. [[">
            <button id="import-build" onclick="importBuild()">Import</button>
            <button id="export-build" onclick="exportBuild()">Export</button>
        </div>
        <div class="calculator-actions">
            <button id="reset-all" onclick="resetAllTalents()">Reset All</button>
        </div>
    </div>
   
    <script type="text/javascript">]] .. js .. [[</script>
</div>
</div>
]]
]]
Line 99: Line 104:
end
end


-- Main rendering function
-- Function to render a single talent tree
function p.renderTalentCalculator(frame)
function p.renderTalentTree(tree)
    local talentState = {
        pvm = {},
        skilling = {},
        utility = {},
        pointsSpent = {
            pvm = 0,
            skilling = 0,
            utility = 0
        },
        totalPoints = 0
    }
   
    -- Base HTML and CSS
     local html = [[
     local html = [[
<style>
    <div class="talent-tree" id="]] .. tree.id .. [[">
    .talent-calculator {
         <div class="tree-header" style="background-color: ]] .. tree.color .. [[;">
         display: flex;
            <h3>]] .. tree.name .. [[</h3>
        flex-direction: column;
            <div class="tree-points">
        font-family: Arial, sans-serif;
                <span id="]] .. tree.id .. [[-points">0</span> Points
         max-width: 900px;
                <button class="reset-tree-btn" onclick="resetTree(']] .. tree.id .. [[')">Reset</button>
         margin: 0 auto;
            </div>
    }
         </div>
          
        <div class="tree-talents">]]
      
      
     .talent-trees {
     -- Generate tiers
        display: flex;
     for tier = 1, tree.tiers do
        flex-wrap: wrap;
         html = html .. [[
        gap: 20px;
            <div class="talent-tier" data-tier="]] .. tier .. [[" id="]] .. tree.id .. [[-tier-]] .. tier .. [[">
        justify-content: center;
                <div class="tier-header">
    }
                    <span class="tier-number">Tier ]] .. tier .. [[</span>
   
                    <span class="tier-requirement">Required Points: ]] .. ((tier - 1) * 5) .. [[</span>
     .talent-tree {
                </div>
         border: 1px solid #ccc;
                <div class="tier-talents">]]
        border-radius: 8px;
       
        padding: 15px;
        -- Add the talents for this tier
        width: 280px;
        for _, talent in ipairs(talentConfig.talents) do
    }
            if talent.tree == tree.id and talent.tier == tier then
   
                html = html .. p.renderTalent(talent)
    .talent-tree-header {
            end
        text-align: center;
        end
        font-weight: bold;
       
        font-size: 1.2em;
        html = html .. [[
        padding-bottom: 10px;
                </div>
        border-bottom: 1px solid #eee;
            </div>]]
        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
     end
      
      
    -- Add controls section
     html = html .. [[
     html = html .. [[
     </div>
        </div>
     </div>]]
      
      
     <div class="controls">
     return html
         <div class="points-display">
end
            <span id="points-spent">Total Points Spent: 0/100</span>
 
             <div>
-- Function to render a single talent
                <span id="points-pvm">PvM: 0 points</span> |
function p.renderTalent(talent)
                 <span id="points-skilling">Skilling: 0 points</span> |
    local html = [[
                <span id="points-utility">Utility: 0 points</span>
         <div class="talent" id="]] .. talent.id .. [[" data-max-rank="]] .. talent.maxRank .. [[" data-cost="]] .. talent.cost .. [[">
             <div class="talent-icon">
                 <div class="talent-rank">0/]] .. talent.maxRank .. [[</div>
             </div>
             </div>
        </div>
            <div class="talent-info">
        <div class="import-export">
                <h4>]] .. talent.name .. [[</h4>
            <button class="button" id="export-build">Export Build</button>
                <div class="talent-cost">Cost: ]] .. talent.cost .. [[ point(s) each</div>
            <button class="button" id="import-build">Import Build</button>
                <div class="talent-description">]] .. talent.description .. [[</div>
            <button class="button" id="reset-build">Reset</button>
                <div class="talent-controls">
        </div>
                    <button class="talent-minus" onclick="decrementTalent(']] .. talent.id .. [[')">-</button>
    </div>
                    <button class="talent-plus" onclick="incrementTalent(']] .. talent.id .. [[')">+</button>
                </div>
            </div>
        </div>]]
      
      
     <div class="talent-tooltip" id="talent-tooltip"></div>
     return html
   
end
    <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;">
-- Function to generate CSS for the talent calculator
             <span id="close-modal" style="position:absolute; top:10px; right:15px; cursor:pointer; font-size:20px;">&times;</span>
function p.generateCSS()
             <h3 id="modal-title">Import/Export Build</h3>
    return [[
             <p>Copy this code to share your build:</p>
        /* General styling */
             <textarea id="build-code" style="width:100%; height:100px;"></textarea>
        .talent-calculator-wrapper {
             <button id="modal-button" class="button" style="margin-top:10px;">Copy to Clipboard</button>
            font-family: Arial, sans-serif;
        </div>
            max-width: 1200px;
    </div>
            margin: 0 auto;
   
            background-color: #f5f5f5;
    <script>
            border: 1px solid #ddd;
         // Talent calculator functionality
            border-radius: 5px;
         (function() {
            padding: 15px;
             let talentState = {
        }
                pvm: {},
       
                skilling: {},
        .talent-calculator-header {
                utility: {},
            display: flex;
                pointsSpent: {
            justify-content: space-between;
                    pvm: 0,
            align-items: center;
                    skilling: 0,
            margin-bottom: 15px;
                    utility: 0
            padding-bottom: 10px;
                },
            border-bottom: 2px solid #ccc;
                totalPoints: 0,
        }
                 maxPoints: 100
       
             };
        .talent-calculator-header h2 {
            margin: 0;
            font-size: 1.8em;
        }
       
        .points-counter {
            font-size: 1.3em;
            font-weight: bold;
        }
       
        /* Tree styling */
        .talent-calculator-trees {
            display: flex;
            flex-wrap: wrap;
            gap: 20px;
            justify-content: space-between;
            margin-bottom: 20px;
        }
       
        .talent-tree {
            flex: 1;
            min-width: 300px;
            background-color: #fff;
            border: 1px solid #ddd;
            border-radius: 3px;
            box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
        }
       
        .tree-header {
            color: white;
            padding: 10px;
            border-top-left-radius: 3px;
            border-top-right-radius: 3px;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
       
        .tree-header h3 {
            margin: 0;
            font-size: 1.3em;
        }
       
        .tree-points {
            font-weight: bold;
        }
       
        .reset-tree-btn {
            margin-left: 10px;
            padding: 2px 6px;
            background-color: rgba(255, 255, 255, 0.2);
            border: 1px solid rgba(255, 255, 255, 0.4);
            color: white;
            cursor: pointer;
            border-radius: 3px;
        }
       
        /* Tier styling */
        .talent-tier {
            padding: 10px;
            border-bottom: 1px solid #eee;
        }
       
        .talent-tier.locked {
            opacity: 0.6;
            pointer-events: none;
         }
       
        .tier-header {
            display: flex;
            justify-content: space-between;
            margin-bottom: 8px;
            font-size: 0.9em;
            color: #666;
        }
       
        /* Talent styling */
        .tier-talents {
            display: flex;
            flex-wrap: wrap;
            gap: 10px;
            justify-content: space-around;
        }
       
        .talent {
            display: flex;
            width: 100%;
            background-color: #f9f9f9;
            border: 1px solid #ddd;
            border-radius: 3px;
            padding: 8px;
            margin-bottom: 8px;
        }
       
        .talent-icon {
            width: 40px;
            height: 40px;
            background-color: #ddd;
            border-radius: 3px;
            position: relative;
             margin-right: 10px;
        }
       
        .talent-rank {
            position: absolute;
            bottom: -5px;
            right: -5px;
            background-color: #333;
            color: white;
            padding: 2px 4px;
            border-radius: 10px;
            font-size: 0.8em;
            font-weight: bold;
        }
       
        .talent-info {
            flex: 1;
        }
       
        .talent-info h4 {
            margin: 0 0 5px 0;
            font-size: 1.1em;
        }
       
        .talent-cost {
            font-size: 0.8em;
            color: #666;
            margin-bottom: 5px;
        }
       
        .talent-description {
            font-size: 0.9em;
             margin-bottom: 8px;
        }
       
        .talent-controls {
             display: flex;
             gap: 5px;
        }
       
        .talent-controls button {
            width: 30px;
            height: 25px;
            font-weight: bold;
            cursor: pointer;
            background-color: #eee;
            border: 1px solid #ccc;
             border-radius: 3px;
        }
       
        .talent-controls button:hover {
            background-color: #ddd;
        }
       
        .talent-controls button:disabled {
            opacity: 0.5;
            cursor: not-allowed;
        }
       
         /* Import/Export styling */
        .talent-calculator-controls {
            display: flex;
            flex-direction: column;
            gap: 10px;
            margin-top: 20px;
        }
          
        .import-export {
             display: flex;
            gap: 10px;
        }
       
        #build-string {
            flex: 1;
            padding: 8px;
            border: 1px solid #ccc;
            border-radius: 3px;
            font-family: monospace;
        }
       
        button {
            padding: 8px 15px;
            background-color: #4a4a4a;
            color: white;
            border: none;
            border-radius: 3px;
            cursor: pointer;
        }
       
        button:hover {
            background-color: #333;
        }
       
        .calculator-actions {
            text-align: right;
        }
       
        /* Responsiveness */
        @media (max-width: 1000px) {
            .talent-calculator-trees {
                 flex-direction: column;
             }
              
              
             const treeRequirements = {
             .talent-tree {
                 pvm: [0, 5, 10, 15, 20],
                 margin-bottom: 20px;
                skilling: [0, 5, 10, 15, 20],
            }
                utility: [0, 5, 10, 15, 20]
        }
             };
    ]]
              
end
             const talents = ]].. mw.text.jsonEncode(talents) .. [[;
 
-- Function to generate JavaScript for the talent calculator
function p.generateJavaScript(maxPoints)
    return [[
        // Global variables
        const MAX_POINTS = ]] .. maxPoints .. [[;
        const talentConfig = ]] .. p.serializeToJS(talentConfig) .. [[;
       
        // State management
        let state = {
            pointsSpent: 0,
            trees: {},
             talents: {}
        };
       
        // Initialize state
        function initializeState() {
             // Initialize tree state
             talentConfig.trees.forEach(tree => {
                state.trees[tree.id] = {
                    points: 0,
                    unlockedTiers: 1
                };
            });
              
              
             // Initialize talent state
             // Initialize talent state
             Object.keys(talents).forEach(treeId => {
             talentConfig.talents.forEach(talent => {
                 Object.keys(talents[treeId]).forEach(talentId => {
                 state.talents[talent.id] = {
                     talentState[treeId][talentId] = 0;
                     currentRank: 0,
                 });
                    maxRank: talent.maxRank,
                    cost: talent.cost,
                    tree: talent.tree,
                    tier: talent.tier
                 };
             });
             });
              
              
             // Add event listeners to talents
             updateUI();
             document.querySelectorAll('.talent').forEach(talent => {
        }
                 talent.addEventListener('click', handleTalentClick);
       
                 talent.addEventListener('mouseover', showTooltip);
        // Talent manipulation functions
                 talent.addEventListener('mouseout', hideTooltip);
        function incrementTalent(talentId) {
            const talent = state.talents[talentId];
             const tree = state.trees[talent.tree];
           
            if (talent.currentRank >= talent.maxRank || state.pointsSpent >= MAX_POINTS) {
                return;
            }
           
            talent.currentRank++;
            state.pointsSpent += talent.cost;
            tree.points += talent.cost;
           
            // Check if this unlocks the next tier
            const pointsNeededForNextTier = talent.tier * 5;
            if (tree.points >= pointsNeededForNextTier && tree.unlockedTiers <= talent.tier) {
                 tree.unlockedTiers = talent.tier + 1;
            }
           
            updateUI();
        }
       
        function decrementTalent(talentId) {
            const talent = state.talents[talentId];
           
            if (talent.currentRank <= 0) {
                 return;
            }
           
            // Check if this would break tier requirements for higher talents
            const tree = state.trees[talent.tree];
            const pointsInTreeAfterDecrement = tree.points - talent.cost;
           
            // Check if any higher-tier talents have points that would be invalidated
            let canDecrement = true;
            Object.values(state.talents).forEach(t => {
                 if (t.tree === talent.tree && t.tier > talent.tier && t.currentRank > 0) {
                    const requiredPoints = (t.tier - 1) * 5;
                    if (pointsInTreeAfterDecrement < requiredPoints) {
                        canDecrement = false;
                    }
                }
             });
             });
              
              
             // Handle talent click
             if (!canDecrement) {
            function handleTalentClick(event) {
                 alert("Cannot remove point - higher tier talents depend on this.");
                 const talentElement = event.currentTarget;
                 return;
                 const talentId = talentElement.getAttribute('data-talent-id');
            }
                const treeId = talentElement.getAttribute('data-tree-id');
           
                const tier = parseInt(talentElement.getAttribute('data-tier'));
            talent.currentRank--;
                const maxPoints = parseInt(talentElement.getAttribute('data-max-points'));
            state.pointsSpent -= talent.cost;
                const rightClick = event.button === 2 || event.ctrlKey;
            tree.points -= talent.cost;
               
           
                // Check if tier is unlocked
            // Update unlocked tiers
                if (!isTierUnlocked(treeId, tier)) return;
            if (tree.unlockedTiers > 1) {
               
                for (let tier = tree.unlockedTiers; tier > 1; tier--) {
                // Add or remove points
                    const pointsNeededForThisTier = (tier - 1) * 5;
                if (rightClick) {
                    if (tree.points < pointsNeededForThisTier) {
                    // Remove point
                         tree.unlockedTiers = tier - 1;
                    if (talentState[treeId][talentId] > 0) {
                     } else {
                        // Check if this would break tier requirements for talents in higher tiers
                         break;
                        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
             updateUI();
            function isTierUnlocked(treeId, tier) {
        }
                if (tier === 1) return true;
       
                 return talentState.pointsSpent[treeId] >= treeRequirements[treeId][tier-1];
        // Tree reset function
        function resetTree(treeId) {
            if (!confirm('Are you sure you want to reset all ' + treeId + ' talents?')) {
                 return;
             }
             }
              
              
             // Check if removing a point will break tier requirements
             const tree = state.trees[treeId];
             function canRemovePoint(treeId, tier) {
              
                // Check if any talents are allocated in higher tiers
            // Reset all talents in this tree
                for (let t = tier + 1; t <= 5; t++) {
            Object.entries(state.talents).forEach(([talentId, talent]) => {
                    // Find if any talents in this tier have points
                if (talent.tree === treeId && talent.currentRank > 0) {
                    const talentsInTier = Object.keys(talents[treeId]).filter(id =>  
                     state.pointsSpent -= (talent.currentRank * talent.cost);
                        talents[treeId][id].tier === t && talentState[treeId][id] > 0
                     talent.currentRank = 0;
                    );
                      
                    if (talentsInTier.length > 0 &&
                        talentState.pointsSpent[treeId] - 1 < treeRequirements[treeId][t-1]) {
                        return false;
                     }
                 }
                 }
                 return true;
            });
           
            // Reset tree state
            tree.points = 0;
            tree.unlockedTiers = 1;
           
            updateUI();
        }
       
        // Reset all talents
        function resetAllTalents() {
            if (!confirm('Are you sure you want to reset ALL talents?')) {
                 return;
             }
             }
              
              
             // Update talent point display
            state.pointsSpent = 0;
             function updateTalentPoint(talentId, points) {
           
                 const pointsElement = document.querySelector(`#${talentId}-points`);
             // Reset all trees
                 if (pointsElement) {
             Object.values(state.trees).forEach(tree => {
                     pointsElement.textContent = points;
                 tree.points = 0;
                tree.unlockedTiers = 1;
            });
           
            // Reset all talents
            Object.values(state.talents).forEach(talent => {
                talent.currentRank = 0;
            });
           
            updateUI();
        }
       
        // Import/Export functions
        function exportBuild() {
            let exportString = '';
           
            // Format: talentId:rank,talentId:rank,...
            Object.entries(state.talents).forEach(([talentId, talent]) => {
                 if (talent.currentRank > 0) {
                     exportString += talentId + ':' + talent.currentRank + ',';
                 }
                 }
            });
           
            // Remove trailing comma
            if (exportString.endsWith(',')) {
                exportString = exportString.substring(0, exportString.length - 1);
             }
             }
              
              
             // Update the overall points display
             document.getElementById('build-string').value = exportString;
            function updatePointsDisplay() {
        }
                document.getElementById('points-spent').textContent =  
       
                    `Total Points Spent: ${talentState.totalPoints}/${talentState.maxPoints}`;
        function importBuild() {
                document.getElementById('points-pvm').textContent =
            // Reset current build first
                    `PvM: ${talentState.pointsSpent.pvm} points`;
            resetAllTalents();
                document.getElementById('points-skilling').textContent =
           
                    `Skilling: ${talentState.pointsSpent.skilling} points`;
            const importString = document.getElementById('build-string').value.trim();
                document.getElementById('points-utility').textContent =
            if (!importString) {
                    `Utility: ${talentState.pointsSpent.utility} points`;
                return;
             }
             }
              
              
             // Update tier availability based on points spent
             try {
            function updateTierAvailability() {
                const talents = importString.split(',');
                 Object.keys(talents).forEach(treeId => {
               
                     for (let tier = 2; tier <= 5; tier++) {
                // Process each talent entry
                        const tierElement = document.querySelector(`.talent-tier[data-tree="${treeId}"][data-tier="${tier}"]`);
                 talents.forEach(talentEntry => {
                        if (isTierUnlocked(treeId, tier)) {
                     const [talentId, rankStr] = talentEntry.split(':');
                            tierElement.classList.remove('talent-tier-disabled');
                    const rank = parseInt(rankStr, 10);
                        } else {
                   
                            tierElement.classList.add('talent-tier-disabled');
                    if (state.talents[talentId] && !isNaN(rank)) {
                        // Apply points tier by tier to maintain requirements
                        for (let i = 0; i < rank; i++) {
                            const talent = state.talents[talentId];
                            const tree = state.trees[talent.tree];
                           
                            // Check if tier is unlocked
                            const requiredPoints = (talent.tier - 1) * 5;
                            if (tree.points >= requiredPoints) {
                                // Add the point
                                if (talent.currentRank < talent.maxRank && state.pointsSpent < MAX_POINTS) {
                                    talent.currentRank++;
                                    state.pointsSpent += talent.cost;
                                    tree.points += talent.cost;
                                   
                                    // Update unlocked tiers
                                    if (tree.unlockedTiers <= talent.tier) {
                                        const nextTierPoints = talent.tier * 5;
                                        if (tree.points >= nextTierPoints) {
                                            tree.unlockedTiers = talent.tier + 1;
                                        }
                                    }
                                }
                            }
                         }
                         }
                     }
                     }
                 });
                 });
            }
           
            // 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';
                 updateUI();
                 tooltip.style.left = (event.pageX + 10) + 'px';
            } catch (e) {
                 tooltip.style.top = (event.pageY + 10) + 'px';
                 console.error("Error importing build:", e);
                 alert("Invalid build string format. Please check and try again.");
             }
             }
        }
       
        // UI update function
        function updateUI() {
            // Update global points counter
            document.getElementById('spent-points').textContent = state.pointsSpent;
              
              
             // Hide tooltip
             // Update each tree
             function hideTooltip() {
             talentConfig.trees.forEach(tree => {
                document.getElementById('talent-tooltip').style.display = 'none';
                 const treeState = state.trees[tree.id];
            }
           
            // 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';
                 // Update tree points
                modalButton.textContent = 'Copy to Clipboard';
                document.getElementById(tree.id + '-points').textContent = treeState.points;
                  
                  
                 // Generate export code - simple base64 encoding of JSON state
                 // Update tier availability
                 const exportData = {
                 for (let tier = 1; tier <= 5; tier++) {
                     pvm: talentState.pvm,
                     const tierElement = document.getElementById(tree.id + '-tier-' + tier);
                    skilling: talentState.skilling,
                   
                    utility: talentState.utility
                    if (tier <= treeState.unlockedTiers) {
                };
                        tierElement.classList.remove('locked');
               
                     } else {
                const exportString = btoa(JSON.stringify(exportData));
                        tierElement.classList.add('locked');
                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
             // Update talent ranks and buttons
             document.getElementById('import-build').addEventListener('click', function() {
             talentConfig.talents.forEach(talent => {
                 const modal = document.getElementById('modal');
                 const talentState = state.talents[talent.id];
                 const buildCode = document.getElementById('build-code');
                 const talentElement = document.getElementById(talent.id);
                 const modalTitle = document.getElementById('modal-title');
                 const rankDisplay = talentElement.querySelector('.talent-rank');
                const modalButton = document.getElementById('modal-button');
                  
                  
                 modalTitle.textContent = 'Import Build';
                 // Update rank display
                 modalButton.textContent = 'Import';
                 rankDisplay.textContent = talentState.currentRank + '/' + talentState.maxRank;
                buildCode.value = '';
                  
                  
                 modal.style.display = 'block';
                 // Update buttons
                const minusBtn = talentElement.querySelector('.talent-minus');
                const plusBtn = talentElement.querySelector('.talent-plus');
                  
                  
                 modalButton.onclick = function() {
                 // Minus button disabled if rank is 0
                    try {
                minusBtn.disabled = talentState.currentRank === 0;
                        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;
                 // Plus button disabled if max rank or out of points
                updatePointsDisplay();
                plusBtn.disabled = (
                updateTierAvailability();
                    talentState.currentRank >= talentState.maxRank ||
            }
                    state.pointsSpent >= MAX_POINTS ||
           
                    state.trees[talentState.tree].unlockedTiers <= talentState.tier
            // Close modal
                 );
            document.getElementById('close-modal').addEventListener('click', function() {
                 document.getElementById('modal').style.display = 'none';
             });
             });
           
        }
            // Initialize tier availability
       
            updateTierAvailability();
        // Initialize on page load
           
        initializeState();
            // Prevent context menu on talents to use right-click for point removal
       
            document.querySelectorAll('.talent').forEach(talent => {
        // Apply import if provided
                talent.addEventListener('contextmenu', function(e) {
        if (document.getElementById('build-string').value) {
                    e.preventDefault();
            importBuild();
                    handleTalentClick({...e, button: 2, currentTarget: talent});
         }
                    return false;
     ]]
                });
end
            });
         })();
     </script>
</div>
]]


     return html
-- Helper function to serialize Lua tables to JavaScript objects
function p.serializeToJS(obj)
     local objType = type(obj)
    if objType == "table" then
        local isArray = #obj > 0
        local parts = {}
       
        if isArray then
            -- Array-like table
            for _, v in ipairs(obj) do
                table.insert(parts, p.serializeToJS(v))
            end
            return "[" .. table.concat(parts, ",") .. "]"
        else
            -- Object-like table
            for k, v in pairs(obj) do
                if type(k) ~= "number" or k > #obj then
                    table.insert(parts, '"' .. k .. '":' .. p.serializeToJS(v))
                end
            end
            return "{" .. table.concat(parts, ",") .. "}"
        end
    elseif objType == "string" then
        -- Escape special characters in strings
        local escaped = obj:gsub('\\', '\\\\'):gsub('"', '\\"'):gsub('\n', '\\n')
        return '"' .. escaped .. '"'
    elseif objType == "number" or objType == "boolean" then
        return tostring(obj)
    elseif objType == "nil" then
        return "null"
    else
        -- For other types, convert to string
        return '"' .. tostring(obj) .. '"'
    end
end
end


-- Main entry function
-- For debugging purposes
function p.main(frame)
function p.debug(frame)
     return mw.html.raw(p.renderTalentCalculator(frame))
     local args = frame:getParent().args
    local output = "Talent Calculator Debug:<br>"
   
    for k, v in pairs(args) do
        output = output .. k .. " = " .. v .. "<br>"
    end
   
    return output
end
end


return p
return p

Latest revision as of 13:25, 16 May 2025

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

local p = {}

-- Configuration for the talent trees
local talentConfig = {
    trees = {
        {
            id = "pvm",
            name = "PvM Talents",
            color = "#CF4040", -- Red theme
            tiers = 5
        },
        {
            id = "skilling",
            name = "Skilling Talents",
            color = "#40CF40", -- Green theme
            tiers = 5
        },
        {
            id = "utility",
            name = "Utility Talents",
            color = "#4040CF", -- Blue theme
            tiers = 5
        }
    },
    -- Dummy talents for each tier and tree
    talents = {}
}

-- Initialize dummy talents
for _, tree in ipairs(talentConfig.trees) do
    for tier = 1, 5 do
        for talent = 1, 5 do
            local talentId = tree.id .. "_t" .. tier .. "_" .. talent
            local displayName = tree.name:gsub(" Talents", "") .. " " .. tier .. "." .. talent
            
            table.insert(talentConfig.talents, {
                id = talentId,
                name = displayName,
                tree = tree.id,
                tier = tier,
                maxRank = math.min(10, tier * 2), -- Scale max rank with tier (capped at 10)
                cost = tier, -- Cost equal to tier number
                description = "This is a dummy " .. displayName .. " talent. Each point increases effectiveness by 5%."
            })
        end
    end
end

-- Main function to render the talent calculator
function p.renderCalculator(frame)
    local args = frame.args
    local parent = frame:getParent()

    if parent then
        args = parent.args
    end

    -- Optional parameters
    local maxPoints = tonumber(args.maxPoints or "51") -- Default max points: 51
    local importString = args.import or ""
    
    -- Generate JavaScript and CSS
    local js = p.generateJavaScript(maxPoints)
    local css = p.generateCSS()
    
    -- Start building the HTML
    local html = [[
<div class="talent-calculator-wrapper">
    <style>]] .. css .. [[</style>
    
    <div class="talent-calculator-header">
        <h2>Talent Calculator</h2>
        <div class="points-counter">
            <span id="spent-points">0</span>/<span id="max-points">]] .. maxPoints .. [[</span> Points Spent
        </div>
    </div>
    
    <div class="talent-calculator-trees">]]
    
    -- Generate HTML for each talent tree
    for _, tree in ipairs(talentConfig.trees) do
        html = html .. p.renderTalentTree(tree)
    end
    
    html = html .. [[
    </div>
    
    <div class="talent-calculator-controls">
        <div class="import-export">
            <input type="text" id="build-string" placeholder="Paste build string here..." value="]] .. importString .. [[">
            <button id="import-build" onclick="importBuild()">Import</button>
            <button id="export-build" onclick="exportBuild()">Export</button>
        </div>
        <div class="calculator-actions">
            <button id="reset-all" onclick="resetAllTalents()">Reset All</button>
        </div>
    </div>
    
    <script type="text/javascript">]] .. js .. [[</script>
</div>
]]

    return html
end

-- Function to render a single talent tree
function p.renderTalentTree(tree)
    local html = [[
    <div class="talent-tree" id="]] .. tree.id .. [[">
        <div class="tree-header" style="background-color: ]] .. tree.color .. [[;">
            <h3>]] .. tree.name .. [[</h3>
            <div class="tree-points">
                <span id="]] .. tree.id .. [[-points">0</span> Points
                <button class="reset-tree-btn" onclick="resetTree(']] .. tree.id .. [[')">Reset</button>
            </div>
        </div>
        
        <div class="tree-talents">]]
    
    -- Generate tiers
    for tier = 1, tree.tiers do
        html = html .. [[
            <div class="talent-tier" data-tier="]] .. tier .. [[" id="]] .. tree.id .. [[-tier-]] .. tier .. [[">
                <div class="tier-header">
                    <span class="tier-number">Tier ]] .. tier .. [[</span>
                    <span class="tier-requirement">Required Points: ]] .. ((tier - 1) * 5) .. [[</span>
                </div>
                <div class="tier-talents">]]
        
        -- Add the talents for this tier
        for _, talent in ipairs(talentConfig.talents) do
            if talent.tree == tree.id and talent.tier == tier then
                html = html .. p.renderTalent(talent)
            end
        end
        
        html = html .. [[
                </div>
            </div>]]
    end
    
    html = html .. [[
        </div>
    </div>]]
    
    return html
end

-- Function to render a single talent
function p.renderTalent(talent)
    local html = [[
        <div class="talent" id="]] .. talent.id .. [[" data-max-rank="]] .. talent.maxRank .. [[" data-cost="]] .. talent.cost .. [[">
            <div class="talent-icon">
                <div class="talent-rank">0/]] .. talent.maxRank .. [[</div>
            </div>
            <div class="talent-info">
                <h4>]] .. talent.name .. [[</h4>
                <div class="talent-cost">Cost: ]] .. talent.cost .. [[ point(s) each</div>
                <div class="talent-description">]] .. talent.description .. [[</div>
                <div class="talent-controls">
                    <button class="talent-minus" onclick="decrementTalent(']] .. talent.id .. [[')">-</button>
                    <button class="talent-plus" onclick="incrementTalent(']] .. talent.id .. [[')">+</button>
                </div>
            </div>
        </div>]]
    
    return html
end

-- Function to generate CSS for the talent calculator
function p.generateCSS()
    return [[
        /* General styling */
        .talent-calculator-wrapper {
            font-family: Arial, sans-serif;
            max-width: 1200px;
            margin: 0 auto;
            background-color: #f5f5f5;
            border: 1px solid #ddd;
            border-radius: 5px;
            padding: 15px;
        }
        
        .talent-calculator-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 15px;
            padding-bottom: 10px;
            border-bottom: 2px solid #ccc;
        }
        
        .talent-calculator-header h2 {
            margin: 0;
            font-size: 1.8em;
        }
        
        .points-counter {
            font-size: 1.3em;
            font-weight: bold;
        }
        
        /* Tree styling */
        .talent-calculator-trees {
            display: flex;
            flex-wrap: wrap;
            gap: 20px;
            justify-content: space-between;
            margin-bottom: 20px;
        }
        
        .talent-tree {
            flex: 1;
            min-width: 300px;
            background-color: #fff;
            border: 1px solid #ddd;
            border-radius: 3px;
            box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
        }
        
        .tree-header {
            color: white;
            padding: 10px;
            border-top-left-radius: 3px;
            border-top-right-radius: 3px;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        
        .tree-header h3 {
            margin: 0;
            font-size: 1.3em;
        }
        
        .tree-points {
            font-weight: bold;
        }
        
        .reset-tree-btn {
            margin-left: 10px;
            padding: 2px 6px;
            background-color: rgba(255, 255, 255, 0.2);
            border: 1px solid rgba(255, 255, 255, 0.4);
            color: white;
            cursor: pointer;
            border-radius: 3px;
        }
        
        /* Tier styling */
        .talent-tier {
            padding: 10px;
            border-bottom: 1px solid #eee;
        }
        
        .talent-tier.locked {
            opacity: 0.6;
            pointer-events: none;
        }
        
        .tier-header {
            display: flex;
            justify-content: space-between;
            margin-bottom: 8px;
            font-size: 0.9em;
            color: #666;
        }
        
        /* Talent styling */
        .tier-talents {
            display: flex;
            flex-wrap: wrap;
            gap: 10px;
            justify-content: space-around;
        }
        
        .talent {
            display: flex;
            width: 100%;
            background-color: #f9f9f9;
            border: 1px solid #ddd;
            border-radius: 3px;
            padding: 8px;
            margin-bottom: 8px;
        }
        
        .talent-icon {
            width: 40px;
            height: 40px;
            background-color: #ddd;
            border-radius: 3px;
            position: relative;
            margin-right: 10px;
        }
        
        .talent-rank {
            position: absolute;
            bottom: -5px;
            right: -5px;
            background-color: #333;
            color: white;
            padding: 2px 4px;
            border-radius: 10px;
            font-size: 0.8em;
            font-weight: bold;
        }
        
        .talent-info {
            flex: 1;
        }
        
        .talent-info h4 {
            margin: 0 0 5px 0;
            font-size: 1.1em;
        }
        
        .talent-cost {
            font-size: 0.8em;
            color: #666;
            margin-bottom: 5px;
        }
        
        .talent-description {
            font-size: 0.9em;
            margin-bottom: 8px;
        }
        
        .talent-controls {
            display: flex;
            gap: 5px;
        }
        
        .talent-controls button {
            width: 30px;
            height: 25px;
            font-weight: bold;
            cursor: pointer;
            background-color: #eee;
            border: 1px solid #ccc;
            border-radius: 3px;
        }
        
        .talent-controls button:hover {
            background-color: #ddd;
        }
        
        .talent-controls button:disabled {
            opacity: 0.5;
            cursor: not-allowed;
        }
        
        /* Import/Export styling */
        .talent-calculator-controls {
            display: flex;
            flex-direction: column;
            gap: 10px;
            margin-top: 20px;
        }
        
        .import-export {
            display: flex;
            gap: 10px;
        }
        
        #build-string {
            flex: 1;
            padding: 8px;
            border: 1px solid #ccc;
            border-radius: 3px;
            font-family: monospace;
        }
        
        button {
            padding: 8px 15px;
            background-color: #4a4a4a;
            color: white;
            border: none;
            border-radius: 3px;
            cursor: pointer;
        }
        
        button:hover {
            background-color: #333;
        }
        
        .calculator-actions {
            text-align: right;
        }
        
        /* Responsiveness */
        @media (max-width: 1000px) {
            .talent-calculator-trees {
                flex-direction: column;
            }
            
            .talent-tree {
                margin-bottom: 20px;
            }
        }
    ]]
end

-- Function to generate JavaScript for the talent calculator
function p.generateJavaScript(maxPoints)
    return [[
        // Global variables
        const MAX_POINTS = ]] .. maxPoints .. [[;
        const talentConfig = ]] .. p.serializeToJS(talentConfig) .. [[;
        
        // State management
        let state = {
            pointsSpent: 0,
            trees: {},
            talents: {}
        };
        
        // Initialize state
        function initializeState() {
            // Initialize tree state
            talentConfig.trees.forEach(tree => {
                state.trees[tree.id] = {
                    points: 0,
                    unlockedTiers: 1
                };
            });
            
            // Initialize talent state
            talentConfig.talents.forEach(talent => {
                state.talents[talent.id] = {
                    currentRank: 0,
                    maxRank: talent.maxRank,
                    cost: talent.cost,
                    tree: talent.tree,
                    tier: talent.tier
                };
            });
            
            updateUI();
        }
        
        // Talent manipulation functions
        function incrementTalent(talentId) {
            const talent = state.talents[talentId];
            const tree = state.trees[talent.tree];
            
            if (talent.currentRank >= talent.maxRank || state.pointsSpent >= MAX_POINTS) {
                return;
            }
            
            talent.currentRank++;
            state.pointsSpent += talent.cost;
            tree.points += talent.cost;
            
            // Check if this unlocks the next tier
            const pointsNeededForNextTier = talent.tier * 5;
            if (tree.points >= pointsNeededForNextTier && tree.unlockedTiers <= talent.tier) {
                tree.unlockedTiers = talent.tier + 1;
            }
            
            updateUI();
        }
        
        function decrementTalent(talentId) {
            const talent = state.talents[talentId];
            
            if (talent.currentRank <= 0) {
                return;
            }
            
            // Check if this would break tier requirements for higher talents
            const tree = state.trees[talent.tree];
            const pointsInTreeAfterDecrement = tree.points - talent.cost;
            
            // Check if any higher-tier talents have points that would be invalidated
            let canDecrement = true;
            Object.values(state.talents).forEach(t => {
                if (t.tree === talent.tree && t.tier > talent.tier && t.currentRank > 0) {
                    const requiredPoints = (t.tier - 1) * 5;
                    if (pointsInTreeAfterDecrement < requiredPoints) {
                        canDecrement = false;
                    }
                }
            });
            
            if (!canDecrement) {
                alert("Cannot remove point - higher tier talents depend on this.");
                return;
            }
            
            talent.currentRank--;
            state.pointsSpent -= talent.cost;
            tree.points -= talent.cost;
            
            // Update unlocked tiers
            if (tree.unlockedTiers > 1) {
                for (let tier = tree.unlockedTiers; tier > 1; tier--) {
                    const pointsNeededForThisTier = (tier - 1) * 5;
                    if (tree.points < pointsNeededForThisTier) {
                        tree.unlockedTiers = tier - 1;
                    } else {
                        break;
                    }
                }
            }
            
            updateUI();
        }
        
        // Tree reset function
        function resetTree(treeId) {
            if (!confirm('Are you sure you want to reset all ' + treeId + ' talents?')) {
                return;
            }
            
            const tree = state.trees[treeId];
            
            // Reset all talents in this tree
            Object.entries(state.talents).forEach(([talentId, talent]) => {
                if (talent.tree === treeId && talent.currentRank > 0) {
                    state.pointsSpent -= (talent.currentRank * talent.cost);
                    talent.currentRank = 0;
                }
            });
            
            // Reset tree state
            tree.points = 0;
            tree.unlockedTiers = 1;
            
            updateUI();
        }
        
        // Reset all talents
        function resetAllTalents() {
            if (!confirm('Are you sure you want to reset ALL talents?')) {
                return;
            }
            
            state.pointsSpent = 0;
            
            // Reset all trees
            Object.values(state.trees).forEach(tree => {
                tree.points = 0;
                tree.unlockedTiers = 1;
            });
            
            // Reset all talents
            Object.values(state.talents).forEach(talent => {
                talent.currentRank = 0;
            });
            
            updateUI();
        }
        
        // Import/Export functions
        function exportBuild() {
            let exportString = '';
            
            // Format: talentId:rank,talentId:rank,...
            Object.entries(state.talents).forEach(([talentId, talent]) => {
                if (talent.currentRank > 0) {
                    exportString += talentId + ':' + talent.currentRank + ',';
                }
            });
            
            // Remove trailing comma
            if (exportString.endsWith(',')) {
                exportString = exportString.substring(0, exportString.length - 1);
            }
            
            document.getElementById('build-string').value = exportString;
        }
        
        function importBuild() {
            // Reset current build first
            resetAllTalents();
            
            const importString = document.getElementById('build-string').value.trim();
            if (!importString) {
                return;
            }
            
            try {
                const talents = importString.split(',');
                
                // Process each talent entry
                talents.forEach(talentEntry => {
                    const [talentId, rankStr] = talentEntry.split(':');
                    const rank = parseInt(rankStr, 10);
                    
                    if (state.talents[talentId] && !isNaN(rank)) {
                        // Apply points tier by tier to maintain requirements
                        for (let i = 0; i < rank; i++) {
                            const talent = state.talents[talentId];
                            const tree = state.trees[talent.tree];
                            
                            // Check if tier is unlocked
                            const requiredPoints = (talent.tier - 1) * 5;
                            if (tree.points >= requiredPoints) {
                                // Add the point
                                if (talent.currentRank < talent.maxRank && state.pointsSpent < MAX_POINTS) {
                                    talent.currentRank++;
                                    state.pointsSpent += talent.cost;
                                    tree.points += talent.cost;
                                    
                                    // Update unlocked tiers
                                    if (tree.unlockedTiers <= talent.tier) {
                                        const nextTierPoints = talent.tier * 5;
                                        if (tree.points >= nextTierPoints) {
                                            tree.unlockedTiers = talent.tier + 1;
                                        }
                                    }
                                }
                            }
                        }
                    }
                });
                
                updateUI();
            } catch (e) {
                console.error("Error importing build:", e);
                alert("Invalid build string format. Please check and try again.");
            }
        }
        
        // UI update function
        function updateUI() {
            // Update global points counter
            document.getElementById('spent-points').textContent = state.pointsSpent;
            
            // Update each tree
            talentConfig.trees.forEach(tree => {
                const treeState = state.trees[tree.id];
                
                // Update tree points
                document.getElementById(tree.id + '-points').textContent = treeState.points;
                
                // Update tier availability
                for (let tier = 1; tier <= 5; tier++) {
                    const tierElement = document.getElementById(tree.id + '-tier-' + tier);
                    
                    if (tier <= treeState.unlockedTiers) {
                        tierElement.classList.remove('locked');
                    } else {
                        tierElement.classList.add('locked');
                    }
                }
            });
            
            // Update talent ranks and buttons
            talentConfig.talents.forEach(talent => {
                const talentState = state.talents[talent.id];
                const talentElement = document.getElementById(talent.id);
                const rankDisplay = talentElement.querySelector('.talent-rank');
                
                // Update rank display
                rankDisplay.textContent = talentState.currentRank + '/' + talentState.maxRank;
                
                // Update buttons
                const minusBtn = talentElement.querySelector('.talent-minus');
                const plusBtn = talentElement.querySelector('.talent-plus');
                
                // Minus button disabled if rank is 0
                minusBtn.disabled = talentState.currentRank === 0;
                
                // Plus button disabled if max rank or out of points
                plusBtn.disabled = (
                    talentState.currentRank >= talentState.maxRank || 
                    state.pointsSpent >= MAX_POINTS ||
                    state.trees[talentState.tree].unlockedTiers <= talentState.tier
                );
            });
        }
        
        // Initialize on page load
        initializeState();
        
        // Apply import if provided
        if (document.getElementById('build-string').value) {
            importBuild();
        }
    ]]
end

-- Helper function to serialize Lua tables to JavaScript objects
function p.serializeToJS(obj)
    local objType = type(obj)
    if objType == "table" then
        local isArray = #obj > 0
        local parts = {}
        
        if isArray then
            -- Array-like table
            for _, v in ipairs(obj) do
                table.insert(parts, p.serializeToJS(v))
            end
            return "[" .. table.concat(parts, ",") .. "]"
        else
            -- Object-like table
            for k, v in pairs(obj) do
                if type(k) ~= "number" or k > #obj then
                    table.insert(parts, '"' .. k .. '":' .. p.serializeToJS(v))
                end
            end
            return "{" .. table.concat(parts, ",") .. "}"
        end
    elseif objType == "string" then
        -- Escape special characters in strings
        local escaped = obj:gsub('\\', '\\\\'):gsub('"', '\\"'):gsub('\n', '\\n')
        return '"' .. escaped .. '"'
    elseif objType == "number" or objType == "boolean" then
        return tostring(obj)
    elseif objType == "nil" then
        return "null"
    else
        -- For other types, convert to string
        return '"' .. tostring(obj) .. '"'
    end
end

-- For debugging purposes
function p.debug(frame)
    local args = frame:getParent().args
    local output = "Talent Calculator Debug:<br>"
    
    for k, v in pairs(args) do
        output = output .. k .. " = " .. v .. "<br>"
    end
    
    return output
end

return p