mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-22 04:32:49 +00:00
refactor(dashboard): reduce the CDClient browser to a raw table viewer
Drops the analysis views (objects, loot odds, missions, skills, behavior trees, activities, zones), the name search and the 3D preview, with their API routes and CDClientRules.h. What stays: the table list, paging, sort, any-column search, column filters, and linked values that open the target table filtered to that ID. Old links such as /cdclient#/object/<LOT> (UGC page, world view) now open the Objects table filtered to that LOT. Check in the dashboard: CDClient Browser lists every table; open one, sort by a column, add a filter, search, page; click a LOT or loot matrix value; /cdclient#/object/1727 opens Objects id = 1727. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,33 +1,24 @@
|
||||
/**
|
||||
* The CDClient Browser page. Everything is addressed by the hash, so views can be linked and the back button works:
|
||||
* #/ search and the list of tables
|
||||
* #/search/<text> search results
|
||||
* The CDClient Browser page: a raw table viewer. Everything is addressed by the hash, so views can be linked and the
|
||||
* back button works:
|
||||
* #/ the list of tables
|
||||
* #/table/<Name>?... a table's rows (start, order, dir, q, filters as JSON [{column, op, value}])
|
||||
* #/<link>/<id> object, mission, skill, behavior, activity, zone, loot_matrix, loot_table
|
||||
* Which columns link where comes from /api/cdclient/schema; links without their own view open the target table
|
||||
* filtered to that value.
|
||||
* #/<link>/<id> shorthand for the target table filtered to that ID (#/object/1727 is Objects id = 1727)
|
||||
* Which columns link where comes from /api/cdclient/schema; a linked value opens the target table filtered to it.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var VIEWS = ['object', 'mission', 'skill', 'behavior', 'activity', 'zone', 'loot_matrix', 'loot_table'];
|
||||
var PAGE = 50;
|
||||
var OPS = ['=', '!=', '<', '<=', '>', '>=', 'contains', 'starts', 'null', 'notnull'];
|
||||
var nf = new Intl.NumberFormat();
|
||||
var view = document.getElementById('view');
|
||||
var schema = null;
|
||||
var viewer = null;
|
||||
|
||||
function api404(d) { return d && d.success === false; }
|
||||
|
||||
// ---- links ----
|
||||
|
||||
function hashFor(link, id) {
|
||||
if (VIEWS.indexOf(link) !== -1) return '#/' + link + '/' + id;
|
||||
var target = schema && schema.links[link];
|
||||
if (!target) return null;
|
||||
return tableHash(target.table, { filters: [{ column: target.column, op: '=', value: String(id) }] });
|
||||
}
|
||||
|
||||
function tableHash(table, params) {
|
||||
var query = [];
|
||||
Object.keys(params || {}).forEach(function (k) {
|
||||
@@ -38,20 +29,15 @@
|
||||
return '#/table/' + encodeURIComponent(table) + (query.length ? '?' + query.join('&') : '');
|
||||
}
|
||||
|
||||
function refLink(ref) {
|
||||
var href = hashFor(ref.link, ref.id);
|
||||
var text = esc(ref.name) + ' <span class="text-body-secondary">' + esc(ref.id) + '</span>';
|
||||
return href ? '<a href="' + href + '">' + text + '</a>' : text;
|
||||
}
|
||||
|
||||
function chips(refs) {
|
||||
if (!refs || !refs.length) return '<span class="text-body-secondary">None</span>';
|
||||
return refs.map(function (r) { return '<span class="cdc-chip badge bg-body-tertiary text-body border">' + refLink(r) + '</span>'; }).join('');
|
||||
// The rows a link points at: its target table filtered to the ID
|
||||
function hashFor(link, id) {
|
||||
var target = schema && schema.links[link];
|
||||
if (!target) return null;
|
||||
return tableHash(target.table, { filters: [{ column: target.column, op: '=', value: String(id) }] });
|
||||
}
|
||||
|
||||
function componentTable(type) {
|
||||
var match = schema.componentTypes.filter(function (c) { return c.type === type; })[0];
|
||||
return match ? match : null;
|
||||
return schema.componentTypes.filter(function (c) { return c.type === type; })[0] || null;
|
||||
}
|
||||
|
||||
// One cell: linked when its column points somewhere
|
||||
@@ -77,55 +63,8 @@
|
||||
return text;
|
||||
}
|
||||
|
||||
function columnsOf(table) {
|
||||
var t = schema.tables.filter(function (x) { return x.name === table; })[0];
|
||||
return t ? t.columns : [];
|
||||
}
|
||||
|
||||
// A row as a two-column table
|
||||
function keyValues(row, columns, table) {
|
||||
columns = columns || Object.keys(row).map(function (k) { return { name: k }; });
|
||||
return '<table class="table table-sm cdc-kv mb-0"><tbody>' + columns.map(function (c) {
|
||||
return '<tr><th>' + esc(c.name) + (c.link ? ' <span class="badge bg-body-tertiary text-body border">' + esc(c.link) + '</span>' : '') + '</th><td>' + cell(row[c.name], c, row, table) + '</td></tr>';
|
||||
}).join('') + '</tbody></table>';
|
||||
}
|
||||
|
||||
// Rows as a table, with the schema's links
|
||||
function rowsTable(rows, table, columns) {
|
||||
if (!rows.length) return '<div class="text-body-secondary small">No rows.</div>';
|
||||
columns = columns || (table ? columnsOf(table) : null) || [];
|
||||
if (!columns.length) columns = Object.keys(rows[0]).map(function (k) { return { name: k }; });
|
||||
return '<div class="cdc-rows-wrap"><table class="table table-sm table-hover cdc-rows mb-0"><thead><tr>' +
|
||||
columns.map(function (c) { return '<th>' + esc(c.name) + '</th>'; }).join('') + '</tr></thead><tbody>' +
|
||||
rows.map(function (row) {
|
||||
return '<tr>' + columns.map(function (c) { return '<td>' + cell(row[c.name], c, row, table) + '</td>'; }).join('') + '</tr>';
|
||||
}).join('') + '</tbody></table></div>';
|
||||
}
|
||||
|
||||
function card(title, body, extra) {
|
||||
return '<div class="card mb-3"><div class="card-header d-flex justify-content-between align-items-center"><h5 class="mb-0 fs-6">' + title + '</h5>' +
|
||||
(extra || '') + '</div><div class="card-body">' + body + '</div></div>';
|
||||
}
|
||||
|
||||
function localized(texts) {
|
||||
var keys = Object.keys(texts || {});
|
||||
if (!keys.length) return '';
|
||||
return card('Texts (locale)', '<table class="table table-sm cdc-kv mb-0"><tbody>' + keys.map(function (k) {
|
||||
return '<tr><th>' + esc(k) + '</th><td style="font-family: inherit">' + esc(texts[k]) + '</td></tr>';
|
||||
}).join('') + '</tbody></table>');
|
||||
}
|
||||
|
||||
// "Used by": everything pointing here, per table and column
|
||||
function usedBy(references, link, id) {
|
||||
if (!references || !references.length) return card('Used by', '<span class="text-body-secondary">Nothing in the CDClient points here.</span>');
|
||||
return card('Used by', references.map(function (r) {
|
||||
var href = tableHash(r.table, { filters: [{ column: r.column, op: '=', value: String(id) }] });
|
||||
return '<details class="mb-2"><summary><a href="' + href + '">' + esc(r.table) + '.' + esc(r.column) + '</a> <span class="badge text-bg-secondary">' + nf.format(r.count) + '</span>' +
|
||||
(r.objects && r.objects.length ? ' <span class="small text-body-secondary">on ' + r.objects.length + ' object(s)</span>' : '') + '</summary>' +
|
||||
(r.objects && r.objects.length ? '<div class="my-2">' + chips(r.objects) + '</div>' : '') +
|
||||
rowsTable(r.rows, r.table) + (r.count > r.rows.length ? '<div class="small text-body-secondary">First ' + r.rows.length + ' of ' + nf.format(r.count) + '; <a href="' + href + '">see all</a>.</div>' : '') +
|
||||
'</details>';
|
||||
}).join(''));
|
||||
function card(title, body) {
|
||||
return '<div class="card mb-3"><div class="card-header"><h5 class="mb-0 fs-6">' + title + '</h5></div><div class="card-body">' + body + '</div></div>';
|
||||
}
|
||||
|
||||
function header(title, subtitle) {
|
||||
@@ -136,10 +75,6 @@
|
||||
view.innerHTML = '<div class="alert alert-warning">' + esc(message) + '</div>';
|
||||
}
|
||||
|
||||
function disposeViewer() {
|
||||
if (viewer) { viewer.dispose(); viewer = null; }
|
||||
}
|
||||
|
||||
// ---- views ----
|
||||
|
||||
function home() {
|
||||
@@ -149,29 +84,20 @@
|
||||
'<div class="cdc-tables" id="tableLinks">' + tables.map(function (t) {
|
||||
return '<a href="' + tableHash(t.name) + '" data-name="' + esc(t.name.toLowerCase()) + '">' + esc(t.name) + '</a>';
|
||||
}).join('') + '</div>');
|
||||
document.getElementById('tableFilter').addEventListener('input', function (e) {
|
||||
var text = e.target.value.trim().toLowerCase();
|
||||
var filter = document.getElementById('tableFilter');
|
||||
filter.addEventListener('input', function () {
|
||||
var text = filter.value.trim().toLowerCase();
|
||||
document.querySelectorAll('#tableLinks a').forEach(function (a) { a.classList.toggle('d-none', text && a.dataset.name.indexOf(text) === -1); });
|
||||
});
|
||||
}
|
||||
|
||||
function search(text) {
|
||||
document.getElementById('searchInput').value = text;
|
||||
api.get('/api/cdclient/search?q=' + encodeURIComponent(text)).then(function (d) {
|
||||
if (api404(d)) return showError(d.error);
|
||||
var sections = [['Exact ID', d.exact], ['Objects', d.objects], ['Missions', d.missions], ['Skills', d.skills], ['Activities', d.activities], ['Zones', d.zones]];
|
||||
var found = sections.filter(function (s) { return s[1].length; });
|
||||
if (found.length === 1 && found[0][1].length === 1) { window.location.hash = hashFor(found[0][1][0].link, found[0][1][0].id); return; }
|
||||
view.innerHTML = header('Results for "' + esc(text) + '"') + (found.length ? found.map(function (s) {
|
||||
return card(s[0], '<div class="list-group list-group-flush">' + s[1].map(function (r) {
|
||||
return '<div class="list-group-item px-0"><span class="badge bg-body-tertiary text-body border me-2">' + esc(r.link) + '</span>' + (r.link === 'object' ? item(r) : refLink(r)) + '</div>';
|
||||
}).join('') + '</div>');
|
||||
}).join('') : '<div class="text-body-secondary">Nothing found.</div>');
|
||||
// Enter opens the only table left
|
||||
filter.addEventListener('keydown', function (e) {
|
||||
if (e.key !== 'Enter') return;
|
||||
var shown = document.querySelectorAll('#tableLinks a:not(.d-none)');
|
||||
if (shown.length === 1) window.location.hash = shown[0].getAttribute('href');
|
||||
});
|
||||
filter.focus();
|
||||
}
|
||||
|
||||
var OPS = ['=', '!=', '<', '<=', '>', '>=', 'contains', 'starts', 'null', 'notnull'];
|
||||
|
||||
function tableView(name, params) {
|
||||
var start = parseInt(params.start || '0', 10) || 0;
|
||||
var filters = [];
|
||||
@@ -193,7 +119,7 @@
|
||||
var arrow = state.order === c.name ? (state.dir === 'desc' ? ' ↓' : ' ↑') : '';
|
||||
return '<th><a href="#" class="text-body text-decoration-none" data-order="' + esc(c.name) + '" title="' + esc(c.type) + (c.link ? ', links to ' + esc(c.link) : '') + '">' + esc(c.name) + arrow + '</a></th>';
|
||||
}).join('') + '</tr>';
|
||||
view.innerHTML = header(esc(name), nf.format(d.total) + ' matching rows') +
|
||||
view.innerHTML = header('<a href="#/" class="text-decoration-none">Tables</a> / ' + esc(name), nf.format(d.total) + ' matching rows') +
|
||||
'<div class="card mb-3"><div class="card-body">' +
|
||||
'<form class="row g-2 align-items-end mb-2" id="filterForm">' +
|
||||
'<div class="col-sm-3"><label class="form-label small mb-1" for="fColumn">Column</label><select class="form-select form-select-sm" id="fColumn">' + columnOptions + '</select></div>' +
|
||||
@@ -231,454 +157,9 @@
|
||||
});
|
||||
}
|
||||
|
||||
// ---- game values ----
|
||||
|
||||
// A chance as a percentage with the digits that matter (0.4%, 12.5%, 100%)
|
||||
function pct(chance) {
|
||||
var p = (chance || 0) * 100;
|
||||
if (p === 0) return '0%';
|
||||
if (p >= 99.95) return '100%';
|
||||
return (p >= 10 ? p.toFixed(1) : p >= 1 ? p.toFixed(2) : +p.toPrecision(2)) + '%';
|
||||
}
|
||||
|
||||
function num(value) {
|
||||
return typeof value === 'number' ? nf.format(+value.toPrecision(7)) : esc(value === null || value === undefined ? '' : value);
|
||||
}
|
||||
|
||||
// Rarity as the game shows it: 1 to 4 stars, coloured
|
||||
function rarity(value) {
|
||||
if (!value || value < 1) return '';
|
||||
return ' <span class="rarity-' + esc(value) + '" title="Rarity ' + esc(value) + '" aria-label="Rarity ' + esc(value) + '">' + '\u2605'.repeat(Math.min(value, 5)) + '</span>';
|
||||
}
|
||||
|
||||
function icon(lot) {
|
||||
return '<img class="cdc-icon" src="/api/icon/' + esc(lot) + '" alt="" loading="lazy" onerror="this.remove()">';
|
||||
}
|
||||
|
||||
function iconById(id, size) {
|
||||
if (!id || id <= 0) return '';
|
||||
return '<img src="/api/icon_id/' + esc(id) + '" alt="" width="' + (size || 20) + '" height="' + (size || 20) + '" class="me-1 rounded" loading="lazy" onerror="this.remove()">';
|
||||
}
|
||||
|
||||
// An object with its icon and rarity; other links as they are
|
||||
function item(ref) {
|
||||
if (!ref) return '<span class="text-body-secondary">-</span>';
|
||||
if (ref.link !== 'object') return ref.link ? refLink(ref) : '<span class="font-monospace">' + esc(ref.id) + '</span>';
|
||||
return '<a href="' + hashFor('object', ref.id) + '">' + icon(ref.id) + esc(ref.name) + '</a>' + rarity(ref.rarity) + ' <span class="text-body-secondary small">' + esc(ref.id) + '</span>';
|
||||
}
|
||||
|
||||
function items(refs, count) {
|
||||
if (!refs || !refs.length) return '<span class="text-body-secondary">None</span>';
|
||||
return refs.map(function (r) { return '<div>' + item(r.item || r) + (count && r.count !== undefined ? ' × ' + num(r.count) : '') + '</div>'; }).join('');
|
||||
}
|
||||
|
||||
function none(text) { return '<span class="text-body-secondary">' + (text || 'None') + '</span>'; }
|
||||
|
||||
function raw(title, body) {
|
||||
return '<details class="mt-2"><summary class="small text-body-secondary">' + title + '</summary><div class="mt-2">' + body + '</div></details>';
|
||||
}
|
||||
|
||||
// Like raw, but the body is only built when opened (loot lists can run to thousands of rows)
|
||||
var lazy = {}, lazyCount = 0;
|
||||
function later(title, render) {
|
||||
lazy[++lazyCount] = render;
|
||||
return '<details class="mt-2" data-lazy="' + lazyCount + '"><summary class="small text-body-secondary">' + title + '</summary><div class="mt-2"></div></details>';
|
||||
}
|
||||
|
||||
view.addEventListener('toggle', function (e) {
|
||||
var details = e.target;
|
||||
if (!details.open || !details.dataset || !lazy[details.dataset.lazy]) return;
|
||||
details.lastElementChild.innerHTML = lazy[details.dataset.lazy]();
|
||||
delete lazy[details.dataset.lazy];
|
||||
}, true);
|
||||
|
||||
function table(head, rows, cls) {
|
||||
return '<div class="cdc-rows-wrap"><table class="table table-sm align-middle mb-0 ' + (cls || '') + '"><thead><tr>' + head.map(function (h) {
|
||||
return '<th' + (h.num ? ' class="num"' : '') + '>' + h.text + '</th>';
|
||||
}).join('') + '</tr></thead><tbody>' + rows.join('') + '</tbody></table></div>';
|
||||
}
|
||||
|
||||
function stat(label, value) {
|
||||
return '<div class="col"><div class="small text-body-secondary">' + label + '</div><div class="cdc-stat">' + value + '</div></div>';
|
||||
}
|
||||
|
||||
// Mission tasks that count something
|
||||
function countedBy(tasks) {
|
||||
if (!tasks || !tasks.length) return '';
|
||||
return card('Counted by mission tasks', tasks.map(function (t) {
|
||||
return '<div>' + refLink(t.mission) + ' <span class="badge bg-body-tertiary text-body border">' + esc(t.taskType) + '</span>' +
|
||||
(t.targetValue ? ' <span class="small text-body-secondary">× ' + num(t.targetValue) + '</span>' : '') + '</div>';
|
||||
}).join(''));
|
||||
}
|
||||
|
||||
/**
|
||||
* A loot matrix as the server rolls it: each item's chance of dropping at least once and how many on average, and
|
||||
* under that how each entry rolls. `per` names what one roll is (a smash, an opened package, an activity reward).
|
||||
*/
|
||||
function lootOdds(odds, per) {
|
||||
if (!odds || !odds.entries || !odds.entries.length) return none('No loot matrix rows');
|
||||
var rows = odds.items.map(function (i) {
|
||||
return '<tr><td>' + item(i.item) + '</td><td class="num">' + pct(i.chance) + '</td><td class="num">' + num(+i.expected.toPrecision(3)) + '</td></tr>';
|
||||
});
|
||||
var shown = rows.length > 25 ? rows.slice(0, 25) : rows;
|
||||
var html = table([{ text: 'Item' }, { text: 'Chance per ' + esc(per), num: true }, { text: 'Average count', num: true }], shown, 'cdc-odds');
|
||||
if (rows.length > shown.length) html += later('All ' + rows.length + ' items', function () { return table([{ text: 'Item' }, { text: 'Chance', num: true }, { text: 'Average', num: true }], rows, 'cdc-odds'); });
|
||||
html += later('How it rolls (' + odds.entries.length + (odds.entries.length === 1 ? ' entry' : ' entries') + ')', function () { return odds.entries.map(function (e) {
|
||||
return '<div class="mb-3"><div><a href="' + hashFor('loot_table', e.lootTable) + '">Loot table ' + esc(e.lootTable) + '</a>: rolls with ' + pct(e.percent) + ', then ' +
|
||||
(e.minToDrop === e.maxToDrop ? esc(e.minToDrop) : esc(e.minToDrop) + '–' + esc(e.maxToDrop)) + ' drop(s)' +
|
||||
(e.flagID ? ' <span class="small text-body-secondary">(flagID ' + esc(e.flagID) + ' in the row; the server\'s roll doesn\'t check it)</span>' : '') + '</div>' +
|
||||
'<div class="small text-body-secondary">Each drop rolls a rarity (<a href="' + hashFor('rarity_table', e.rarityTable) + '">rarity table ' + esc(e.rarityTable) + '</a>): ' +
|
||||
e.rolledRarity.map(function (r) { return rarity(r.rarity).trim() + ' ' + pct(r.chance); }).join(', ') +
|
||||
', then picks evenly among the items of that rarity (or the next lower one the table has).</div>' +
|
||||
table([{ text: 'Item' }, { text: 'Per drop', num: true }, { text: 'Chance', num: true }, { text: '' }], e.items.map(function (i) {
|
||||
return '<tr><td>' + item(i.item) + '</td><td class="num">' + pct(i.perDrop) + '</td><td class="num">' + pct(i.chance) + '</td><td>' +
|
||||
(i.missionDrop ? '<span class="badge bg-body-tertiary text-body border" title="Dropped only while a mission needs it">mission item</span>' : '') + '</td></tr>';
|
||||
}), 'cdc-odds') + '</div>';
|
||||
}).join(''); }) + '<div class="small text-body-secondary mt-2">Chances before any live event loot bonus. Mission items drop only for players whose mission needs them.</div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
// ---- views ----
|
||||
|
||||
function objectView(lot) {
|
||||
api.get('/api/cdclient/objects/' + lot).then(function (d) {
|
||||
if (api404(d)) return showError(d.error);
|
||||
var sub = [esc(d.row.type || '')];
|
||||
if (d.item) sub.push(esc(d.item.itemTypeName));
|
||||
var html = header('<img src="/api/icon/' + lot + '" alt="" width="48" height="48" class="me-2 rounded" onerror="this.remove()">' + esc(d.name) +
|
||||
(d.item ? rarity(d.item.rarity) : '') + ' <span class="text-body-secondary fs-5">' + lot + '</span>', sub.filter(Boolean).join(' · ') +
|
||||
(d.row._internalNotes ? ' · ' + esc(d.row._internalNotes) : ''));
|
||||
if (d.description) html += '<p class="text-body-secondary">' + esc(d.description) + '</p>';
|
||||
|
||||
var left = '', right = '';
|
||||
if (d.item) left += itemCard(d.item);
|
||||
if (d.destructible) left += destructibleCard(d.destructible);
|
||||
d.loot.forEach(function (l) {
|
||||
left += card('Drops (' + esc(l.table) + ', <a href="' + hashFor('loot_matrix', l.matrix) + '">loot matrix ' + esc(l.matrix) + '</a>)',
|
||||
lootOdds(l.odds, 'roll'));
|
||||
});
|
||||
if (d.vendor) left += vendorCard(d.vendor);
|
||||
if (d.skills.length) left += skillsCard(d.skills);
|
||||
if (d.inventory) left += card('Starts with (inventory component)', d.inventory.map(function (i) {
|
||||
return '<div>' + item(i.item) + ' × ' + num(i.count) + (i.equip ? ' <span class="badge bg-body-tertiary text-body border">equipped</span>' : '') + '</div>';
|
||||
}).join(''));
|
||||
if (d.missions) left += card('Missions here', d.missions.map(function (m) {
|
||||
return '<div>' + refLink(m.mission) + (m.offers ? ' <span class="badge text-bg-primary">offers</span>' : '') + (m.accepts ? ' <span class="badge text-bg-success">takes turn-ins</span>' : '') + '</div>';
|
||||
}).join(''));
|
||||
if (d.collectibleMission) left += card('Collectible', 'Needs ' + refLink(d.collectibleMission));
|
||||
|
||||
right += d.model
|
||||
? card('Model', '<div id="modelViewer" class="cdc-model rounded overflow-hidden bg-body-tertiary d-flex align-items-center justify-content-center">' +
|
||||
'<button class="btn btn-sm btn-outline-primary" id="showModel">Show in 3D</button></div><div class="small text-body-secondary mt-1">' + esc(d.model) + '</div>')
|
||||
: '';
|
||||
right += sourcesCard(d.sources);
|
||||
if (d.rewardedBy.length) right += card('Mission rewards', d.rewardedBy.map(function (r) {
|
||||
return '<div>' + refLink(r.mission) + ' × ' + num(r.count) + (r.repeat ? ' <span class="badge bg-body-tertiary text-body border">on repeats</span>' : '') +
|
||||
(r.choice ? ' <span class="badge bg-body-tertiary text-body border">one of a choice</span>' : '') + '</div>';
|
||||
}).join(''));
|
||||
right += countedBy(d.countedBy);
|
||||
right += card('Components', d.components.length ? d.components.map(function (c) {
|
||||
var title = '<span class="fw-semibold">' + esc(c.typeName) + '</span> <span class="text-body-secondary">type ' + c.type +
|
||||
', id ' + c.componentId + (c.table ? ' in <a href="' + tableHash(c.table, { filters: [{ column: 'id', op: '=', value: String(c.componentId) }] }) + '">' + esc(c.table) + '</a>' : ', no table') + '</span>';
|
||||
// Nothing to unfold without rows (components with no table of their own)
|
||||
if (!c.rows.length) return '<div class="mb-2 ps-3">' + title + '</div>';
|
||||
return '<details class="mb-2"><summary>' + title + '</summary>' +
|
||||
(c.rows.length === 1 ? keyValues(c.rows[0], c.columns, c.table) : rowsTable(c.rows, c.table, c.columns)) + '</details>';
|
||||
}).join('') : none());
|
||||
right += card('Objects row', raw('All columns', keyValues(d.row, d.columns, 'Objects'))) + localized(d.localized);
|
||||
|
||||
html += '<div class="row g-3"><div class="col-xl-6">' + left + '</div><div class="col-xl-6">' + right + '</div></div>';
|
||||
html += usedBy(d.usedBy, 'object', lot);
|
||||
view.innerHTML = html;
|
||||
var button = document.getElementById('showModel');
|
||||
if (button) button.addEventListener('click', function () { showModel(lot, d.name); });
|
||||
});
|
||||
}
|
||||
|
||||
function itemCard(i) {
|
||||
var rows = [
|
||||
['Type', esc(i.itemTypeName) + ' <span class="text-body-secondary small">(' + esc(i.itemType) + ')</span>'],
|
||||
['Equips to', i.equipLocation ? '<span class="font-monospace">' + esc(i.equipLocation) + '</span>' : none('Not equipped')],
|
||||
['Stack size', num(i.stackSize)],
|
||||
['Base value', i.baseValue >= 0 ? num(i.baseValue) + ' coins <span class="small text-body-secondary">(a vendor charges it × its buy scalar)</span>' : none('No coin price')]
|
||||
];
|
||||
if (i.price.altCurrency) rows.push(['Also costs', item(i.price.altCurrency.item) + ' × ' + num(i.price.altCurrency.base) + ' <span class="small text-body-secondary">(× buy scalar)</span>']);
|
||||
if (i.price.costs.length) rows.push(['Crafting costs', items(i.price.costs, true)]);
|
||||
if (i.commendation) rows.push(['Commendation cost', item(i.commendation.item) + ' × ' + num(i.commendation.count)]);
|
||||
if (i.subItems.length) rows.push(['Equips with it', items(i.subItems)]);
|
||||
if (i.preconditions.length) rows.push(['Preconditions', i.preconditions.map(function (p) {
|
||||
return '<div><a href="' + tableHash('Preconditions', { filters: [{ column: 'id', op: '=', value: String(p.id) }] }) + '">' + esc(p.id) + '</a> ' + esc(p.reason) + '</div>';
|
||||
}).join('')]);
|
||||
var stats = i.stats || {};
|
||||
var bonuses = ['life', 'armor', 'imagination'].filter(function (k) { return stats[k]; }).map(function (k) { return '+' + esc(stats[k]) + ' ' + k; });
|
||||
if (bonuses.length) rows.push(['Stats', bonuses.join(', ')]);
|
||||
(stats.skills || []).forEach(function (s) { rows.push(['Skill', iconById(s.icon) + '<a href="' + hashFor('skill', s.id) + '">' + esc(s.description) + '</a>']); });
|
||||
if (i.set) rows.push(['Item set', iconById(i.set.icon) + esc(i.set.name || 'Set ' + i.set.id) + ' <span class="small text-body-secondary">rank ' + esc(i.set.rank) + '</span>' +
|
||||
i.set.bonuses.map(function (b) {
|
||||
var parts = ['life', 'armor', 'imagination'].filter(function (k) { return b.stats[k]; }).map(function (k) { return '+' + esc(b.stats[k]) + ' ' + k; })
|
||||
.concat((b.stats.skills || []).map(function (s) { return esc(s.description); }));
|
||||
return '<div class="small">' + esc(b.pieces) + ' pieces: ' + parts.join(', ') + '</div>';
|
||||
}).join('')]);
|
||||
return card('Item', '<table class="table table-sm mb-0"><tbody>' + rows.map(function (r) {
|
||||
return '<tr><th class="fw-normal text-body-secondary" style="width: 10rem">' + r[0] + '</th><td>' + r[1] + '</td></tr>';
|
||||
}).join('') + '</tbody></table>');
|
||||
}
|
||||
|
||||
function destructibleCard(x) {
|
||||
return card('Health and smashing (destructible component)', '<div class="row text-center mb-2">' +
|
||||
stat('Life', num(x.life)) + stat('Armor', num(x.armor)) + stat('Imagination', num(x.imagination)) + stat('Level', num(x.level)) + '</div>' +
|
||||
'<div>Coins when smashed: ' + (x.coins ? num(x.coins.min) + '–' + num(x.coins.max) : none()) +
|
||||
' <span class="small text-body-secondary">(currency table at its level)</span></div>' +
|
||||
'<div class="small text-body-secondary">Faction ' + esc(x.faction) + (x.isSmashable ? ' · smashable' : '') + '</div>');
|
||||
}
|
||||
|
||||
function vendorCard(v) {
|
||||
var scalar = v.buyScalar ? 'prices are the base value × ' + num(v.buyScalar) : 'buy scalar 0: prices use the world\'s vendor_buy_multiplier';
|
||||
return card('Sells (vendor component, <a href="' + hashFor('loot_matrix', v.matrix) + '">loot matrix ' + esc(v.matrix) + '</a>)',
|
||||
'<div class="small text-body-secondary mb-2">' + scalar + '; buys back at × ' + num(v.sellScalar) +
|
||||
(v.refreshTimeSeconds > 0 ? '; restocks every ' + num(v.refreshTimeSeconds / 60) + ' min' : '') + '</div>' +
|
||||
v.stock.map(function (e) {
|
||||
return '<div class="mb-2"><div class="small"><a href="' + hashFor('loot_table', e.lootTable) + '">Loot table ' + esc(e.lootTable) + '</a>: ' +
|
||||
(e.all ? 'all of it' : (e.minToDrop === e.maxToDrop ? esc(e.minToDrop) : esc(e.minToDrop) + '–' + esc(e.maxToDrop)) + ' of its ' + e.items.length + ' items at random') + '</div>' +
|
||||
table([{ text: 'Item' }, { text: 'Price', num: true }, { text: 'In stock', num: true }], e.items.map(function (i) {
|
||||
var price = i.price.coins !== null ? num(i.price.coins) + ' coins' : '';
|
||||
if (i.price.altCurrency) price += (price ? ' + ' : '') + num(i.price.altCurrency.count !== null ? i.price.altCurrency.count : i.price.altCurrency.base) + ' ' + esc(i.price.altCurrency.item.name);
|
||||
i.price.costs.forEach(function (c) { price += (price ? ' + ' : '') + num(c.count) + ' ' + esc(c.item.name); });
|
||||
return '<tr><td>' + item(i.item) + '</td><td class="num">' + (price || none('-')) + '</td><td class="num">' + (i.sold ? pct(i.chance) : '<span class="text-body-secondary" title="No item component: vendors skip it">never</span>') + '</td></tr>';
|
||||
}), 'cdc-odds') + '</div>';
|
||||
}).join(''));
|
||||
}
|
||||
|
||||
function skillsCard(skills) {
|
||||
return card('Skills', table([{ text: 'Skill' }, { text: 'Cost', num: true }, { text: 'Cooldown', num: true }, { text: 'castOnType <span class="fw-normal text-body-secondary">(raw)</span>', num: true }],
|
||||
skills.map(function (s) {
|
||||
return '<tr><td>' + iconById(s.skillIcon) + refLink(s.skill) + (s.description ? '<div class="small text-body-secondary">' + esc(s.description) + '</div>' : '') +
|
||||
(s.behaviorID ? '<div class="small"><a href="' + hashFor('behavior', s.behaviorID) + '">behavior ' + esc(s.behaviorID) + '</a></div>' : '') + '</td>' +
|
||||
'<td class="num">' + (s.imaginationcost ? num(s.imaginationcost) + ' imagination' : '') + '</td><td class="num">' + (s.cooldown ? num(s.cooldown) + ' s' : '') + '</td><td class="num">' + esc(s.castOnType) + '</td></tr>';
|
||||
})));
|
||||
}
|
||||
|
||||
// Where an object comes from: drops, vendors, packages, activity rewards; with the chances
|
||||
function sourcesCard(sources) {
|
||||
if (!sources.length) return '';
|
||||
return card('Comes from', sources.map(function (s) {
|
||||
var columns = columnsOf(s.table);
|
||||
var rows = s.matrices.map(function (m) {
|
||||
// Objects with the component, or the rows themselves (with their links) for other tables
|
||||
var users = m.objects.slice(0, 5).map(function (o) { return '<div>' + item(o) + '</div>'; }).join('') +
|
||||
(m.objects.length > 5 ? '<div class="small text-body-secondary">and ' + (m.objects.length - 5) + ' more</div>' : '') +
|
||||
(m.rows || []).map(function (r) {
|
||||
return '<div class="small">' + columns.filter(function (c) { return r[c.name] !== null && r[c.name] !== '' && r[c.name] !== undefined; }).map(function (c) {
|
||||
return '<span class="text-body-secondary">' + esc(c.name) + '</span> ' + cell(r[c.name], c, r, s.table);
|
||||
}).join(' · ') + '</div>';
|
||||
}).join('');
|
||||
return '<tr><td><a href="' + hashFor('loot_matrix', m.matrix) + '">' + esc(m.matrix) + '</a></td><td class="num">' +
|
||||
(m.chance !== undefined ? pct(m.chance) : '<span class="text-body-secondary" title="Only the first matrices\' odds are worked out">?</span>') +
|
||||
'</td><td class="num">' + (m.expected !== undefined ? num(+m.expected.toPrecision(3)) : '') + '</td><td>' + (users || none()) + '</td></tr>';
|
||||
});
|
||||
var head = [{ text: 'Loot matrix' }, { text: s.vendor ? 'In stock' : 'Chance', num: true }, { text: s.vendor ? '' : 'Average', num: true }, { text: 'Used by' }];
|
||||
return '<div class="mb-3"><div class="small text-body-secondary mb-1">Through ' + esc(s.table) + '.' + esc(s.column) + ' (' + rows.length + ')</div>' +
|
||||
table(head, rows.slice(0, 10), 'cdc-odds') +
|
||||
(rows.length > 10 ? later('All ' + rows.length, function () { return table(head, rows, 'cdc-odds'); }) : '') + '</div>';
|
||||
}).join('') + '<div class="small text-body-secondary">Chance per smash, opened package or activity reward, before live event bonuses; for vendors, the chance it is in stock.</div>');
|
||||
}
|
||||
|
||||
function showModel(lot, name) {
|
||||
var container = document.getElementById('modelViewer');
|
||||
container.textContent = 'Loading…';
|
||||
import('/js/lddviewer.js').then(function (module) {
|
||||
disposeViewer();
|
||||
viewer = module.createViewer(container, {});
|
||||
return viewer.load([{ id: lot, name: name, lot: lot, position: [0, 0, 0], rotation: [0, 0, 0, 1], url: '/api/cdclient/objects/' + lot + '/lxfml' }], 1);
|
||||
}).catch(function (e) { container.textContent = 'The 3D view could not load: ' + e.message; });
|
||||
}
|
||||
|
||||
// Prerequisites as the server reads them: each term joined to all the rest, so "a or b and c" is a or (b and c)
|
||||
function prerequisites(terms, text) {
|
||||
if (!terms.length || (terms.length === 1 && !terms[0].mission)) return none('None');
|
||||
function term(t) {
|
||||
if (!t.mission) return '<span class="badge text-bg-success">always met</span>';
|
||||
return '<span class="cdc-chip badge bg-body-tertiary text-body border">' + refLink(t.mission) + (t.stateName ? ' in state ' + esc(t.stateName) : ' complete') + '</span>';
|
||||
}
|
||||
function from(i) {
|
||||
var t = terms[i];
|
||||
if (i === terms.length - 1) return term(t);
|
||||
var rest = from(i + 1);
|
||||
// Brackets where the rest joins differently than this term does
|
||||
if (i + 1 < terms.length - 1 && terms[i + 1].or !== t.or) rest = '( ' + rest + ' )';
|
||||
return term(t) + ' <b>' + (t.or ? 'or' : 'and') + '</b> ' + rest;
|
||||
}
|
||||
return '<div class="cdc-expr">' + from(0) + '</div><div class="small text-body-secondary mt-1">As the server reads <span class="font-monospace">' + esc(text) + '</span>: ' +
|
||||
'each mission is joined to everything after it, and brackets don\'t group.</div>';
|
||||
}
|
||||
|
||||
function taskCard(tasks) {
|
||||
if (!tasks.length) return card('Tasks', none());
|
||||
return card('Tasks', tasks.map(function (t) {
|
||||
var parts = [];
|
||||
if (t.targets.length) parts.push('<div><span class="small text-body-secondary">Targets:</span> ' + t.targets.map(item).join(', ') + '</div>');
|
||||
if (t.targetText) parts.push('<div><span class="small text-body-secondary">Target group:</span> <span class="font-monospace">' + esc(t.targetText) + '</span></div>');
|
||||
if (t.parameters.length) parts.push('<div><span class="small text-body-secondary">Parameters:</span> ' + t.parameters.map(item).join(', ') + '</div>');
|
||||
if (t.racingParameter) parts.push('<div><span class="small text-body-secondary">Racing task:</span> ' + esc(t.racingParameter) + '</div>');
|
||||
return '<div class="mb-3">' + iconById(t.iconID) + '<span class="badge text-bg-secondary">' + esc(t.taskTypeName) + '</span> ' +
|
||||
'<span class="fw-semibold">× ' + num(t.targetValue) + '</span> ' + esc(t.description || '') +
|
||||
(t.progressed ? '' : ' <span class="badge text-bg-warning" title="MissionTask::Progress has no case for this type">the server never progresses this type</span>') +
|
||||
parts.join('') + raw('MissionTasks row (uid ' + esc(t.uid) + ')', keyValues(t.row, columnsOf('MissionTasks'), 'MissionTasks')) + '</div>';
|
||||
}).join(''));
|
||||
}
|
||||
|
||||
function rewardsCard(r, row) {
|
||||
var choice = row.isChoiceReward ? ' <span class="small text-body-secondary">(the player picks one)</span>' : '';
|
||||
var first = '<h6 class="small text-body-secondary">First completion' + choice + '</h6>' + (r.items.length ? items(r.items, true) : '');
|
||||
var extras = [];
|
||||
if (row.reward_currency > 0) extras.push(num(row.reward_currency) + ' coins');
|
||||
if (row.LegoScore > 0) extras.push(num(row.LegoScore) + ' U-score');
|
||||
if (row.reward_reputation > 0) extras.push(num(row.reward_reputation) + ' reputation');
|
||||
first += extras.length ? '<div>' + extras.join(', ') + '</div>' : '';
|
||||
if (r.emotes.length) first += '<div><span class="small text-body-secondary">Emotes:</span> ' + r.emotes.map(refLink).join(', ') + '</div>';
|
||||
if (r.stats.length) first += r.stats.map(function (s) { return '<div><span class="font-monospace small">' + esc(s.column) + '</span> +' + num(s.value) + '</div>'; }).join('');
|
||||
if (!r.items.length && !extras.length && !r.emotes.length && !r.stats.length) first += none();
|
||||
var html = first;
|
||||
if (row.repeatable) {
|
||||
var again = [];
|
||||
if (row.reward_currency_repeatable > 0) again.push(num(row.reward_currency_repeatable) + ' coins');
|
||||
if (row.LegoScore > 0) again.push(num(row.LegoScore) + ' U-score');
|
||||
if (row.reward_reputation > 0) again.push(num(row.reward_reputation) + ' reputation');
|
||||
html += '<h6 class="small text-body-secondary mt-3">Each repeat' + choice + '</h6>' + items(r.repeatItems, true) + (again.length ? '<div>' + again.join(', ') + '</div>' : '');
|
||||
}
|
||||
return card('Rewards', html + '<div class="small text-body-secondary mt-2">At the level cap U-score is paid as coins.</div>');
|
||||
}
|
||||
|
||||
function missionView(id) {
|
||||
api.get('/api/cdclient/missions/' + id).then(function (d) {
|
||||
if (api404(d)) return showError(d.error);
|
||||
var row = d.row;
|
||||
var badges = [row.isMission ? 'Mission' : 'Achievement'];
|
||||
if (row.repeatable) badges.push('Repeatable' + (row.cooldownTime > 0 ? ' every ' + num(row.cooldownTime) + ' min' : ''));
|
||||
if (row.time_limit > 0) badges.push('time_limit ' + num(row.time_limit));
|
||||
var html = header(iconById(row.missionIconID, 40) + esc(d.name) + ' <span class="text-body-secondary fs-5">' + id + '</span>',
|
||||
esc([row.defined_type, row.defined_subtype].filter(Boolean).join(' / ')) + ' ' + badges.map(function (b) { return '<span class="badge bg-body-tertiary text-body border">' + b + '</span>'; }).join(' '));
|
||||
html += '<div class="row g-3"><div class="col-xl-6">';
|
||||
html += taskCard(d.tasks) + rewardsCard(d.rewards, row);
|
||||
html += card('Prerequisites', prerequisites(d.prerequisites, row.prereqMissionID || ''));
|
||||
html += card('Unlocks', chips(d.unlocks)) + countedBy(d.countedBy);
|
||||
html += '</div><div class="col-xl-6">';
|
||||
html += card('Offered by', chips(d.offeredBy)) + card('Turned in to', chips(d.acceptedBy));
|
||||
html += localized(d.text) + localized(d.localized) + card('Missions row', raw('All columns', keyValues(row, d.columns, 'Missions'))) + '</div></div>';
|
||||
html += usedBy(d.usedBy, 'mission', id);
|
||||
view.innerHTML = html;
|
||||
});
|
||||
}
|
||||
|
||||
// A behavior tree, nested; each behavior with its template (from the server's enum) and parameters
|
||||
function behaviorTree(tree) {
|
||||
var shown = {};
|
||||
function node(behaviorId, label) {
|
||||
var n = tree.nodes[String(behaviorId)];
|
||||
var title = (label ? '<span class="text-body-secondary">' + esc(label) + ':</span> ' : '') +
|
||||
'<a href="' + hashFor('behavior', behaviorId) + '">' + esc(behaviorId) + '</a> ';
|
||||
if (!n) return '<div class="ms-3">' + title + '<span class="text-danger">not in BehaviorTemplate' + (tree.truncated ? ' (or past the depth shown)' : '') + '</span></div>';
|
||||
title += '<span class="fw-semibold">' + esc(n.templateName) + '</span> <span class="small text-body-secondary">template ' + esc(n.templateID) + '</span>' +
|
||||
(n.effectID ? ' <span class="small text-body-secondary">effect ' + esc(n.effectID) + (n.effectHandle ? ' ' + esc(n.effectHandle) : '') + '</span>' : '');
|
||||
if (shown[behaviorId]) return '<div class="ms-3">' + title + ' <span class="small text-body-secondary">(shown above)</span></div>';
|
||||
shown[behaviorId] = true;
|
||||
var values = n.parameters.filter(function (p) { return p.child === null; });
|
||||
var children = n.parameters.filter(function (p) { return p.child !== null; });
|
||||
return '<details open><summary>' + title + '</summary>' +
|
||||
(values.length ? '<table class="params"><tbody>' + values.map(function (p) {
|
||||
return '<tr><td class="text-body-secondary">' + esc(p.name) + '</td><td>' + num(p.value) + '</td></tr>';
|
||||
}).join('') + '</tbody></table>' : '') +
|
||||
children.map(function (p) { return node(p.child, p.name); }).join('') + '</details>';
|
||||
}
|
||||
return '<div class="cdc-tree">' + node(tree.root, '') + '</div>' + (tree.truncated ? '<div class="small text-warning">Cut short: too big to show whole.</div>' : '');
|
||||
}
|
||||
|
||||
function skillView(id) {
|
||||
api.get('/api/cdclient/skills/' + id).then(function (d) {
|
||||
if (api404(d)) return showError(d.error);
|
||||
var row = d.row;
|
||||
var html = header(iconById(row.skillIcon, 40) + esc(d.name) + ' <span class="text-body-secondary fs-5">' + id + '</span>', esc(d.description || ''));
|
||||
html += '<div class="row text-center mb-3">' + stat('Imagination cost', num(row.imaginationcost || 0)) + stat('Cooldown', num(row.cooldown || 0) + ' s') +
|
||||
stat('Cooldown group', num(row.cooldowngroup || 0)) + '</div>';
|
||||
html += '<div class="row g-3"><div class="col-xl-7">' + card('Behavior tree' + (row.behaviorID ? ' <a href="' + hashFor('behavior', row.behaviorID) + '" class="small fw-normal">' + esc(row.behaviorID) + '</a>' : ''),
|
||||
d.tree ? behaviorTree(d.tree) : none('No behavior')) + '</div><div class="col-xl-5">';
|
||||
html += card('On objects', d.objects.length ? table([{ text: 'Object' }, { text: 'castOnType <span class="fw-normal text-body-secondary">(raw)</span>', num: true }], d.objects.map(function (o) {
|
||||
return '<tr><td>' + item(o.object) + '</td><td class="num">' + esc(o.castOnType) + '</td></tr>';
|
||||
})) : none());
|
||||
html += countedBy(d.countedBy) + localized(d.localized) + card('SkillBehavior row', raw('All columns', keyValues(row, d.columns, 'SkillBehavior'))) + '</div></div>';
|
||||
view.innerHTML = html + usedBy(d.usedBy, 'skill', id);
|
||||
});
|
||||
}
|
||||
|
||||
function behaviorView(id) {
|
||||
api.get('/api/cdclient/behaviors/' + id).then(function (d) {
|
||||
if (api404(d)) return showError(d.error);
|
||||
var count = Object.keys(d.tree.nodes).length;
|
||||
view.innerHTML = header('Behavior ' + id, nf.format(count) + (count === 1 ? ' behavior' : ' behaviors') + ' in this tree') +
|
||||
card('Tree', behaviorTree(d.tree)) + usedBy(d.usedBy, 'behavior', id);
|
||||
});
|
||||
}
|
||||
|
||||
function activityView(id) {
|
||||
api.get('/api/cdclient/activities/' + id).then(function (d) {
|
||||
if (api404(d)) return showError(d.error);
|
||||
var row = d.row;
|
||||
var html = header(esc(d.name) + ' <span class="text-body-secondary fs-5">' + id + '</span>', d.zone ? 'Played in ' + refLink(d.zone) : '');
|
||||
html += '<p>Costs to play: ' + (d.cost ? item(d.cost.item) + ' × ' + num(d.cost.count) : none('nothing')) + '</p>';
|
||||
var rewards = d.rewards.map(function (r, i) {
|
||||
return '<div class="mb-3"><div><span class="fw-semibold">Rating ' + esc(r.rating) + '</span>' + (i === 0 ? ' <span class="badge bg-body-tertiary text-body border" title="Activity instances give the first reward row">first row</span>' : '') +
|
||||
(r.description ? ' <span class="text-body-secondary">' + esc(r.description) + '</span>' : '') + '</div>' +
|
||||
'<div class="small">Coins: ' + (r.coins ? num(r.coins.min) + '–' + num(r.coins.max) : none()) + (r.matrix > 0 ? ' · <a href="' + hashFor('loot_matrix', r.matrix) + '">loot matrix ' + esc(r.matrix) + '</a>' : '') + '</div>' +
|
||||
(r.odds ? raw('Drops', lootOdds(r.odds, 'reward')) : '') + '</div>';
|
||||
}).join('');
|
||||
html += '<div class="row g-3"><div class="col-xl-7">' + card('Rewards', (rewards || none()) + '<div class="small text-body-secondary">Scripts that rate a result give the row with the highest rating at or below it; ' +
|
||||
'activity instances give the first row. Coins come from the currency table at level 1.</div>') + '</div><div class="col-xl-5">';
|
||||
html += countedBy(d.countedBy) + localized(d.localized) + card('Activities row', raw('All columns', keyValues(row, d.columns, 'Activities'))) + '</div></div>';
|
||||
view.innerHTML = html + usedBy(d.usedBy, 'activity', id);
|
||||
});
|
||||
}
|
||||
|
||||
function zoneView(id) {
|
||||
api.get('/api/cdclient/zones/' + id).then(function (d) {
|
||||
if (api404(d)) return showError(d.error);
|
||||
var c = d.caps;
|
||||
var override = function (value) { return value !== null ? ' <span class="badge text-bg-primary" title="Set on the Instances page">override</span>' : ''; };
|
||||
var html = header(esc(d.name) + ' <span class="text-body-secondary fs-5">' + id + '</span>', esc(d.row.mapFolder || '') + (d.row.zoneName ? ' · ' + esc(d.row.zoneName) : ''));
|
||||
html += '<div class="row g-3"><div class="col-xl-6">';
|
||||
html += card('Players per instance', '<div class="row text-center mb-2">' + stat('Soft cap' + override(c.softOverride), num(c.soft)) + stat('Hard cap' + override(c.hardOverride), num(c.hard)) + '</div>' +
|
||||
'<div class="small text-body-secondary">The master sends players to a running instance while it has fewer than the soft cap (friends joining them up to the hard cap), ' +
|
||||
'otherwise starts a new one. The ZoneTable says ' + num(c.clientSoft) + ' and ' + num(c.clientHard) + '.' +
|
||||
(c.overridesShown ? '' : ' Overrides set on the Instances page aren\'t shown without access to it.') + ' <a href="/instances">Instances</a></div>');
|
||||
html += card('Zone control object', d.control ? item(d.control) : none('None set: the server spawns its default zone control object'));
|
||||
html += '</div><div class="col-xl-6">' + localized(d.localized) + card('ZoneTable row', raw('All columns', keyValues(d.row, d.columns, 'ZoneTable'))) + '</div></div>';
|
||||
view.innerHTML = html + usedBy(d.usedBy, 'zone', id);
|
||||
});
|
||||
}
|
||||
|
||||
function lootMatrixView(id) {
|
||||
api.get('/api/cdclient/loot_matrix/' + id).then(function (d) {
|
||||
if (api404(d)) return showError(d.error);
|
||||
view.innerHTML = header('Loot matrix ' + id) + card('Drops', lootOdds(d.odds, 'roll')) + usedBy(d.usedBy, 'loot_matrix', id);
|
||||
});
|
||||
}
|
||||
|
||||
function lootTableView(id) {
|
||||
api.get('/api/cdclient/loot_table/' + id).then(function (d) {
|
||||
if (api404(d)) return showError(d.error);
|
||||
view.innerHTML = header('Loot table ' + id, nf.format(d.items.length) + ' items, highest rarity first as the server sorts them') +
|
||||
card('Items', table([{ text: 'Item' }, { text: 'Rarity', num: true }, { text: '' }], d.items.map(function (i) {
|
||||
return '<tr><td>' + item(i.item) + '</td><td class="num">' + (rarity(i.rarity) || none('0')) + '</td><td>' +
|
||||
(i.missionDrop ? '<span class="badge bg-body-tertiary text-body border" title="Dropped only while a mission needs it">mission item</span>' : '') + '</td></tr>';
|
||||
}), 'cdc-odds') + '<div class="small text-body-secondary mt-2">The chance of each depends on the loot matrix and rarity table it is rolled with: open one of the matrices below.</div>') +
|
||||
usedBy(d.usedBy, 'loot_table', id);
|
||||
});
|
||||
}
|
||||
|
||||
// ---- routing ----
|
||||
|
||||
function route() {
|
||||
disposeViewer();
|
||||
lazy = {};
|
||||
var hash = window.location.hash.replace(/^#\/?/, '');
|
||||
var queryAt = hash.indexOf('?');
|
||||
var path = (queryAt === -1 ? hash : hash.slice(0, queryAt)).split('/').map(decodeURIComponent);
|
||||
@@ -689,28 +170,15 @@
|
||||
});
|
||||
view.innerHTML = '<div class="text-body-secondary">Loading…</div>';
|
||||
window.scrollTo(0, 0);
|
||||
if (path[0] === '') return home();
|
||||
if (path[0] === 'table') return tableView(path[1], params);
|
||||
// #/object/1727 and the like: the rows it names, without adding a history entry
|
||||
var id = parseInt(path[1], 10);
|
||||
switch (path[0]) {
|
||||
case '': return home();
|
||||
case 'search': return search(path.slice(1).join('/'));
|
||||
case 'table': return tableView(path[1], params);
|
||||
case 'object': return objectView(id);
|
||||
case 'mission': return missionView(id);
|
||||
case 'skill': return skillView(id);
|
||||
case 'behavior': return behaviorView(id);
|
||||
case 'activity': return activityView(id);
|
||||
case 'zone': return zoneView(id);
|
||||
case 'loot_matrix': return lootMatrixView(id);
|
||||
case 'loot_table': return lootTableView(id);
|
||||
default: return showError('Nothing here.');
|
||||
}
|
||||
var target = !isNaN(id) && hashFor(path[0], id);
|
||||
if (target) return window.location.replace(target);
|
||||
showError('Nothing here.');
|
||||
}
|
||||
|
||||
document.getElementById('searchForm').addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
var text = document.getElementById('searchInput').value.trim();
|
||||
if (text) window.location.hash = '#/search/' + encodeURIComponent(text);
|
||||
});
|
||||
window.addEventListener('hashchange', route);
|
||||
|
||||
api.get('/api/cdclient/schema').then(function (d) {
|
||||
|
||||
Reference in New Issue
Block a user