Module:TalentCalculator
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