1077 lines
29 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { moduleName, moduleHelpers, log } from '../globals.js';
import { firstOwner, addActiveEffectsToToken } from '../helpers.js';
import { templates } from '../preloadTemplates.js';
const MAINTAIN_ICONS = [
'icons/magic/symbols/rune-sigil-black-pink.webp',
'icons/magic/symbols/rune-sigil-green-purple.webp',
'icons/magic/symbols/rune-sigil-hook-white-red.webp',
'icons/magic/symbols/rune-sigil-red-orange.webp',
'icons/magic/symbols/rune-sigil-rough-white-teal.webp',
'icons/magic/symbols/rune-sigil-white-pink.webp',
'icons/magic/symbols/runes-star-blue.webp',
'icons/magic/symbols/runes-star-magenta.webp',
'icons/magic/symbols/runes-star-orange-purple.webp',
'icons/magic/symbols/runes-star-orange.webp',
'icons/magic/symbols/runes-star-pentagon-blue.webp',
'icons/magic/symbols/runes-star-pentagon-magenta.webp',
'icons/magic/symbols/runes-star-pentagon-orange-purple.webp',
'icons/magic/symbols/runes-star-pentagon-orange.webp',
'icons/magic/symbols/runes-triangle-blue.webp',
'icons/magic/symbols/runes-triangle-magenta.webp',
'icons/magic/symbols/runes-triangle-orange-purple.webp',
'icons/magic/symbols/runes-triangle-orange.webp',
'icons/magic/symbols/triangle-glow-purple.webp',
'icons/magic/symbols/triangle-glowing-green.webp',
];
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
function _hashCode(str) {
let hash = 0;
if (str.length === 0) {
return hash;
}
for (let i = 0; i < str.length; i++) {
const c = str.charCodeAt(i);
hash = (hash << 5) - hash + c;
hash |= 0;
}
return Math.abs(hash);
}
export class PowerFormApplication extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(powerEffect) {
const name = powerEffect.name.replaceAll(/[^a-zA-Z]/g, '');
const id = `${PowerFormApplication.DEFAULT_OPTIONS.id}${name}`;
super({ id });
this.powerEffect = powerEffect;
}
static DEFAULT_OPTIONS = {
id: 'mbSwadePowerEffectsApplicationV2',
form: {
handler: PowerFormApplication.#onSubmit,
closeOnSubmit: true,
},
tag: 'form',
position: {
width: 600,
height: 'auto',
},
classes: ['mbSwade', 'mbSwadeForm', 'mbSwadePowerEffectsForm'],
window: {
icon: 'fa-solid fa-hand-sparkles',
title: 'Apply Effect',
},
};
static PARTS = {
header: {
template: templates['dialogHeader.html'],
classes: ['mbSwade', 'mbSwadeDialogHeader', 'mbSwadePowerEffectsHeader'],
},
body: {
template: templates['powerDialog.html'],
classes: ['mbSwade', 'mbSwadePowerEffectsBody', 'scrollable'],
},
footer: {
template: 'templates/generic/form-footer.hbs',
},
};
static sortMods(a, b) {
if (a.isGlobal !== b.isGlobal) {
return a.isGlobal ? -1 : 1;
}
if ((a.sortOrder ?? 0) !== (b.sortOrder ?? 0)) {
return (a.sortOrder ?? 0) < (b.sortOrder ?? 0) ? -1 : 1;
}
if (a.type !== b.type) {
return a.type === 'checkbox' ? -1 : 1;
}
return a.name === b.name ? 0 : a.name < b.name ? -1 : 1;
}
async _prepareContext() {
await this.powerEffect.init();
let modifiers = foundry.utils.deepClone(this.powerEffect.modifiers);
modifiers.sort(PowerFormApplication.sortMods);
for (const modifier of modifiers) {
modifier.isCheckbox = modifier.type === 'checkbox';
modifier.isSelect = modifier.type === 'select';
modifier.isRadio = modifier.type === 'radio';
modifier.isNumber = modifier.type === 'number';
modifier.isText = modifier.type === 'text';
if (modifier.isNumber) {
modifier.step = modifier?.step ?? 1;
}
if (modifier.isSelect || modifier.isRadio) {
for (const choice in modifier.choices) {
let val = '';
if (modifier.values[choice] !== 0) {
val = ` (${modifier.values[choice] > 0 ? '+' : ''}${modifier.values[choice]})`;
}
modifier.choices[choice] = `${modifier.choices[choice]}${val}`;
}
}
if (modifier.isRadio) {
for (const choice in modifier.choices) {
let val = modifier.choices[choice];
modifier.choices[choice] = { text: val, checked: choice == modifier.default };
}
}
}
const data = {
name: this.powerEffect.name,
formId: foundry.utils.randomID(),
headerTitle: `${this.powerEffect.name} Effect`,
headerSubtitle: `Apply the effects from ${this.powerEffect.name}`,
icon: this.powerEffect.icon,
basePowerPoints: this.powerEffect.basePowerPoints,
modifiers,
recipients: {
cost: 0,
number: 0,
total: 0,
},
extraDescription: this.powerEffect.extraDescription,
targets: [],
buttons: this.powerEffect.menuButtons,
};
for (let button of data.buttons) {
button.action = button.value;
button.type = button.value === 'cancel' ? 'cancel' : 'submit';
}
if (this.powerEffect.isTargeted) {
if (this.powerEffect.oneTarget) {
data.targets = [this.powerEffect.targets?.[0]?.name ?? '<em>No Target Selected!</em>'];
} else {
data.targets = this.powerEffect.targets.map((t) => t.name);
}
}
if (this.powerEffect.hasAdditionalRecipients && this.powerEffect.targets.length > 1) {
data.recipients.cost = this.powerEffect.additionalRecipientCost;
data.recipients.count = this.powerEffect.additionalRecipientCount;
data.recipients.total = data.recipients.cost * data.recipients.count;
data.recipients.epic = this.powerEffect.additionalRecipientsIsEpic;
data.recipients.text = this.powerEffect.additionalRecipientText;
}
return data;
}
static async #onSubmit(event, form, formData) {
log(this.formData);
formData.object.submit = event?.submitter?.dataset?.action ?? 'cancel';
if (formData.object.submit !== 'cancel') {
this.powerEffect.formData = formData.object;
this.powerEffect.applyEffect();
}
}
}
export class PowerEffect {
constructor(token, targets) {
this.source = token;
this.targets = targets;
this.data = {};
}
async init() {
log('Power Effect', this.name, 'Init');
}
static async getStatus(label, name, favorite = true) {
const effect = foundry.utils.deepClone(CONFIG.statusEffects.find((se) => se.label === label || se.name === label));
effect.name = 'name' in effect ? effect.name : effect.label;
effect.duration = {};
if (!('flags' in effect)) {
effect.flags = {};
}
effect.flags.swade = {};
if (favorite) {
effect.flags.swade.favorite = true;
}
effect.statuses ??= [];
effect.statuses.push(effect.id);
return effect;
}
createEffectDocument(icon, name, changes = null) {
if (changes === null) {
changes = [];
}
return {
icon,
name,
changes,
description: `<p>From <strong>${this.source.name}</strong> casting <em>${this.name}</em></p>`,
duration: {},
system: {
loseTurnOnHold: false,
expiration: null,
},
flags: {
[moduleName]: {
powerEffect: true,
},
swade: {
loseTurnOnHold: false,
expiration: null,
},
},
};
}
async applyActiveEffects(token, effectDocuments) {
const owner = firstOwner(token);
await moduleHelpers.socket.executeAsUser(
addActiveEffectsToToken,
owner.id,
token?.scene?.id ?? token.parent.id,
token.id,
effectDocuments,
);
}
get name() {
return 'Unknown Power';
}
get effectName() {
return this.name;
}
get extraDescription() {
return '';
}
get icon() {
return 'icons/magic/symbols/question-stone-yellow.webp';
}
get duration() {
return 5;
}
get basePowerPoints() {
return 0;
}
get usePrimaryEffect() {
return true;
}
get isDamaging() {
return false;
}
get hasAdditionalRecipients() {
return false;
}
get additionalRecipientsIsEpic() {
return false;
}
get additionalRecipientText() {
return 'Additional Recipients';
}
get additionalRecipientCount() {
if (!this.hasAdditionalRecipients) {
return 0;
}
return Math.max(0, this.targets.length - 1);
}
get additionalRecipientCost() {
return 0;
}
get isTargeted() {
return false;
}
get oneTarget() {
return false;
}
get hasRange() {
return true;
}
get isRaisable() {
return true;
}
get hasAoe() {
return false;
}
get modifiers() {
const mods = [];
mods.push({
name: 'Adaptable Caster',
type: 'checkbox',
default: false,
id: 'adaptable',
value: 1,
epic: false,
effect: false,
isGlobal: true,
});
mods.push({
name: 'Fatigue',
type: 'checkbox',
default: false,
id: 'fatigue',
value: 2,
epic: false,
effect: false,
isGlobal: true,
});
mods.push({
name: 'Glow/Shroud',
id: 'glowshroud',
type: 'radio',
isGlobal: true,
default: 'none',
choices: { none: 'None', glow: 'Glow', shroud: 'Shroud' },
values: { none: 0, glow: 1, shroud: 1 },
effects: {
none: null,
glow: {
name: 'Glow',
icon: 'icons/magic/light/orb-shadow-blue.webp',
changes: [
{
key: '@Skill{Stealth}[system.die.modifier]',
value: -2,
priority: 0,
mode: foundry.CONST.ACTIVE_EFFECT_MODES.ADD,
},
],
},
shroud: {
name: 'Shroud',
icon: 'icons/magic/perception/shadow-stealth-eyes-purple.webp',
changes: [
{
key: '@Skill{Stealth}[system.die.modifier]',
value: 1,
priority: 0,
mode: foundry.CONST.ACTIVE_EFFECT_MODES.ADD,
},
],
},
},
epic: false,
});
if (this.isDamaging) {
mods.push({
name: 'Armor Piercing',
id: 'ap',
type: 'select',
default: 'none',
choices: { none: 'None', 2: 'AP 2', 4: 'AP 4', 6: 'AP 6' },
values: { none: 0, 2: 1, 4: 2, 6: 3 },
effects: { none: null, 2: null, 4: null, 6: null },
epic: false,
isGlobal: true,
});
mods.push({
name: 'Lingering Damage',
id: 'lingeringdamage',
type: 'checkbox',
default: false,
value: 2,
epic: false,
effect: false,
isGlobal: true,
});
mods.push({
name: 'Heavy Weapon',
id: 'heavyweapon',
value: 2,
epic: false,
effect: false,
type: 'checkbox',
default: false,
isGlobal: true,
});
}
mods.push({
name: 'Hinder/Hurry',
id: 'hinderhurry',
type: 'radio',
default: 'none',
value: 1,
epic: false,
choices: { none: 'None', hinder: 'Hinder', hurry: 'Hurry' },
values: { none: 0, hinder: 1, hurry: 1 },
isGlobal: true,
effects: {
none: null,
hinder: {
name: 'Hinder',
icon: 'icons/magic/control/debuff-chains-shackle-movement-red.webp',
changes: [
{
key: 'system.pace',
value: -2,
priority: 0,
mode: foundry.CONST.ACTIVE_EFFECT_MODES.ADD,
},
],
},
hurry: {
name: 'Hurry',
icon: 'icons/skills/movement/feet-winged-sandals-tan.webp',
changes: [
{
key: 'system.pace',
value: 2,
priority: 0,
mode: foundry.CONST.ACTIVE_EFFECT_MODES.ADD,
},
],
},
},
});
if (this.hasAoe) {
mods.push({
type: 'checkbox',
default: false,
name: 'Selective',
id: 'selective',
value: 1,
epic: false,
effect: false,
isGlobal: true,
});
}
if (this.hasRange) {
mods.push({
type: 'select',
default: 0,
name: 'Range',
id: 'range',
choices: {
normal: 'Normal Range',
x2: 'Range ×2',
x3: 'Range ×3',
},
values: {
normal: 0,
x2: 1,
x3: 2,
},
isGlobal: true,
effects: { normal: null, x2: null, x3: null },
});
}
return mods;
}
get menuButtons() {
const data = [{ label: 'Apply', value: 'apply' }];
if (this.isRaisable) {
data.push({ label: 'Apply with Raise', value: 'raise' });
}
data.push({ label: 'Cancel', value: 'cancel' });
return data;
}
render() {
new PowerFormApplication(this).render(true);
}
async applyEffect() {
await this.parseValues();
await this.apply();
await this.chatMessage();
await this.sideEffects();
}
async parseValues() {
this.data.raise = this.formData.submit === 'raise';
this.data.button = this.formData.submit;
this.data.maintId = foundry.utils.randomID();
for (const mod of this.modifiers) {
this.data[mod.id] = this.formData[mod.id];
}
}
enhanceSecondaryEffect(maintId, doc) {
doc.statuses = doc.statuses ?? [];
doc.statuses.push('powerEffect');
if (this.duration === 0 && !this.usePrimaryEffect) {
// set secondary effects of instant spells to expire on victim's next
// turn
doc.duration.rounds = 1;
doc.flags.swade.expiration = CONFIG.SWADE.CONST.STATUS_EFFECT_EXPIRATION.EndOfTurnAuto;
doc.system.expiration = doc.flags.swade.expiration;
} else {
doc.flags[moduleName].maintId = maintId;
if (moduleHelpers.useVAE) {
doc.flags['visual-active-effects'] = {
data: {
inclusion: 1,
},
};
} else {
doc.duration.seconds = 594;
}
}
return doc;
}
async createSecondaryEffects(maintId) {
const docs = [];
for (const mod of this.modifiers) {
const modValue = this.data[mod.id];
if (modValue && (mod?.effect || (mod?.effects?.[modValue] ?? false))) {
const icon = 'effects' in mod ? mod.effects[modValue].icon : mod.icon;
const name = 'effects' in mod ? mod.effects[modValue].name : mod.name;
const changes = 'effects' in mod ? mod.effects[modValue].changes : mod.changes;
const doc = this.enhanceSecondaryEffect(maintId, this.createEffectDocument(icon, name, changes));
const desc = 'effects' in mod ? mod.effects?.[modValue]?.description : mod.description;
if (desc) {
doc.description += desc;
}
docs.push(doc);
}
}
return docs;
}
getPrimaryEffectChanges() {
const changes = [
{
key: 'flags.swade-mb-helpers.powerAffected',
value: 1,
priority: 0,
mode: foundry.CONST.ACTIVE_EFFECT_MODES.OVERRIDE,
},
];
return changes;
}
getMaintainEffectChanges() {
const changes = [
{
key: 'flags.swade-mb-helpers.powerMaintained',
value: 1,
priority: 0,
mode: foundry.CONST.ACTIVE_EFFECT_MODES.OVERRIDE,
},
];
return changes;
}
get description() {
return '';
}
get primaryEffectButtons() {
// button objects should have a label and a type.
// type should have one of the following, with the associated additional
// fields:
// roll:
// formula: dice formula eg '3d6 + 3'
// flavor: flavor text (optional)
// trait:
// rollType: 'attribute' or 'skill
// rollDesc: name or swid of the attribute or skill
// flavor: flavor text (optional)
// mods: list of mods { label, value, ignore }
// damage:
// formula: dice formula for example '1d4x[Blades]'
// ap: optional, a positive integer or 0, armor piercing
// flavor: flavor text (optional)
// callback:
// callback: the function callback to run, takes a token as an argument
return [];
}
get maintEffectButtons() {
// see the comment for primaryEffectButtons
return [];
}
get basePrimaryEffect() {
return this.createEffectDocument(this.icon, this.effectName, this.getPrimaryEffectChanges());
}
async createPrimaryEffect(maintId) {
const doc = this.basePrimaryEffect;
if (moduleHelpers.useVAE) {
doc.flags['visual-active-effects'] = {
data: {
inclusion: 1,
},
};
}
doc.description += this.description;
doc.statuses = doc.statuses ?? [];
doc.statuses.push('powerEffect');
doc.flags[moduleName].maintId = maintId;
const effectButtons = this.primaryEffectButtons;
if (effectButtons.length > 0) {
doc.flags[moduleName].buttons = effectButtons;
}
return doc;
}
async createMaintainEffect(maintId) {
let icon = MAINTAIN_ICONS[_hashCode(this.name) % MAINTAIN_ICONS.length];
if (!this.usePrimaryEffect) {
icon = this.icon;
}
const doc = this.createEffectDocument(icon, `Maintaining ${this.effectName}`, this.getMaintainEffectChanges());
doc.duration.rounds = this.duration;
doc.description += this.description;
doc.flags.swade.expiration = CONFIG.SWADE.CONST.STATUS_EFFECT_EXPIRATION.EndOfTurnPrompt;
doc.flags.swade.loseTurnOnHold = true;
doc.system.expiration = doc.flags.swade.expiration;
doc.system.loseTurnOnHold = true;
doc.flags[moduleName].maintainingId = maintId;
if (this.isTargeted) {
doc.flags[moduleName].targetIds = this.targets.map((t) => t.id);
} else {
doc.flags[moduleName].targetIds = [this.source.id];
}
doc.statuses = doc.statuses ?? [];
doc.statuses.push('powerMaintainEffect');
const effectButtons = this.maintEffectButtons;
if (effectButtons.length > 0) {
doc.flags[moduleName].buttons = effectButtons;
}
return doc;
}
// eslint-disable-next-line no-unused-vars
async secondaryDocsForTarget(docs, target) {
return foundry.utils.deepClone(docs);
}
async primaryDocForTarget(doc, target) {
const newDoc = foundry.utils.deepClone(doc);
newDoc.flags[moduleName].maintainingId = doc.flags[moduleName].maintId;
newDoc.flags[moduleName].targetIds = [target.id];
return newDoc;
}
async apply() {
const maintId = this.data.maintId;
const secondaryDocs = await this.createSecondaryEffects(maintId);
const primaryDoc = await this.createPrimaryEffect(maintId);
const maintainDoc = await this.createMaintainEffect(maintId);
if (this.isTargeted) {
for (const target of this.targets) {
const targetDocs = await this.secondaryDocsForTarget(secondaryDocs, target);
if (this.duration > 0 || this.usePrimaryEffect) {
targetDocs.push(await this.primaryDocForTarget(primaryDoc, target));
}
if (targetDocs.length > 0) {
await this.applyActiveEffects(target, targetDocs);
}
}
} else {
const targetDocs = await this.secondaryDocsForTarget(secondaryDocs, this.source);
if (targetDocs.length > 0) {
await this.applyActiveEffects(this.source, targetDocs);
}
}
if (this.duration > 0) {
await this.applyActiveEffects(this.source, [maintainDoc]);
}
}
async sideEffects() {
if (this.data.fatigue && this.isTargeted) {
for (const target of this.targets) {
const actor = target.actor;
const update = {
system: {
fatigue: {
value: actor.system.fatigue.value + 1,
},
},
};
if (actor.system.fatigue.value < actor.system.fatigue.max) {
await actor.update(update);
}
}
}
}
get powerPoints() {
let total = this.basePowerPoints;
for (const mod of this.modifiers) {
const modValue = this.data[mod.id];
if (modValue) {
if ('values' in mod) {
total += mod.values[modValue];
} else {
total += mod.value;
}
}
}
total += this.additionalRecipientCost * this.additionalRecipientCount;
return total;
}
get chatMessageEffects() {
const list = [];
if (this.hasAdditionalRecipients && this.targets.length > 1) {
list.push(`${this.targets.length - 1} Additional Recipients`);
}
if (this.data.adaptable) {
list.push('Different Trapping (Adaptable Caster)');
}
if (this.data.fatigue) {
list.push('Fatigue (applied to targets)');
}
if (this.data.heavyweapon) {
list.push('Heavy Weapon');
}
if (this.data.lingeringdamage) {
list.push('Lingering Damage');
}
if (this.data.selective) {
list.push('Selective');
}
if (this.isDamaging && this.data.ap > 0) {
list.push(`AP ${this.data.ap}`);
}
if (this.data.range ?? 'none' != 'none') {
list.push(`Range ${this.data.range}`);
}
return list;
}
get chatMessageText() {
let text = `<p>Cast ${this.name}`;
if (this.isTargeted && this.targets.length > 0) {
text += ` on ${this.targets.map((t) => t.name).join(', ')}`;
}
text += '</p>';
const desc = this.description;
if (desc) {
text += `<details open><summary>Description</summary>${desc}</details>`;
}
const effects = this.chatMessageEffects;
if (effects.length > 0) {
text += '<details><summary>Other Effects:</summary><ul><li>' + effects.join('</li><li>') + '</li></ul></details>';
}
return text;
}
async chatMessage() {
return ChatMessage.create(
{
flavor: `Calculated cost: ${this.powerPoints} pp`,
speaker: ChatMessage.getSpeaker(this.source.actor),
content: this.chatMessageText,
whisper: ChatMessage.getWhisperRecipients('GM', game.user.name),
},
{ chatBubble: false },
);
}
}
export class ActorFolderEffect extends PowerEffect {
async init() {
await super.init();
this.packActors = await this.actorFolderPack.getIndex({ fields: ['system.stats.size'] });
this.data.actors = this.prepActors();
}
get actorFolderPackId() {
return 'no.compendium.needs.override';
}
get actorFolderBase() {
return '';
}
get actorFolderPack() {
const pack = game.packs.get(this.actorFolderPackId);
if (!pack) {
return undefined;
}
return game.user.isGM || pack.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER)
? pack
: undefined;
}
get actorFolder() {
return `${this.name}`;
}
getPackFolderByPath(path) {
const names = path.split('/');
if (names[0] === '') {
names.shift();
}
let name = names.shift();
if (!this.actorFolderPack) {
return undefined;
}
let folder = this.actorFolderPack.folders
.filter((f) => f.type === 'Actor' && !f.folder)
.find((f) => f.name === name);
if (!folder) {
return undefined;
}
while (names.length > 0) {
name = names.shift();
folder = folder.children.find((c) => c.folder.name === name);
if (!folder) {
return undefined;
}
folder = folder.folder;
}
return folder;
}
getPackActorsInFolder(inFolder) {
const prefixStack = [''];
const actors = {};
const folderStack = [inFolder];
while (folderStack.length > 0) {
const prefix = prefixStack.shift();
const folder = folderStack.shift();
for (const actor of this.packActors.filter((pa) => pa.folder === folder.id)) {
actors[`${prefix}${actor.name}`] = actor;
}
for (const child of folder.children) {
const newPrefix = `${prefix}${child.folder.name} | `;
prefixStack.push(newPrefix);
folderStack.push(child.folder);
}
}
return actors;
}
prepFolders() {
const folders = [];
const folderNames = [
this.actorFolder,
`${this.actorFolder} - Default`,
`${this.actorFolder}/Default`,
`${this.actorFolder} - ${this.source.name}`,
`${this.actorFolder} - ${this.source.actor.name}`,
`${this.actorFolder}/${this.source.name}`,
`${this.actorFolder}/${this.source.actor.name}`,
];
for (const folderName of folderNames) {
const folder = this.getPackFolderByPath(folderName);
if (folder) {
log(`Found actor folder ${folderName}`);
folders.push(folder);
}
}
if (folders.length > 1) {
folders.shift();
}
return folders;
}
prepActors() {
const folders = this.prepFolders();
const actors = {};
for (const folder of folders) {
const folderActors = this.getPackActorsInFolder(folder);
for (const key in folderActors) {
actors[key] = folderActors[key];
}
}
return actors;
}
// eslint-disable-next-line no-unused-vars
actorValue(actor) {
return 0;
}
getActors() {
const choices = {};
const effects = {};
const values = {};
Object.keys(this.data.actors)
.filter((k) => !k.includes('_template'))
.sort()
.forEach((key) => {
const id = this.data.actors[key].uuid;
choices[id] = key;
effects[id] = null;
values[id] = this.actorValue(this.data.actors[key]);
});
return { choices, effects, values };
}
get modifiers() {
const { choices, effects, values } = this.getActors();
return [
...super.modifiers,
{
name: 'Select Creature',
id: 'actorId',
type: 'select',
choices,
effects,
values,
epic: false,
effect: false,
},
];
}
get spawnUpdates() {
const updates = {
actor: {},
token: {
actorLink: false,
},
embedded: {
ActiveEffect: {},
Item: {},
},
};
return updates;
}
#documentFinder(documentType, oldDoc, newDoc) {
if (documentType === 'Item') {
return oldDoc.name.toLowerCase() === newDoc.name.toLowerCase() && oldDoc.type === newDoc.type;
}
return oldDoc.name.toLowerCase() === newDoc.name.toLowerCase();
}
async updateEmbedded(actor, newDocs) {
const adds = {};
const updates = {};
for (const documentType of Object.keys(newDocs ?? {})) {
const collection = actor.getEmbeddedCollection(documentType);
adds[documentType] = [];
updates[documentType] = [];
log('docType', documentType);
for (const newDocKey in newDocs[documentType]) {
log('newDocKey', newDocKey);
const newDoc = newDocs[documentType][newDocKey].toObject();
const oldDoc = collection.find((doc) => this.#documentFinder(documentType, doc, newDoc));
if (newDoc.type === 'power' && newDoc?.system?.choiceSets?.length > 0) {
newDoc.system.choiceSets = [];
}
if (oldDoc) {
const _id = oldDoc.id;
updates[documentType].push({ ...newDoc, _id });
} else {
adds[documentType].push(newDoc);
}
}
const updateOpts = {};
updateOpts.mbItemCreationSource = moduleName;
if (documentType === 'Item') {
updateOpts.renderSheet = false;
}
try {
if (adds[documentType].length > 0) {
actor.createEmbeddedDocuments(documentType, adds[documentType], updateOpts);
}
} catch (e) {
log('ERROR', e);
}
try {
if (updates[documentType].length > 0) {
actor.updateEmbeddedDocuments(documentType, updates[documentType], updateOpts);
}
} catch (e) {
log('ERROR', e);
}
}
}
async parseValues() {
await super.parseValues();
this.data.maintId = foundry.utils.randomID();
this.targetActor = await game.tcal.importTransientActor(this.data.actorId, { preferExisting: true });
this.targetTokenDoc = await this.targetActor.getTokenDocument();
const perm = CONST?.DOCUMENT_PERMISSION_LEVELS?.OWNER ?? CONST?.DOCUMENT_OWNERSHIP_LEVELS?.OWNER;
const sourceUpdates = {
delta: {
ownership: {
[game.user.id]: perm,
},
},
};
this.targetTokenDoc.updateSource(sourceUpdates);
}
async spawn() {
this.targetTokenDoc.updateSource({
x: this.source.x,
y: this.source.y,
elevation: this.source.elevation,
});
return this.source.scene.createEmbeddedDocuments('Token', [this.targetTokenDoc]);
}
async apply() {
this.data.spawned = await this.spawn();
const updates = this.spawnUpdates;
const secondaryDocs = await this.createSecondaryEffects(this.data.maintId);
const primaryDoc = await this.createPrimaryEffect(this.data.maintId);
const promises = [];
for (const token of this.data.spawned) {
if (updates?.token) {
promises.push(token.update(updates.token));
}
if (updates?.actor) {
promises.push(token.actor.update(updates.actor));
}
if (updates?.embedded) {
promises.push(this.updateEmbedded(token.actor, updates.embedded));
}
const activeEffects = await this.secondaryDocsForTarget(secondaryDocs, token);
activeEffects.push(await this.primaryDocForTarget(primaryDoc, token));
promises.push(this.applyActiveEffects(token, activeEffects));
}
await Promise.all(promises);
}
async createPrimaryEffect(maintId) {
const doc = await super.createPrimaryEffect(maintId);
doc.flags[moduleName].spawnedTempToken = true;
return doc;
}
async sideEffects() {
if (this.data.fatigue) {
for (const target of this.data.spawned) {
const actor = target.actor;
const update = {
system: {
fatigue: {
value: actor.system.fatigue.value + 1,
},
},
};
if (actor.system.fatigue.value < actor.system.fatigue.max) {
await actor.update(update);
}
}
}
}
}
export function embeddedHelperHook(item, data, options) {
if (options?.mbItemCreationSource === moduleName) {
options.renderSheet = false;
}
}