You can give this script a go, though it's not really tested, or polished, or much of anything. Usage is !copyRepSec --sel <character_id> --tar <character_id> --section <section name> [--name <section name attribute>] --sel is the source character id --tar is the target character(s), comma delimited if you want to use more than one --section is the section name, in between 'repeating' and the rowId, so for 5e things like attack, traits, inventory, spell-7 --name is for supplying the name attribute for the given section (for display purposes). This should be handled automatically for 5e, but for other sheets, if you were looking in a section where the row name was stored in repeating_shoulderpads_-abcdefgh1234567890_padsname , you would supply --name padsname For the 5e sheet, there's a shortcut section name, 'spells', which will automatically grab all 10 spell repeating sections. So the simplest way to run it for spells in 5e, is select the source character and type: !copyrepsec --sel @{selected|character_id} --tar @{target|character_id} --section spells Then click the buttons to copy each spell. The target id(s) are baked into the buttons, so you'll need to run the above macro again if you want to copy to a different sheet. Like I said, barely tested, so.... maybe make copies of sheets before trying it out :) The script: /* globals on, sendChat, getObj, findObjs, createObj */ on('ready', () => { const scriptName = 'copyRepSec'; const sectionNameAttributes = { spells: 'spellname', tool: 'toolname', inventory: 'itemname', attack: 'atkname', } const sectionShortcuts = { spells: ['spell-cantrip', 'spell-1', 'spell-2', 'spell-3', 'spell-4', 'spell-5', 'spell-6', 'spell-7', 'spell-8', 'spell-9'], } const rx = { id: /-[0-z_-]{19}/, strictId: /^-[0-z_-]{19}$/, rowId: /_(-[0-z-]{19})_/, repeatingSectionName: /(repeating_.+)_-[0-z_-]{19}/, } const buttonStyle = `background: none; display: inline-block; margin: 0.25rem 0.5rem 0.25rem 0; color: blue; border: 1px solid blue; border-radius: 4px;`; const attributeTasks = { spells: { spelloutput: { condition: (attrObj) => /attack/i.test(attrObj.get('current')), task: async (attr) => { // attr.setWithWorker({ current: 'SPELLCARD' }); // await timeout(1000); // attr.setWithWorker({ current: 'ATTACK' }); } }, ['details-flag']: { condition: () => true, task: async (attr) => { attr.setWithWorker({ current: 0 }) } } } } const timeout = async (ms) => new Promise((res) => setTimeout(()=>res(), ms)) const copyRepeatingRow = async (characterId, section, repeatingRowArray) => { const taskQueue = [], newRowId = generateUID().replace(/_/g, 'z'), taskCategory = Object.entries(sectionShortcuts).reduce((out, sec) => sec[1].includes(section) ? sec[0] : out, section); console.log(`Copying ${repeatingRowArray.length} attributes from row...`); await Promise.all(repeatingRowArray.map(async (attr) => { const newAttribute = { characterid: characterId, name: `repeating_${section}_${newRowId}_${attr.name}`, current: attr.current, max: attr.max }; const newAttr = createObj('attribute', newAttribute); if (attributeTasks[taskCategory] && attributeTasks[taskCategory][attr.name]) { const taskRequired = attributeTasks[taskCategory][attr.name].condition(newAttr); if (taskRequired) { taskQueue.push({ task: attributeTasks[taskCategory][attr.name].task, attribute: newAttr }); } } await timeout(1); return 1; })); console.log(`Finished writing attributes...`); await timeout(50); if (taskQueue.length) { console.info(`Executing ${taskQueue.length} tasks...`); taskQueue.forEach(t => t.task(t.attribute)); } } const getRepeatingRow = (characterId, sectionName, rowId) => { const allAttrs = findObjs({ type: 'attribute', characterid: characterId, }), rxRow = new RegExp(`^repeating_${sectionName}_${rowId}_`, 'i'); return allAttrs .filter(a => rxRow.test(a.get('name'))) .map(a => ({ name: a.get('name').replace(rxRow, ''), current: a.get('current'), max: a.get('max') })); } const buildTemplate = (sections, commands) => { const templateRows = Object.entries(sections).map(kv => { return kv[1].length ? `{{${kv[0]}=${kv[1].reduce((out, row) => out += `[${row.name.trim() || '???' }](!${scriptName} --copy --sel ${commands.selected} --tar ${commands.targets.join(', ')} --sec ${kv[0]} --row ${row.rowId}" class="copyrepsec" style="${buttonStyle})`, '')}}}` : `{{${kv[0]}=No entries found}}`; }); return `&{template:default} {{name=${commands.sectionName} => ${commands.nameAttribute}}} ${templateRows.join('')}`; } const getSectionRows = (characterId, sectionName, nameAttribute) => { const sectionArray = sectionShortcuts[sectionName] || [ sectionName ], allAttrs = findObjs({ type: 'attribute', characterid: characterId }), output = sectionArray.reduce((out, sec) => { const rxRowNames = new RegExp(`repeating_${sec}_-[0-z_-]{19}_${nameAttribute}`, 'i'), sectionRowNameAttributes = allAttrs.filter(a => rxRowNames.test(a.get('name'))); out[sec] = sectionRowNameAttributes.reduce((out, attr) => [ ...out, { name: attr.get('current'), rowId: (attr.get('name').match(rx.rowId)||[])[1] } ], []); return out; }, {}); // console.info(output); return output; } const checkCharacterId = (id) => (rx.strictId.test(id) && getObj('character', id)); const checkRepeatingSection = (sectionName, characterId) => { const char = characterId ? getObj('character', characterId) : null; if (!char) return false; if (Object.keys(sectionShortcuts).includes(sectionName)) return true; const attrs = findObjs({ type: 'attribute', characterid: characterId }) || [], rxSection = new RegExp(`repeating_${sectionName}_`, 'i'); return attrs.filter(v => rxSection.test(v.get('name'))).length; } const parseCommands = (cliString) => { if (!cliString) return null; const commands = cliString.split(/\s*--\s*/g).filter(v=>v); const output = commands.reduce((out, command) => { const [ , option, parameterString ] = (command.match(/(\w+)\s*(.*)/)||[]), parameters = (parameterString||'').trim().split(/\s*,\s*/g); if (option) { if (/^sel/i.test(option) && checkCharacterId(parameters[0])) { return { ...out, selected: parameters[0] }; } else if (/^tar/.test(option)) { return { ...out, targets: parameters.filter(v => checkCharacterId(v)) }; } else if (/^sec/.test(option)) { return { ...out, sectionName: parameters[0] }; } else if (/^name/.test(option) && parameters[0]) { return { ...out, nameAttribute: parameters[0] } } else if (/^copy/.test(option)) return { ...out, copy: true } else if (/^row/.test(option) && rx.strictId.test(parameters[0])) return { ...out, sourceRow: parameters[0] } else return out; } // console.log(option, parameters); }, { copy: false, sourceRow: '', selected: '', targets: [], sectionName: '', nameAttribute: '' }); output.nameAttribute = output.nameAttribute || sectionNameAttributes[output.sectionName] || 'name'; return output; } const cleanMessageName = (name) => `${name}`.replace(/\s*\(GM\)$/, ''); on('chat:message', (msg) => { if (msg.type === 'api' && /^!copyRepSec\s+[\S]+/i.test(msg.content)) { const cliString = (msg.content.match(/^!copyrepsec\s+(.+)/i)||[])[1], commands = parseCommands(cliString), validSection = checkRepeatingSection(commands.sectionName, commands.selected), msgWho = cleanMessageName(msg.who); if (commands.copy) { const errorMessage = !commands.selected ? `${scriptName} error copying row: no selected character ID passed, or bad character ID` : !commands.targets.length ? `${scriptName} error copying row: no target character ID(s) passed, or bad character ID(s)` : !validSection ? `${scriptName} error copying row: no repeating section name passed, or no attributes found in repeating section "${commands.sectionName}"` : !commands.sourceRow ? `${scriptName} error copying row: no repeating section row passed, or bad rowId passed.` : null; if (errorMessage) { console.log(commands); sendChat(scriptName, `/w "${msgWho}" ${errorMessage}`); return; } // sendChat(scriptName, `/w "${msgWho}" copying ${commands.sectionName} => ${commands.sourceRow}`); const sourceRowArray = getRepeatingRow(commands.selected, commands.sectionName, commands.sourceRow); // console.info(sourceRowArray); commands.targets.forEach(t => copyRepeatingRow(t, commands.sectionName, sourceRowArray)); } else { const errorMessage = !commands.selected ? `${scriptName} error creating list: no selected character ID passed, or bad character ID` : !commands.targets.length ? `${scriptName} error creating list: no target character ID(s) passed, or bad character ID(s)` : !validSection ? `${scriptName} error creating list: no repeating section name passed, or no attributes found in repeating section "${commands.sectionName}"` : null; if (errorMessage) { console.log(commands); sendChat(scriptName, `/w "${msgWho}" ${errorMessage}`); return; } const rowEntries = getSectionRows(commands.selected, commands.sectionName, commands.nameAttribute), chatMessage = buildTemplate(rowEntries, commands); sendChat(scriptName, `/w "${msgWho}" ${chatMessage}`); } } }); const randomInt = (range=100, depth=32) => { const max = range * 2**depth; let random; do { random = Math.floor(Math.random() * 2**depth) } while (random >= max); return random % range; } const generateUID = (numIds = 1) => { let output = [], key = ''; const chars = '-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_'; let ts = Date.now(); for (let i = 8; i > 0; i--) { output[i] = chars.charAt(ts % 64), ts = Math.floor(ts / 64) } for (let j = 0; j < 12; j++) { output.push(chars.charAt(randomInt(64))) } key = output.join(''); if (numIds > 1) { numIds = Math.min(32, numIds); output = Array(numIds).fill().map((v,i) => { let lastChar = chars[(chars.indexOf(key[19])+i)%64]; return `${key.slice(0,18)}${lastChar}`; }); return output; } else return key; } });