aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--internal/admin/static/admin.css15
-rw-r--r--internal/admin/static/dropeditor.js11
-rw-r--r--internal/admin/static/dropview.js239
-rw-r--r--internal/admin/static/intereditor.js1
-rw-r--r--internal/admin/static/itemeditor.js1
-rw-r--r--internal/admin/static/map.js1
-rw-r--r--internal/admin/static/mobeditor.js23
-rw-r--r--internal/admin/static/objecteditor.js28
-rw-r--r--internal/admin/static/talktree.js1
-rw-r--r--internal/admin/static/triggerseditor.js1
-rw-r--r--internal/admin/templates/drops.html1
-rw-r--r--internal/admin/templates/map.html1
-rw-r--r--internal/admin/templates/mobs.html1
-rw-r--r--internal/admin/templates/objects.html1
-rw-r--r--internal/admin/templates/triggers.html1
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();