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(-)
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 = '
';
+ rows += '';
+ 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 += '';
+ 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 @@
+