v0.16.0 - Complement community scripts (AGY refactor)
REMOVED (owned by MB/HVUT): - RE timer (HVUtils has one) - Battle keybinds Q/H/C (Monsterbation owns battle keys; kept , for settings) - Hover-attack battle UI hooks (Monsterbation) - Battle logger (Monsterbation combat log) - Spirit/potion/scroll auto-use in battle (Monsterbation) - Armory bulk sell/salvage/shrine UI + Keep Top 10% button (HVUtils bulk tools) RESOLVED: - Q keybind collision: yielded to Monsterbation - DOM double-injection: removed duplicate overlays - Storage: hvunified_ namespace isolated from HVUT GM_* storage KEPT (gaps community doesn't cover): - Gear analysis/scoring (equipped vs store, 19-category advisory) - Strategy guidance as advisory layer (HV.advice/HV.action) - Guidance/abilities/difficulty advisor - window.HV public API bridge
This commit is contained in:
parent
589ed7e34c
commit
f2fc00965e
15 changed files with 198 additions and 3051 deletions
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -10,16 +10,14 @@ function executeAction(a) {
|
|||
case 'spell': result = a.selfTarget ? castSelfSpell(a.name) : castTargetSpell(a.name, a.target); break;
|
||||
case 'skill': result = useSkill(a.name, a.target); break;
|
||||
case 'item': result = useItem(a.id); break;
|
||||
case 'toggle_spirit': result = toggleSpiritStance(); break;
|
||||
}
|
||||
if (result) {
|
||||
// After dispatching an action, stagger the debounce so we don't queue
|
||||
// another action while the game processes this one (typically ~500-1000ms)
|
||||
STATE._lastActionTime = Date.now();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
function attackMonster(i) {
|
||||
if (i < 0 || i >= STATE.monsters.length) return false;
|
||||
const m = STATE.monsters[i];
|
||||
|
|
@ -111,9 +109,3 @@ function useSkill(n, i) {
|
|||
return true;
|
||||
}
|
||||
|
||||
function toggleSpiritStance() {
|
||||
const t = document.getElementById('ckey_spirit');
|
||||
if (!t) return false;
|
||||
t.click();
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
263
src/armory.js
263
src/armory.js
|
|
@ -24,264 +24,7 @@ function gradeEquipment(name) {
|
|||
}
|
||||
|
||||
function enhanceArmory() {
|
||||
const equipList = document.getElementById('equiplist');
|
||||
if (!equipList || document.getElementById('hv-armory-bar')) return;
|
||||
|
||||
// ── Parse current equipment in the list ──
|
||||
const items = [];
|
||||
let currentCategory = '';
|
||||
|
||||
$$('tr', equipList).forEach(row => {
|
||||
if (row.className === 'eqtplabel') {
|
||||
currentCategory = (row.textContent || '').trim();
|
||||
return;
|
||||
}
|
||||
|
||||
const cb = row.querySelector('input[name="eqids[]"]');
|
||||
if (!cb) return;
|
||||
|
||||
const id = cb.value;
|
||||
const label = row.querySelector('label');
|
||||
const name = label ? (label.textContent || '').trim() : '';
|
||||
|
||||
// Status icons in the label text
|
||||
const equipped = name.includes('🗡');
|
||||
const locked = name.includes('🔒');
|
||||
const pinned = name.includes('📌');
|
||||
const stored = name.includes('📦');
|
||||
const protected_ = name.includes('🛡');
|
||||
|
||||
// Strip status icons for clean name
|
||||
const cleanName = name.replace(/[🗡🔒📌📦🛡️]/g, '').trim();
|
||||
|
||||
items.push({
|
||||
id, name: cleanName, slot: currentCategory,
|
||||
equipped, locked, pinned, stored, protected: protected_,
|
||||
checkbox: cb, row,
|
||||
grade: gradeEquipment(cleanName),
|
||||
});
|
||||
});
|
||||
|
||||
if (items.length === 0) return;
|
||||
|
||||
// ── Detect which armory tab we're on ──
|
||||
let screen = 'organize';
|
||||
const url = window.location.href || '';
|
||||
const sm = url.match(/screen=(\w+)/);
|
||||
if (sm) screen = sm[1];
|
||||
|
||||
// ── Counts ──
|
||||
const sellCount = items.filter(i => i.grade.action === 'sell' && !i.equipped && !i.locked).length;
|
||||
const salvCount = items.filter(i => i.grade.action === 'salvage' && !i.equipped && !i.locked).length;
|
||||
const keepCount = items.filter(i => i.grade.action === 'keep').length;
|
||||
|
||||
// ── Build the toolbar ──
|
||||
const toolbar = document.createElement('div');
|
||||
toolbar.id = 'hv-armory-bar';
|
||||
toolbar.style.cssText = css({
|
||||
display: 'flex',
|
||||
gap: '4px',
|
||||
flexWrap: 'wrap',
|
||||
padding: '4px 6px',
|
||||
marginBottom: '2px',
|
||||
background: '#1a1a2e',
|
||||
borderRadius: '4px',
|
||||
fontSize: '10px',
|
||||
fontFamily: 'monospace',
|
||||
alignItems: 'center',
|
||||
});
|
||||
|
||||
// Summary
|
||||
const summary = document.createElement('span');
|
||||
summary.style.cssText = 'color:#888;margin-right:6px';
|
||||
summary.textContent = `✅${keepCount} ♻${salvCount} 💰${sellCount}`;
|
||||
toolbar.appendChild(summary);
|
||||
|
||||
// ── Smart auto-select: check boxes based on grade ──
|
||||
function autoSelect(action) {
|
||||
items.forEach(item => {
|
||||
if (item.grade.action !== action) return;
|
||||
if (item.equipped || item.locked) return;
|
||||
item.checkbox.checked = true;
|
||||
});
|
||||
// Trigger the game's update function if available
|
||||
if (typeof update_selected_count === 'function') update_selected_count();
|
||||
}
|
||||
|
||||
// ── Select by quality ──
|
||||
function selectByQuality(below) {
|
||||
const idx = KB.qualities.indexOf(below);
|
||||
items.forEach(item => {
|
||||
const qi = KB.qualities.indexOf(KB.qualities.find(q => item.name.includes(q)));
|
||||
if (qi < 0) return;
|
||||
if (qi <= idx && !item.equipped && !item.locked) {
|
||||
item.checkbox.checked = true;
|
||||
}
|
||||
});
|
||||
if (typeof update_selected_count === 'function') update_selected_count();
|
||||
}
|
||||
|
||||
function clearAll() {
|
||||
items.forEach(item => item.checkbox.checked = false);
|
||||
if (typeof update_selected_count === 'function') update_selected_count();
|
||||
}
|
||||
|
||||
// Buttons — only show relevant ones for current screen
|
||||
if (screen === 'sell') {
|
||||
const b = document.createElement('input');
|
||||
b.type = 'button';
|
||||
b.value = `💰 Select Crude (${sellCount})`;
|
||||
b.style.cssText = 'padding:2px 6px;cursor:pointer;background:#d50c2d;color:#fff;border:none;border-radius:3px;font-size:10px';
|
||||
b.onclick = () => autoSelect('sell');
|
||||
toolbar.appendChild(b);
|
||||
|
||||
const b2 = document.createElement('input');
|
||||
b2.type = 'button';
|
||||
b2.value = '💰 Select ≤Fair';
|
||||
b2.style.cssText = 'padding:2px 6px;cursor:pointer;background:#a00;color:#fff;border:none;border-radius:3px;font-size:10px';
|
||||
b2.onclick = () => selectByQuality('Fair');
|
||||
toolbar.appendChild(b2);
|
||||
}
|
||||
|
||||
if (screen === 'salvage') {
|
||||
const b = document.createElement('input');
|
||||
b.type = 'button';
|
||||
b.value = `♻ Select Salvage (${salvCount})`;
|
||||
b.style.cssText = 'padding:2px 6px;cursor:pointer;background:#555;color:#fff;border:none;border-radius:3px;font-size:10px';
|
||||
b.onclick = () => autoSelect('salvage');
|
||||
toolbar.appendChild(b);
|
||||
|
||||
const b2 = document.createElement('input');
|
||||
b2.type = 'button';
|
||||
b2.value = '♻ Select ≤Average';
|
||||
b2.style.cssText = 'padding:2px 6px;cursor:pointer;background:#444;color:#fff;border:none;border-radius:3px;font-size:10px';
|
||||
b2.onclick = () => selectByQuality('Average');
|
||||
toolbar.appendChild(b2);
|
||||
|
||||
// "Sell Salvaged Equipment" toggle is already native
|
||||
}
|
||||
|
||||
if (screen === 'organize') {
|
||||
const b = document.createElement('input');
|
||||
b.type = 'button';
|
||||
b.value = '📌 Pin Equipped';
|
||||
b.style.cssText = 'padding:2px 6px;cursor:pointer;background:#2a5a2a;color:#fff;border:none;border-radius:3px;font-size:10px';
|
||||
b.onclick = () => {
|
||||
// Select all equipped items so they can be pinned
|
||||
items.forEach(item => { if (item.equipped) item.checkbox.checked = true; });
|
||||
if (typeof update_selected_count === 'function') update_selected_count();
|
||||
};
|
||||
toolbar.appendChild(b);
|
||||
}
|
||||
|
||||
// ── Keep Top 5 per category (score-based), select the rest ──
|
||||
// Categories: 1H, 2H, Staff, Shield + 5 armor slots × (Cloth, Light, Heavy)
|
||||
function qualityIndex(name) {
|
||||
const q = KB.qualities.find(qq => name.includes(qq));
|
||||
return q ? KB.qualities.indexOf(q) : 0;
|
||||
}
|
||||
|
||||
function keepTop5AndSelect(action) {
|
||||
const catMap = new Map();
|
||||
|
||||
items.forEach(item => {
|
||||
if (item.equipped || item.locked || item.pinned) return;
|
||||
const data = getHVEquipData(item.id);
|
||||
const stats = data && data.d ? parseEquipHTML(data.d) : null;
|
||||
if (!stats) return; // skip unscraped items — leave untouched
|
||||
|
||||
const type = (stats.type || '');
|
||||
let cat = '?';
|
||||
if (type.includes('Two-handed')) cat = '2H';
|
||||
else if (type.includes('One-handed')) cat = '1H';
|
||||
else if (type.includes('Staff')) cat = 'Staff';
|
||||
else if (type.includes('Shield')) cat = 'Shield';
|
||||
else if (type.includes('Cloth')) cat = 'Cloth';
|
||||
else if (type.includes('Light')) cat = 'Light';
|
||||
else if (type.includes('Heavy')) cat = 'Heavy';
|
||||
|
||||
// For armor, append the slot (Head/Body/Hands/Legs/Feet)
|
||||
if (['Cloth', 'Light', 'Heavy'].includes(cat)) {
|
||||
const slot = gearDetectSlot({ name: item.name, category: cat });
|
||||
if (!slot) return;
|
||||
cat += ' ' + slot;
|
||||
}
|
||||
|
||||
const isWeaponCat = cat.startsWith('1H') || cat.startsWith('2H') || cat === 'Staff' || cat === 'Shield';
|
||||
const quality = qualityIndex(item.name);
|
||||
const score = isWeaponCat
|
||||
? gearScoreWeapon({ name: item.name, stats, quality })
|
||||
: gearScoreArmor({ name: item.name, stats, quality });
|
||||
|
||||
if (!catMap.has(cat)) catMap.set(cat, []);
|
||||
catMap.get(cat).push({ item, score });
|
||||
});
|
||||
|
||||
// Keep top 10% per category (min 5), select the rest
|
||||
let selected = 0;
|
||||
catMap.forEach((list, cat) => {
|
||||
list.sort((a, b) => b.score - a.score);
|
||||
const keepCount = Math.max(5, Math.ceil(list.length * 0.10));
|
||||
const keep = list.slice(0, keepCount);
|
||||
const dump = list.slice(keepCount);
|
||||
dump.forEach(({ item }) => {
|
||||
item.checkbox.checked = true;
|
||||
selected++;
|
||||
});
|
||||
console.log(`%c[HV] 🗂 ${cat}: ${list.length} items → keep ${keep.length} (top ${keep.map(k => k.score.toFixed(0)).join('/')}), select ${dump.length}`, 'color:#0f0');
|
||||
});
|
||||
|
||||
if (typeof update_selected_count === 'function') update_selected_count();
|
||||
return selected;
|
||||
}
|
||||
|
||||
// ── Keep Top 10% button — available on sell and salvage screens ──
|
||||
if (screen === 'sell' || screen === 'salvage') {
|
||||
const b3 = document.createElement('input');
|
||||
b3.type = 'button';
|
||||
b3.value = `⭐ Keep Top 10% / Select ${screen === 'sell' ? 'Sell' : 'Salvage'} Rest`;
|
||||
b3.style.cssText = 'padding:2px 6px;cursor:pointer;background:#7a5a2a;color:#fff;border:none;border-radius:3px;font-size:10px';
|
||||
b3.title = 'Keeps the top 10% (min 5) highest-scoring items in each category (1H, 2H, Staff, Shield, and each armor slot per Cloth/Light/Heavy). Selects everything else.';
|
||||
b3.onclick = () => {
|
||||
const n = keepTop5AndSelect(screen);
|
||||
console.log(`%c[HV] ⭐ Keep Top 10%: ${n} items selected for ${screen}`, 'color:#0f0');
|
||||
};
|
||||
toolbar.appendChild(b3);
|
||||
}
|
||||
|
||||
// Clear button always available
|
||||
const clear = document.createElement('input');
|
||||
clear.type = 'button';
|
||||
clear.value = '✗ Clear';
|
||||
clear.style.cssText = 'padding:2px 6px;cursor:pointer;background:#555;color:#fff;border:none;border-radius:3px;font-size:10px';
|
||||
clear.onclick = clearAll;
|
||||
toolbar.appendChild(clear);
|
||||
|
||||
// Select All / Invert Selection (useful on any screen)
|
||||
const selAll = document.createElement('input');
|
||||
selAll.type = 'button';
|
||||
selAll.value = '☐ All';
|
||||
selAll.style.cssText = 'padding:2px 6px;cursor:pointer;background:#3a4a6a;color:#fff;border:none;border-radius:3px;font-size:10px';
|
||||
selAll.onclick = () => {
|
||||
items.forEach(item => { item.checkbox.checked = true; });
|
||||
if (typeof update_selected_count === 'function') update_selected_count();
|
||||
};
|
||||
toolbar.appendChild(selAll);
|
||||
|
||||
const invert = document.createElement('input');
|
||||
invert.type = 'button';
|
||||
invert.value = '⊞ Invert';
|
||||
invert.style.cssText = 'padding:2px 6px;cursor:pointer;background:#444;color:#fff;border:none;border-radius:3px;font-size:10px';
|
||||
invert.onclick = () => {
|
||||
items.forEach(item => { item.checkbox.checked = !item.checkbox.checked; });
|
||||
if (typeof update_selected_count === 'function') update_selected_count();
|
||||
};
|
||||
toolbar.appendChild(invert);
|
||||
|
||||
// ── Insert into page ──
|
||||
const eqSelect = document.getElementById('equipselect_outer') ||
|
||||
document.querySelector('#equipselect_left, #armory_right > div');
|
||||
if (equipList && equipList.parentNode) {
|
||||
equipList.parentNode.insertBefore(toolbar, equipList);
|
||||
}
|
||||
// Physical bulk select/sell/salvage/shrine buttons in Armory are owned by HVUT.
|
||||
// HV Unified provides advisory scoring via HV.analyzeGear() and gear-analysis panel.
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,108 +1,9 @@
|
|||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// BATTLE LOGGER — saves raw battle log lines to localStorage in real time
|
||||
// BATTLE LOGGER — REMOVED (Handled by Monsterbation combat log)
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
let _lastLogCount = 0;
|
||||
const LOG_KEY = SP + 'battleLog';
|
||||
function initBattleLog() {}
|
||||
function logRound() {}
|
||||
function getBattleLog() { return null; }
|
||||
function getBattleSummary() { return null; }
|
||||
|
||||
function initBattleLog() {
|
||||
_lastLogCount = 0;
|
||||
// Clear previous log
|
||||
try { localStorage.removeItem(LOG_KEY); } catch (e) {}
|
||||
}
|
||||
|
||||
function logRound() {
|
||||
const log = document.getElementById('textlog');
|
||||
if (!log) return;
|
||||
|
||||
const allRows = log.querySelectorAll('tr');
|
||||
if (allRows.length <= _lastLogCount) return;
|
||||
|
||||
// Get only the NEW rows since last capture
|
||||
const newLines = [];
|
||||
for (let i = _lastLogCount; i < allRows.length; i++) {
|
||||
const text = (allRows[i].textContent || '').trim();
|
||||
if (text) newLines.push(text);
|
||||
}
|
||||
_lastLogCount = allRows.length;
|
||||
|
||||
if (newLines.length === 0) return;
|
||||
|
||||
// Read existing log from localStorage, append new lines, save back
|
||||
let existing = [];
|
||||
try {
|
||||
const saved = localStorage.getItem(LOG_KEY);
|
||||
if (saved) existing = JSON.parse(saved);
|
||||
} catch (e) {}
|
||||
|
||||
existing.push(...newLines);
|
||||
|
||||
try {
|
||||
localStorage.setItem(LOG_KEY, JSON.stringify(existing));
|
||||
} catch (e) {
|
||||
// localStorage full — keep only last 500 lines
|
||||
try {
|
||||
const trimmed = existing.slice(-500);
|
||||
localStorage.setItem(LOG_KEY, JSON.stringify(trimmed));
|
||||
} catch (e2) {}
|
||||
}
|
||||
}
|
||||
|
||||
function getBattleLog() {
|
||||
try {
|
||||
const saved = localStorage.getItem(LOG_KEY);
|
||||
return saved ? JSON.parse(saved) : null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getBattleSummary() {
|
||||
const lines = getBattleLog();
|
||||
if (!lines || lines.length === 0) return null;
|
||||
|
||||
let kills = 0, dmgDealt = 0, dmgTaken = 0, healed = 0;
|
||||
const spells = {}, skills = {}, items = {};
|
||||
|
||||
for (const line of lines) {
|
||||
const mKill = line.match(/(.+) has been defeated\./);
|
||||
if (mKill && !line.includes('gains the effect')) kills++;
|
||||
|
||||
const mDmg = line.match(/causing (\d+) points/);
|
||||
if (mDmg) dmgDealt += parseInt(mDmg[1]);
|
||||
|
||||
const mTaken = line.match(/take (\d+) (\w+)/);
|
||||
if (mTaken) dmgTaken += parseInt(mTaken[1]);
|
||||
|
||||
const mRegen = line.match(/Regen restores (\d+)/);
|
||||
if (mRegen) healed += parseInt(mRegen[1]);
|
||||
const mPot = line.match(/Regeneration restores (\d+)/);
|
||||
if (mPot) healed += parseInt(mPot[1]);
|
||||
const mCure = line.match(/healed for (\d+)/);
|
||||
if (mCure) healed += parseInt(mCure[1]);
|
||||
|
||||
const mSpell = line.match(/You cast (\w[\w\s-]+)\./);
|
||||
if (mSpell) spells[mSpell[1]] = (spells[mSpell[1]] || 0) + 1;
|
||||
|
||||
const mSkill = line.match(/You use (\w[\w\s-]+)\./);
|
||||
if (mSkill && !mSkill[1].includes('Draught') && !mSkill[1].includes('Potion') && !mSkill[1].includes('Elixir') && !mSkill[1].includes('Gem')) {
|
||||
skills[mSkill[1]] = (skills[mSkill[1]] || 0) + 1;
|
||||
}
|
||||
|
||||
const mItem = line.match(/You use (.+?)\.$/);
|
||||
if (mItem && (mItem[1].includes('Draught') || mItem[1].includes('Potion') || mItem[1].includes('Elixir') || mItem[1].includes('Gem'))) {
|
||||
items[mItem[1]] = (items[mItem[1]] || 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
totalLines: lines.length,
|
||||
kills,
|
||||
dmgDealt,
|
||||
dmgTaken,
|
||||
healed,
|
||||
spells,
|
||||
skills,
|
||||
items,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,17 +2,13 @@
|
|||
// CONFIG — default settings
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
const VERSION = '0.15.4';
|
||||
const VERSION = '0.16.0';
|
||||
|
||||
const CFG = {
|
||||
// — Battle automation
|
||||
hotkey: 'KeyQ',
|
||||
hotkeyMod: '',
|
||||
hoverEnabled: true,
|
||||
// — Battle strategy advisory
|
||||
autoBuff: true,
|
||||
autoDebuff: true,
|
||||
autoCure: true,
|
||||
autoSpirit: false, // Manual spirit stance only (auto-disable at 65% SP for Spark)
|
||||
|
||||
// — Thresholds (simulation-backed)
|
||||
cureHP: 0.45, // Cast Cure below this HP
|
||||
|
|
@ -20,29 +16,18 @@ const CFG = {
|
|||
cureRegenHP: 0.65, // Cast Regen below this HP
|
||||
manaGemMP: 0.45, // Use mana gems below this MP
|
||||
manaPotionMP: 0.25, // Use mana potions at this MP
|
||||
spiritPotionSP: 0.60, // Use spirit gems/potions below this SP (Spark costs 50%)
|
||||
spiritStanceOC: 80, // Manual spirit stance only (auto-disable at 65% SP for Spark)
|
||||
skillOC: 60, // Use weapon skills at this OC (lower = faster, key on IWBTH)
|
||||
skillOC: 60, // Use weapon skills at this OC
|
||||
useAttackSpells: false, // Don't use attack spells (let channel proc naturally)
|
||||
|
||||
// — Out of battle
|
||||
autoSell: true,
|
||||
sellQuality: 'Average',
|
||||
bulkShrine: true,
|
||||
reTimer: true,
|
||||
trainingQueue: true,
|
||||
|
||||
// — UI
|
||||
showCooldowns: true,
|
||||
showMonsterHP: true,
|
||||
showDurations: true,
|
||||
alertColours: true,
|
||||
showMonsterNumbers: true,
|
||||
// — UI & Guidance
|
||||
cfgButton: true,
|
||||
showGuidance: true,
|
||||
showEquipAdvice: true,
|
||||
autoDifficulty: true,
|
||||
|
||||
// — Combat style
|
||||
useAttackSpells: true,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
// ==UserScript==
|
||||
// @name HV Unified
|
||||
// @namespace hvunified
|
||||
// @version 0.15.4
|
||||
// @description HV Unified — battle automation, guidance, and UI enhancements for HentaiVerse
|
||||
// @version 0.16.0
|
||||
// @description HV Unified — Bridge & Strategy/Gear Advisor for HentaiVerse (complements Monsterbation & HVUtils)
|
||||
// @author GaboGG + Hermes
|
||||
// @match *://*.hentaiverse.org/*
|
||||
// @match *://alt.hentaiverse.org/*
|
||||
|
|
@ -12,3 +12,4 @@
|
|||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,27 +1,6 @@
|
|||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// HOVER SYSTEM — mouse-over monster actions
|
||||
// HOVER SYSTEM — REMOVED (Handled by Monsterbation / MB)
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
function setupHover() {
|
||||
if (!CFG.hoverEnabled) return;
|
||||
function setupHover() {}
|
||||
|
||||
STATE.monsters.forEach((m, i) => {
|
||||
if (!m || m.dataset.hvHover) return;
|
||||
m.dataset.hvHover = '1';
|
||||
|
||||
const area = m.querySelector('.btm6') || m;
|
||||
area.addEventListener('mouseenter', () => {
|
||||
STATE.hoverTarget = i;
|
||||
if (!STATE.interruptHover && !STATE.interruptAlert) {
|
||||
const ac = getSmartAction();
|
||||
if (ac) executeAction(ac);
|
||||
}
|
||||
});
|
||||
area.addEventListener('mouseleave', () => { STATE.hoverTarget = -1; });
|
||||
});
|
||||
|
||||
// RiddleMaster blocks hover
|
||||
if (document.getElementById('riddlemaster')) {
|
||||
STATE.interruptHover = true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
61
src/init.js
61
src/init.js
|
|
@ -1,41 +1,31 @@
|
|||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// INIT — bootstrap everything
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
function init() {
|
||||
loadConfig();
|
||||
detectLevel();
|
||||
STATE.page = detectPage();
|
||||
|
||||
// Community script presence detection
|
||||
STATE.hasMB = !!(window.Monsterbation || document.getElementById('mb_vitals') || document.querySelector('.mb_vital'));
|
||||
STATE.hasHVUT = !!(window.HVUT || document.querySelector('.hvut-main') || document.getElementById('hvut_menu'));
|
||||
|
||||
// Always-on features
|
||||
setupRETimer();
|
||||
addConfigButton();
|
||||
|
||||
if (STATE.page === 'battle') {
|
||||
initializeBattle();
|
||||
autoCheckTask('battle');
|
||||
} else {
|
||||
// Non-battle page enhancements
|
||||
// Non-battle page enhancements & advisory tools
|
||||
if (STATE.page === 'itemshop') { enhanceItemShop(); autoCheckTask('buy-health'); }
|
||||
if (STATE.page === 'equipshop') { enhanceEquipShopWithAdvice(); }
|
||||
if (STATE.page === 'equipshop') { enhanceEquipShopWithAdvice(); enhanceGearAnalysis(); }
|
||||
if (STATE.page === 'shrine') enhanceShrine();
|
||||
if (STATE.page === 'training') { enhanceTraining(); autoCheckTask('training'); }
|
||||
if (STATE.page === 'monsterlab') { enhanceMonsterLab(); autoCheckTask('feed'); }
|
||||
if (STATE.page === 'arena') { autoCheckTask('arenas'); autoCheckTask('first-blood'); }
|
||||
if (STATE.page === 'character') showGuidancePanel();
|
||||
if (STATE.page === 'settings') enhanceSettings();
|
||||
if (STATE.page === 'armory') { enhanceArmory(); enhanceGearAnalysis(); }
|
||||
if (STATE.page === 'equipshop') enhanceGearAnalysis();
|
||||
if (STATE.page === 'abilities') enhanceAbilities();
|
||||
if (STATE.page === 'monster') enhanceMonsterLab();
|
||||
}
|
||||
|
||||
// Dawn initialization
|
||||
if (!localStorage[SP + 'lastDawn']) {
|
||||
try { localStorage[SP + 'lastDawn'] = JSON.stringify(Date.now()); } catch (e) {}
|
||||
}
|
||||
updateRETimer();
|
||||
|
||||
// Watch for dynamic page transitions (arena/grindfest → battle)
|
||||
const bodyObserver = new MutationObserver(() => {
|
||||
if (!STATE.inBattle && isBattlePage()) {
|
||||
|
|
@ -49,62 +39,35 @@ function initializeBattle() {
|
|||
if (STATE.battleInitialized && STATE.page === 'battle') return;
|
||||
STATE.page = 'battle';
|
||||
STATE.inBattle = true;
|
||||
STATE._mysticUsed = false; // Reset Mystic Gem tracking on new battle
|
||||
STATE._spiritForSkill = 0; // Reset spirit burst toggle
|
||||
STATE._baseMpCosts = {}; // Reset base cost cache
|
||||
|
||||
parseBattleState();
|
||||
// Force tier update from cached level (battle page may not have level_readout)
|
||||
if (STATE.level > 1) updateTier();
|
||||
setupHover();
|
||||
setupKeybindings();
|
||||
addMonsterNumbers();
|
||||
refreshUI();
|
||||
addConfigButton();
|
||||
showDifficultyBanner();
|
||||
|
||||
// Watch battle log
|
||||
// Passive battle state parser (updates STATE without interfering with MB UI/clicks)
|
||||
const log = document.getElementById('textlog');
|
||||
if (log && !log.dataset.hvObserved) {
|
||||
log.dataset.hvObserved = '1';
|
||||
const obs = new MutationObserver(() => {
|
||||
parseBattleState();
|
||||
saveMonsterData();
|
||||
logRound(); // Log this round's data for analysis
|
||||
if (CFG.hoverEnabled && !STATE.interruptHover && !STATE.interruptAlert && STATE.hoverTarget >= 0) {
|
||||
const a = getSmartAction();
|
||||
if (a) executeAction(a);
|
||||
}
|
||||
refreshUI();
|
||||
updateConfigButton();
|
||||
});
|
||||
obs.observe(log, { childList: true, subtree: true, characterData: true });
|
||||
}
|
||||
|
||||
// Watch vitals pane
|
||||
const vitals = document.getElementById('pane_vitals');
|
||||
if (vitals && !vitals.dataset.hvObserved) {
|
||||
vitals.dataset.hvObserved = '1';
|
||||
const vobs = new MutationObserver(() => {
|
||||
parseBattleState();
|
||||
logRound(); // Also log on vitals changes (catches end-of-round)
|
||||
refreshUI();
|
||||
updateConfigButton();
|
||||
// Check if battle ended (completion pane appeared)
|
||||
if (document.getElementById('pane_completion')) {
|
||||
// Battle ended — handled by log observer
|
||||
}
|
||||
});
|
||||
vobs.observe(vitals, { childList: true, subtree: true, attributes: true });
|
||||
}
|
||||
|
||||
// RiddleMaster detection
|
||||
if (document.getElementById('riddlemaster')) {
|
||||
STATE.interruptHover = true;
|
||||
}
|
||||
|
||||
// Auto-scrape gear data on character/armory pages (reads from HV's in-memory store)
|
||||
// Wait for dynjs_equip to load (external script, may load after document-end)
|
||||
// Auto-scrape gear data on character/armory pages
|
||||
const url = window.location.href || '';
|
||||
if (url.includes('ss=eq') || url.includes('ss=ch') || url.includes('ss=am')) {
|
||||
let attempts = 0;
|
||||
|
|
@ -115,9 +78,8 @@ function initializeBattle() {
|
|||
return;
|
||||
}
|
||||
if (++attempts < 10) {
|
||||
setTimeout(tryScrape, 500); // retry every 500ms up to 5 seconds
|
||||
setTimeout(tryScrape, 500);
|
||||
} else {
|
||||
// Fallback: scrape names/IDs without stats
|
||||
autoScrapeGear();
|
||||
}
|
||||
};
|
||||
|
|
@ -135,9 +97,10 @@ if (document.readyState === 'loading') {
|
|||
init();
|
||||
}
|
||||
|
||||
console.log('%c🛡️ HV Unified v' + VERSION + '%c | Q=action H=hover C=cure ,=settings',
|
||||
console.log('%c🛡️ HV Unified v' + VERSION + ' Bridge & Advisory Mode%c | ,=settings',
|
||||
'color:#fdcb00;font-weight:bold', '');
|
||||
console.log('%c HV.advice() for guidance | HV.difficulty() for difficulty check',
|
||||
console.log('%c HV.analyzeGear() for gear scoring | HV.advice() for build guidance | HV.difficulty() for difficulty check',
|
||||
'color:#888;font-size:10px');
|
||||
|
||||
})();
|
||||
|
||||
|
|
|
|||
|
|
@ -3,14 +3,13 @@
|
|||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
let keybindingsSetup = false;
|
||||
const Q_DEBOUNCE_MS = 300; // Ignore Q repeats faster than this
|
||||
|
||||
function setupKeybindings() {
|
||||
if (keybindingsSetup) return;
|
||||
keybindingsSetup = true;
|
||||
|
||||
document.addEventListener('keydown', function(e) {
|
||||
// Settings — comma key
|
||||
// Settings — comma key (non-conflicting with Monsterbation)
|
||||
if (e.keyCode === KEYS.COMMA) {
|
||||
if (isBattlePage()) {
|
||||
e.preventDefault();
|
||||
|
|
@ -19,90 +18,8 @@ function setupKeybindings() {
|
|||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Main action hotkey (default: Q)
|
||||
if (e.code === CFG.hotkey && !e.ctrlKey && !e.altKey && !e.metaKey) {
|
||||
if (!isInputFocused() && isBattlePage()) {
|
||||
// Debounce: ignore key-repeats until game processes the action
|
||||
const now = Date.now();
|
||||
if (now - (STATE._lastActionTime || 0) < Q_DEBOUNCE_MS) return;
|
||||
STATE._lastActionTime = now;
|
||||
|
||||
console.log('%c[HV] Q pressed — checking action...', 'color:#888');
|
||||
parseBattleState();
|
||||
// Debug: show full battle state
|
||||
const activeBuffs = Object.entries(STATE.buffs)
|
||||
.filter(([k, v]) => v > 0 && k !== 'regen' || v > 0)
|
||||
.map(([k, v]) => `${k}=${v}`).join(' ');
|
||||
// Monster SP bars (threat levels) with alive/dead markers
|
||||
const threatLevels = STATE.monsters.map((m, i) => {
|
||||
const sp = STATE.monsterSp[i] || 0;
|
||||
const alive = m && m.hasAttribute && m.hasAttribute('onclick') ? 'A' : 'D';
|
||||
return `${i}:${sp}${alive}`;
|
||||
}).join(' ');
|
||||
// Skill cooldowns — check DOM for opacity/onclick on skill elements
|
||||
const skillCDs = ['Rending Blow', 'Shatter Strike', 'Great Cleave']
|
||||
.filter(s => STATE.skillsKnown.includes(s))
|
||||
.map(s => {
|
||||
const el = document.querySelector(`.btqs[onmouseover*="set_infopane_spell('${s}'"], .btsd[onmouseover*="set_infopane_spell('${s}'"]`);
|
||||
const ready = el && el.hasAttribute('onclick');
|
||||
return `${s.charAt(0)}${ready ? 'R' : 'C'}`;
|
||||
}).join('');
|
||||
const curMp = parseInt((document.getElementById('vrm') || {}).textContent) || 0;
|
||||
const curHp = parseInt((document.getElementById('vrhd') || {}).textContent) || 0;
|
||||
// Item counts from itemsKnown
|
||||
const itemTypes = {};
|
||||
Object.values(STATE.itemsKnown).forEach(info => {
|
||||
const def = ITEMS[parseInt(info)];
|
||||
if (def) itemTypes[def.t] = (itemTypes[def.t] || 0) + 1;
|
||||
});
|
||||
const itemStr = ['heal','spirit','mana'].filter(t => itemTypes[t]).map(t => `${t.charAt(0)}${itemTypes[t]}`).join(' ');
|
||||
console.log(
|
||||
`%c[HV] ch=${STATE.channeling} hp=${curHp} mp=${curMp}(${(STATE.mp*100).toFixed(0)}%) ` +
|
||||
`sp=${(STATE.sp*100).toFixed(0)}% oc=${STATE.oc.toFixed(0)} ` +
|
||||
`mon:${STATE.monsters.length} sk:${skillCDs || '--'} ` +
|
||||
`${itemStr} ` +
|
||||
`buffs: ${activeBuffs} | spBars: ${threatLevels}`,
|
||||
'color:#888'
|
||||
);
|
||||
const a = getRecommendedAction();
|
||||
if (a) {
|
||||
e.preventDefault();
|
||||
// Show explicit action details
|
||||
let label = '';
|
||||
if (a.type === 'spell') {
|
||||
label = a.name + (a.target >= 0 ? ' → monster ' + a.target : ' (self)');
|
||||
} else if (a.type === 'skill') {
|
||||
label = a.name + ' → monster ' + a.target;
|
||||
} else if (a.type === 'item') {
|
||||
const itemDef = ITEMS[parseInt(STATE.itemsKnown[a.id] || '0')];
|
||||
label = (itemDef ? itemDef.n : a.id) + (a.selfTarget ? ' (self)' : '');
|
||||
} else if (a.type === 'attack') {
|
||||
label = 'monster ' + a.target;
|
||||
} else {
|
||||
label = a.target >= 0 ? 'monster ' + a.target : a.name || a.id;
|
||||
}
|
||||
console.log(`%c[HV] ▶ ${a.type} → ${label}${a._reason ? ' (' + a._reason + ')' : ''}`, 'color:#0f0');
|
||||
executeAction(a);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hover toggle (H)
|
||||
if (e.code === 'KeyH' && !e.ctrlKey && !e.altKey && !e.metaKey && !isInputFocused()) {
|
||||
STATE.interruptHover = !STATE.interruptHover;
|
||||
console.log(`%c[HV] Hover ${STATE.interruptHover ? 'OFF' : 'ON'}`, 'color:#f80');
|
||||
}
|
||||
|
||||
// Emergency heal (C) — always cast Cure/Full-Cure regardless of strategy
|
||||
if (e.code === 'KeyC' && !e.ctrlKey && !e.altKey && !e.metaKey && !isInputFocused()) {
|
||||
const c = hasSpell('Full-Cure') ? 'Full-Cure' : hasSpell('Cure') ? 'Cure' : null;
|
||||
if (c) {
|
||||
console.log(`%c[HV] 🚑 Emergency: casting ${c}`, 'color:#f44');
|
||||
castSelfSpell(c);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log('%c⌨ [HV] Keys: Q=action H=hover C=cure ,=settings', 'color:#0f0;font-size:11px');
|
||||
console.log('%c⌨ [HV] Key: ,=settings (Battle input managed by Monsterbation)', 'color:#0f0;font-size:11px');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -130,38 +130,7 @@ function enhanceEquipShopWithAdvice() {
|
|||
// ── Shrine: bulk selection ──
|
||||
|
||||
function enhanceShrine() {
|
||||
if (!CFG.bulkShrine) return;
|
||||
const m = document.getElementById('mainpane');
|
||||
if (!m || document.getElementById('hv-bulk-shrine')) return;
|
||||
|
||||
const r = document.createElement('div');
|
||||
r.id = 'hv-bulk-shrine';
|
||||
r.style.cssText = 'margin:6px;display:flex;gap:6px';
|
||||
|
||||
const b1 = document.createElement('input');
|
||||
b1.type = 'button';
|
||||
b1.value = '⛩️ Select Non-Figurines';
|
||||
b1.style.cssText = 'padding:4px 10px;cursor:pointer;background:#fdcb00;color:#000;border:none;border-radius:3px;font-size:12px';
|
||||
b1.onclick = () => {
|
||||
$$('tr', m).forEach(row => {
|
||||
const t = (row.textContent || '');
|
||||
if (!t.includes('Figurine') && !t.includes('Collectable')) {
|
||||
const cb = row.querySelector('input[type="checkbox"]');
|
||||
if (cb) cb.checked = true;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const b2 = document.createElement('input');
|
||||
b2.type = 'button';
|
||||
b2.value = '✗ Clear';
|
||||
b2.style.cssText = 'padding:4px 10px;cursor:pointer;background:#666;color:#fff;border:none;border-radius:3px;font-size:12px';
|
||||
b2.onclick = () => { $$('input[type="checkbox"]', m).forEach(cb => cb.checked = false); };
|
||||
|
||||
r.appendChild(b1);
|
||||
r.appendChild(b2);
|
||||
const ta = m.querySelector('div');
|
||||
if (ta) ta.insertBefore(r, ta.firstChild);
|
||||
// Shrine enhancements are owned by HVUT.
|
||||
}
|
||||
|
||||
// ── Training: priority guide ──
|
||||
|
|
@ -180,103 +149,11 @@ function enhanceTraining() {
|
|||
if (ta) ta.insertBefore(d, ta.firstChild);
|
||||
}
|
||||
|
||||
// ── Monster Lab: floating info panel + feed buttons ──
|
||||
|
||||
let _mlDragOffset = { x: 0, y: 0 };
|
||||
|
||||
function enhanceMonsterLab() {
|
||||
// Create floating draggable panel
|
||||
if (document.getElementById('hv-monster')) return;
|
||||
|
||||
const panel = document.createElement('div');
|
||||
panel.id = 'hv-monster';
|
||||
panel.style.cssText = css({
|
||||
position: 'fixed',
|
||||
top: '80px',
|
||||
right: '4px',
|
||||
zIndex: '9999',
|
||||
background: '#111827',
|
||||
color: '#d1d5db',
|
||||
padding: '8px 10px',
|
||||
borderRadius: '8px',
|
||||
fontSize: '10px',
|
||||
fontFamily: 'monospace',
|
||||
maxWidth: '320px',
|
||||
boxShadow: '0 0 15px rgba(0,0,0,0.6)',
|
||||
border: '1px solid #374151',
|
||||
cursor: 'move',
|
||||
userSelect: 'none',
|
||||
});
|
||||
|
||||
const body = `<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;cursor:move" id="hv-monster-header">
|
||||
<b style="color:#fdcb00;font-size:11px">🧬 Monster Lab</b>
|
||||
<span id="hv-monster-toggle" style="color:#888;font-size:14px;cursor:pointer">−</span>
|
||||
</div>
|
||||
<div id="hv-monster-body">
|
||||
<div style="margin-bottom:6px;display:flex;gap:4px">
|
||||
<input type="button" value="🍖 Feed All" style="padding:4px 10px;cursor:pointer;background:#2a5a2a;color:#fff;border:none;border-radius:3px;font-size:11px" id="hv-feed-all">
|
||||
<input type="button" value="💊 Happy Pills All" style="padding:4px 10px;cursor:pointer;background:#5a3a2a;color:#fff;border:none;border-radius:3px;font-size:11px" id="hv-pill-all">
|
||||
</div>
|
||||
<div style="color:#9ca3af;font-size:9px;border-top:1px solid #374151;padding-top:4px">
|
||||
New to ML? Pick a <b style="color:#fdcb00">Beast-class</b> (Dog/Cow/Pig) for STR/END crystals.<br>
|
||||
<span style="color:#888">Click + drag header to move.</span>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
panel.innerHTML = body;
|
||||
document.body.appendChild(panel);
|
||||
|
||||
// ── Feed All ──
|
||||
panel.querySelector('#hv-feed-all').onclick = () => {
|
||||
const m = document.getElementById('mainpane');
|
||||
if (m) $$('input[value="Feed"]', m).forEach(b => b.click());
|
||||
};
|
||||
|
||||
// ── Happy Pills All ──
|
||||
panel.querySelector('#hv-pill-all').onclick = () => {
|
||||
const m = document.getElementById('mainpane');
|
||||
if (m) {
|
||||
const pills = $$('option', m).filter(o => (o.textContent || '').includes('Happy Pill'));
|
||||
if (pills.length > 0) {
|
||||
pills[0].selected = true;
|
||||
$$('input[value="Feed"]', m).forEach(b => b.click());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ── Toggle ──
|
||||
panel.querySelector('#hv-monster-toggle').onclick = () => {
|
||||
const body = panel.querySelector('#hv-monster-body');
|
||||
const isHidden = body.style.display === 'none';
|
||||
body.style.display = isHidden ? 'block' : 'none';
|
||||
panel.querySelector('#hv-monster-toggle').textContent = isHidden ? '−' : '+';
|
||||
};
|
||||
|
||||
// ── Make draggable ──
|
||||
const header = panel.querySelector('#hv-monster-header');
|
||||
if (header) {
|
||||
header.addEventListener('mousedown', (e) => {
|
||||
_mlDragOffset.x = e.clientX - panel.getBoundingClientRect().left;
|
||||
_mlDragOffset.y = e.clientY - panel.getBoundingClientRect().top;
|
||||
const onMove = (ev) => {
|
||||
panel.style.left = (ev.clientX - _mlDragOffset.x) + 'px';
|
||||
panel.style.right = 'auto';
|
||||
panel.style.top = (ev.clientY - _mlDragOffset.y) + 'px';
|
||||
};
|
||||
const onUp = () => {
|
||||
document.removeEventListener('mousemove', onMove);
|
||||
document.removeEventListener('mouseup', onUp);
|
||||
};
|
||||
document.addEventListener('mousemove', onMove);
|
||||
document.addEventListener('mouseup', onUp);
|
||||
});
|
||||
}
|
||||
|
||||
// Remove the old inline bar if it exists
|
||||
const oldBar = document.getElementById('hv-lab-enhance');
|
||||
if (oldBar) oldBar.remove();
|
||||
// Monster Lab enhancements are owned by HVUT.
|
||||
}
|
||||
|
||||
|
||||
// ── Config button (bottom-right) ──
|
||||
|
||||
function addConfigButton() {
|
||||
|
|
|
|||
|
|
@ -1,58 +1,7 @@
|
|||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// RE TIMER — Random Encounter countdown
|
||||
// RE TIMER — REMOVED (Handled by HVUtils / HVUT)
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
let reTimerEl = null;
|
||||
function setupRETimer() {}
|
||||
function updateRETimer() {}
|
||||
|
||||
function setupRETimer() {
|
||||
if (!CFG.reTimer) return;
|
||||
if (document.getElementById('hv-re-timer')) return;
|
||||
|
||||
reTimerEl = document.createElement('div');
|
||||
reTimerEl.id = 'hv-re-timer';
|
||||
reTimerEl.style.cssText = css({
|
||||
position: 'fixed',
|
||||
top: '2px',
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
zIndex: '99999',
|
||||
background: '#1a1a2e',
|
||||
color: '#0f0',
|
||||
padding: '2px 10px',
|
||||
borderRadius: '0 0 6px 6px',
|
||||
fontSize: '10px',
|
||||
fontFamily: 'monospace',
|
||||
opacity: '0.9',
|
||||
});
|
||||
reTimerEl.textContent = 'RE: --:--';
|
||||
document.body.appendChild(reTimerEl);
|
||||
updateRETimer();
|
||||
setInterval(updateRETimer, 30000);
|
||||
}
|
||||
|
||||
function updateRETimer() {
|
||||
if (!reTimerEl) return;
|
||||
const now = Date.now();
|
||||
const dt = document.body.textContent || '';
|
||||
|
||||
// Detect dawn
|
||||
if (dt.includes('Dawn of a New Day') || dt.includes('A new day dawns')) {
|
||||
try { localStorage[SP + 'lastDawn'] = JSON.stringify(now); } catch (e) {}
|
||||
}
|
||||
|
||||
const ld = JSON.parse(localStorage[SP + 'lastDawn'] || now);
|
||||
const ms = (now - ld) / 60000;
|
||||
const nr = 30 - (ms % 30);
|
||||
|
||||
if (nr < 1) {
|
||||
reTimerEl.textContent = '⚡ RE READY!';
|
||||
reTimerEl.style.color = '#f00';
|
||||
reTimerEl.style.fontWeight = 'bold';
|
||||
} else {
|
||||
const mn = Math.floor(nr);
|
||||
const sc = Math.floor((nr - mn) * 60);
|
||||
reTimerEl.textContent = `RE: ${String(mn).padStart(2, '0')}:${String(sc).padStart(2, '0')}`;
|
||||
reTimerEl.style.color = '#0f0';
|
||||
reTimerEl.style.fontWeight = 'normal';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,41 +51,29 @@ function openSettings() {
|
|||
|
||||
p.innerHTML = `
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
|
||||
<b style="color:#fdcb00;font-size:14px">⚙ HV Unified v${VERSION}</b>
|
||||
<b style="color:#fdcb00;font-size:14px">⚙ HV Unified v${VERSION} (Bridge & Advisor)</b>
|
||||
<button id="hv-settings-close" style="background:#d50c2d;color:#fff;border:none;border-radius:4px;padding:4px 10px;cursor:pointer">✕</button>
|
||||
</div>
|
||||
<div style="border-top:1px solid #444;margin:8px 0"></div>
|
||||
<b style="color:#0f0">Battle</b>
|
||||
${mkTog('Auto-buff (Haste, Protection, etc.)', 'autoBuff')}
|
||||
${mkTog('Auto-debuff (Imperil, Weaken)', 'autoDebuff')}
|
||||
${mkTog('Auto-cure when HP low', 'autoCure')}
|
||||
${mkTog('Auto Spirit Stance', 'autoSpirit')}
|
||||
${mkTog('Hover-to-attack mode', 'hoverEnabled')}
|
||||
<b style="color:#0f0">Strategy Advisory</b>
|
||||
${mkTog('Auto-buff recommendations', 'autoBuff')}
|
||||
${mkTog('Auto-debuff recommendations', 'autoDebuff')}
|
||||
${mkTog('Auto-cure recommendations', 'autoCure')}
|
||||
${mkTog('Auto-difficulty suggestion', 'autoDifficulty')}
|
||||
${mkTog('Use attack spells', 'useAttackSpells')}
|
||||
${mkTog('Use attack spells in advice', 'useAttackSpells')}
|
||||
${mkNum('Cure HP threshold', 'cureHP')}
|
||||
${mkNum('Item HP threshold', 'cureItemHP')}
|
||||
${mkNum('Mana gem MP threshold', 'manaGemMP')}
|
||||
${mkNum('Mana potion MP threshold', 'manaPotionMP')}
|
||||
${mkNum('Spirit Stance OC%', 'spiritStanceOC', 5)}
|
||||
${mkNum('Skill OC% threshold', 'skillOC', 5)}
|
||||
<div style="border-top:1px solid #444;margin:8px 0"></div>
|
||||
<b style="color:#0f0">Out of Battle</b>
|
||||
${mkTog('Equip quick sell buttons', 'autoSell')}
|
||||
${mkTog('Bulk Shrine buttons', 'bulkShrine')}
|
||||
${mkTog('RE Timer', 'reTimer')}
|
||||
<b style="color:#0f0">Out of Battle & Guidance</b>
|
||||
${mkTog('Training queue info', 'trainingQueue')}
|
||||
${mkTog('Advisor panel', 'showGuidance')}
|
||||
${mkTog('Equip KEEP/SELL tags', 'showEquipAdvice')}
|
||||
<div style="border-top:1px solid #444;margin:8px 0"></div>
|
||||
<b style="color:#0f0">UI</b>
|
||||
${mkTog('Cooldown timers', 'showCooldowns')}
|
||||
${mkTog('Monster HP numbers', 'showMonsterHP')}
|
||||
${mkTog('Buff duration counters', 'showDurations')}
|
||||
${mkTog('Alert colours', 'alertColours')}
|
||||
${mkTog('Monster numbers', 'showMonsterNumbers')}
|
||||
${mkTog('Config button', 'cfgButton')}
|
||||
<div style="margin-top:12px;text-align:center;color:#666;font-size:10px">
|
||||
Changes apply immediately. Press <b>,</b> in battle to open settings.
|
||||
Changes apply immediately. Press <b>,</b> to open settings.
|
||||
</div>`;
|
||||
|
||||
document.body.appendChild(p);
|
||||
|
|
@ -103,3 +91,4 @@ function openSettings() {
|
|||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -444,28 +444,12 @@ function strategyNovice() {
|
|||
return { type: 'spell', name: buffCandidates[0].spell, selfTarget: true, _reason: STATE.channeling ? 'chBuff' : 'buff' };
|
||||
}
|
||||
|
||||
// 4. Mana/spirit items — AFTER buffs (Novice)
|
||||
// 4. Mana items — AFTER buffs (Novice)
|
||||
if (STATE.mp < CFG.manaGemMP) {
|
||||
const gem = hasUsableGem('mana');
|
||||
if (gem) return { type: 'item', id: gem.id, selfTarget: true };
|
||||
}
|
||||
// Spirit gem (P-slot) — auto-consume when available
|
||||
const pGem = document.getElementById('ikey_p');
|
||||
const pGemId = pGem ? (pGem.getAttribute('onmouseover') || '').match(/set_infopane_item\((\d+)\)/) : null;
|
||||
if (pGemId && pGemId[1] === '10007') {
|
||||
if (hasUsableGem('spirit')) return { type: 'item', id: 'p', selfTarget: true };
|
||||
}
|
||||
// Spirit draughts/potions — only when SP drops below threshold
|
||||
if (STATE.sp < CFG.spiritPotionSP) {
|
||||
// Check non-P-slot spirit items only
|
||||
for (const [slot, info] of Object.entries(STATE.itemsKnown)) {
|
||||
if (slot === 'p') continue;
|
||||
const def = ITEMS[parseInt(info)];
|
||||
if (def && def.t === 'spirit') {
|
||||
return { type: 'item', id: slot, selfTarget: true };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 5. Weapon skills — these are better than basic attacks
|
||||
const skillOC = CFG.skillOC || 80;
|
||||
|
|
@ -529,20 +513,6 @@ function strategyAdept() {
|
|||
if (gem && !STATE.channeling && !STATE._mysticUsed)
|
||||
return { type: 'item', id: gem.id, selfTarget: true, _reason: 'mystic' };
|
||||
|
||||
// 0b. Spark of Life SP safety
|
||||
if (STATE.sp < 0.55) {
|
||||
for (const [slot, info] of Object.entries(STATE.itemsKnown)) {
|
||||
if (slot === 'p') continue;
|
||||
const def = ITEMS[parseInt(info)];
|
||||
if (def && def.t === 'spirit') return { type: 'item', id: slot, selfTarget: true, _reason: `sp${(STATE.sp*100).toFixed(0)}` };
|
||||
}
|
||||
}
|
||||
|
||||
// 0c. Spirit Stance safety auto-disable
|
||||
if (STATE.spiritStance && STATE.sp < 0.60) {
|
||||
return { type: 'toggle_spirit', _reason: 'sp_safety' };
|
||||
}
|
||||
|
||||
// 1. Health items — safe to use early
|
||||
if (STATE.hp < CFG.cureItemHP) {
|
||||
const gem = hasUsableGem('heal');
|
||||
|
|
@ -555,15 +525,6 @@ function strategyAdept() {
|
|||
if (c) return { type: 'spell', name: c, selfTarget: true };
|
||||
}
|
||||
|
||||
// 2b. Spirit items — Spark of Life costs 50% SP. Keep SP above 55% so
|
||||
// Spark can actually save us.
|
||||
if (STATE.sp < 0.55) {
|
||||
for (const [slot, info] of Object.entries(STATE.itemsKnown)) {
|
||||
if (slot === 'p') continue;
|
||||
const def = ITEMS[parseInt(info)];
|
||||
if (def && def.t === 'spirit') return { type: 'item', id: slot, selfTarget: true };
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Buffs
|
||||
// On first round, cast cheapest-first to proc channeling
|
||||
|
|
@ -592,25 +553,12 @@ function strategyAdept() {
|
|||
return { type: 'spell', name: buffCandidates[0].spell, selfTarget: true, _reason: STATE.channeling ? 'chBuff' : 'buff' };
|
||||
}
|
||||
|
||||
// 4. Mana/spirit items — AFTER buffs (Adept)
|
||||
// 4. Mana items — AFTER buffs (Adept)
|
||||
if (STATE.mp < CFG.manaGemMP) {
|
||||
const gem = hasUsableGem('mana');
|
||||
if (gem) return { type: 'item', id: gem.id, selfTarget: true };
|
||||
}
|
||||
// Spirit gem (P-slot) — auto-consume when available
|
||||
const apGem = document.getElementById('ikey_p');
|
||||
const apGemId = apGem ? (apGem.getAttribute('onmouseover') || '').match(/set_infopane_item\((\d+)\)/) : null;
|
||||
if (apGemId && apGemId[1] === '10007') {
|
||||
if (hasUsableGem('spirit')) return { type: 'item', id: 'p', selfTarget: true };
|
||||
}
|
||||
// Spirit draughts/potions — only at SP < threshold
|
||||
if (STATE.sp < CFG.spiritPotionSP) {
|
||||
for (const [slot, info] of Object.entries(STATE.itemsKnown)) {
|
||||
if (slot === 'p') continue;
|
||||
const def = ITEMS[parseInt(info)];
|
||||
if (def && def.t === 'spirit') return { type: 'item', id: slot, selfTarget: true };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 4. Debuffs — conservative: Imperil on bosses/rares or 5+ mobs
|
||||
// Weaken only on high-threat or bosses. Estoc handles <5.
|
||||
|
|
@ -707,45 +655,6 @@ function strategyVeteran() {
|
|||
if (gem && !STATE.channeling && !STATE._mysticUsed)
|
||||
return { type: 'item', id: gem.id, selfTarget: true, _reason: 'mystic' };
|
||||
|
||||
// 0b. Spark of Life SP safety — Spark costs 50% SP when triggered.
|
||||
// If SP < 60%, consume a spirit item before anything else.
|
||||
if (STATE.sp < 0.60) {
|
||||
for (const [slot, info] of Object.entries(STATE.itemsKnown)) {
|
||||
if (slot === 'p') continue;
|
||||
const def = ITEMS[parseInt(info)];
|
||||
if (def && def.t === 'spirit') return { type: 'item', id: slot, selfTarget: true, _reason: `sp${(STATE.sp*100).toFixed(0)}` };
|
||||
}
|
||||
}
|
||||
|
||||
// 0c. Spirit Stance safety auto-disable — if left on, drains 10% SP/turn
|
||||
if (STATE.spiritStance && STATE.sp < 0.60) {
|
||||
return { type: 'toggle_spirit', _reason: 'sp_safety' };
|
||||
}
|
||||
|
||||
// 0d. Spirit Stance for burst clear — toggle on for 7+ mob, off after one skill
|
||||
// Gated by CFG.autoSpirit (default OFF — manual toggle only)
|
||||
const is2H = STATE.skillsKnown.includes('Great Cleave') || STATE.skillsKnown.includes('Rending Blow');
|
||||
if (CFG.autoSpirit && is2H && STATE.monsters && STATE.monsters.length >= 7 && STATE.oc >= (CFG.skillOC || 60)) {
|
||||
if (STATE._spiritForSkill >= 1 && STATE.spiritStance) {
|
||||
if (STATE._spiritForSkill === 1) {
|
||||
STATE._spiritForSkill = 2;
|
||||
} else if (STATE._spiritForSkill === 2) {
|
||||
STATE._spiritForSkill = 0;
|
||||
return { type: 'toggle_spirit', _reason: 'spirit_off_7' };
|
||||
}
|
||||
} else if (!STATE.spiritStance && STATE._spiritForSkill === 0) {
|
||||
STATE._spiritForSkill = 1;
|
||||
return { type: 'toggle_spirit', _reason: 'spirit_on_7' };
|
||||
}
|
||||
}
|
||||
// Toggle-off guard: if flag says skill was cast, toggle off even if mob count dropped
|
||||
if (STATE._spiritForSkill === 2 && STATE.spiritStance) {
|
||||
STATE._spiritForSkill = 0;
|
||||
return { type: 'toggle_spirit', _reason: 'spirit_off_7' };
|
||||
}
|
||||
// Reset flag if spirit got turned off externally
|
||||
if (!STATE.spiritStance) STATE._spiritForSkill = 0;
|
||||
|
||||
// 0e. HIGH-THREAT WEAPON SKILLS — only when 3+ enemies
|
||||
// Skip if ≤2 enemies — basic attacks build OC for next wave instead.
|
||||
const SINGLE_THREAT = Math.round(120 * 90 / 100);
|
||||
|
|
@ -770,16 +679,6 @@ function strategyVeteran() {
|
|||
}
|
||||
}
|
||||
|
||||
// 2b. Spirit items — Spark of Life costs 50% SP. Keep SP above 55% so
|
||||
// Spark can actually save us. Use spirit draughts/potions proactively.
|
||||
if (STATE.sp < 0.55) {
|
||||
for (const [slot, info] of Object.entries(STATE.itemsKnown)) {
|
||||
if (slot === 'p') continue;
|
||||
const def = ITEMS[parseInt(info)];
|
||||
if (def && def.t === 'spirit') return { type: 'item', id: slot, selfTarget: true, _reason: `sp${(STATE.sp*100).toFixed(0)}` };
|
||||
}
|
||||
}
|
||||
|
||||
// 2c. OC-building priority: when 5+ monsters, OC < 60, and AoE skills ready,
|
||||
// skip debuffs/non-essential buffs and basic attack to build OC fast.
|
||||
const needsOC = STATE.monsters && STATE.monsters.length >= 4
|
||||
|
|
@ -788,26 +687,21 @@ function strategyVeteran() {
|
|||
&& document.querySelector(`.btqs[onmouseover*="set_infopane_spell('Rending Blow'"], .btsd[onmouseover*="set_infopane_spell('Rending Blow'"]`)
|
||||
?.hasAttribute('onclick');
|
||||
if (needsOC) {
|
||||
// No hp/mp guard — if Spark procced, hp=0 but we still need OC
|
||||
const t = hasHighThreat(THREAT_PX) ? findDangerousMonster() : findOptimalDominoTarget();
|
||||
if (t >= 0 && STATE.monsters[t] && STATE.monsters[t].hasAttribute('onclick'))
|
||||
return { type: 'attack', target: t, _reason: 'oc_build' };
|
||||
}
|
||||
|
||||
// 3. Buffs
|
||||
// On first round, cast cheapest-first to proc channeling
|
||||
if (CFG.autoBuff) {
|
||||
{
|
||||
const ib = castInitialBuffs();
|
||||
if (ib) return ib;
|
||||
}
|
||||
// If channeling is active, prioritize most expensive buff for best value
|
||||
if (STATE.channeling && STATE.mp > 0.10) {
|
||||
const ch = findBestChannelingTarget();
|
||||
if (ch) return ch;
|
||||
}
|
||||
// Collect available buff candidates and sort by cost
|
||||
// No channeling → cheapest first (proc chance), channeling → most expensive (MP value)
|
||||
const buffCandidates = [];
|
||||
for (const b of BUFF_PRIORITY) {
|
||||
if (shouldBuff(b.icon, b.minTurns) && STATE.mp > 0.10 && hasReadySpell(b.spell)) {
|
||||
|
|
@ -822,35 +716,19 @@ function strategyVeteran() {
|
|||
}
|
||||
}
|
||||
|
||||
// 4. Mana/spirit items — AFTER buffs (Novice)
|
||||
// 4. Mana items
|
||||
if (STATE.mp < CFG.manaGemMP) {
|
||||
const gem = hasUsableGem('mana');
|
||||
if (gem) return { type: 'item', id: gem.id, selfTarget: true };
|
||||
}
|
||||
// Spirit gem (P-slot) — use immediately
|
||||
const vpGem = document.getElementById('ikey_p');
|
||||
const vpGemId = vpGem ? (vpGem.getAttribute('onmouseover') || '').match(/set_infopane_item\((\d+)\)/) : null;
|
||||
if (vpGemId && vpGemId[1] === '10007') {
|
||||
if (hasUsableGem('spirit')) return { type: 'item', id: 'p', selfTarget: true };
|
||||
}
|
||||
// Spirit draughts/potions — only at SP < threshold
|
||||
if (STATE.sp < CFG.spiritPotionSP) {
|
||||
for (const [slot, info] of Object.entries(STATE.itemsKnown)) {
|
||||
if (slot === 'p') continue;
|
||||
const def = ITEMS[parseInt(info)];
|
||||
if (def && def.t === 'spirit') return { type: 'item', id: slot, selfTarget: true };
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Debuffs — only when NOT in spirit burst mode (flag >= 1 means skill pending)
|
||||
if (STATE._spiritForSkill === 0 && CFG.autoDebuff && STATE.mp > 0.35) {
|
||||
// Imperil: bosses only, or 5+ mobs in Grindfest (Estoc handles <5)
|
||||
// 4. Debuffs
|
||||
if (CFG.autoDebuff && STATE.mp > 0.35) {
|
||||
if (hasReadySpell('Imperil') && (anyRareMonster() || STATE.monsters.length >= 5)) {
|
||||
const b = findStrongestMonster();
|
||||
if (b >= 0 && !checkMonsterDebuff(b, 'imperil'))
|
||||
return { type: 'spell', name: 'Imperil', target: b, _reason: 'imperil' };
|
||||
}
|
||||
// Weaken: only on monsters about to use special attacks (high SP) or bosses
|
||||
if (hasReadySpell('Weaken') && (anyRareMonster() || hasHighThreat(THREAT_PX))) {
|
||||
const b = findDangerousMonster();
|
||||
if (b >= 0 && !checkMonsterDebuff(b, 'weaken'))
|
||||
|
|
@ -859,20 +737,16 @@ function strategyVeteran() {
|
|||
}
|
||||
|
||||
// 5. Weapon skills — prioritize high-threat monsters (high SP/OC bar)
|
||||
// If 2+ enemies have >85% SP bar, they're about to special — emergency
|
||||
const isHighThreat = hasHighThreat(THREAT_PX);
|
||||
if (isHighThreat) console.log('%c[HV] ⚠ HIGH THREAT: 2+ monsters with SP >85%', 'color:#f44');
|
||||
|
||||
const askillOC3 = CFG.skillOC || 80;
|
||||
if (STATE.oc >= askillOC3 || (isHighThreat && STATE.monsters.length >= 3)) {
|
||||
if (isHighThreat && STATE.oc < askillOC3) console.log('%c[HV] ⚠ emergency: using skill at OC=' + STATE.oc.toFixed(0), 'color:#f80');
|
||||
// Emergency: 2+ monsters about to special — spend OC to kill them fast
|
||||
// Rending Blow: AoE armor pen vs 3+ enemies. Target center monster for max 5-target splash.
|
||||
if (STATE.monsters.length >= 3 && STATE.skillsKnown.includes('Rending Blow')) {
|
||||
const t = isHighThreat ? findDangerousMonster() : findOptimalDominoTarget();
|
||||
if (t >= 0) return { type: 'skill', name: 'Rending Blow', target: t, _reason: isHighThreat ? 'threat' : 'oc' };
|
||||
}
|
||||
// Shatter Strike: AoE stun vs 3+ enemies with armor break
|
||||
if (STATE.monsters.length >= 3 && STATE.skillsKnown.includes('Shatter Strike')) {
|
||||
const hasArmorBreak = STATE.monsters.some((m, i) => checkMonsterDebuff(i, 'penetrated_armor'));
|
||||
if (hasArmorBreak) {
|
||||
|
|
@ -880,12 +754,10 @@ function strategyVeteran() {
|
|||
if (t >= 0) return { type: 'skill', name: 'Shatter Strike', target: t, _reason: 'stun' };
|
||||
}
|
||||
}
|
||||
// Great Cleave: boss/rare fights or single-target threat
|
||||
if (anyRareMonster() && STATE.skillsKnown.includes('Great Cleave')) {
|
||||
const t = isHighThreat ? findDangerousMonster() : findStrongestMonster();
|
||||
if (t >= 0) return { type: 'skill', name: 'Great Cleave', target: t, _reason: isHighThreat ? 'threat' : 'boss' };
|
||||
}
|
||||
// If high threat and 3+ enemies, use single-target skills on the most dangerous
|
||||
if (isHighThreat && STATE.oc >= (CFG.skillOC || 60) && STATE.monsters.length >= 3) {
|
||||
const ps = ['Frenzied Blows', 'Skyward Sword', 'Iris Strike', 'Merciful Blow',
|
||||
'Vital Strike', 'Shield Bash', 'Concussive Strike'];
|
||||
|
|
@ -898,6 +770,7 @@ function strategyVeteran() {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
// 5. Spirit gem auto-consume
|
||||
const spGem = document.getElementById('ikey_p');
|
||||
const spGemId = spGem ? (spGem.getAttribute('onmouseover') || '').match(/set_infopane_item\((\d+)\)/) : null;
|
||||
|
|
|
|||
|
|
@ -1,132 +1,12 @@
|
|||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// UI OVERLAYS — cooldowns, durations, alerts, monster numbers, monster HP
|
||||
// UI OVERLAYS — REMOVED (Handled by Monsterbation / MB)
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
function updateCooldownDisplay() {
|
||||
if (!CFG.showCooldowns) return;
|
||||
$$('.hv-cd-overlay').forEach(e => e.remove());
|
||||
$$('.btqb').forEach(el => {
|
||||
const img = el.querySelector('img');
|
||||
if (!img) return;
|
||||
const src = (img.src || '').toLowerCase();
|
||||
for (const [k, v] of Object.entries(STATE.cooldowns)) {
|
||||
if (v <= 0) continue;
|
||||
if (src.includes(k.toLowerCase()) || src.includes(k.replace(/_/g, ''))) {
|
||||
const o = document.createElement('div');
|
||||
o.className = 'hv-cd-overlay';
|
||||
o.style.cssText = 'position:absolute;top:0;right:0;background:rgba(0,0,0,0.7);color:#f00;font-size:9px;font-weight:bold;padding:0 2px;border-radius:2px;pointer-events:none';
|
||||
o.textContent = v;
|
||||
el.style.position = 'relative';
|
||||
el.appendChild(o);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
function updateCooldownDisplay() {}
|
||||
function updateDurationDisplay() {}
|
||||
function updateAlertColours() {}
|
||||
function addMonsterNumbers() {}
|
||||
function updateMonsterHPDisplay() {}
|
||||
function saveMonsterData() {}
|
||||
function refreshUI() {}
|
||||
|
||||
function updateDurationDisplay() {
|
||||
if (!CFG.showDurations) return;
|
||||
$$('.hv-dur-overlay').forEach(e => e.remove());
|
||||
const pane = document.getElementById('pane_effects');
|
||||
if (!pane) return;
|
||||
$$('img[onmouseover]', pane).forEach(img => {
|
||||
const omo = img.getAttribute('onmouseover') || '';
|
||||
const dur = omo.match(/(\d+)\s*turns?\s*rem/);
|
||||
if (!dur) return;
|
||||
const t = parseInt(dur[1]);
|
||||
const st = omo.match(/(\d+)\s*stacks?/i);
|
||||
const o = document.createElement('div');
|
||||
o.className = 'hv-dur-overlay';
|
||||
o.style.cssText = `position:absolute;bottom:-2px;left:50%;transform:translateX(-50%);background:${t < 3 ? '#f44' : '#44f'};color:#fff;font-size:8px;font-weight:bold;padding:0 2px;border-radius:2px;pointer-events:none;z-index:1`;
|
||||
o.textContent = st ? `${t}x${parseInt(st[1])}` : t;
|
||||
img.parentElement.style.position = 'relative';
|
||||
img.parentElement.appendChild(o);
|
||||
});
|
||||
}
|
||||
|
||||
function updateAlertColours() {
|
||||
if (!CFG.alertColours) return;
|
||||
let bg = '#EDEBDF';
|
||||
STATE.interruptAlert = false;
|
||||
|
||||
if (!$('img[src$="bar_dgreen.png"]') && $('img[src$="fallenshield.png"]')) {
|
||||
bg = 'magenta';
|
||||
STATE.interruptAlert = true;
|
||||
} else if (STATE.hp < 0.25) {
|
||||
bg = '#ff4488';
|
||||
STATE.interruptAlert = true;
|
||||
} else if (STATE.mp < 0.15) {
|
||||
bg = '#4444aa';
|
||||
} else if (Object.values(STATE.buffs).some(d => d < 2 && d > 0)) {
|
||||
bg = '#88ccff';
|
||||
}
|
||||
|
||||
const v = document.getElementById('pane_vitals');
|
||||
if (v) v.style.background = bg;
|
||||
}
|
||||
|
||||
function addMonsterNumbers() {
|
||||
if (!CFG.showMonsterNumbers) return;
|
||||
STATE.monsters.forEach((m, i) => {
|
||||
if (!m || !m.querySelector || m.querySelector('.hv-monster-num')) return;
|
||||
const b = m.querySelector('.btm1');
|
||||
if (!b) return;
|
||||
const s = document.createElement('span');
|
||||
s.className = 'hv-monster-num';
|
||||
s.style.cssText = 'font-weight:bold;margin-right:3px;color:#999';
|
||||
s.textContent = (i + 1);
|
||||
b.insertBefore(s, b.firstChild);
|
||||
});
|
||||
}
|
||||
|
||||
function updateMonsterHPDisplay() {
|
||||
if (!CFG.showMonsterHP) return;
|
||||
STATE.monsters.forEach((m, i) => {
|
||||
if (!m || !m.hasAttribute || !m.hasAttribute('onclick')) return;
|
||||
const n = m.querySelector('.btm1');
|
||||
if (!n) return;
|
||||
const nm = (n.textContent || '').replace(/^\d+\s*/, '').trim();
|
||||
const md = STATE.monsterData[nm];
|
||||
if (!md || !md.hp || md.seen < 3) return;
|
||||
const hpBar = m.querySelector('img[src$="nbargreen.png"]');
|
||||
if (!hpBar) return;
|
||||
const r = clamp(parseInt(hpBar.style.width || '') / 120, 0.01, 1);
|
||||
const txt = `HP: ${Math.max(1, Math.round(r * md.hp)).toLocaleString()} / ${md.hp.toLocaleString()}`;
|
||||
const ex = m.querySelector('.hv-monster-hp');
|
||||
if (ex) { ex.textContent = txt; return; }
|
||||
const d = document.createElement('div');
|
||||
d.className = 'hv-monster-hp';
|
||||
d.style.cssText = 'display:inline-block;position:relative;left:204px;top:-20px;font-size:9px;color:#888';
|
||||
d.textContent = txt;
|
||||
m.appendChild(d);
|
||||
});
|
||||
}
|
||||
|
||||
function saveMonsterData() {
|
||||
STATE.monsters.forEach((m, i) => {
|
||||
if (!m || !m.hasAttribute || !m.hasAttribute('onclick')) return;
|
||||
const n = m.querySelector('.btm1');
|
||||
if (!n) return;
|
||||
const nm = (n.textContent || '').replace(/^\d+\s*/, '').trim();
|
||||
if (!nm) return;
|
||||
const hpBar = m.querySelector('img[src$="nbargreen.png"]');
|
||||
if (hpBar) {
|
||||
const r = clamp(parseInt(hpBar.style.width || '') / 120, 0.01, 1);
|
||||
if (!STATE.monsterData[nm]) STATE.monsterData[nm] = { hp: 0, seen: 0 };
|
||||
STATE.monsterData[nm].hp = Math.max(
|
||||
STATE.monsterData[nm].hp,
|
||||
Math.round(STATE.monsterData[nm].hp * 0.7 + 1000 / r * 0.3)
|
||||
);
|
||||
STATE.monsterData[nm].seen++;
|
||||
}
|
||||
});
|
||||
try { localStorage[SP + 'monsters'] = JSON.stringify(STATE.monsterData); } catch (e) {}
|
||||
}
|
||||
|
||||
function refreshUI() {
|
||||
if (CFG.showDurations) updateDurationDisplay();
|
||||
if (CFG.showCooldowns) updateCooldownDisplay();
|
||||
if (CFG.alertColours) updateAlertColours();
|
||||
if (CFG.showMonsterNumbers) addMonsterNumbers();
|
||||
if (CFG.showMonsterHP) updateMonsterHPDisplay();
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue