Add full character window with live stats, vitals, skills, and allegiance display

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
erik 2026-02-26 15:10:48 +00:00
parent e71dfb4ec3
commit a545a8b920

View file

@ -1101,7 +1101,221 @@ function showCharacterWindow(name) {
return; return;
} }
content.innerHTML = '<div style="padding: 20px; color: #c8b89a;">Awaiting character data...</div>'; win.dataset.character = name;
characterWindows[name] = win;
const esc = CSS.escape(name);
content.innerHTML = `
<div class="ac-panel">
<div class="ac-header" id="charHeader-${esc}">
<div class="ac-name">${name}</div>
<div class="ac-subtitle">Awaiting character data...</div>
</div>
<div class="ac-section">
<div class="ac-section-title">Attributes</div>
<div class="ac-attributes" id="charAttribs-${esc}">
<div class="ac-attr-row">
<div class="ac-attr"><span class="ac-attr-label">Strength</span><span class="ac-attr-value">\u2014</span></div>
<div class="ac-attr"><span class="ac-attr-label">Quickness</span><span class="ac-attr-value">\u2014</span></div>
</div>
<div class="ac-attr-row">
<div class="ac-attr"><span class="ac-attr-label">Endurance</span><span class="ac-attr-value">\u2014</span></div>
<div class="ac-attr"><span class="ac-attr-label">Focus</span><span class="ac-attr-value">\u2014</span></div>
</div>
<div class="ac-attr-row">
<div class="ac-attr"><span class="ac-attr-label">Coordination</span><span class="ac-attr-value">\u2014</span></div>
<div class="ac-attr"><span class="ac-attr-label">Self</span><span class="ac-attr-value">\u2014</span></div>
</div>
</div>
</div>
<div class="ac-section">
<div class="ac-section-title">Vitals</div>
<div class="ac-vitals" id="charVitals-${esc}">
<div class="ac-vital">
<span class="ac-vital-label">Health</span>
<div class="ac-vital-bar ac-health-bar"><div class="ac-vital-fill"></div></div>
<span class="ac-vital-text">\u2014 / \u2014</span>
</div>
<div class="ac-vital">
<span class="ac-vital-label">Stamina</span>
<div class="ac-vital-bar ac-stamina-bar"><div class="ac-vital-fill"></div></div>
<span class="ac-vital-text">\u2014 / \u2014</span>
</div>
<div class="ac-vital">
<span class="ac-vital-label">Mana</span>
<div class="ac-vital-bar ac-mana-bar"><div class="ac-vital-fill"></div></div>
<span class="ac-vital-text">\u2014 / \u2014</span>
</div>
</div>
</div>
<div class="ac-section ac-skills-section">
<div class="ac-section-title">Skills</div>
<div class="ac-skills" id="charSkills-${esc}">
<div class="ac-skill-placeholder">Awaiting data...</div>
</div>
</div>
<div class="ac-section">
<div class="ac-section-title">Allegiance</div>
<div class="ac-allegiance" id="charAllegiance-${esc}">
<div class="ac-skill-placeholder">Awaiting data...</div>
</div>
</div>
<div class="ac-footer" id="charFooter-${esc}">
<div class="ac-footer-row"><span>Total XP:</span><span>\u2014</span></div>
<div class="ac-footer-row"><span>Unassigned XP:</span><span>\u2014</span></div>
<div class="ac-footer-row"><span>Luminance:</span><span>\u2014</span></div>
<div class="ac-footer-row"><span>Deaths:</span><span>\u2014</span></div>
</div>
</div>
`;
// Fetch existing data from API
fetch(`${API_BASE}/api/character-stats/${encodeURIComponent(name)}`)
.then(r => r.ok ? r.json() : null)
.then(data => {
if (data && !data.error) {
characterStats[name] = data;
updateCharacterWindow(name, data);
}
})
.catch(err => handleError('Character stats', err));
// If we already have vitals from the live stream, apply them
if (characterVitals[name]) {
updateCharacterVitals(name, characterVitals[name]);
}
}
function updateCharacterWindow(name, data) {
const escapedName = CSS.escape(name);
// Header
const header = document.getElementById(`charHeader-${escapedName}`);
if (header) {
const level = data.level || '?';
const race = data.race || '';
const gender = data.gender || '';
const subtitle = [`Level ${level}`, race, gender].filter(Boolean).join(' \u00b7 ');
header.querySelector('.ac-subtitle').textContent = subtitle;
}
// Attributes
const attribs = document.getElementById(`charAttribs-${escapedName}`);
if (attribs && data.attributes) {
const order = [
['strength', 'quickness'],
['endurance', 'focus'],
['coordination', 'self']
];
const rows = attribs.querySelectorAll('.ac-attr-row');
order.forEach((pair, i) => {
if (rows[i]) {
const cells = rows[i].querySelectorAll('.ac-attr-value');
pair.forEach((attr, j) => {
if (cells[j] && data.attributes[attr]) {
const val = data.attributes[attr].base || '\u2014';
const creation = data.attributes[attr].creation;
cells[j].textContent = val;
if (creation !== undefined) {
cells[j].title = `Creation: ${creation}`;
}
}
});
}
});
}
// Skills
const skillsDiv = document.getElementById(`charSkills-${escapedName}`);
if (skillsDiv && data.skills) {
const grouped = { Specialized: [], Trained: [], Untrained: [] };
for (const [skill, info] of Object.entries(data.skills)) {
const training = info.training || 'Untrained';
const displayName = skill.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
if (grouped[training]) {
grouped[training].push({ name: displayName, base: info.base });
}
}
for (const group of Object.values(grouped)) {
group.sort((a, b) => b.base - a.base);
}
let html = '';
for (const [training, skills] of Object.entries(grouped)) {
if (skills.length === 0) continue;
html += `<div class="ac-skill-group">`;
html += `<div class="ac-skill-group-title ac-${training.toLowerCase()}">${training}</div>`;
for (const s of skills) {
html += `<div class="ac-skill-row ac-${training.toLowerCase()}">`;
html += `<span class="ac-skill-name">${s.name}</span>`;
html += `<span class="ac-skill-value">${s.base}</span>`;
html += `</div>`;
}
html += `</div>`;
}
skillsDiv.innerHTML = html;
}
// Allegiance
const allegianceDiv = document.getElementById(`charAllegiance-${escapedName}`);
if (allegianceDiv && data.allegiance) {
const a = data.allegiance;
let html = '';
if (a.name) html += `<div class="ac-alleg-row"><span>Allegiance:</span><span>${a.name}</span></div>`;
if (a.monarch) html += `<div class="ac-alleg-row"><span>Monarch:</span><span>${a.monarch.name || '\u2014'}</span></div>`;
if (a.patron) html += `<div class="ac-alleg-row"><span>Patron:</span><span>${a.patron.name || '\u2014'}</span></div>`;
if (a.rank !== undefined) html += `<div class="ac-alleg-row"><span>Rank:</span><span>${a.rank}</span></div>`;
if (a.followers !== undefined) html += `<div class="ac-alleg-row"><span>Followers:</span><span>${a.followers}</span></div>`;
allegianceDiv.innerHTML = html || '<div class="ac-skill-placeholder">No allegiance</div>';
}
// Footer
const footer = document.getElementById(`charFooter-${escapedName}`);
if (footer) {
const rows = footer.querySelectorAll('.ac-footer-row');
const formatNum = n => n != null ? n.toLocaleString() : '\u2014';
if (rows[0]) rows[0].querySelector('span:last-child').textContent = formatNum(data.total_xp);
if (rows[1]) rows[1].querySelector('span:last-child').textContent = formatNum(data.unassigned_xp);
if (rows[2]) {
const lum = data.luminance_earned != null && data.luminance_total != null
? `${formatNum(data.luminance_earned)} / ${formatNum(data.luminance_total)}`
: '\u2014';
rows[2].querySelector('span:last-child').textContent = lum;
}
if (rows[3]) rows[3].querySelector('span:last-child').textContent = formatNum(data.deaths);
}
}
function updateCharacterVitals(name, vitals) {
const escapedName = CSS.escape(name);
const vitalsDiv = document.getElementById(`charVitals-${escapedName}`);
if (!vitalsDiv) return;
const vitalElements = vitalsDiv.querySelectorAll('.ac-vital');
if (vitalElements[0]) {
const fill = vitalElements[0].querySelector('.ac-vital-fill');
const txt = vitalElements[0].querySelector('.ac-vital-text');
if (fill) fill.style.width = `${vitals.health_percentage || 0}%`;
if (txt && vitals.health_current !== undefined) {
txt.textContent = `${vitals.health_current} / ${vitals.health_max}`;
}
}
if (vitalElements[1]) {
const fill = vitalElements[1].querySelector('.ac-vital-fill');
const txt = vitalElements[1].querySelector('.ac-vital-text');
if (fill) fill.style.width = `${vitals.stamina_percentage || 0}%`;
if (txt && vitals.stamina_current !== undefined) {
txt.textContent = `${vitals.stamina_current} / ${vitals.stamina_max}`;
}
}
if (vitalElements[2]) {
const fill = vitalElements[2].querySelector('.ac-vital-fill');
const txt = vitalElements[2].querySelector('.ac-vital-text');
if (fill) fill.style.width = `${vitals.mana_percentage || 0}%`;
if (txt && vitals.mana_current !== undefined) {
txt.textContent = `${vitals.mana_current} / ${vitals.mana_max}`;
}
}
} }
// Inventory tooltip functions // Inventory tooltip functions
@ -1870,6 +2084,9 @@ function initWebSocket() {
updateVitalsDisplay(msg); updateVitalsDisplay(msg);
} else if (msg.type === 'rare') { } else if (msg.type === 'rare') {
triggerEpicRareNotification(msg.character_name, msg.name); triggerEpicRareNotification(msg.character_name, msg.name);
} else if (msg.type === 'character_stats') {
characterStats[msg.character_name] = msg;
updateCharacterWindow(msg.character_name, msg);
} else if (msg.type === 'server_status') { } else if (msg.type === 'server_status') {
handleServerStatusUpdate(msg); handleServerStatusUpdate(msg);
} }
@ -2026,6 +2243,8 @@ wrap.addEventListener('mouseleave', () => {
/* ---------- vitals display functions ----------------------------- */ /* ---------- vitals display functions ----------------------------- */
// Store vitals data per character // Store vitals data per character
const characterVitals = {}; const characterVitals = {};
const characterStats = {};
const characterWindows = {};
function updateVitalsDisplay(vitalsMsg) { function updateVitalsDisplay(vitalsMsg) {
// Store the vitals data for this character // Store the vitals data for this character
@ -2033,11 +2252,20 @@ function updateVitalsDisplay(vitalsMsg) {
health_percentage: vitalsMsg.health_percentage, health_percentage: vitalsMsg.health_percentage,
stamina_percentage: vitalsMsg.stamina_percentage, stamina_percentage: vitalsMsg.stamina_percentage,
mana_percentage: vitalsMsg.mana_percentage, mana_percentage: vitalsMsg.mana_percentage,
health_current: vitalsMsg.health_current,
health_max: vitalsMsg.health_max,
stamina_current: vitalsMsg.stamina_current,
stamina_max: vitalsMsg.stamina_max,
mana_current: vitalsMsg.mana_current,
mana_max: vitalsMsg.mana_max,
vitae: vitalsMsg.vitae vitae: vitalsMsg.vitae
}; };
// Re-render the player list to update vitals in the UI // Re-render the player list to update vitals in the UI
renderList(); renderList();
// Also update character window if open
updateCharacterVitals(vitalsMsg.character_name, characterVitals[vitalsMsg.character_name]);
} }
function createVitalsHTML(characterName) { function createVitalsHTML(characterName) {