From 09d325dcf5e779eab5550d3fd3377bde50101428 Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Wed, 8 Jul 2026 19:59:14 -0400 Subject: feat: unify on use, on look, and on kill. all support same conditions/actions now. --- internal/admin/static/mobeditor.js | 163 ++++++++++++++++++++++++++++++++++++- 1 file changed, 162 insertions(+), 1 deletion(-) (limited to 'internal/admin/static/mobeditor.js') diff --git a/internal/admin/static/mobeditor.js b/internal/admin/static/mobeditor.js index b5d2395..7f8c0e3 100644 --- a/internal/admin/static/mobeditor.js +++ b/internal/admin/static/mobeditor.js @@ -120,6 +120,16 @@ var MOB_SECTIONS = [ ]} ] }, + { + id: 'on_kill', label: 'On Kill', labelHint: '(fired when this mob is defeated or its task completes — additive over drops)', path: 'on_kill', isArray: true, fullWidth: true, interactionStyle: true, + detect: function(d) { return d.on_kill && Array.isArray(d.on_kill); }, + fields: [ + ['item_id', 'Item ID', 'search', '(optional — only fires if you wield this weapon when the mob is defeated)', '', null, 'items'], + ['condition', 'Condition', 'json', 'e.g. {"global_flag":"quest_active"}'], + ['action', 'Action', 'json', 'e.g. {"set_global_flags":{"boss_killed":true},"broadcast":"%p has slain the boss!","spawn_mob":"mini_boss"}'], + ['message', 'Message', 'text', 'Shown to the killer when this fires...'], + ] + }, ]; function mobCardPath(sec) { return ced_cardPath(sec); } @@ -236,6 +246,8 @@ function renderMobCard(sec, data) { h += renderMobCoreCard(data); } else if (sec.id === 'combat') { h += renderMobCombatCard(data); + } else if (sec.isArray) { + h += renderMobArraySection(sec, data); } else if (sec.subtables) { h += '
'; sec.fields.forEach(function(f) { @@ -423,6 +435,135 @@ function renderMobField(sec, f, data, idx, subPath, optStyle) { return ced_renderField(sec, f, data, idx, subPath, optStyle, mobCardPath, mobFieldID, mobGetVal); } +// ---- Interaction-style array rendering (On Kill) ---- + +function renderMobArraySection(sec, data) { + var arr = data[mobCardPath(sec)] || []; + var h = ''; + var isInter = !!sec.interactionStyle; + if (isInter) { + window._mobInterVis = window._mobInterVis || {}; + if (!window._mobInterVis[sec.id]) window._mobInterVis[sec.id] = {}; + } + arr.forEach(function(item, idx) { + var visMap = window._mobInterVis[sec.id]; + var vis = visMap[idx] || {}; + visMap[idx] = vis; + var showItem = vis.item_id || (item.item_id && item.item_id !== ''); + var showCond = vis.condition || (item.condition && item.condition !== ''); + var showAct = vis.action || (item.action && item.action !== ''); + var showMsg = vis.message || (item.message && item.message !== ''); + + h += '
'; + h += ''; + h += '
'; + if (!showItem) { + h += ''; + h += '
'; + } + if (!showCond) { + h += ''; + } + if (!showAct) { + h += ''; + } + if (!showMsg) { + h += ''; + } + h += '
'; + h += '
'; + h += renderMobField(sec, sec.fields[0], data, idx, null, 'flex:1;min-width:0'); + h += ''; + h += '
'; + h += '
'; + h += renderMobField(sec, sec.fields[1], data, idx, null, 'flex:1;min-width:0'); + h += ''; + h += '
'; + h += '
'; + h += renderMobField(sec, sec.fields[2], data, idx, null, 'flex:1;min-width:0'); + h += ''; + h += '
'; + h += '
'; + h += renderMobField(sec, sec.fields[3], data, idx, null, 'flex:1;min-width:0'); + h += ''; + h += '
'; + h += '
'; + }); + h += ''; + if (isInter) { + var helpKey = 'mob_' + sec.id + '_helpCollapsed'; + var helpCollapsed = window[helpKey] === undefined ? true : window[helpKey]; + h += '
'; + h += '
'; + h += '' + (helpCollapsed ? '▶' : '▼') + ''; + h += 'How Conditions & Actions Work'; + h += '
'; + if (!helpCollapsed) { + h += '
'; + h += 'Entries checked top-to-bottom, first match wins (one fires per kill; additive to standard loot).

'; + h += 'Item ID: only fires if you wield this weapon (in main_hand or off_hand) when the mob is defeated. Leave empty to fire unconditionally.
'; + h += 'Condition: JSON gate. {"global_flag":"boss_quest"}, {"player_flag":"step","value":2}, {"has_item":"trophy"}, {"all_of":[...]}, {"any_of":[...]}.
'; + h += 'Action: JSON effects. {"set_global_flags":{...}}, {"set_player_flags":{...}}, {"give_item":"gem"}, {"take_item":"key"}, {"teleport":5}, {"heal":10}, {"credits":100}, {"aps_node":true}, {"spawn_mob":"mini_boss"}, {"despawn_mob":"boss"}, {"broadcast":"%p vanquished the boss!"}, {"broadcast_global":"..."}.
'; + h += 'Message: shown to the killer when this interaction fires.'; + h += '
'; + } + h += '
'; + } + return h; +} + +function mobAddArrayItem(cardID) { + var sec = MOB_SECTIONS.find(function(s) { return s.id === cardID; }); + if (!sec || !sec.isArray) return; + var arr = mobData[sec.path] || []; + var empty = {}; + sec.fields.forEach(function(f) { + empty[f[0]] = f[2] === 'checkbox' ? false : (f[2] === 'number' ? 0 : ''); + }); + arr.push(empty); + mobData[sec.path] = arr; + renderCurrent(); +} + +function mobRemoveArrayItem(cardID, idx) { + var sec = MOB_SECTIONS.find(function(s) { return s.id === cardID; }); + if (!sec || !sec.isArray) return; + ced_syncDOMToData(mobData); + var arr = mobData[sec.path] || []; + arr.splice(idx, 1); + renderMobEditor(mobID, mobData); +} + +function mobShowInterField(secId, idx, key) { + window._mobInterVis = window._mobInterVis || {}; + if (!window._mobInterVis[secId]) window._mobInterVis[secId] = {}; + if (!window._mobInterVis[secId][idx]) window._mobInterVis[secId][idx] = {}; + window._mobInterVis[secId][idx][key] = true; + renderCurrent(); +} + +function mobHideInterField(secId, idx, key) { + window._mobInterVis = window._mobInterVis || {}; + if (!window._mobInterVis[secId]) window._mobInterVis[secId] = {}; + if (!window._mobInterVis[secId][idx]) window._mobInterVis[secId][idx] = {}; + window._mobInterVis[secId][idx][key] = false; + ced_syncDOMToData(mobData); + if (mobData[secId] && mobData[secId][idx]) { + mobData[secId][idx][key] = ''; + } + renderMobEditor(mobID, mobData); +} + +function mobToggleInterHelp(secId) { + var key = 'mob_' + secId + '_helpCollapsed'; + if (window[key] === undefined) { + window[key] = false; + } else { + window[key] = !window[key]; + } + renderCurrent(); +} + // ---- Subtable rendering ---- function renderMobSubField(sec, stKey, idx, f, val) { @@ -602,7 +743,9 @@ function mobAddSection() { if (!sec) return; if (sec.path) { - if (id === 'combat') { + if (sec.isArray) { + mobData[sec.path] = []; + } else if (id === 'combat') { mobData[sec.path] = { kind: 'combat', aggressive: false, @@ -694,6 +837,24 @@ function saveMob() { return; } + if (sec.isArray) { + var arr = []; + var card = document.querySelector('.obj-card[data-card="' + sec.id + '"]'); + if (!card) return; + var rows = card.querySelectorAll('.obj-sub-row'); + rows.forEach(function(row, idx) { + var item = {}; + sec.fields.forEach(function(f) { + var fid = mobFieldID(sec, f[0], idx); + var el = document.getElementById(fid); + if (el) item[f[0]] = ced_collectFieldValue(el, f[2]); + }); + if (Object.keys(item).length > 0) arr.push(item); + }); + if (arr.length > 0) out[sec.path] = arr; + return; + } + var sectionObj = {}; var hasValues = sec.always; -- cgit v1.2.3