diff --git a/src/module/powers/basePowers.js b/src/module/powers/basePowers.js index 373d6eb..a82bcc0 100644 --- a/src/module/powers/basePowers.js +++ b/src/module/powers/basePowers.js @@ -117,7 +117,7 @@ export class PowerFormApplication extends FormApplication { formData.submit = ev?.submitter?.value ?? 'cancel'; if (formData.submit !== 'cancel') { this.powerEffect.formData = formData; - await this.powerEffect.applyEffect(); + this.powerEffect.applyEffect(); } } } @@ -447,6 +447,29 @@ export class PowerEffect { } } + 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; + } 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) { @@ -455,26 +478,7 @@ export class PowerEffect { 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); - 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; - } else { - doc.flags[moduleName].maintId = maintId; - if (moduleHelpers.useVAE) { - doc.flags['visual-active-effects'] = { - data: { - inclusion: 1, - }, - }; - } else { - doc.duration.seconds = 594; - } - } + const doc = this.enhanceSecondaryEffect(maintId, this.createEffectDocument(icon, name, changes)); docs.push(doc); } } diff --git a/src/module/powers/blind.js b/src/module/powers/blind.js index 1c0b7f5..77675c0 100644 --- a/src/module/powers/blind.js +++ b/src/module/powers/blind.js @@ -69,7 +69,7 @@ export class BlindEffect extends PowerEffect { const docs = await super.createSecondaryEffects(maintId); if (this.data.raise) { const strong = this.data.strong; - const doc = this.createEffectDocument(this.icon, `Blinded (${strong ? 'Strong, ' : ''}Raise)`, [ + let doc = this.createEffectDocument(this.icon, `Blinded (${strong ? 'Strong, ' : ''}Raise)`, [ { key: 'system.stats.globalMods.trait', value: -2, @@ -77,9 +77,8 @@ export class BlindEffect extends PowerEffect { mode: foundry.CONST.ACTIVE_EFFECT_MODES.ADD, }, ]); - doc.duration.seconds = 594; + doc = this.enhanceSecondaryEffect(maintId, doc); doc.description = this.description + '

This is the raise effect which can be shaken off separately.

'; - doc.flags[moduleName].maintId = maintId; docs.push(doc); } return docs; diff --git a/src/module/powers/boostLowerTrait.js b/src/module/powers/boostLowerTrait.js index 63d5592..22ef770 100644 --- a/src/module/powers/boostLowerTrait.js +++ b/src/module/powers/boostLowerTrait.js @@ -70,10 +70,8 @@ export class BoostLowerTraitEffect extends PowerEffect { mode: foundry.CONST.ACTIVE_EFFECT_MODES.ADD, }, ]; - const doc = this.createEffectDocument(this.icon, name, changes); - doc.duration.seconds = 594; + const doc = this.enhanceSecondaryEffect(maintId, this.createEffectDocument(this.icon, name, changes)); doc.description = this.description + '

This is the raise effect which can be shaken off separately.

'; - doc.flags[moduleName].maintId = maintId; docs.push(doc); } return docs; diff --git a/src/module/powers/powers.js b/src/module/powers/powers.js index 236c568..ddb03f3 100644 --- a/src/module/powers/powers.js +++ b/src/module/powers/powers.js @@ -56,6 +56,7 @@ import { SmiteEffect } from './smite.js'; import { SoundSilenceEffect } from './soundSilence.js'; import { SpeakLanguageEffect } from './speakLanguage.js'; import { StunEffect } from './stun.js'; +import { SummonAllyEffect } from './summon.js'; const PowerClasses = { 'arcane-protection': ArcaneProtectionEffect, @@ -132,6 +133,7 @@ const PowerClasses = { sound: SoundSilenceEffect, 'speak-language': SpeakLanguageEffect, stun: StunEffect, + 'summon-ally': SummonAllyEffect, }; /* ---------------------------------------------------------------- */ diff --git a/src/module/powers/summon.js b/src/module/powers/summon.js index c0657e5..2b422ef 100644 --- a/src/module/powers/summon.js +++ b/src/module/powers/summon.js @@ -1,4 +1,5 @@ /* globals Portal */ +import { moduleName } from '../globals.js'; import { ActorFolderEffect } from './basePowers.js'; class BaseSummonEffect extends ActorFolderEffect { @@ -80,16 +81,79 @@ class BaseSummonEffect extends ActorFolderEffect { if (!(this.hasIncreasedTrait && this?.data?.increasedTrait)) { return; } + const skillSet = new Set(); + for (const skill of this.targetActor.items.filter((i) => i.type === 'skill')) { + skillSet.add(skill.name); + } + for (const item of Object.values(this.data.embeddedUpdates.Item).filter((i) => i.type === 'skill')) { + skillSet.add(item.name); + } + const skillList = Array.from(skillSet); + skillList.sort(); + const attrList = ['Agility', 'Smarts', 'Spirit', 'Strength', 'Vigor']; + let html = `

Increase Attributes

(+1 pp each)

`; + html += attrList + .map( + (name) => `
+ +
`, + ) + .join(''); + html += `

Increase Skills

(+1 pp each)

`; + html += skillList + .map( + (name) => `
+ +
`, + ) + .join(''); + const formData = await Dialog.wait({ + title: 'Select Trait increases', + content: html, + buttons: { + submit: { + label: 'Submit', + callback: (html) => { + const formElement = html[0].querySelector('form'); + const formData = new FormDataExtended(formElement); + return formData.object; + }, + }, + cancel: { label: 'Cancel' }, + }, + }); + const mode = CONST.ACTIVE_EFFECT_MODES.ADD; + const value = 2; + const priority = 0; + this.data.increasedTraitChanges = []; + for (const attr of attrList) { + if (formData[attr]) { + this.data.increasedTraitChanges.push({ + key: `system.attributes.${attr.toLowerCase()}.die.sides`, + value, + mode, + priority, + }); + } + } + for (const skill of skillList) { + if (formData[skill]) { + this.data.increasedTraitChanges.push({ + key: `@Skill{${skill}}[system.die.sides]`, + value, + mode, + priority, + }); + } + } } + async prePrep() {} + async parseValues() { await super.parseValues(); this.data.actorUpdates = { name: `${this.source.actor.name}'s summoned ${this.targetActor.name}`, - system: { - wildcard: this.source.actor.system.wildcard, - attributes: {}, - }, }; this.data.tokenUpdates = { actorLink: false, @@ -103,9 +167,35 @@ class BaseSummonEffect extends ActorFolderEffect { ActiveEffect: {}, Item: {}, }; + this.data.primaryEffectChanges = []; + await this.prePrep(); await this.prepIncreasedTrait(); } + getPrimaryEffectChanges() { + return [...super.getPrimaryEffectChanges(), ...this.data.primaryEffectChanges]; + } + + async createSecondaryEffects(maintId) { + const effects = await super.createSecondaryEffects(maintId); + if (this.data.mindRider) { + const doc = this.enhanceSecondaryEffect( + maintId, + this.createEffectDocument('icons/magic/control/hypnosis-mesmerism-eye.webp', 'Mind Rider', []), + ); + doc.description = `The caster can communicate and sense through the ally`; + effects.push(doc); + } + if ((this.data?.increasedTraitChanges?.length ?? 0) > 0) { + const doc = this.enhanceSecondaryEffect( + maintId, + this.createEffectDocument(this.icon, 'Increased Trait', this.data.increasedTraitChanges), + ); + effects.push(doc); + } + return effects; + } + get spawnUpdates() { const updates = super.spawnUpdates; mergeObject(updates.actor, this.data.actorUpdates); @@ -116,14 +206,23 @@ class BaseSummonEffect extends ActorFolderEffect { async spawn() { const spawned = await new Portal() - .addCreature(this.targetTokenDoc) + .addCreature(this.targetTokenDoc, { count: this.summonCount }) .texture(this.targetTokenDoc.texture.src) .spawn(); return spawned; } + + async apply() { + await super.apply(); + const maintainDoc = await this.createMaintainEffect(this.data.maintId); + maintainDoc.flags[moduleName].targetIds = this.data.spawned.map((t) => t.id); + if (this.duration > 0) { + await this.applyActiveEffects(this.source, [maintainDoc]); + } + } } -export class SummonAllyEffect extends BaseSummonEffect() { +export class SummonAllyEffect extends BaseSummonEffect { get name() { return 'Summon Ally'; } @@ -132,6 +231,18 @@ export class SummonAllyEffect extends BaseSummonEffect() { return 0; } + get duration() { + return 5; + } + + get icon() { + return 'icons/magic/control/silhouette-hold-beam-blue.webp'; + } + + get isTargeted() { + return false; + } + get values() { return { attendant: 1, @@ -181,6 +292,86 @@ export class SummonAllyEffect extends BaseSummonEffect() { epic: false, effect: false, }); + const { choices, effects, values } = this._edges; return mods; } + + async prePrep() { + await super.prePrep(); + if (this.data.raise && this.data.actors['raise_template']) { + const raiseTemplate = this.summonableActors.raise_template; + for (const item of raiseTemplate.items) { + const raiseItemDoc = await raiseTemplate.getEmbeddedDocument('Item', item.id); + this.data.embeddedUpdates.Item[item.name] = raiseItemDoc; + } + } + if (this.targetActor.name !== 'Mirror Self') { + return; + } + const mirrorActor = this.source.actor; + mergeObject(this.data.actorUpdates, { + system: mirrorActor.system + .clone({ + 'fatigue.value': 0, + 'wounds.value': 0, + 'wounds.max': 0, + 'bennies.max': 0, + 'bennies.value': 0, + }) + .toObject(), + name: `Mirror ${mirrorActor.name}`, + img: mirrorActor.img, + }); + this.data.actorUpdates.system.wildcard = false; + mergeObject(this.data.tokenUpdates, { + name: `Mirror ${this.source.name}`, + texture: { + src: this.source.document.texture.src, + scaleX: this.source.document.texture.scaleX * -1, + scaleY: this.source.document.texture.scaleY, + }, + }); + this.data.mirrorChanges = []; + for (const mirrorItem of mirrorActor.items) { + if ( + mirrorItem.type === 'power' && + (mirrorItem.system?.swid === 'summon-ally' || mirrorItem.name === 'Summon Ally') + ) { + continue; + } + if (['weapon', 'armor', 'consumable', 'gear'].includes(mirrorItem.type)) { + continue; + } + this.data.embeddedUpdates.Item[mirrorItem.name] = await mirrorActor.getEmbeddedDocument('Item', mirrorItem.id); + if (mirrorItem.type === 'skill') { + this.data.mirrorChanges.push({ + key: `@Skill{${mirrorItem.name}}[system.die.sides]`, + mode: CONST.ACTIVE_EFFECT_MODES.ADD, + value: -2, + priority: 0, + }); + } + } + } + + async parseValues() { + await super.parseValues(); + } + + async createSecondaryEffects(maintId) { + const effects = await super.createSecondaryEffects(maintId); + if ((this.data?.mirrorChanges?.length ?? 0) > 0) { + const doc = this.enhanceSecondaryEffect( + maintId, + this.createEffectDocument( + 'icons/magic/control/mouth-smile-deception-purple.webp', + 'Mirror Self', + this.data.mirrorChanges, + ), + ); + doc.description = 'A mirror self ally has skills 1 die type worse than the caster'; + effects.push(doc); + } + return effects; + } }