Created
December 20, 2025 12:54
-
-
Save EncodeTheCode/da8f14bd9c591e3bbf3a335b1f7aecc3 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!doctype html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta name="viewport" content="width=device-width,initial-scale=1" /> | |
| <title>Rogue — Talent Build Mock (HTML5 + JS)</title> | |
| <style> | |
| :root{--bg:#0f1724;--panel:#0b1220;--accent:#0ea5e9;--muted:#94a3b8;--node:#111827} | |
| html,body{height:100%;margin:0;font-family:Inter,Segoe UI,Arial,sans-serif;background:linear-gradient(180deg,#071226 0%, #021017 100%);color:#e6eef6} | |
| #game-wrap{display:flex;height:100vh;gap:12px;padding:12px;box-sizing:border-box} | |
| #left{flex:1;position:relative;display:flex;flex-direction:column} | |
| canvas{background:linear-gradient(180deg, rgba(255,255,255,0.02), rgba(0,0,0,0.02));border-radius:8px;box-shadow:0 6px 18px rgba(2,6,23,0.6);} | |
| .hud{position:absolute;left:20px;top:20px;background:rgba(2,6,23,0.6);padding:8px 10px;border-radius:8px;border:1px solid rgba(255,255,255,0.03)} | |
| .hud small{display:block;color:var(--muted);} | |
| #right{width:380px;display:flex;flex-direction:column;gap:12px} | |
| .card{background:linear-gradient(180deg, rgba(255,255,255,0.02), rgba(0,0,0,0.02));padding:12px;border-radius:10px;border:1px solid rgba(255,255,255,0.03)} | |
| .title{font-weight:700;font-size:18px;color:#fff;margin-bottom:8px} | |
| .muted{color:var(--muted);font-size:13px} | |
| /* Modal / Talent Window */ | |
| .overlay{position:fixed;inset:0;background:rgba(0,0,0,0.6);display:none;align-items:center;justify-content:center;z-index:1000} | |
| .talent-window{width:1000px;max-width:calc(100% - 40px);height:680px;background:linear-gradient(180deg,#071226,#07142a);border-radius:12px;padding:18px;display:flex;gap:12px;box-shadow:0 10px 40px rgba(1,4,10,0.8);border:1px solid rgba(255,255,255,0.04)} | |
| .tree{flex:1;background:linear-gradient(180deg, rgba(255,255,255,0.02), rgba(0,0,0,0.02));border-radius:8px;padding:10px;display:flex;flex-direction:column;align-items:center} | |
| .tree h3{margin:0 0 6px 0} | |
| .points{font-weight:700;color:var(--accent)} | |
| .grid{display:grid;grid-template-rows:repeat(7,60px);grid-template-columns:repeat(3,1fr);gap:8px;margin-top:6px} | |
| .node{background:linear-gradient(180deg, rgba(255,255,255,0.01), rgba(0,0,0,0.02));border-radius:8px;border:1px solid rgba(255,255,255,0.03);display:flex;align-items:center;justify-content:center;position:relative;cursor:pointer;padding:6px} | |
| .node.locked{opacity:0.35} | |
| .node.threshold{background:linear-gradient(180deg, rgba(14,165,233,0.06), rgba(2,6,23,0.02));border:1px dashed rgba(14,165,233,0.18)} | |
| .node .label{font-size:12px;text-align:center} | |
| .node .count{position:absolute;right:6px;top:6px;background:rgba(0,0,0,0.4);padding:2px 6px;border-radius:8px;font-size:11px} | |
| .node .controls{position:absolute;left:6px;bottom:6px;display:flex;gap:6px} | |
| .btn{background:transparent;border:1px solid rgba(255,255,255,0.06);padding:2px 6px;border-radius:6px;color:var(--muted);font-weight:700;cursor:pointer} | |
| .btn:disabled{opacity:0.35;cursor:not-allowed} | |
| .footer{display:flex;justify-content:space-between;margin-top:10px} | |
| .close{background:var(--accent);border:none;padding:8px 10px;border-radius:8px;color:#022235;font-weight:700;cursor:pointer} | |
| /* Tooltip */ | |
| .tooltip{position:fixed;background:#071226;border:1px solid rgba(255,255,255,0.04);padding:10px;border-radius:8px;pointer-events:none;display:none;max-width:300px;z-index:2000} | |
| .tooltip h4{margin:0 0 4px 0} | |
| .tooltip p{margin:0;color:var(--muted);font-size:13px} | |
| /* Simple responsive */ | |
| @media (max-width:1100px){#right{display:none}} | |
| </style> | |
| </head> | |
| <body> | |
| <div id="game-wrap"> | |
| <div id="left"> | |
| <canvas id="game" width="960" height="640"></canvas> | |
| <div class="hud"> | |
| <div><strong>Rogue Mock — Playable</strong></div> | |
| <small>Move: WASD / Aim: mouse — Open talent window: <strong>K</strong></small> | |
| </div> | |
| </div> | |
| <div id="right"> | |
| <div class="card"> | |
| <div class="title">Controls</div> | |
| <div class="muted">Use WASD to move the blue orb. Move your mouse to aim the black line.</div> | |
| <hr style="margin:10px 0;opacity:0.07"> | |
| <div class="muted">Open/close talent window with <strong>K</strong>. Click + to place a point (subject to requirements). Remove only if it won't break prerequisites.</div> | |
| </div> | |
| <div class="card"> | |
| <div class="title">Talent Points</div> | |
| <div style="display:flex;gap:10px;align-items:center"><div>Available:</div><div class="points" id="availablePoints">51</div></div> | |
| <div style="margin-top:8px;color:var(--muted)">Baseline: 51 points (WoW Classic standard). Tier unlocks: 5, 10, 15, 20, 25, 30 points in a tree.</div> | |
| </div> | |
| <div class="card"> | |
| <div class="title">Quick Tips</div> | |
| <ul style="margin:6px 0 0 18px;color:var(--muted)"> | |
| <li>Some talents are single-point (unique). Others allow multiple points.</li> | |
| <li>Invest 5/10/15/20... points in a tree to unlock higher-tier talents in that tree.</li> | |
| <li>Removing points is blocked if it would leave dependent talents invalid.</li> | |
| </ul> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Talent overlay --> | |
| <div class="overlay" id="overlay" aria-hidden="true"> | |
| <div class="talent-window" role="dialog" aria-modal="true"> | |
| <div class="tree" id="tree0"> | |
| <h3>Stalker</h3> | |
| <div class="muted">A nimble specialist who relies on stealth and critical strikes.</div> | |
| <div style="margin-top:8px">Points: <span class="points" id="tree0Points">0</span></div> | |
| <div class="grid" data-tree="0" id="grid0"></div> | |
| </div> | |
| <div class="tree" id="tree1"> | |
| <h3>Duelist</h3> | |
| <div class="muted">A master of sustained combat and tactical advantage.</div> | |
| <div style="margin-top:8px">Points: <span class="points" id="tree1Points">0</span></div> | |
| <div class="grid" data-tree="1" id="grid1"></div> | |
| </div> | |
| <div class="tree" id="tree2"> | |
| <h3>Nightblade</h3> | |
| <div class="muted">Focuses on shadowy tricks and speed to outmaneuver foes.</div> | |
| <div style="margin-top:8px">Points: <span class="points" id="tree2Points">0</span></div> | |
| <div class="grid" data-tree="2" id="grid2"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="tooltip" id="tooltip" role="tooltip"></div> | |
| <script> | |
| // === Configuration === | |
| const TOTAL_POINTS = 51; // same as WoW Classic baseline | |
| const ROWS = 7, COLS = 3; | |
| // Exact tier unlock thresholds (points spent in that tree required before placing a point in that tier) | |
| const TIER_THRESHOLDS = [0,5,10,15,20,25,30]; // row index -> required points | |
| function tierRequirement(row){ return TIER_THRESHOLDS[row] || (row*5); } | |
| // We'll programmatically generate talents per tree. Each talent has: id,name,desc,max,spent,prereq(optional) | |
| const trees = []; | |
| const treeNames = ['Stalker','Duelist','Nightblade']; | |
| function makeTalentId(ti,row,col){return `t${ti}r${row}c${col}`} | |
| // Deterministic function for max ranks for each slot. | |
| function maxRanksFor(ti,row,col){ | |
| // Row 0 (tier 1): mix of multi and single | |
| if(row === 0) return (col === 1 ? 3 : 1); | |
| // Row 1: typically single-point unique talents | |
| if(row === 1) return 1; | |
| // Row 2: small utilities, allow 2 ranks center, 1 on sides | |
| if(row === 2) return (col === 1 ? 2 : 1); | |
| // Row 3: higher-tier uniques | |
| if(row === 3) return 1; | |
| // Row 4: some multi | |
| if(row === 4) return (col === 1 ? 2 : 1); | |
| // Row 5 and 6: usually single-point powerful talents | |
| return 1; | |
| } | |
| // Predefined flavor names to keep things readable and non-infringing | |
| const ADJ = ['Quick','Keen','Shadow','Deadly','Fleet','Razor','Lethal']; | |
| const NNS = ['Strike','Edge','Trick','Focus','Assault','Ambush','Feint']; | |
| for(let ti=0;ti<3;ti++){ | |
| const talents = []; | |
| for(let r=0;r<ROWS;r++){ | |
| for(let c=0;c<COLS;c++){ | |
| const id = makeTalentId(ti,r,c); | |
| // For central column and rows >=1, create visual 'tier threshold' nodes that auto-fill when total points meet requirement | |
| const isThreshold = (c===1 && r>=1); | |
| const name = isThreshold ? `Invest ${tierRequirement(r)} pts` : `${ADJ[(r+c+ti)%ADJ.length]} ${NNS[(r*3+c+ti)%NNS.length]}`; | |
| const desc = isThreshold ? `This is a tier marker. It becomes active when you've invested ${tierRequirement(r)} points into the ${treeNames[ti]} tree, unlocking this tier.` : `Tier ${r+1} talent. ${name} grants passive fantasy benefit for a rogue-like: increases some combat stat or grants a special passive. (Mock description.)`; | |
| const max = isThreshold ? 1 : maxRanksFor(ti,r,c); | |
| const talent = {id,name,desc,max,spent:0,row:r,col:c,prereq:null,isThreshold:isThreshold,threshold: isThreshold ? tierRequirement(r) : null}; | |
| // Prerequisites: starting from row 2+, require the talent directly above (if exists) to have at least 1 point for lateral gating | |
| if(r >= 2 && !isThreshold){ | |
| const aboveId = makeTalentId(ti, Math.max(0,r-1), c); | |
| talent.prereq = {id: aboveId, points: (r<=3?1: (r<=4?2:1))}; | |
| } | |
| talents.push(talent); | |
| } | |
| } | |
| trees.push({name:treeNames[ti],talents,spent:0}); | |
| } | |
| // === State === | |
| let availablePoints = TOTAL_POINTS; | |
| // DOM refs | |
| const overlay = document.getElementById('overlay'); | |
| const availableEl = document.getElementById('availablePoints'); | |
| const tooltip = document.getElementById('tooltip'); | |
| // Build UI grid | |
| function buildGrids(){ | |
| for(let ti=0;ti<3;ti++){ | |
| const grid = document.getElementById(`grid${ti}`); | |
| grid.innerHTML=''; | |
| const talents = trees[ti].talents; | |
| for(const t of talents){ | |
| const node = document.createElement('div'); | |
| node.className = 'node'; | |
| if(t.isThreshold) node.classList.add('threshold'); | |
| node.dataset.id = t.id; | |
| node.dataset.tree = ti; | |
| node.dataset.row = t.row; | |
| node.dataset.col = t.col; | |
| const label = document.createElement('div'); label.className='label'; label.innerText = t.name; | |
| const count = document.createElement('div'); count.className='count'; count.innerText = t.isThreshold ? `0/${t.max}` : `${t.spent}/${t.max}`; | |
| const controls = document.createElement('div'); controls.className='controls'; | |
| if(!t.isThreshold){ | |
| const minus = document.createElement('button'); minus.className='btn'; minus.innerText='–'; | |
| const plus = document.createElement('button'); plus.className='btn'; plus.innerText='+'; | |
| controls.appendChild(minus); controls.appendChild(plus); | |
| plus.addEventListener('click', (e)=>{e.stopPropagation(); trySpendPoint(t.id);}); | |
| minus.addEventListener('click',(e)=>{e.stopPropagation(); tryRemovePoint(t.id);}); | |
| } else { | |
| // threshold nodes are not clickable - show a small hint | |
| const info = document.createElement('div'); info.style.fontSize='11px'; info.style.color='var(--muted)'; info.innerText = `Requires ${t.threshold}`; | |
| controls.appendChild(info); | |
| } | |
| node.appendChild(label); node.appendChild(count); node.appendChild(controls); | |
| node.addEventListener('mouseenter',(e)=>{showTooltip(t,e.clientX,e.clientY);}); | |
| node.addEventListener('mousemove',(e)=>{moveTooltip(e.clientX,e.clientY);}); | |
| node.addEventListener('mouseleave',hideTooltip); | |
| grid.appendChild(node); | |
| } | |
| } | |
| refreshUI(); | |
| } | |
| function pointsSpentInTree(ti){ | |
| return trees[ti].talents.reduce((s,t)=> s + (t.isThreshold ? 0 : t.spent), 0); | |
| } | |
| function findTalentById(id){ | |
| for(let ti=0;ti<3;ti++){ | |
| const t = trees[ti].talents.find(x=>x.id===id); | |
| if(t) return {tal:t, tree:ti}; | |
| } | |
| return null; | |
| } | |
| function isTalentUnlocked(ti,t){ | |
| // threshold nodes are unlocked when the tree has enough total points | |
| if(t.isThreshold){ | |
| return pointsSpentInTree(ti) >= t.threshold; | |
| } | |
| // check tree tier requirement first | |
| const req = tierRequirement(t.row); | |
| const spent = pointsSpentInTree(ti); | |
| if(spent < req) return false; | |
| // check explicit prerequisite talent if present | |
| if(t.prereq){ | |
| const p = findTalentById(t.prereq.id); | |
| if(!p) return false; | |
| // If prereq is a threshold node, check threshold; otherwise check spent | |
| if(p.tal.isThreshold){ | |
| if(pointsSpentInTree(ti) < p.tal.threshold) return false; | |
| } else { | |
| if(p.tal.spent < t.prereq.points) return false; | |
| } | |
| } | |
| return true; | |
| } | |
| function trySpendPoint(id){ | |
| const info = findTalentById(id); if(!info) return; | |
| const t = info.tal; const ti = info.tree; | |
| // cannot manually spend into threshold nodes | |
| if(t.isThreshold) return; | |
| if(availablePoints <=0) return; // no points | |
| if(t.spent >= t.max) return; | |
| if(!isTalentUnlocked(ti,t)) return; // locked | |
| // ok spend | |
| t.spent += 1; availablePoints -=1; refreshUI(); | |
| } | |
| function canRemovePoint(id){ | |
| const info = findTalentById(id); if(!info) return false; | |
| const t = info.tal; const ti = info.tree; | |
| if(t.isThreshold) return false; // cannot remove threshold | |
| if(t.spent <=0) return false; | |
| // simulate removal and ensure no other talent would become invalid that currently has points | |
| t.spent -=1; | |
| const valid = trees[ti].talents.every(other=>{ | |
| if(other.isThreshold){ | |
| // thresholds depend on total points | |
| if(pointsSpentInTree(ti) < other.threshold && other.spent>0) return false; | |
| return true; | |
| } | |
| if(other.spent<=0) return true; | |
| // check tier req | |
| if(pointsSpentInTree(ti) < tierRequirement(other.row)) return false; | |
| if(other.prereq){ | |
| const p = findTalentById(other.prereq.id); | |
| if(!p) return false; | |
| if(p.tal.isThreshold){ | |
| if(pointsSpentInTree(ti) < p.tal.threshold) return false; | |
| } else { | |
| if(p.tal.spent < other.prereq.points) return false; | |
| } | |
| } | |
| return true; | |
| }); | |
| t.spent +=1; // revert | |
| return valid; | |
| } | |
| function tryRemovePoint(id){ | |
| if(!canRemovePoint(id)){ | |
| flashNode(id,'locked'); return; | |
| } | |
| const info = findTalentById(id); if(!info) return; | |
| const t = info.tal; t.spent -=1; availablePoints +=1; refreshUI(); | |
| } | |
| function flashNode(id,cls){ | |
| const el = document.querySelector(`[data-id='${id}']`); | |
| if(!el) return; el.classList.add(cls); | |
| setTimeout(()=>el.classList.remove(cls),350); | |
| } | |
| function refreshUI(){ | |
| // update nodes | |
| for(let ti=0;ti<3;ti++){ | |
| const grid = document.getElementById(`grid${ti}`); | |
| const nodes = grid.querySelectorAll('.node'); | |
| nodes.forEach(n=>{ | |
| const id = n.dataset.id; const info = findTalentById(id); | |
| const t = info.tal; | |
| // For threshold nodes, auto-set spent state based on tree points | |
| if(t.isThreshold){ | |
| const filled = pointsSpentInTree(ti) >= t.threshold; | |
| t.spent = filled ? 1 : 0; | |
| n.querySelector('.count').innerText = filled ? '✓' : `0/${t.max}`; | |
| // locked styling: if threshold not met, mark locked (dim), else active | |
| if(filled) n.classList.remove('locked'); else n.classList.add('locked'); | |
| // controls area shows requirement; already set on build | |
| } else { | |
| n.querySelector('.count').innerText = `${t.spent}/${t.max}`; | |
| // locked status | |
| if(isTalentUnlocked(ti,t)) n.classList.remove('locked'); else n.classList.add('locked'); | |
| // update +/- buttons | |
| const plus = n.querySelector('.controls .btn:nth-child(2)'); | |
| const minus = n.querySelector('.controls .btn:nth-child(1)'); | |
| if(plus) plus.disabled = !(availablePoints>0 && t.spent < t.max && isTalentUnlocked(ti,t)); | |
| if(minus) minus.disabled = !canRemovePoint(id); | |
| } | |
| }); | |
| // update tree points | |
| document.getElementById(`tree${ti}Points`).innerText = pointsSpentInTree(ti); | |
| } | |
| availableEl.innerText = availablePoints; | |
| } | |
| function showTooltip(t,eX,eY){ | |
| tooltip.style.display='block'; | |
| tooltip.innerHTML = `<h4>${t.name} <span style="float:right;font-weight:700;color:var(--accent)">${t.isThreshold ? (pointsSpentInTree(findTalentById(t.id).tree) >= t.threshold ? '✓' : `0/${t.max}`) : `${t.spent}/${t.max}`}</span></h4><p>${t.desc}</p>`; | |
| moveTooltip(eX,eY); | |
| } | |
| function moveTooltip(x,y){ | |
| const pad = 14; | |
| // keep tooltip inside viewport | |
| const winW = window.innerWidth, winH = window.innerHeight; | |
| const tw = tooltip.offsetWidth || 260, th = tooltip.offsetHeight || 60; | |
| let left = x + pad; | |
| let top = y + pad; | |
| if(left + tw > winW) left = x - tw - pad; | |
| if(top + th > winH) top = y - th - pad; | |
| tooltip.style.left = (left) + 'px'; | |
| tooltip.style.top = (top) + 'px'; | |
| } | |
| function hideTooltip(){tooltip.style.display='none'} | |
| // Toggle overlay with key | |
| let overlayOpen = false; | |
| function toggleOverlay(){overlayOpen = !overlayOpen; overlay.style.display = overlayOpen?'flex':'none'; overlay.setAttribute('aria-hidden', overlayOpen ? 'false' : 'true');} | |
| document.addEventListener('keydown',(e)=>{ | |
| if(e.key.toLowerCase()==='k'){ | |
| toggleOverlay(); | |
| } | |
| }); | |
| // Initialize | |
| buildGrids(); | |
| // === Game canvas: player movement and aiming === | |
| const canvas = document.getElementById('game'); | |
| const ctx = canvas.getContext('2d'); | |
| const playerState = {x:canvas.width/2,y:canvas.height/2, speed:180, aimX:canvas.width/2, aimY:canvas.height/2}; | |
| const keys = {}; | |
| window.addEventListener('keydown', e=>{keys[e.key.toLowerCase()] = true;}); | |
| window.addEventListener('keyup', e=>{keys[e.key.toLowerCase()] = false;}); | |
| canvas.addEventListener('mousemove',(e)=>{const rect = canvas.getBoundingClientRect(); playerState.aimX = e.clientX-rect.left; playerState.aimY = e.clientY-rect.top;}); | |
| let last = performance.now(); | |
| function loop(now){ | |
| const dt = Math.min(0.06,(now-last)/1000); last = now; | |
| update(dt); draw(); requestAnimationFrame(loop); | |
| } | |
| function update(dt){ | |
| let dx=0,dy=0; if(keys['w']||keys['arrowup']) dy-=1; if(keys['s']||keys['arrowdown']) dy+=1; if(keys['a']||keys['arrowleft']) dx-=1; if(keys['d']||keys['arrowright']) dx+=1; | |
| const mag = Math.hypot(dx,dy); if(mag>0){dx/=mag;dy/=mag} | |
| playerState.x += dx * playerState.speed * dt; playerState.y += dy * playerState.speed * dt; | |
| // clamp | |
| playerState.x = Math.max(16,Math.min(canvas.width-16,playerState.x)); playerState.y = Math.max(16,Math.min(canvas.height-16,playerState.y)); | |
| } | |
| function draw(){ | |
| ctx.clearRect(0,0,canvas.width,canvas.height); | |
| // subtle grid background | |
| ctx.save(); | |
| for(let i=0;i<canvas.width;i+=40){ctx.beginPath();ctx.moveTo(i,0);ctx.lineTo(i,canvas.height);ctx.strokeStyle='rgba(255,255,255,0.02)';ctx.lineWidth=1;ctx.stroke();} | |
| for(let j=0;j<canvas.height;j+=40){ctx.beginPath();ctx.moveTo(0,j);ctx.lineTo(canvas.width,j);ctx.strokeStyle='rgba(255,255,255,0.02)';ctx.lineWidth=1;ctx.stroke();} | |
| ctx.restore(); | |
| // draw aim line (black) | |
| ctx.beginPath(); ctx.moveTo(playerState.x,playerState.y); ctx.lineTo(playerState.aimX,playerState.aimY); ctx.strokeStyle='#000'; ctx.lineWidth=3; ctx.stroke(); | |
| // draw player (blue circle) | |
| ctx.beginPath(); ctx.arc(playerState.x,playerState.y,14,0,Math.PI*2); ctx.fillStyle='#2ea3ff'; ctx.fill(); | |
| // rim | |
| ctx.beginPath(); ctx.arc(playerState.x,playerState.y,14,0,Math.PI*2); ctx.strokeStyle='rgba(0,0,0,0.25)'; ctx.lineWidth=2; ctx.stroke(); | |
| // small crosshair at aim | |
| ctx.beginPath(); ctx.arc(playerState.aimX,playerState.aimY,4,0,Math.PI*2); ctx.fillStyle='rgba(255,255,255,0.9)'; ctx.fill(); | |
| } | |
| requestAnimationFrame(loop); | |
| // Friendly helpers for users: export build as JSON and import | |
| function exportBuild(){ | |
| const data = {trees:trees.map(t=>t.talents.map(x=>x.isThreshold? (pointsSpentInTree(findTalentById(x.id).tree) >= x.threshold?1:0) : x.spent)), available:availablePoints}; | |
| const blob = new Blob([JSON.stringify(data,null,2)],{type:'application/json'}); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); a.href = url; a.download = 'rogue_build.json'; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); | |
| } | |
| // Add quick key for export: Ctrl+E | |
| document.addEventListener('keydown',(e)=>{ if((e.ctrlKey||e.metaKey) && e.key.toLowerCase()==='e'){ e.preventDefault(); exportBuild(); alert('Build exported to rogue_build.json'); }}); | |
| // Save/Load to localStorage automatically | |
| function saveLS(){ | |
| const payload = {trees:trees.map(t=>t.talents.map(x=>x.isThreshold? (pointsSpentInTree(findTalentById(x.id).tree) >= x.threshold?1:0) : x.spent)), available:availablePoints}; | |
| localStorage.setItem('rogue_mock_build',JSON.stringify(payload)); | |
| } | |
| function loadLS(){ | |
| const raw = localStorage.getItem('rogue_mock_build'); if(!raw) return; | |
| try { | |
| const s = JSON.parse(raw); | |
| if(s && s.trees){ | |
| s.trees.forEach((arr,ti)=>{ trees[ti].talents.forEach((t,i)=>{ if(!t.isThreshold) t.spent = arr[i] || 0 }); }); | |
| availablePoints = (typeof s.available === 'number') ? s.available : TOTAL_POINTS; | |
| } | |
| } catch(e){ console.warn('Failed to load saved build:', e); } | |
| } | |
| // autosave frequently | |
| setInterval(()=>{ saveLS(); },2000); | |
| // load on start if present | |
| loadLS(); refreshUI(); | |
| // Accessibility: close overlay on Escape | |
| document.addEventListener('keydown',(e)=>{ if(e.key==='Escape' && overlayOpen){ toggleOverlay(); }}); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment