Roll20 has a handy feature, the Character Vault. You can use this to transfer a character from one campaign to another.
A big frustration with it, though, is that you have to reassign the sheet to the owning player. When you move a character, it doesn’t automatically save ownership information. This means players can’t use their own characters until the GM is present to update the sheet.
Aaron to the rescue! Install the script at the end of this post in all relevant campaigns – the source campaign, and any campaigns you want to move characters to.
Run the following command in the original campaign:
!update-controlledby
Code language: JavaScript (javascript)
You only need to run this command once, in the campaign. It will then create an ‘OwnerRoll20IDs’ attribute on all existing sheets, and on new sheets created after command is run. That attribute will contain the owners of the sheet and will be updated whenever it changes.
When a character is imported into a new campaign (whether via the Character Vault or the Transmogrifier), and that character has an OwnerRoll20IDs attribute, this script will update the ControlledBy and VisibleTo fields of the character.
This means that when you transfer characters, the owner is retained and they can immediately start using the character with no intervention from the GM.
Script Code
/* global TokenMod */
on('ready',()=>{
const OwnerAttrName = 'OwnerRoll20IDs';
const assureOwnerID = (c, p, force = false) => {
let cb = c.get('controlledby');
if(cb.length && ! /\ball\b/i.test(cb)) {
let shouldUpdate = force;
let props = {
type: 'attribute',
characterid: c.id,
name: OwnerAttrName
};
let attr = findObjs(props)[0];
if(!attr) {
attr = createObj('attribute', props);
shouldUpdate = true;
}
if(shouldUpdate){
attr.set({
current: c.get('controlledby')
.split(/,/)
.map( id => findObjs({ type: 'player', id })[0] )
.filter( p => undefined !== p )
.map( p => p.get('d20userid') )
.join(',')
});
return 1;
}
}
return 0;
};
const restoreOwnersOnCharacter = (c) => {
let props = {
type: 'attribute',
characterid: c.id,
name: OwnerAttrName
};
let attr = findObjs(props)[0];
if(attr){
let controlList = attr.get('current')
.split(/,/)
.map( id => findObjs({ type: 'player', d20userid: id})[0] )
.filter( p => undefined !== p )
.map( p => p.id)
.join(',')
;
c.set({
controlledby: controlList,
inplayerjournals: controlList
});
return 1;
}
return 0;
};
const considerChangeOnToken = (t) => setTimeout( () => {
let token = getObj('graphic',t.id);
if(token){
let c = getObj('character',token.get('represents'));
if(c){
assureOwnerID(c,null,true);
}
}
}, 100);
on('change:character:controlledby', assureOwnerID );
on('add:character', restoreOwnersOnCharacter );
if('undefined' !== typeof TokenMod && TokenMod.ObserveTokenChange){
TokenMod.ObserveTokenChange(considerChangeOnToken);
}
on('chat:message', (msg)=>{
if('api' === msg.type && /^!update-ownership\b/i.test(msg.content) && playerIsGM(msg.playerid) ){
let chars = findObjs({type: 'character'});
let updates = 0;
sendChat('',`/w gm <div><b>Considering <code>${chars.length}</code> characters for update.</b></div>`);
const burndown = ()=>{
let c = chars.shift();
if(c){
updates += assureOwnerID(c,null,true);
setTimeout(burndown,0);
} else {
sendChat('',`/w gm <div><b>Updated ownership on <code>${updates}</code> characters.</b></div>`);
}
};
burndown();
}
if('api' === msg.type && /^!update-controlledby\b/i.test(msg.content) && playerIsGM(msg.playerid) ){
let chars = findObjs({type: 'attribute', name: OwnerAttrName})
.map(a=>getObj('character',a.get('characterid')))
.filter( c => undefined !== c);
let updates = 0;
sendChat('',`/w gm <div><b>Considering <code>${chars.length}</code> character owners for update.</b></div>`);
const burndown = ()=>{
let c = chars.shift();
if(c){
updates += restoreOwnersOnCharacter(c);
setTimeout(burndown,0);
} else {
sendChat('',`/w gm <div><b>Updated controlledby on <code>${updates}</code> characters.</b></div>`);
}
};
burndown();
}
});
});
Code language: JavaScript (javascript)