Module:TalentCalculator
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 p.renderTalentCalculator(frame)
end
return p