Roll20 uses cookies to improve your experience on our site. Cookies enable you to enjoy certain features, social sharing functionality, and tailor message and display ads to your interests on our site and others. They also help us understand how our site is being used. By continuing to use our site, you consent to our use of cookies. Update your cookie preferences .
×

Connect Token Bar Value to Repeating Section (PF2e)

Hi all! I'm trying to connect a bar on one of my tokens to a repeating section in my character sheet but the only valid options in the drop down menu are for non-repeating sections (as far as I can tell). Is there any way to do this, or will I need a mod for that? I already know that the repeating section is repeating_items-other_$id_action, and I know which item it is, I'm just not sure how to link it to a bar. Thanks!
1664380028
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
Repeating sections are not valid targets for bar linkage. I'm not currently aware of a mod that handles this either.
Unfortunate. Thanks Scott!
1664422094

Edited 1664422415
Oosh
Sheet Author
API Scripter
Ok, this is very untested, but you can give it a go. It should link a repeating attribute to a token bar. The token must represent a character sheet (obviously). Command line looks like this: !attrlink --bar 3 --attr repeating_resource_$0_resource_left --bar must be 1, 2 or 3 --attr must be a repeating attribute. You can use either the index '$0', or the actual row ID '-qwertyuiop987654321', but otherwise it needs to be an exact match of the attribute name on the sheet --remove will instead remove all observers & attributes that have been created for the attribute supplied in --attr The script just creates a static attribute, then links that to the token bar. It then sets up an observer so each time the repeating target updates, the static middle-man attribute also updates. If you're lucky it might even work.... /* globals on, sendChat, findObjs, getObj, log, state, createObj */ const attributeLink = (() => { //eslint-disable-line no-unused-vars const scriptName = 'attributeLink', scriptVersion = '0.1.0'; const setTokenBarLink = ({ tokenId, tokenBar, targetAttribute }) => { const token = getObj('graphic', tokenId); token.set({ [`bar${tokenBar}_link`]: targetAttribute.id }); // console.info(targetAttribute.id, token.get(`bar${tokenBar}_link`)); } const removeObserver = ({ sourceAttribute, characterId }) => { if (!state[scriptName][characterId]) return postChat(`No observers found for id "${characterId}"`); state[scriptName][characterId] = state[scriptName][characterId].filter(observer => { if (observer.sourceId === sourceAttribute.id) { const attributeCleanup = getObj('attribute', observer.targetId); if (attributeCleanup) attributeCleanup.remove(); return false; } return true; }); } const setObserver = ({ sourceAttribute, targetAttribute, characterId }) => { if (!sourceAttribute || !targetAttribute || !characterId) return null; state[scriptName][characterId] = Array.isArray(state[scriptName][characterId]) ? state[scriptName][characterId] : []; const existingObserver = state[scriptName][characterId].filter(o => o.sourceId === sourceAttribute.id && o.targetId === targetAttribute.id); if (existingObserver.length) { postChat(`Observer already exists for that attribute, skipping setObserver.`); return true; } state[scriptName][characterId].push({ sourceId: sourceAttribute.id, targetId: targetAttribute.id, }); // console.info(state[scriptName]); return true; } const createLinkedAttribute = ({ sourceAttribute, characterId }) => { if (!sourceAttribute) return; const staticAttributeName = `attrlink_${sourceAttribute.get('name')}`; const existing = findObjs({ type: 'attribute', characterid: characterId }).find(attribute => attribute.get('name') === staticAttributeName); // console.warn(existing); if (existing) { existing.set({ current: sourceAttribute.get('current') || '', max: sourceAttribute.get('max') || '', }); // console.info(existing); return existing; } const newAttribute = createObj('attribute', { name: staticAttributeName, current: sourceAttribute.get('current') || '', max: sourceAttribute.get('max') || '', characterid: characterId }); // console.warn(newAttribute); return newAttribute; } const resolveRepeatingIndex = (attributeName, characterId, allAttributes) => { const repeatingSectionName = (attributeName.match(/(repeating_\w+?)_/i)||[])[1], reporderAttributeName = `_reporder_${repeatingSectionName}`, rxRepeatingSectionName = new RegExp(`${repeatingSectionName}`, 'i'), rxRowId = /^repeating_[^_]+_(-[0-z]{19})_/i; // console.log(reporderAttributeName, repeatingSectionName, rxRepeatingSectionName); const reporderAttribute = allAttributes.find(attribute => attribute.get('name') === reporderAttributeName), reorderedRowIds = reporderAttribute ? reporderAttribute.get('current').split(/\s*,\s*/g) : []; const allRepeatingRowIds = allAttributes.reduce((output, attribute) => { if (rxRepeatingSectionName.test(attribute.get('name'))) { const rowId = (attribute.get('name').match(rxRowId)||[])[1]; if (rowId && !output.includes(rowId)) return [ ...output, rowId ]; } return output; }, []); // console.info(allRepeatingRowIds, reorderedRowIds); const finalRowOrder = [ ...reorderedRowIds, ...allRepeatingRowIds.filter(v => !reorderedRowIds.includes(v)) ]; // console.warn(finalRowOrder); const attributeIndex = (attributeName.match(/_\$(\d+)_/)||[])[1]; // console.log(attributeIndex); const targetRowId = (finalRowOrder.length && attributeIndex != null) ? finalRowOrder[parseInt(attributeIndex)] : null; return targetRowId ? attributeName.replace(`$${attributeIndex}`, targetRowId) : null; } const findAttribute = (attributeString, characterId) => { if (!/^repeating/.test(attributeString)) return postChat(`This script only works on repeating attributes.`); const allAttributes = findObjs({ type: 'attribute', characterid: characterId }); attributeString = (/_\$\d+_/.test(attributeString)) ? resolveRepeatingIndex(attributeString, characterId, allAttributes) : attributeString; const targetAttribute = allAttributes.find(attribute => attribute.get('name') === attributeString); // console.info(targetAttribute); return targetAttribute; } const getSelectedTokenAndCharacter = (msg) => { if (!msg.selected) return {}; const tokenId = msg.selected[0]._id, token = tokenId ? getObj('graphic', tokenId) : null, characterId = token ? token.get('represents') : null; return { tokenId, characterId } } const handleInput = (msg) => { if (msg.type === 'api' && /^!attr(ibute)?link\s/i.test(msg.content)) { const { tokenId, characterId } = getSelectedTokenAndCharacter(msg); if (!tokenId || !characterId) return postChat(`No selected token or couldn't find linked character sheet.`); const commands = msg.content.trim().split(/\s*--\s*/g); commands.shift(); const scriptParameters = { tokenBar: null, sourceAttribute: null, remove: false, characterId, tokenId } commands.forEach(command => { const args = command.replace(/^[\S]+\s*/, ''); if (/^bar/i.test(command)) { const barNumber = parseInt(args.replace(/\D/g, '')); if (barNumber > 0 && barNumber < 4) scriptParameters.tokenBar = barNumber; else return postChat(`${barNumber} is not a valid token bar.`); } else if (/^attr/i.test(command)) { const targetAttribute = findAttribute(args.trim(), characterId); if (!targetAttribute) return postChat(`Could not find attribute from "${args}"`); scriptParameters.sourceAttribute = targetAttribute; } else if (/^rem(ove)?/i.test(command)) scriptParameters.remove = true; }); if (scriptParameters.tokenBar && scriptParameters.sourceAttribute) { if (scriptParameters.remove) { removeObserver(scriptParameters); } else { scriptParameters.targetAttribute = createLinkedAttribute(scriptParameters); if (!scriptParameters.targetAttribute) return postChat(`Could not create linked attribute.`); const observerResult = setObserver(scriptParameters); if (!observerResult) return postChat(`Could not set up observer for static attribute.`); setTokenBarLink(scriptParameters); } } } } const checkAttributes = (event, previous) => { // console.info(event, previous); const charId = event.get('characterid'); // console.log(charId); if (!state[scriptName][charId]) return; else { // console.info(event.id); const validObservers = state[scriptName][charId].filter(observer => observer.sourceId === event.id); // console.info(validObservers); validObservers.forEach(observer => { console.info(`Triggered`, observer); const targetAttribute = getObj('attribute', observer.targetId); const changedValueType = `${event.get('max')}` === `${previous.max}` ? 'current' : 'max'; if (targetAttribute) targetAttribute.setWithWorker(changedValueType, event.get(changedValueType)); else { sendChat(scriptName, 'Cleaning up observer for non existent attribute.'); removeObserver({ sourceAttribute: { id: event.id }, characterId: charId }); } }) } } const postChat = (chatText, whisper = 'gm') => { const whisperText = whisper ? `/w "${whisper}" ` : ``; sendChat(scriptName, `${whisperText}${chatText}`, null, { noarchive: true }); } on('ready', () => { if (!state[scriptName]) state[scriptName] = { version: scriptVersion }; on('chat:message', handleInput); on('change:attribute', checkAttributes); log(`${scriptName} - v${scriptVersion}`); }); })();