aboutsummaryrefslogtreecommitdiff
path: root/internal/admin/static/mobeditor.js
diff options
context:
space:
mode:
Diffstat (limited to 'internal/admin/static/mobeditor.js')
-rw-r--r--internal/admin/static/mobeditor.js188
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') {