Module:TalentCalculator

From August Wiki
Jump to navigation Jump to search

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