item to actors script

I have this macro for updating actors in a compendium with spells from a compendium of spells (i.e. replacing by name any spells on the actors, so I can push my improvements to spell automation onto the monsters in my compendium). It works, but I'm wondering if I can make it more efficient - currently, it's doing an individual update per actor, and that feels like it should be possible to batch into a single update (which would also allow me to construct the update list by getting an extended index with the items for the monsters, and not have to getDocument on each monster). There's definitely a bunch of smaller improvements that can be made as well - this is slightly sketchy code and I'm out of practice.
let pack = game.packs.get("DDBImports.spells")
let index = pack.index
let monsterpack = game.packs.get("DDBImports.monsters")
//await monsterpack.getIndex({fields:["items"]})
let monsterindex = Array.from(monsterpack.index)
for (let monster of monsterindex){
console.log(monster.name)
let act = await monsterpack.getDocument(monster._id)
let items = Array.from(act.items)
let updates = []
for (let item of items){
if(item.type !== "spell") continue //only replacing spells
let newitem = await pack.getDocument(index.find(i => i.name === item.name)?._id) //find a spell with the same name
if(!newitem) continue //if none, then skip to next
let update = duplicate(newitem.data)
update.data.preparation = item.data.data.preparation;
update.data.uses = item.data.data.uses
update.data.consume = item.data.data.consume
update._id = item.data._id
updates.push(update)
}
if(!updates) continue
console.log(updates)
await act.updateEmbeddedDocuments("Item", updates)
}
let pack = game.packs.get("DDBImports.spells")
let index = pack.index
let monsterpack = game.packs.get("DDBImports.monsters")
//await monsterpack.getIndex({fields:["items"]})
let monsterindex = Array.from(monsterpack.index)
for (let monster of monsterindex){
console.log(monster.name)
let act = await monsterpack.getDocument(monster._id)
let items = Array.from(act.items)
let updates = []
for (let item of items){
if(item.type !== "spell") continue //only replacing spells
let newitem = await pack.getDocument(index.find(i => i.name === item.name)?._id) //find a spell with the same name
if(!newitem) continue //if none, then skip to next
let update = duplicate(newitem.data)
update.data.preparation = item.data.data.preparation;
update.data.uses = item.data.data.uses
update.data.consume = item.data.data.consume
update._id = item.data._id
updates.push(update)
}
if(!updates) continue
console.log(updates)
await act.updateEmbeddedDocuments("Item", updates)
}
7 Replies
Calego
Calego3y ago
this is an interesting problem if you have the inclination, playing around and trying to get updateAll to work would be a solution. https://foundryvtt.com/api/CompendiumCollection.html#updateAll But I suspect it's an "accidentally" inherited method that won't work with compendiums at all. I think the getIndex might be the best answer to this problem. but I do not know how well (if at all) one can index items on actors in a compendium like ideally you'd be able to index in such a way that only figures out if the one item needed exists Hmmmm updating all spells in the compendium at once prevents that too getDocuments with an nedb query feels like the best solution here, but it's going to be a gnarly query you'll need to construct based on the spell pack index
const docsToUpdate = await game.packs.get("DDBImports.monsters").getDocuments({
items: {$elemMatch: { type: 'spell' }}
});

const documentUpdates = docsToUpdate.map((actor) => {
// some Promise.all shennanegins will be needed in here somewhere
const itemsToUpdate = actor.items
.filter(({type}) => type === 'spell')
.map((item) => {
// how to get the compendium items somewhat performantly? probably getDocument is fine as it caches individual documents as needed
const newItemId = game.packs.get("DDBImports.spells").index.find(({name}) => name === item.name)?._id;
const newItemData = await game.packs.get("DDBImports.spells").getDocument(newItemId).toJSON();
return {
...newItemData,
_id: item._id,
}
});

return {
_id: actor._id,
items: itemsToUpdate
}
});

Actor.updateDocuments(documentUpdates, {pack: 'DDBImports.monsters'});
const docsToUpdate = await game.packs.get("DDBImports.monsters").getDocuments({
items: {$elemMatch: { type: 'spell' }}
});

const documentUpdates = docsToUpdate.map((actor) => {
// some Promise.all shennanegins will be needed in here somewhere
const itemsToUpdate = actor.items
.filter(({type}) => type === 'spell')
.map((item) => {
// how to get the compendium items somewhat performantly? probably getDocument is fine as it caches individual documents as needed
const newItemId = game.packs.get("DDBImports.spells").index.find(({name}) => name === item.name)?._id;
const newItemData = await game.packs.get("DDBImports.spells").getDocument(newItemId).toJSON();
return {
...newItemData,
_id: item._id,
}
});

return {
_id: actor._id,
items: itemsToUpdate
}
});

Actor.updateDocuments(documentUpdates, {pack: 'DDBImports.monsters'});
BadIdeasBureau
There's actually a way of doing updateAll on a compendium via Document.updateDocuments with an appropriate context modifier, e.g.:
let updated = await Actor.updateDocuments(updates, {pack: packname})
let updated = await Actor.updateDocuments(updates, {pack: packname})
It's more just how to construct the updates object for that
Calego
Calego3y ago
the promise of updateAll was being able to use a function instead of an object for the update operation
BadIdeasBureau
aaaah, right, yeah that makes sense Though I think doing getIndex({fields:[items]}) would give you enough to manipulate to construct the update object without grabbing the full Documents (since all you'd need is {_id, items}.... Which is actually the answer here, isn't it? Grab the expanded index, copy each entry with spells into updates = [{_id, items},...], futz with the items objects to swap out the spells, Actor.updateDocuments(updates, pack). I think that should work, unless there's some additional stuff updateEmbeddedDocuments does which would be bad to skip over... (Also if it helps with the query you can assume that any monster with an item of type "spell" needs an update - the spells compendium is a complete import from D&DBeyond, as is the monsters compendium) Hmmm.... I should probably prep for the session tonight before poking this more (first session of Witchlight, so the players would have to make some very poor life choices to end up in a combat where this matters, and I need to be ready to speak a whole lot in rhyme...)
Calego
Calego3y ago
Alright @badideasbureau that's as good as I got. I know there's some Promise shennanegins that'll need to be sorted out in there for sure but the concept seems sound Oh shit you can do .find with index? that's dope does Foundry add .find to the Map prototype?? no, not on all the mindblown
BadIdeasBureau
My general assumption with Foundry at this point is that if I want to treat it like an Array it's probably worth a shot 😛
Calego
Calego3y ago
Ahh it's a Collection under the hood, neat I hope it goes without saying that you should try this with something tame in a test world first 😛