';
$('#editorMain').innerHTML = html;
setupSearchFields();
setTimeout(function() {
document.querySelectorAll('.color-field').forEach(function(el) { bindColorField(el); });
}, 50);
SECTIONS.forEach(function(sec) {
if (sec.id === 'talk' && data.talk && data.talk.nodes) {
renderTalkTree(data);
}
});
}
function cardPath(sec) {
return sec.path || '';
}
function getVal(data, sec, key) {
var p = cardPath(sec);
if (p === 'on_look' || sec.isArray) {
return data[p] && data[p][key] !== undefined ? data[p][key] : '';
}
var fp = p ? p + '.' + key : key;
var v = getNested(data, fp);
return v === undefined || v === null ? '' : v;
}
function fieldID(sec, key, idx) {
if (idx !== undefined && idx >= 0) return 'f_' + sec.id + '_' + idx + '_' + key.replace(/\./g, '_');
return 'f_' + sec.id + '_' + key.replace(/\./g, '_');
}
function fieldIDSub(sec, subKey, idx, fieldKey) {
return 'f_' + sec.id + '_' + subKey + '_' + idx + '_' + fieldKey;
}
function renderCard(sec, data) {
var collapsed = expandedCards[sec.id] === false;
var h = '
';
h += '
';
h += '' + (collapsed ? '▶' : '▼') + '';
h += '' + sec.label + '';
if (!sec.always) {
h += '';
}
h += '
';
if (!collapsed) {
h += '
';
if (sec.isArray) {
h += renderArraySection(sec, data);
} else if (sec.id === 'core') {
h += renderCoreCard(data);
} else {
h += '
';
sec.fields.forEach(function(f) {
h += renderField(sec, f, data, -1);
});
h += '
';
if (sec.inline) {
var rowGroups = {};
var noRow = [];
sec.inline.forEach(function(il) {
if (il.row !== undefined) {
if (!rowGroups[il.row]) rowGroups[il.row] = [];
rowGroups[il.row].push(il);
} else {
noRow.push(il);
}
});
Object.keys(rowGroups).sort().forEach(function(r) {
h += '
';
rowGroups[r].forEach(function(il) {
if (il.type === 'tags') {
h += '
';
h += renderTags(sec, il, data);
h += '
';
} else if (il.type === 'kv') {
h += '
';
h += renderKV(sec, il, data);
h += '
';
} else {
h += '
';
h += '' + esc(il.label) + '';
h += '
';
il.fields.forEach(function(f) {
h += renderField(sec, f, data, -1, il.path);
});
h += '
';
h += '
';
}
});
h += '
';
});
noRow.forEach(function(il) {
if (il.type === 'tags') {
h += renderTags(sec, il, data);
} else if (il.type === 'kv') {
h += renderKV(sec, il, data);
} else {
h += '
';
h += '' + esc(il.label) + '';
h += '
';
il.fields.forEach(function(f) {
h += renderField(sec, f, data, -1, il.path);
});
h += '
';
h += '
';
}
});
}
if (sec.subtables) {
sec.subtables.forEach(function(st) {
h += renderSubtable(sec, st, data);
});
}
if (sec.id === 'talk') {
h += '';
}
}
h += '
';
}
h += '
';
return h;
}
function renderField(sec, f, data, idx, subPath, optStyle) {
var key = f[0], label = f[1], type = f[2], hint = f[3], wide = f[4], showIf = f[5], searchEntity = f[6];
var fp = subPath ? subPath + '.' + key : key;
var fid = fieldID(sec, fp, idx);
var val;
if (sec.isArray && idx >= 0) {
var arr = objectData[cardPath(sec)] || [];
var item = arr[idx] || {};
val = item[key];
} else if (subPath) {
var sectionRoot = objectData[cardPath(sec)] || {};
var subObj = sectionRoot[subPath] || {};
val = subObj[key];
} else {
val = getVal(objectData, sec, key);
}
val = val === undefined || val === null ? '' : val;
var hiddenStyle = '';
if (showIf) {
var sectionData = objectData[cardPath(sec)] || {};
if (!showIf(sectionData)) hiddenStyle = ' style="display:none"';
}
function wrap(cls, inner) {
var w = (wide === 'wide') ? ' obj-wide' : '';
if (wide === 'narrow') w = ' obj-narrow';
var s = optStyle ? ' style="' + optStyle + '"' : '';
return '';
}
if (type === 'checkbox') {
return wrap('obj-field obj-check', ' ' + esc(label) + '');
}
if (type === 'json') {
var jstr = typeof val === 'object' ? JSON.stringify(val) : String(val);
return wrap('obj-field', '' + esc(label) + '');
}
if (type === 'textarea') {
var dstr = typeof val === 'object' ? (Array.isArray(val) ? val.map(function(v){return v.text||'';}).join('\n') : JSON.stringify(val)) : String(val);
return wrap('obj-field', '' + esc(label) + '');
}
if (type === 'select') {
var opts = hint ? hint.split('|') : [];
var inner = '' + esc(label) + '';
return wrap('obj-field', inner);
}
if (type === 'color') {
var swatch = '';
return wrap('obj-field color-field', '' + esc(label) + '
' + swatch + '
');
}
if (type === 'search') {
var entity = searchEntity || 'items';
var ph = hint ? ' placeholder="' + esc(hint) + '"' : ' placeholder="Search ' + entity + '..."';
return wrap('obj-field obj-search', '' + esc(label) + '
');
}
var ph = hint ? ' placeholder="' + esc(hint) + '"' : '';
return wrap('obj-field', '' + esc(label) + '');
}
function renderCoreCard(data) {
var sec = SECTIONS[0];
var h = '
';
h += renderField(sec, sec.fields[0], data, -1, null, 'flex:1'); // name
h += renderField(sec, sec.fields[2], data, -1, null, 'flex:2'); // aliases
h += renderField(sec, sec.fields[1], data, -1); // color
h += renderField(sec, sec.fields[3], data, -1); // hidden
h += '
';
h += '
';
h += renderField(sec, sec.fields[5], data, -1); // description
h += renderField(sec, sec.fields[4], data, -1); // in-room description
h += renderField(sec, sec.fields[6], data, -1); // removal item
h += '
';
return h;
}
function renderArraySection(sec, data) {
var arr = data[cardPath(sec)] || [];
var h = '';
arr.forEach(function(item, idx) {
h += '
';
h += '
';
sec.fields.forEach(function(f) {
h += renderField(sec, f, data, idx);
});
h += '
';
h += '';
h += '
';
});
h += '';
return h;
}
function renderSubtable(sec, st, data) {
var sectionRoot = objectData[cardPath(sec)] || {};
var arr;
if (st.single) {
arr = sectionRoot[st.key] ? [sectionRoot[st.key]] : [];
} else {
arr = sectionRoot[st.key] || [];
}
var isDrops = sec.id === 'gather' && st.key === 'drops';
var h = '
';
h += '
' + esc(st.label) + '
';
arr.forEach(function(item, idx) {
h += '
';
if (isDrops) {
var isItemDrop = item._type !== undefined ? item._type !== 't' : !item.table;
var showField = isItemDrop ? 'item_id' : 'table';
var hideField = isItemDrop ? 'table' : 'item_id';
h += '
';
h += '';
st.fields.forEach(function(f) {
if (f[0] === 'message' || f[0] === hideField) return;
var fid = fieldIDSub(sec, st.key, idx, f[0]);
var val = item[f[0]];
val = val === undefined || val === null ? '' : val;
var extraCls = '';
if (f[4] === 'narrow') extraCls = ' obj-narrow';
if (f[2] === 'search') {
var entity = f[5] || 'items';
var ph = f[3] ? ' placeholder="' + escAttr(f[3]) + '"' : ' placeholder="Search ' + entity + '..."';
h += '';
} else if (f[2] === 'checkbox') {
h += '';
} else {
h += '';
}
});
if (!st.single) {
h += '';
}
h += '
';
h += '
';
var fidMsg = fieldIDSub(sec, st.key, idx, 'message');
var msgVal = item.message !== undefined && item.message !== null ? item.message : '';
var msgField = st.fields.find(function(f){return f[0]==='message';});
h += '';
h += '
';
} else {
st.fields.forEach(function(f) {
var fid = fieldIDSub(sec, st.key, idx, f[0]);
var val = item[f[0]];
val = val === undefined || val === null ? '' : val;
var extraCls = '';
if (f[4] === 'narrow') extraCls = ' obj-narrow';
else if (f[4] === 'grow') extraCls = ' obj-grow';
if (f[2] === 'search') {
var entity = f[5] || 'items';
var ph = f[3] ? ' placeholder="' + escAttr(f[3]) + '"' : ' placeholder="Search ' + entity + '..."';
h += '';
} else if (f[2] === 'checkbox') {
h += '';
} else {
h += '';
}
});
if (!st.single) {
h += '';
}
}
h += '
';
});
if (isDrops) {
h += '
';
h += '';
h += '';
h += '
';
} else if (!st.single || arr.length === 0) {
h += '';
}
h += '
';
return h;
}
function renderTags(sec, il, data) {
var sectionRoot = objectData[cardPath(sec)] || {};
var arr = sectionRoot[il.key] || [];
var str = Array.isArray(arr) ? arr.join(', ') : String(arr || '');
var fid = fieldID(sec, il.key);
return '';
}
function renderKV(sec, il, data) {
var sectionRoot = objectData[cardPath(sec)] || {};
var map = sectionRoot[il.key] || {};
var h = '
';
h += '' + esc(il.label) + '';
h += '
';
Object.keys(map).forEach(function(k, idx) {
h += '
';
h += '';
h += '
';
});
h += '
';
h += '';
h += '
';
return h;
}
function toggleCard(id) {
expandedCards[id] = expandedCards[id] === false ? true : false;
renderCurrent();
}
function removeSection(id) {
var sec = SECTIONS.find(function(s) { return s.id === id; });
if (!sec) return;
if (sec.path) {
delete objectData[sec.path];
} else if (id === 'steal') {
delete objectData.steal_table;
delete objectData.steal_level;
delete objectData.steal_xp;
delete objectData.steal_speed;
delete objectData.guard_mob;
}
renderCurrent();
}
function addSection() {
var sel = $('#addSectionSelect');
var id = sel.value;
if (!id) return;
var sec = SECTIONS.find(function(s) { return s.id === id; });
if (!sec) return;
if (sec.path) {
if (sec.isArray) {
objectData[sec.path] = [];
} else if (sec.id === 'on_look') {
objectData[sec.path] = {};
} else if (sec.id === 'gather') {
objectData[sec.path] = {skill:'', tools:[], success:{base:0, per_level:0, cap:1}, drops:[], gather_message:'', fail_message:'', respawn_timer:0, respawn_broadcast:'', deplete_timer:0, nest_chance:0};
} else if (sec.id === 'use') {
objectData[sec.path] = {message:'', wait:1, consume:{}, reward:{}, fail_message:'', success:{base:0, per_level:0, cap:1}, skill:'', level:1, xp:0};
} else if (sec.id === 'safespot') {
objectData[sec.path] = {tier:1, max_block_size:'', max_occupants:1, unsafe_chance:0, decay_ticks:0, decay_chance:0, respawn_on_hide:false, respawn_ticks:0, levels:[]};
} else if (sec.id === 'talk') {
objectData[sec.path] = {nodes:{start:{message:'',options:[]}}};
} else {
objectData[sec.path] = {};
}
}
expandedCards[id] = true;
renderCurrent();
}
function addArrayItem(cardID) {
var sec = SECTIONS.find(function(s) { return s.id === cardID; });
if (!sec || !sec.isArray) return;
var arr = objectData[sec.path] || [];
var empty = {};
sec.fields.forEach(function(f) {
empty[f[0]] = f[2] === 'checkbox' ? false : (f[2] === 'number' ? 0 : '');
});
arr.push(empty);
objectData[sec.path] = arr;
renderCurrent();
}
function removeArrayItem(cardID, idx) {
var sec = SECTIONS.find(function(s) { return s.id === cardID; });
if (!sec || !sec.isArray) return;
var arr = objectData[sec.path] || [];
arr.splice(idx, 1);
renderCurrent();
}
function addSubRow(cardID, subKey) {
var sec = SECTIONS.find(function(s) { return s.id === cardID; });
if (!sec) return;
var sectionRoot = objectData[sec.path || ''] || objectData;
var arr = sectionRoot[subKey] || [];
var empty = {};
var st = (sec.subtables || []).find(function(s) { return s.key === subKey; });
if (st) {
st.fields.forEach(function(f) {
empty[f[0]] = f[2] === 'checkbox' ? false : (f[2] === 'number' ? 0 : '');
});
}
arr.push(empty);
sectionRoot[subKey] = arr;
renderCurrent();
}
function removeSubRow(cardID, subKey, idx) {
var sec = SECTIONS.find(function(s) { return s.id === cardID; });
if (!sec) return;
var sectionRoot = objectData[sec.path || ''] || objectData;
var arr = sectionRoot[subKey] || [];
arr.splice(idx, 1);
renderCurrent();
}
function addKVRow(cardID, subKey) {
var el = document.querySelector('.obj-kv-list[data-sec="' + cardID + '"][data-key="' + subKey + '"]');
if (!el) return;
var row = document.createElement('div');
row.className = 'obj-kv-row';
row.innerHTML = '';
el.appendChild(row);
}
function addDropRow(cardID, subKey, dropType) {
var sec = SECTIONS.find(function(s) { return s.id === cardID; });
if (!sec) return;
var sectionRoot = objectData[sec.path || ''] || objectData;
var arr = sectionRoot[subKey] || [];
var empty = { item_id: '', table: '', weight: 0, level: 0, xp: 0, quantity: 0, depletes: false, message: '', _type: (dropType === 'table' ? 't' : 'i') };
arr.push(empty);
sectionRoot[subKey] = arr;
renderCurrent();
}
function validateDropRow(idx) {
// No longer needed — drops are either item or table type, never both.
}
function renderCurrent() {
if (!objectData || !objectID) return;
renderObjectEditor(objectID, objectData);
}
function toggleGatherConditionals() {
var skillEl = document.getElementById('f_gather_skill');
var skill = skillEl ? skillEl.value : '';
var fishing = skill === 'fishing';
var wood = skill === 'woodcutting';
var els = ['bait', 'exhausted_message', 'deplete_timer', 'nest_chance'];
var vis = {bait: fishing, exhausted_message: wood, deplete_timer: wood, nest_chance: wood};
els.forEach(function(key) {
var el = document.getElementById('f_gather_' + key);
if (el) el.closest('.obj-field').style.display = vis[key] ? '' : 'none';
});
}
function setupSearchFields() {
document.querySelectorAll('.search-input').forEach(function(input) {
if (input._searchSetup) return;
input._searchSetup = true;
var entityType = input.getAttribute('data-search-type') || 'items';
var results = input.parentElement.querySelector('.search-results');
if (!results) return;
results._targetInput = input;
input.addEventListener('input', function() {
var val = input.value.trim();
if (!val) { results.style.display = 'none'; results.innerHTML = ''; return; }
API.get('/api/search?q=' + encodeURIComponent(val)).then(function(data) {
var items = [];
if (entityType === 'items' && data.items) items = data.items;
if (entityType === 'objects' && data.objects) items = data.objects;
if (entityType === 'mobs' && data.mobs) items = data.mobs;
if (entityType === 'drops' && data.drops) items = data.drops;
if (items.length === 0) {
results.style.display = 'block';
results.innerHTML = '