/** * Character page: missions (with completing, resetting and giving one for staff), progress (completion per group and * zone, compared with the server) and giving back what was lost since a snapshot. Names, states, groups and the * possible changes all come from the server. */ (function () { 'use strict'; var config = window.characterToolsConfig; if (!config) return; var nf = new Intl.NumberFormat(); var canChange = !!config.canMissions; function count(c) { return nf.format(c.done) + ' / ' + nf.format(c.available); } function percent(c) { return c.available ? Math.round(100 * c.done / c.available) : 0; } function bar(c) { return '
' + '
'; } function group(m) { return [m.type, m.subtype].filter(Boolean).join(' / '); } // ---- missions ---- var missions = [], changes = [], found = []; function taskList(m) { var tasks = m.tasks.map(function (t) { return '
  • ' + esc(t.description || t.type) + ' ' + nf.format(t.progress) + ' / ' + nf.format(t.target) + '
  • '; }).join(''); var r = m.rewards, rewards = []; if (r) { if (r.coins) rewards.push(nf.format(r.coins) + ' coins'); if (r.uscore) rewards.push(nf.format(r.uscore) + ' U-score'); if (r.reputation) rewards.push(nf.format(r.reputation) + ' reputation'); r.items.forEach(function (i) { rewards.push(nf.format(i.count) + 'x ' + (i.name || 'LOT ' + i.lot)); }); } return (tasks ? '' : '') + (rewards.length ? '
    Rewards: ' + esc(rewards.join(', ')) + '
    ' : ''); } function changeButtons(id) { if (!canChange) return ''; return '
    ' + changes.map(function (c) { return ''; }).join('') + '
    '; } function renderMissions() { var text = document.getElementById('missionFilter').value.trim().toLowerCase(); var kind = document.getElementById('missionKind').value; var show = document.getElementById('missionShow').value; var rows = missions.filter(function (m) { if (kind && m.kind !== kind) return false; if (show === 'current' && !m.current) return false; if (show === 'done' && !m.done) return false; return !text || (m.name + ' ' + m.id + ' ' + group(m)).toLowerCase().indexOf(text) !== -1; }).map(function (m) { return '
    ' + esc(m.name) + ' ' + esc(m.id) + '
    ' + '
    ' + esc(m.kind) + (group(m) ? ' · ' + esc(group(m)) : '') + '
    ' + taskList(m) + '' + '' + fmt.badge(m.state_name, m.done && !m.current ? 'success' : 'secondary') + (m.completions ? '
    ' + nf.format(m.completions) + 'x, last ' + esc(fmt.unix(m.completed_at)) + '
    ' : '') + '' + (canChange ? '' + changeButtons(m.id) + '' : '') + ''; }); document.getElementById('missionRows').innerHTML = rows.join('') || 'No missions match'; } function loadMissions() { return api.get('/api/characters/' + config.id + '/missions').then(function (d) { if (!d.success) { toast(d.error || 'Could not read the missions', 'danger'); return; } missions = d.missions.sort(function (a, b) { return (b.completed_at || 0) - (a.completed_at || 0) || a.id - b.id; }); changes = d.changes; var kinds = []; missions.forEach(function (m) { if (kinds.indexOf(m.kind) === -1) kinds.push(m.kind); }); var select = document.getElementById('missionKind'), current = select.value; select.innerHTML = '' + kinds.map(function (k) { return ''; }).join(''); select.value = current; renderMissions(); }); } function changeMission(id, change) { var name = (missions.concat(found).filter(function (m) { return String(m.id) === String(id); })[0] || {}).name || 'mission ' + id; var info = changes.filter(function (c) { return c.value === change; })[0] || {}; var rewards = !!info.rewards && document.getElementById('missionRewards').checked; if (!confirm((info.name || change) + ' "' + name + '"' + (info.rewards ? (rewards ? ' with its rewards' : ' without its rewards') : '') + '?')) return; api.action('/api/characters/' + config.id + '/missions/' + id, { change: change, rewards: rewards }).then(function (d) { return d.result; }).then(function (r) { if (r && r.success) loadMissions(); }).catch(function () {}); } var searchTimer = null; function wireMissions() { var button = document.getElementById('missionsBtn'); if (!button) return; button.addEventListener('click', function () { bootstrap.Modal.getOrCreateInstance(document.getElementById('missionsModal')).show(); loadMissions(); }); ['missionFilter', 'missionKind', 'missionShow'].forEach(function (id) { document.getElementById(id).addEventListener('input', renderMissions); }); document.getElementById('missionsModal').addEventListener('click', function (e) { var b = e.target.closest('[data-change]'); if (b) changeMission(b.dataset.mission, b.dataset.change); }); var search = document.getElementById('missionSearch'); if (!search) return; search.addEventListener('input', function () { clearTimeout(searchTimer); var q = search.value.trim(); searchTimer = setTimeout(function () { if (q.length < 2 && !/^\d+$/.test(q)) { document.getElementById('missionSearchResults').innerHTML = ''; return; } api.get('/api/missions?q=' + encodeURIComponent(q)).then(function (d) { found = d.missions || []; document.getElementById('missionSearchResults').innerHTML = found.map(function (m) { return '
  • ' + esc(m.name) + ' ' + esc(m.id) + ' · ' + esc(m.kind) + (group(m) ? ' · ' + esc(group(m)) : '') + '' + changeButtons(m.id) + '
  • '; }).join('') || '
  • Nothing found
  • '; }); }, 250); }); } // ---- progress ---- var progressRetries = 0; function standing(label, c, s) { return '
    ' + '
    ' + esc(label) + '
    ' + '
    ' + count(c) + ' (' + percent(c) + '%)
    ' + bar(c) + (s ? '
    Server average ' + nf.format(Math.round(s.average)) + '; ahead of ' + Math.round(s.percentile) + '% of characters
    ' : '') + '
    '; } function renderProgress(d) { var p = d.progress, box = document.getElementById('progressBody'); var html = '
    ' + standing('Achievements', p.achievements, p.average && p.average.achievements) + standing('Missions', p.missions, p.average && p.average.missions) + '
    '; if (p.average) html += '

    Compared with the ' + nf.format(p.average.characters) + ' characters that have finished anything, as of ' + esc(fmt.unix(p.average.updated)) + '.

    '; else if (d.average_pending) html += '

    Working out the server\'s averages…

    '; html += '
    Zones
    '; p.zones.forEach(function (z) { var c = z.category !== null ? p.categories[z.category] : null; var summary = z.summary.map(function (s) { return '
    ' + esc(s.name) + ': ' + nf.format(s.found) + ' / ' + nf.format(s.total) + (s.done ? ' ' + fmt.badge('done', 'success') : '') + '
    '; }).join(''); var stats = z.stats ? [nf.format(z.stats.achievements) + ' achievements', nf.format(z.stats.coins) + ' coins', nf.format(z.stats.enemies) + ' enemies', nf.format(z.stats.quickbuilds) + ' quick builds'].join(', ') : ''; html += '' + '' + '' + ''; }); html += '
    ZoneMissionsAchievementsTo findEarned there
    ' + esc(z.name) + (z.visited ? '' : ' ' + fmt.badge('not visited', 'secondary')) + '' + (c ? count(c.missions) + bar(c.missions) : '') + '' + (c ? count(c.achievements) + bar(c.achievements) : '') + '' + summary + '' + stats + '
    '; html += '
    All groups
    ' + (p.average ? '' : '') + ''; p.categories.slice().sort(function (a, b) { return group(a).localeCompare(group(b)); }).forEach(function (c) { html += '' + '' + (p.average ? '' : '') + ''; }); box.innerHTML = html + '
    GroupMissionsAchievementsServer average
    ' + esc(group(c) || '-') + '' + (c.missions.available ? count(c.missions) : '') + '' + (c.achievements.available ? count(c.achievements) : '') + '' + [c.missions.available ? nf.format(Math.round(c.average.missions)) + ' missions' : '', c.achievements.available ? nf.format(Math.round(c.average.achievements)) + ' achievements' : ''].filter(Boolean).join(', ') + '
    '; } function loadProgress() { api.get('/api/characters/' + config.id + '/progress').then(function (d) { if (!d.success) { document.getElementById('progressBody').innerHTML = '

    ' + esc(d.error) + '

    '; return; } renderProgress(d); // Averages come from reading every character; ask again once they're ready if (d.average_pending && progressRetries++ < 10) setTimeout(loadProgress, 3000); }); } function wireProgress() { var button = document.getElementById('progressBtn'); if (!button) return; button.addEventListener('click', function () { bootstrap.Modal.getOrCreateInstance(document.getElementById('progressModal')).show(); progressRetries = 0; loadProgress(); }); } // ---- giving back lost items ---- var restoreSnapshot = null, missing = []; function renderRestore(data) { missing = data.missing; var box = document.getElementById('restoreCompare'); var restorable = missing.filter(function (m) { return m.restorable > 0; }); var html = '
    ' + esc(fmt.unix(data.taken_at)) + ' → now
    '; var sets = data.sets.filter(function (s) { return s.lots.some(function (lot) { return restorable.some(function (m) { return m.lot === lot; }); }); }); if (sets.length) html += '
    ' + sets.map(function (s) { return ''; }).join('') + '
    '; html += '
    ' + missing.map(function (m) { var accounted = []; if (m.mailed) accounted.push(nf.format(m.mailed) + ' in their mailbox'); if (m.elsewhere) accounted.push(nf.format(m.elsewhere) + ' someone else has'); var disabled = m.restorable ? '' : ' disabled'; return '' + '' + '' + '' + ''; }).join('') + '
    ItemThenNowAccounted forGive back
    ' + esc(m.name || 'LOT ' + m.lot) + ' ' + esc(m.lot) + '' + nf.format(m.then) + '' + nf.format(m.now) + '' + esc(accounted.join(', ')) + '
    '; if (!missing.length) html = '

    Nothing the character had then is missing now.

    '; var byInventory = {}; data.changes.forEach(function (c) { (byInventory[c.inventory] = byInventory[c.inventory] || []).push(c); }); html += '
    Every change, per inventory' + Object.keys(byInventory).map(function (type) { return '
    ' + esc(Labels.name('inventories', type) || type) + '
    '; }).join('') + '
    '; box.innerHTML = html; document.getElementById('restoreSend').disabled = !restorable.length; } function openRestoreSnapshot(id) { restoreSnapshot = id; document.querySelectorAll('#restoreSnapshots [data-snapshot]').forEach(function (el) { el.classList.toggle('active', el.dataset.snapshot === String(id)); }); var box = document.getElementById('restoreCompare'); box.innerHTML = '

    Comparing with everyone\'s items and mail…

    '; api.job('/api/characters/' + config.id + '/restore/' + id, null, 'GET').then(renderRestore).catch(function (e) { box.innerHTML = '

    ' + esc(e.message) + '

    '; }); } function wireRestore() { var button = document.getElementById('restoreBtn'); if (!button) return; button.addEventListener('click', function () { bootstrap.Modal.getOrCreateInstance(document.getElementById('restoreModal')).show(); api.get('/api/characters/' + config.id + '/snapshots').then(function (d) { var list = document.getElementById('restoreSnapshots'); if (!d.success) { list.innerHTML = '
    ' + esc(d.error) + '
    '; return; } list.innerHTML = d.snapshots.map(function (s) { return '
    ' + esc(fmt.unix(s.taken_at)) + '
    ' + '
    ' + esc(s.reason) + (s.actor ? ' by ' + esc(s.actor) : '') + '
    '; }).join('') || '
    No snapshots yet.
    '; }); }); document.getElementById('restoreSnapshots').addEventListener('click', function (e) { var item = e.target.closest('[data-snapshot]'); if (item) { e.preventDefault(); openRestoreSnapshot(item.dataset.snapshot); } }); document.getElementById('restoreCompare').addEventListener('click', function (e) { var set = e.target.closest('[data-set]'); if (!set) return; var lots = []; // every missing piece of the set document.querySelectorAll('#restoreCompare [data-pick]').forEach(function (box) { var m = missing.filter(function (x) { return String(x.lot) === box.dataset.pick; })[0]; if (m && String(m.set) === set.dataset.set && !box.disabled) { box.checked = true; lots.push(m.lot); } }); if (!lots.length) toast('Nothing of that set can be given back', 'warning'); }); document.getElementById('restoreSend').addEventListener('click', function () { var items = []; document.querySelectorAll('#restoreCompare [data-pick]:checked').forEach(function (box) { var input = document.querySelector('#restoreCompare [data-count="' + box.dataset.pick + '"]'); items.push({ lot: Number(box.dataset.pick), count: Number(input.value) || 0 }); }); if (!items.length) { toast('Tick the items to give back', 'warning'); return; } if (!confirm('Mail ' + items.length + ' kind(s) of item back to this character?')) return; api.action('/api/characters/' + config.id + '/restore/' + restoreSnapshot, { items: items, note: document.getElementById('restoreNote').value.trim() }) .then(function (d) { return d.result; }).then(function (r) { if (r && r.success) openRestoreSnapshot(restoreSnapshot); }).catch(function () {}); }); } wireMissions(); wireProgress(); wireRestore(); })();