From a545a8b920fb039466c7cd0774aeb5e10fa0aca8 Mon Sep 17 00:00:00 2001 From: erik Date: Thu, 26 Feb 2026 15:10:48 +0000 Subject: [PATCH] Add full character window with live stats, vitals, skills, and allegiance display Co-Authored-By: Claude Opus 4.6 --- static/script.js | 250 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 239 insertions(+), 11 deletions(-) diff --git a/static/script.js b/static/script.js index e98cc426..ef44795d 100644 --- a/static/script.js +++ b/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 = '
Awaiting character data...
'; + win.dataset.character = name; + characterWindows[name] = win; + + const esc = CSS.escape(name); + content.innerHTML = ` +
+
+
${name}
+
Awaiting character data...
+
+
+
Attributes
+
+
+
Strength\u2014
+
Quickness\u2014
+
+
+
Endurance\u2014
+
Focus\u2014
+
+
+
Coordination\u2014
+
Self\u2014
+
+
+
+
+
Vitals
+
+
+ Health +
+ \u2014 / \u2014 +
+
+ Stamina +
+ \u2014 / \u2014 +
+
+ Mana +
+ \u2014 / \u2014 +
+
+
+
+
Skills
+
+
Awaiting data...
+
+
+
+
Allegiance
+
+
Awaiting data...
+
+
+ +
+ `; + + // 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 += `
`; + html += `
${training}
`; + for (const s of skills) { + html += `
`; + html += `${s.name}`; + html += `${s.base}`; + html += `
`; + } + html += `
`; + } + skillsDiv.innerHTML = html; + } + + // Allegiance + const allegianceDiv = document.getElementById(`charAllegiance-${escapedName}`); + if (allegianceDiv && data.allegiance) { + const a = data.allegiance; + let html = ''; + if (a.name) html += `
Allegiance:${a.name}
`; + if (a.monarch) html += `
Monarch:${a.monarch.name || '\u2014'}
`; + if (a.patron) html += `
Patron:${a.patron.name || '\u2014'}
`; + if (a.rank !== undefined) html += `
Rank:${a.rank}
`; + if (a.followers !== undefined) html += `
Followers:${a.followers}
`; + allegianceDiv.innerHTML = html || '
No allegiance
'; + } + + // 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) {