feat: add mana tracker panel to inventory

Derive equipped item mana state and time-remaining data in inventory-service, then render a Mana panel inside the inventory window with live icon, state, mana, and countdown display.
This commit is contained in:
Erik 2026-03-11 20:02:52 +01:00
parent 4e73a5d07d
commit 0e8186b8e5
4 changed files with 3930 additions and 2283 deletions

View file

@ -1484,6 +1484,108 @@ function renderInventoryState(state) {
}
state.itemGrid.appendChild(cell);
}
renderInventoryManaPanel(state);
}
function getManaTrackedItems(state) {
if (!state || !state.items) return [];
const snapshotMs = Date.now();
return state.items
.filter(item => (item.current_wielded_location || 0) > 0)
.filter(item => item.is_mana_tracked || item.current_mana !== undefined || item.max_mana !== undefined || item.spellcraft !== undefined)
.map(item => {
const result = { ...item };
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)) {
const elapsed = Math.max(0, Math.floor((snapshotMs - snapshotUtc) / 1000));
result.live_mana_time_remaining_seconds = Math.max((result.mana_time_remaining_seconds || 0) - elapsed, 0);
} else {
result.live_mana_time_remaining_seconds = result.mana_time_remaining_seconds;
}
} else {
result.live_mana_time_remaining_seconds = null;
}
return result;
})
.sort((a, b) => {
const aRemaining = a.live_mana_time_remaining_seconds;
const bRemaining = b.live_mana_time_remaining_seconds;
if (aRemaining === null && bRemaining === null) return (a.name || '').localeCompare(b.name || '');
if (aRemaining === null) return 1;
if (bRemaining === null) return -1;
if (aRemaining !== bRemaining) return aRemaining - bRemaining;
return (a.name || '').localeCompare(b.name || '');
});
}
function formatManaRemaining(totalSeconds) {
if (totalSeconds === null || totalSeconds === undefined) return '--';
const safeSeconds = Math.max(0, Math.floor(totalSeconds));
const hours = Math.floor(safeSeconds / 3600);
const minutes = Math.floor((safeSeconds % 3600) / 60);
return `${hours}h${String(minutes).padStart(2, '0')}m`;
}
function renderInventoryManaPanel(state) {
if (!state || !state.manaListBody || !state.manaSummary) return;
const items = getManaTrackedItems(state);
state.manaListBody.innerHTML = '';
if (items.length === 0) {
const empty = document.createElement('div');
empty.className = 'inv-mana-empty';
empty.textContent = 'No equipped mana-bearing items';
state.manaListBody.appendChild(empty);
state.manaSummary.textContent = 'Mana: 0 tracked';
return;
}
const activeCount = items.filter(item => item.mana_state === 'active').length;
const lowCount = items.filter(item => (item.live_mana_time_remaining_seconds || 0) > 0 && item.live_mana_time_remaining_seconds <= 7200).length;
state.manaSummary.textContent = `Mana: ${items.length} tracked, ${activeCount} active, ${lowCount} low`;
items.forEach(item => {
const row = document.createElement('div');
row.className = 'inv-mana-row';
const iconWrap = document.createElement('div');
iconWrap.className = 'inv-mana-icon';
iconWrap.appendChild(createInventorySlot(item));
const nameEl = document.createElement('div');
nameEl.className = 'inv-mana-name';
nameEl.textContent = item.name || item.Name || 'Unknown Item';
const stateEl = document.createElement('div');
const stateName = item.mana_state || 'unknown';
stateEl.className = `inv-mana-state mana-state-${stateName}`;
stateEl.textContent = stateName.replace(/_/g, ' ');
const manaEl = document.createElement('div');
manaEl.className = 'inv-mana-value';
if (item.current_mana !== undefined && item.max_mana !== undefined) {
manaEl.textContent = `${item.current_mana} / ${item.max_mana}`;
} else if (item.mana_display) {
manaEl.textContent = item.mana_display;
} else {
manaEl.textContent = '--';
}
const timeEl = document.createElement('div');
timeEl.className = 'inv-mana-time';
timeEl.textContent = formatManaRemaining(item.live_mana_time_remaining_seconds);
row.appendChild(iconWrap);
row.appendChild(nameEl);
row.appendChild(stateEl);
row.appendChild(manaEl);
row.appendChild(timeEl);
state.manaListBody.appendChild(row);
});
}
function showInventoryWindow(name) {
@ -1559,6 +1661,23 @@ function showInventoryWindow(name) {
const bottomSection = document.createElement('div');
bottomSection.className = 'inv-bottom-section';
const itemSection = document.createElement('div');
itemSection.className = 'inv-item-section';
const manaPanel = document.createElement('div');
manaPanel.className = 'inv-mana-panel';
const manaHeader = document.createElement('div');
manaHeader.className = 'inv-mana-header';
manaHeader.textContent = 'Mana';
const manaSummary = document.createElement('div');
manaSummary.className = 'inv-mana-summary';
manaSummary.textContent = 'Mana: loading';
const manaListBody = document.createElement('div');
manaListBody.className = 'inv-mana-list';
manaPanel.appendChild(manaHeader);
manaPanel.appendChild(manaSummary);
manaPanel.appendChild(manaListBody);
const contentsHeader = document.createElement('div');
contentsHeader.className = 'inv-contents-header';
@ -1567,8 +1686,10 @@ function showInventoryWindow(name) {
const itemGrid = document.createElement('div');
itemGrid.className = 'inv-item-grid';
bottomSection.appendChild(contentsHeader);
bottomSection.appendChild(itemGrid);
itemSection.appendChild(contentsHeader);
itemSection.appendChild(itemGrid);
bottomSection.appendChild(itemSection);
bottomSection.appendChild(manaPanel);
invContent.appendChild(topSection);
invContent.appendChild(bottomSection);
@ -1613,6 +1734,8 @@ function showInventoryWindow(name) {
burdenFill: burdenFill,
burdenLabel: burdenLabel,
contentsHeader: contentsHeader,
manaSummary: manaSummary,
manaListBody: manaListBody,
characterName: name
};

View file

@ -908,11 +908,17 @@ body.noselect, body.noselect * {
.inv-bottom-section {
flex: 1;
display: flex;
flex-direction: column;
flex-direction: row;
margin-top: 10px;
margin-right: 52px;
overflow: hidden;
min-height: 0;
gap: 8px;
}
.inv-bottom-section > :first-child,
.inv-bottom-section > :nth-child(2) {
flex-shrink: 0;
}
.inv-contents-header {
@ -924,6 +930,12 @@ body.noselect, body.noselect * {
padding-bottom: 2px;
}
.inv-item-section {
display: flex;
flex-direction: column;
min-width: 0;
}
.inv-item-grid {
display: grid;
grid-template-columns: repeat(6, 36px);
@ -939,6 +951,110 @@ body.noselect, body.noselect * {
justify-content: start;
}
.inv-mana-panel {
width: 118px;
min-width: 118px;
display: flex;
flex-direction: column;
background: rgba(6, 10, 18, 0.92);
border: 1px solid var(--ac-border-light);
padding: 4px;
min-height: 0;
}
.inv-mana-header {
color: var(--ac-gold);
font-size: 14px;
text-align: center;
border-bottom: 1px solid var(--ac-border-light);
padding-bottom: 2px;
}
.inv-mana-summary {
color: var(--ac-text-dim);
font-size: 10px;
line-height: 1.2;
padding: 4px 0;
border-bottom: 1px solid rgba(255,255,255,0.08);
margin-bottom: 4px;
}
.inv-mana-list {
flex: 1;
min-height: 0;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 4px;
}
.inv-mana-row {
display: grid;
grid-template-columns: 36px 1fr;
grid-template-rows: auto auto auto;
gap: 1px 6px;
align-items: center;
background: rgba(18, 24, 34, 0.9);
border: 1px solid rgba(255,255,255,0.08);
padding: 3px;
}
.inv-mana-icon {
grid-row: 1 / span 3;
width: 36px;
height: 36px;
}
.inv-mana-icon .inventory-slot {
width: 36px;
height: 36px;
}
.inv-mana-name {
color: #f2e6c9;
font-size: 10px;
line-height: 1.15;
word-break: break-word;
}
.inv-mana-state,
.inv-mana-value,
.inv-mana-time {
font-size: 10px;
line-height: 1.1;
}
.inv-mana-value {
color: #98d7ff;
}
.inv-mana-time {
color: #cfe6a0;
}
.mana-state-active {
color: #76d17f;
}
.mana-state-not_active {
color: #ff8e6f;
}
.mana-state-unknown {
color: #d4c27a;
}
.mana-state-not_activatable {
color: #97a1ad;
}
.inv-mana-empty {
color: var(--ac-text-dim);
font-size: 11px;
text-align: center;
padding: 12px 6px;
}
.inv-item-grid::-webkit-scrollbar {
width: 12px;
}