aboutsummaryrefslogtreecommitdiff
path: root/internal/admin/static/charactereditor.js
diff options
context:
space:
mode:
Diffstat (limited to 'internal/admin/static/charactereditor.js')
-rw-r--r--internal/admin/static/charactereditor.js323
1 files changed, 323 insertions, 0 deletions
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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
+function escAttr(s) { return String(s || '').replace(/&/g,'&amp;').replace(/"/g,'&quot;'); }
+
+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 += '<div style="padding:12px;color:#888;font-size:12px">No characters found</div>';
+ } else {
+ filtered.forEach(function(c) {
+ var active = currentChar === c.name ? ' active' : '';
+ html += '<div class="item' + active + '" onclick="loadCharacter(\'' + escAttr(c.name) + '\')">';
+ html += '<div style="font-weight:600">' + esc(c.name) + '</div>';
+ html += '<div style="font-size:10px;color:#888;margin-top:2px">Total Lvl ' + esc(c.total_level) + ' &middot; Room ' + esc(c.room) + '</div>';
+ if (c.account) {
+ html += '<div style="font-size:10px;color:#aaa">' + esc(c.account) + '</div>';
+ }
+ html += '</div>';
+ });
+ }
+
+ 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 = '<p style="color:var(--danger)">Failed to load: ' + esc(e.message) + '</p>';
+ });
+}
+
+function renderCharacter(name, data) {
+ var html = '<h2 style="margin-top:0">Character: ' + esc(name) + '</h2>';
+
+ html += '<div class="tabs"><button class="tab active" onclick="switchTab(event,\'fields\')">Fields</button>';
+ html += '<button class="tab" onclick="switchTab(event,\'yaml\')">Raw YAML</button></div>';
+
+ html += '<div class="tab-content" id="tabFields">';
+
+ html += sectionHeader('Identity');
+ html += '<div class="form-grid">';
+ 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 += '</div>';
+
+ if (data.description) {
+ html += sectionHeader('Description');
+ html += '<pre style="background:var(--input-bg);padding:12px;border-radius:4px;font-size:12px;border:1px solid var(--border);white-space:pre-wrap">' + esc(data.description) + '</pre>';
+ }
+
+ if (data.skill_levels && Object.keys(data.skill_levels).length > 0) {
+ html += sectionHeader('Skills');
+ html += '<div class="form-grid">';
+ var skillNames = Object.keys(data.skill_levels).sort();
+ skillNames.forEach(function(sk) {
+ html += fieldDisplay(capitalize(sk), esc(data.skill_levels[sk]));
+ });
+ html += '</div>';
+ }
+
+ if (data.equipment && Object.keys(data.equipment).length > 0) {
+ html += sectionHeader('Equipment');
+ html += '<div class="form-grid">';
+ var eqSlots = Object.keys(data.equipment).sort();
+ eqSlots.forEach(function(slot) {
+ html += fieldDisplay(formatSlot(slot), esc(data.equipment[slot]));
+ });
+ html += '</div>';
+ }
+
+ if (data.inventory && Object.keys(data.inventory).length > 0) {
+ html += sectionHeader('Inventory');
+ html += '<div class="form-grid">';
+ 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 += '</div>';
+ }
+
+ if (data.bank && Object.keys(data.bank).length > 0) {
+ html += sectionHeader('Bank');
+ html += '<div class="form-grid">';
+ 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 += '</div>';
+ }
+
+ if (data.stats) {
+ html += sectionHeader('Stats');
+ html += '<div class="form-grid">';
+ 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 += '</div>';
+
+ if (data.stats.mob_kills && Object.keys(data.stats.mob_kills).length > 0) {
+ html += sectionHeader('Mob Kills');
+ html += '<div class="form-grid">';
+ var mobs = Object.keys(data.stats.mob_kills).sort();
+ mobs.forEach(function(mob) {
+ html += fieldDisplay(capitalize(mob), esc(data.stats.mob_kills[mob]));
+ });
+ html += '</div>';
+ }
+ }
+
+ if (data.flags && Object.keys(data.flags).length > 0) {
+ html += sectionHeader('Flags');
+ html += '<div class="form-grid">';
+ var flagKeys = Object.keys(data.flags).sort();
+ flagKeys.forEach(function(k) {
+ html += fieldDisplay(esc(k), esc(data.flags[k]));
+ });
+ html += '</div>';
+ }
+
+ html += '</div>';
+
+ html += '<div class="tab-content" id="tabYaml" style="display:none">';
+ html += '<pre>' + esc(data._raw || JSON.stringify(data, null, 2)) + '</pre>';
+ html += '</div>';
+
+ $('#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 = '<p style="color:var(--danger)">Failed to load account: ' + esc(e.message) + '</p>';
+ });
+}
+
+function renderAccount(name, data) {
+ var html = '<h2 style="margin-top:0">Account: ' + esc(name) + '</h2>';
+
+ html += '<div class="tabs"><button class="tab active" onclick="switchTab(event,\'fields\')">Fields</button>';
+ html += '<button class="tab" onclick="switchTab(event,\'yaml\')">Raw YAML</button></div>';
+
+ html += '<div class="tab-content" id="tabFields">';
+
+ html += sectionHeader('Identity');
+ html += '<div class="form-grid">';
+ html += fieldDisplay('Name', esc(data.name));
+ html += fieldDisplay('Admin', data.admin ? '<span style="color:var(--accent)">Yes</span>' : 'No');
+ html += '</div>';
+
+ if (data.characters && data.characters.length > 0) {
+ html += sectionHeader('Characters');
+ html += '<div class="form-grid">';
+ data.characters.forEach(function(charName) {
+ html += fieldDisplay('', buildCharacterLink(charName));
+ });
+ html += '</div>';
+ }
+
+ if (data.aliases && Object.keys(data.aliases).length > 0) {
+ html += sectionHeader('Aliases');
+ html += '<div class="form-grid">';
+ var aliasKeys = Object.keys(data.aliases).sort();
+ aliasKeys.forEach(function(k) {
+ html += fieldDisplay(esc(k), esc(data.aliases[k]));
+ });
+ html += '</div>';
+ }
+
+ if (data.colors && Object.keys(data.colors).length > 0) {
+ html += sectionHeader('Colors');
+ html += '<div class="form-grid">';
+ var colorKeys = Object.keys(data.colors).sort();
+ colorKeys.forEach(function(k) {
+ html += fieldDisplay(esc(k), esc(data.colors[k]));
+ });
+ html += '</div>';
+ }
+
+ if (data.options && Object.keys(data.options).length > 0) {
+ html += sectionHeader('Options');
+ html += '<div class="form-grid">';
+ var optKeys = Object.keys(data.options).sort();
+ optKeys.forEach(function(k) {
+ html += fieldDisplay(esc(k), esc(data.options[k]));
+ });
+ html += '</div>';
+ }
+
+ html += '</div>';
+
+ html += '<div class="tab-content" id="tabYaml" style="display:none">';
+ html += '<pre>' + esc(data._raw || JSON.stringify(data, null, 2)) + '</pre>';
+ html += '</div>';
+
+ $('#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 '<a href="javascript:void(0)" onclick="loadAccount(\'' + escAttr(accountName) + '\')" class="entity-link">' + esc(accountName) + '</a>';
+}
+
+function buildCharacterLink(charName) {
+ return '<a href="javascript:void(0)" onclick="loadCharacter(\'' + escAttr(charName) + '\')" class="entity-link">' + esc(charName) + '</a>';
+}
+
+function sectionHeader(label) {
+ return '<h3 style="margin:16px 0 8px 0;color:var(--accent);border-bottom:1px solid var(--border);padding-bottom:4px;font-size:13px;text-transform:uppercase;letter-spacing:0.5px">' + esc(label) + '</h3>';
+}
+
+function fieldDisplay(label, value) {
+ if (label) {
+ return '<div class="form-group"><label>' + label + '</label><span style="font-size:12px;word-break:break-all">' + value + '</span></div>';
+ }
+ return '<div class="form-group"><span style="font-size:12px;word-break:break-all">' + value + '</span></div>';
+}
+
+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;
+ }
+}