582 lines
15 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 { templates } from '../preloadTemplates.js';
import { moduleName } from '../globals.js';
const MAINTAIN_ICON = 'icons/magic/symbols/runes-star-blue.webp';
export class PowerFormApplication extends FormApplication {
constructor(powerEffect) {
super();
this.powerEffect = powerEffect;
}
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
classes: ['sheet', 'mbSwadeForm', 'mbSwadePowerEffectsForm'],
popOut: true,
template: templates['powerDialog.html'],
id: ['mbSwadePowerEffectsApplication'],
title: 'Power Effects',
width: 350,
});
}
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 = 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.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,
},
targets: [],
buttons: this.powerEffect.menuButtons,
};
if (this.powerEffect.isTargeted) {
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.targets.length - 1;
data.recipients.total = data.recipients.cost * data.recipients.count;
}
return data;
}
async _updateObject(ev, formData) {
formData.submit = ev?.submitter?.value ?? 'cancel';
if (formData.submit !== 'cancel') {
this.powerEffect.formData = formData;
await 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 = deepClone(CONFIG.statusEffects.find((se) => se.label === label));
effect.name = 'name' in effect ? effect.name : effect.label;
if (!('flags' in effect)) {
effect.flags = {};
}
if (favorite) {
if (!('swade' in effect.flags)) {
effect.flags.swade = {};
}
effect.flags.swade.favorite = true;
}
effect.flags.core = { statusId: 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: { rounds: 99 },
flags: {
[moduleName]: {
powerEffect: true,
},
swade: {
loseTurnOnHold: false,
expiration: CONFIG.SWADE.CONST.STATUS_EFFECT_EXPIRATION.StartOfTurnAuto,
},
},
};
}
async applyActiveEffects(token, effectDocuments) {
await token.actor.createEmbeddedDocuments('ActiveEffect', effectDocuments);
// const mutation = {
// embedded: { ActiveEffect: {} },
// };
// const mutateOptions = {
// permanent: true,
// description: `${this.source.name} applying ${effectDocuments[effectDocuments.length - 1]?.name} to ${token.name}`,
// };
// for (const effectDocument of effectDocuments) {
// mutation.embedded.ActiveEffect[effectDocument.name] = effectDocument;
// }
// await warpgate.mutate(token.document, mutation, {}, mutateOptions);
}
get name() {
return 'Unknown Power';
}
get effectName() {
return this.name;
}
get icon() {
return 'icons/magic/symbols/question-stone-yellow.webp';
}
get duration() {
return 5;
}
get basePowerPoints() {
return 0;
}
get usePrimaryEffect() {
return true;
}
get hasAdditionalRecipients() {
return false;
}
get isDamaging() {
return false;
}
get additionalRecipientCost() {
return 0;
}
get isTargeted() {
return false;
}
get oneTarget() {
return false;
}
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.stats.speed.value',
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.stats.speed.value',
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,
});
}
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;
for (const mod of this.modifiers) {
this.data[mod.id] = this.formData[mod.id];
}
}
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.createEffectDocument(icon, name, changes);
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;
} else {
doc.duration.seconds = 594;
doc.flags[moduleName].maintId = maintId;
}
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;
}
get description() {
return '';
}
async createPrimaryEffect(maintId) {
const doc = this.createEffectDocument(this.icon, this.effectName, this.getPrimaryEffectChanges());
doc.description += this.description;
doc.flags[moduleName].maintId = maintId;
doc.duration.seconds = 594;
return doc;
}
async createMaintainEffect(maintId) {
let icon = MAINTAIN_ICON;
if (!this.usePrimaryEffect) {
icon = this.icon;
}
const doc = this.createEffectDocument(icon, `Maintaining ${this.effectName}`, []);
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.flags[moduleName].maintainingId = maintId;
doc.flags[moduleName].targetIds = this.targets.map((t) => t.id);
return doc;
}
// eslint-disable-next-line no-unused-vars
async secondaryDocsForTarget(docs, target) {
return deepClone(docs);
}
async primaryDocForTarget(doc, target) {
const newDoc = deepClone(doc);
newDoc.flags[moduleName].maintainingId = doc.flags[moduleName].maintId;
newDoc.flags[moduleName].targetIds = [target.id];
return newDoc;
}
async apply() {
const maintId = randomID();
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);
}
}
}
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;
}
}
}
if (this.targets.length > 1 && this.hasAdditionalRecipients) {
total += (this.targets.length - 1) * this.additionalRecipientCost;
}
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') {
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 },
);
}
}