From 93a412aed2b11406399504c81d6d19ed1341908a Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Mon, 13 Jul 2026 15:07:28 -0400 Subject: feat(admin): hide irrelevant combat stats for task mobs, show relevant combat stats for hazards --- internal/admin/static/mobeditor.js | 162 ++++++++++++++++++++----------------- 1 file changed, 89 insertions(+), 73 deletions(-) (limited to 'internal/admin/static/mobeditor.js') diff --git a/internal/admin/static/mobeditor.js b/internal/admin/static/mobeditor.js index 3934fcd..10b256e 100644 --- a/internal/admin/static/mobeditor.js +++ b/internal/admin/static/mobeditor.js @@ -22,7 +22,6 @@ var MOB_SECTIONS = [ id: 'combat', label: 'Combat Stats', path: 'combat', detect: function(d) { return d.combat; }, fields: [ - ['kind', 'Kind', 'select', 'combat|task'], ['aggressive', 'Aggressive', 'checkbox'], ['respawn_ticks', 'Respawn Ticks', 'number', '30', 'narrow'], ['stats.hp', 'HP', 'number', '10', 'narrow'], @@ -46,7 +45,7 @@ var MOB_SECTIONS = [ ['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', '|bio|eco|hydro|solar|chaos'], + ['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', 'e.g. stake', '', null, 'items'], @@ -211,6 +210,7 @@ function renderMobEditor(id, data) { $('#editorMain').innerHTML = html; ced_setupSearchFields(); setupMobProtectedToggle(); + applyTaskVisibility(); MOB_SECTIONS.forEach(function(sec) { if (sec.id === 'talk' && data.talk && data.talk.nodes) { @@ -248,6 +248,8 @@ function renderMobCard(sec, data) { 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) { @@ -305,43 +307,21 @@ function renderMobCoreCard(data) { 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 (pill: stab/slash/crush) plus optional ranged/science (checkboxes). -function renderMobAttackTypeWidget(data) { +function renderMobKindToggle(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 meleeOpts = ['stab','slash','crush']; - var activeIdx = meleeOpts.indexOf(melee); - if (activeIdx < 0) activeIdx = 2; - + var kind = combat.kind === 'task' ? 'task' : 'combat'; + var activeC = kind === 'combat'; var h = '
'; - h += 'Atk Type'; - h += '
'; - meleeOpts.forEach(function(t, i) { - h += ''; - }); - h += '
'; - h += '
'; - h += ''; - h += ''; - h += '
'; - h += '
'; + h += 'Kind'; + h += '
'; + h += ''; + h += ''; + h += '
'; return h; } -function mobAtkPillSelect(val, btn) { - var pill = document.getElementById('mob_atktype_pill'); +function mobKindPillSelect(val, btn) { + var pill = document.getElementById('mob_kind_pill'); if (!pill) return; var opts = pill.querySelectorAll('.seg-opt'); var idx = -1; @@ -354,23 +334,25 @@ function mobAtkPillSelect(val, btn) { } }); 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(); } -// 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 pill = document.getElementById('mob_atktype_pill'); - if (pill) { - var active = pill.querySelector('.seg-opt.seg-active'); - if (active) melee = active.getAttribute('data-v'); - } - 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 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) { @@ -379,28 +361,43 @@ function renderMobCombatCard(data) { 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 += renderMobKindToggle(data); h += renderMobField(sec, byKey['respawn_ticks'], data, -1); + h += '
'; + h += renderMobField(sec, byKey['aggressive'], data, -1); + h += '
'; h += '
'; - h += renderMobAttackTypeWidget(data); + h += '
'; + h += ced_renderAtkTypeWidget(data, 'mob_atktype', 'ced_atkPillSelect'); + h += '
'; 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 += 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 += '
'; @@ -409,6 +406,7 @@ function renderMobCombatCard(data) { bonusKeys.forEach(function(k) { h += renderMobField(sec, byKey[k], data, -1); }); h += '
'; h += '
'; + h += '
'; h += '
'; h += 'Defenses'; @@ -438,6 +436,20 @@ function renderMobCombatCard(data) { 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'; }); @@ -457,6 +469,12 @@ function setupMobProtectedToggle() { 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); } @@ -728,6 +746,9 @@ function removeMobSection(id) { 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); } @@ -805,22 +826,6 @@ function addMobLootRow(cardID, subKey, kind) { renderCurrent(); } -// ---- Nested object helpers ---- - -function unflattenObj(obj) { - var result = {}; - Object.keys(obj).forEach(function(key) { - var parts = key.split('.'); - var current = result; - for (var i = 0; i < parts.length - 1; i++) { - if (!current[parts[i]]) current[parts[i]] = {}; - current = current[parts[i]]; - } - current[parts[parts.length - 1]] = obj[key]; - }); - return result; -} - // ---- Save ---- function saveMob() { @@ -874,9 +879,20 @@ function saveMob() { hasValues = ced_saveSectionFields(sec, sectionObj, cardPath, fieldID) || hasValues; } - if (sec.id === 'combat' && document.getElementById('mob_atktype_pill')) { - sectionObj['attack_types'] = collectMobAttackType(); + 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]; }); + } } if (sec.subtables) { @@ -901,7 +917,7 @@ function saveMob() { if (sec.always) { Object.keys(sectionObj).forEach(function(k) { out[k] = sectionObj[k]; }); } else if (hasValues && sec.path) { - out[sec.path] = unflattenObj(sectionObj); + out[sec.path] = ced_unflattenObj(sectionObj); } else if (hasValues && !sec.path) { Object.keys(sectionObj).forEach(function(k) { out[k] = sectionObj[k]; }); } -- cgit v1.2.3