diff options
| author | historia <[not public]> | 2026-07-01 18:36:21 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-07-01 18:36:21 -0400 |
| commit | 78b522d57a9ebc691e075523910d7cdaa06b6493 (patch) | |
| tree | b2a68397e0cecc5abb56e5ae451c05a9435cc5fe /internal/admin/static/mobeditor.js | |
| parent | e6b4cb9f5816c6ee239233bc85f74ee446a161c2 (diff) | |
| download | thehouseoficarus-78b522d57a9ebc691e075523910d7cdaa06b6493.tar.gz | |
feat: shop system removed from talk system, added per-item restock and variable pricing
Diffstat (limited to 'internal/admin/static/mobeditor.js')
| -rw-r--r-- | internal/admin/static/mobeditor.js | 188 |
1 files changed, 165 insertions, 23 deletions
diff --git a/internal/admin/static/mobeditor.js b/internal/admin/static/mobeditor.js index 59eb16c..9928adb 100644 --- a/internal/admin/static/mobeditor.js +++ b/internal/admin/static/mobeditor.js @@ -10,6 +10,11 @@ var MOB_SECTIONS = [ fields: [ ['name', 'Name', 'text', 'spaceport attendant'], ['description', 'Description', 'textarea', 'A lanky man in a wrinkled grey uniform.'], + ['unique', 'Unique', 'checkbox'], + ['protected', 'Protected', 'checkbox'], + ['aggressive', 'Aggressive', 'checkbox'], + ['kind', 'Kind', 'select', 'combat|task'], + ['respawn_ticks', 'Respawn Ticks', 'number', '', 'narrow'], ], inline: [ {label:'Idle Descriptions', key:'idle_descriptions', type:'tags'}, @@ -45,17 +50,6 @@ var MOB_SECTIONS = [ ] }, { - id: 'behavior', label: 'Behavior', - detect: function(d) { return d.aggressive || d.protected || d.unique || d.respawn_ticks || d.kind; }, - fields: [ - ['aggressive', 'Aggressive', 'checkbox'], - ['protected', 'Protected', 'checkbox'], - ['unique', 'Unique', 'checkbox'], - ['respawn_ticks', 'Respawn Ticks', 'number', '', 'narrow'], - ['kind', 'Kind', 'select', 'combat|task'], - ] - }, - { id: 'steal', label: 'Steal', detect: function(d) { return d.steal_table || d.steal_level || d.steal_speed || d.steal_xp; }, fields: [ @@ -96,14 +90,13 @@ var MOB_SECTIONS = [ ['message', 'Greeting', 'text', 'What would you like to buy or sell?'], ['buy_percentage', 'Buy %', 'number', '40', 'narrow'], ['change_percentage', 'Change %', 'number', '3', 'narrow'], - ['restock_ticks', 'Restock Ticks', 'number', '', 'narrow'], ['buys_anything', 'Buys Anything', 'checkbox'], ], subtables: [ - {label:'Items', key:'items', addLabel:'+ Add Item', fields:[ - ['item_id', 'Item ID', 'search', '', '', null, 'items'], + {label:'Items', key:'items', addLabel:'+ Add Item', table:true, fields:[ + ['item_id', 'Item', 'search', '', '', null, 'items'], ['stock', 'Stock', 'number', '', 'narrow'], - ['restock_ticks', 'Restock Ticks', 'number', '', 'narrow'], + ['restock_ticks', 'Restock Ticks', 'number', '1000', 'narrow'], ]} ] }, @@ -111,15 +104,13 @@ var MOB_SECTIONS = [ id: 'drops', label: 'Drops', path: 'drops', detect: function(d) { return d.drops && (d.drops.remains || (d.drops.loot && d.drops.loot.length > 0)); }, fields: [ - ['remains', 'Remains', 'text', 'bones'], + ['remains', 'Remains', 'search', '', '', null, 'items'], ], subtables: [ {label:'Loot', key:'loot', fields:[ - ['item_id', 'Item ID', 'search', '', '', null, 'items'], + ['item_id', 'Item', 'search', '', '', null, 'items'], ['table', 'Table', 'search', '', '', null, 'drops'], ['weight', 'Weight', 'number', '', 'narrow'], - ['level', 'Level', 'number', '', 'narrow'], - ['xp', 'XP', 'number', '', 'narrow'], ['quantity', 'Quantity', 'number', '', 'narrow'], ]} ] @@ -149,6 +140,11 @@ function loadMob(id) { renderList(''); API.get('/api/mobs/' + encodeURIComponent(id)).then(function(data) { mobData = data; + // buys_anything defaults to true (Go treats absent as true); reflect that in + // the checkbox unless the shop explicitly opted out with false. + if (mobData.shop && mobData.shop.buys_anything === undefined) { + mobData.shop.buys_anything = true; + } renderMobEditor(id, data); }).catch(function(e) { $('#editorMain').innerHTML = '<p style="color:var(--danger)">Failed to load: ' + esc(e.message) + '</p>'; @@ -198,6 +194,7 @@ function renderMobEditor(id, data) { $('#editorMain').innerHTML = html; ced_setupSearchFields(); setupMobKindChange(); + setupMobProtectedToggle(); MOB_SECTIONS.forEach(function(sec) { if (sec.id === 'talk' && data.talk && data.talk.nodes) { @@ -291,16 +288,52 @@ function renderMobCard(sec, data) { function renderMobCoreCard(data) { var sec = MOB_SECTIONS[0]; + var byKey = {}; + sec.fields.forEach(function(f) { byKey[f[0]] = f; }); var h = '<div class="obj-core-row">'; - h += renderMobField(sec, sec.fields[0], data, -1, null, 'flex:1'); + h += renderMobField(sec, byKey['name'], data, -1, null, 'flex:1'); h += '</div>'; - h += renderMobField(sec, sec.fields[1], data, -1); + h += renderMobField(sec, byKey['description'], data, -1); sec.inline.forEach(function(il) { h += ced_renderTags(sec, il, data); }); + // Behavior fields (merged into Core). + h += '<div class="obj-card-grid" style="margin-top:6px">'; + h += renderMobField(sec, byKey['unique'], data, -1); + h += renderMobField(sec, byKey['protected'], data, -1); + h += renderMobField(sec, byKey['aggressive'], data, -1); + h += '</div>'; + h += '<div class="obj-card-grid" style="margin-top:6px">'; + h += renderMobField(sec, byKey['kind'], data, -1); + h += renderMobField(sec, byKey['respawn_ticks'], data, -1); + h += '</div>'; return h; } +// setupMobProtectedToggle hides Aggressive, Respawn Ticks and Kind while +// Protected is checked (protected mobs never attack, respawn, or work). Toggles +// CSS only — no re-render — so other unsaved field edits are preserved. +function setupMobProtectedToggle() { + var sec = MOB_SECTIONS.find(function(s) { return s.id === 'core'; }); + if (!sec) return; + var protEl = document.getElementById(mobFieldID(sec, 'protected', -1)); + if (!protEl) return; + function apply() { + var hide = protEl.checked; + ['aggressive', 'respawn_ticks', 'kind'].forEach(function(key) { + var el = document.getElementById(mobFieldID(sec, key, -1)); + if (!el) return; + var field = el.closest('.obj-field'); + if (field) field.style.display = hide ? 'none' : ''; + }); + } + if (!protEl._hasProtectedToggle) { + protEl._hasProtectedToggle = true; + protEl.addEventListener('change', apply); + } + apply(); +} + function renderMobField(sec, f, data, idx, subPath, optStyle) { return ced_renderField(sec, f, data, idx, subPath, optStyle, mobCardPath, mobFieldID, mobGetVal); } @@ -340,6 +373,12 @@ function renderMobSubtable(sec, st, data) { } else { arr = []; } + if (st.table) { + return renderMobTableSubtable(sec, st, arr); + } + if (sec.id === 'drops' && st.key === 'loot') { + return renderMobLootSubtable(sec, st, arr); + } var h = '<div class="obj-subtable">'; h += '<div class="obj-subtable-header">' + esc(st.label) + '</div>'; arr.forEach(function(item, idx) { @@ -360,6 +399,94 @@ function renderMobSubtable(sec, st, data) { return h; } +// renderMobLootSubtable renders the Drops loot list where each row is either an +// item drop (Item + Weight + Quantity) or a table drop (Table + Weight + Rolls). +// A table entry's "Rolls" is stored in the `quantity` field and means the number +// of independent draws from that table (see behavior.ResolveDropList). +function renderMobLootSubtable(sec, st, arr) { + var byKey = {}; + st.fields.forEach(function(f) { byKey[f[0]] = f; }); + var rollsField = ['quantity', 'Rolls', 'number', '', 'narrow']; + + var h = '<div class="obj-subtable">'; + h += '<div class="obj-subtable-header">' + esc(st.label) + '</div>'; + arr.forEach(function(item, idx) { + var kind = item.table ? 'table' : (item.item_id ? 'item' : (item._kind || 'item')); + h += '<div class="obj-sub-row">'; + if (kind === 'table') { + h += renderMobSubField(sec, st.key, idx, byKey['table'], item.table); + h += renderMobSubField(sec, st.key, idx, byKey['weight'], item.weight); + h += renderMobSubField(sec, st.key, idx, rollsField, item.quantity); + } else { + h += renderMobSubField(sec, st.key, idx, byKey['item_id'], item.item_id); + h += renderMobSubField(sec, st.key, idx, byKey['weight'], item.weight); + h += renderMobSubField(sec, st.key, idx, byKey['quantity'], item.quantity); + } + h += '<button class="btn btn-sm btn-danger obj-sub-remove" style="margin-bottom:3px" onclick="removeMobSubRow(\'' + sec.id + '\',\'' + st.key + '\',' + idx + ')">X</button>'; + h += '</div>'; + }); + h += '<div style="display:flex;gap:6px">'; + h += '<button class="btn btn-sm obj-sub-add" onclick="addMobLootRow(\'item\')">+ Add Item</button>'; + h += '<button class="btn btn-sm obj-sub-add" onclick="addMobLootRow(\'table\')">+ Add Table</button>'; + h += '</div>'; + h += '</div>'; + return h; +} + +// Column width (px) for a non-search field, sized to fit its header label. +function mobTableColWidth(f) { + return Math.max(60, esc(f[1]).length * 8 + 20); +} + +// renderMobTableSubtable renders a subtable in the ingredients-style table +// layout: a single column-header row (no card header) and one row per entry. +function renderMobTableSubtable(sec, st, arr) { + var h = '<div class="ingredients-table" style="margin-top:10px">'; + h += '<div class="ingredients-header-row">'; + st.fields.forEach(function(f) { + if (f[2] === 'search') { + h += '<div class="ingredients-col ingredients-col-items">' + esc(f[1]) + '</div>'; + } else { + h += '<div class="ingredients-col" style="width:' + mobTableColWidth(f) + 'px">' + esc(f[1]) + '</div>'; + } + }); + h += '<div class="ingredients-col ingredients-col-x"></div>'; + h += '</div>'; + + arr.forEach(function(item, idx) { + h += '<div class="ingredients-ingredient"><div class="ingredients-pair">'; + st.fields.forEach(function(f) { + var fid = mobFieldIDSub(sec, st.key, idx, f[0]); + var val = item[f[0]]; + if (f[2] === 'search') { + var entity = f[6] || 'items'; + var sval = (val === undefined || val === null) ? '' : String(val); + h += '<div class="ingredients-col ingredients-col-items"><div class="obj-field obj-search" style="width:100%"><div style="position:relative;width:100%">' + + '<input id="' + fid + '" value="' + escAttr(sval) + '" class="search-input" data-search-type="' + entity + '" placeholder="Search ' + entity + '...">' + + '<div class="search-results" style="display:none"></div></div></div></div>'; + } else { + // Numeric column. Blank a 0/unset value only when the field has a + // placeholder, so the placeholder (e.g. "1000") shows through. + var hasPh = !!f[3]; + var nval; + if (val === undefined || val === null || val === '') nval = ''; + else if (val === 0 && hasPh) nval = ''; + else nval = String(val); + var ph = hasPh ? ' placeholder="' + escAttr(f[3]) + '"' : ''; + h += '<div class="ingredients-col" style="width:' + mobTableColWidth(f) + 'px">' + + '<label class="obj-field"><input id="' + fid + '" value="' + escAttr(nval) + '"' + ph + '></label>' + + '</div>'; + } + }); + h += '<div class="ingredients-col ingredients-col-x"><button class="btn btn-sm btn-danger" style="padding:2px 6px;font-size:10px" onclick="removeMobSubRow(\'' + sec.id + '\',\'' + st.key + '\',' + idx + ')">X</button></div>'; + h += '</div></div>'; + }); + + h += '</div>'; + h += '<button class="btn btn-sm obj-sub-add" onclick="addMobSubRow(\'' + sec.id + '\',\'' + st.key + '\')">' + esc(st.addLabel || '+ Add') + '</button>'; + return h; +} + // ---- Card toggle / section add-remove ---- function toggleMobCard(id) { @@ -390,7 +517,7 @@ function mobAddSection() { if (sec.path) { if (sec.id === 'drops') { - mobData[sec.path] = {remains:'', loot:[]}; + mobData[sec.path] = {remains:'bones', loot:[]}; } else if (sec.id === 'talk') { mobData[sec.path] = {nodes:{start:{messages:[],options:[]}}}; } else if (sec.id === 'shop') { @@ -420,10 +547,25 @@ function removeMobSubRow(cardID, subKey, idx) { renderCurrent(); } +// addMobLootRow appends an item- or table-kind loot entry. The ephemeral _kind +// marker is only used for rendering fresh empty rows; it's never saved (save +// rebuilds rows from the visible inputs) and is re-inferred from item_id/table +// on reload. +function addMobLootRow(kind) { + if (!mobData.drops) mobData.drops = {}; + if (!Array.isArray(mobData.drops.loot)) mobData.drops.loot = []; + if (kind === 'table') { + mobData.drops.loot.push({table:'', weight:0, quantity:1, _kind:'table'}); + } else { + mobData.drops.loot.push({item_id:'', weight:0, quantity:1, _kind:'item'}); + } + renderCurrent(); +} + // ---- Kind change handler (show/hide task fields) ---- function setupMobKindChange() { - var kindSel = document.getElementById(mobFieldID(MOB_SECTIONS.find(function(s){return s.id==='behavior';}), 'kind', -1)); + var kindSel = document.getElementById(mobFieldID(MOB_SECTIONS.find(function(s){return s.id==='core';}), 'kind', -1)); if (!kindSel) return; kindSel.addEventListener('change', function() { if (kindSel.value === 'task') { |
