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 = `
+
+
+
+
Attributes
+
+
+
Strength\u2014
+
Quickness\u2014
+
+
+
Endurance\u2014
+
Focus\u2014
+
+
+
Coordination\u2014
+
Self\u2014
+
+
+
+
+
Vitals
+
+
+
Health
+
+
\u2014 / \u2014
+
+
+
Stamina
+
+
\u2014 / \u2014
+
+
+
Mana
+
+
\u2014 / \u2014
+
+
+
+
+
+
+
+ `;
+
+ // 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) {