Skip to content

Instantly share code, notes, and snippets.

@EncodeTheCode
Created December 20, 2025 12:54
Show Gist options
  • Select an option

  • Save EncodeTheCode/da8f14bd9c591e3bbf3a335b1f7aecc3 to your computer and use it in GitHub Desktop.

Select an option

Save EncodeTheCode/da8f14bd9c591e3bbf3a335b1f7aecc3 to your computer and use it in GitHub Desktop.
<!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