diff options
| author | historia <[not public]> | 2026-07-01 19:41:34 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-07-01 19:41:34 -0400 |
| commit | 8eec7b75ff9c8c368b252373db45fb891732d2ca (patch) | |
| tree | 528ab3f235e3082a51b8bd6c8e5ae269c41b4f2b /internal/admin | |
| parent | 78b522d57a9ebc691e075523910d7cdaa06b6493 (diff) | |
| download | thehouseoficarus-8eec7b75ff9c8c368b252373db45fb891732d2ca.tar.gz | |
mob combat stats overhauled and standardized
Diffstat (limited to 'internal/admin')
| -rw-r--r-- | internal/admin/static/mobeditor.js | 261 | ||||
| -rw-r--r-- | internal/admin/static/objecteditor.js | 6 |
2 files changed, 139 insertions, 128 deletions
diff --git a/internal/admin/static/mobeditor.js b/internal/admin/static/mobeditor.js index 9928adb..3d6f4d4 100644 --- a/internal/admin/static/mobeditor.js +++ b/internal/admin/static/mobeditor.js @@ -12,66 +12,60 @@ var MOB_SECTIONS = [ ['description', 'Description', 'textarea', 'A lanky man in a wrinkled grey uniform.'], ['unique', 'Unique', 'checkbox'], ['protected', 'Protected', 'checkbox'], - ['aggressive', 'Aggressive', 'checkbox'], - ['kind', 'Kind', 'select', 'combat|task'], - ['respawn_ticks', 'Respawn Ticks', 'number', '', 'narrow'], ], inline: [ {label:'Idle Descriptions', key:'idle_descriptions', type:'tags'}, - {label:'Combat Descriptions', key:'combat_descriptions', type:'tags'}, ] }, { - id: 'combat', label: 'Combat Stats', - detect: function(d) { return d.attack || d.strength || d.defense || d.hp; }, + id: 'combat', label: 'Combat Stats', path: 'combat', + detect: function(d) { return d.combat; }, fields: [ - ['attack', 'Attack', 'number', '', 'narrow'], - ['strength', 'Strength', 'number', '', 'narrow'], - ['defense', 'Defense', 'number', '', 'narrow'], - ['hp', 'HP', 'number', '', 'narrow'], - ['ranged', 'Ranged', 'number', '', 'narrow'], - ['science', 'Science', 'number', '', 'narrow'], - ['speed', 'Speed', 'number', '', 'narrow'], + ['kind', 'Kind', 'select', 'combat|task'], + ['stats.hp', 'HP', 'number', '', 'narrow'], + ['stats.attack', 'Attack', 'number', '', 'narrow'], + ['stats.strength', 'Strength', 'number', '', 'narrow'], + ['stats.defense', 'Defense', 'number', '', 'narrow'], + ['stats.ranged', 'Ranged', 'number', '', 'narrow'], + ['stats.science', 'Science', 'number', '', 'narrow'], + ['stats.attack_type', 'Atk Type', 'select', 'stab|slash|crush|ranged|science'], + ['stats.speed', 'Speed', 'number', '', 'narrow'], + ['stats.aggressive', 'Aggressive', 'checkbox'], + ['stats.respawn_ticks', 'Respawn Ticks', 'number', '', 'narrow'], + ['stats.bonuses.attack_bonus', 'Atk Bonus', 'number', '', 'narrow'], + ['stats.bonuses.strength_bonus', 'Str Bonus', 'number', '', 'narrow'], + ['stats.bonuses.science_bonus', 'Sci Bonus', 'number', '', 'narrow'], + ['stats.bonuses.science_percent_bonus', 'Sci % Bonus', 'number', '', 'narrow'], + ['stats.bonuses.ranged_bonus', 'Rng Bonus', 'number', '', 'narrow'], + ['stats.bonuses.ranged_strength_bonus', 'Rng Str Bonus', 'number', '', 'narrow'], + ['stats.defenses.stab_defense', 'Stab Def', 'number', '', 'narrow'], + ['stats.defenses.slash_defense', 'Slash Def', 'number', '', 'narrow'], + ['stats.defenses.crush_defense', 'Crush Def', 'number', '', 'narrow'], + ['stats.defenses.science_defense', 'Sci Def', 'number', '', 'narrow'], + ['stats.defenses.ranged_defense', 'Rng Def', 'number', '', 'narrow'], + ['stats.defenses.weakness', 'Weakness', 'text'], + ['assassin_level', 'Assassin Lvl', 'number', '', 'narrow'], + ['finishing_blow', 'Finishing Blow', 'search', '', '', null, 'items'], + ['damage_without', 'Damage Without', 'search', '', '', null, 'items'], + ['size', 'Size', 'text'], ], inline: [ - {label:'Bonuses', path:'bonuses', fields:[ - ['attack_bonus', 'Atk Bonus', 'number', '', 'narrow'], - ['strength_bonus', 'Str Bonus', 'number', '', 'narrow'], - ['attack_type', 'Atk Type', 'select', 'stab|slash|crush|ranged|science'], - ]}, - {label:'Defenses', path:'defenses', fields:[ - ['stab_defense', 'Stab', 'number', '', 'narrow'], - ['slash_defense', 'Slash', 'number', '', 'narrow'], - ['crush_defense', 'Crush', 'number', '', 'narrow'], - ['science_defense', 'Science', 'number', '', 'narrow'], - ['ranged_defense', 'Ranged', 'number', '', 'narrow'], - ['weakness', 'Weakness', 'text'], - ]}, - ] - }, - { - id: 'steal', label: 'Steal', - detect: function(d) { return d.steal_table || d.steal_level || d.steal_speed || d.steal_xp; }, - fields: [ - ['steal_table', 'Table ID', 'search', '', '', null, 'drops'], - ['steal_level', 'Level', 'number', '', 'narrow'], - ['steal_xp', 'XP', 'number', '', 'narrow'], - ['steal_speed', 'Speed', 'number', '', 'narrow'], + {label:'Combat Descriptions', key:'combat_descriptions', type:'tags'}, ] }, { - id: 'assassin', label: 'Assassin', - detect: function(d) { return d.assassin_level || d.finishing_blow || d.damage_without || d.size; }, + id: 'steal', label: 'Steal', path: 'steal', + detect: function(d) { return d.steal; }, fields: [ - ['assassin_level', 'Level', 'number', '', 'narrow'], - ['finishing_blow', 'Finishing Blow', 'text'], - ['damage_without', 'Damage Without', 'text'], - ['size', 'Size', 'text'], + ['table', 'Table ID', 'search', '', '', null, 'drops'], + ['level', 'Level', 'number', '', 'narrow'], + ['xp', 'XP', 'number', '', 'narrow'], + ['speed', 'Speed', 'number', '', 'narrow'], ] }, { - id: 'task', label: 'Task', - detect: function(d) { return d.kind === 'task'; }, + id: 'task', label: 'Task', path: 'task', + detect: function(d) { return d.task; }, fields: [ ['verb', 'Verb', 'text', 'work'], ['progress_noun', 'Progress Noun', 'text', 'construction'], @@ -140,8 +134,6 @@ function loadMob(id) { renderList(''); API.get('/api/mobs/' + encodeURIComponent(id)).then(function(data) { mobData = data; - // buys_anything defaults to true (Go treats absent as true); reflect that in - // the checkbox unless the shop explicitly opted out with false. if (mobData.shop && mobData.shop.buys_anything === undefined) { mobData.shop.buys_anything = true; } @@ -193,7 +185,6 @@ function renderMobEditor(id, data) { $('#editorMain').innerHTML = html; ced_setupSearchFields(); - setupMobKindChange(); setupMobProtectedToggle(); MOB_SECTIONS.forEach(function(sec) { @@ -225,6 +216,8 @@ function renderMobCard(sec, data) { if (sec.id === 'core') { h += renderMobCoreCard(data); + } else if (sec.id === 'combat') { + h += renderMobCombatCard(data); } else if (sec.subtables) { h += '<div class="obj-card-grid">'; sec.fields.forEach(function(f) { @@ -232,23 +225,6 @@ function renderMobCard(sec, data) { }); h += '</div>'; - if (sec.inline) { - sec.inline.forEach(function(il) { - if (il.type === 'tags') { - h += ced_renderTags(sec, il, data); - } else { - h += '<div class="obj-inline-group">'; - h += '<span class="obj-inline-label">' + esc(il.label) + '</span>'; - h += '<div class="obj-card-grid" style="margin-top:4px">'; - il.fields.forEach(function(f) { - h += renderMobField(sec, f, data, -1, il.path); - }); - h += '</div>'; - h += '</div>'; - } - }); - } - sec.subtables.forEach(function(st) { h += renderMobSubtable(sec, st, data); }); @@ -263,15 +239,6 @@ function renderMobCard(sec, data) { sec.inline.forEach(function(il) { if (il.type === 'tags') { h += ced_renderTags(sec, il, data); - } else { - h += '<div class="obj-inline-group">'; - h += '<span class="obj-inline-label">' + esc(il.label) + '</span>'; - h += '<div class="obj-card-grid" style="margin-top:4px">'; - il.fields.forEach(function(f) { - h += renderMobField(sec, f, data, -1, il.path); - }); - h += '</div>'; - h += '</div>'; } }); } @@ -297,35 +264,81 @@ function renderMobCoreCard(data) { sec.inline.forEach(function(il) { h += ced_renderTags(sec, il, data); }); - // Behavior fields (merged into Core). h += '<div class="obj-card-grid" style="margin-top:6px">'; h += renderMobField(sec, byKey['unique'], data, -1); h += renderMobField(sec, byKey['protected'], data, -1); - h += renderMobField(sec, byKey['aggressive'], data, -1); h += '</div>'; - h += '<div class="obj-card-grid" style="margin-top:6px">'; + return h; +} + +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 = '<div class="obj-card-grid" style="margin-top:6px">'; h += renderMobField(sec, byKey['kind'], data, -1); - h += renderMobField(sec, byKey['respawn_ticks'], data, -1); h += '</div>'; + + h += '<div class="obj-inline-group">'; + h += '<span class="obj-inline-label">Stats</span>'; + h += '<div class="obj-card-grid" style="margin-top:4px">'; + 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 += '</div>'; + h += '<div class="obj-card-grid" style="margin-top:4px">'; + var statKeys2 = ['stats.attack_type','stats.speed','stats.aggressive','stats.respawn_ticks']; + statKeys2.forEach(function(k) { h += renderMobField(sec, byKey[k], data, -1); }); + h += '</div>'; + h += '</div>'; + + h += '<div class="obj-inline-group">'; + h += '<span class="obj-inline-label">Bonuses</span>'; + h += '<div class="obj-card-grid" style="margin-top:4px">'; + 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 += '</div>'; + h += '</div>'; + + h += '<div class="obj-inline-group">'; + h += '<span class="obj-inline-label">Defenses</span>'; + h += '<div class="obj-card-grid" style="margin-top:4px">'; + 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 += '</div>'; + h += '<div class="obj-card-grid" style="margin-top:4px">'; + h += renderMobField(sec, byKey['stats.defenses.weakness'], data, -1); + h += '</div>'; + h += '</div>'; + + h += '<div class="obj-card-grid" style="margin-top:6px">'; + h += renderMobField(sec, byKey['assassin_level'], data, -1); + h += renderMobField(sec, byKey['size'], data, -1); + h += '</div>'; + h += '<div class="obj-card-grid" style="margin-top:4px">'; + h += renderMobField(sec, byKey['finishing_blow'], data, -1); + h += renderMobField(sec, byKey['damage_without'], data, -1); + h += '</div>'; + + sec.inline.forEach(function(il) { + h += ced_renderTags(sec, il, data); + }); return h; } -// setupMobProtectedToggle hides Aggressive, Respawn Ticks and Kind while -// Protected is checked (protected mobs never attack, respawn, or work). Toggles -// CSS only — no re-render — so other unsaved field edits are preserved. +// 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 hide = protEl.checked; - ['aggressive', 'respawn_ticks', 'kind'].forEach(function(key) { - var el = document.getElementById(mobFieldID(sec, key, -1)); - if (!el) return; - var field = el.closest('.obj-field'); - if (field) field.style.display = hide ? 'none' : ''; - }); + var combatCard = document.querySelector('.obj-card[data-card="combat"]'); + if (combatCard) { + combatCard.style.display = protEl.checked ? 'none' : ''; + } } if (!protEl._hasProtectedToggle) { protEl._hasProtectedToggle = true; @@ -399,10 +412,6 @@ function renderMobSubtable(sec, st, data) { return h; } -// renderMobLootSubtable renders the Drops loot list where each row is either an -// item drop (Item + Weight + Quantity) or a table drop (Table + Weight + Rolls). -// A table entry's "Rolls" is stored in the `quantity` field and means the number -// of independent draws from that table (see behavior.ResolveDropList). function renderMobLootSubtable(sec, st, arr) { var byKey = {}; st.fields.forEach(function(f) { byKey[f[0]] = f; }); @@ -433,13 +442,10 @@ function renderMobLootSubtable(sec, st, arr) { return h; } -// Column width (px) for a non-search field, sized to fit its header label. function mobTableColWidth(f) { return Math.max(60, esc(f[1]).length * 8 + 20); } -// renderMobTableSubtable renders a subtable in the ingredients-style table -// layout: a single column-header row (no card header) and one row per entry. function renderMobTableSubtable(sec, st, arr) { var h = '<div class="ingredients-table" style="margin-top:10px">'; h += '<div class="ingredients-header-row">'; @@ -465,8 +471,6 @@ function renderMobTableSubtable(sec, st, arr) { '<input id="' + fid + '" value="' + escAttr(sval) + '" class="search-input" data-search-type="' + entity + '" placeholder="Search ' + entity + '...">' + '<div class="search-results" style="display:none"></div></div></div></div>'; } else { - // Numeric column. Blank a 0/unset value only when the field has a - // placeholder, so the placeholder (e.g. "1000") shows through. var hasPh = !!f[3]; var nval; if (val === undefined || val === null || val === '') nval = ''; @@ -516,11 +520,23 @@ function mobAddSection() { if (!sec) return; if (sec.path) { - if (sec.id === 'drops') { + if (id === 'combat') { + mobData[sec.path] = { + kind: 'combat', + stats: { + hp: 1, attack: 1, strength: 1, defense: 1, + attack_type: 'crush', speed: 5, + aggressive: false, respawn_ticks: 30, + 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} + }, + size: 'small' + }; + } else if (id === 'drops') { mobData[sec.path] = {remains:'bones', loot:[]}; - } else if (sec.id === 'talk') { + } else if (id === 'talk') { mobData[sec.path] = {nodes:{start:{messages:[],options:[]}}}; - } else if (sec.id === 'shop') { + } else if (id === 'shop') { mobData[sec.path] = {buy_percentage:40, change_percentage:3, buys_anything:true, items:[]}; } else { mobData[sec.path] = {}; @@ -547,10 +563,6 @@ function removeMobSubRow(cardID, subKey, idx) { renderCurrent(); } -// addMobLootRow appends an item- or table-kind loot entry. The ephemeral _kind -// marker is only used for rendering fresh empty rows; it's never saved (save -// rebuilds rows from the visible inputs) and is re-inferred from item_id/table -// on reload. function addMobLootRow(kind) { if (!mobData.drops) mobData.drops = {}; if (!Array.isArray(mobData.drops.loot)) mobData.drops.loot = []; @@ -562,26 +574,20 @@ function addMobLootRow(kind) { renderCurrent(); } -// ---- Kind change handler (show/hide task fields) ---- - -function setupMobKindChange() { - var kindSel = document.getElementById(mobFieldID(MOB_SECTIONS.find(function(s){return s.id==='core';}), 'kind', -1)); - if (!kindSel) return; - kindSel.addEventListener('change', function() { - if (kindSel.value === 'task') { - mobData.kind = 'task'; - if (!mobData.verb) mobData.verb = ''; - if (!mobData.progress_noun) mobData.progress_noun = ''; - if (!mobData.complete_message) mobData.complete_message = ''; - addedSections['task'] = true; - expandedCards['task'] = true; - renderCurrent(); - } else { - mobData.kind = ''; - delete addedSections['task']; - 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 ---- @@ -611,13 +617,22 @@ function saveMob() { } if (sec.inline) { - if (ced_saveInlineFields(sec, sectionObj, mobCardPath, mobFieldID)) hasValues = true; + sec.inline.forEach(function(il) { + if (il.type === 'tags') { + var fid = mobFieldID(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] = sectionObj; + out[sec.path] = unflattenObj(sectionObj); } else if (hasValues && !sec.path) { Object.keys(sectionObj).forEach(function(k) { out[k] = sectionObj[k]; }); } diff --git a/internal/admin/static/objecteditor.js b/internal/admin/static/objecteditor.js index 4d02295..d45c26e 100644 --- a/internal/admin/static/objecteditor.js +++ b/internal/admin/static/objecteditor.js @@ -121,12 +121,8 @@ var SECTIONS = [ ['take_item', 'Take Item', 'text'], ['teleport', 'Teleport', 'number', '', 'narrow'], ['heal', 'Heal', 'number', '', 'narrow'], - ['cost', 'Cost', 'number', '', 'narrow'], - ['assign_task', 'Assign Task', 'checkbox'], - ['skip_task', 'Skip Task', 'checkbox'], - ['extend_task', 'Extend Task', 'checkbox'], + ['credits', 'Credits', 'number', '', 'narrow'], ['reputation_cost', 'Reputation Cost', 'number', '', 'narrow'], - ['sawmill', 'Sawmill', 'checkbox'], ['aps_node', 'APS Node', 'checkbox'], ] }, |
