Redesign character window to match TreeStats layout and style

Replace the AC stone-themed single-scroll character window with a TreeStats-
style tabbed interface. Two side-by-side tab containers: left (Attributes,
Skills, Titles) and right (Augmentations, Ratings, Other), plus an Allegiance
section below. Exact TreeStats color palette (#000022 bg, #af7a30 gold
borders, purple specialized, teal trained). Backend accepts new properties
and titles fields in character_stats message for JSONB storage.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
erik 2026-02-27 14:39:20 +00:00
parent 45cedd0ec9
commit 176fb020ec
3 changed files with 797 additions and 279 deletions

View file

@ -1088,6 +1088,63 @@ function showInventoryWindow(name) {
debugLog('Inventory window created for:', name);
}
// === TreeStats Property ID Mappings ===
const TS_AUGMENTATIONS = {
218: "Reinforcement of the Lugians", 219: "Bleeargh's Fortitude", 220: "Oswald's Enhancement",
221: "Siraluun's Blessing", 222: "Enduring Calm", 223: "Steadfast Will",
224: "Ciandra's Essence", 225: "Yoshi's Essence", 226: "Jibril's Essence",
227: "Celdiseth's Essence", 228: "Koga's Essence", 229: "Shadow of the Seventh Mule",
230: "Might of the Seventh Mule", 231: "Clutch of the Miser", 232: "Enduring Enchantment",
233: "Critical Protection", 234: "Quick Learner", 235: "Ciandra's Fortune",
236: "Charmed Smith", 237: "Innate Renewal", 238: "Archmage's Endurance",
239: "Enhancement of the Blade Turner", 240: "Enhancement of the Arrow Turner",
241: "Enhancement of the Mace Turner", 242: "Caustic Enhancement", 243: "Fierce Impaler",
244: "Iron Skin of the Invincible", 245: "Eye of the Remorseless", 246: "Hand of the Remorseless",
294: "Master of the Steel Circle", 295: "Master of the Focused Eye",
296: "Master of the Five Fold Path", 297: "Frenzy of the Slayer",
298: "Iron Skin of the Invincible", 299: "Jack of All Trades",
300: "Infused Void Magic", 301: "Infused War Magic",
302: "Infused Life Magic", 309: "Infused Item Magic",
310: "Infused Creature Magic", 326: "Clutch of the Miser",
328: "Enduring Enchantment"
};
const TS_AURAS = {
333: "Valor / Destruction", 334: "Protection", 335: "Glory / Retribution",
336: "Temperance / Hardening", 338: "Aetheric Vision", 339: "Mana Flow",
340: "Mana Infusion", 342: "Purity", 343: "Craftsman", 344: "Specialization",
365: "World"
};
const TS_RATINGS = {
370: "Damage", 371: "Damage Resistance", 372: "Critical", 373: "Critical Resistance",
374: "Critical Damage", 375: "Critical Damage Resistance", 376: "Healing Boost",
379: "Vitality"
};
const TS_SOCIETY = { 287: "Celestial Hand", 288: "Eldrytch Web", 289: "Radiant Blood" };
const TS_MASTERIES = { 354: "Melee", 355: "Ranged", 362: "Summoning" };
const TS_MASTERY_NAMES = { 1: "Unarmed", 2: "Swords", 3: "Axes", 4: "Maces", 5: "Spears", 6: "Daggers", 7: "Staves", 8: "Bows", 9: "Crossbows", 10: "Thrown", 11: "Two-Handed", 12: "Void", 13: "War", 14: "Life" };
const TS_GENERAL = { 181: "Chess Rank", 192: "Fishing Skill", 199: "Total Augmentations", 322: "Aetheria Slots", 390: "Enlightenment" };
function _tsSocietyRank(v) {
if (v >= 1001) return "Master";
if (v >= 301) return "Lord";
if (v >= 151) return "Knight";
if (v >= 31) return "Adept";
return "Initiate";
}
function _tsSetupTabs(container) {
const tabs = container.querySelectorAll('.ts-tab');
const boxes = container.querySelectorAll('.ts-box');
tabs.forEach((tab, i) => {
tab.addEventListener('click', () => {
tabs.forEach(t => { t.classList.remove('active'); t.classList.add('inactive'); });
boxes.forEach(b => { b.classList.remove('active'); b.classList.add('inactive'); });
tab.classList.remove('inactive'); tab.classList.add('active');
if (boxes[i]) { boxes[i].classList.remove('inactive'); boxes[i].classList.add('active'); }
});
});
}
function showCharacterWindow(name) {
debugLog('showCharacterWindow called for:', name);
const windowId = `characterWindow-${name}`;
@ -1106,71 +1163,98 @@ function showCharacterWindow(name) {
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 class="ts-character-header" id="charHeader-${esc}">
<h1>${name} <span class="ts-level"></span></h1>
<div class="ts-subtitle">Awaiting character data...</div>
</div>
<div class="ts-xplum" id="charXpLum-${esc}">
<div class="ts-left">Total XP: \u2014</div>
<div class="ts-right">Unassigned XP: \u2014</div>
<div class="ts-left">Luminance: \u2014</div>
<div class="ts-right">Deaths: \u2014</div>
</div>
<div class="ts-tabrow">
<div class="ts-tabcontainer" id="charTabLeft-${esc}">
<div class="ts-tabbar">
<div class="ts-tab active">Attributes</div>
<div class="ts-tab inactive">Skills</div>
<div class="ts-tab inactive">Titles</div>
</div>
<div class="ts-box active" id="charAttribs-${esc}">
<div class="ts-vitals" id="charVitals-${esc}">
<div class="ts-vital">
<span class="ts-vital-label">Health</span>
<div class="ts-vital-bar ts-health-bar"><div class="ts-vital-fill"></div></div>
<span class="ts-vital-text">\u2014 / \u2014</span>
</div>
<div class="ts-vital">
<span class="ts-vital-label">Stamina</span>
<div class="ts-vital-bar ts-stamina-bar"><div class="ts-vital-fill"></div></div>
<span class="ts-vital-text">\u2014 / \u2014</span>
</div>
<div class="ts-vital">
<span class="ts-vital-label">Mana</span>
<div class="ts-vital-bar ts-mana-bar"><div class="ts-vital-fill"></div></div>
<span class="ts-vital-text">\u2014 / \u2014</span>
</div>
</div>
<table class="ts-char" id="charAttribTable-${esc}">
<tr class="ts-colnames"><td>Attribute</td><td>Creation</td><td>Base</td></tr>
<tr><td class="ts-headerleft">Strength</td><td class="ts-creation">\u2014</td><td class="ts-headerright">\u2014</td></tr>
<tr><td class="ts-headerleft">Endurance</td><td class="ts-creation">\u2014</td><td class="ts-headerright">\u2014</td></tr>
<tr><td class="ts-headerleft">Coordination</td><td class="ts-creation">\u2014</td><td class="ts-headerright">\u2014</td></tr>
<tr><td class="ts-headerleft">Quickness</td><td class="ts-creation">\u2014</td><td class="ts-headerright">\u2014</td></tr>
<tr><td class="ts-headerleft">Focus</td><td class="ts-creation">\u2014</td><td class="ts-headerright">\u2014</td></tr>
<tr><td class="ts-headerleft">Self</td><td class="ts-creation">\u2014</td><td class="ts-headerright">\u2014</td></tr>
</table>
<table class="ts-char" id="charVitalsTable-${esc}">
<tr class="ts-colnames"><td>Vital</td><td>Base</td></tr>
<tr><td class="ts-headerleft">Health</td><td class="ts-headerright">\u2014</td></tr>
<tr><td class="ts-headerleft">Stamina</td><td class="ts-headerright">\u2014</td></tr>
<tr><td class="ts-headerleft">Mana</td><td class="ts-headerright">\u2014</td></tr>
</table>
<table class="ts-char" id="charCredits-${esc}">
<tr><td class="ts-headerleft">Skill Credits</td><td class="ts-headerright">\u2014</td></tr>
</table>
</div>
<div class="ts-box inactive" id="charSkills-${esc}">
<div class="ts-placeholder">Awaiting data...</div>
</div>
<div class="ts-box inactive" id="charTitles-${esc}">
<div class="ts-placeholder">Awaiting data...</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 class="ts-tabcontainer" id="charTabRight-${esc}">
<div class="ts-tabbar">
<div class="ts-tab active">Augmentations</div>
<div class="ts-tab inactive">Ratings</div>
<div class="ts-tab inactive">Other</div>
</div>
<div class="ts-box active" id="charAugs-${esc}">
<div class="ts-placeholder">Awaiting data...</div>
</div>
<div class="ts-box inactive" id="charRatings-${esc}">
<div class="ts-placeholder">Awaiting data...</div>
</div>
<div class="ts-box inactive" id="charOther-${esc}">
<div class="ts-placeholder">Awaiting data...</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>
<div class="ts-allegiance-section" id="charAllegiance-${esc}">
<div class="ts-section-title">Allegiance</div>
<div class="ts-placeholder">Awaiting data...</div>
</div>
`;
// Wire up tab switching
const leftTabs = document.getElementById(`charTabLeft-${esc}`);
const rightTabs = document.getElementById(`charTabRight-${esc}`);
if (leftTabs) _tsSetupTabs(leftTabs);
if (rightTabs) _tsSetupTabs(rightTabs);
// Fetch existing data from API
fetch(`${API_BASE}/api/character-stats/${encodeURIComponent(name)}`)
fetch(`${API_BASE}/character-stats/${encodeURIComponent(name)}`)
.then(r => r.ok ? r.json() : null)
.then(data => {
if (data && !data.error) {
@ -1187,130 +1271,260 @@ function showCharacterWindow(name) {
}
function updateCharacterWindow(name, data) {
const escapedName = CSS.escape(name);
const esc = CSS.escape(name);
const fmt = n => n != null ? n.toLocaleString() : '\u2014';
// Header
const header = document.getElementById(`charHeader-${escapedName}`);
// -- Header --
const header = document.getElementById(`charHeader-${esc}`);
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;
const parts = [gender, race].filter(Boolean).join(' ');
header.querySelector('.ts-subtitle').textContent = parts || 'Awaiting data...';
const levelSpan = header.querySelector('.ts-level');
if (levelSpan) levelSpan.textContent = level;
}
// 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}`;
}
}
});
// -- XP / Luminance row --
const xplum = document.getElementById(`charXpLum-${esc}`);
if (xplum) {
const divs = xplum.querySelectorAll('div');
if (divs[0]) divs[0].textContent = `Total XP: ${fmt(data.total_xp)}`;
if (divs[1]) divs[1].textContent = `Unassigned XP: ${fmt(data.unassigned_xp)}`;
if (divs[2]) {
const lum = data.luminance_earned != null && data.luminance_total != null
? `${fmt(data.luminance_earned)} / ${fmt(data.luminance_total)}`
: '\u2014';
divs[2].textContent = `Luminance: ${lum}`;
}
if (divs[3]) divs[3].textContent = `Deaths: ${fmt(data.deaths)}`;
}
// -- Attributes table --
const attribTable = document.getElementById(`charAttribTable-${esc}`);
if (attribTable && data.attributes) {
const order = ['strength', 'endurance', 'coordination', 'quickness', 'focus', 'self'];
const rows = attribTable.querySelectorAll('tr:not(.ts-colnames)');
order.forEach((attr, i) => {
if (rows[i] && data.attributes[attr]) {
const cells = rows[i].querySelectorAll('td');
if (cells[1]) cells[1].textContent = data.attributes[attr].creation ?? '\u2014';
if (cells[2]) cells[2].textContent = data.attributes[attr].base ?? '\u2014';
}
});
}
// Skills
const skillsDiv = document.getElementById(`charSkills-${escapedName}`);
if (skillsDiv && data.skills) {
const grouped = { Specialized: [], Trained: [], Untrained: [] };
// -- Vitals table (base values) --
const vitalsTable = document.getElementById(`charVitalsTable-${esc}`);
if (vitalsTable && data.vitals) {
const vOrder = ['health', 'stamina', 'mana'];
const vRows = vitalsTable.querySelectorAll('tr:not(.ts-colnames)');
vOrder.forEach((v, i) => {
if (vRows[i] && data.vitals[v]) {
const cells = vRows[i].querySelectorAll('td');
if (cells[1]) cells[1].textContent = data.vitals[v].base ?? '\u2014';
}
});
}
// -- Skill credits --
const creditsTable = document.getElementById(`charCredits-${esc}`);
if (creditsTable) {
const cell = creditsTable.querySelector('td.ts-headerright');
if (cell) cell.textContent = fmt(data.skill_credits);
}
// -- Skills tab --
const skillsBox = document.getElementById(`charSkills-${esc}`);
if (skillsBox && data.skills) {
const grouped = { Specialized: [], Trained: [] };
for (const [skill, info] of Object.entries(data.skills)) {
const training = info.training || 'Untrained';
if (training === 'Untrained' || training === 'Unusable') continue;
const displayName = skill.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
if (grouped[training]) {
grouped[training].push({ name: displayName, base: info.base });
if (grouped[training]) grouped[training].push({ name: displayName, base: info.base });
}
for (const g of Object.values(grouped)) g.sort((a, b) => a.name.localeCompare(b.name));
let html = '<table class="ts-char">';
html += '<tr class="ts-colnames"><td>Skill</td><td>Level</td></tr>';
if (grouped.Specialized.length) {
for (const s of grouped.Specialized) {
html += `<tr><td class="ts-specialized">${s.name}</td><td class="ts-specialized" style="text-align:right">${s.base}</td></tr>`;
}
}
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>`;
if (grouped.Trained.length) {
for (const s of grouped.Trained) {
html += `<tr><td class="ts-trained">${s.name}</td><td class="ts-trained" style="text-align:right">${s.base}</td></tr>`;
}
html += `</div>`;
}
skillsDiv.innerHTML = html;
html += '</table>';
skillsBox.innerHTML = html;
}
// Allegiance
const allegianceDiv = document.getElementById(`charAllegiance-${escapedName}`);
if (allegianceDiv && data.allegiance) {
// -- Titles tab --
const titlesBox = document.getElementById(`charTitles-${esc}`);
if (titlesBox) {
const statsData = data.stats_data || data;
const titles = statsData.titles;
if (titles && titles.length > 0) {
let html = '<div class="ts-titles-list">';
for (const t of titles) html += `<div>${t}</div>`;
html += '</div>';
titlesBox.innerHTML = html;
} else {
titlesBox.innerHTML = '<div class="ts-placeholder">No titles data</div>';
}
}
// -- Properties-based tabs (Augmentations, Ratings, Other) --
const statsData = data.stats_data || data;
const props = statsData.properties || {};
// Augmentations tab
const augsBox = document.getElementById(`charAugs-${esc}`);
if (augsBox) {
let augRows = [], auraRows = [];
for (const [id, val] of Object.entries(props)) {
const nid = parseInt(id);
if (TS_AUGMENTATIONS[nid] && val > 0) augRows.push({ name: TS_AUGMENTATIONS[nid], uses: val });
if (TS_AURAS[nid] && val > 0) auraRows.push({ name: TS_AURAS[nid], uses: val });
}
if (augRows.length || auraRows.length) {
let html = '';
if (augRows.length) {
html += '<div class="ts-section-title">Augmentations</div>';
html += '<table class="ts-props"><tr class="ts-colnames"><td>Name</td><td>Uses</td></tr>';
for (const a of augRows) html += `<tr><td>${a.name}</td><td style="text-align:right">${a.uses}</td></tr>`;
html += '</table>';
}
if (auraRows.length) {
html += '<div class="ts-section-title">Auras</div>';
html += '<table class="ts-props"><tr class="ts-colnames"><td>Name</td><td>Uses</td></tr>';
for (const a of auraRows) html += `<tr><td>${a.name}</td><td style="text-align:right">${a.uses}</td></tr>`;
html += '</table>';
}
augsBox.innerHTML = html;
} else {
augsBox.innerHTML = '<div class="ts-placeholder">No augmentation data</div>';
}
}
// Ratings tab
const ratingsBox = document.getElementById(`charRatings-${esc}`);
if (ratingsBox) {
let rows = [];
for (const [id, val] of Object.entries(props)) {
const nid = parseInt(id);
if (TS_RATINGS[nid] && val > 0) rows.push({ name: TS_RATINGS[nid], value: val });
}
if (rows.length) {
let html = '<table class="ts-props"><tr class="ts-colnames"><td>Rating</td><td>Value</td></tr>';
for (const r of rows) html += `<tr><td>${r.name}</td><td style="text-align:right">${r.value}</td></tr>`;
html += '</table>';
ratingsBox.innerHTML = html;
} else {
ratingsBox.innerHTML = '<div class="ts-placeholder">No rating data</div>';
}
}
// Other tab (General, Masteries, Society)
const otherBox = document.getElementById(`charOther-${esc}`);
if (otherBox) {
let html = '';
// General section
let generalRows = [];
if (data.birth) generalRows.push({ name: 'Birth', value: data.birth });
if (data.deaths != null) generalRows.push({ name: 'Deaths', value: fmt(data.deaths) });
for (const [id, val] of Object.entries(props)) {
const nid = parseInt(id);
if (TS_GENERAL[nid]) generalRows.push({ name: TS_GENERAL[nid], value: val });
}
if (generalRows.length) {
html += '<div class="ts-section-title">General</div>';
html += '<table class="ts-props">';
for (const r of generalRows) html += `<tr><td>${r.name}</td><td style="text-align:right">${r.value}</td></tr>`;
html += '</table>';
}
// Masteries section
let masteryRows = [];
for (const [id, val] of Object.entries(props)) {
const nid = parseInt(id);
if (TS_MASTERIES[nid]) {
const mName = TS_MASTERY_NAMES[val] || `Unknown (${val})`;
masteryRows.push({ name: TS_MASTERIES[nid], value: mName });
}
}
if (masteryRows.length) {
html += '<div class="ts-section-title">Masteries</div>';
html += '<table class="ts-props">';
for (const m of masteryRows) html += `<tr><td>${m.name}</td><td style="text-align:right">${m.value}</td></tr>`;
html += '</table>';
}
// Society section
let societyRows = [];
for (const [id, val] of Object.entries(props)) {
const nid = parseInt(id);
if (TS_SOCIETY[nid] && val > 0) {
societyRows.push({ name: TS_SOCIETY[nid], rank: _tsSocietyRank(val), value: val });
}
}
if (societyRows.length) {
html += '<div class="ts-section-title">Society</div>';
html += '<table class="ts-props">';
for (const s of societyRows) html += `<tr><td>${s.name}</td><td style="text-align:right">${s.rank} (${s.value})</td></tr>`;
html += '</table>';
}
otherBox.innerHTML = html || '<div class="ts-placeholder">No additional data</div>';
}
// -- Allegiance section --
const allegDiv = document.getElementById(`charAllegiance-${esc}`);
if (allegDiv && 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);
let html = '<div class="ts-section-title">Allegiance</div>';
html += '<table class="ts-allegiance">';
if (a.name) html += `<tr><td>Name</td><td>${a.name}</td></tr>`;
if (a.monarch) html += `<tr><td>Monarch</td><td>${a.monarch.name || '\u2014'}</td></tr>`;
if (a.patron) html += `<tr><td>Patron</td><td>${a.patron.name || '\u2014'}</td></tr>`;
if (a.rank !== undefined) html += `<tr><td>Rank</td><td>${a.rank}</td></tr>`;
if (a.followers !== undefined) html += `<tr><td>Followers</td><td>${a.followers}</td></tr>`;
html += '</table>';
allegDiv.innerHTML = html;
}
}
function updateCharacterVitals(name, vitals) {
const escapedName = CSS.escape(name);
const vitalsDiv = document.getElementById(`charVitals-${escapedName}`);
const esc = CSS.escape(name);
const vitalsDiv = document.getElementById(`charVitals-${esc}`);
if (!vitalsDiv) return;
const vitalElements = vitalsDiv.querySelectorAll('.ac-vital');
const vitalElements = vitalsDiv.querySelectorAll('.ts-vital');
if (vitalElements[0]) {
const fill = vitalElements[0].querySelector('.ac-vital-fill');
const txt = vitalElements[0].querySelector('.ac-vital-text');
const fill = vitalElements[0].querySelector('.ts-vital-fill');
const txt = vitalElements[0].querySelector('.ts-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');
const fill = vitalElements[1].querySelector('.ts-vital-fill');
const txt = vitalElements[1].querySelector('.ts-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');
const fill = vitalElements[2].querySelector('.ts-vital-fill');
const txt = vitalElements[2].querySelector('.ts-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}`;