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:
parent
e71dfb4ec3
commit
a545a8b920
1 changed files with 239 additions and 11 deletions
250
static/script.js
250
static/script.js
|
|
@ -1089,19 +1089,233 @@ function showInventoryWindow(name) {
|
|||
}
|
||||
|
||||
function showCharacterWindow(name) {
|
||||
debugLog('showCharacterWindow called for:', name);
|
||||
const windowId = `characterWindow-${name}`;
|
||||
debugLog('showCharacterWindow called for:', name);
|
||||
const windowId = `characterWindow-${name}`;
|
||||
|
||||
const { win, content, isNew } = createWindow(
|
||||
windowId, `Character: ${name}`, 'character-window'
|
||||
);
|
||||
const { win, content, isNew } = createWindow(
|
||||
windowId, `Character: ${name}`, 'character-window'
|
||||
);
|
||||
|
||||
if (!isNew) {
|
||||
debugLog('Existing character window found, showing it');
|
||||
return;
|
||||
}
|
||||
if (!isNew) {
|
||||
debugLog('Existing character window found, showing it');
|
||||
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
|
||||
|
|
@ -1870,6 +2084,9 @@ function initWebSocket() {
|
|||
updateVitalsDisplay(msg);
|
||||
} else if (msg.type === 'rare') {
|
||||
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') {
|
||||
handleServerStatusUpdate(msg);
|
||||
}
|
||||
|
|
@ -2026,6 +2243,8 @@ wrap.addEventListener('mouseleave', () => {
|
|||
/* ---------- vitals display functions ----------------------------- */
|
||||
// Store vitals data per character
|
||||
const characterVitals = {};
|
||||
const characterStats = {};
|
||||
const characterWindows = {};
|
||||
|
||||
function updateVitalsDisplay(vitalsMsg) {
|
||||
// Store the vitals data for this character
|
||||
|
|
@ -2033,11 +2252,20 @@ function updateVitalsDisplay(vitalsMsg) {
|
|||
health_percentage: vitalsMsg.health_percentage,
|
||||
stamina_percentage: vitalsMsg.stamina_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
|
||||
};
|
||||
|
||||
|
||||
// Re-render the player list to update vitals in the UI
|
||||
renderList();
|
||||
|
||||
// Also update character window if open
|
||||
updateCharacterVitals(vitalsMsg.character_name, characterVitals[vitalsMsg.character_name]);
|
||||
}
|
||||
|
||||
function createVitalsHTML(characterName) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue