hv-unified/src/abilities.js
GaboGG 402db6bb2f Initial commit: HV Unified v0.11.0 structure
- 23 source files in src/ (build via scripts/build.sh)
- Forum-sourced player knowledge in references/
- DESIGN.md with architecture and corrections
- References to existing scripts (Monsterbation, jpx, HV Utils)
2026-07-20 19:30:26 -04:00

169 lines
7.1 KiB
JavaScript

// ═══════════════════════════════════════════════════════════════════════
// ABILITIES — spell & skill unlock guide for abilities page
// ═══════════════════════════════════════════════════════════════════════
function enhanceAbilities() {
if (document.getElementById('hv-abilities')) return;
const lv = STATE.level || 1;
// Detect current tree
const url = window.location.href || '';
const treeMap = {
'tree=general': 'General',
'tree=onehanded': 'One-Handed',
'tree=twohanded': 'Two-Handed',
'tree=dualwield': 'Dual Wield',
'tree=niten': 'Niten',
'tree=staff': 'Staff',
'tree=cloth': 'Cloth',
'tree=light': 'Light',
'tree=heavy': 'Heavy',
'tree=deprecating1': 'Deprecating 1',
'tree=deprecating2': 'Deprecating 2',
'tree=supportive1': 'Supportive 1',
'tree=supportive2': 'Supportive 2',
'tree=elemental': 'Elemental',
'tree=forbidden': 'Forbidden',
'tree=divine': 'Divine',
};
let tree = 'General';
for (const [k, v] of Object.entries(treeMap)) {
if (url.includes(k)) { tree = v; break; }
}
// Read AP from CSS-digit counter
let ap = 0;
$$('#ability_top [class*="f4b"]').some(div => {
const txt = div.textContent || '';
if (txt.includes('Ability') || txt.includes('Mastery')) {
const d = readCSSDigits(div);
if (d !== null) ap = d;
return true;
}
});
// Parse all visible abilities
const abilities = [];
$$('[id^="slot_"][onmouseover]').forEach(el => {
const mo = el.getAttribute('onmouseover') || '';
const m = mo.match(/overability\((\d+),\s*'([^']+)',\s*'([^']*)',\s*'([^']*)',\s*'([^']*)'/);
if (!m) return;
const name = m[2];
const status = m[4];
const nextHtml = m[5];
const lvlMatch = nextHtml.match(/Level\s*(\d+)/i);
const lvlReq = lvlMatch ? parseInt(lvlMatch[1]) : 0;
const apMatch = nextHtml.match(/(\d+)\s*Ability\s*Points?/i);
const apCost = apMatch ? parseInt(apMatch[1]) : 0;
const tierMatch = status.match(/Tier\s*(\d+)/i);
const curTier = tierMatch ? parseInt(tierMatch[1]) : 0;
const acquired = status !== 'Not Acquired' && status !== 'Locked';
abilities.push({ name, status, acquired, curTier, lvlReq, apCost, el });
});
// Free slots
const freeSlots = $$('#ability_top [id^="slot_"]').filter(el => {
return (el.style.backgroundImage || '').includes('t/0.png');
}).length;
// Build panel
let body = `<b>💎 AP: ${ap}</b><br>`;
if (freeSlots > 0) {
body += `<div style="color:#f88;font-size:9px">⚠ ${freeSlots} empty slots — assign abilities!</div>`;
}
body += `<div style="border-top:1px solid #374151;margin:4px 0;padding:4px 0"><b>🌳 ${tree}</b>`;
const affordable = abilities.filter(a => !a.acquired && lv >= a.lvlReq && ap >= a.apCost);
const lockedByLevel = abilities.filter(a => !a.acquired && lv < a.lvlReq);
const owned = abilities.filter(a => a.acquired);
if (owned.length > 0) body += `<div style="color:#8f8;font-size:9px">✅ ${owned.length} owned</div>`;
if (affordable.length > 0) {
const best = affordable.sort((a, b) => a.apCost - b.apCost)[0];
body += `<div style="color:#0f0;font-size:9px">⬆ <b>Buy: ${best.name}</b> (${best.apCost} AP)</div>`;
}
if (lockedByLevel.length > 0) {
const next = lockedByLevel.sort((a, b) => a.lvlReq - b.lvlReq)[0];
body += `<div style="color:#fdcb00;font-size:9px">🔒 ${next.name} @ Lv${next.lvlReq}</div>`;
}
body += `</div>`;
// Full table
if (abilities.length > 0) {
body += `<div style="border-top:1px solid #374151;margin:4px 0;padding:4px 0;font-size:9px">
<table style="width:100%;border-collapse:collapse">
<tr style="color:#888"><th style="text-align:left;padding:1px 4px">Ability</th>
<th style="padding:1px 4px">AP</th><th style="padding:1px 4px">Lv</th>
<th style="text-align:right;padding:1px 4px">Status</th></tr>`;
const sorted = [].concat(
affordable.sort((a, b) => a.apCost - b.apCost),
owned,
lockedByLevel.sort((a, b) => a.lvlReq - b.lvlReq),
);
const seen = new Set();
for (const a of sorted) {
const k = a.name + a.curTier;
if (seen.has(k)) continue;
seen.add(k);
const color = a.acquired ? '#8f8'
: (lv >= a.lvlReq && ap >= a.apCost) ? '#0f0'
: lv >= a.lvlReq ? '#fdcb00' : '#888';
const lbl = a.acquired ? `T${a.curTier}`
: (lv >= a.lvlReq && ap >= a.apCost) ? '⬆ Buy'
: lv >= a.lvlReq ? '🔒AP' : `Lv${a.lvlReq}`;
body += `<tr><td style="padding:1px 4px;color:${color}">${a.name}</td>
<td style="padding:1px 4px;color:#888">${a.apCost}</td>
<td style="padding:1px 4px;color:#888">${a.lvlReq}</td>
<td style="padding:1px 4px;text-align:right;color:${color}">${lbl}</td></tr>`;
}
body += `</table></div>`;
}
// Global priority
body += `<div style="border-top:1px solid #374151;margin:4px 0;padding:4px 0;font-size:9px;color:#888">
<b>🗺 Priority:</b> General (Tanks) → Supportive → Weapon → Elemental → Deprecating<br>
Visit each tree tab for detailed recommendations.
</div>`;
const panel = document.createElement('div');
panel.id = 'hv-abilities';
panel.style.cssText = css({
position: 'fixed',
top: '60px',
right: '4px',
zIndex: '9995',
background: '#111827',
color: '#d1d5db',
padding: '0',
borderRadius: '8px',
fontSize: '10px',
fontFamily: 'monospace',
maxWidth: '320px',
boxShadow: '0 0 15px rgba(0,0,0,0.6)',
border: '1px solid #374151',
});
const collapsed = localStorage[SP + 'abCollapsed'] === '1';
panel.innerHTML = `<div style="display:flex;justify-content:space-between;align-items:center;padding:6px 8px;cursor:pointer" id="hv-ab-header">
<b style="color:#fdcb00;font-size:10px">📖 Abilities (Lv${lv})</b>
<span id="hv-ab-toggle" style="color:#888;font-size:12px">${collapsed ? '▶' : '▼'}</span>
</div>
<div id="hv-ab-body" style="padding:0 8px 6px;display:${collapsed ? 'none' : 'block'}">${body}</div>`;
panel.querySelector('#hv-ab-header').onclick = () => {
const b = document.getElementById('hv-ab-body');
const t = document.getElementById('hv-ab-toggle');
const h = b.style.display === 'none';
b.style.display = h ? 'block' : 'none';
t.textContent = h ? '▼' : '▶';
localStorage[SP + 'abCollapsed'] = h ? '0' : '1';
};
document.body.appendChild(panel);
}