diff options
| author | historia <[not public]> | 2026-07-13 17:25:14 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-07-13 17:25:14 -0400 |
| commit | 8952653be9bfeeb9aadf59f845de507630d65dc4 (patch) | |
| tree | 64babb2c89568734cc5ff59b386845f1707f7ecf | |
| parent | 709917f657eda63f10ea244546e8bb7fd4d839e8 (diff) | |
| download | thehouseoficarus-8952653be9bfeeb9aadf59f845de507630d65dc4.tar.gz | |
feat(admin): add drop tables to gui showing 1/n drop chances
| -rw-r--r-- | internal/admin/static/admin.css | 15 | ||||
| -rw-r--r-- | internal/admin/static/dropeditor.js | 11 | ||||
| -rw-r--r-- | internal/admin/static/dropview.js | 239 | ||||
| -rw-r--r-- | internal/admin/static/intereditor.js | 1 | ||||
| -rw-r--r-- | internal/admin/static/itemeditor.js | 1 | ||||
| -rw-r--r-- | internal/admin/static/map.js | 1 | ||||
| -rw-r--r-- | internal/admin/static/mobeditor.js | 23 | ||||
| -rw-r--r-- | internal/admin/static/objecteditor.js | 28 | ||||
| -rw-r--r-- | internal/admin/static/talktree.js | 1 | ||||
| -rw-r--r-- | internal/admin/static/triggerseditor.js | 1 | ||||
| -rw-r--r-- | internal/admin/templates/drops.html | 1 | ||||
| -rw-r--r-- | internal/admin/templates/map.html | 1 | ||||
| -rw-r--r-- | internal/admin/templates/mobs.html | 1 | ||||
| -rw-r--r-- | internal/admin/templates/objects.html | 1 | ||||
| -rw-r--r-- | internal/admin/templates/triggers.html | 1 |
15 files changed, 326 insertions, 0 deletions
diff --git a/internal/admin/static/admin.css b/internal/admin/static/admin.css index 798a2a2..42e5340 100644 --- a/internal/admin/static/admin.css +++ b/internal/admin/static/admin.css @@ -441,6 +441,21 @@ g.ud-hover:hover text{font-weight:bold} .ie-step-addbar-bottom{display:grid;grid-template-columns:repeat(auto-fit,minmax(max(100%/5,110px),1fr));gap:4px;margin-top:6px} .ie-act-addbar{display:grid;grid-template-columns:repeat(auto-fit,minmax(max(100%/5,110px),1fr));gap:4px;margin-top:2px} +/* ── Compact drop chance view ── */ +.obj-drop-view{background:rgba(255,255,255,.03);border:1px solid var(--border);border-radius:4px;margin-top:8px;overflow:hidden;width:fit-content} +.obj-drop-view-header{display:flex;align-items:center;gap:6px;padding:5px 8px;background:rgba(255,255,255,.04);border-bottom:1px solid var(--border);font-size:9px;color:#888;text-transform:uppercase;letter-spacing:.5px} +.obj-drop-view-header span{flex:1} +.obj-drop-view-refresh{width:18px;height:18px;padding:0;display:inline-flex;align-items:center;justify-content:center;font-size:12px;min-width:auto;background:rgba(255,255,255,0.07);color:#888;border:1px solid rgba(255,255,255,0.12);border-radius:3px;cursor:pointer;font-family:monospace;line-height:1;flex-shrink:0} +.obj-drop-view-refresh:hover{color:var(--text);border-color:var(--border);background:rgba(255,255,255,0.12)} +.obj-drop-view-body{padding:4px 8px;max-height:200px;overflow-y:auto} +.obj-drop-view-table{display:inline-grid;grid-template-columns:auto auto;gap:2px 12px} +.obj-drop-view-row{display:contents} +.obj-drop-view-item{font-size:10px;color:#ccc;padding:2px 0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.obj-drop-view-chance{font-size:10px;color:#6cf;padding:2px 0;white-space:nowrap;font-family:monospace} +.obj-drop-view-empty{font-size:10px;color:#555;padding:4px 0;font-style:italic} +.obj-drop-view-loading{font-size:10px;color:#888;font-style:italic} +.obj-drop-view-error{font-size:10px;color:var(--danger)} + /* ── Message rows within a step ── */ .ie-msg-row{display:flex;align-items:center;gap:4px} .ie-msg-up,.ie-msg-dn{padding:0 5px;font-size:10px;line-height:1} diff --git a/internal/admin/static/dropeditor.js b/internal/admin/static/dropeditor.js index 5900930..e8ade87 100644 --- a/internal/admin/static/dropeditor.js +++ b/internal/admin/static/dropeditor.js @@ -78,6 +78,7 @@ function renderDropEditor(id, data) { $('#editorMain').innerHTML = html; setupSearchFields(); + setupDropEditorDropView(); } function renderCurrent() { @@ -140,6 +141,7 @@ function renderArraySection(sec, data) { h += '<button class="btn btn-sm obj-sub-add" style="flex:1;margin-top:0" onclick="addDropRow(\'' + sec.id + '\',\'item\')">+ Add Item Row</button>'; h += '<button class="btn btn-sm obj-sub-add" style="flex:1;margin-top:0" onclick="addDropRow(\'' + sec.id + '\',\'table\')">+ Add Table Row</button>'; h += '</div>'; + h += '<div class="obj-drop-view"></div>'; return h; } @@ -291,3 +293,12 @@ function duplicateDrop() { loadDropEditor(newID); }).catch(function(e) { notify('Duplicate failed: ' + e.message, 'error'); }); } + +function setupDropEditorDropView() { + var card = document.querySelector('.obj-card[data-card="drops"]'); + if (!card) return; + DropView.mount(card.querySelector('.obj-card-body'), function() { + ced_syncDOMToData(dropData); + return (dropData.drops || []).filter(function(e) { return e.item_id || e.table; }); + }); +} 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 + }; +})(); diff --git a/internal/admin/static/intereditor.js b/internal/admin/static/intereditor.js index 06159fa..da1812d 100644 --- a/internal/admin/static/intereditor.js +++ b/internal/admin/static/intereditor.js @@ -700,6 +700,7 @@ function ie_renderAct(stepObj, secId, idx, si, secPath) { rows += '<div class="ie-act-kv" data-ie-act-key="drop_table">'; rows += '<div class="ie-act-kv-header"><span>Drop Table</span><button class="btn btn-sm btn-danger ie-act-rm" style="margin-left:auto" onclick="ie_removeActEffect(this,\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',' + si + ',\'drop_table\')">X</button></div>'; rows += ie_renderDropTableBlock(val, 'ie_addDropRow(this,\''+escAttr(secId)+'\',\''+escAttr(secPath)+'\','+idx+','+si+',\'item\')', 'ie_addDropRow(this,\''+escAttr(secId)+'\',\''+escAttr(secPath)+'\','+idx+','+si+',\'table\')', 'ie_removeDropRow(this,\''+escAttr(secId)+'\',\''+escAttr(secPath)+'\','+idx+','+si+',__RI__)'); + rows += '<div class="obj-drop-view"></div>'; rows += '</div>'; } else { rows += '<div class="ie-act-row"><span class="ie-act-label">' + esc(at.label) + '</span>'; diff --git a/internal/admin/static/itemeditor.js b/internal/admin/static/itemeditor.js index c28f273..04ad20b 100644 --- a/internal/admin/static/itemeditor.js +++ b/internal/admin/static/itemeditor.js @@ -220,6 +220,7 @@ function renderItemEditor(id, data) { setupCraftTypeChange(); setupEquipTypeChange(); updateOutputQtyVisibility(); + DropView.setupAllInter(); setTimeout(function() { document.querySelectorAll('.color-field').forEach(function(el) { bindColorField(el); }); }, 50); diff --git a/internal/admin/static/map.js b/internal/admin/static/map.js index 390043e..259fac6 100644 --- a/internal/admin/static/map.js +++ b/internal/admin/static/map.js @@ -602,6 +602,7 @@ function renderSidePanel(data) { html += '</div>'; panel.innerHTML = html; + DropView.setupAllInter(); setTimeout(function() { if (currentPanelTab >= 8) { var content = panel.querySelector('.panel-tab-content'); diff --git a/internal/admin/static/mobeditor.js b/internal/admin/static/mobeditor.js index 4b136a3..5e2b9cc 100644 --- a/internal/admin/static/mobeditor.js +++ b/internal/admin/static/mobeditor.js @@ -217,6 +217,8 @@ function renderMobEditor(id, data) { renderTalkTree(data); } }); + + setupMobDropViews(); } function renderCurrent() { @@ -674,6 +676,7 @@ function renderMobLootSubtable(sec, st, arr) { h += '<button class="btn btn-sm obj-sub-add" onclick="addMobLootRow(\'' + sec.id + '\',\'' + st.key + '\',\'item\')">+ Add Item</button>'; h += '<button class="btn btn-sm obj-sub-add" onclick="addMobLootRow(\'' + sec.id + '\',\'' + st.key + '\',\'table\')">+ Add Table</button>'; h += '</div>'; + h += '<div class="obj-drop-view"></div>'; h += '</div>'; return h; } @@ -1001,6 +1004,26 @@ function deleteMob() { }).catch(function(e) { notify('Delete failed: ' + e.message, 'error'); }); } +function setupMobDropViews() { + var dropsCard = document.querySelector('.obj-card[data-card="drops"]'); + if (dropsCard) { + DropView.mount(dropsCard.querySelector('.obj-card-body'), function() { + ced_syncDOMToData(mobData); + var loot = (mobData.drops && mobData.drops.loot) || []; + return loot.filter(function(e) { return e.item_id || e.table; }); + }); + } + var stealCard = document.querySelector('.obj-card[data-card="steal"]'); + if (stealCard) { + DropView.mount(stealCard.querySelector('.obj-card-body'), function() { + ced_syncDOMToData(mobData); + var drops = (mobData.steal && mobData.steal.drops) || []; + return drops.filter(function(e) { return e.item_id || e.table; }); + }); + } + DropView.setupAllInter(); +} + // ---- editor.js compatibility ---- function loadItem(id) { loadMob(id); } diff --git a/internal/admin/static/objecteditor.js b/internal/admin/static/objecteditor.js index b4d8282..6ae1dba 100644 --- a/internal/admin/static/objecteditor.js +++ b/internal/admin/static/objecteditor.js @@ -174,6 +174,7 @@ function renderObjectEditor(id, data) { $('#editorMain').innerHTML = html; setupSearchFields(); + setupObjectDropViews(); setTimeout(function() { document.querySelectorAll('.color-field').forEach(function(el) { bindColorField(el); }); }, 50); @@ -640,6 +641,7 @@ function renderSubtable(sec, st, data) { h += '<button class="btn btn-sm obj-sub-add" style="flex:1;margin-top:0" onclick="addDropRow(\'' + sec.id + '\',\'' + st.key + '\',\'item\')">+ Add Item Drop</button>'; h += '<button class="btn btn-sm obj-sub-add" style="flex:1;margin-top:0" onclick="addDropRow(\'' + sec.id + '\',\'' + st.key + '\',\'table\')">+ Add Table Drop</button>'; h += '</div>'; + h += '<div class="obj-drop-view"></div>'; } else if (!st.single || arr.length === 0) { h += '<button class="btn btn-sm obj-sub-add" onclick="addSubRow(\'' + sec.id + '\',\'' + st.key + '\')">+ Add</button>'; } @@ -1200,6 +1202,32 @@ function deleteObject() { }).catch(function(e) { notify('Delete failed: ' + e.message, 'error'); }); } +function setupObjectDropViews() { + var gatherCard = document.querySelector('.obj-card[data-card="gather"]'); + if (gatherCard) { + var body = gatherCard.querySelector('.obj-card-body'); + if (body && body.querySelector('.obj-drop-view')) { + DropView.mount(body, function() { + ced_syncDOMToData(objectData); + var drops = (objectData.gather && objectData.gather.drops) || []; + return drops.filter(function(e) { return e.item_id || e.table; }); + }); + } + } + var stealCard = document.querySelector('.obj-card[data-card="steal"]'); + if (stealCard) { + var body = stealCard.querySelector('.obj-card-body'); + if (body && body.querySelector('.obj-drop-view')) { + DropView.mount(body, function() { + ced_syncDOMToData(objectData); + var drops = (objectData.steal && objectData.steal.drops) || []; + return drops.filter(function(e) { return e.item_id || e.table; }); + }); + } + } + DropView.setupAllInter(); +} + function loadItem(id) { loadObject(id); } function createNew() { var name = prompt('Object ID:'); diff --git a/internal/admin/static/talktree.js b/internal/admin/static/talktree.js index ff44ff1..7c7090d 100644 --- a/internal/admin/static/talktree.js +++ b/internal/admin/static/talktree.js @@ -317,6 +317,7 @@ html += renderAddNodeRow(); html += '</div>'; container.innerHTML = html; + if (typeof DropView !== 'undefined') DropView.setupAllInter(); if (typeof ced_setupSearchFields === 'function') { setTimeout(function() { ced_setupSearchFields(); }, 10); } diff --git a/internal/admin/static/triggerseditor.js b/internal/admin/static/triggerseditor.js index 0312a41..7cb5d9e 100644 --- a/internal/admin/static/triggerseditor.js +++ b/internal/admin/static/triggerseditor.js @@ -182,6 +182,7 @@ function renderTriggerEditor(id, data) { if (r) r.checked = true; }); } + DropView.setupAllInter(); } function saveTrigger() { diff --git a/internal/admin/templates/drops.html b/internal/admin/templates/drops.html index f270fe8..d7ae8de 100644 --- a/internal/admin/templates/drops.html +++ b/internal/admin/templates/drops.html @@ -11,6 +11,7 @@ </div> <script src="/static/editor.js"></script> <script src="/static/cardeditor.js"></script> +<script src="/static/dropview.js"></script> <script src="/static/dropeditor.js"></script> <script> initDropEditor(); diff --git a/internal/admin/templates/map.html b/internal/admin/templates/map.html index 79cd14f..4fe99a8 100644 --- a/internal/admin/templates/map.html +++ b/internal/admin/templates/map.html @@ -52,5 +52,6 @@ </div> <script src="/static/cardeditor.js"></script> <script src="/static/intereditor.js"></script> +<script src="/static/dropview.js"></script> <script src="/static/map.js"></script> {{end}} diff --git a/internal/admin/templates/mobs.html b/internal/admin/templates/mobs.html index 14ac360..9824e7c 100644 --- a/internal/admin/templates/mobs.html +++ b/internal/admin/templates/mobs.html @@ -13,6 +13,7 @@ <script src="/static/cardeditor.js"></script> <script src="/static/intereditor.js"></script> <script src="/static/talktree.js"></script> +<script src="/static/dropview.js"></script> <script src="/static/mobeditor.js"></script> <script> initMobEditor(); diff --git a/internal/admin/templates/objects.html b/internal/admin/templates/objects.html index 3062c70..89f282f 100644 --- a/internal/admin/templates/objects.html +++ b/internal/admin/templates/objects.html @@ -12,6 +12,7 @@ <script src="/static/editor.js"></script> <script src="/static/cardeditor.js"></script> <script src="/static/intereditor.js"></script> +<script src="/static/dropview.js"></script> <script src="/static/objecteditor.js"></script> <script> initObjectEditor(); diff --git a/internal/admin/templates/triggers.html b/internal/admin/templates/triggers.html index d790427..4acf4b4 100644 --- a/internal/admin/templates/triggers.html +++ b/internal/admin/templates/triggers.html @@ -12,6 +12,7 @@ <script src="/static/editor.js"></script> <script src="/static/cardeditor.js"></script> <script src="/static/intereditor.js"></script> +<script src="/static/dropview.js"></script> <script src="/static/triggerseditor.js"></script> <script> initTriggerEditor(); |
