/** * Players' creations on the property and character pages: the icon the UGC server made of a model, car or rocket in * place of the item's own icon, its state and a link to it on the /ugc page (for those who may open it). The routes * (/api/ugc_links/...) let whoever may view the page see the icons of the creations on it. */ (function () { var STATES = { done: ['Made', 'success'], pending: ['Waiting', 'secondary'], failed: ['Failed', 'danger'] }; function row(label, value) { return value === '' || value === null || value === undefined ? '' : '
' + esc(label) + '
' + value + '
'; } // Triangles before and after hidden surfaces were removed, per LOD function triangles(stats) { return (stats.lods || []).map(function (l) { return 'LOD ' + esc(l.lod) + ': ' + esc(l.opaqueBefore) + ' → ' + esc(l.opaqueAfter) + (l.transparent ? ' + ' + esc(l.transparent) + ' transparent' : '') + ' (' + esc(l.vertices) + ' vertices)'; }).join('
'); } function timings(ms) { if (!ms) return ''; var parts = [['build', 'build'], ['hiddenSurfaces', 'hidden faces'], ['ambientOcclusion', 'AO'], ['icon', 'icon']] .filter(function (p) { return ms[p[0]] !== undefined; }).map(function (p) { return esc(p[1]) + ' ' + esc(ms[p[0]]); }); return esc(ms.total) + ' ms' + (parts.length ? ' (' + parts.join(', ') + ')' : ''); } function renderDetails(d, modelId) { var placement = (d.placements || []).find(function (p) { return p.modelId === String(modelId); }) || (d.placements || [])[0]; var name = placement && placement.name && !/^Objects_\d+_name$/.test(placement.name) ? placement.name : ''; var s = d.stats, prev = d.previousStats; var waiting = d.state === 'pending' && d.processAfter > Date.now() / 1000; var html = '
' + '
Now
' + '
Before · NIF
' + '
' + row('State', UgcLinks.state(d) + (d.attempts ? ' ' + esc(d.attempts) + ' attempt' + (d.attempts === 1 ? '' : 's') + '' : '') + (d.error ? '
' + esc(d.error) + '
' : '')) + row('Waiting until', waiting ? esc(fmt.unix(d.processAfter)) + ' (the owner saved it recently)' : '') + row('UGC id', esc(d.ugcId)) + row('Owner', (d.characterName ? fmt.character(d.characterId, d.characterName) : esc(d.characterId)) + (d.accountName ? ' (' + esc(d.accountName) + ')' : '')) + row('Name', name ? esc(name) : '') + row('Description', placement && placement.description ? esc(placement.description) : '') + row('File', d.fileName ? esc(d.fileName) : '') + row('Made', d.processedAt ? esc(fmt.unix(d.processedAt)) : '') + row('Baked AO', d.bakeAo === undefined ? '' : (d.bakeAo ? 'Yes' : 'No')) + row('Bricks', s ? esc(s.bricks) + (prev && prev.bricks !== s.bricks ? ' (was ' + esc(prev.bricks) + ')' : '') : '') + row('Triangles', s ? triangles(s) : '') + row('Time', s ? timings(s.ms) : '') + row('Before', prev ? esc(prev.bricks) + ' bricks
' + triangles(prev) : '') + '
'; if (d.state === 'done' && !s) html += '
No stats (made before the UGC server wrote them).
'; var buttons = ''; if (d.link) buttons += 'Open in UGC viewer'; if (d.canManage) buttons += ''; if (d.state === 'done') buttons += 'Download NIF'; if (modelId) buttons += 'Download LXFML'; return html + (buttons ? '
' + buttons + '
' : '') + '
'; } // "Make again" in any UGC block document.addEventListener('click', function (e) { var button = e.target.closest && e.target.closest('[data-ugc-remake]'); if (!button) return; button.disabled = true; api.post('/api/ugc/reprocess', { kind: 'model', id: button.dataset.ugcRemake }).then(function (d) { toast(d.success ? 'The UGC server will make it again' : (d.error || 'Failed'), d.success ? 'success' : 'danger'); }).catch(function () { toast('Failed', 'danger'); }).then(function () { button.disabled = false; }); }); // Expandable blocks (
) load when first opened document.addEventListener('toggle', function (e) { var box = e.target; if (!box.matches || !box.matches('details[data-ugc-details]') || !box.open || box.dataset.loaded) return; box.dataset.loaded = '1'; var body = box.querySelector('.ugc-details-body'); UgcLinks.details(box.dataset.ugcDetails, box.dataset.via, box.dataset.model).then(function (html) { body.innerHTML = html; }); }, true); var linkCache = {}; // character id -> promise of its item links window.UgcLinks = { // A badge for the UGC server's state; a failure's reason as its tooltip state: function (item) { var s = STATES[item.state] || [item.state, 'secondary']; return '' + esc(s[0]) + ''; }, // An of the creation's icon that falls back to the item's own icon until the UGC server made one icon: function (item, lot, size) { size = size || 40; return ''; }, /** * The UGC block for a brick-built model on the property pages: its icons (and the previous version's), state, * owner, the name the player gave it, the UGC server's stats and links. `via` is the page's ?property= / ?character= * query that lets its viewer see it; `modelId` (a placed model) picks its name and LXFML download. */ details: function (ugcId, via, modelId) { if (!ugcId || ugcId === '0') return Promise.resolve('
No UGC data: this model has no blueprint.
'); return api.get('/api/ugc_links/model/' + encodeURIComponent(ugcId) + (via ? '?' + via : '')).then(function (d) { if (!d || d.success === false) return '
' + esc((d && d.error) || 'No UGC data for this model') + '
'; return renderDetails(d, modelId); }).catch(function (e) { return '
' + esc((e && e.message) || 'Could not load the UGC data') + '
'; }); }, // {modelId: item} for the player-built models on a property // {modelId: item} for the player-built models on a property forProperty: function (propertyId) { return api.get('/api/ugc_links/property/' + propertyId).then(function (d) { var map = {}; ((d && d.items) || []).forEach(function (item) { map[item.modelId] = item; }); return map; }).catch(function () { return {}; }); }, // Shows the creations among a character page's items (.inv-item[data-id]) with their own icon and a link // Itemid -> the UGC link of each of a character's player-built items (fetched once per character) inventoryLinks: function (characterId) { if (!linkCache[characterId]) { linkCache[characterId] = api.get('/api/ugc_links/character/' + characterId).then(function (d) { var map = {}; ((d && d.items) || []).forEach(function (item) { map[item.itemId] = item; }); return map; }); } return linkCache[characterId]; }, // Shows a player-built item's UGC icon and a link to the UGC viewer on one inventory item element decorateItem: function (el, item) { var img = el.querySelector('img'); if (img) { var own = img.src; img.onerror = function () { img.onerror = null; img.src = own; }; img.src = item.icon; } el.classList.add('inv-ugc'); el.title = (el.title ? el.title + ' - ' : '') + 'Player-built ' + (item.kind === 'modular' ? 'car or rocket' : 'model') + ' (UGC ' + item.ugcId + ', ' + item.state + ')'; if (item.link && !el.querySelector('.inv-ugc-link')) { // Items may sit inside a link already (the item trace), so this is a button, not a nested link var a = document.createElement('span'); a.className = 'inv-ugc-link'; a.setAttribute('role', 'link'); a.tabIndex = 0; a.textContent = 'UGC'; a.title = 'Open in the UGC viewer'; var open = function (e) { e.preventDefault(); e.stopPropagation(); window.location.href = item.link; }; a.addEventListener('click', open); a.addEventListener('keydown', function (e) { if (e.key === 'Enter') open(e); }); el.appendChild(a); } }, // Decorates the inventory items already on the page decorateInventory: function (characterId, root) { var self = this; return this.inventoryLinks(characterId).then(function (map) { Object.keys(map).forEach(function (id) { (root || document).querySelectorAll('.inv-item[data-id="' + id + '"]').forEach(function (el) { self.decorateItem(el, map[id]); }); }); }).catch(function () {}); } }; })();