';
html += '';
html += '';
html += '';
html += '';
html += '
';
$('#editorMain').innerHTML = html;
setupSearchFields();
setupCraftTypeChange();
updateOutputQtyVisibility();
setTimeout(function() {
document.querySelectorAll('.color-field').forEach(function(el) { bindColorField(el); });
}, 50);
}
function renderCurrent() {
ced_syncDOMToData(itemData);
if (!itemData || !itemID) return;
renderItemEditor(itemID, itemData);
}
// =========================================================================
// Card rendering
// =========================================================================
function renderCard(sec, data) {
var collapsed = expandedCards[sec.id] === false;
var isFull = cardWidths[sec.id] !== undefined ? cardWidths[sec.id] : (sec.fullWidth || false);
var fullClass = isFull ? ' obj-card-full' : '';
var h = '
';
h += '
';
h += '' + (collapsed ? '▶' : '▼') + '';
h += '' + sec.label + '';
h += '';
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 if (sec.id === 'farming') {
h += renderFarmingCard(data);
} else if (sec.id === 'potions') {
h += renderPotionsCard(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[5], 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 += '
';
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;
}
function renderFarmingCard(data) {
var sec = SECTIONS.find(function(s) { return s.id === 'farming'; });
var byKey = {};
sec.fields.forEach(function(f) { byKey[f[0]] = f; });
var h = '
';
h += renderField(sec, byKey['farm_patch_type'], data, -1);
h += renderField(sec, byKey['farm_product'], data, -1, null, 'flex:1');
h += '
';
h += '
';
h += renderField(sec, byKey['farm_plant_xp'], data, -1);
h += renderField(sec, byKey['farm_harvest_xp'], data, -1);
h += renderField(sec, byKey['farm_level'], data, -1);
h += '
';
h += '
';
h += renderField(sec, byKey['farm_stages'], data, -1);
h += renderField(sec, byKey['farm_min_yield'], data, -1);
h += renderField(sec, byKey['farm_max_yield'], data, -1);
h += '
';
return h;
}
function renderPotionsCard(data) {
var sec = SECTIONS.find(function(s) { return s.id === 'potions'; });
var byKey = {};
sec.fields.forEach(function(f) { byKey[f[0]] = f; });
var h = '
';
h += renderField(sec, byKey['potion_effect'], data, -1, null, 'flex:1');
h += '
';
h += '
';
h += renderField(sec, byKey['potion_bonus'], data, -1);
h += renderField(sec, byKey['potion_duration'], data, -1);
h += '
');
}
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) {
ced_syncDOMToData(itemData);
if (itemData.equipment) delete itemData.equipment[blockKey];
renderItemEditor(itemID, itemData);
}
// =========================================================================
// 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 p = cardPath(sec);
var sectionData = p ? (itemData[p] || {}) : itemData;
if (!showIf(sectionData)) hiddenStyle = ' style="display:none"';
}
var pfx = cardPath(sec);
var dp;
if (idx >= 0) {
dp = pfx + '[' + idx + '].' + key;
} else if (subPath) {
dp = (pfx ? pfx + '.' : '') + subPath + '.' + key;
} else {
dp = (pfx ? pfx + '.' : '') + key;
}
var dpAttr = ' data-field-path="' + escAttr(dp) + '"';
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%"';
var defaultPh = '';
if (f[3]) defaultPh = ' placeholder="' + escAttr(f[3]) + '"';
var dp = cardPath(sec) + '.' + stKey + '[' + idx + '].' + f[0];
var dpAttr = ' data-field-path="' + escAttr(dp) + '"';
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) {
if (fieldKey === '|') {
h += '|';
return;
}
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]];
if (val === undefined && item.success && (f[0] === 'success_base' || f[0] === 'success_per_level' || f[0] === 'success_cap')) {
var nestedKey = f[0] === 'success_base' ? 'base' : (f[0] === 'success_per_level' ? 'per_level' : 'cap');
val = item.success[nestedKey];
}
h += renderSubFieldHTML(sec, st.key, idx, f, val);
});
}
if (st.fieldGroups) {
st.fieldGroups.forEach(function(group) {
if (group.note) {
h += '
' + esc(group.note) + '
';
} else if (group.label) {
if (group.label === 'Success') {
var hasSuccess = !!(item.success_base || item.success_per_level || item.success_cap || (item.success && (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 toggleCardWidth(id) {
cardWidths[id] = !cardWidths[id];
renderCurrent();
}
function removeSection(id) {
var sec = SECTIONS.find(function(s) { return s.id === id; });
if (!sec) return;
ced_syncDOMToData(itemData);
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];
renderItemEditor(itemID, itemData);
}
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;
ced_syncDOMToData(itemData);
var arr = itemData[sec.path] || [];
arr.splice(idx, 1);
renderItemEditor(itemID, itemData);
}
// =========================================================================
// 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;
ced_syncDOMToData(itemData);
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;
renderItemEditor(itemID, itemData);
}
// =========================================================================
// 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;
ced_syncDOMToData(itemData);
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);
renderItemEditor(itemID, itemData);
}
// =========================================================================
// 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'));
var combineHideFields = ['success_message', 'fail_message', 'tool', 'ticks_per_cycle',
'success_base', 'success_per_level', 'success_cap', 'fail'];
var recipeRow = typeSelect.closest('.obj-sub-row');
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' : '';
}
combineHideFields.forEach(function(fKey) {
var el = document.getElementById(id.replace(/_type$/, '_' + fKey));
if (!el) return;
var lbl = el.closest('.obj-field');
if (lbl) lbl.style.display = t === 'combine' ? 'none' : '';
});
var sepEl = recipeRow.querySelector('.craft-sep');
if (sepEl) sepEl.style.display = t === 'combine' ? 'none' : '';
var successGroup = document.querySelector('.success-group');
if (successGroup) successGroup.style.display = t === 'combine' ? 'none' : '';
var successBtn = document.getElementById(successGroup ? successGroup.id + '_btn' : '');
if (successBtn) successBtn.style.display = t === 'combine' ? 'none' : '';
var stepsSection = recipeRow.querySelector('[data-section="steps-section"]');
var stepsAddBtn = recipeRow.querySelector('[data-section^="steps-add-"]');
var showSteps = t === 'combine';
if (stepsSection) stepsSection.style.display = showSteps ? '' : 'none';
if (stepsAddBtn) stepsAddBtn.style.display = showSteps ? '' : '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 updateOutputQtyVisibility() {
var isStackable = false;
var stackableEl = document.getElementById('f_core_stackable');
if (stackableEl) {
isStackable = stackableEl.checked;
if (!stackableEl._hasOutputQtyListener) {
stackableEl._hasOutputQtyListener = true;
stackableEl.addEventListener('change', updateOutputQtyVisibility);
}
}
document.querySelectorAll('[id$="_output_qty"]').forEach(function(el) {
var lbl = el.closest('.obj-field');
if (lbl) lbl.style.display = isStackable ? '' : 'none';
});
document.querySelectorAll('.output-qty-note').forEach(function(el) {
el.style.display = isStackable ? '' : 'none';
});
}
function setupSearchFields() { ced_setupSearchFields(); }
function selectSearchResult(el) { ced_selectSearchResult(el); }
function updateSearchHighlight(results) { ced_updateSearchHighlight(results); }
// =========================================================================
// Validation
// =========================================================================
function validateFields() {
var errors = [];
document.querySelectorAll('.obj-field-error').forEach(function(field) {
field.classList.remove('obj-field-error');
});
document.querySelectorAll('input[id]:not([type="checkbox"])').forEach(function(input) {
var id = input.id;
if (!id) return;
var labelEl = input.closest('.obj-field');
if (!labelEl) return;
if (input.classList.contains('search-input') || input.classList.contains('color-input')) return;
var isNumber = false;
var labelText = '';
var fieldDiv = labelEl.closest('[data-card]');
var cardID = fieldDiv ? fieldDiv.getAttribute('data-card') : '';
var sec = SECTIONS.find(function(s) { return s.id === cardID; });
if (!sec) return;
var baseMatch = id.match(/^f_(\w+)_(\w+)$/);
var subMatch = id.match(/^f_(\w+)_(\w+)_(\d+)_(\w.+)$/);
var subSubMatch = id.match(/^f_(\w+)_(\w+)_(\d+)__(\w+)_(\d+)_(\w+)$/);
var equipStatMatch = id.match(/^f_equipment_(\w+)_(\w+)$/);
if (subSubMatch) {
var ssSecId = subSubMatch[1], ssSubKey = subSubMatch[2];
var ssSubSubKey = subSubMatch[4], ssFieldKey = subSubMatch[6];
var st = (sec.subtables || []).find(function(s) { return s.key === ssSubKey; });
if (st && st.subSubtables) {
var sst = st.subSubtables.find(function(ss) { return ss.key === ssSubSubKey; });
if (sst) {
var nf = sst.fields.find(function(f) { return f[0] === ssFieldKey && f[2] === 'number'; });
if (nf) { isNumber = true; labelText = nf[1]; }
}
}
} else if (subMatch) {
var subKey = subMatch[2], fieldKey = subMatch[4];
var st = (sec.subtables || []).find(function(s) { return s.key === subKey; });
if (st) {
var nf = st.fields.find(function(f) { return f[0] === fieldKey && f[2] === 'number'; });
if (nf) { isNumber = true; labelText = nf[1]; }
}
} else if (equipStatMatch) {
var blockKey = equipStatMatch[1], statField = equipStatMatch[2];
var fields = EQUIP_STAT_FIELDS[blockKey];
if (fields) {
var nf = fields.find(function(f) { return f[0] === statField; });
if (nf) { isNumber = true; labelText = nf[1]; }
}
} else if (baseMatch) {
var fieldKey = baseMatch[2];
var nf = sec.fields ? sec.fields.find(function(f) {
var fid = fieldID(sec, f[0], -1);
return fid === id && f[2] === 'number';
}) : null;
if (!nf && sec.inline) {
sec.inline.forEach(function(il) {
if (il.fields) {
var inf = il.fields.find(function(f) {
var fid = fieldID(sec, il.path + '.' + f[0], -1);
return fid === id && f[2] === 'number';
});
if (inf) nf = inf;
}
});
}
if (nf) { isNumber = true; labelText = nf[1]; }
}
if (isNumber) {
var rawVal = input.value.trim();
if (rawVal !== '' && !/^-?\d*\.?\d*$/.test(rawVal)) {
labelEl.classList.add('obj-field-error');
input.focus();
input.select();
labelText = labelText || (labelEl.querySelector('span') || {}).textContent || '';
errors.push('Invalid value in "' + esc(labelText) + '": "' + esc(rawVal) + '"');
}
}
});
if (errors.length > 0) {
notify(errors.join('; '), 'error');
return false;
}
return true;
}
// =========================================================================
// Field value collector
// =========================================================================
function collectFieldValue(el, type) { return ced_collectFieldValue(el, type); }
// =========================================================================
// Save
// =========================================================================
function saveItem() {
if (!validateFields()) return;
var out = {};
SECTIONS.forEach(function(sec) {
if (sec.isArray) {
var arr = [];
var card = document.querySelector('.obj-card[data-card="' + sec.id + '"]');
if (!card) return;
var rows = card.querySelectorAll('.obj-sub-row');
rows.forEach(function(row, idx) {
var item = {};
sec.fields.forEach(function(f) {
var fid = fieldID(sec, f[0], idx);
var el = document.getElementById(fid);
if (el) item[f[0]] = collectFieldValue(el, f[2]);
});
if (Object.keys(item).length > 0) arr.push(item);
});
if (arr.length > 0) out[sec.path] = arr;
return;
}
if (sec.id === 'equipment') {
var eq = {};
var hasEq = false;
['type', 'slot', 'attack_type', 'junk'].forEach(function(k) {
var el = document.getElementById('f_equipment_' + k);
if (!el) return;
var val = collectFieldValue(el, k === 'type' || k === 'slot' ? 'select' : (k === 'junk' ? 'search' : 'text'));
if (val !== '' && val !== false && val !== 0 && val !== null) hasEq = true;
eq[k] = val;
});
if (eq.type === 'ammo') eq.slot = 'ammo';
var speedEl = document.getElementById('f_equipment_speed');
if (speedEl) {
var spd = parseFloat(speedEl.value);
if (!isNaN(spd) && spd !== 0) { eq.speed = spd; hasEq = true; }
}
EQUIP_STAT_FIELDS.attack.forEach(function(f) { saveEquipBlock(eq, 'attack', f[0]); });
EQUIP_STAT_FIELDS.defense.forEach(function(f) { saveEquipBlock(eq, 'defense', f[0]); });
EQUIP_STAT_FIELDS.other.forEach(function(f) { saveEquipBlock(eq, 'other', f[0]); });
if (hasEq && Object.keys(eq).length > 0) out.equipment = eq;
var recovEl = document.getElementById('f_core_recoverable');
if (recovEl) out.recoverable = recovEl.checked;
var kvEl = document.querySelector('.obj-kv-list[data-sec="equipment_req"][data-key="requirements"]');
if (kvEl) {
var map = {};
kvEl.querySelectorAll('.obj-kv-row').forEach(function(row) {
var keyEl = row.querySelector('.obj-kv-key');
var valEl = row.querySelector('.obj-kv-val');
if (keyEl && keyEl.value.trim() && valEl) {
map[keyEl.value.trim()] = parseFloat(valEl.value) || 0;
}
});
if (Object.keys(map).length > 0) out.requirements = map;
}
return;
}
var sectionObj = {};
var hasValues = sec.always;
if (sec.fields) sec.fields.forEach(function(f) {
var fid = fieldID(sec, f[0]);
var el = document.getElementById(fid);
if (!el) return;
var val = collectFieldValue(el, f[2]);
if (val !== '' && val !== false && val !== 0 && val !== null) hasValues = true;
sectionObj[f[0]] = val;
});
if (sec.subtables) {
sec.subtables.forEach(function(st) {
var items = [];
for (var i = 0; ; i++) {
var item = {};
var found = false;
st.fields.forEach(function(f) {
var fid = fieldIDSub(sec, st.key, i, f[0]);
var el = document.getElementById(fid);
if (el) found = true;
if (el) item[f[0]] = collectFieldValue(el, f[2]);
});
if (!found) break;
if (item.success_base !== undefined || item.success_per_level !== undefined || item.success_cap !== undefined) {
var sBase = Number(item.success_base) || 0;
var sPer = Number(item.success_per_level) || 0;
var sCap = Number(item.success_cap) || 0;
delete item.success_base;
delete item.success_per_level;
delete item.success_cap;
if (sBase || sPer || sCap) {
item.success = {base: sBase, per_level: sPer, cap: sCap};
} else {
delete item.success;
}
}
if (item.station !== undefined && typeof item.station === 'string') {
item.station = item.station.split(',').map(function(s) { return s.trim(); }).filter(function(s) { return s; });
}
if (st.subSubtables) {
st.subSubtables.forEach(function(sst) {
var isIngredients = sst.key === 'ingredients';
var subItems = [];
for (var j = 0; ; j++) {
var subItem = {};
var subFound = false;
sst.fields.forEach(function(f) {
var fid = fieldIDSubSub(sec, st.key, i, sst.key, j, f[0]);
var el = document.getElementById(fid);
if (el) subFound = true;
if (el) subItem[f[0]] = collectFieldValue(el, f[2]);
});
if (!subFound) break;
if (isIngredients) {
var itemsArr = [];
var bypArr = [];
for (var k = 0; ; k++) {
var itemFid = ingredientPairID(sec, st.key, i, sst.key, j, k, 'items');
var itemEl = document.getElementById(itemFid);
if (!itemEl) break;
var itemVal = itemEl.value.trim();
itemsArr.push(itemVal);
var bypFid = ingredientPairID(sec, st.key, i, sst.key, j, k, 'byproducts');
var bypEl = document.getElementById(bypFid);
var bypVal = bypEl ? bypEl.value.trim() : '';
bypArr.push(bypVal);
}
var hasItems = itemsArr.some(function(v) { return v !== ''; });
if (hasItems) {
subItem.items = itemsArr.filter(function(v) { return v !== ''; });
if (bypArr.some(function(v) { return v !== ''; })) subItem.byproducts = bypArr;
}
if (!subItem.items) { subFound = false; }
} else {
if (subItem.items !== undefined && typeof subItem.items === 'string') {
subItem.items = subItem.items.split(',').map(function(s) { return s.trim(); }).filter(function(s) { return s; });
}
if (subItem.byproducts !== undefined && typeof subItem.byproducts === 'string') {
subItem.byproducts = subItem.byproducts.split(',').map(function(s) { return s.trim(); }).filter(function(s) { return s; });
}
}
if (Object.keys(subItem).length > 0) subItems.push(subItem);
}
if (subItems.length > 0) item[sst.key] = subItems;
});
}
if (Object.keys(item).length > 0) items.push(item);
}
if (st.single) {
if (items.length > 0) { sectionObj[st.key] = items[0]; hasValues = true; }
} else {
if (items.length > 0) { sectionObj[st.key] = items; hasValues = true; }
}
});
}
if (sec.inline) {
sec.inline.forEach(function(il) {
if (il.type === 'tags') {
var fid = fieldID(sec, il.key);
var el = document.getElementById(fid);
if (el && el.value.trim()) {
sectionObj[il.key] = el.value.split(',').map(function(s) { return s.trim(); }).filter(function(s) { return s; });
hasValues = true;
}
} else if (il.type === 'kv') {
var kvEl = document.querySelector('.obj-kv-list[data-sec="' + sec.id + '"][data-key="' + il.key + '"]');
if (kvEl) {
var map = {};
kvEl.querySelectorAll('.obj-kv-row').forEach(function(row) {
var keyEl = row.querySelector('.obj-kv-key');
var valEl = row.querySelector('.obj-kv-val');
if (keyEl && keyEl.value.trim() && valEl) {
map[keyEl.value.trim()] = parseFloat(valEl.value) || 0;
}
});
if (Object.keys(map).length > 0) { sectionObj[il.key] = map; hasValues = true; }
}
} else {
var ssec = {};
var sHas = false;
il.fields.forEach(function(f) {
var fp = il.path + '.' + f[0];
var fid = fieldID(sec, fp);
var el = document.getElementById(fid);
if (el) sHas = true;
if (el) ssec[f[0]] = collectFieldValue(el, f[2]);
});
if (sHas) { sectionObj[il.path] = ssec; hasValues = true; }
}
});
}
if (sec.always) {
Object.keys(sectionObj).forEach(function(k) { out[k] = sectionObj[k]; });
} else if (hasValues && sec.path) {
out[sec.path] = sectionObj;
} else if (hasValues && !sec.path) {
Object.keys(sectionObj).forEach(function(k) { out[k] = sectionObj[k]; });
}
});
cleanOutput(out);
out.id = itemID;
API.put('/api/items/' + encodeURIComponent(itemID), out).then(function(resp) {
notify('Item ' + itemID + ' saved', 'success');
updateUndoBar();
if (resp && resp._raw) itemData._raw = resp._raw;
}).catch(function(e) { notify('Save failed: ' + e.message, 'error'); });
}
function saveEquipBlock(eq, blockKey, fieldKey) {
var fid = 'f_equipment_' + blockKey + '_' + fieldKey;
var el = document.getElementById(fid);
if (!el) return;
var val = parseFloat(el.value);
if (isNaN(val)) return;
if (!eq[blockKey]) eq[blockKey] = {};
eq[blockKey][fieldKey] = val;
}
function cleanOutput(obj) {
for (var key in obj) {
var val = obj[key];
if (val === '' || val === null || val === undefined) {
delete obj[key];
} else if (typeof val === 'object' && val !== null && !Array.isArray(val)) {
cleanOutput(val);
if (Object.keys(val).length === 0) delete obj[key];
} else if (Array.isArray(val) && val.length === 0) {
delete obj[key];
}
}
}
// =========================================================================
// Duplicate
// =========================================================================
function duplicateItem() {
var baseID = itemID + '_copy';
var newID = baseID;
var counter = 2;
while (true) {
var exists = false;
for (var i = 0; i < allIDs.length; i++) {
if (allIDs[i] === newID) { exists = true; break; }
}
if (!exists) break;
newID = baseID + '_' + counter;
counter++;
}
var copy = JSON.parse(JSON.stringify(itemData));
delete copy._raw;
delete copy._path;
copy.id = newID;
var dir = '';
if (itemData._path) {
var parts = itemData._path.split('/');
var parentDir = parts[parts.length - 2];
if (parentDir !== 'items') dir = parentDir;
}
API.post('/api/items', {id: newID}).then(function() {
var putOp = API.put('/api/items/' + encodeURIComponent(newID), copy);
if (dir) {
return putOp.then(function() {
return API.post('/api/items/move', {id: newID, dir: dir});
});
}
return putOp;
}).then(function() {
notify('Duplicated as ' + newID, 'success');
updateUndoBar();
loadList();
loadItem(newID);
}).catch(function(e) { notify('Duplicate failed: ' + e.message, 'error'); });
}
// =========================================================================
// Delete
// =========================================================================
function deleteItem() {
if (!confirm('Delete item "' + itemID + '"?')) return;
API.del('/api/items/' + encodeURIComponent(itemID)).then(function() {
notify('Item ' + itemID + ' deleted', 'success');
updateUndoBar();
itemID = null;
currentID = null;
itemData = null;
$('#editorMain').innerHTML = '