var objectData = null; var objectID = null; var expandedCards = {}; var SECTIONS = [ { id: 'core', label: 'Core', always: true, fields: [ ['name', 'Name', 'text', 'copper rock'], ['color', 'Color', 'color', 'B2'], ['aliases', 'Aliases', 'text', 'rock, ore'], ['hidden', 'Hidden', 'checkbox'], ['inroom_description', 'In-Room Description', 'text', 'A copper rock juts from the ground.'], ['description', 'Description', 'textarea', 'A vein of copper runs through this rock.'], ['removal_item', 'Removal Item ID', 'search', 'e.g. ashes', '', null, 'items'], ] }, { id: 'steal', label: 'Steal', detect: function(d) { return d.steal_table || d.steal_level || d.steal_speed || d.steal_xp || d.guard_mob; }, fields: [ ['steal_table', 'Table ID', 'text'], ['steal_level', 'Level', 'number', '', 'narrow'], ['steal_xp', 'XP', 'number', '', 'narrow'], ['steal_speed', 'Speed', 'number', '', 'narrow'], ['guard_mob', 'Guard Mob', 'text'], ] }, { id: 'gather', label: 'Gather', path: 'gather', detect: function(d) { return d.gather; }, fields: [ ['skill', 'Skill', 'select', 'mining|woodcutting|fishing|crafting'], ['bait', 'Bait Item', 'search', 'fishing_bait', '', function(d) { return d.skill === 'fishing'; }, 'items'], ['gather_message', 'Gather Message', 'text', 'You swing your pickaxe at the rock...', 'wide'], ['fail_message', 'Fail Message', 'text', 'You chip away but get nothing useful.', 'wide'], ['depleted_message', 'Depleted Message', 'text', "You don't see any ore in this rock.", 'wide'], ['exhausted_message', 'Exhausted Message', 'text', 'The tree comes crashing down!', 'wide', function(d) { return d.skill === 'woodcutting'; }], ['respawn_broadcast', 'Respawn Broadcast', 'text', 'A glint of copper catches your eye from some {name}.', 'wide'], ], inline: [ {label:'Success', path:'success', row:0, fields:[ ['base', 'Base', 'number', '0.5', 'narrow'], ['per_level', 'Per Lvl', 'number', '0.01', 'narrow'], ['cap', 'Cap', 'number', '0.95', 'narrow'] ]}, {label:'Other', row:0, fields:[ ['respawn_timer', 'Respawn', 'number', '50', 'narrow'], ['deplete_timer', 'Deplete', 'number', '45', 'narrow', function(d) { return d.skill === 'woodcutting'; }], ['nest_chance', 'Nest 1/n', 'number', '256', 'narrow', function(d) { return d.skill === 'woodcutting'; }], ]}, {label:'Tools', key:'tools', type:'tags'}, ], subtables: [ {label:'Drops', key:'drops', fields:[ ['item_id', 'Item ID', 'search', 'copper_ore', '', 'items'], ['table', 'Table', 'search', 'gem_table', '', 'drops'], ['weight', 'Weight', 'number', '90', 'narrow'], ['level', 'Level', 'number', '1', 'narrow'], ['xp', 'XP', 'number', '17', 'narrow'], ['quantity', 'Quantity', 'number', '1', 'narrow'], ['depletes', 'Depletes node', 'checkbox'], ['message', 'Message', 'text', 'You manage to mine some copper ore.'], ]} ] }, { id: 'use', label: 'Use (Station)', path: 'use', detect: function(d) { return d.use; }, fields: [ ['message', 'Message', 'text'], ['wait', 'Wait', 'number', '', 'narrow'], ['skill', 'Skill', 'text'], ['level', 'Level', 'number', '', 'narrow'], ['xp', 'XP', 'number', '', 'narrow'], ['fail_message', 'Fail Message', 'text'], ], inline: [ {label:'Success', path:'success', fields:[ ['base', 'Base', 'number', '', 'narrow'], ['per_level', 'Per Lvl', 'number', '', 'narrow'], ['cap', 'Cap', 'number', '', 'narrow'] ]}, {label:'Consume', key:'consume', type:'kv', valType:'number'}, ], subtables: [ {label:'Reward', key:'reward', single:true, fields:[ ['item_id', 'Item ID', 'search', '', '', 'items'], ['weight', 'Weight', 'number'], ['quantity', 'Quantity', 'number'], ['message', 'Message', 'text'], ]} ] }, { id: 'safespot', label: 'Safespot', path: 'safespot', detect: function(d) { return d.safespot; }, fields: [ ['tier', 'Tier', 'number', '', 'narrow'], ['max_block_size', 'Max Block Size', 'text'], ['max_occupants', 'Max Occupants', 'number', '', 'narrow'], ['unsafe_chance', 'Unsafe Chance', 'number', '', 'narrow'], ['decay_ticks', 'Decay Ticks', 'number', '', 'narrow'], ['decay_chance', 'Decay Chance', 'number', '', 'narrow'], ['respawn_on_hide', 'Respawn on Hide', 'checkbox'], ['respawn_ticks', 'Respawn Ticks', 'number', '', 'narrow'], ], subtables: [ {label:'Levels', key:'levels', fields:[ ['message', 'Message', 'text'], ['degrade_message', 'Degrade Message', 'text'] ]} ] }, { id: 'use_interactions', label: 'Use Interactions', path: 'use_interactions', isArray: true, detect: function(d) { return d.use_interactions && Array.isArray(d.use_interactions); }, fields: [ ['item_id', 'Item ID', 'text'], ['message', 'Message', 'text'], ['condition', 'Condition', 'json'], ['action', 'Action', 'json'], ] }, { id: 'talk', label: 'Talk', path: 'talk', detect: function(d) { return d.talk && d.talk.nodes; }, fields: [] }, { id: 'on_look', label: 'On Look', path: 'on_look', detect: function(d) { return d.on_look; }, fields: [ ['set_flags', 'Set Flags', 'json'], ['set_player_flags', 'Set Player Flags', 'json'], ['give_item', 'Give Item', 'text'], ['take_item', 'Take Item', 'text'], ['teleport', 'Teleport', 'number', '', 'narrow'], ['heal', 'Heal', 'number', '', 'narrow'], ['cost', 'Cost', 'number', '', 'narrow'], ['assign_task', 'Assign Task', 'checkbox'], ['skip_task', 'Skip Task', 'checkbox'], ['extend_task', 'Extend Task', 'checkbox'], ['reputation_cost', 'Reputation Cost', 'number', '', 'narrow'], ['sawmill', 'Sawmill', 'checkbox'], ['aps_node', 'APS Node', 'checkbox'], ] }, ]; function initObjectEditor() { editorType = 'objects'; editorFields = []; loadList(); if (window.location.hash) loadObject(window.location.hash.substring(1)); } window.onTalkTreeChange = function(data) { objectData.talk = data.talk; }; function loadObject(id) { objectID = id; currentID = id; window.location.hash = id; renderList(''); API.get('/api/objects/' + encodeURIComponent(id)).then(function(data) { objectData = data; renderObjectEditor(id, data); }).catch(function(e) { $('#editorMain').innerHTML = '

Failed to load: ' + esc(e.message) + '

'; }); } function renderObjectEditor(id, data) { var html = '

Object: ' + esc(id) + '

'; html += '
'; html += '
'; html += '
'; html += '
'; SECTIONS.forEach(function(sec) { if (!sec.always && sec.detect && !sec.detect(data)) return; html += renderCard(sec, data); }); html += '
'; html += '
'; html += ''; html += ''; html += '
'; html += '
'; html += ''; html += '
'; html += ''; html += ''; html += ''; html += '
'; $('#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] = {}; } } else if (id === 'steal') { objectData.steal_table = ''; objectData.steal_level = 0; objectData.steal_xp = 0; objectData.steal_speed = 0; objectData.guard_mob = ''; } 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 = ''; results._selectedIndex = -1; 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 = '
No results
'; return; } results.style.display = 'block'; results.innerHTML = items.map(function(r) { return '
' + esc(r.id) + ' ' + esc(r.name || '') + '
'; }).join(''); results._items = items; results._selectedIndex = -1; }).catch(function() { results.style.display = 'none'; }); }); input.addEventListener('focus', function() { if (input.value.trim()) { input.dispatchEvent(new Event('input')); } }); input.addEventListener('blur', function() { setTimeout(function() { results.style.display = 'none'; }, 200); }); input.addEventListener('keydown', function(e) { if (results.style.display === 'none' || !results.children.length) return; if (results._selectedIndex === undefined) results._selectedIndex = -1; if (e.key === 'ArrowDown') { e.preventDefault(); results._selectedIndex = Math.min(results._selectedIndex + 1, results.children.length - 1); updateSearchHighlight(results); } else if (e.key === 'ArrowUp') { e.preventDefault(); results._selectedIndex = Math.max(results._selectedIndex - 1, 0); updateSearchHighlight(results); } else if (e.key === 'Enter') { if (results._selectedIndex >= 0) { e.preventDefault(); selectObjSearchResult(results.children[results._selectedIndex]); } } else if (e.key === 'Escape') { results.style.display = 'none'; results._selectedIndex = -1; } }); }); document.addEventListener('click', function(e) { if (!e.target.closest('.search-results') && !e.target.closest('.search-input')) { document.querySelectorAll('.search-results').forEach(function(r) { r.style.display = 'none'; }); } }); } function selectObjSearchResult(el) { var results = el.parentElement; var idx = Array.prototype.indexOf.call(results.children, el); if (results._items && results._items[idx] && results._targetInput) { results._targetInput.value = results._items[idx].id; results._targetInput.dispatchEvent(new Event('input')); results.style.display = 'none'; results.innerHTML = ''; } } function updateSearchHighlight(results) { for (var i = 0; i < results.children.length; i++) { results.children[i].classList.toggle('active', i === results._selectedIndex); if (i === results._selectedIndex) results.children[i].scrollIntoView({block: 'nearest'}); } } function validateFields() { var errors = []; document.querySelectorAll('.obj-field-error').forEach(function(field) { field.classList.remove('obj-field-error'); }); document.querySelectorAll('input[id][type="text"], input[id]:not([type])').forEach(function(input) { var id = input.id; if (!id) return; var labelEl = input.closest('.obj-field'); if (!labelEl) return; 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 isSearch = input.classList.contains('search-input'); var subKeyMatch = id.match(/^f_(\w+)_(\w+)_(\d+)_(\w+)$/); var numberField = null; if (isSearch) { return; } if (subKeyMatch) { var subKey = subKeyMatch[2]; var st = (sec.subtables || []).find(function(s) { return s.key === subKey; }); if (st) { var fieldKey = subKeyMatch[4]; numberField = st.fields.find(function(f) { return f[0] === fieldKey && f[2] === 'number'; }); } } else { numberField = sec.fields.find(function(f) { var fp = f[0]; if (sec.path && sec.path !== 'on_look') fp = fp; var fid = fieldID(sec, fp, -1); return fid === id && f[2] === 'number'; }); if (!numberField && sec.inline) { sec.inline.forEach(function(il) { if (il.fields) { var nf = il.fields.find(function(f) { var fid = fieldID(sec, il.path + '.' + f[0], -1); return fid === id && f[2] === 'number'; }); if (nf) numberField = nf; } }); } } if (numberField) { var rawVal = input.value.trim(); if (rawVal !== '' && !/^-?\d*\.?\d*$/.test(rawVal)) { labelEl.classList.add('obj-field-error'); input.focus(); input.select(); var labelText = (labelEl.querySelector('span') || {}).textContent || numberField[1] || ''; errors.push('Invalid value in "' + esc(labelText) + '": "' + esc(rawVal) + '"'); return; } } }); if (errors.length > 0) { notify(errors.join('; '), 'error'); return false; } return true; } function collectFieldValue(el, type) { if (type === 'checkbox') return el.checked; if (type === 'select') return el.value; if (type === 'search') return el.value; if (type === 'number') { var n = parseFloat(el.value); return isNaN(n) ? 0 : n; } if (type === 'json') { var raw = el.value.trim(); if (!raw) return null; try { return JSON.parse(raw); } catch(e) { return raw; } } if (type === 'textarea') return el.value; return el.value; } function saveObject() { 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 === 'talk') { if (objectData.talk) out.talk = objectData.talk; return; } var sectionObj = {}; var hasValues = sec.always; 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 (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]; }); if (typeof out.aliases === 'string') { out.aliases = out.aliases ? out.aliases.split(',').map(function(s) { return s.trim(); }).filter(function(s) { return s; }) : []; } } else if (hasValues && sec.path) { out[sec.path] = sectionObj; } }); out.id = objectID; API.put('/api/objects/' + encodeURIComponent(objectID), out).then(function() { notify('Object ' + objectID + ' saved', 'success'); updateUndoBar(); }).catch(function(e) { notify('Save failed: ' + e.message, 'error'); }); } function duplicateObject() { var baseID = objectID + '_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(objectData)); delete copy._raw; delete copy._path; copy.id = newID; API.post('/api/objects', {id: newID}).then(function() { return API.put('/api/objects/' + encodeURIComponent(newID), copy); }).then(function() { notify('Duplicated as ' + newID, 'success'); updateUndoBar(); loadList(); loadObject(newID); }).catch(function(e) { notify('Duplicate failed: ' + e.message, 'error'); }); } function deleteObject() { if (!confirm('Delete object "' + objectID + '"?')) return; API.del('/api/objects/' + encodeURIComponent(objectID)).then(function() { notify('Object ' + objectID + ' deleted', 'success'); updateUndoBar(); objectID = null; currentID = null; objectData = null; $('#editorMain').innerHTML = '

Deleted

'; loadList(); }).catch(function(e) { notify('Delete failed: ' + e.message, 'error'); }); } function loadItem(id) { loadObject(id); } function createNew() { var name = prompt('Object ID:'); if (!name) return; API.post('/api/objects', {id: name}).then(function() { notify('Object ' + name + ' created', 'success'); updateUndoBar(); loadList(); loadObject(name); }).catch(function(e) { notify('Create failed: ' + e.message, 'error'); }); }