aboutsummaryrefslogtreecommitdiff
path: root/internal/admin/static/dropview.js
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-07-13 17:25:14 -0400
committerhistoria <[not public]>2026-07-13 17:25:14 -0400
commit8952653be9bfeeb9aadf59f845de507630d65dc4 (patch)
tree64babb2c89568734cc5ff59b386845f1707f7ecf /internal/admin/static/dropview.js
parent709917f657eda63f10ea244546e8bb7fd4d839e8 (diff)
downloadthehouseoficarus-8952653be9bfeeb9aadf59f845de507630d65dc4.tar.gz
feat(admin): add drop tables to gui showing 1/n drop chances
Diffstat (limited to 'internal/admin/static/dropview.js')
-rw-r--r--internal/admin/static/dropview.js239
1 files changed, 239 insertions, 0 deletions
diff --git a/internal/admin/static/dropview.js b/internal/admin/static/dropview.js
new file mode 100644
index 0000000..9f4ea15
--- /dev/null
+++ b/internal/admin/static/dropview.js
@@ -0,0 +1,239 @@
+var DropView = (function() {
+ var _cache = {};
+ var _timers = {};
+ var _seq = 0;
+ var DEBOUNCE_MS = 200;
+
+ function fetchTable(id) {
+ if (!id) return Promise.resolve(null);
+ if (id in _cache) return Promise.resolve(_cache[id]);
+ return API.get('/api/drops/' + encodeURIComponent(id)).then(function(data) {
+ _cache[id] = data;
+ return data;
+ }).catch(function() {
+ _cache[id] = null;
+ return null;
+ });
+ }
+
+ function invalidate(id) {
+ if (id !== undefined && id !== null) {
+ delete _cache[id];
+ } else {
+ _cache = {};
+ }
+ }
+
+ function entryWeight(e) {
+ var w = parseInt(e.weight) || 0;
+ if (w <= 0) return 0;
+ if (!e.item_id && !e.table) return 0;
+ return w;
+ }
+
+ function mapAdd(map, key, prob) {
+ map.set(key, (map.get(key) || 0) + prob);
+ }
+
+ function computeNestedProbMap(tableID, visited) {
+ return fetchTable(tableID).then(function(data) {
+ var map = new Map();
+ if (!data || !Array.isArray(data.drops) || data.drops.length === 0) return map;
+ if (visited.has(tableID)) return map;
+ if (visited.size >= 32) return map;
+ visited.add(tableID);
+ return _computeEntriesProbMap(data.drops, visited).then(function(result) {
+ visited.delete(tableID);
+ return result;
+ }).catch(function() {
+ visited.delete(tableID);
+ return new Map();
+ });
+ }).catch(function() {
+ return new Map();
+ });
+ }
+
+ function _computeEntriesProbMap(entries, visited) {
+ var total = 0;
+ for (var i = 0; i < entries.length; i++) {
+ total += entryWeight(entries[i]);
+ }
+ if (total === 0) return Promise.resolve(new Map());
+
+ var map = new Map();
+ var innerCache = {};
+ var promises = [];
+
+ for (var i = 0; i < entries.length; i++) {
+ (function(e) {
+ var w = entryWeight(e);
+ if (w === 0) return;
+ var pEntry = w / total;
+
+ if (e.table) {
+ var rolls = parseInt(e.quantity) || 1;
+ if (!innerCache[e.table]) {
+ innerCache[e.table] = computeNestedProbMap(e.table, new Set(visited));
+ }
+ promises.push(
+ innerCache[e.table].then(function(innerMap) {
+ innerMap.forEach(function(innerP, key) {
+ var atLeastOnce = 1 - Math.pow(1 - innerP, rolls);
+ mapAdd(map, key, pEntry * atLeastOnce);
+ });
+ })
+ );
+ } else if (e.item_id) {
+ var qty = parseInt(e.quantity) || 1;
+ var key = e.item_id + '\x00' + qty;
+ mapAdd(map, key, pEntry);
+ }
+ })(entries[i]);
+ }
+
+ return Promise.all(promises).then(function() {
+ return map;
+ });
+ }
+
+ function computeProbMap(getEntries) {
+ var entries = getEntries();
+ if (!entries || !entries.length) return Promise.resolve(new Map());
+ var visited = new Set();
+ return _computeEntriesProbMap(entries, visited);
+ }
+
+ function snapUnitFraction(p) {
+ if (p <= 0) return '0';
+ if (p <= 0.0000001) return 'Very rare';
+ if (p >= 0.9999) return '1';
+ var n = Math.round(1 / p);
+ if (n > 10000000) return 'Very rare';
+ if (n <= 1) return '1';
+ return '1/' + n;
+ }
+
+ function renderCompact(probMap) {
+ if (probMap.size === 0) return '<div class="obj-drop-view-empty">No drops</div>';
+ var items = [];
+ probMap.forEach(function(p, key) {
+ var parts = key.split('\x00');
+ items.push({ itemID: parts[0], qty: parseInt(parts[1]) || 1, prob: p });
+ });
+ items.sort(function(a, b) {
+ if (b.prob !== a.prob) return b.prob - a.prob;
+ var nameCmp = a.itemID.localeCompare(b.itemID);
+ if (nameCmp !== 0) return nameCmp;
+ return a.qty - b.qty;
+ });
+ var h = '<div class="obj-drop-view-table">';
+ for (var i = 0; i < items.length; i++) {
+ var snap = snapUnitFraction(items[i].prob);
+ var qtyStr = items[i].qty > 1 ? items[i].qty + '\u00d7 ' : '';
+ h += '<div class="obj-drop-view-row">';
+ h += '<span class="obj-drop-view-item">' + esc(qtyStr + items[i].itemID) + '</span>';
+ h += '<span class="obj-drop-view-chance">' + esc(snap) + '</span>';
+ h += '</div>';
+ }
+ h += '</div>';
+ return h;
+ }
+
+ function refreshViewEl(view, getEntries) {
+ if (!document.contains(view)) return;
+ var body = view.querySelector('.obj-drop-view-body');
+ if (!body) return;
+ body.innerHTML = '<span class="obj-drop-view-loading">Computing...</span>';
+ computeProbMap(getEntries).then(function(probMap) {
+ if (!document.contains(view)) return;
+ body.innerHTML = renderCompact(probMap);
+ }).catch(function(e) {
+ if (!document.contains(view)) return;
+ body.innerHTML = '<span class="obj-drop-view-error">Error: ' + esc(e.message) + '</span>';
+ });
+ }
+
+ function scheduleRefresh(view, getEntries) {
+ var key = view._dvKey || (view._dvKey = ++_seq);
+ if (_timers[key]) clearTimeout(_timers[key]);
+ _timers[key] = setTimeout(function() {
+ delete _timers[key];
+ refreshViewEl(view, getEntries);
+ }, DEBOUNCE_MS);
+ }
+
+ function mount(container, getEntries) {
+ if (typeof container === 'string') container = document.querySelector(container);
+ if (!container) return null;
+
+ var viewEl = container.querySelector('.obj-drop-view');
+ if (!viewEl) {
+ viewEl = document.createElement('div');
+ viewEl.className = 'obj-drop-view';
+ container.appendChild(viewEl);
+ }
+ if (!viewEl.querySelector('.obj-drop-view-refresh')) {
+ viewEl.innerHTML =
+ '<div class="obj-drop-view-header">' +
+ '<span>Drop chance</span>' +
+ '<button class="btn btn-sm obj-drop-view-refresh" title="Refresh">\u21bb</button>' +
+ '</div>' +
+ '<div class="obj-drop-view-body">Computing...</div>';
+ }
+
+ viewEl._getEntries = getEntries;
+ viewEl.querySelector('.obj-drop-view-refresh').onclick = function(e) {
+ e.stopPropagation();
+ invalidate();
+ scheduleRefresh(viewEl, getEntries);
+ };
+
+ if (!container._dvSetup) {
+ container._dvSetup = true;
+ container.addEventListener('input', function(e) {
+ if (e.target.tagName === 'INPUT') {
+ scheduleRefresh(viewEl, getEntries);
+ }
+ });
+ container.addEventListener('change', function(e) {
+ scheduleRefresh(viewEl, getEntries);
+ });
+ }
+
+ scheduleRefresh(viewEl, getEntries);
+ return viewEl;
+ }
+
+ function refreshView(btn) {
+ var view = (btn && btn.closest) ? btn.closest('.obj-drop-view') : null;
+ if (!view || !view._getEntries) return;
+ invalidate();
+ scheduleRefresh(view, view._getEntries);
+ }
+
+ function setupAllInter() {
+ document.querySelectorAll('.ie-act-kv[data-ie-act-key="drop_table"]').forEach(function(kv) {
+ if (kv.querySelector('.obj-drop-view')) {
+ mount(kv, function() {
+ var dt = kv.querySelector('.ie-drop-table');
+ if (!dt) return [];
+ var collected = ie_collectDropTableBlock(dt);
+ if (!collected) return [];
+ return collected.map(function(e) {
+ return { item_id: e.item_id || '', table: e.table || '', weight: e.weight || 0, quantity: e.quantity || 0 };
+ });
+ });
+ }
+ });
+ }
+
+ return {
+ mount: mount,
+ invalidate: invalidate,
+ refreshView: refreshView,
+ computeProbMap: computeProbMap,
+ snapUnitFraction: snapUnitFraction,
+ setupAllInter: setupAllInter
+ };
+})();