From d02f0d2cdf67f1445a9ccae86b99a2578adab585 Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Fri, 10 Jul 2026 17:02:13 -0400 Subject: feat(admin): update search to use standard condition/action framework rather than bespoke implementation --- internal/admin/static/admin.css | 2 + internal/admin/static/intereditor.js | 118 +++++++++++++++++++++++++++++++- internal/admin/static/itemeditor.js | 74 ++++++++++++++++---- internal/admin/static/mobeditor.js | 2 +- internal/admin/static/objecteditor.js | 4 +- internal/admin/static/triggerseditor.js | 37 +++++++++- internal/admin/templates/items.html | 1 + internal/behavior/behavior.go | 51 +++++++------- internal/behavior/types.go | 25 +++---- internal/game/act.go | 2 - internal/game/act_effects.go | 75 ++++++++++++++++---- internal/game/act_search.go | 107 ++--------------------------- internal/game/act_state.go | 2 - internal/game/cmd_search.go | 3 +- internal/item/item.go | 4 +- internal/validate/checks.go | 74 ++++++++++++++------ 16 files changed, 375 insertions(+), 206 deletions(-) (limited to 'internal') diff --git a/internal/admin/static/admin.css b/internal/admin/static/admin.css index f77e55a..496a031 100644 --- a/internal/admin/static/admin.css +++ b/internal/admin/static/admin.css @@ -391,6 +391,8 @@ g.ud-hover:hover text{font-weight:bold} .ie-act-rm{flex-shrink:0} .ie-act-kv{background:var(--bg);border:1px solid var(--border);border-radius:3px;padding:6px} .ie-act-kv-header{display:flex;align-items:center;gap:8px;margin-bottom:4px} +.ie-drop-table{display:flex;flex-direction:column;gap:4px} +.ie-drop-row{display:flex;gap:6px;align-items:center} .ie-kv-list{display:flex;flex-direction:column;gap:3px} .ie-kv-row{display:flex;gap:4px;align-items:center} .ie-kv-key,.ie-kv-val{font-size:11px;padding:2px 4px;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:3px;font-family:monospace;width:120px} diff --git a/internal/admin/static/intereditor.js b/internal/admin/static/intereditor.js index c4f16e7..0e2c03d 100644 --- a/internal/admin/static/intereditor.js +++ b/internal/admin/static/intereditor.js @@ -3,7 +3,8 @@ // Trigger = { lock, item_id?, condition?, steps[], on_player_flag?, on_global_flag?, value? } // Step = { condition?, wait, messages[], broadcast, broadcast_global, // spawn_mob, despawn_mob, set_global_flags, set_player_flags, -// give_item, take_item, teleport, heal, credits, aps_node } +// give_item, take_item, teleport, heal, credits, aps_node, +// drop_table[] } // Shared by objecteditor.js, mobeditor.js, and map.js. // ── Condition leaf types ── @@ -32,6 +33,7 @@ var IE_ACT_TYPES = [ {key: 'set_player_flags', label: 'Set Player Flags', kind: 'kv'}, {key: 'give_item', label: 'Give Item', kind: 'search', entity: 'items'}, {key: 'take_item', label: 'Take Item', kind: 'search', entity: 'items'}, + {key: 'drop_table', label: 'Drop Table', kind: 'droptable'}, {key: 'teleport', label: 'Teleport', kind: 'number', hint: 'room ID'}, {key: 'heal', label: 'Heal', kind: 'number', hint: 'HP to restore'}, {key: 'credits', label: 'Credits', kind: 'number', hint: 'amount (negative = cost)'}, @@ -83,6 +85,62 @@ function ie_renderBottomAddbar(secId, secPath, idx) { return h; } +// ── Drop Table block renderer (shared between intereditor and triggerseditor) ── + +function ie_renderDropTableBlock(drops, onAddItem, onAddTable, onRemove) { + drops = drops || []; + var h = '
'; + drops.forEach(function(row, ri) { + var kind = row.table ? 'table' : (row.item_id ? 'item' : (row._kind || 'item')); + h += '
'; + if (kind === 'table') { + h += ''; + } else { + h += ''; + } + h += ''; + h += ''; + h += ''; + h += '
'; + }); + h += '
'; + h += ''; + h += ''; + h += '
'; + h += '
'; + return h; +} + +function ie_collectDropTableBlock(container) { + if (!container) return null; + var rows = container.querySelectorAll(':scope > .ie-drop-row'); + var out = []; + rows.forEach(function(row) { + var tableInput = row.querySelector('.ie-drop-table-input'); + var itemInput = row.querySelector('.ie-drop-item-input'); + var weightInput = row.querySelector('.ie-drop-weight'); + var qtyInput = row.querySelector('.ie-drop-qty'); + var entry = {}; + if (tableInput && tableInput.value.trim()) { + entry.table = tableInput.value.trim(); + entry._kind = 'table'; + } else if (itemInput && itemInput.value.trim()) { + entry.item_id = itemInput.value.trim(); + entry._kind = 'item'; + } else if (itemInput) { + entry.item_id = ''; + entry._kind = 'item'; + } else { + entry.table = ''; + entry._kind = 'table'; + } + entry.weight = weightInput ? (parseInt(weightInput.value, 10) || 0) : 0; + entry.quantity = qtyInput ? (parseInt(qtyInput.value, 10) || 0) : 0; + out.push(entry); + }); + return out.length > 0 ? out : null; +} + // ie_addStepKind creates a new step initialised with a single action kind. function ie_addStepKind(secId, secPath, idx, kind) { var ed = window._editorData; @@ -103,6 +161,7 @@ function ie_addStepKind(secId, secPath, idx, kind) { if (at.kind === 'kv') step[kind] = {}; else if (at.kind === 'checkbox') step[kind] = true; else if (at.kind === 'number') step[kind] = 0; + else if (at.kind === 'droptable') step[kind] = []; else step[kind] = ''; } ed[secPath][idx].steps.push(step); @@ -507,6 +566,11 @@ function ie_renderAct(stepObj, secId, idx, si, secPath) { rows += ''; rows += ''; rows += ''; + } else if (at.kind === 'droptable') { + rows += '
'; + rows += '
Drop Table
'; + rows += ie_renderDropTableBlock(val, 'ie_addDropRow(\''+escAttr(secId)+'\',\''+escAttr(secPath)+'\','+idx+','+si+',\'item\')', 'ie_addDropRow(\''+escAttr(secId)+'\',\''+escAttr(secPath)+'\','+idx+','+si+',\'table\')', 'ie_removeDropRow(\''+escAttr(secId)+'\',\''+escAttr(secPath)+'\','+idx+','+si+',__RI__)'); + rows += '
'; } else { rows += '
' + esc(at.label) + ''; rows += ''; @@ -595,6 +659,11 @@ function ie_collectAct(container) { var key = kvEl.getAttribute('data-ie-act-key'); if (!key) return; if (key === 'messages') return; + if (key === 'drop_table') { + var dt = ie_collectDropTableBlock(kvEl.querySelector('.ie-drop-table')); + if (dt) r.drop_table = dt; + return; + } var map = {}; kvEl.querySelectorAll('.ie-kv-row').forEach(function(row) { var kEl = row.querySelector('.ie-kv-key'); @@ -1013,6 +1082,8 @@ function ie_addActEffect(secId, secPath, idx, si, effectKey) { step[effectKey] = true; } else if (at.kind === 'number') { step[effectKey] = 0; + } else if (at.kind === 'droptable') { + step[effectKey] = []; } else { step[effectKey] = ''; } @@ -1077,8 +1148,37 @@ function ie_addMessages(secId, secPath, idx, si) { if (!ed) return; ie_syncStep(ed, secId, secPath, idx, si); if (!ed[secPath] || !ed[secPath][idx] || !ed[secPath][idx].steps || !ed[secPath][idx].steps[si]) return; - ed[secPath][idx].steps[si].messages = ['']; - ced_syncDOMToData(ed); + var step = ed[secPath][idx].steps[si]; + if (!step.messages || !Array.isArray(step.messages)) step.messages = []; + step.messages.push(''); + window._ieRenderCurrent(); +} + +// ── Drop Table row add/remove ── + +function ie_addDropRow(secId, secPath, idx, si, kind) { + var ed = window._editorData; + if (!ed) return; + ie_syncStep(ed, secId, secPath, idx, si); + if (!ed[secPath] || !ed[secPath][idx] || !ed[secPath][idx].steps || !ed[secPath][idx].steps[si]) return; + var step = ed[secPath][idx].steps[si]; + if (!Array.isArray(step.drop_table)) step.drop_table = []; + if (kind === 'table') { + step.drop_table.push({table: '', weight: 10, quantity: 1, _kind: 'table'}); + } else { + step.drop_table.push({item_id: '', weight: 10, quantity: 1, _kind: 'item'}); + } + window._ieRenderOnly(); +} + +function ie_removeDropRow(secId, secPath, idx, si, ri) { + var ed = window._editorData; + if (!ed) return; + ie_syncStep(ed, secId, secPath, idx, si); + if (!ed[secPath] || !ed[secPath][idx] || !ed[secPath][idx].steps || !ed[secPath][idx].steps[si]) return; + var step = ed[secPath][idx].steps[si]; + if (!Array.isArray(step.drop_table)) return; + step.drop_table.splice(ri, 1); window._ieRenderOnly(); } @@ -1214,6 +1314,18 @@ function ie_cleanStep(step) { if (!step || !Object.prototype.hasOwnProperty.call(step, at.key)) return; var v = step[at.key]; if (v === undefined || v === null || v === '') return; + if (at.key === 'drop_table' && Array.isArray(v)) { + var cleaned = v.map(function(e) { + var c = {}; + if (e.item_id) c.item_id = e.item_id; + if (e.table) c.table = e.table; + if (e.weight) c.weight = e.weight; + if (e.quantity) c.quantity = e.quantity; + return c; + }).filter(function(e) { return Object.keys(e).length > 0; }); + if (cleaned.length > 0) s.drop_table = cleaned; + return; + } if (typeof v === 'object') { if (Object.keys(v).length === 0) return; s[at.key] = v; diff --git a/internal/admin/static/itemeditor.js b/internal/admin/static/itemeditor.js index a86d240..53242ab 100644 --- a/internal/admin/static/itemeditor.js +++ b/internal/admin/static/itemeditor.js @@ -84,13 +84,9 @@ var SECTIONS = [ ] }, { - id: 'search', label: 'Search', - detect: function(d) { return d.search_table || d.search_ticks; }, - fields: [ - ['search_table', 'Search Table', 'search', 'e.g. gem_table', '', null, 'drops'], - ['search_ticks', 'Search Ticks', 'number', '8', 'narrow'], - ['search_message', 'Search Message', 'text', 'You search...', 'wide'], - ] + id: 'search', label: 'Search', path: 'search', isArray: true, fullWidth: true, interactionStyle: true, itemIDAllowed: false, + detect: function(d) { return d.search && Array.isArray(d.search); }, + fields: [] }, { id: 'food', label: 'Food', @@ -126,6 +122,9 @@ var SECTIONS = [ ]; function initItemEditor() { + window._ieRenderCurrent = renderCurrent; + window._ieRenderOnly = function() { if (itemID && itemData) renderItemEditor(itemID, itemData); }; + window._ieSyncAll = function(ed) { if (ed) ie_syncAllInteractions(ed, SECTIONS); }; editorType = 'items'; editorFields = []; loadList(); @@ -167,6 +166,7 @@ function loadItemEditor(id) { API.get('/api/items/' + encodeURIComponent(id)).then(function(data) { itemData = data; window._editorData = itemData; + window._interVis = {}; renderItemEditor(id, data); }).catch(function(e) { $('#editorMain').innerHTML = '

Failed to load: ' + esc(e.message) + '

'; @@ -225,6 +225,7 @@ function renderItemEditor(id, data) { function renderCurrent() { ced_syncDOMToData(itemData); + if (window._ieSyncAll) window._ieSyncAll(itemData); if (!itemData || !itemID) return; renderItemEditor(itemID, itemData); } @@ -250,7 +251,22 @@ function renderCard(sec, data) { h += '
'; if (sec.isArray) { - h += renderArraySection(sec, data); + if (sec.interactionStyle) { + window._interVis = window._interVis || {}; + if (!window._interVis[sec.id]) window._interVis[sec.id] = {}; + h += ie_renderArraySection({ + sec: sec, + data: data, + visMap: window._interVis[sec.id], + showRemove: function(idx) { return 'removeArrayItem(\'' + sec.id + '\',' + idx + ')'; }, + showField: function(secId, idx, key) { return 'showInterField(\'' + secId + '\',' + idx + ',\'' + key + '\')'; }, + hideField: function(secId, idx, key) { return 'hideInterField(\'' + secId + '\',' + idx + ',\'' + key + '\')'; }, + renderField: function(sec, f, data, idx, optStyle) { return renderField(sec, f, data, idx, null, optStyle); }, + onAdd: function() { return 'addArrayItem(\'' + sec.id + '\')'; }, + }); + } else { + h += renderArraySection(sec, data); + } } else if (sec.id === 'core') { h += renderCoreCard(data); } else if (sec.id === 'equipment') { @@ -964,6 +980,8 @@ function addSection() { if (sec.path) { if (sec.id === 'equipment') { itemData.equipment = {type:'', slot:'', attack_type:'', speed:0, junk:''}; + } else if (sec.isArray && sec.interactionStyle) { + itemData[sec.path] = []; } else { itemData[sec.path] = {}; } @@ -981,11 +999,15 @@ function addArrayItem(cardID) { var sec = SECTIONS.find(function(s) { return s.id === cardID; }); if (!sec || !sec.isArray) return; var arr = itemData[sec.path] || []; - var empty = {}; - sec.fields.forEach(function(f) { - empty[f[0]] = f[2] === 'checkbox' ? false : (f[2] === 'number' ? 0 : ''); - }); - arr.push(empty); + if (sec.interactionStyle) { + arr.push({steps: []}); + } else { + var empty = {}; + sec.fields.forEach(function(f) { + empty[f[0]] = f[2] === 'checkbox' ? false : (f[2] === 'number' ? 0 : ''); + }); + arr.push(empty); + } itemData[sec.path] = arr; renderCurrent(); } @@ -994,11 +1016,24 @@ function removeArrayItem(cardID, idx) { var sec = SECTIONS.find(function(s) { return s.id === cardID; }); if (!sec || !sec.isArray) return; ced_syncDOMToData(itemData); + if (window._ieSyncAll) window._ieSyncAll(itemData); var arr = itemData[sec.path] || []; arr.splice(idx, 1); renderItemEditor(itemID, itemData); } +function showInterField(secId, idx, key) { + window._interVis = window._interVis || {}; + ie_showField(window._interVis, secId, idx, key); + renderCurrent(); +} + +function hideInterField(secId, idx, key) { + window._interVis = window._interVis || {}; + ie_hideField(window._interVis, secId, idx, key); + renderCurrent(); +} + // ========================================================================= // Subtable row add/remove // ========================================================================= @@ -1332,6 +1367,19 @@ function saveItem() { var el = document.getElementById(fid); if (el) item[f[0]] = collectFieldValue(el, f[2]); }); + if (sec.interactionStyle) { + var dataArr = itemData[sec.path] || []; + var dataItem = dataArr[idx] || {}; + if (dataItem.condition && typeof dataItem.condition === 'object') { + var cleanedCond = ie_cleanCondition(dataItem.condition); + if (cleanedCond) item.condition = cleanedCond; + } + if (dataItem.lock) item.lock = true; + if (dataItem.steps && Array.isArray(dataItem.steps)) { + var cleanedSteps = dataItem.steps.map(ie_cleanStep).filter(function(s) { return Object.keys(s).length > 0; }); + if (cleanedSteps.length > 0) item.steps = cleanedSteps; + } + } if (Object.keys(item).length > 0) arr.push(item); }); if (arr.length > 0) out[sec.path] = arr; diff --git a/internal/admin/static/mobeditor.js b/internal/admin/static/mobeditor.js index 183aef9..29664dc 100644 --- a/internal/admin/static/mobeditor.js +++ b/internal/admin/static/mobeditor.js @@ -121,7 +121,7 @@ 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, + id: 'on_kill', label: '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'], diff --git a/internal/admin/static/objecteditor.js b/internal/admin/static/objecteditor.js index 1abccbf..88bc5b7 100644 --- a/internal/admin/static/objecteditor.js +++ b/internal/admin/static/objecteditor.js @@ -86,14 +86,14 @@ var SECTIONS = [ ] }, { - id: 'on_use', label: 'On Use', labelHint: '(what happens when you use items on this)', path: 'on_use', isArray: true, fullWidth: true, interactionStyle: true, + id: 'on_use', label: 'Use', labelHint: '(what happens when you use items on this)', path: 'on_use', isArray: true, fullWidth: true, interactionStyle: true, detect: function(d) { return d.on_use && Array.isArray(d.on_use); }, fields: [ ['item_id', 'Item ID', 'search', 'e.g. keycard (empty = bare use)', '', null, 'items'], ] }, { - id: 'on_look', label: 'On Look', labelHint: '(what happens when you "look ")', path: 'on_look', isArray: true, fullWidth: true, interactionStyle: true, + id: 'on_look', label: 'Look', labelHint: '(what happens when you "look ")', path: 'on_look', isArray: true, fullWidth: true, interactionStyle: true, detect: function(d) { return d.on_look && Array.isArray(d.on_look); }, fields: [ ['item_id', 'Item ID', 'search', '(optional — only fires if you carry this item)', '', null, 'items'], diff --git a/internal/admin/static/triggerseditor.js b/internal/admin/static/triggerseditor.js index 29759eb..dbc1e41 100644 --- a/internal/admin/static/triggerseditor.js +++ b/internal/admin/static/triggerseditor.js @@ -110,7 +110,7 @@ function syncStepEffects(row, si) { if (!effectsEl) return; var step = triggerData.steps[si] || {}; ['messages','broadcast','broadcast_global','teleport','heal','credits', - 'give_item','take_item','despawn_mob','aps_node'].forEach(function(k) { delete step[k]; }); + 'give_item','take_item','despawn_mob','aps_node','drop_table'].forEach(function(k) { delete step[k]; }); delete step.set_global_flags; delete step.set_player_flags; delete step.spawn_mob; var msgInputs = effectsEl.querySelectorAll('.trig-msg-input'); @@ -147,6 +147,12 @@ function syncStepEffects(row, si) { step.spawn_mob.id = smInput.value.trim(); } + var dtBlock = effectsEl.querySelector('.ie-drop-table'); + if (dtBlock) { + var dt = ie_collectDropTableBlock(dtBlock); + if (dt) step.drop_table = dt; + } + triggerData.steps[si] = step; } @@ -336,9 +342,37 @@ function renderStepEffects(step, si) { h += renderKVSection('Set Player Flags', 'set_player_flags', step.set_player_flags); } + if (Array.isArray(step.drop_table) || step.drop_table !== undefined) { + h += '
Drop Table
'; + h += ie_renderDropTableBlock(step.drop_table, 'trigAddDropRow('+si+',\'item\')', 'trigAddDropRow('+si+',\'table\')', 'trigRemoveDropRow('+si+',__RI__)'); + h += '
'; + } + return h; } +function trigAddDropRow(si, kind) { + if (!triggerData || !Array.isArray(triggerData.steps) || !triggerData.steps[si]) return; + syncAllStepEffects(); + var step = triggerData.steps[si]; + if (!Array.isArray(step.drop_table)) step.drop_table = []; + if (kind === 'table') { + step.drop_table.push({table: '', weight: 10, quantity: 1, _kind: 'table'}); + } else { + step.drop_table.push({item_id: '', weight: 10, quantity: 1, _kind: 'item'}); + } + renderCurrent(); +} + +function trigRemoveDropRow(si, ri) { + if (!triggerData || !Array.isArray(triggerData.steps) || !triggerData.steps[si]) return; + syncAllStepEffects(); + var step = triggerData.steps[si]; + if (!Array.isArray(step.drop_table)) return; + step.drop_table.splice(ri, 1); + renderCurrent(); +} + function addStep() { if (!triggerData) return; if (!Array.isArray(triggerData.steps)) triggerData.steps = []; @@ -378,6 +412,7 @@ function addStepEffect(si, kind) { if (!at) return; if (at.kind === 'kv') step[kind] = {}; else if (at.kind === 'checkbox') step[kind] = true; + else if (at.kind === 'droptable') step[kind] = []; else if (at.kind === 'number') step[kind] = 0; else if (at.kind === 'search') step[kind] = ''; else step[kind] = ''; diff --git a/internal/admin/templates/items.html b/internal/admin/templates/items.html index 5d82ace..7efeded 100644 --- a/internal/admin/templates/items.html +++ b/internal/admin/templates/items.html @@ -11,6 +11,7 @@
+