From 5a6986a8c539364b7a166abdfb608b3cc00ecb3b Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Mon, 13 Jul 2026 23:01:45 -0400 Subject: feat(admin): convert players page into characters page with basic stats/yaml --- internal/admin/static/admin.css | 2 + internal/admin/static/charactereditor.js | 323 +++++++++++++++++++++++++++++++ 2 files changed, 325 insertions(+) create mode 100644 internal/admin/static/charactereditor.js (limited to 'internal/admin/static') diff --git a/internal/admin/static/admin.css b/internal/admin/static/admin.css index f6072ad..63a12e5 100644 --- a/internal/admin/static/admin.css +++ b/internal/admin/static/admin.css @@ -172,6 +172,8 @@ details.dir-group summary.dir-header::-webkit-details-marker{display:none} .dir-group[open]>.dir-header .dir-arrow::before{content:'\25BC'} .dir-drop-target{background:rgba(15,52,96,.3)!important;outline:1px dashed var(--accent);outline-offset:-2px} .editor-main pre{background:var(--input-bg);padding:12px;border-radius:4px;font-size:12px;overflow-x:auto;border:1px solid var(--border)} +.entity-link{color:var(--accent);text-decoration:none;cursor:pointer} +.entity-link:hover{text-decoration:underline;color:#4a90d9} .form-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:4px 10px} .form-grid .form-group{margin-bottom:4px} .form-grid .form-group input,.form-grid .form-group select{font-size:11px;padding:4px 6px} diff --git a/internal/admin/static/charactereditor.js b/internal/admin/static/charactereditor.js new file mode 100644 index 0000000..bf40f09 --- /dev/null +++ b/internal/admin/static/charactereditor.js @@ -0,0 +1,323 @@ +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; + } +} -- cgit v1.2.3