function esc(s) { return String(s || '').replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); } function escAttr(s) { return String(s || '').replace(/&/g,'&').replace(/"/g,'"'); } var charList = []; var currentView = null; var currentChar = null; var currentAcc = null; function initCharacterEditor() { loadCharList(); if (window.location.hash) { var hash = window.location.hash.substring(1); if (hash.indexOf('account:') === 0) { loadAccount(hash.substring(8)); } else { loadCharacter(hash); } } } function loadCharList() { API.get('/api/players').then(function(data) { charList = data || []; renderCharList(''); }).catch(function(e) { notify('Failed to load characters: ' + e.message, 'error'); }); } function filterCharList(val) { renderCharList(val); } function renderCharList(filter) { var el = $('#listEntries'); var f = (filter || '').toLowerCase(); var html = ''; var filtered = charList.filter(function(c) { var name = (c.name || '').toLowerCase(); var account = (c.account || '').toLowerCase(); return !f || name.indexOf(f) >= 0 || account.indexOf(f) >= 0; }); if (filtered.length === 0) { html += '
No characters found
'; } else { filtered.forEach(function(c) { var active = currentChar === c.name ? ' active' : ''; html += '
'; html += '
' + esc(c.name) + '
'; html += '
Total Lvl ' + esc(c.total_level) + ' · Room ' + esc(c.room) + '
'; if (c.account) { html += '
' + esc(c.account) + '
'; } html += '
'; }); } el.innerHTML = html; } function loadCharacter(name) { currentChar = name; currentView = 'character'; currentAcc = null; renderCharList(''); window.location.hash = name; API.get('/api/players/' + encodeURIComponent(name)).then(function(data) { renderCharacter(name, data); }).catch(function(e) { $('#editorMain').innerHTML = '

Failed to load: ' + esc(e.message) + '

'; }); } function renderCharacter(name, data) { var html = '

Character: ' + esc(name) + '

'; html += '
'; html += '
'; html += '
'; html += sectionHeader('Identity'); html += '
'; html += fieldDisplay('Account', buildAccountLink(data.account || '-')); html += fieldDisplay('Current Room', esc(data.room_id)); html += fieldDisplay('HP / Max HP', esc(data.hp) + ' / ' + esc(data.max_hp)); html += fieldDisplay('Credits', esc(data.credits)); html += fieldDisplay('Banked Credits', esc(data.banked_credits)); html += fieldDisplay('Combat Level', esc(data.combat_level)); html += fieldDisplay('Total Level', esc(data.total_level)); html += fieldDisplay('Attack Style', esc(data.attack_style)); html += fieldDisplay('Battery', esc(data.battery)); html += fieldDisplay('Prompt', esc(data.prompt)); html += '
'; if (data.description) { html += sectionHeader('Description'); html += '
' + esc(data.description) + '
'; } if (data.skill_levels && Object.keys(data.skill_levels).length > 0) { html += sectionHeader('Skills'); html += '
'; var skillNames = Object.keys(data.skill_levels).sort(); skillNames.forEach(function(sk) { html += fieldDisplay(capitalize(sk), esc(data.skill_levels[sk])); }); html += '
'; } if (data.equipment && Object.keys(data.equipment).length > 0) { html += sectionHeader('Equipment'); html += '
'; var eqSlots = Object.keys(data.equipment).sort(); eqSlots.forEach(function(slot) { html += fieldDisplay(formatSlot(slot), esc(data.equipment[slot])); }); html += '
'; } if (data.inventory && Object.keys(data.inventory).length > 0) { html += sectionHeader('Inventory'); html += '
'; var invSlots = Object.keys(data.inventory).sort(byNum); invSlots.forEach(function(sk) { var slot = data.inventory[sk]; var item = slot ? (slot.item_id || JSON.stringify(slot)) : '-'; var qty = slot && slot.quantity ? ' x' + slot.quantity : ''; html += fieldDisplay('Slot ' + esc(sk), esc(item) + qty); }); html += '
'; } if (data.bank && Object.keys(data.bank).length > 0) { html += sectionHeader('Bank'); html += '
'; var bankSlots = Object.keys(data.bank).sort(byNum); bankSlots.forEach(function(sk) { var slot = data.bank[sk]; var item = slot ? (slot.item_id || JSON.stringify(slot)) : '-'; var qty = slot && slot.quantity ? ' x' + slot.quantity : ''; html += fieldDisplay('Slot ' + esc(sk), esc(item) + qty); }); html += '
'; } if (data.stats) { html += sectionHeader('Stats'); html += '
'; html += fieldDisplay('Total Kills', esc(data.stats.total_kills)); html += fieldDisplay('Deaths', esc(data.stats.deaths)); if (data.stats.rooms_visited) { html += fieldDisplay('Unique Rooms', esc(roomVisitedCount(data.stats.rooms_visited))); } html += '
'; if (data.stats.mob_kills && Object.keys(data.stats.mob_kills).length > 0) { html += sectionHeader('Mob Kills'); html += '
'; var mobs = Object.keys(data.stats.mob_kills).sort(); mobs.forEach(function(mob) { html += fieldDisplay(capitalize(mob), esc(data.stats.mob_kills[mob])); }); html += '
'; } } if (data.flags && Object.keys(data.flags).length > 0) { html += sectionHeader('Flags'); html += '
'; var flagKeys = Object.keys(data.flags).sort(); flagKeys.forEach(function(k) { html += fieldDisplay(esc(k), esc(data.flags[k])); }); html += '
'; } html += '
'; html += ''; $('#editorMain').innerHTML = html; } function loadAccount(name) { currentAcc = name; currentView = 'account'; currentChar = null; renderCharList(''); window.location.hash = 'account:' + name; API.get('/api/players/account/' + encodeURIComponent(name)).then(function(data) { renderAccount(name, data); }).catch(function(e) { $('#editorMain').innerHTML = '

Failed to load account: ' + esc(e.message) + '

'; }); } function renderAccount(name, data) { var html = '

Account: ' + esc(name) + '

'; html += '
'; html += '
'; html += '
'; html += sectionHeader('Identity'); html += '
'; html += fieldDisplay('Name', esc(data.name)); html += fieldDisplay('Admin', data.admin ? 'Yes' : 'No'); html += '
'; if (data.characters && data.characters.length > 0) { html += sectionHeader('Characters'); html += '
'; data.characters.forEach(function(charName) { html += fieldDisplay('', buildCharacterLink(charName)); }); html += '
'; } if (data.aliases && Object.keys(data.aliases).length > 0) { html += sectionHeader('Aliases'); html += '
'; var aliasKeys = Object.keys(data.aliases).sort(); aliasKeys.forEach(function(k) { html += fieldDisplay(esc(k), esc(data.aliases[k])); }); html += '
'; } if (data.colors && Object.keys(data.colors).length > 0) { html += sectionHeader('Colors'); html += '
'; var colorKeys = Object.keys(data.colors).sort(); colorKeys.forEach(function(k) { html += fieldDisplay(esc(k), esc(data.colors[k])); }); html += '
'; } if (data.options && Object.keys(data.options).length > 0) { html += sectionHeader('Options'); html += '
'; var optKeys = Object.keys(data.options).sort(); optKeys.forEach(function(k) { html += fieldDisplay(esc(k), esc(data.options[k])); }); html += '
'; } html += '
'; html += ''; $('#editorMain').innerHTML = html; } function switchTab(e, tab) { e.target.parentElement.querySelectorAll('.tab').forEach(function(t) { t.classList.remove('active'); }); e.target.classList.add('active'); var tabs = $('#editorMain').querySelectorAll('.tab-content'); tabs.forEach(function(t) { t.style.display = 'none'; }); var target = document.getElementById('tab' + tab.charAt(0).toUpperCase() + tab.slice(1)); if (target) target.style.display = 'block'; } function buildAccountLink(accountName) { if (!accountName || accountName === '-') return esc(accountName); return '' + esc(accountName) + ''; } function buildCharacterLink(charName) { return '' + esc(charName) + ''; } function sectionHeader(label) { return '

' + esc(label) + '

'; } function fieldDisplay(label, value) { if (label) { return '
' + value + '
'; } return '
' + value + '
'; } function capitalize(s) { var str = String(s); return str.charAt(0).toUpperCase() + str.slice(1).replace(/_/g, ' '); } function formatSlot(slot) { return capitalize(String(slot)); } function byNum(a, b) { return parseInt(a) - parseInt(b); } function roomVisitedCount(base64Str) { if (typeof base64Str !== 'string' || base64Str.length === 0) return '0'; try { var binary = atob(base64Str); var count = 0; for (var i = 0; i < binary.length; i++) { var byte = binary.charCodeAt(i); for (var bit = 0; bit < 8; bit++) { if (byte & (1 << bit)) count++; } } return count; } catch (e) { return base64Str.length > 40 ? base64Str.substring(0, 40) + '...' : base64Str; } }