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', 'e.g. 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 = never, e.g. 0.05', 'narrow'], ['decay_ticks', 'Decay Ticks', 'number', '0 = never, e.g. 100', 'narrow'], ['decay_chance', 'Decay Chance', 'number', '0 = never, e.g. 0.01', 'narrow'], ['respawn_ticks', 'Respawn Ticks', 'number', '0 = no respawn, e.g. 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: 'use_interactions', label: 'Use Interactions', labelHint: '(what happens when you use items on this)', path: 'use_interactions', isArray: true, fullWidth: true, detect: function(d) { return d.use_interactions && Array.isArray(d.use_interactions); }, fields: [ ['item_id', 'Item ID', 'search', 'e.g. keycard', '', null, 'items'], ['condition', 'Condition', 'json', 'e.g. {"flag":"my_flag"} or {"player_flag":"x","value":1} or {"all_of":[{"flag":"a"},{"has_item":"key"}]}'], ['action', 'Action', 'json', 'e.g. {"set_flags":{"my_flag":true},"message":"You activate it.","give_item":"gem"}'], ['message', 'Message', 'text', 'You use the object...'], ] }, { id: 'on_look', label: 'On Look', labelHint: '(what happens when you "look ")', path: 'on_look', detect: function(d) { return d.on_look; }, fields: [ ['set_flags', 'Set Global Flags', 'json', '{"flag":"value"}'], ['set_player_flags', 'Set Player Flags', 'json', '{"pflag":"value"}'], ['give_item', 'Give Item', 'search', 'e.g. keycard', '', null, 'items'], ['take_item', 'Take Item', 'search', 'e.g. bomb', '', null, 'items'], ['teleport', 'Teleport', 'number', '5', 'narrow'], ['heal', 'Heal', 'number', '10', 'narrow'], ['credits', 'Credits', 'number', '100', 'narrow'], ] }, ]; function initObjectEditor() { 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._useInterVis = {}; 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 (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 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 if (sec.id === 'on_look') { h += renderOnLookCard(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 += '
'; } } 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 += ''; h += '
'; } if (showRemoval) { h += '
'; h += '
' + renderField(sec, sec.fields[6], data, -1) + '
'; h += ''; h += '
'; } h += '
'; h += '
'; if (!showAliases) { h += ''; } if (!showInRoom) { h += ''; } if (!showRemoval) { h += ''; } h += '
'; 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 renderOnLookCard(data) { var sec = SECTIONS.find(function(s) { return s.id === 'on_look'; }); var h = '
'; h += renderField(sec, sec.fields[0], data, -1, null, 'flex:1'); h += renderField(sec, sec.fields[1], data, -1, null, 'flex:1'); h += '
'; h += '
'; h += renderField(sec, sec.fields[2], data, -1, null, 'flex:1'); h += renderField(sec, sec.fields[3], data, -1, null, 'flex:1'); h += '
'; h += '
'; h += renderField(sec, sec.fields[4], data, -1); h += renderField(sec, sec.fields[5], data, -1); h += renderField(sec, sec.fields[6], data, -1); h += '
'; var helpCollapsed = window._onLookHelpCollapsed === undefined ? true : window._onLookHelpCollapsed; h += '
'; h += '
'; h += '' + (helpCollapsed ? '▶' : '▼') + ''; h += 'How Set Global Flags & Set Player Flags Work'; h += '
'; if (!helpCollapsed) { h += '
'; h += 'When a player looks at this object, these fields apply effects.

'; h += 'Set Global Flags — Sets world flags shared by all players. JSON object mapping flag names to values:
'; h += '  {"gate_open":true,"boss_summoned":false}
'; h += '  Flags persist until changed by another on_look, room script, or interact action.

'; h += 'Set Player Flags — Sets per-character flags saved to the player\'s YAML. JSON object mapping flag names to values:
'; h += '  {"has_seen_cave":true,"dialog_spoke":1}
'; h += '  These flags can be used in conditions (player_flag) to track player progress.

'; h += 'Other FieldsGive Item grants an item, Take Item removes one, '; h += 'Teleport sends player to a room, Heal restores HP, Credits adds/removes credits (use negative for cost).

'; h += 'All effects fire simultaneously when the player looks at this object.'; h += '
'; } h += '
'; return h; } function renderArraySection(sec, data) { var arr = data[cardPath(sec)] || []; var h = ''; var isUseInter = sec.id === 'use_interactions'; if (isUseInter) { window._useInterVis = window._useInterVis || {}; } arr.forEach(function(item, idx) { if (isUseInter) { var vis = window._useInterVis[idx] || {}; window._useInterVis[idx] = vis; var showCond = vis.condition || (item.condition && item.condition !== ''); var showAct = vis.action || (item.action && item.action !== ''); var showMsg = vis.message || (item.message && item.message !== ''); h += '
'; h += ''; h += '
'; h += renderField(sec, sec.fields[0], data, idx, null, 'max-width:260px'); h += '
'; if (!showCond) { h += ''; } if (!showAct) { h += ''; } if (!showMsg) { h += ''; } h += '
'; h += '
'; h += renderField(sec, sec.fields[1], data, idx, null, 'flex:1;min-width:0'); h += ''; h += '
'; h += '
'; h += renderField(sec, sec.fields[2], data, idx, null, 'flex:1;min-width:0'); h += ''; h += '
'; h += '
'; h += renderField(sec, sec.fields[3], data, idx, null, 'flex:1;min-width:0'); h += ''; h += '
'; h += '
'; } else { h += '
'; h += '
'; sec.fields.forEach(function(f) { h += renderField(sec, f, data, idx); }); h += '
'; h += ''; h += '
'; } }); h += ''; if (isUseInter) { var helpCollapsed = window._useInterHelpCollapsed === undefined ? true : window._useInterHelpCollapsed; h += '
'; h += '
'; h += '' + (helpCollapsed ? '▶' : '▼') + ''; h += 'How Conditions & Actions Work'; h += '
'; if (!helpCollapsed) { h += '
'; h += 'Entries checked top-to-bottom, first match wins.

Item ID: required for use <item> on <object>; omit for plain use <object>.
'; h += 'Condition: JSON. When this interaction is available:
'; h += '  {"flag":"gate_open","not":true} — world flag is NOT set
'; h += '  {"player_flag":"has_key","value":1} — player flag matches value
'; h += '  {"has_item":"silver_bar"} — player carries item
'; h += '  {"all_of":[{"flag":"a"},{"has_item":"b"}]} — ALL sub-conditions
'; h += '  {"any_of":[...]} — ANY sub-condition
'; h += '  Omit for an unconditional interaction.
'; h += 'Action: JSON. What happens when the interaction fires:
'; h += '  {"set_flags":{"gate_open":true}} — set world flags
'; h += '  {"set_player_flags":{"spoke":true}} — set player flags
'; h += '  {"give_item":"keycard","teleport":5} — give item + teleport
'; h += '  {"take_item":"bomb","heal":10} — take item + heal
'; h += '  {"credits":-50} — deduct credits
'; h += '  {"reputation_cost":1,"aps_node":true} — rep cost + mark APS
'; h += 'Message: Sent to the player when the interaction fires (1-tick action).'; 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) { var hint = il.hint ? '
' + esc(il.hint) + '
' : ''; return hint + ''; } 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 === '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 === '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 = {}; 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; 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 : (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; 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 : (f[2] === 'number' ? 0 : ''); }); } 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); syncGatherToolsFromDOM(); if (!objectData || !objectID) return; renderObjectEditor(objectID, objectData); } function showUseInterField(idx, key) { window._useInterVis = window._useInterVis || {}; if (!window._useInterVis[idx]) window._useInterVis[idx] = {}; window._useInterVis[idx][key] = true; renderCurrent(); } function hideUseInterField(idx, key) { window._useInterVis = window._useInterVis || {}; if (!window._useInterVis[idx]) window._useInterVis[idx] = {}; window._useInterVis[idx][key] = false; ced_syncDOMToData(objectData); if (objectData.use_interactions && objectData.use_interactions[idx]) { objectData.use_interactions[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 toggleUseInterHelp() { if (window._useInterHelpCollapsed === undefined) { window._useInterHelpCollapsed = false; } else { window._useInterHelpCollapsed = !window._useInterHelpCollapsed; } renderCurrent(); } function toggleOnLookHelp() { if (window._onLookHelpCollapsed === undefined) { window._onLookHelpCollapsed = false; } else { window._onLookHelpCollapsed = !window._onLookHelpCollapsed; } renderCurrent(); } 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; 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; } 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]; }); } }); 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'); }); }