// mobeditor.js — Card-based mob editor following the object/item pattern. var mobData = null; var mobID = null; var expandedCards = {}; var addedSections = {}; var cardWidths = {}; var MOB_SECTIONS = [ { id: 'core', label: 'Core', always: true, fields: [ ['name', 'Name', 'text', 'spaceport attendant'], ['description', 'Description', 'textarea', 'A lanky man in a wrinkled grey uniform.'], ['unique', 'Unique', 'checkbox', '', '', null, null, 'Mob name is displayed without an article (e.g. "John Smith" instead of "a John Smith")'], ['protected', 'Protected', 'checkbox', '', '', null, null, 'Cannot be attacked.'], ], inline: [ {label:'Idle Descriptions', key:'idle_descriptions', type:'tags'}, ] }, { id: 'combat', label: 'Combat Stats', path: 'combat', detect: function(d) { return d.combat; }, fields: [ ['aggressive', 'Aggressive', 'checkbox'], ['respawn_ticks', 'Respawn Ticks', 'number', '30', 'narrow'], ['stats.hp', 'HP', 'number', '10', 'narrow'], ['stats.attack', 'Attack', 'number', '5', 'narrow'], ['stats.strength', 'Strength', 'number', '5', 'narrow'], ['stats.defense', 'Defense', 'number', '5', 'narrow'], ['stats.ranged', 'Ranged', 'number', '1', 'narrow'], ['stats.science', 'Science', 'number', '1', 'narrow'], ['stats.speed', 'Speed', 'number', '5', 'narrow'], ['stats.max_melee_hit', 'Max Melee Hit', 'number', '2', 'narrow'], ['stats.max_ranged_hit', 'Max Ranged Hit', 'number', '0', 'narrow'], ['stats.max_science_hit', 'Max Science Hit', 'number', '0', 'narrow'], ['stats.bonuses.attack_bonus', 'Atk Bonus', 'number', '0', 'narrow'], ['stats.bonuses.strength_bonus', 'Str Bonus', 'number', '0', 'narrow'], ['stats.bonuses.science_bonus', 'Sci Bonus', 'number', '0', 'narrow'], ['stats.bonuses.science_percent_bonus', 'Sci % Bonus', 'number', '0', 'narrow'], ['stats.bonuses.ranged_bonus', 'Rng Bonus', 'number', '0', 'narrow'], ['stats.bonuses.ranged_strength_bonus', 'Rng Str Bonus', 'number', '0', 'narrow'], ['stats.defenses.stab_defense', 'Stab Def', 'number', '0', 'narrow'], ['stats.defenses.slash_defense', 'Slash Def', 'number', '0', 'narrow'], ['stats.defenses.crush_defense', 'Crush Def', 'number', '0', 'narrow'], ['stats.defenses.science_defense', 'Sci Def', 'number', '0', 'narrow'], ['stats.defenses.ranged_defense', 'Rng Def', 'number', '0', 'narrow'], ['stats.defenses.weakness', 'Weakness', 'select', '(none)|bio|eco|hydro|solar|chaos'], ['stats.defenses.weakness_percent', 'Weakness %', 'number', '50', 'narrow'], ['assassin_level', 'Assassin Lvl', 'number', '1', 'narrow'], ['finishing_blow', 'Finishing Blow', 'search', 'stake', '', null, 'items'], ['damage_without', 'Damage Without', 'search', 'holy_symbol', '', null, 'items'], ['size', 'Size', 'text', 'small'], ], inline: [ ] }, { id: 'steal', label: 'Steal', path: 'steal', detect: function(d) { return d.steal; }, fields: [ ['level', 'Level', 'number', '1', 'narrow'], ['xp', 'XP', 'number', '5', 'narrow'], ['speed', 'Speed', 'number', '4', 'narrow'], ], subtables: [ {label:'Drops', key:'drops', fields:[ ['item_id', 'Item', 'search', '', '', null, 'items'], ['table', 'Table', 'search', '', '', null, 'drops'], ['weight', 'Weight', 'number', '', 'narrow'], ['quantity', 'Quantity', 'number', '', 'narrow'], ]} ] }, { id: 'task', label: 'Task', path: 'task', detect: function(d) { return d.task; }, fields: [ ['verb', 'Verb', 'text', 'work'], ['progress_noun', 'Progress Noun', 'text', 'construction'], ['complete_message', 'Complete Message', 'text', 'The work is finished.'], ] }, { id: 'talk', label: 'Talk', path: 'talk', fullWidth: true, detect: function(d) { return d.talk && d.talk.nodes; }, fields: [] }, { id: 'shop', label: 'Shop', path: 'shop', detect: function(d) { return !!d.shop; }, fields: [ ['message', 'Greeting', 'text', 'What would you like to buy or sell?'], ['buy_percentage', 'Buy %', 'number', '40', 'narrow'], ['change_percentage', 'Change %', 'number', '3', 'narrow'], ['buys_anything', 'Buys Anything', 'checkbox'], ], subtables: [ {label:'Items', key:'items', addLabel:'+ Add Item', table:true, fields:[ ['item_id', 'Item', 'search', '', '', null, 'items'], ['stock', 'Stock', 'number', '', 'narrow'], ['restock_ticks', 'Restock Ticks', 'number', '1000', 'narrow'], ]} ] }, { id: 'drops', label: 'Drops', path: 'drops', detect: function(d) { return d.drops && (d.drops.remains || (d.drops.loot && d.drops.loot.length > 0)); }, fields: [ ['remains', 'Remains', 'search', '', '', null, 'items'], ], subtables: [ {label:'Loot', key:'loot', fields:[ ['item_id', 'Item', 'search', '', '', null, 'items'], ['table', 'Table', 'search', '', '', null, 'drops'], ['weight', 'Weight', 'number', '', 'narrow'], ['quantity', 'Quantity', 'number', '', 'narrow'], ]} ] }, { id: 'on_kill', label: 'Kill', labelHint: '(fired when this mob is defeated or its task completes — additive over drops)', path: 'on_kill', isArray: true, fullWidth: true, interactionStyle: true, detect: function(d) { return d.on_kill && Array.isArray(d.on_kill); }, fields: [ ['item_id', 'Item ID', 'search', '(optional — only fires if you wield this weapon when the mob is defeated)', '', null, 'items'], ] }, ]; function cardPath(sec) { return ced_cardPath(sec); } function getVal(data, sec, key) { return ced_getVal(data, sec, key); } function fieldID(sec, key, idx) { return ced_fieldID(sec, key, idx); } function fieldIDSub(sec, subKey, idx, fieldKey) { return ced_fieldIDSub(sec, subKey, idx, fieldKey); } function initMobEditor() { window._ieRenderCurrent = renderCurrent; window._ieRenderOnly = function() { if (mobID && mobData) renderMobEditor(mobID, mobData); }; window._ieSyncAll = function(ed) { if (ed) ie_syncAllInteractions(ed, MOB_SECTIONS); }; editorType = 'mobs'; editorFields = []; loadList(); if (window.location.hash) loadMob(window.location.hash.substring(1)); } window.onTalkTreeChange = function(data) { mobData.talk = data.talk; }; function loadMob(id) { mobID = id; currentID = id; window.location.hash = id; renderList(''); API.get('/api/mobs/' + encodeURIComponent(id)).then(function(data) { mobData = data; window._editorData = mobData; window._mobInterVis = {}; if (mobData.shop && mobData.shop.buys_anything === undefined) { mobData.shop.buys_anything = true; } renderMobEditor(id, data); }).catch(function(e) { $('#editorMain').innerHTML = '

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

'; }); } function renderMobEditor(id, data) { var html = renderRenameableHeader('Mob', id); html += '
'; html += '
'; html += '
'; html += '
'; MOB_SECTIONS.forEach(function(sec) { if (!sec.always && sec.detect && !sec.detect(data) && !addedSections[sec.id]) return; html += renderMobCard(sec, data); }); html += '
'; html += '
'; html += ''; html += ''; html += renderMoveBar(); html += '
'; html += '
'; html += ''; html += '
'; html += ''; html += ''; html += ''; html += ''; html += '
'; $('#editorMain').innerHTML = html; ced_setupSearchFields(); setupMobProtectedToggle(); applyTaskVisibility(); MOB_SECTIONS.forEach(function(sec) { if (sec.id === 'talk' && data.talk && data.talk.nodes) { renderTalkTree(data); } }); setupMobDropViews(); } function renderCurrent() { ced_syncDOMToData(mobData); ie_syncAllInteractions(mobData, MOB_SECTIONS); if (!mobData || !mobID) return; renderMobEditor(mobID, mobData); } // ---- Card rendering ---- function renderMobCard(sec, data) { var collapsed = expandedCards[sec.id] === false; var isFull = cardWidths[sec.id] !== undefined ? cardWidths[sec.id] : (sec.fullWidth || false); var fullClass = isFull ? ' obj-card-full' : ''; var h = '
'; h += '
'; h += '' + (collapsed ? '▶' : '▼') + ''; h += '' + sec.label + ''; h += ''; if (!sec.always) { h += ''; } h += '
'; if (!collapsed) { h += '
'; if (sec.id === 'core') { h += renderMobCoreCard(data); } else if (sec.id === 'combat') { h += renderMobCombatCard(data); } else if (sec.id === 'task') { h += renderMobTaskCard(data); } else if (sec.id === 'drops') { h += renderMobDropsCard(data); } else if (sec.isArray) { h += renderMobArraySection(sec, data); } else if (sec.subtables) { h += '
'; sec.fields.forEach(function(f) { h += renderMobField(sec, f, data, -1); }); h += '
'; sec.subtables.forEach(function(st) { h += renderMobSubtable(sec, st, data); }); } else { h += '
'; sec.fields.forEach(function(f) { h += renderMobField(sec, f, data, -1); }); h += '
'; if (sec.inline) { sec.inline.forEach(function(il) { if (il.type === 'tags') { h += ced_renderTags(sec, il, data); } }); } if (sec.id === 'talk') { h += '
'; } } h += '
'; } h += '
'; return h; } function renderMobCoreCard(data) { var sec = MOB_SECTIONS[0]; var byKey = {}; sec.fields.forEach(function(f) { byKey[f[0]] = f; }); var h = '
'; h += renderMobField(sec, byKey['name'], data, -1, null, 'flex:1'); h += '
'; h += renderMobField(sec, byKey['description'], data, -1); sec.inline.forEach(function(il) { h += ced_renderTags(sec, il, data); }); h += '
'; h += '
'; h += renderMobField(sec, byKey['unique'], data, -1); h += '
'; h += '
'; h += renderMobField(sec, byKey['protected'], data, -1); h += '
'; h += '
'; h += ced_renderTagHelp('color'); return h; } function renderMobKindToggle(data) { var combat = data.combat || {}; var kind = combat.kind === 'task' ? 'task' : 'combat'; var activeC = kind === 'combat'; var h = '
'; h += 'Kind'; h += '
'; h += ''; h += ''; h += '
'; return h; } function mobKindPillSelect(val, btn) { var pill = document.getElementById('mob_kind_pill'); if (!pill) return; var opts = pill.querySelectorAll('.seg-opt'); var idx = -1; opts.forEach(function(o, i) { if (o.getAttribute('data-v') === val) { o.classList.add('seg-active'); idx = i; } else { o.classList.remove('seg-active'); } }); pill.className = pill.className.replace(/seg-idx-\d/g, 'seg-idx-' + idx); if (val === 'task') { if (!mobData.combat) mobData.combat = {}; mobData.combat.kind = 'task'; if (!mobData.task) mobData.task = {verb:'work', progress_noun:'construction', complete_message:'The work is finished.'}; addedSections.task = true; expandedCards.task = true; } else { if (mobData.combat) mobData.combat.kind = 'combat'; delete mobData.task; delete addedSections.task; } renderCurrent(); } function collectMobKind() { var pill = document.getElementById('mob_kind_pill'); if (!pill) return 'combat'; var active = pill.querySelector('.seg-opt.seg-active'); return active ? active.getAttribute('data-v') : 'combat'; } function renderMobCombatCard(data) { var sec = MOB_SECTIONS.find(function(s) { return s.id === 'combat'; }); if (!sec) return ''; var byKey = {}; sec.fields.forEach(function(f) { byKey[f[0]] = f; }); var h = '
'; h += renderMobKindToggle(data); h += renderMobField(sec, byKey['respawn_ticks'], data, -1); h += '
'; h += renderMobField(sec, byKey['aggressive'], data, -1); h += '
'; h += '
'; h += '
'; h += ced_renderAtkTypeWidget(data, 'mob_atktype', 'ced_atkPillSelect'); h += '
'; h += '
'; h += 'Stats'; h += '
'; h += renderMobField(sec, byKey['stats.hp'], data, -1); h += renderMobField(sec, byKey['stats.defense'], data, -1); h += '
'; h += '
'; h += '
'; var atkStatKeys = ['stats.attack','stats.strength','stats.ranged','stats.science']; atkStatKeys.forEach(function(k) { h += renderMobField(sec, byKey[k], data, -1); }); h += '
'; h += '
'; h += '
'; h += '
'; h += renderMobField(sec, byKey['stats.speed'], data, -1); h += '
'; h += '
'; h += '
'; h += '
'; var statKeys3 = ['stats.max_melee_hit','stats.max_ranged_hit','stats.max_science_hit']; statKeys3.forEach(function(k) { h += renderMobField(sec, byKey[k], data, -1); }); h += '
'; h += '
'; h += '
'; h += '
'; h += '
'; h += 'Bonuses'; h += '
'; var bonusKeys = ['stats.bonuses.attack_bonus','stats.bonuses.strength_bonus','stats.bonuses.science_bonus', 'stats.bonuses.science_percent_bonus','stats.bonuses.ranged_bonus','stats.bonuses.ranged_strength_bonus']; bonusKeys.forEach(function(k) { h += renderMobField(sec, byKey[k], data, -1); }); h += '
'; h += '
'; h += '
'; h += '
'; h += 'Defenses'; h += '
'; var defKeys = ['stats.defenses.stab_defense','stats.defenses.slash_defense','stats.defenses.crush_defense', 'stats.defenses.science_defense','stats.defenses.ranged_defense']; defKeys.forEach(function(k) { h += renderMobField(sec, byKey[k], data, -1); }); h += '
'; h += '
'; h += renderMobField(sec, byKey['stats.defenses.weakness'], data, -1); h += renderMobField(sec, byKey['stats.defenses.weakness_percent'], data, -1); h += '
'; h += '
'; h += '
'; h += renderMobField(sec, byKey['assassin_level'], data, -1); h += renderMobField(sec, byKey['size'], data, -1); h += '
'; h += '
'; h += renderMobField(sec, byKey['finishing_blow'], data, -1); h += renderMobField(sec, byKey['damage_without'], data, -1); h += '
'; var combat = data.combat || {}; var combatDescs = Array.isArray(combat.combat_descriptions) ? combat.combat_descriptions : []; var cdgid = 'cdg_combat'; var hasCD = combatDescs.length > 0; h += ''; h += ''; return h; } function renderMobTaskCard(data) { var sec = MOB_SECTIONS.find(function(s) { return s.id === 'task'; }); if (!sec) return ''; var byKey = {}; sec.fields.forEach(function(f) { byKey[f[0]] = f; }); var h = '

Task mobs can be attacked but do not fight back. The Verb and Progress Noun appear in combat messages. Complete Message prints when the task is finished.

'; h += '
'; h += renderMobField(sec, byKey['verb'], data, -1); h += renderMobField(sec, byKey['progress_noun'], data, -1); h += '
'; h += renderMobField(sec, byKey['complete_message'], data, -1, null, 'width:100%'); return h; } // setupMobProtectedToggle hides the entire Combat card when Protected is checked. function setupMobProtectedToggle() { var sec = MOB_SECTIONS.find(function(s) { return s.id === 'core'; }); if (!sec) return; var protEl = document.getElementById(fieldID(sec, 'protected', -1)); if (!protEl) return; function apply() { var combatCard = document.querySelector('.obj-card[data-card="combat"]'); if (combatCard) { combatCard.style.display = protEl.checked ? 'none' : ''; } } if (!protEl._hasProtectedToggle) { protEl._hasProtectedToggle = true; protEl.addEventListener('change', apply); } apply(); } function applyTaskVisibility() { var isTask = mobData && mobData.combat && mobData.combat.kind === 'task'; var hidden = document.querySelectorAll('.mob-task-hide'); hidden.forEach(function(el) { el.style.display = isTask ? 'none' : ''; }); } function renderMobField(sec, f, data, idx, subPath, optStyle) { return ced_renderField(sec, f, data, idx, subPath, optStyle, cardPath, fieldID, getVal); } // ---- Interaction-style array rendering (On Kill) ---- function renderMobArraySection(sec, data) { if (sec.interactionStyle) { window._mobInterVis = window._mobInterVis || {}; if (!window._mobInterVis[sec.id]) window._mobInterVis[sec.id] = {}; return ie_renderArraySection({ sec: sec, data: data, visMap: window._mobInterVis[sec.id], showRemove: function(idx) { return 'mobRemoveArrayItem(\'' + sec.id + '\',' + idx + ')'; }, showField: function(secId, idx, key) { return 'mobShowInterField(\'' + secId + '\',' + idx + ',\'' + key + '\')'; }, hideField: function(secId, idx, key) { return 'mobHideInterField(\'' + secId + '\',' + idx + ',\'' + key + '\')'; }, addCondition: function(secId, idx) { return 'mobAddInterRootCondition(\'' + secId + '\',' + idx + ')'; }, addEffect: function(secId, idx) { return 'mobAddInterAction(\'' + secId + '\',' + idx + ')'; }, renderField: function(sec, f, data, idx, optStyle) { return renderMobField(sec, f, data, idx, null, optStyle); }, onAdd: function() { return 'mobAddArrayItem(\'' + sec.id + '\')'; }, }); } return ''; } function mobAddArrayItem(cardID) { var sec = MOB_SECTIONS.find(function(s) { return s.id === cardID; }); if (!sec || !sec.isArray) return; var arr = mobData[sec.path] || []; var empty = {}; sec.fields.forEach(function(f) { empty[f[0]] = f[2] === 'checkbox' ? false : ''; }); arr.push(empty); mobData[sec.path] = arr; renderCurrent(); } function mobRemoveArrayItem(cardID, idx) { var sec = MOB_SECTIONS.find(function(s) { return s.id === cardID; }); if (!sec || !sec.isArray) return; ced_syncDOMToData(mobData); var arr = mobData[sec.path] || []; arr.splice(idx, 1); renderMobEditor(mobID, mobData); } function mobShowInterField(secId, idx, key) { window._mobInterVis = window._mobInterVis || {}; ie_showField(window._mobInterVis, secId, idx, key); renderCurrent(); } function mobAddInterRootCondition(secId, idx) { var sec = MOB_SECTIONS.find(function(s) { return s.id === secId; }); if (!sec) return; ie_addRootConditionGroup(sec.path, idx); } function mobAddInterAction(secId, idx) { var sec = MOB_SECTIONS.find(function(s) { return s.id === secId; }); if (!sec) return; ie_addEffectToInteraction(sec.path, idx); } function mobHideInterField(secId, idx, key) { window._mobInterVis = window._mobInterVis || {}; ie_hideField(window._mobInterVis, secId, idx, key); var sec = MOB_SECTIONS.find(function(s) { return s.id === secId; }); var secPath = sec ? sec.path : secId; ced_syncDOMToData(mobData); ie_syncAllInteractions(mobData, MOB_SECTIONS); if (mobData[secPath] && mobData[secPath][idx]) { if (key === 'condition' || key === 'action') { delete mobData[secPath][idx][key]; } else { mobData[secPath][idx][key] = ''; } } renderMobEditor(mobID, mobData); } // ---- Subtable rendering ---- function renderMobSubField(sec, stKey, idx, f, val) { var fid = fieldIDSub(sec, stKey, idx, 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'; var defaultPh = ''; if (f[3]) defaultPh = ' placeholder="' + escAttr(f[3]) + '"'; var dp = cardPath(sec) + '.' + stKey + '[' + idx + '].' + f[0]; var dpAttr = ' data-field-path="' + escAttr(dp) + '"'; if (f[2] === 'search') { var entity = f[6] || 'items'; var ph = f[3] ? ' placeholder="' + escAttr(f[3]) + '"' : ' placeholder="Search ' + entity + '..."'; return ''; } if (f[2] === 'checkbox') { return ''; } return ''; } function renderMobSubtable(sec, st, data) { var p = cardPath(sec); var sectionRoot = p ? (mobData[p] || {}) : mobData; var raw = sectionRoot[st.key]; var arr; if (st.single) { arr = raw ? [raw] : []; } else if (Array.isArray(raw)) { arr = raw; } else if (raw && typeof raw === 'object') { arr = [raw]; } else { arr = []; } if (st.table) { return renderMobTableSubtable(sec, st, arr); } if ((sec.id === 'drops' && st.key === 'loot') || (sec.id === 'steal' && st.key === 'drops')) { return renderMobLootSubtable(sec, st, arr); } var h = '
'; h += '
' + esc(st.label) + '
'; arr.forEach(function(item, idx) { h += '
'; st.fields.forEach(function(f) { var val = item[f[0]]; h += renderMobSubField(sec, st.key, idx, f, val); }); if (!st.single) { h += ''; } h += '
'; }); if (!st.single || arr.length === 0) { h += ''; } h += '
'; return h; } function renderMobDropsCard(data) { var h = ''; var drops = data.drops || {}; var hasRemains = drops.remains !== undefined && drops.remains !== null; if (hasRemains) { h += '
'; h += ''; h += ''; h += '
'; } else { h += ''; } var lootSec = MOB_SECTIONS.find(function(s) { return s.id === 'drops'; }); var lootSt = lootSec.subtables[0]; h += renderMobLootSubtable(lootSec, lootSt, drops.loot || []); return h; } function renderMobLootSubtable(sec, st, arr) { var byKey = {}; st.fields.forEach(function(f) { byKey[f[0]] = f; }); var isSteal = sec.id === 'steal'; var rollsField = ['quantity', 'Rolls', 'number', '', 'narrow']; var h = '
'; h += '
' + esc(st.label) + '
'; arr.forEach(function(item, idx) { var kind = item.table ? 'table' : (item.item_id ? 'item' : (item._kind || 'item')); h += '
'; if (kind === 'table') { h += renderMobSubField(sec, st.key, idx, byKey['table'], item.table); h += renderMobSubField(sec, st.key, idx, byKey['weight'], item.weight); h += renderMobSubField(sec, st.key, idx, isSteal ? byKey['quantity'] : rollsField, item.quantity); } else { h += renderMobSubField(sec, st.key, idx, byKey['item_id'], item.item_id); h += renderMobSubField(sec, st.key, idx, byKey['weight'], item.weight); h += renderMobSubField(sec, st.key, idx, byKey['quantity'], item.quantity); } h += ''; h += '
'; }); h += '
'; h += ''; h += ''; h += '
'; h += '
'; h += '
'; return h; } function mobTableColWidth(f) { return Math.max(60, esc(f[1]).length * 8 + 20); } function renderMobTableSubtable(sec, st, arr) { var h = '
'; h += '
'; st.fields.forEach(function(f) { if (f[2] === 'search') { h += '
' + esc(f[1]) + '
'; } else { h += '
' + esc(f[1]) + '
'; } }); h += '
'; h += '
'; arr.forEach(function(item, idx) { h += '
'; st.fields.forEach(function(f) { var fid = fieldIDSub(sec, st.key, idx, f[0]); var val = item[f[0]]; var dp = cardPath(sec) + '.' + st.key + '[' + idx + '].' + f[0]; if (f[2] === 'search') { var entity = f[6] || 'items'; var sval = (val === undefined || val === null) ? '' : String(val); h += '
'; } else { var hasPh = !!f[3]; var nval; if (val === undefined || val === null || val === '') nval = ''; else if (val === 0 && hasPh) nval = ''; else nval = String(val); var ph = hasPh ? ' placeholder="' + escAttr(f[3]) + '"' : ''; h += '
' + '' + '
'; } }); h += '
'; h += '
'; }); h += '
'; h += ''; return h; } // ---- Card toggle / section add-remove ---- function toggleMobCard(id) { expandedCards[id] = expandedCards[id] === false ? true : false; renderCurrent(); } function toggleMobCardWidth(id) { cardWidths[id] = !cardWidths[id]; renderCurrent(); } function removeMobSection(id) { var sec = MOB_SECTIONS.find(function(s) { return s.id === id; }); if (!sec) return; ced_syncDOMToData(mobData); if (sec.path) { delete mobData[sec.path]; } else { if (sec.fields) sec.fields.forEach(function(f) { delete mobData[f[0]]; }); if (sec.inline) sec.inline.forEach(function(il) { if (il.key) delete mobData[il.key]; }); if (sec.subtables) sec.subtables.forEach(function(st) { delete mobData[st.key]; }); } if (id === 'task' && mobData.combat && mobData.combat.kind === 'task') { mobData.combat.kind = 'combat'; } delete addedSections[id]; renderMobEditor(mobID, mobData); } function mobAddSection() { var sel = $('#addSectionSelect'); var id = sel.value; if (!id) return; var sec = MOB_SECTIONS.find(function(s) { return s.id === id; }); if (!sec) return; if (sec.path) { if (sec.isArray) { mobData[sec.path] = []; } else if (id === 'combat') { mobData[sec.path] = { kind: 'combat', aggressive: false, attack_types: ['crush'], respawn_ticks: 30, stats: { hp: 1, attack: 1, strength: 1, defense: 1, speed: 5, max_melee_hit: 0, max_ranged_hit: 0, max_science_hit: 0, bonuses: {attack_bonus:0, strength_bonus:0, science_bonus:0, science_percent_bonus:0, ranged_bonus:0, ranged_strength_bonus:0}, defenses: {stab_defense:0, slash_defense:0, crush_defense:0, science_defense:0, ranged_defense:0, weakness_percent:0} }, size: 'small' }; } else if (id === 'drops') { mobData[sec.path] = {loot:[]}; } else if (id === 'talk') { mobData[sec.path] = {nodes:{start:{messages:[],options:[]}}}; } else if (id === 'shop') { mobData[sec.path] = {buy_percentage:40, change_percentage:3, buys_anything:true, items:[]}; } else if (id === 'steal') { mobData[sec.path] = {drops: []}; } else { mobData[sec.path] = {}; } } addedSections[id] = true; expandedCards[id] = true; renderCurrent(); } // ---- Subtable row add/remove ---- function addMobSubRow(cardID, subKey) { var sec = MOB_SECTIONS.find(function(s) { return s.id === cardID; }); if (!sec) return; ced_addSubRow(sec, subKey, mobData, cardPath); renderCurrent(); } function removeMobSubRow(cardID, subKey, idx) { var sec = MOB_SECTIONS.find(function(s) { return s.id === cardID; }); if (!sec) return; ced_syncDOMToData(mobData); ced_removeSubRow(sec, subKey, idx, mobData, cardPath); renderMobEditor(mobID, mobData); } function addMobLootRow(cardID, subKey, kind) { var sec = MOB_SECTIONS.find(function(s) { return s.id === cardID; }); if (!sec) return; var p = cardPath(sec); var root = p ? (mobData[p] || (mobData[p] = {})) : mobData; if (!Array.isArray(root[subKey])) root[subKey] = []; if (kind === 'table') { root[subKey].push({table:'', weight:0, quantity:0, _kind:'table'}); } else { root[subKey].push({item_id:'', weight:0, quantity:0, _kind:'item'}); } renderCurrent(); } function addCombatDescRow(cdgid) { var list = document.querySelector('#' + cdgid + ' .obj-combat-desc-list'); if (!list) return; var row = document.createElement('div'); row.className = 'obj-combat-desc-row'; row.style.cssText = 'display:flex;align-items:center;gap:4px;margin-bottom:4px'; row.innerHTML = ''; list.appendChild(row); } // ---- Save ---- function saveMob() { if (!ced_validateFields(MOB_SECTIONS, fieldID, fieldIDSub)) return; ie_syncAllInteractions(mobData, MOB_SECTIONS); var out = {}; MOB_SECTIONS.forEach(function(sec) { if (sec.id === 'talk') { if (mobData.talk) out.talk = mobData.talk; return; } 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]] = ced_collectFieldValue(el, f[2]); }); if (sec.interactionStyle) { var dataArr = mobData[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 (Array.isArray(dataItem.steps)) { item.steps = dataItem.steps.map(ie_cleanStep).filter(function(s) { return s && Object.keys(s).length > 0; }); } } if (Object.keys(item).length > 0) arr.push(item); }); if (arr.length > 0) out[sec.path] = arr; return; } var sectionObj = {}; var hasValues = sec.always; if (sec.fields) { hasValues = ced_saveSectionFields(sec, sectionObj, cardPath, fieldID) || hasValues; } if (sec.id === 'combat') { sectionObj['kind'] = collectMobKind(); hasValues = true; if (sectionObj['kind'] !== 'task' && document.getElementById('mob_atktype')) { sectionObj['attack_types'] = ced_collectAtkType('mob_atktype'); } if (sectionObj['kind'] === 'task') { var taskStrip = ['aggressive', 'stats.attack','stats.strength','stats.ranged','stats.science', 'stats.max_melee_hit','stats.max_ranged_hit','stats.max_science_hit', 'stats.bonuses.attack_bonus','stats.bonuses.strength_bonus','stats.bonuses.science_bonus', 'stats.bonuses.science_percent_bonus','stats.bonuses.ranged_bonus','stats.bonuses.ranged_strength_bonus']; taskStrip.forEach(function(k) { delete sectionObj[k]; }); } var descRows = document.querySelectorAll('.obj-combat-desc'); if (descRows.length > 0) { var descs = []; descRows.forEach(function(inp) { var v = inp.value.trim(); if (v) descs.push(v); }); if (descs.length > 0) { sectionObj['combat_descriptions'] = descs; } } } if (sec.subtables) { sec.subtables.forEach(function(st) { if (ced_saveSubtable(sec, st, sectionObj, cardPath, fieldIDSub)) 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; } } }); } if (sec.always) { Object.keys(sectionObj).forEach(function(k) { out[k] = sectionObj[k]; }); } else if (hasValues && sec.path) { out[sec.path] = ced_unflattenObj(sectionObj); } else if (hasValues && !sec.path) { Object.keys(sectionObj).forEach(function(k) { out[k] = sectionObj[k]; }); } }); ced_cleanOutput(out); out.id = mobID; API.put('/api/mobs/' + encodeURIComponent(mobID), out).then(function(resp) { notify('Mob ' + mobID + ' saved', 'success'); updateUndoBar(); if (resp && resp._raw) { mobData._raw = resp._raw; var pre = document.querySelector('#tabYaml pre'); if (pre) pre.textContent = mobData._raw; } }).catch(function(e) { notify('Save failed: ' + e.message, 'error'); }); } // ---- Duplicate ---- function duplicateMob() { var baseID = mobID + '_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(mobData)); delete copy._raw; delete copy._path; copy.id = newID; var dir = ''; if (mobData._path) { var parts = mobData._path.split('/'); var parentDir = parts[parts.length - 2]; if (parentDir !== 'mobs') dir = parentDir; } API.post('/api/mobs', {id: newID}).then(function() { var putOp = API.put('/api/mobs/' + encodeURIComponent(newID), copy); if (dir) { return putOp.then(function() { return API.post('/api/mobs/move', {id: newID, dir: dir}); }); } return putOp; }).then(function() { notify('Duplicated as ' + newID, 'success'); updateUndoBar(); loadList(); loadMob(newID); }).catch(function(e) { notify('Duplicate failed: ' + e.message, 'error'); }); } // ---- Delete ---- function deleteMob() { if (!confirm('Delete mob "' + mobID + '"?')) return; API.del('/api/mobs/' + encodeURIComponent(mobID)).then(function() { notify('Mob ' + mobID + ' deleted', 'success'); updateUndoBar(); mobID = null; currentID = null; mobData = null; $('#editorMain').innerHTML = '

Deleted

'; loadList(); }).catch(function(e) { notify('Delete failed: ' + e.message, 'error'); }); } function setupMobDropViews() { var dropsCard = document.querySelector('.obj-card[data-card="drops"]'); if (dropsCard) { DropView.mount(dropsCard.querySelector('.obj-card-body'), function() { ced_syncDOMToData(mobData); var loot = (mobData.drops && mobData.drops.loot) || []; return loot.filter(function(e) { return e.item_id || e.table; }); }); } var stealCard = document.querySelector('.obj-card[data-card="steal"]'); if (stealCard) { DropView.mount(stealCard.querySelector('.obj-card-body'), function() { ced_syncDOMToData(mobData); var drops = (mobData.steal && mobData.steal.drops) || []; return drops.filter(function(e) { return e.item_id || e.table; }); }); } DropView.setupAllInter(); } // ---- editor.js compatibility ---- function loadItem(id) { loadMob(id); } function createNew() { var name = prompt('Mob ID:'); if (!name) return; API.post('/api/mobs', {id: name}).then(function() { notify('Mob ' + name + ' created', 'success'); updateUndoBar(); loadList(); loadMob(name); }).catch(function(e) { notify('Create failed: ' + e.message, 'error'); }); }