/** * 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/ search results * #/table/?... a table's rows (start, order, dir, q, filters as JSON [{column, op, value}]) * #// 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. */ (function () { 'use strict'; var VIEWS = ['object', 'mission', 'skill', 'behavior', 'activity', 'zone', 'loot_matrix', 'loot_table']; var PAGE = 50; 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) { var v = params[k]; if (v === undefined || v === null || v === '' || (Array.isArray(v) && !v.length)) return; query.push(k + '=' + encodeURIComponent(Array.isArray(v) ? JSON.stringify(v) : v)); }); return '#/table/' + encodeURIComponent(table) + (query.length ? '?' + query.join('&') : ''); } function refLink(ref) { var href = hashFor(ref.link, ref.id); var text = esc(ref.name) + ' ' + esc(ref.id) + ''; return href ? '' + text + '' : text; } function chips(refs) { if (!refs || !refs.length) return 'None'; return refs.map(function (r) { return '' + refLink(r) + ''; }).join(''); } function componentTable(type) { var match = schema.componentTypes.filter(function (c) { return c.type === type; })[0]; return match ? match : null; } // One cell: linked when its column points somewhere function cell(value, column, row, table) { if (value === null || value === undefined) return 'NULL'; // REAL columns hold 32-bit floats: show the digits they actually have (0.4, not 0.4000000059604645) var text = esc(column && column.type === 'REAL' && typeof value === 'number' ? +value.toPrecision(7) : value); if (column && column.link && value !== 0 && value !== -1) { var href = hashFor(column.link, value); if (href) return '' + text + ''; } // ComponentsRegistry: the type by name, and the component's row in its table if (table === 'ComponentsRegistry' && column) { if (column.name === 'component_type') { var type = componentTable(value); return text + (type ? ' ' + esc(type.name) + '' : ''); } if (column.name === 'component_id') { var owner = componentTable(row.component_type); if (owner && owner.table) return '' + text + ''; } } 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 '' + columns.map(function (c) { return ''; }).join('') + '
' + esc(c.name) + (c.link ? ' ' + esc(c.link) + '' : '') + '' + cell(row[c.name], c, row, table) + '
'; } // Rows as a table, with the schema's links function rowsTable(rows, table, columns) { if (!rows.length) return '
No rows.
'; columns = columns || (table ? columnsOf(table) : null) || []; if (!columns.length) columns = Object.keys(rows[0]).map(function (k) { return { name: k }; }); return '
' + columns.map(function (c) { return ''; }).join('') + '' + rows.map(function (row) { return '' + columns.map(function (c) { return ''; }).join('') + ''; }).join('') + '
' + esc(c.name) + '
' + cell(row[c.name], c, row, table) + '
'; } function card(title, body, extra) { return '
' + title + '
' + (extra || '') + '
' + body + '
'; } function localized(texts) { var keys = Object.keys(texts || {}); if (!keys.length) return ''; return card('Texts (locale)', '' + keys.map(function (k) { return ''; }).join('') + '
' + esc(k) + '' + esc(texts[k]) + '
'); } // "Used by": everything pointing here, per table and column function usedBy(references, link, id) { if (!references || !references.length) return card('Used by', 'Nothing in the CDClient points here.'); return card('Used by', references.map(function (r) { var href = tableHash(r.table, { filters: [{ column: r.column, op: '=', value: String(id) }] }); return '
' + esc(r.table) + '.' + esc(r.column) + ' ' + nf.format(r.count) + '' + (r.objects && r.objects.length ? ' on ' + r.objects.length + ' object(s)' : '') + '' + (r.objects && r.objects.length ? '
' + chips(r.objects) + '
' : '') + rowsTable(r.rows, r.table) + (r.count > r.rows.length ? '
First ' + r.rows.length + ' of ' + nf.format(r.count) + '; see all.
' : '') + '
'; }).join('')); } function header(title, subtitle) { return '

' + title + '

' + (subtitle ? '
' + subtitle + '
' : '') + '
'; } function showError(message) { view.innerHTML = '
' + esc(message) + '
'; } function disposeViewer() { if (viewer) { viewer.dispose(); viewer = null; } } // ---- views ---- function home() { var tables = schema.tables; view.innerHTML = card('Tables ' + tables.length + '', '' + ''); document.getElementById('tableFilter').addEventListener('input', function (e) { var text = e.target.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], '
' + s[1].map(function (r) { return '
' + esc(r.link) + '' + (r.link === 'object' ? item(r) : refLink(r)) + '
'; }).join('') + '
'); }).join('') : '
Nothing found.
'); }); } var OPS = ['=', '!=', '<', '<=', '>', '>=', 'contains', 'starts', 'null', 'notnull']; function tableView(name, params) { var start = parseInt(params.start || '0', 10) || 0; var filters = []; try { filters = params.filters ? JSON.parse(params.filters) : []; } catch (e) { filters = []; } var url = '/api/cdclient/tables/' + encodeURIComponent(name) + '/rows?start=' + start + '&length=' + PAGE + (params.order ? '&order=' + encodeURIComponent(params.order) + '&dir=' + (params.dir || 'asc') : '') + (params.q ? '&q=' + encodeURIComponent(params.q) : '') + (filters.length ? '&filters=' + encodeURIComponent(JSON.stringify(filters)) : ''); api.get(url).then(function (d) { if (api404(d)) return showError(d.error); var state = { filters: filters, q: params.q || '', order: params.order || '', dir: params.dir || 'asc' }; var go = function (changes) { window.location.hash = tableHash(name, Object.assign({ q: state.q, filters: state.filters, order: state.order, dir: state.order ? state.dir : '', start: 0 }, changes)); }; var columnOptions = d.columns.map(function (c) { return ''; }).join(''); var filterChips = filters.map(function (f, i) { return '' + esc(f.column) + ' ' + esc(f.op) + ' ' + esc(f.value || '') + ' '; }).join(''); var pages = Math.max(1, Math.ceil(d.total / PAGE)), page = Math.floor(start / PAGE) + 1; var head = '' + d.columns.map(function (c) { var arrow = state.order === c.name ? (state.dir === 'desc' ? ' ↓' : ' ↑') : ''; return '' + esc(c.name) + arrow + ''; }).join('') + ''; view.innerHTML = header(esc(name), nf.format(d.total) + ' matching rows') + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + filterChips + '
' + '
' + head + '' + d.rows.map(function (row) { return '' + d.columns.map(function (c) { return ''; }).join('') + ''; }).join('') + '
' + cell(row[c.name], c, row, name) + '
'; document.getElementById('filterForm').addEventListener('submit', function (e) { e.preventDefault(); var search = document.getElementById('fSearch'); if (document.activeElement === search) return go({ q: search.value.trim() }); var filter = { column: document.getElementById('fColumn').value, op: document.getElementById('fOp').value, value: document.getElementById('fValue').value }; go({ filters: state.filters.concat([filter]) }); }); view.querySelectorAll('[data-remove]').forEach(function (b) { b.addEventListener('click', function () { go({ filters: state.filters.filter(function (f, i) { return i !== parseInt(b.dataset.remove, 10); }) }); }); }); view.querySelectorAll('[data-order]').forEach(function (a) { a.addEventListener('click', function (e) { e.preventDefault(); var column = a.dataset.order; go({ order: column, dir: state.order === column && state.dir === 'asc' ? 'desc' : 'asc' }); }); }); document.getElementById('prevPage').addEventListener('click', function () { go({ start: Math.max(0, start - PAGE) }); }); document.getElementById('nextPage').addEventListener('click', function () { go({ start: start + PAGE }); }); }); } // ---- 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 ' ' + '\u2605'.repeat(Math.min(value, 5)) + ''; } function icon(lot) { return ''; } function iconById(id, size) { if (!id || id <= 0) return ''; return ''; } // An object with its icon and rarity; other links as they are function item(ref) { if (!ref) return '-'; if (ref.link !== 'object') return ref.link ? refLink(ref) : '' + esc(ref.id) + ''; return '' + icon(ref.id) + esc(ref.name) + '' + rarity(ref.rarity) + ' ' + esc(ref.id) + ''; } function items(refs, count) { if (!refs || !refs.length) return 'None'; return refs.map(function (r) { return '
' + item(r.item || r) + (count && r.count !== undefined ? ' × ' + num(r.count) : '') + '
'; }).join(''); } function none(text) { return '' + (text || 'None') + ''; } function raw(title, body) { return '
' + title + '
' + body + '
'; } // 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 '
' + title + '
'; } 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 '
' + head.map(function (h) { return '' + h.text + ''; }).join('') + '' + rows.join('') + '
'; } function stat(label, value) { return '
' + label + '
' + value + '
'; } // Mission tasks that count something function countedBy(tasks) { if (!tasks || !tasks.length) return ''; return card('Counted by mission tasks', tasks.map(function (t) { return '
' + refLink(t.mission) + ' ' + esc(t.taskType) + '' + (t.targetValue ? ' × ' + num(t.targetValue) + '' : '') + '
'; }).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 '' + item(i.item) + '' + pct(i.chance) + '' + num(+i.expected.toPrecision(3)) + ''; }); 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 '
Loot table ' + esc(e.lootTable) + ': rolls with ' + pct(e.percent) + ', then ' + (e.minToDrop === e.maxToDrop ? esc(e.minToDrop) : esc(e.minToDrop) + '–' + esc(e.maxToDrop)) + ' drop(s)' + (e.flagID ? ' (flagID ' + esc(e.flagID) + ' in the row; the server\'s roll doesn\'t check it)' : '') + '
' + '
Each drop rolls a rarity (rarity table ' + esc(e.rarityTable) + '): ' + 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).
' + table([{ text: 'Item' }, { text: 'Per drop', num: true }, { text: 'Chance', num: true }, { text: '' }], e.items.map(function (i) { return '' + item(i.item) + '' + pct(i.perDrop) + '' + pct(i.chance) + '' + (i.missionDrop ? 'mission item' : '') + ''; }), 'cdc-odds') + '
'; }).join(''); }) + '
Chances before any live event loot bonus. Mission items drop only for players whose mission needs them.
'; 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('' + esc(d.name) + (d.item ? rarity(d.item.rarity) : '') + ' ' + lot + '', sub.filter(Boolean).join(' · ') + (d.row._internalNotes ? ' · ' + esc(d.row._internalNotes) : '')); if (d.description) html += '

' + esc(d.description) + '

'; 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) + ', loot matrix ' + esc(l.matrix) + ')', 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 '
' + item(i.item) + ' × ' + num(i.count) + (i.equip ? ' equipped' : '') + '
'; }).join('')); if (d.missions) left += card('Missions here', d.missions.map(function (m) { return '
' + refLink(m.mission) + (m.offers ? ' offers' : '') + (m.accepts ? ' takes turn-ins' : '') + '
'; }).join('')); if (d.collectibleMission) left += card('Collectible', 'Needs ' + refLink(d.collectibleMission)); right += d.model ? card('Model', '
' + '
' + esc(d.model) + '
') : ''; right += sourcesCard(d.sources); if (d.rewardedBy.length) right += card('Mission rewards', d.rewardedBy.map(function (r) { return '
' + refLink(r.mission) + ' × ' + num(r.count) + (r.repeat ? ' on repeats' : '') + (r.choice ? ' one of a choice' : '') + '
'; }).join('')); right += countedBy(d.countedBy); right += card('Components', d.components.length ? d.components.map(function (c) { var title = '' + esc(c.typeName) + ' type ' + c.type + ', id ' + c.componentId + (c.table ? ' in ' + esc(c.table) + '' : ', no table') + ''; // Nothing to unfold without rows (components with no table of their own) if (!c.rows.length) return '
' + title + '
'; return '
' + title + '' + (c.rows.length === 1 ? keyValues(c.rows[0], c.columns, c.table) : rowsTable(c.rows, c.table, c.columns)) + '
'; }).join('') : none()); right += card('Objects row', raw('All columns', keyValues(d.row, d.columns, 'Objects'))) + localized(d.localized); html += '
' + left + '
' + right + '
'; 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) + ' (' + esc(i.itemType) + ')'], ['Equips to', i.equipLocation ? '' + esc(i.equipLocation) + '' : none('Not equipped')], ['Stack size', num(i.stackSize)], ['Base value', i.baseValue >= 0 ? num(i.baseValue) + ' coins (a vendor charges it × its buy scalar)' : none('No coin price')] ]; if (i.price.altCurrency) rows.push(['Also costs', item(i.price.altCurrency.item) + ' × ' + num(i.price.altCurrency.base) + ' (× buy scalar)']); 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 '
' + esc(p.id) + ' ' + esc(p.reason) + '
'; }).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) + '' + esc(s.description) + '']); }); if (i.set) rows.push(['Item set', iconById(i.set.icon) + esc(i.set.name || 'Set ' + i.set.id) + ' rank ' + esc(i.set.rank) + '' + 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 '
' + esc(b.pieces) + ' pieces: ' + parts.join(', ') + '
'; }).join('')]); return card('Item', '' + rows.map(function (r) { return ''; }).join('') + '
' + r[0] + '' + r[1] + '
'); } function destructibleCard(x) { return card('Health and smashing (destructible component)', '
' + stat('Life', num(x.life)) + stat('Armor', num(x.armor)) + stat('Imagination', num(x.imagination)) + stat('Level', num(x.level)) + '
' + '
Coins when smashed: ' + (x.coins ? num(x.coins.min) + '–' + num(x.coins.max) : none()) + ' (currency table at its level)
' + '
Faction ' + esc(x.faction) + (x.isSmashable ? ' · smashable' : '') + '
'); } 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, loot matrix ' + esc(v.matrix) + ')', '
' + scalar + '; buys back at × ' + num(v.sellScalar) + (v.refreshTimeSeconds > 0 ? '; restocks every ' + num(v.refreshTimeSeconds / 60) + ' min' : '') + '
' + v.stock.map(function (e) { return '
Loot table ' + esc(e.lootTable) + ': ' + (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') + '
' + 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 '' + item(i.item) + '' + (price || none('-')) + '' + (i.sold ? pct(i.chance) : 'never') + ''; }), 'cdc-odds') + '
'; }).join('')); } function skillsCard(skills) { return card('Skills', table([{ text: 'Skill' }, { text: 'Cost', num: true }, { text: 'Cooldown', num: true }, { text: 'castOnType (raw)', num: true }], skills.map(function (s) { return '' + iconById(s.skillIcon) + refLink(s.skill) + (s.description ? '
' + esc(s.description) + '
' : '') + (s.behaviorID ? '' : '') + '' + '' + (s.imaginationcost ? num(s.imaginationcost) + ' imagination' : '') + '' + (s.cooldown ? num(s.cooldown) + ' s' : '') + '' + esc(s.castOnType) + ''; }))); } // 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 '
' + item(o) + '
'; }).join('') + (m.objects.length > 5 ? '
and ' + (m.objects.length - 5) + ' more
' : '') + (m.rows || []).map(function (r) { return '
' + columns.filter(function (c) { return r[c.name] !== null && r[c.name] !== '' && r[c.name] !== undefined; }).map(function (c) { return '' + esc(c.name) + ' ' + cell(r[c.name], c, r, s.table); }).join(' · ') + '
'; }).join(''); return '' + esc(m.matrix) + '' + (m.chance !== undefined ? pct(m.chance) : '?') + '' + (m.expected !== undefined ? num(+m.expected.toPrecision(3)) : '') + '' + (users || none()) + ''; }); var head = [{ text: 'Loot matrix' }, { text: s.vendor ? 'In stock' : 'Chance', num: true }, { text: s.vendor ? '' : 'Average', num: true }, { text: 'Used by' }]; return '
Through ' + esc(s.table) + '.' + esc(s.column) + ' (' + rows.length + ')
' + table(head, rows.slice(0, 10), 'cdc-odds') + (rows.length > 10 ? later('All ' + rows.length, function () { return table(head, rows, 'cdc-odds'); }) : '') + '
'; }).join('') + '
Chance per smash, opened package or activity reward, before live event bonuses; for vendors, the chance it is in stock.
'); } 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 'always met'; return '' + refLink(t.mission) + (t.stateName ? ' in state ' + esc(t.stateName) : ' complete') + ''; } 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) + ' ' + (t.or ? 'or' : 'and') + ' ' + rest; } return '
' + from(0) + '
As the server reads ' + esc(text) + ': ' + 'each mission is joined to everything after it, and brackets don\'t group.
'; } 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('
Targets: ' + t.targets.map(item).join(', ') + '
'); if (t.targetText) parts.push('
Target group: ' + esc(t.targetText) + '
'); if (t.parameters.length) parts.push('
Parameters: ' + t.parameters.map(item).join(', ') + '
'); if (t.racingParameter) parts.push('
Racing task: ' + esc(t.racingParameter) + '
'); return '
' + iconById(t.iconID) + '' + esc(t.taskTypeName) + ' ' + '× ' + num(t.targetValue) + ' ' + esc(t.description || '') + (t.progressed ? '' : ' the server never progresses this type') + parts.join('') + raw('MissionTasks row (uid ' + esc(t.uid) + ')', keyValues(t.row, columnsOf('MissionTasks'), 'MissionTasks')) + '
'; }).join('')); } function rewardsCard(r, row) { var choice = row.isChoiceReward ? ' (the player picks one)' : ''; var first = '
First completion' + choice + '
' + (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 ? '
' + extras.join(', ') + '
' : ''; if (r.emotes.length) first += '
Emotes: ' + r.emotes.map(refLink).join(', ') + '
'; if (r.stats.length) first += r.stats.map(function (s) { return '
' + esc(s.column) + ' +' + num(s.value) + '
'; }).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 += '
Each repeat' + choice + '
' + items(r.repeatItems, true) + (again.length ? '
' + again.join(', ') + '
' : ''); } return card('Rewards', html + '
At the level cap U-score is paid as coins.
'); } 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) + ' ' + id + '', esc([row.defined_type, row.defined_subtype].filter(Boolean).join(' / ')) + ' ' + badges.map(function (b) { return '' + b + ''; }).join(' ')); html += '
'; 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 += '
'; 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'))) + '
'; 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 ? '' + esc(label) + ': ' : '') + '' + esc(behaviorId) + ' '; if (!n) return '
' + title + 'not in BehaviorTemplate' + (tree.truncated ? ' (or past the depth shown)' : '') + '
'; title += '' + esc(n.templateName) + ' template ' + esc(n.templateID) + '' + (n.effectID ? ' effect ' + esc(n.effectID) + (n.effectHandle ? ' ' + esc(n.effectHandle) : '') + '' : ''); if (shown[behaviorId]) return '
' + title + ' (shown above)
'; 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 '
' + title + '' + (values.length ? '' + values.map(function (p) { return ''; }).join('') + '
' + esc(p.name) + '' + num(p.value) + '
' : '') + children.map(function (p) { return node(p.child, p.name); }).join('') + '
'; } return '
' + node(tree.root, '') + '
' + (tree.truncated ? '
Cut short: too big to show whole.
' : ''); } 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) + ' ' + id + '', esc(d.description || '')); html += '
' + stat('Imagination cost', num(row.imaginationcost || 0)) + stat('Cooldown', num(row.cooldown || 0) + ' s') + stat('Cooldown group', num(row.cooldowngroup || 0)) + '
'; html += '
' + card('Behavior tree' + (row.behaviorID ? ' ' + esc(row.behaviorID) + '' : ''), d.tree ? behaviorTree(d.tree) : none('No behavior')) + '
'; html += card('On objects', d.objects.length ? table([{ text: 'Object' }, { text: 'castOnType (raw)', num: true }], d.objects.map(function (o) { return '' + item(o.object) + '' + esc(o.castOnType) + ''; })) : none()); html += countedBy(d.countedBy) + localized(d.localized) + card('SkillBehavior row', raw('All columns', keyValues(row, d.columns, 'SkillBehavior'))) + '
'; 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) + ' ' + id + '', d.zone ? 'Played in ' + refLink(d.zone) : ''); html += '

Costs to play: ' + (d.cost ? item(d.cost.item) + ' × ' + num(d.cost.count) : none('nothing')) + '

'; var rewards = d.rewards.map(function (r, i) { return '
Rating ' + esc(r.rating) + '' + (i === 0 ? ' first row' : '') + (r.description ? ' ' + esc(r.description) + '' : '') + '
' + '
Coins: ' + (r.coins ? num(r.coins.min) + '–' + num(r.coins.max) : none()) + (r.matrix > 0 ? ' · loot matrix ' + esc(r.matrix) + '' : '') + '
' + (r.odds ? raw('Drops', lootOdds(r.odds, 'reward')) : '') + '
'; }).join(''); html += '
' + card('Rewards', (rewards || none()) + '
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.
') + '
'; html += countedBy(d.countedBy) + localized(d.localized) + card('Activities row', raw('All columns', keyValues(row, d.columns, 'Activities'))) + '
'; 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 ? ' override' : ''; }; var html = header(esc(d.name) + ' ' + id + '', esc(d.row.mapFolder || '') + (d.row.zoneName ? ' · ' + esc(d.row.zoneName) : '')); html += '
'; html += card('Players per instance', '
' + stat('Soft cap' + override(c.softOverride), num(c.soft)) + stat('Hard cap' + override(c.hardOverride), num(c.hard)) + '
' + '
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.') + ' Instances
'); html += card('Zone control object', d.control ? item(d.control) : none('None set: the server spawns its default zone control object')); html += '
' + localized(d.localized) + card('ZoneTable row', raw('All columns', keyValues(d.row, d.columns, 'ZoneTable'))) + '
'; 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 '' + item(i.item) + '' + (rarity(i.rarity) || none('0')) + '' + (i.missionDrop ? 'mission item' : '') + ''; }), 'cdc-odds') + '
The chance of each depends on the loot matrix and rarity table it is rolled with: open one of the matrices below.
') + 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); var params = {}; if (queryAt !== -1) hash.slice(queryAt + 1).split('&').forEach(function (p) { var eq = p.indexOf('='); if (eq > 0) params[p.slice(0, eq)] = decodeURIComponent(p.slice(eq + 1)); }); view.innerHTML = '
Loading…
'; window.scrollTo(0, 0); 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.'); } } 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) { if (api404(d)) return showError(d.error); schema = d; route(); }); })();