From 7b5ed92e003d8a4bf9bdc69d43b7354eb2260dfa Mon Sep 17 00:00:00 2001
From: historia <[not public]>
Date: Sun, 5 Jul 2026 19:18:50 -0400
Subject: feat: duplicate items/obj/mobs from sidebar. item inputs fields
changed to search fields
---
internal/admin/static/dropeditor.js | 55 ++++++++++++++++++---
internal/admin/static/editor.js | 45 ++++++++++++++++++
internal/admin/static/hazardeditor.js | 63 ++++++++++++++++++++----
internal/admin/static/itemeditor.js | 90 +++++++++++++++++++++++++++--------
internal/admin/static/mobeditor.js | 81 ++++++++++++++++++-------------
internal/admin/static/moduleeditor.js | 55 ++++++++++++++++++---
internal/admin/static/objecteditor.js | 64 +++++++++++++++----------
internal/admin/static/techeditor.js | 71 +++++++++++++++++++++------
8 files changed, 409 insertions(+), 115 deletions(-)
(limited to 'internal')
diff --git a/internal/admin/static/dropeditor.js b/internal/admin/static/dropeditor.js
index c5da314..a6537e9 100644
--- a/internal/admin/static/dropeditor.js
+++ b/internal/admin/static/dropeditor.js
@@ -7,14 +7,14 @@ var DROP_SECTIONS = [
{
id: 'drops', label: 'Drops', always: true, path: 'drops', isArray: true,
fields: [
- ['item_id', 'Item ID', 'search', '', '', null, 'items'],
- ['table', 'Table', 'search', '', '', null, 'drops'],
- ['weight', 'Weight', 'number', '', 'narrow'],
+ ['item_id', 'Item ID', 'search', 'e.g. copper_ore', '', null, 'items'],
+ ['table', 'Table', 'search', 'e.g. gem_table', '', null, 'drops'],
+ ['weight', 'Weight', 'number', '10', 'narrow'],
['quantity', 'Quantity', 'number', '1', 'narrow'],
- ['level', 'Level', 'number', '', 'narrow'],
- ['xp', 'XP', 'number', '', 'narrow'],
+ ['level', 'Level', 'number', '1', 'narrow'],
+ ['xp', 'XP', 'number', '5', 'narrow'],
['depletes', 'Depletes', 'checkbox'],
- ['success_message', 'Success Message', 'text', '', 'wide'],
+ ['success_message', 'Success Message', 'text', 'You get some %n.', 'wide'],
]
}
];
@@ -75,6 +75,7 @@ function renderDropEditor(id, data) {
html += '
';
if (sec.fields) sec.fields.forEach(function(f) {
@@ -316,6 +320,41 @@ function renderCoreCard(data) {
return h;
}
+function renderFarmingCard(data) {
+ var sec = SECTIONS.find(function(s) { return s.id === 'farming'; });
+ var byKey = {};
+ sec.fields.forEach(function(f) { byKey[f[0]] = f; });
+ var h = '
';
+ h += renderField(sec, byKey['farm_patch_type'], data, -1);
+ h += renderField(sec, byKey['farm_product'], data, -1, null, 'flex:1');
+ h += '
';
+ h += '
';
+ h += renderField(sec, byKey['farm_plant_xp'], data, -1);
+ h += renderField(sec, byKey['farm_harvest_xp'], data, -1);
+ h += renderField(sec, byKey['farm_level'], data, -1);
+ h += '
';
+ h += '
';
+ h += renderField(sec, byKey['farm_stages'], data, -1);
+ h += renderField(sec, byKey['farm_min_yield'], data, -1);
+ h += renderField(sec, byKey['farm_max_yield'], data, -1);
+ h += '
';
+ return h;
+}
+
+function renderPotionsCard(data) {
+ var sec = SECTIONS.find(function(s) { return s.id === 'potions'; });
+ var byKey = {};
+ sec.fields.forEach(function(f) { byKey[f[0]] = f; });
+ var h = '
';
+ h += renderField(sec, byKey['potion_effect'], data, -1, null, 'flex:1');
+ h += '
';
+ h += '
';
+ h += renderField(sec, byKey['potion_bonus'], data, -1);
+ h += renderField(sec, byKey['potion_duration'], data, -1);
+ h += '
';
+ return h;
+}
+
// =========================================================================
// Equipment card (custom render with 3 stat blocks)
// =========================================================================
@@ -1551,8 +1590,21 @@ function duplicateItem() {
delete copy._path;
copy.id = newID;
+ var dir = '';
+ if (itemData._path) {
+ var parts = itemData._path.split('/');
+ var parentDir = parts[parts.length - 2];
+ if (parentDir !== 'items') dir = parentDir;
+ }
+
API.post('/api/items', {id: newID}).then(function() {
- return API.put('/api/items/' + encodeURIComponent(newID), copy);
+ var putOp = API.put('/api/items/' + encodeURIComponent(newID), copy);
+ if (dir) {
+ return putOp.then(function() {
+ return API.post('/api/items/move', {id: newID, dir: dir});
+ });
+ }
+ return putOp;
}).then(function() {
notify('Duplicated as ' + newID, 'success');
updateUndoBar();
diff --git a/internal/admin/static/mobeditor.js b/internal/admin/static/mobeditor.js
index debe7e0..c142fb4 100644
--- a/internal/admin/static/mobeditor.js
+++ b/internal/admin/static/mobeditor.js
@@ -24,34 +24,34 @@ var MOB_SECTIONS = [
fields: [
['kind', 'Kind', 'select', 'combat|task'],
['aggressive', 'Aggressive', 'checkbox'],
- ['respawn_ticks', 'Respawn Ticks', 'number', '', 'narrow'],
- ['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.speed', 'Speed', 'number', '', 'narrow'],
- ['stats.max_melee_hit', 'Max Melee Hit', 'number', '', 'narrow'],
- ['stats.max_ranged_hit', 'Max Ranged Hit', 'number', '', 'narrow'],
- ['stats.max_science_hit', 'Max Science Hit', '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'],
- ['stats.defenses.weakness_percent', 'Weakness %', 'number', '', 'narrow'],
- ['assassin_level', 'Assassin Lvl', 'number', '', 'narrow'],
- ['finishing_blow', 'Finishing Blow', 'search', '', '', null, 'items'],
- ['damage_without', 'Damage Without', 'search', '', '', null, 'items'],
- ['size', 'Size', 'text'],
+ ['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', 'text', 'e.g. crush'],
+ ['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'],
+ ['damage_without', 'Damage Without', 'search', 'e.g. holy_symbol', '', null, 'items'],
+ ['size', 'Size', 'text', 'small'],
],
inline: [
{label:'Combat Descriptions', key:'combat_descriptions', type:'tags'},
@@ -61,10 +61,10 @@ var MOB_SECTIONS = [
id: 'steal', label: 'Steal', path: 'steal',
detect: function(d) { return d.steal; },
fields: [
- ['table', 'Table ID', 'search', '', '', null, 'drops'],
- ['level', 'Level', 'number', '', 'narrow'],
- ['xp', 'XP', 'number', '', 'narrow'],
- ['speed', 'Speed', 'number', '', 'narrow'],
+ ['table', 'Table ID', 'search', 'e.g. gem_table', '', null, 'drops'],
+ ['level', 'Level', 'number', '1', 'narrow'],
+ ['xp', 'XP', 'number', '5', 'narrow'],
+ ['speed', 'Speed', 'number', '4', 'narrow'],
]
},
{
@@ -73,7 +73,7 @@ var MOB_SECTIONS = [
fields: [
['verb', 'Verb', 'text', 'work'],
['progress_noun', 'Progress Noun', 'text', 'construction'],
- ['complete_message', 'Complete Message', 'text'],
+ ['complete_message', 'Complete Message', 'text', 'The work is finished.'],
]
},
{
@@ -756,8 +756,21 @@ function duplicateMob() {
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() {
- return API.put('/api/mobs/' + encodeURIComponent(newID), copy);
+ 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();
diff --git a/internal/admin/static/moduleeditor.js b/internal/admin/static/moduleeditor.js
index 5bcb7bc..ac632e8 100644
--- a/internal/admin/static/moduleeditor.js
+++ b/internal/admin/static/moduleeditor.js
@@ -8,13 +8,13 @@ var MODULE_SECTIONS = [
{
id: 'core', label: 'Core', always: true,
fields: [
- ['name', 'Name', 'text'],
- ['level', 'Level', 'number', '', 'narrow'],
- ['max_hit', 'Max Hit', 'number', '', 'narrow'],
- ['base_xp', 'Base XP', 'number', '', 'narrow'],
+ ['name', 'Name', 'text', 'Fire Blast'],
+ ['level', 'Level', 'number', '20', 'narrow'],
+ ['max_hit', 'Max Hit', 'number', '10', 'narrow'],
+ ['base_xp', 'Base XP', 'number', '50', 'narrow'],
['category', 'Category', 'select', 'combat|utility|enchant|processing|transport'],
- ['element', 'Element', 'text'],
- ['destination', 'Destination', 'number', '', 'narrow'],
+ ['element', 'Element', 'text', 'e.g. fire'],
+ ['destination', 'Destination', 'number', '5', 'narrow'],
],
inline: [
{label:'Junk Cost', key:'junk_cost', type:'kv', valType:'number'},
@@ -100,6 +100,7 @@ function renderModuleEditor(id, data) {
html += '
';
html += '';
+ html += '';
html += '';
html += '';
html += '
';
@@ -321,3 +322,45 @@ function deleteModule() {
loadList();
}).catch(function(e) { notify('Delete failed: ' + e.message, 'error'); });
}
+
+function duplicateModule() {
+ var baseID = moduleID + '_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(moduleData));
+ delete copy._raw;
+ delete copy._path;
+ copy.id = newID;
+
+ var dir = '';
+ if (moduleData._path) {
+ var parts = moduleData._path.split('/');
+ var parentDir = parts[parts.length - 2];
+ if (parentDir !== 'modules') dir = parentDir;
+ }
+
+ API.post('/api/modules', {id: newID}).then(function() {
+ var putOp = API.put('/api/modules/' + encodeURIComponent(newID), copy);
+ if (dir) {
+ return putOp.then(function() {
+ return API.post('/api/modules/move', {id: newID, dir: dir});
+ });
+ }
+ return putOp;
+ }).then(function() {
+ notify('Duplicated as ' + newID, 'success');
+ updateUndoBar();
+ loadList();
+ loadModuleEditor(newID);
+ }).catch(function(e) { notify('Duplicate failed: ' + e.message, 'error'); });
+}
diff --git a/internal/admin/static/objecteditor.js b/internal/admin/static/objecteditor.js
index 5444985..7e2d04b 100644
--- a/internal/admin/static/objecteditor.js
+++ b/internal/admin/static/objecteditor.js
@@ -21,7 +21,7 @@ var SECTIONS = [
id: 'steal', label: 'Steal', path: 'steal',
detect: function(d) { return d.steal !== undefined; },
fields: [
- ['table', 'Table ID', 'search', '', '', null, 'drops'],
+ ['table', 'Table ID', 'search', 'e.g. gem_table', '', null, 'drops'],
['level', 'Level', 'number', '1', 'narrow'],
['xp', 'XP', 'number', '5', 'narrow'],
['speed', 'Speed', 'number', '4', 'narrow'],
@@ -64,16 +64,16 @@ var SECTIONS = [
id: 'use', label: 'Use (Station)', path: 'use',
detect: function(d) { return d.use; },
fields: [
- ['message', 'Message', 'text'],
- ['wait', 'Wait', 'number', '', 'narrow'],
- ['skill', 'Skill', 'text'],
- ['level', 'Level', 'number', '', 'narrow'],
- ['xp', 'XP', 'number', '', 'narrow'],
- ['fail_message', 'Fail Message', 'text'],
+ ['message', 'Message', 'text', 'You use the station...'],
+ ['wait', 'Wait', 'number', '4', 'narrow'],
+ ['skill', 'Skill', 'text', 'e.g. smithing'],
+ ['level', 'Level', 'number', '1', 'narrow'],
+ ['xp', 'XP', 'number', '25', 'narrow'],
+ ['fail_message', 'Fail Message', 'text', 'You fail to use the station.'],
],
inline: [
{label:'Success', path:'success', fields:[
- ['base', 'Base', 'number', '', 'narrow'], ['per_level', 'Per Lvl', 'number', '', 'narrow'], ['cap', 'Cap', 'number', '', 'narrow']
+ ['base', 'Base', 'number', '0.5', 'narrow'], ['per_level', 'Per Lvl', 'number', '0.01', 'narrow'], ['cap', 'Cap', 'number', '0.95', 'narrow']
]},
{label:'Consume', key:'consume', type:'kv', valType:'number'},
],
@@ -88,14 +88,13 @@ var SECTIONS = [
id: 'safespot', label: 'Safespot', path: 'safespot',
detect: function(d) { return d.safespot; },
fields: [
- ['tier', 'Tier', 'number', '', 'narrow'],
+ ['tier', 'Tier', 'number', '1', 'narrow'],
['max_block_size', 'Max Block Size', 'select', '|small|medium|large|massive'],
- ['max_occupants', 'Max Occupants', 'number', '', 'narrow'],
- ['unsafe_chance', 'Unsafe Chance', 'number', '', 'narrow'],
- ['decay_ticks', 'Decay Ticks', 'number', '', 'narrow'],
- ['decay_chance', 'Decay Chance', 'number', '', 'narrow'],
- ['respawn_on_hide', 'Respawn on Hide', 'checkbox'],
- ['respawn_ticks', 'Respawn Ticks', 'number', '', 'narrow'],
+ ['max_occupants', 'Max Occupants', 'number', '1', 'narrow'],
+ ['unsafe_chance', 'Unsafe Chance', 'number', '0.05', 'narrow'],
+ ['decay_ticks', 'Decay Ticks', 'number', '100', 'narrow'],
+ ['decay_chance', 'Decay Chance', 'number', '0.1', 'narrow'],
+ ['respawn_ticks', 'Respawn Ticks', 'number', '50', 'narrow'],
],
subtables: [
{label:'Levels', key:'levels', fields:[
@@ -107,24 +106,24 @@ var SECTIONS = [
id: 'use_interactions', label: 'Use Interactions', labelHint: '(what happens when you use items on this)', path: 'use_interactions', isArray: true, fullWidth: true,
detect: function(d) { return d.use_interactions && Array.isArray(d.use_interactions); },
fields: [
- ['item_id', 'Item ID', 'search', '', '', null, 'items'],
+ ['item_id', 'Item ID', 'search', 'e.g. keycard', '', null, 'items'],
['condition', 'Condition', 'json', 'e.g. {"flag":"my_flag"} or {"player_flag":"x","value":1} or {"all_of":[{"flag":"a"},{"has_item":"key"}]}'],
['action', 'Action', 'json', 'e.g. {"set_flags":{"my_flag":true},"message":"You activate it.","give_item":"gem"}'],
- ['message', 'Message', 'text'],
+ ['message', 'Message', 'text', 'You use the object...'],
]
},
{
id: 'on_look', label: 'On Look', path: 'on_look',
detect: function(d) { return d.on_look; },
fields: [
- ['set_flags', 'Set Flags', 'json'],
- ['set_player_flags', 'Set Player Flags', 'json'],
- ['give_item', 'Give Item', 'text'],
- ['take_item', 'Take Item', 'text'],
- ['teleport', 'Teleport', 'number', '', 'narrow'],
- ['heal', 'Heal', 'number', '', 'narrow'],
- ['credits', 'Credits', 'number', '', 'narrow'],
- ['reputation_cost', 'Reputation Cost', 'number', '', 'narrow'],
+ ['set_flags', 'Set Flags', 'json', '{"flag":"value"}'],
+ ['set_player_flags', 'Set Player Flags', 'json', '{"pflag":"value"}'],
+ ['give_item', 'Give Item', 'text', 'e.g. keycard'],
+ ['take_item', 'Take Item', 'text', 'e.g. bomb'],
+ ['teleport', 'Teleport', 'number', '5', 'narrow'],
+ ['heal', 'Heal', 'number', '10', 'narrow'],
+ ['credits', 'Credits', 'number', '100', 'narrow'],
+ ['reputation_cost', 'Reputation Cost', 'number', '1', 'narrow'],
['aps_node', 'APS Node', 'checkbox'],
]
},
@@ -1106,8 +1105,21 @@ function duplicateObject() {
delete copy._path;
copy.id = newID;
+ var dir = '';
+ if (objectData._path) {
+ var parts = objectData._path.split('/');
+ var parentDir = parts[parts.length - 2];
+ if (parentDir !== 'objects') dir = parentDir;
+ }
+
API.post('/api/objects', {id: newID}).then(function() {
- return API.put('/api/objects/' + encodeURIComponent(newID), copy);
+ var putOp = API.put('/api/objects/' + encodeURIComponent(newID), copy);
+ if (dir) {
+ return putOp.then(function() {
+ return API.post('/api/objects/move', {id: newID, dir: dir});
+ });
+ }
+ return putOp;
}).then(function() {
notify('Duplicated as ' + newID, 'success');
updateUndoBar();
diff --git a/internal/admin/static/techeditor.js b/internal/admin/static/techeditor.js
index ffb2b7b..8e18a7a 100644
--- a/internal/admin/static/techeditor.js
+++ b/internal/admin/static/techeditor.js
@@ -7,25 +7,25 @@ var TECH_SECTIONS = [
{
id: 'core', label: 'Core', always: true,
fields: [
- ['name', 'Name', 'text'],
- ['level', 'Level', 'number', '', 'narrow'],
- ['drain_rate', 'Drain Rate', 'number', '', 'narrow'],
- ['category', 'Category', 'text'],
- ['group', 'Group', 'text'],
+ ['name', 'Name', 'text', 'Protect from Melee'],
+ ['level', 'Level', 'number', '37', 'narrow'],
+ ['drain_rate', 'Drain Rate', 'number', '0.5', 'narrow'],
+ ['category', 'Category', 'text', 'protection'],
+ ['group', 'Group', 'text', 'melee'],
]
},
{
id: 'effects', label: 'Effects', always: true, path: 'effects',
fields: [
- ['accuracy_percent', 'Accuracy %', 'number', '', 'narrow'],
- ['strength_percent', 'Strength %', 'number', '', 'narrow'],
- ['defense_percent', 'Defense %', 'number', '', 'narrow'],
- ['ranged_percent', 'Ranged %', 'number', '', 'narrow'],
- ['science_percent', 'Science %', 'number', '', 'narrow'],
- ['damage_reduction', 'Damage Reduction', 'number', '', 'narrow'],
- ['hp_regen_multi', 'HP Regen Multi', 'number', '', 'narrow'],
- ['preserve_drain', 'Preserve Drain', 'number', '', 'narrow'],
- ['retribution_pct', 'Retribution %', 'number', '', 'narrow'],
+ ['accuracy_percent', 'Accuracy %', 'number', '5', 'narrow'],
+ ['strength_percent', 'Strength %', 'number', '5', 'narrow'],
+ ['defense_percent', 'Defense %', 'number', '5', 'narrow'],
+ ['ranged_percent', 'Ranged %', 'number', '5', 'narrow'],
+ ['science_percent', 'Science %', 'number', '5', 'narrow'],
+ ['damage_reduction', 'Damage Reduction', 'number', '40', 'narrow'],
+ ['hp_regen_multi', 'HP Regen Multi', 'number', '2', 'narrow'],
+ ['preserve_drain', 'Preserve Drain', 'number', '1', 'narrow'],
+ ['retribution_pct', 'Retribution %', 'number', '25', 'narrow'],
['protect_melee', 'Protect Melee', 'checkbox'],
['protect_ranged', 'Protect Ranged', 'checkbox'],
['protect_science', 'Protect Science', 'checkbox'],
@@ -89,6 +89,7 @@ function renderTechEditor(id, data) {
html += '
';
html += '';
+ html += '';
html += '';
html += '';
html += '
';
@@ -192,3 +193,45 @@ function deleteTech() {
loadList();
}).catch(function(e) { notify('Delete failed: ' + e.message, 'error'); });
}
+
+function duplicateTech() {
+ var baseID = techID + '_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(techData));
+ delete copy._raw;
+ delete copy._path;
+ copy.id = newID;
+
+ var dir = '';
+ if (techData._path) {
+ var parts = techData._path.split('/');
+ var parentDir = parts[parts.length - 2];
+ if (parentDir !== 'techs') dir = parentDir;
+ }
+
+ API.post('/api/techs', {id: newID}).then(function() {
+ var putOp = API.put('/api/techs/' + encodeURIComponent(newID), copy);
+ if (dir) {
+ return putOp.then(function() {
+ return API.post('/api/techs/move', {id: newID, dir: dir});
+ });
+ }
+ return putOp;
+ }).then(function() {
+ notify('Duplicated as ' + newID, 'success');
+ updateUndoBar();
+ loadList();
+ loadTechEditor(newID);
+ }).catch(function(e) { notify('Duplicate failed: ' + e.message, 'error'); });
+}
--
cgit v1.2.3