feat: stream live equipment cantrip states

This commit is contained in:
Erik 2026-03-13 08:59:43 +01:00
parent 0cb8e2f75a
commit a7e2d4d404
2 changed files with 90 additions and 103 deletions

View file

@ -309,6 +309,7 @@ const chatWindows = {};
const statsWindows = {};
// Keep track of open inventory windows: character_name -> DOM element
const inventoryWindows = {};
const equipmentCantripStates = {};
/**
* ---------- Application Constants -----------------------------
@ -1491,29 +1492,35 @@ function renderInventoryState(state) {
function getManaTrackedItems(state) {
if (!state || !state.items) return [];
const overlayItems = equipmentCantripStates[state.characterName]?.items;
const overlayMap = new Map();
if (Array.isArray(overlayItems)) {
overlayItems.forEach(item => {
if (item && item.item_id != null) {
overlayMap.set(Number(item.item_id), item);
}
});
}
const snapshotMs = Date.now();
return state.items
.filter(item => (item.current_wielded_location || 0) > 0)
.filter(item => !isAetheriaItem(item))
.filter(item => {
const spellInfo = item.spells;
const hasSpellData =
(Array.isArray(spellInfo?.spells) && spellInfo.spells.length > 0) ||
(Array.isArray(spellInfo?.active_spells) && spellInfo.active_spells.length > 0) ||
Number(spellInfo?.spell_count || 0) > 0 ||
Number(spellInfo?.active_spell_count || 0) > 0;
return (
item.is_mana_tracked ||
item.current_mana !== undefined ||
item.max_mana !== undefined ||
item.spellcraft !== undefined ||
hasSpellData
);
})
.filter(item => overlayMap.has(Number(item.item_id)))
.map(item => {
const result = { ...item };
result.mana_state = deriveManaStateFromCharacterStats(result, state.characterName);
const overlay = overlayMap.get(Number(item.item_id)) || {};
result.mana_state = overlay.state || 'unknown';
if (overlay.current_mana !== undefined && overlay.current_mana !== null) {
result.current_mana = overlay.current_mana;
}
if (overlay.max_mana !== undefined && overlay.max_mana !== null) {
result.max_mana = overlay.max_mana;
}
if (overlay.mana_time_remaining_seconds !== undefined && overlay.mana_time_remaining_seconds !== null) {
result.mana_time_remaining_seconds = overlay.mana_time_remaining_seconds;
result.mana_snapshot_utc = equipmentCantripStates[state.characterName]?.timestamp || null;
}
if (result.mana_time_remaining_seconds !== undefined && result.mana_time_remaining_seconds !== null) {
const snapshotUtc = result.mana_snapshot_utc ? Date.parse(result.mana_snapshot_utc) : NaN;
if (!Number.isNaN(snapshotUtc)) {
@ -1538,91 +1545,6 @@ function getManaTrackedItems(state) {
});
}
function isActionableManaSpell(spell) {
if (!spell) return false;
const spellName = (spell.name || '').toLowerCase();
if (!spellName || spellName.startsWith('unknown_spell_')) return false;
if (spellName.startsWith('cantrip portal send') || spellName.startsWith('cantrip portal recall')) return false;
if (spellName.startsWith('incantation of ') || spellName.startsWith('aura of incantation ')) return true;
if (
spellName.startsWith('feeble ') ||
spellName.startsWith('minor ') ||
spellName.startsWith('lesser ') ||
spellName.startsWith('moderate ') ||
spellName.startsWith('inner ') ||
spellName.startsWith('major ') ||
spellName.startsWith('epic ') ||
spellName.startsWith('legendary ') ||
spellName.startsWith('prodigal ')
) return true;
const duration = spell.duration;
return duration !== undefined && duration !== null && Number(duration) <= 0;
}
function doesSpellMatch(activeSpell, spell) {
if (!activeSpell || !spell) return false;
if (activeSpell.id === spell.id) return true;
if (activeSpell.family == null || spell.family == null) return false;
if (Number(activeSpell.family) !== Number(spell.family)) return false;
if (activeSpell.difficulty == null || spell.difficulty == null) return true;
return Number(activeSpell.difficulty) >= Number(spell.difficulty);
}
function isHandEquippedItem(item) {
const mask = Number(item?.current_wielded_location || 0);
return (
mask === 1048576 ||
mask === 2097152 ||
mask === 4194304 ||
mask === 16777216 ||
mask === 33554432
);
}
function isAetheriaItem(item) {
const mask = Number(item?.current_wielded_location || 0);
return mask === 268435456 || mask === 536870912 || mask === 1073741824;
}
function deriveManaStateFromCharacterStats(item, characterName) {
const stats = characterStats[characterName];
const activeEnchantments = stats?.active_item_enchantments;
const itemActiveSpells = Array.isArray(item.spells?.active_spells) ? item.spells.active_spells : [];
if ((!Array.isArray(activeEnchantments) || activeEnchantments.length === 0) && itemActiveSpells.length === 0) {
return item.mana_state;
}
if (item.current_mana === undefined || item.current_mana === null) return item.mana_state;
if (item.current_mana <= 0) return 'not_active';
const translatedSpells = Array.isArray(item.spells?.spells) ? item.spells.spells : [];
const actionableSpells = translatedSpells.filter(isActionableManaSpell);
if (actionableSpells.length === 0) return item.mana_state;
const allMatchedOnItem = actionableSpells.every(spell => itemActiveSpells.some(activeSpell => doesSpellMatch(activeSpell, spell)));
if (allMatchedOnItem) {
return 'active';
}
if (!Array.isArray(activeEnchantments) || activeEnchantments.length === 0) {
if (isHandEquippedItem(item)) {
return 'not_active';
}
return allMatchedOnItem ? 'active' : item.mana_state;
}
const allMatched = actionableSpells.every(spell => {
if (!spell) return false;
return activeEnchantments.some(activeSpell => doesSpellMatch(activeSpell, spell));
});
if (allMatched) return 'active';
if (item.mana_state === 'active') return 'not_active';
return item.mana_state || 'not_active';
}
function formatManaRemaining(totalSeconds) {
if (totalSeconds === null || totalSeconds === undefined) return '--';
const safeSeconds = Math.max(0, Math.floor(totalSeconds));
@ -1883,6 +1805,20 @@ function showInventoryWindow(name) {
.catch(() => {});
}
if (!equipmentCantripStates[name]) {
fetch(`${API_BASE}/equipment-cantrip-state/${encodeURIComponent(name)}`)
.then(r => r.ok ? r.json() : null)
.then(data => {
if (data && !data.error) {
equipmentCantripStates[name] = data;
if (win._inventoryState) {
renderInventoryState(win._inventoryState);
}
}
})
.catch(() => {});
}
debugLog('Inventory window created for:', name);
}
@ -3102,6 +3038,11 @@ function initWebSocket() {
if (inventoryWindows[msg.character_name] && inventoryWindows[msg.character_name]._inventoryState) {
renderInventoryState(inventoryWindows[msg.character_name]._inventoryState);
}
} else if (msg.type === 'equipment_cantrip_state') {
equipmentCantripStates[msg.character_name] = msg;
if (inventoryWindows[msg.character_name] && inventoryWindows[msg.character_name]._inventoryState) {
renderInventoryState(inventoryWindows[msg.character_name]._inventoryState);
}
} else if (msg.type === 'inventory_delta') {
updateInventoryLive(msg);
} else if (msg.type === 'server_status') {