diff options
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/admin/static/admin.css | 2 | ||||
| -rw-r--r-- | internal/admin/static/intereditor.js | 118 | ||||
| -rw-r--r-- | internal/admin/static/itemeditor.js | 74 | ||||
| -rw-r--r-- | internal/admin/static/mobeditor.js | 2 | ||||
| -rw-r--r-- | internal/admin/static/objecteditor.js | 4 | ||||
| -rw-r--r-- | internal/admin/static/triggerseditor.js | 37 | ||||
| -rw-r--r-- | internal/admin/templates/items.html | 1 | ||||
| -rw-r--r-- | internal/behavior/behavior.go | 51 | ||||
| -rw-r--r-- | internal/behavior/types.go | 25 | ||||
| -rw-r--r-- | internal/game/act.go | 2 | ||||
| -rw-r--r-- | internal/game/act_effects.go | 75 | ||||
| -rw-r--r-- | internal/game/act_search.go | 107 | ||||
| -rw-r--r-- | internal/game/act_state.go | 2 | ||||
| -rw-r--r-- | internal/game/cmd_search.go | 3 | ||||
| -rw-r--r-- | internal/item/item.go | 4 | ||||
| -rw-r--r-- | 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 = '<div class="ie-drop-table">'; + drops.forEach(function(row, ri) { + var kind = row.table ? 'table' : (row.item_id ? 'item' : (row._kind || 'item')); + h += '<div class="ie-drop-row">'; + if (kind === 'table') { + h += '<div class="obj-search" style="position:relative;min-width:0;flex:2"><input class="search-input ie-drop-table-input" data-search-type="drops" value="' + escAttr(row.table || '') + '" placeholder="Table ID"><div class="search-results" style="display:none"></div></div>'; + } else { + h += '<div class="obj-search" style="position:relative;min-width:0;flex:2"><input class="search-input ie-drop-item-input" data-search-type="items" value="' + escAttr(row.item_id || '') + '" placeholder="Item ID"><div class="search-results" style="display:none"></div></div>'; + } + h += '<input class="ie-drop-weight" type="number" value="' + (row.weight || 10) + '" placeholder="Weight" style="width:60px" min="0">'; + h += '<input class="ie-drop-qty" type="number" value="' + (row.quantity || 1) + '" placeholder="Qty" style="width:50px" min="0">'; + h += '<button class="btn btn-sm btn-danger" onclick="' + onRemove.replace(/__RI__/g, String(ri)) + '">X</button>'; + h += '</div>'; + }); + h += '<div style="display:flex;gap:6px;margin-top:4px">'; + h += '<button class="btn btn-sm obj-sub-add" onclick="' + onAddItem + '">+ Add Item</button>'; + h += '<button class="btn btn-sm obj-sub-add" onclick="' + onAddTable + '">+ Add Table</button>'; + h += '</div>'; + h += '</div>'; + 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 += '<div class="obj-search" style="position:relative;min-width:0;flex:1"><input class="ie-act-input search-input" data-ie-act-key="' + escAttr(at.key) + '" data-search-type="' + escAttr(at.entity) + '" placeholder="' + escAttr(at.hint || 'Search ' + at.entity + '...') + '" value="' + escAttr(String(val)) + '"><div class="search-results" style="display:none"></div></div>'; rows += '<button class="btn btn-sm btn-danger ie-act-rm" onclick="ie_removeActEffect(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',' + si + ',\'' + escAttr(at.key) + '\')">X</button>'; rows += '</div>'; + } else if (at.kind === 'droptable') { + 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(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',' + si + ',\'drop_table\')">X</button></div>'; + 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 += '</div>'; } else { rows += '<div class="ie-act-row"><span class="ie-act-label">' + esc(at.label) + '</span>'; rows += '<input class="ie-act-input" data-ie-act-key="' + escAttr(at.key) + '"' + (at.kind === 'number' ? ' type="number"' : '') + ' data-ie-act-kind="' + escAttr(at.kind) + '" value="' + escAttr(String(val)) + '" placeholder="' + escAttr(at.hint || '') + '" style="flex:1">'; @@ -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 = '<p style="color:var(--danger)">Failed to load: ' + esc(e.message) + '</p>'; @@ -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 += '<div class="obj-card-body">'; 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 <this>")', path: 'on_look', isArray: true, fullWidth: true, interactionStyle: true, + id: 'on_look', label: 'Look', labelHint: '(what happens when you "look <this>")', 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 += '<div class="ie-act-kv"><div class="ie-act-kv-header"><span>Drop Table</span></div>'; + h += ie_renderDropTableBlock(step.drop_table, 'trigAddDropRow('+si+',\'item\')', 'trigAddDropRow('+si+',\'table\')', 'trigRemoveDropRow('+si+',__RI__)'); + h += '</div>'; + } + 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 @@ </div> <script src="/static/editor.js"></script> <script src="/static/cardeditor.js"></script> +<script src="/static/intereditor.js"></script> <script src="/static/itemeditor.js"></script> <script> initItemEditor(); diff --git a/internal/behavior/behavior.go b/internal/behavior/behavior.go index 75f836b..9152f8c 100644 --- a/internal/behavior/behavior.go +++ b/internal/behavior/behavior.go @@ -55,21 +55,22 @@ type TalkOption struct { // scheduler and ignores the per-step Condition (its gating is done at the // parent Trigger level instead). type Step struct { - Condition *Condition `yaml:"condition,omitempty"` - Wait int `yaml:"wait,omitempty"` - Messages []string `yaml:"messages,omitempty"` - Broadcast string `yaml:"broadcast,omitempty"` - BroadcastGlobal string `yaml:"broadcast_global,omitempty"` - SpawnMob *SpawnMobConfig `yaml:"spawn_mob,omitempty"` - DespawnMob string `yaml:"despawn_mob,omitempty"` - SetGlobalFlags map[string]any `yaml:"set_global_flags,omitempty"` - SetPlayerFlags map[string]any `yaml:"set_player_flags,omitempty"` - GiveItem string `yaml:"give_item,omitempty"` - TakeItem string `yaml:"take_item,omitempty"` - Teleport int `yaml:"teleport,omitempty"` - Heal int `yaml:"heal,omitempty"` - Credits int `yaml:"credits,omitempty"` - ApsNode bool `yaml:"aps_node,omitempty"` + Condition *Condition `yaml:"condition,omitempty" json:"condition,omitempty"` + Wait int `yaml:"wait,omitempty" json:"wait,omitempty"` + Messages []string `yaml:"messages,omitempty" json:"messages,omitempty"` + Broadcast string `yaml:"broadcast,omitempty" json:"broadcast,omitempty"` + BroadcastGlobal string `yaml:"broadcast_global,omitempty" json:"broadcast_global,omitempty"` + SpawnMob *SpawnMobConfig `yaml:"spawn_mob,omitempty" json:"spawn_mob,omitempty"` + DespawnMob string `yaml:"despawn_mob,omitempty" json:"despawn_mob,omitempty"` + SetGlobalFlags map[string]any `yaml:"set_global_flags,omitempty" json:"set_global_flags,omitempty"` + SetPlayerFlags map[string]any `yaml:"set_player_flags,omitempty" json:"set_player_flags,omitempty"` + GiveItem string `yaml:"give_item,omitempty" json:"give_item,omitempty"` + TakeItem string `yaml:"take_item,omitempty" json:"take_item,omitempty"` + Teleport int `yaml:"teleport,omitempty" json:"teleport,omitempty"` + Heal int `yaml:"heal,omitempty" json:"heal,omitempty"` + Credits int `yaml:"credits,omitempty" json:"credits,omitempty"` + ApsNode bool `yaml:"aps_node,omitempty" json:"aps_node,omitempty"` + DropTable []DropEntry `yaml:"drop_table,omitempty" json:"drop_table,omitempty"` } // HasEffects reports whether the step carries any effect beyond a pure wait. @@ -78,7 +79,7 @@ func (s Step) HasEffects() bool { s.SpawnMob != nil || s.DespawnMob != "" || len(s.SetGlobalFlags) > 0 || len(s.SetPlayerFlags) > 0 || s.GiveItem != "" || s.TakeItem != "" || s.Teleport != 0 || s.Heal != 0 || - s.Credits != 0 || s.ApsNode + s.Credits != 0 || s.ApsNode || len(s.DropTable) > 0 } // Trigger is the one conditional wrapper used by every event block in the @@ -211,13 +212,13 @@ func (c *ShopConfig) FindItem(itemID string) *ShopItem { // Not inverter. Room matches when the triggering player is currently in that // room (ignored for global-flag triggers with no player). type Condition struct { - GlobalFlag string `yaml:"global_flag,omitempty"` - PlayerFlag string `yaml:"player_flag,omitempty"` - Room int `yaml:"room,omitempty"` - Value any `yaml:"value,omitempty"` - Not bool `yaml:"not,omitempty"` - HasItem string `yaml:"has_item,omitempty"` - MinCredits int `yaml:"min_credits,omitempty"` - AllOf []Condition `yaml:"all_of,omitempty"` - AnyOf []Condition `yaml:"any_of,omitempty"` + GlobalFlag string `yaml:"global_flag,omitempty" json:"global_flag,omitempty"` + PlayerFlag string `yaml:"player_flag,omitempty" json:"player_flag,omitempty"` + Room int `yaml:"room,omitempty" json:"room,omitempty"` + Value any `yaml:"value,omitempty" json:"value,omitempty"` + Not bool `yaml:"not,omitempty" json:"not,omitempty"` + HasItem string `yaml:"has_item,omitempty" json:"has_item,omitempty"` + MinCredits int `yaml:"min_credits,omitempty" json:"min_credits,omitempty"` + AllOf []Condition `yaml:"all_of,omitempty" json:"all_of,omitempty"` + AnyOf []Condition `yaml:"any_of,omitempty" json:"any_of,omitempty"` } diff --git a/internal/behavior/types.go b/internal/behavior/types.go index 7aba045..86115a7 100644 --- a/internal/behavior/types.go +++ b/internal/behavior/types.go @@ -33,7 +33,6 @@ const ( TypeTalk ActionType = "talk" TypeBurn ActionType = "burn" TypeStoke ActionType = "stoke" - TypeSearch ActionType = "search" TypeIdentify ActionType = "identify" TypePlant ActionType = "plant" TypeHarvest ActionType = "harvest" @@ -165,12 +164,6 @@ type FarmData struct { MaxYield float64 } -type SearchData struct { - ItemID string - SlotIdx int - Started bool -} - type CombineData struct { ItemID string Phase int @@ -205,15 +198,15 @@ type TriggerModuleData struct { } type DropEntry struct { - ItemID string `yaml:"item_id"` - Table string `yaml:"table"` - Tool string `yaml:"tool,omitempty"` - Weight int `yaml:"weight"` - Quantity int `yaml:"quantity"` - Depletes bool `yaml:"depletes"` - SuccessMessage string `yaml:"success_message"` - Level int `yaml:"level"` - XP int `yaml:"xp"` + ItemID string `yaml:"item_id" json:"item_id"` + Table string `yaml:"table" json:"table"` + Tool string `yaml:"tool,omitempty" json:"tool,omitempty"` + Weight int `yaml:"weight" json:"weight"` + Quantity int `yaml:"quantity" json:"quantity"` + Depletes bool `yaml:"depletes" json:"depletes"` + SuccessMessage string `yaml:"success_message" json:"success_message"` + Level int `yaml:"level" json:"level"` + XP int `yaml:"xp" json:"xp"` } type DropTableDef struct { diff --git a/internal/game/act.go b/internal/game/act.go index 352f235..64d1848 100644 --- a/internal/game/act.go +++ b/internal/game/act.go @@ -213,8 +213,6 @@ func (g *Game) AdvanceActions() { g.advanceBurn(sess, p) case "stoke": g.advanceStoke(sess, p) - case "search": - g.advanceSearch(sess, p) case "identify": g.advanceIdentify(sess, p) case "steal": diff --git a/internal/game/act_effects.go b/internal/game/act_effects.go index cb60c5d..cadd05e 100644 --- a/internal/game/act_effects.go +++ b/internal/game/act_effects.go @@ -44,12 +44,14 @@ const ( // 5. set_player_flags (player scope; cascades via setPlayerFlag) // 6. take_item (player scope) // 7. give_item (player scope; honors 28-slot inventory limit) -// 8. heal (player scope; clamps to MaxHP) -// 9. credits (player scope; negative is gated by affordability) -// 10. spawn_mob (player scope: player-owned; global scope: world-owned) -// 11. despawn_mob (player scope: owner-filtered; global scope: all) -// 12. teleport (player scope; re-runs look + on_enter of target) -// 13. aps_node (player scope; marks "aps_node_<roomID>" player flag) +// 8. drop_table (player scope; resolves weighted drops into inventory, +// overflow to ground; supports credits pseudo-item) +// 9. heal (player scope; clamps to MaxHP) +// 10. credits (player scope; negative is gated by affordability) +// 11. spawn_mob (player scope: player-owned; global scope: world-owned) +// 12. despawn_mob (player scope: owner-filtered; global scope: all) +// 13. teleport (player scope; re-runs look + on_enter of target) +// 14. aps_node (player scope; marks "aps_node_<roomID>" player flag) func (g *Game) applyStep(sess *net.Session, p *player.Player, step *behavior.Step, roomID int, sc effectScope, flagValue any) { if step == nil { return @@ -97,7 +99,7 @@ func (g *Game) applyStep(sess *net.Session, p *player.Player, step *behavior.Ste } if sc == scopeGlobal { - // 10/11. world-owned spawn/despawn. + // 11/12. world-owned spawn/despawn. if step.SpawnMob != nil { g.spawnWorldTriggerMob(step.SpawnMob, roomID) } @@ -138,7 +140,17 @@ func (g *Game) applyStep(sess *net.Session, p *player.Player, step *behavior.Ste } } - // 8. heal + // 8. drop_table — weighted loot into inventory, overflow to ground. + if len(step.DropTable) > 0 { + for _, d := range behavior.ResolveDropList(g.DataDir, step.DropTable) { + if d.ItemID == "" { + continue + } + g.giveTriggerDrop(sess, p, &d) + } + } + + // 9. heal if step.Heal > 0 { p.HP += step.Heal if maxHP := p.MaxHP(); p.HP > maxHP { @@ -150,7 +162,7 @@ func (g *Game) applyStep(sess *net.Session, p *player.Player, step *behavior.Ste } } - // 9. credits (signed; negative is gated). + // 10. credits (signed; negative is gated). if step.Credits != 0 { if step.Credits < 0 { if p.Credits >= -step.Credits { @@ -165,7 +177,7 @@ func (g *Game) applyStep(sess *net.Session, p *player.Player, step *behavior.Ste } } - // 10/11. spawn_mob / despawn_mob (player-owned / owner-filtered). + // 11/12. spawn_mob / despawn_mob (player-owned / owner-filtered). if step.SpawnMob != nil { g.spawnTriggerMob(sess, p, step.SpawnMob, roomID) } @@ -173,12 +185,12 @@ func (g *Game) applyStep(sess *net.Session, p *player.Player, step *behavior.Ste g.despawnTriggerMobs(step.DespawnMob, p.Name) } - // 12. teleport + // 13. teleport if step.Teleport > 0 && sess != nil { g.teleportPlayer(sess, p, step.Teleport) } - // 13. aps_node — dynamic room-id flag, kept as a dedicated effect. + // 14. aps_node — dynamic room-id flag, kept as a dedicated effect. if step.ApsNode { g.setPlayerFlag(p, "aps_node_"+strconv.Itoa(p.RoomID), true) g.AccountStore.SaveCharacter(p) @@ -200,6 +212,45 @@ func (g *Game) writePlayerMessage(sess *net.Session, msg string, playerName stri sess.WriteLine(rendered) } +// giveTriggerDrop awards a resolved DropEntry to a player. Item goes to the +// first free inventory slot; if inventory is full it falls to the ground +// (reserved for the player). "credits" pseudo-item adds credits directly. +func (g *Game) giveTriggerDrop(sess *net.Session, p *player.Player, drop *behavior.DropEntry) { + if p == nil { + return + } + qty := drop.Quantity + if qty <= 0 { + qty = 1 + } + + if drop.ItemID == "credits" { + p.Credits += qty + if sess != nil { + sess.WriteLine(fmt.Sprintf("You receive %d credits.", qty)) + } + g.AccountStore.SaveCharacter(p) + return + } + + name := drop.ItemID + lootDef, _ := g.ItemStore.Load(drop.ItemID) + if lootDef != nil && lootDef.Name != "" { + name = lootDef.Name + } + + freeSlot := p.FirstFreeSlot() + if freeSlot == -1 { + g.World.AddReservedItem(p.RoomID, drop.ItemID, qty, p.Name) + sess.WriteLine(fmt.Sprintf("You receive %s. It falls to the ground.", g.itemColorize(sess, lootDef, name))) + return + } + + p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: drop.ItemID, Quantity: qty}) + sess.WriteLine(fmt.Sprintf("You receive %s.", g.itemColorize(sess, lootDef, name))) + g.AccountStore.SaveCharacter(p) +} + // applyTalkStep fires a talk node/option's Action (a single Step) // synchronously at scopePlayer, scoped to the player's current room. func (g *Game) applyTalkStep(sess *net.Session, step *behavior.Step) { diff --git a/internal/game/act_search.go b/internal/game/act_search.go index 83ea534..430b265 100644 --- a/internal/game/act_search.go +++ b/internal/game/act_search.go @@ -1,116 +1,17 @@ package game import ( - "fmt" - - "thehouseoficarus/internal/behavior" - "thehouseoficarus/internal/engine" "thehouseoficarus/internal/net" "thehouseoficarus/internal/player" ) -func (g *Game) startSearch(sess *net.Session, p *player.Player, itemID string, slotIdx int) { +func (g *Game) doSearchItem(sess *net.Session, p *player.Player, itemID string) { def, err := g.ItemStore.Load(itemID) - if err != nil || def.SearchTable == "" { + if err != nil || len(def.Search) == 0 { sess.WriteLine("You can't search that.") return } - - ticks := def.SearchTicks - if ticks <= 0 { - ticks = 3 - } - - p.Action = &behavior.Action{ - Type: "search", - TargetID: itemID, - TargetName: def.Name, - WaitLeft: engine.ToTicks(ticks), - Data: &behavior.SearchData{ - ItemID: itemID, - SlotIdx: slotIdx, - Started: false, - }, - } - - g.broadcastAction(sess, "%s searches a %s.", p.Name, def.Name) -} - -func (g *Game) advanceSearch(sess *net.Session, p *player.Player) { - d, ok := p.Action.Data.(*behavior.SearchData) - if !ok || d == nil { - sess.WriteLine("Something went wrong.") - g.cancelAction(p) - return - } - - def, err := g.ItemStore.Load(d.ItemID) - if err != nil { - sess.WriteLine("Something went wrong.") - g.cancelAction(p) - return - } - - if !d.Started { - msg := def.SearchMessage - if msg == "" { - msg = fmt.Sprintf("digging through the %s", def.Name) - } - sess.WriteLine(fmt.Sprintf("You begin %s.", msg)) - d.Started = true - return - } - - slot := p.InvSlot(d.SlotIdx) - if slot == nil || slot.ItemID != d.ItemID || slot.Quantity <= 0 { - sess.WriteLine("The item is gone.") - g.cancelAction(p) - return - } - - if slot.Quantity > 1 { - slot.Quantity-- - } else { - p.SetInvSlot(d.SlotIdx, nil) - } - - dt, err := behavior.LoadDropTable(g.DataDir, def.SearchTable) - if err == nil && len(dt.Drops) > 0 { - drop := behavior.ResolveDrop(g.DataDir, dt.Drops) - if drop != nil && drop.ItemID != "" { - g.giveSearchLoot(sess, p, drop) - } - } - - g.AccountStore.SaveCharacter(p) - g.cancelAction(p) -} - -func (g *Game) giveSearchLoot(sess *net.Session, p *player.Player, drop *behavior.DropEntry) { - qty := drop.Quantity - if qty <= 0 { - qty = 1 - } - - name := drop.ItemID - lootDef, _ := g.ItemStore.Load(drop.ItemID) - if lootDef != nil { - name = lootDef.Name - } - - if drop.ItemID == "credits" { - p.Credits += qty - sess.WriteLine(g.colorize(sess, "credits_pickup", fmt.Sprintf("You find %d credits.", qty))) - return - } - - freeSlot := p.FirstFreeSlot() - if freeSlot == -1 { - g.World.AddGroundItem(p.RoomID, drop.ItemID, qty) - sess.WriteLine(fmt.Sprintf("You find %s. It falls to the ground.", g.itemColorize(sess, lootDef, name))) - return + if !g.runTrigger(sess, p, def.Search, p.RoomID, p.HasItem, false) { + sess.WriteLine("You can't search that.") } - - p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: drop.ItemID, Quantity: qty}) - sess.WriteLine(fmt.Sprintf("You find %s.", g.itemColorize(sess, lootDef, name))) } diff --git a/internal/game/act_state.go b/internal/game/act_state.go index 73ecd57..335ecfb 100644 --- a/internal/game/act_state.go +++ b/internal/game/act_state.go @@ -49,8 +49,6 @@ func (g *Game) playerActionDisplay(p *player.Player) string { return "trying to start a fire" case behavior.TypeStoke: return "tending to a fire" - case behavior.TypeSearch: - return "digging through a " + a.TargetName case behavior.TypeIdentify: return "identifying scrap at " + a.TargetName case behavior.TypePlant: diff --git a/internal/game/cmd_search.go b/internal/game/cmd_search.go index 4122e0a..2c5017e 100644 --- a/internal/game/cmd_search.go +++ b/internal/game/cmd_search.go @@ -40,6 +40,5 @@ func (g *Game) doSearch(sess *net.Session, input string) { } itemID := matches[0].ID - slotIdx := matches[0].Slot - g.startSearch(sess, p, itemID, slotIdx) + g.doSearchItem(sess, p, itemID) } diff --git a/internal/item/item.go b/internal/item/item.go index 0a6ef99..227eac5 100644 --- a/internal/item/item.go +++ b/internal/item/item.go @@ -92,9 +92,7 @@ type ItemDef struct { MaxQuality int `yaml:"max_quality"` Craft CraftList `yaml:"craft,omitempty"` - SearchTable string `yaml:"search_table"` - SearchTicks float64 `yaml:"search_ticks"` - SearchMessage string `yaml:"search_message"` + Search []behavior.Trigger `yaml:"search,omitempty"` HealValue int `yaml:"heal_value"` EatMessage string `yaml:"eat_message"` diff --git a/internal/validate/checks.go b/internal/validate/checks.go index 1a0a233..f89e52d 100644 --- a/internal/validate/checks.go +++ b/internal/validate/checks.go @@ -69,7 +69,7 @@ func validateRooms(s Source) []Issue { } issues = append(issues, validateTriggerBlock( fmt.Sprintf("Room %d: exit %q on_traverse", id, dir), - exit.OnTraverse, false, itemIDs, roomIndex, mobIDs)...) + exit.OnTraverse, false, itemIDs, roomIndex, mobIDs, nil)...) } for _, rm := range room.Mobs { @@ -221,7 +221,7 @@ func validateLocalObject(roomID int, robj world.RoomObject, itemIDs map[string]b } if len(def.OnLook) > 0 { - issues = append(issues, validateTriggerBlock(prefix+": on_look", def.OnLook, false, itemIDs, roomIndex, mobIDs)...) + issues = append(issues, validateTriggerBlock(prefix+": on_look", def.OnLook, false, itemIDs, roomIndex, mobIDs, nil)...) } return issues @@ -486,7 +486,7 @@ func validateMobs(s Source) []Issue { } issues = append(issues, validateTriggerBlock( - fmt.Sprintf("Mob %q: on_kill", id), def.OnKill, false, itemIDs, roomIndex, mobIDs)...) + fmt.Sprintf("Mob %q: on_kill", id), def.OnKill, false, itemIDs, roomIndex, mobIDs, dropIDs)...) } return issues @@ -591,7 +591,7 @@ func validateObjects(s Source) []Issue { } issues = append(issues, validateTriggerBlock( - fmt.Sprintf("Object %q: on_use", id), obj.OnUse, false, itemIDs, roomIndex, mobIDs)...) + fmt.Sprintf("Object %q: on_use", id), obj.OnUse, false, itemIDs, roomIndex, mobIDs, dropIDs)...) if obj.Gather != nil { issues = append(issues, validateGather(fmt.Sprintf("Object %q: gather", id), obj.Gather, itemIDs, dropIDs)...) @@ -602,7 +602,7 @@ func validateObjects(s Source) []Issue { } if len(obj.OnLook) > 0 { - issues = append(issues, validateTriggerBlock(fmt.Sprintf("Object %q: on_look", id), obj.OnLook, false, itemIDs, roomIndex, mobIDs)...) + issues = append(issues, validateTriggerBlock(fmt.Sprintf("Object %q: on_look", id), obj.OnLook, false, itemIDs, roomIndex, mobIDs, dropIDs)...) } } @@ -613,6 +613,8 @@ func validateItems(s Source) []Issue { var issues []Issue itemIDs := s.Items.IDSet() dropIDs := dropTableIDSet(s.DataDir) + roomIndex := s.World.RoomIndex() + mobIDs := s.Mobs.AllDefIDs() for id := range itemIDs { def, err := s.Items.Load(id) @@ -632,13 +634,9 @@ func validateItems(s Source) []Issue { }) } - if def.SearchTable != "" && !dropIDs[def.SearchTable] { - issues = append(issues, Issue{ - Level: "ERROR", - Type: "reference", - Message: fmt.Sprintf("Item %q: search_table %q does not exist", - id, def.SearchTable), - }) + if len(def.Search) > 0 { + issues = append(issues, validateTriggerBlock( + fmt.Sprintf("Item %q: search", id), def.Search, false, itemIDs, roomIndex, mobIDs, dropIDs)...) } if def.FarmProduct != "" && !itemIDs[def.FarmProduct] { @@ -803,11 +801,11 @@ func validateRoomTriggers(s Source) []Issue { } if len(room.OnEnter) > 0 { issues = append(issues, validateTriggerBlock( - fmt.Sprintf("Room %d: on_enter", id), room.OnEnter, false, itemIDs, roomIndex, mobIDs)...) + fmt.Sprintf("Room %d: on_enter", id), room.OnEnter, false, itemIDs, roomIndex, mobIDs, nil)...) } if len(room.OnExit) > 0 { issues = append(issues, validateTriggerBlock( - fmt.Sprintf("Room %d: on_exit", id), room.OnExit, false, itemIDs, roomIndex, mobIDs)...) + fmt.Sprintf("Room %d: on_exit", id), room.OnExit, false, itemIDs, roomIndex, mobIDs, nil)...) } } @@ -1166,10 +1164,9 @@ func validateExitReciprocity(s Source) []Issue { return issues } -// validateStep validates a single Step's effects. mobIDs may be nil when the -// caller doesn't track mob ids (e.g. local-object on_look); spawn/despawn mob -// references are then skipped. -func validateStep(prefix string, step *behavior.Step, itemIDs map[string]bool, roomIndex map[int]bool, mobIDs map[string]bool) []Issue { +// validateStep validates a single Step's effects. mobIDs and dropIDs may be nil +// when the caller doesn't track those entities; validation is skipped for nil sets. +func validateStep(prefix string, step *behavior.Step, itemIDs map[string]bool, roomIndex map[int]bool, mobIDs map[string]bool, dropIDs map[string]bool) []Issue { var issues []Issue if step == nil { return issues @@ -1241,6 +1238,41 @@ func validateStep(prefix string, step *behavior.Step, itemIDs map[string]bool, r }) } + for i, e := range step.DropTable { + if e.ItemID == "" && e.Table == "" { + issues = append(issues, Issue{ + Level: "WARN", + Type: "semantic", + Message: fmt.Sprintf("%s: drop_table[%d] has no item_id or table", + prefix, i), + }) + } + if e.ItemID != "" && e.ItemID != "credits" && !itemIDs[e.ItemID] { + issues = append(issues, Issue{ + Level: "ERROR", + Type: "reference", + Message: fmt.Sprintf("%s: drop_table item %q does not exist", + prefix, e.ItemID), + }) + } + if e.Table != "" && dropIDs != nil && !dropIDs[e.Table] { + issues = append(issues, Issue{ + Level: "ERROR", + Type: "reference", + Message: fmt.Sprintf("%s: drop_table table %q does not exist", + prefix, e.Table), + }) + } + if e.Weight <= 0 { + issues = append(issues, Issue{ + Level: "WARN", + Type: "semantic", + Message: fmt.Sprintf("%s: drop_table[%d] weight is %d (never drops)", + prefix, i, e.Weight), + }) + } + } + if step.Condition != nil { issues = append(issues, validateCondition(prefix, step.Condition, roomIndex)...) } @@ -1315,7 +1347,7 @@ func checkPlayerOnlyInCondition(prefix string, c *behavior.Condition) []Issue { // require on_player_flag/on_global_flag and forbid item_id) and false for verb // blocks (on_use/on_look/on_kill/on_enter/on_exit/on_traverse), where item_id // is checked against itemIDs. -func validateTriggerBlock(prefix string, block []behavior.Trigger, isFlag bool, itemIDs map[string]bool, roomIndex map[int]bool, mobIDs map[string]bool) []Issue { +func validateTriggerBlock(prefix string, block []behavior.Trigger, isFlag bool, itemIDs map[string]bool, roomIndex map[int]bool, mobIDs map[string]bool, dropIDs map[string]bool) []Issue { var issues []Issue for i := range block { t := &block[i] @@ -1369,7 +1401,7 @@ func validateTriggerBlock(prefix string, block []behavior.Trigger, isFlag bool, issues = append(issues, checkPlayerOnlyInCondition(entryPrefix, t.Condition)...) } for si := range t.Steps { - issues = append(issues, validateStep(fmt.Sprintf("%s: step[%d]", entryPrefix, si), &t.Steps[si], itemIDs, roomIndex, mobIDs)...) + issues = append(issues, validateStep(fmt.Sprintf("%s: step[%d]", entryPrefix, si), &t.Steps[si], itemIDs, roomIndex, mobIDs, dropIDs)...) if isFlag && t.OnGlobalFlag != "" && t.Steps[si].Condition != nil { issues = append(issues, checkPlayerOnlyInCondition(fmt.Sprintf("%s: step[%d]", entryPrefix, si), t.Steps[si].Condition)...) } @@ -1384,5 +1416,5 @@ func validateTalkStep(prefix string, step *behavior.Step, itemIDs map[string]boo if step == nil { return nil } - return validateStep(prefix, step, itemIDs, roomIndex, nil) + return validateStep(prefix, step, itemIDs, roomIndex, nil, nil) } |
