Module:TalentCalculator: Difference between revisions
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 = {} | ||
-- [ | -- 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) | ||
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;">×</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 | ||
-- | -- Main entry function | ||
function p.main(frame) | function p.main(frame) | ||
return mw.html.raw(p.renderTalentCalculator(frame)) | |||
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;">×</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