';
html += '';
html += '';
html += '';
html += '';
html += '
';
$('#editorMain').innerHTML = html;
ced_setupSearchFields();
setupMobProtectedToggle();
MOB_SECTIONS.forEach(function(sec) {
if (sec.id === 'talk' && data.talk && data.talk.nodes) {
renderTalkTree(data);
}
});
}
function renderCurrent() {
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.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 += renderMobField(sec, byKey['unique'], data, -1);
h += renderMobField(sec, byKey['protected'], data, -1);
h += '
';
return h;
}
// normalizeAttackTypes coerces a mob attack_types value (array) into an array of
// strings, tolerating a legacy scalar string for display only.
function normalizeAttackTypes(v) {
if (Array.isArray(v)) return v.slice();
if (typeof v === 'string' && v) return [v];
return [];
}
// renderMobAttackTypeWidget renders the Atk Type selector: one required melee
// type (radio: stab/slash/crush) plus optional ranged/science (checkboxes).
function renderMobAttackTypeWidget(data) {
var combat = data.combat || {};
var types = normalizeAttackTypes(combat.attack_types);
var melee = types.filter(function(t) { return t === 'stab' || t === 'slash' || t === 'crush'; })[0] || 'crush';
var hasRanged = types.indexOf('ranged') >= 0;
var hasScience = types.indexOf('science') >= 0;
var h = '
';
h += 'Atk Type';
h += '
';
['stab','slash','crush'].forEach(function(m) {
h += '';
});
h += '
';
h += '
';
h += '';
h += '';
h += '
';
h += '
';
return h;
}
// collectMobAttackType reads the Atk Type widget into an array: the selected
// melee type first, then any optional ranged/science types.
function collectMobAttackType() {
var melee = 'crush';
var r = document.querySelector('input[name="mob_atktype_melee"]:checked');
if (r) melee = r.value;
var out = [melee];
var rng = document.getElementById('mob_atktype_ranged');
var sci = document.getElementById('mob_atktype_science');
if (rng && rng.checked) out.push('ranged');
if (sci && sci.checked) out.push('science');
return out;
}
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 += renderMobField(sec, byKey['kind'], data, -1);
h += renderMobField(sec, byKey['aggressive'], data, -1);
h += renderMobField(sec, byKey['respawn_ticks'], data, -1);
h += '
';
h += renderMobAttackTypeWidget(data);
h += '
';
h += 'Stats';
h += '
';
var statKeys = ['stats.hp','stats.attack','stats.strength','stats.defense','stats.ranged','stats.science'];
statKeys.forEach(function(k) { h += renderMobField(sec, byKey[k], data, -1); });
h += '
';
h += '
';
h += renderMobField(sec, byKey['stats.speed'], data, -1);
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 += '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 += '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 += '
';
sec.inline.forEach(function(il) {
h += ced_renderTags(sec, il, data);
});
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(mobFieldID(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 renderMobField(sec, f, data, idx, subPath, optStyle) {
return ced_renderField(sec, f, data, idx, subPath, optStyle, mobCardPath, mobFieldID, mobGetVal);
}
// ---- Subtable rendering ----
function renderMobSubField(sec, stKey, idx, f, val) {
var fid = mobFieldIDSub(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]) + '"';
if (f[2] === 'search') {
var entity = f[6] || 'items';
var ph = f[3] ? ' placeholder="' + escAttr(f[3]) + '"' : ' placeholder="Search ' + entity + '..."';
var dp = mobCardPath(sec) + '.' + stKey + '[' + idx + '].' + f[0];
return '';
}
if (f[2] === 'checkbox') {
return '';
}
return '';
}
function renderMobSubtable(sec, st, data) {
var p = mobCardPath(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') {
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 renderMobLootSubtable(sec, st, arr) {
var byKey = {};
st.fields.forEach(function(f) { byKey[f[0]] = f; });
var rollsField = ['quantity', 'Rolls', 'number', '', 'narrow'];
var h = '