From 8eec7b75ff9c8c368b252373db45fb891732d2ca Mon Sep 17 00:00:00 2001
From: historia <[not public]>
Date: Wed, 1 Jul 2026 19:41:34 -0400
Subject: mob combat stats overhauled and standardized
---
internal/admin/static/mobeditor.js | 261 ++++++++++++++++++----------------
internal/admin/static/objecteditor.js | 6 +-
2 files changed, 139 insertions(+), 128 deletions(-)
(limited to 'internal/admin/static')
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 += '
';
sec.fields.forEach(function(f) {
@@ -232,23 +225,6 @@ function renderMobCard(sec, data) {
});
h += '
';
- if (sec.inline) {
- sec.inline.forEach(function(il) {
- if (il.type === 'tags') {
- h += ced_renderTags(sec, il, data);
- } else {
- h += '';
- h += '
' + esc(il.label) + '';
- h += '
';
- il.fields.forEach(function(f) {
- h += renderMobField(sec, f, data, -1, il.path);
- });
- h += '
';
- h += '
';
- }
- });
- }
-
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 += '';
- h += '
' + esc(il.label) + '';
- h += '
';
- il.fields.forEach(function(f) {
- h += renderMobField(sec, f, data, -1, il.path);
- });
- h += '
';
- h += '
';
}
});
}
@@ -297,35 +264,81 @@ function renderMobCoreCard(data) {
sec.inline.forEach(function(il) {
h += ced_renderTags(sec, il, data);
});
- // Behavior fields (merged into Core).
h += '';
h += renderMobField(sec, byKey['unique'], data, -1);
h += renderMobField(sec, byKey['protected'], data, -1);
- h += renderMobField(sec, byKey['aggressive'], data, -1);
h += '
';
- h += '';
+ 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 = '
';
h += renderMobField(sec, byKey['kind'], data, -1);
- h += renderMobField(sec, byKey['respawn_ticks'], data, -1);
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 += '
';
+ h += '
';
+ var statKeys2 = ['stats.attack_type','stats.speed','stats.aggressive','stats.respawn_ticks'];
+ statKeys2.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 += '
';
+ 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 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 = '
';
h += '
';
} 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'],
]
},
--
cgit v1.2.3