';
$('#editorMain').innerHTML = html;
setupSearchFields();
setupCraftTypeChange();
setTimeout(function() {
document.querySelectorAll('.color-field').forEach(function(el) { bindColorField(el); });
}, 50);
}
function renderCurrent() {
if (!itemData || !itemID) return;
renderItemEditor(itemID, itemData);
}
// =========================================================================
// Card rendering
// =========================================================================
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 if (sec.id === 'equipment') {
h += renderEquipmentCard(data);
} else {
h += '
';
if (sec.fields) sec.fields.forEach(function(f) {
h += renderField(sec, f, data, -1);
});
h += '
';
if (sec.inline) {
sec.inline.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);
});
}
}
h += '
';
}
h += '
';
return h;
}
function renderCoreCard(data) {
var sec = SECTIONS[0];
var h = '
';
h += renderField(sec, sec.fields[0], data, -1, null, 'flex:1');
h += renderField(sec, sec.fields[1], data, -1);
h += renderField(sec, sec.fields[6], data, -1, null, 'flex:1');
h += '
';
h += '
';
h += renderField(sec, sec.fields[2], data, -1);
h += '
';
h += renderField(sec, sec.fields[3], data, -1);
h += '
';
h += '
';
h += renderField(sec, sec.fields[4], data, -1);
h += renderField(sec, sec.fields[5], data, -1);
h += '
';
h += '
';
var descFid = fieldID(sec, 'description', -1);
var descVal = getVal(itemData, sec, 'description');
var dstr = typeof descVal === 'object' ? (Array.isArray(descVal) ? descVal.map(function(v){return v.text||'';}).join('\n') : JSON.stringify(descVal)) : String(descVal);
h += '';
return h;
}
// =========================================================================
// Equipment card (custom render with 3 stat blocks)
// =========================================================================
var EQUIP_STAT_FIELDS = {
attack: [
['stab', 'Stab', 'number', '', 'narrow'],
['slash', 'Slash', 'number', '', 'narrow'],
['crush', 'Crush', 'number', '', 'narrow'],
['ranged', 'Ranged', 'number', '', 'narrow'],
['science', 'Science', 'number', '', 'narrow'],
],
defense: [
['stab', 'Stab', 'number', '', 'narrow'],
['slash', 'Slash', 'number', '', 'narrow'],
['crush', 'Crush', 'number', '', 'narrow'],
['ranged', 'Ranged', 'number', '', 'narrow'],
['science', 'Science', 'number', '', 'narrow'],
],
other: [
['strength', 'Strength', 'number', '', 'narrow'],
['ranged', 'Ranged', 'number', '', 'narrow'],
['science', 'Science', 'number', '', 'narrow'],
['technology', 'Tech', 'number', '', 'narrow'],
],
};
function equipFieldID(key) {
return 'f_equipment_' + key;
}
function renderEquipField(key, label, type, hint, val, wide, searchEntity) {
var fid = equipFieldID(key);
val = val === undefined || val === null ? '' : val;
function wrap(cls, inner) {
var w = (wide === 'wide') ? ' obj-wide' : '';
if (wide === 'narrow') w = ' obj-narrow';
return '';
}
if (type === 'select') {
var opts = hint ? hint.split('|') : [];
var inner = '' + esc(label) + '';
return wrap('obj-field', inner);
}
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 renderEquipmentCard(data) {
var eq = data.equipment || {};
var h = '';
var isAmmo = eq.type === 'ammo';
// Row 1: type, slot
h += '
';
h += renderEquipField('type', 'Type', 'select', 'melee_weapon|ranged_weapon|tech_weapon|armor|tool|ammo', eq.type);
if (!isAmmo) {
h += renderEquipField('slot', 'Slot', 'select', 'head|neck|torso|legs|hands|feet|back|ammo|main_hand|off_hand|ring', eq.slot);
} else {
h += '';
}
h += '
';
// Ammo: recoverable checkbox
if (isAmmo) {
h += '';
}
// Row 2: attack_type, speed, junk
var isMelee = eq.type === 'melee_weapon';
var isWeapon = ['melee_weapon', 'ranged_weapon', 'tech_weapon'].indexOf(eq.type) >= 0;
h += '
';
if (isMelee) {
h += renderEquipField('attack_type', 'Attack Type', 'select', 'stab|slash|crush', eq.attack_type);
}
if (isWeapon) {
h += renderEquipField('speed', 'Speed', 'number', '', eq.speed, 'narrow');
}
h += renderEquipField('junk', 'Junk', 'search', '', eq.junk, '', 'items');
h += '
';
// 3 buttons row
var btns = [];
if (!eq.attack) btns.push('');
if (!eq.defense) btns.push('');
if (!eq.other) btns.push('');
if (btns.length > 0) {
h += '
' + btns.join('') + '
';
}
// Stat blocks
if (eq.attack) h += renderEquipStatBlock('Attack Stats', 'attack', eq.attack, EQUIP_STAT_FIELDS.attack);
if (eq.defense) h += renderEquipStatBlock('Defense Stats', 'defense', eq.defense, EQUIP_STAT_FIELDS.defense);
if (eq.other) h += renderEquipStatBlock('Other Stats', 'other', eq.other, EQUIP_STAT_FIELDS.other);
// Requirements KV (at root level, stored in itemData.requirements)
h += '
';
h += renderKV({id: 'equipment_req'}, {label:'Requirements', key:'requirements', type:'kv', valType:'number'}, data);
h += '
';
return h;
}
function renderEquipStatBlock(label, blockKey, blockData, fields) {
var h = '
';
h += '
';
h += '' + esc(label) + '';
h += '';
h += '
';
h += '
';
fields.forEach(function(f) {
var fid = 'f_equipment_' + blockKey + '_' + f[0];
var val = blockData[f[0]];
val = val === undefined || val === null ? '' : val;
var extraCls = f[4] === 'narrow' ? ' obj-narrow' : '';
h += '';
});
h += '
';
h += '
';
return h;
}
function addEquipBlock(blockKey) {
if (!itemData.equipment) itemData.equipment = {};
var empty = {};
EQUIP_STAT_FIELDS[blockKey].forEach(function(f) { empty[f[0]] = 0; });
itemData.equipment[blockKey] = empty;
renderCurrent();
}
function removeEquipBlock(blockKey) {
if (itemData.equipment) delete itemData.equipment[blockKey];
renderCurrent();
}
// =========================================================================
// Generic field render (unchanged)
// =========================================================================
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 = itemData[cardPath(sec)] || [];
var item = arr[idx] || {};
val = item[key];
} else if (subPath) {
var sectionRoot = itemData[cardPath(sec)] || {};
var subObj = sectionRoot[subPath] || {};
val = subObj[key];
} else {
val = getVal(itemData, sec, key);
}
val = val === undefined || val === null ? '' : val;
var hiddenStyle = '';
if (showIf) {
var sectionData = itemData[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) + '');
}
// =========================================================================
// Array section rendering
// =========================================================================
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;
}
// =========================================================================
// Subtable rendering
// =========================================================================
function renderSubFieldHTML(sec, stKey, idx, f, val) {
var fid = fieldIDSub(sec, stKey, idx, f[0]);
val = val === undefined || val === null ? '' : val;
var extraCls = '';
var extraStyle = '';
if (f[4] === 'narrow') extraCls = ' obj-narrow';
else if (f[4] === 'grow') extraCls = ' obj-grow';
if (f[4] === 'wide') extraStyle = ' style="flex:1 1 100%"';
if (f[2] === 'search') {
var entity = f[6] || 'items';
var ph = f[3] ? ' placeholder="' + escAttr(f[3]) + '"' : ' placeholder="Search ' + entity + '..."';
return '';
}
if (f[2] === 'checkbox') {
return '';
}
if (f[2] === 'select') {
var opts = f[3] ? f[3].split('|') : [];
var sel = '';
return sel;
}
return '';
}
function renderSubtable(sec, st, data) {
var p = cardPath(sec);
var sectionRoot = p ? (itemData[p] || {}) : itemData;
var raw = sectionRoot[st.key];
var arr;
if (st.single) {
arr = raw ? [raw] : [];
} else if (Array.isArray(raw)) {
arr = raw;
} else if (raw && typeof raw === 'object') {
arr = [raw];
} else {
arr = [];
}
var hasSubSubtables = st.subSubtables && st.subSubtables.length > 0;
var h = '
';
h += '
' + esc(st.label) + '
';
arr.forEach(function(item, idx) {
h += '
';
function renderGroup(fieldKeys) {
fieldKeys.forEach(function(fieldKey) {
var f = st.fields.find(function(sf) { return sf[0] === fieldKey; });
if (!f) return;
if (f[5] && !f[5](item)) return;
var val = item[f[0]];
h += renderSubFieldHTML(sec, st.key, idx, f, val);
});
}
if (st.fieldGroups) {
st.fieldGroups.forEach(function(group) {
if (group.label) {
if (group.label === 'Success') {
var hasSuccess = !!(item.fail || item.success_base || item.success_per_level || item.success_cap);
var gid = 'sg_' + sec.id + '_' + st.key + '_' + idx;
h += '
';
h += '
';
h += 'Success';
h += '';
h += '
';
h += '
';
renderGroup(group.fields);
h += '
';
h += '
';
h += '';
} else {
h += '
';
h += '' + esc(group.label) + '';
h += '
';
renderGroup(group.fields);
h += '
';
h += '
';
}
} else {
var rightStyle = (group && group.right) ? ' style="justify-content:flex-end"' : '';
h += '
';
var keys = Array.isArray(group) ? group : group.fields;
renderGroup(keys);
h += '
';
}
});
} else {
h += '
';
st.fields.forEach(function(f) {
if (f[5] && !f[5](item)) return;
var val = item[f[0]];
h += renderSubFieldHTML(sec, st.key, idx, f, val);
});
h += '
';
}
if (hasSubSubtables) {
st.subSubtables.forEach(function(sst) {
if (sst.key === 'steps') {
var stepsArr = item[sst.key];
if (!stepsArr || !Array.isArray(stepsArr) || stepsArr.length === 0) {
h += '';
return;
}
}
h += renderSubSubtable(sec, st, idx, sst, item);
});
}
if (!st.single) {
h += '';
}
h += '
';
});
if (!st.single || arr.length === 0) {
h += '';
}
h += '
';
h += '';
} else {
arr.forEach(function(subItem, subIdx) {
h += '
';
h += '
';
sst.fields.forEach(function(f) {
var fid = fieldIDSubSub(sec, st.key, rowIdx, sst.key, subIdx, f[0]);
var val = subItem[f[0]];
val = val === undefined || val === null ? '' : val;
var extraCls = '';
if (f[4] === 'narrow') extraCls = ' obj-narrow';
if (f[2] === 'search') {
var entity = f[6] || 'items';
var ph = f[3] ? ' placeholder="' + escAttr(f[3]) + '"' : ' placeholder="Search ' + entity + '..."';
h += '';
} else if (f[2] === 'checkbox') {
h += '';
} else {
h += '';
}
});
h += '
';
h += '';
h += '
';
});
h += '';
}
h += '
';
return h;
}
// =========================================================================
// Tags / KV rendering
// =========================================================================
function renderTags(sec, il, data) {
var p = cardPath(sec);
var sectionRoot = p ? (itemData[p] || {}) : itemData;
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 p = cardPath(sec);
var sectionRoot = p ? (itemData[p] || {}) : itemData;
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;
}
// =========================================================================
// Card toggle / section add-remove
// =========================================================================
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 itemData[sec.path];
} else {
if (sec.fields) sec.fields.forEach(function(f) { delete itemData[f[0]]; });
if (sec.inline) sec.inline.forEach(function(il) { if (il.key) delete itemData[il.key]; });
if (sec.subtables) sec.subtables.forEach(function(st) { delete itemData[st.key]; });
}
delete addedSections[id];
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.id === 'equipment') {
itemData.equipment = {type:'', slot:'', attack_type:'', speed:0, junk:''};
} else {
itemData[sec.path] = {};
}
}
addedSections[id] = true;
expandedCards[id] = true;
renderCurrent();
}
// =========================================================================
// Array item add/remove
// =========================================================================
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);
itemData[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 = itemData[sec.path] || [];
arr.splice(idx, 1);
renderCurrent();
}
// =========================================================================
// Subtable row add/remove
// =========================================================================
function addSubRow(cardID, subKey) {
var sec = SECTIONS.find(function(s) { return s.id === cardID; });
if (!sec) return;
var p = cardPath(sec);
var sectionRoot = p ? (itemData[p] || {}) : itemData;
var raw = sectionRoot[subKey];
var arr;
if (Array.isArray(raw)) {
arr = raw;
} else if (raw && typeof raw === 'object') {
arr = [raw];
} else {
arr = [];
}
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;
if (sectionRoot === itemData) { delete itemData._raw; delete itemData._path; }
renderCurrent();
}
function removeSubRow(cardID, subKey, idx) {
var sec = SECTIONS.find(function(s) { return s.id === cardID; });
if (!sec) return;
var p = cardPath(sec);
var sectionRoot = p ? (itemData[p] || {}) : itemData;
var raw = sectionRoot[subKey];
var arr;
if (Array.isArray(raw)) {
arr = raw;
} else if (raw && typeof raw === 'object') {
arr = [raw];
} else {
arr = [];
}
arr.splice(idx, 1);
sectionRoot[subKey] = arr;
renderCurrent();
}
// =========================================================================
// Sub-subtable row add/remove
// =========================================================================
function addSubSubRow(cardID, subKey, rowIdx, subSubKey) {
try {
var sec = SECTIONS.find(function(s) { return s.id === cardID; });
if (!sec) { console.warn('addSubSubRow: section not found', cardID); return; }
var p = cardPath(sec);
var sectionRoot = p ? (itemData[p] || {}) : itemData;
var arr = sectionRoot[subKey] || [];
if (rowIdx >= arr.length) { console.warn('addSubSubRow: row index out of range', rowIdx, arr.length); return; }
if (!arr[rowIdx]) { arr[rowIdx] = {}; }
var subArr = arr[rowIdx][subSubKey] || [];
var empty = {};
var st = (sec.subtables || []).find(function(s) { return s.key === subKey; });
if (st && st.subSubtables) {
var sst = st.subSubtables.find(function(ss) { return ss.key === subSubKey; });
if (sst) {
sst.fields.forEach(function(f) {
empty[f[0]] = f[2] === 'checkbox' ? false : (f[2] === 'number' ? 0 : '');
});
}
}
subArr.push(empty);
arr[rowIdx][subSubKey] = subArr;
sectionRoot[subKey] = arr;
renderCurrent();
} catch(e) {
console.error('addSubSubRow error:', e);
notify('Failed to add row: ' + e.message, 'error');
}
}
function removeSubSubRow(cardID, subKey, rowIdx, subSubKey, subSubIdx) {
var sec = SECTIONS.find(function(s) { return s.id === cardID; });
if (!sec) return;
var p = cardPath(sec);
var sectionRoot = p ? (itemData[p] || {}) : itemData;
var arr = sectionRoot[subKey] || [];
if (rowIdx >= arr.length) return;
var subArr = arr[rowIdx][subSubKey] || [];
subArr.splice(subSubIdx, 1);
renderCurrent();
}
// =========================================================================
// KV row add
// =========================================================================
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);
}
// =========================================================================
// Search autocomplete
// =========================================================================
function setupCraftTypeChange() {
document.querySelectorAll('.obj-subtable select[id$="_type"]').forEach(function(typeSelect) {
var id = typeSelect.id;
var subtypeSel = document.getElementById(id.replace(/_type$/, '_subtype'));
var levelField = document.getElementById(id.replace(/_type$/, '_level'));
var xpField = document.getElementById(id.replace(/_type$/, '_xp'));
var stationField = document.getElementById(id.replace(/_type$/, '_station'));
function update() {
var t = typeSelect.value;
[levelField, xpField].forEach(function(el) {
if (!el) return;
var lbl = el.closest('.obj-field');
if (lbl) lbl.style.display = t === 'combine' ? 'none' : '';
});
if (stationField) {
var sLbl = stationField.closest('.obj-field');
if (sLbl) sLbl.style.display = (t === 'fletching' || t === 'combine' || t === 'pharmacy') ? 'none' : '';
}
if (subtypeSel) {
var sLbl = subtypeSel.closest('.obj-field');
if (t === 'smithing') {
if (sLbl) sLbl.style.display = '';
subtypeSel.innerHTML = '';
} else if (t === 'pharmacy') {
if (sLbl) sLbl.style.display = '';
subtypeSel.innerHTML = '';
} else {
if (sLbl) sLbl.style.display = 'none';
subtypeSel.value = '';
}
}
}
typeSelect.addEventListener('change', update);
update();
});
}
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 = '