971 lines
26 KiB
JavaScript
971 lines
26 KiB
JavaScript
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',
|
||
];
|
||
|
||
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 FormApplication {
|
||
constructor(powerEffect) {
|
||
super();
|
||
const name = powerEffect.name.replaceAll(/[^a-zA-Z]/g, '');
|
||
this.options.id = `${this.options.id}${name}`;
|
||
this.powerEffect = powerEffect;
|
||
}
|
||
|
||
static get defaultOptions() {
|
||
return foundry.utils.mergeObject(super.defaultOptions, {
|
||
classes: ['sheet', 'mbSwadeForm', 'mbSwadePowerEffectsForm'],
|
||
popOut: true,
|
||
template: templates['powerDialog.html'],
|
||
id: ['mbSwadePowerEffectsApplication'],
|
||
title: 'Power Effects',
|
||
width: 400,
|
||
});
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
getData() {
|
||
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}`;
|
||
}
|
||
}
|
||
}
|
||
const data = {
|
||
name: 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,
|
||
};
|
||
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;
|
||
}
|
||
|
||
async _updateObject(ev, formData) {
|
||
formData.submit = ev?.submitter?.value ?? 'cancel';
|
||
if (formData.submit !== 'cancel') {
|
||
this.powerEffect.formData = formData;
|
||
this.powerEffect.applyEffect();
|
||
}
|
||
}
|
||
}
|
||
|
||
export class PowerEffect {
|
||
constructor(token, targets) {
|
||
this.source = token;
|
||
this.targets = targets;
|
||
this.data = {};
|
||
}
|
||
|
||
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 {
|
||
get actorFolderBase() {
|
||
return 'PowerActors';
|
||
}
|
||
|
||
get actorFolder() {
|
||
return `${this.actorFolderBase}/${this.name}`;
|
||
}
|
||
|
||
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 = moduleHelpers.getActorFolderByPath(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 = moduleHelpers.getActorsInFolder(folder);
|
||
for (const key in folderActors) {
|
||
actors[key] = folderActors[key];
|
||
}
|
||
}
|
||
return actors;
|
||
}
|
||
|
||
// eslint-disable-next-line no-unused-vars
|
||
actorValue(actor) {
|
||
return 0;
|
||
}
|
||
|
||
getActors() {
|
||
this.data.actors = this.prepActors();
|
||
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].id;
|
||
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.actors.get(this.data.actorId);
|
||
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;
|
||
}
|
||
}
|