var objectData = null; var objectID = null; var expandedCards = {}; var cardWidths = {}; var toolTypes = null; 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', 'ashes', '', null, 'items'], ] }, { id: 'steal', label: 'Steal', path: 'steal', detect: function(d) { return d.steal !== undefined; }, fields: [ ['level', 'Level', 'number', '1', 'narrow'], ['xp', 'XP', 'number', '5', 'narrow'], ['speed', 'Speed', 'number', '4', 'narrow'], ['guard_mob', 'Guard Mob', 'search', '', '', null, 'mobs'], ], subtables: [ {label:'Drops', key:'drops', fields:[ ['item_id', 'Item ID', 'search', '', '', 'items'], ['table', 'Table', 'search', '', '', 'drops'], ['weight', 'Weight', 'number', '10', 'narrow'], ['quantity', 'Quantity', 'number', '', 'narrow'], ]} ] }, { 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', function(d) { return d.skill !== 'fishing'; }], ['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', function(d) { return d.skill !== 'fishing'; }], ], 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', function(d) { return d.skill !== 'fishing'; }], ['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'; }], ['no_tool_speed', 'Bare Speed', 'number', '3', 'narrow', function(d) { return !d.tools || d.tools.length === 0; }], ]}, {label:'Tools', key:'tools', type:'tools_checkbox', hint:'any one is sufficient (OR)'}, ], subtables: [ {label:'Drops', key:'drops', fields:[ ['item_id', 'Item ID', 'search', 'copper_ore', '', 'items'], ['table', 'Table', 'search', 'gem_table', '', 'drops'], ['weight', 'Weight', 'number', '10', 'narrow'], ['level', 'Level', 'number', '1', 'narrow'], ['xp', 'XP', 'number', '17', 'narrow'], ['quantity', 'Quantity', 'number', '1', 'narrow'], ['tool', 'Tool', 'select'], ['depletes', 'Depletes node', 'checkbox'], ['message', 'Message', 'text', 'You manage to mine some copper ore.'], ]} ] }, { id: 'safespot', label: 'Safespot', path: 'safespot', detect: function(d) { return d.safespot; }, fields: [ ['tier', 'Tier', 'number', '1', 'narrow'], ['max_block_size', 'Max Block Size', 'select', '|small|medium|large|massive'], ['max_occupants', 'Max Occupants', 'number', '1', 'narrow'], ['unsafe_chance', 'Unsafe Chance', 'number', '0.05', 'narrow'], ['decay_ticks', 'Decay Ticks', 'number', '100', 'narrow'], ['decay_chance', 'Decay Chance', 'number', '0.01', 'narrow'], ['respawn_ticks', 'Respawn Ticks', 'number', '50', 'narrow'], ['respawn_on_hide', 'Respawn on hide', 'checkbox'], ], subtables: [ {label:'Levels of Decay (lower is more decayed)', key:'levels', vertical: true, fields:[ ['message', 'Look Message', 'text'], ['degrade_message', 'Degrade Message', 'text'] ]} ] }, { id: 'on_use', label: 'Use', labelHint: '(what happens when you use items on this)', path: 'on_use', isArray: true, fullWidth: true, interactionStyle: true, detect: function(d) { return d.on_use && Array.isArray(d.on_use); }, fields: [ ['item_id', 'Item ID', 'search', 'keycard', '', null, 'items'], ] }, { id: 'on_look', label: 'Look', labelHint: '(what happens when you "look ")', path: 'on_look', isArray: true, fullWidth: true, interactionStyle: true, detect: function(d) { return d.on_look && Array.isArray(d.on_look); }, fields: [ ['item_id', 'Item ID', 'search', '(optional — only fires if you carry this item)', '', null, 'items'], ] }, ]; function initObjectEditor() { window._ieRenderCurrent = renderCurrent; window._ieRenderOnly = function() { if (objectID && objectData) renderObjectEditor(objectID, objectData); }; window._ieSyncAll = function(ed) { if (ed) ie_syncAllInteractions(ed, SECTIONS); }; editorType = 'objects'; editorFields = []; loadList(); fetchToolTypes(); if (window.location.hash) loadObject(window.location.hash.substring(1)); } function loadObject(id) { objectID = id; currentID = id; window.location.hash = id; renderList(''); API.get('/api/objects/' + encodeURIComponent(id)).then(function(data) { objectData = data; window._editorData = objectData; window._interVis = {}; window._useInterHelpCollapsed = undefined; window._coreVis = {}; window._stealVis = {}; renderObjectEditor(id, data); }).catch(function(e) { $('#editorMain').innerHTML = '

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

'; }); } function renderObjectEditor(id, data) { var html = renderRenameableHeader('Object', 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 += renderMoveBar(); 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); } function cardPath(sec) { return sec.path || ''; } function getVal(data, sec, key) { var p = cardPath(sec); if (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 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; if (sec.labelHint) { h += ' ' + esc(sec.labelHint) + ''; } h += ''; 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 === 'steal') { h += renderStealCard(data); } else { h += '
'; sec.fields.forEach(function(f) { if (sec.id === 'gather' && f[0] === 'bait') { window._gatherVis = window._gatherVis || {}; var gd = objectData.gather || {}; var isFishing = gd.skill === 'fishing'; var showBait = isFishing && (window._gatherVis.bait || (gd.bait && gd.bait !== '')); if (isFishing && showBait) { var bv = gd.bait || ''; h += ''; } else if (isFishing) { h += ''; } return; } 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 === 'tools_checkbox') { h += renderToolsCheckbox(sec, il, data); } 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 === 'tools_checkbox') { h += renderToolsCheckbox(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 === 'safespot') { var helpCollapsed = window._safespotHelpCollapsed === undefined ? true : window._safespotHelpCollapsed; h += '
'; h += '
'; h += '' + (helpCollapsed ? '▶' : '▼') + ''; h += 'How Tiers, Levels, Decay & Respawn Work'; h += '
'; if (!helpCollapsed) { h += '
'; h += 'Tier — Player tier (base 1; +1 with unlocked_rock_cover flag) must be ≥ safespot tier to hide.
'; h += 'Levels of Decay — Degradation stages (N levels above all subtable entries). Level messages shown when hiding; degrade messages broadcast when damaged. Level 0 = destroyed, all occupants evicted.
'; h += 'Decay — Two independent paths: decay_ticks (forced degrade after N ticks of use) and decay_chance (per-tick random degrade). Either or both can be set; 0 disables that path.
'; h += 'Respawn on hide — Resets the cover to full levels when the first person hides on an empty/unused cover.
'; h += 'Respawn ticks — Auto-reforms the cover after N ticks when fully destroyed (level 0). 0 = instance permanently removed.
'; h += 'Max Block Size — Determines which mob sizes are blocked (small < medium < large < massive). Empty = blocks everything.
'; h += 'Unsafe Chance — Per-tick probability of being forced out of cover regardless of decay (0 = never forced out).'; h += '
'; } h += '
'; } if (sec.id === 'gather') { h += ced_renderTagHelp('gather'); } } 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"'; } 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) + ''); } function renderCoreCard(data) { var sec = SECTIONS[0]; window._coreVis = window._coreVis || {}; var vis = window._coreVis; var showAliases = vis.aliases || (data.aliases && data.aliases.length > 0); var showInRoom = vis.inroom_description || (data.inroom_description && data.inroom_description !== ''); var showRemoval = vis.removal_item || (data.removal_item && data.removal_item !== ''); var h = '
'; h += renderField(sec, sec.fields[0], data, -1, null, 'flex:1'); // name 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 if (showAliases) { h += '
'; h += '
' + renderField(sec, sec.fields[2], data, -1, null, 'flex:2') + '
'; h += ''; h += '
'; } if (showInRoom) { h += '
'; h += '
' + renderField(sec, sec.fields[4], data, -1); h += '
Alternative way this object can appear with the \'look\' command
'; h += ''; h += '
'; } if (showRemoval) { h += '
'; h += '
' + renderField(sec, sec.fields[6], data, -1); h += '
Item left behind when this object is removed
'; h += ''; h += '
'; } h += '
'; h += '
'; if (!showAliases) { h += ''; } if (!showInRoom) { h += ''; } if (!showRemoval) { h += ''; } h += '
'; h += ced_renderTagHelp('color'); return h; } function renderStealCard(data) { var sec = SECTIONS[1]; window._stealVis = window._stealVis || {}; var vis = window._stealVis; var stealData = data.steal || {}; var showGuard = vis.guard_mob || (stealData.guard_mob && stealData.guard_mob !== ''); var h = '
'; h += renderField(sec, sec.fields[0], data, -1); h += renderField(sec, sec.fields[1], data, -1); h += renderField(sec, sec.fields[2], data, -1); if (showGuard) { h += '
'; h += '
' + renderField(sec, sec.fields[3], data, -1) + '
'; h += ''; h += '
'; } else { h += ''; } h += '
'; sec.subtables.forEach(function(st) { h += renderSubtable(sec, st, data); }); return h; } function renderArraySection(sec, data) { var arr = data[cardPath(sec)] || []; var h = ''; var isInter = !!sec.interactionStyle; if (isInter) { window._interVis = window._interVis || {}; if (!window._interVis[sec.id]) window._interVis[sec.id] = {}; return ie_renderArraySection({ sec: sec, data: data, visMap: window._interVis[sec.id], showRemove: function(idx) { return 'removeArrayItem(\'' + sec.id + '\',' + idx + ')'; }, showField: function(secId, idx, key) { return 'showInterField(\'' + secId + '\',' + idx + ',\'' + key + '\')'; }, hideField: function(secId, idx, key) { return 'hideInterField(\'' + secId + '\',' + idx + ',\'' + key + '\')'; }, renderField: function(sec, f, data, idx, optStyle) { return renderField(sec, f, data, idx, null, optStyle); }, onAdd: function() { return 'addArrayItem(\'' + sec.id + '\')'; }, }); } 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' || sec.id === 'steal') && st.key === 'drops'; var isVertical = st.vertical; var gatherTools = []; if (isDrops) { gatherTools = (sectionRoot.tools || []).filter(function(t) { return t && t !== ''; }); } var h = '
'; h += '
' + esc(st.label) + '
'; arr.forEach(function(item, idx) { h += '
'; if (isDrops) { if (!st.single) { h += ''; } 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 + '..."'; var dp = cardPath(sec) + '.' + st.key + '[' + idx + '].' + f[0]; h += ''; } else if (f[2] === 'checkbox') { var hideStyle = ''; if (isDrops && f[0] === 'depletes' && sectionRoot.skill === 'fishing') { hideStyle = ' style="display:none"'; } h += ''; } else if (f[2] === 'select' && f[0] === 'tool') { if (gatherTools.length >= 2) { var dpTool = cardPath(sec) + '.' + st.key + '[' + idx + '].' + f[0]; h += ''; } } else { h += ''; } }); h += '
'; var msgField = st.fields.find(function(f){return f[0]==='message';}); if (msgField) { h += '
'; var fidMsg = fieldIDSub(sec, st.key, idx, 'message'); var msgVal = item.message !== undefined && item.message !== null ? item.message : ''; h += ''; h += '
'; } } else { if (isVertical && !st.single) { h += ''; } 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 + '..."'; var dp = cardPath(sec) + '.' + st.key + '[' + idx + '].' + f[0]; h += ''; } else if (f[2] === 'checkbox') { h += ''; } else { h += ''; } }); if (!st.single && !isVertical) { 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 renderToolsCheckbox(sec, il, data) { var sectionRoot = data[cardPath(sec)] || {}; var arr = sectionRoot[il.key] || []; var choices = toolTypes || []; if (choices.length === 0) { return 'Loading tool types...'; } if (arr.length === 0) { return ''; } var h = '
'; h += '' + esc(il.label) + (il.hint ? ' (' + esc(il.hint) + ')' : '') + ''; h += '
'; arr.forEach(function(t, idx) { h += '
'; h += ''; h += ''; h += '
'; }); h += '
'; h += ''; h += '
'; return h; } function addToolRow(secID, key) { var sec = SECTIONS.find(function(s) { return s.id === secID; }); if (!sec) return; var p = cardPath(sec); if (!objectData[p]) objectData[p] = {}; ced_syncDOMToData(objectData); syncGatherToolsFromDOM(); var val = (toolTypes && toolTypes.length > 0) ? toolTypes[0] : ''; objectData[p][key] = (objectData[p][key] || []).concat([val]); renderObjectEditor(objectID, objectData); } function removeToolRow(secID, key, idx) { var sec = SECTIONS.find(function(s) { return s.id === secID; }); if (!sec) return; ced_syncDOMToData(objectData); syncGatherToolsFromDOM(); var p = cardPath(sec); var arr = objectData[p] && objectData[p][key]; if (!arr) return; arr.splice(idx, 1); if (arr.length === 0) delete objectData[p][key]; renderObjectEditor(objectID, objectData); } function fetchToolTypes() { API.get('/api/tools').then(function(tools) { toolTypes = tools || []; renderCurrent(); }).catch(function() { toolTypes = []; renderCurrent(); }); } function syncGatherToolsFromDOM() { if (!objectData || !objectData.gather) return; var selects = document.querySelectorAll('.obj-tool-select'); if (selects.length === 0) return; var tools = []; selects.forEach(function(s) { if (s.value) tools.push(s.value); }); if (tools.length > 0) { objectData.gather.tools = tools; } else if (objectData.gather.tools && objectData.gather.tools.length > 0) { delete objectData.gather.tools; } } function toggleCard(id) { expandedCards[id] = expandedCards[id] === false ? true : false; renderCurrent(); } function toggleCardWidth(id) { cardWidths[id] = !cardWidths[id]; renderCurrent(); } function showBaitField() { window._gatherVis = window._gatherVis || {}; window._gatherVis.bait = true; renderCurrent(); } function hideBaitField() { window._gatherVis = window._gatherVis || {}; window._gatherVis.bait = false; ced_syncDOMToData(objectData); if (objectData.gather) objectData.gather.bait = ''; renderObjectEditor(objectID, objectData); } function toggleSafespotHelp() { if (window._safespotHelpCollapsed === undefined) { window._safespotHelpCollapsed = false; } else { window._safespotHelpCollapsed = !window._safespotHelpCollapsed; } renderCurrent(); } function removeSection(id) { var sec = SECTIONS.find(function(s) { return s.id === id; }); if (!sec) return; ced_syncDOMToData(objectData); if (sec.path) { delete objectData[sec.path]; } else if (id === 'steal') { delete objectData.steal; } renderObjectEditor(objectID, objectData); } 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 === '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 === 'safespot') { objectData[sec.path] = {tier:1, max_block_size:'', max_occupants:1, unsafe_chance:'', decay_ticks:'', decay_chance:'', respawn_on_hide:false, respawn_ticks:'', levels:[]}; } else { objectData[sec.path] = {}; } } else if (id === 'steal') { objectData.steal = {drops: []}; } 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; if (sec.interactionStyle) { empty = {steps: []}; } else { empty = {}; sec.fields.forEach(function(f) { empty[f[0]] = f[2] === 'checkbox' ? false : ''; }); } 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; ced_syncDOMToData(objectData); var arr = objectData[sec.path] || []; arr.splice(idx, 1); renderObjectEditor(objectID, objectData); } 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 : ''; }); } 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; ced_syncDOMToData(objectData); var sectionRoot = objectData[sec.path || ''] || objectData; var arr = sectionRoot[subKey] || []; arr.splice(idx, 1); renderObjectEditor(objectID, objectData); } 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 st = (sec.subtables || []).find(function(s) { return s.key === subKey; }); var empty = { _type: (dropType === 'table' ? 't' : 'i') }; if (st) { st.fields.forEach(function(f) { empty[f[0]] = f[2] === 'checkbox' ? false : ''; }); } empty.weight = 10; arr.push(empty); sectionRoot[subKey] = arr; renderCurrent(); } function validateDropRow(idx) { // No longer needed — drops are either item or table type, never both. } function renderCurrent() { ced_syncDOMToData(objectData); ie_syncAllInteractions(objectData, SECTIONS); syncGatherToolsFromDOM(); if (!objectData || !objectID) return; renderObjectEditor(objectID, objectData); } function showInterField(secId, idx, key) { window._interVis = window._interVis || {}; ie_showField(window._interVis, secId, idx, key); renderCurrent(); } function addInterRootCondition(secId, idx) { var sec = SECTIONS.find(function(s) { return s.id === secId; }); if (!sec) return; ie_addRootConditionGroup(sec.path, idx, 'entry'); } function hideInterField(secId, idx, key) { window._interVis = window._interVis || {}; ie_hideField(window._interVis, secId, idx, key); var sec = SECTIONS.find(function(s) { return s.id === secId; }); var secPath = sec ? sec.path : secId; ced_syncDOMToData(objectData); ie_syncAllInteractions(objectData, SECTIONS); if (objectData[secPath] && objectData[secPath][idx]) { if (key === 'condition') { delete objectData[secPath][idx].condition; } else { objectData[secPath][idx][key] = ''; } } renderObjectEditor(objectID, objectData); } function showCoreField(key) { window._coreVis = window._coreVis || {}; window._coreVis[key] = true; renderCurrent(); } function hideCoreField(key) { window._coreVis = window._coreVis || {}; window._coreVis[key] = false; ced_syncDOMToData(objectData); if (key === 'aliases') { objectData.aliases = []; } else if (key === 'inroom_description') { objectData.inroom_description = ''; } else if (key === 'removal_item') { objectData.removal_item = ''; } renderObjectEditor(objectID, objectData); } function showGuardMob() { window._stealVis = window._stealVis || {}; window._stealVis.guard_mob = true; renderCurrent(); } function hideGuardMob() { window._stealVis = window._stealVis || {}; window._stealVis.guard_mob = false; ced_syncDOMToData(objectData); if (objectData.steal) objectData.steal.guard_mob = ''; 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 = ['exhausted_message', 'depleted_message', 'deplete_timer', 'nest_chance', 'respawn_broadcast', 'respawn_timer']; var vis = {exhausted_message: wood, depleted_message: !fishing, deplete_timer: wood, nest_chance: wood, respawn_broadcast: !fishing, respawn_timer: !fishing}; els.forEach(function(key) { var el = document.getElementById('f_gather_' + key); if (el) el.closest('.obj-field').style.display = vis[key] ? '' : 'none'; }); document.querySelectorAll('[id^="f_gather_drops_"][id$="_depletes"]').forEach(function(el) { el.closest('.obj-field').style.display = fishing ? 'none' : ''; }); renderCurrent(); } function setupSearchFields() { ced_setupSearchFields(); } function selectObjSearchResult(el) { ced_selectSearchResult(el); } function updateSearchHighlight(results) { ced_updateSearchHighlight(results); } function validateFields() { return ced_validateFields(SECTIONS, fieldID, fieldIDSub); } function collectFieldValue(el, type) { return ced_collectFieldValue(el, type); } function saveObject() { if (!validateFields()) return; ie_syncAllInteractions(objectData, SECTIONS); 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) { if (f[2] === 'inter_cond' || f[2] === 'inter_act') return; var fid = fieldID(sec, f[0], idx); var el = document.getElementById(fid); if (el) item[f[0]] = collectFieldValue(el, f[2]); }); if (sec.interactionStyle) { var dataArr = objectData[sec.path] || []; var dataItem = dataArr[idx] || {}; if (dataItem.condition && typeof dataItem.condition === 'object') { var cleanedCond = ie_cleanCondition(dataItem.condition); if (cleanedCond) item.condition = cleanedCond; } if (dataItem.lock) item.lock = true; if (dataItem.steps && Array.isArray(dataItem.steps)) { var cleanedSteps = dataItem.steps.map(ie_cleanStep).filter(function(s) { return Object.keys(s).length > 0; }); if (cleanedSteps.length > 0) item.steps = cleanedSteps; } } if (Object.keys(item).length > 0) arr.push(item); }); if (arr.length > 0) out[sec.path] = arr; 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 === 'tools_checkbox') { var selects = document.querySelectorAll('.obj-tool-select'); var tools = []; selects.forEach(function(s) { if (s.value) tools.push(s.value); }); if (tools.length > 0) { sectionObj[il.key] = tools; 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; } else if (hasValues) { Object.keys(sectionObj).forEach(function(k) { out[k] = sectionObj[k]; }); } }); ced_cleanOutput(out); out.id = objectID; API.put('/api/objects/' + encodeURIComponent(objectID), out).then(function(resp) { notify('Object ' + objectID + ' saved', 'success'); updateUndoBar(); if (resp && resp._raw) { objectData._raw = resp._raw; var pre = document.querySelector('#tabYaml pre'); if (pre) pre.textContent = objectData._raw; } }).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; var dir = ''; if (objectData._path) { var parts = objectData._path.split('/'); var parentDir = parts[parts.length - 2]; if (parentDir !== 'objects') dir = parentDir; } API.post('/api/objects', {id: newID}).then(function() { var putOp = API.put('/api/objects/' + encodeURIComponent(newID), copy); if (dir) { return putOp.then(function() { return API.post('/api/objects/move', {id: newID, dir: dir}); }); } return putOp; }).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'); }); }