Module:TalentCalculator: Difference between revisions
Jump to navigation
Jump to search
No edit summary |
No edit summary |
||
(4 intermediate revisions by the same user not shown) | |||
Line 1: | Line 1: | ||
local p = {} | local p = {} | ||
-- | -- Configuration for the talent trees | ||
local | local talentConfig = { | ||
trees = { | |||
name = "PvM Talents", | { | ||
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 | end | ||
end | end | ||
local | -- 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 = [[ | local html = [[ | ||
<div class="talent- | <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 class="talent- | |||
< | |||
<div class=" | |||
< | |||
</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 | ||
-- | -- Function to render a single talent tree | ||
function p. | function p.renderTalentTree(tree) | ||
local html = [[ | 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>]] | |||
</ | |||
<div | |||
]] | |||
end | end | ||
html = html .. [[ | html = html .. [[ | ||
</div> | </div> | ||
</div>]] | |||
return html | |||
<div class=" | end | ||
<div | -- 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> | ||
<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 | // 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. | |||
talent. | // Talent manipulation functions | ||
talent. | 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(); | ||
} | |||
return | // 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; | |||
} | } | ||
return | }); | ||
// 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 | |||
if ( | 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(','); | |||
for (let | // 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]; | |||
const | |||
// 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 | const talentState = state.talents[talent.id]; | ||
const | const talentElement = document.getElementById(talent.id); | ||
const | 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 | |||
} | |||
]] | |||
return | -- 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 | ||
-- | -- For debugging purposes | ||
function p. | function p.debug(frame) | ||
local | 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