import { moduleName } from './globals.js' const MAINTAIN_ICON = 'icons/magic/symbols/runes-star-blue.webp' 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: `

From ${this.source.name} casting ${this.name}

`, duration: { rounds: 99 }, flags: { [moduleName]: { powerEffect: true }, swade: { loseTurnOnHold: false, expiration: CONFIG.SWADE.CONST.STATUS_EFFECT_EXPIRATION.StartOfTurnAuto } } } } async applyActiveEffects (token, 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 modifiers () { const mods = [] mods.push({ name: 'Adaptable Caster', id: 'adaptable', value: 1, epic: false, effect: false }) mods.push({ name: 'Fatigue', id: 'fatigue', value: 2, epic: false, effect: false }) mods.push({ name: 'Glow', id: 'glow', value: 1, epic: false, effect: true, 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 } ] }) mods.push({ name: 'Shroud', id: 'shroud', value: 1, epic: false, effect: true, 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 } ] }) if (this.isDamaging) { mods.push({ name: 'Heavy Weapon', id: 'heavyweapon', value: 2, epic: false, effect: false}) } mods.push({ name: 'Hinder', id: 'hinder', value: 1, epic: false, effect: true, 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 } ] }) mods.push({ name: 'Hurry', id: 'hurry', value: 1, epic: false, effect: true, 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.isDamaging) { mods.push({ name: 'Lingering Damage', id: 'lingeringdamage', value: 2, epic: false, effect: false}) } mods.push({ name: 'Selective', id: 'selective', value: 1, epic: false, effect: false }) return mods } get menuData () { return { inputs: this.menuInputs, buttons: this.menuButtons, } } get menuInputs () { const inputs = [ { type: 'header', label: `${this.name} Effect` }, { type: 'info', label: `Apply ${this.name} Effect` }, ] if (this.isTargeted) { let label = `Targets: ${this.targets.map(t => t.name).join(',')}` if (this.targets.length > 1 && this.hasAdditionalRecipients) { label += ` (${this.targets.length - 1} additional recipients ` + `+${this.additionalRecipientCost} ea.)` } inputs.push({ type: 'info', label: label }) } for (const mod of this.modifiers) { inputs.push({ type: 'checkbox', label: ( `${mod.epic ? '⭐ ' : ''}${mod.name} ` + `(${mod.value >= 0 ? '+' : ''}${mod.value})` ), }) } if (this.isDamaging) { inputs.push({ type: 'select', label: 'Armor Piercing', options: [ {html: 'None', value: 0, selected: true}, {html: 'AP 2 (+1)', value: 1, selected: false}, {html: 'AP 4 (+2)', value: 2, selected: false}, {html: 'AP 6 (+3)', value: 3, selected: false}, ] }) } inputs.push({type: 'select', label: 'Range', options: [ {html: 'Normal Range', value: 0, selected: true}, {html: 'Range ×2 (+1)', value: 1, selected: false}, {html: 'Range ×3 (+2)', value: 2, selected: false} ] }) return inputs } get menuButtons () { const data = [ { label: 'Apply', value: 'apply' }, { label: 'Apply with Raise', value: 'raise' }, { label: 'Cancel', value: 'cancel' } ] return data } get menuOptions () { return { title: `${this.name} Effect`, defaultButton: 'Cancel', options: {} } } async powerEffect () { const { buttons, inputs } = await warpgate.menu( this.menuData, this.menuOptions ) if (buttons && buttons !== 'cancel') { this.data.button = buttons this.data.values = inputs await this.parseValues() await this.apply() await this.sideEffects() await this.chatMessage() } } async parseValues () { this.data.rawValues = deepClone(this.data.values) this.data.raise = this.data.button === 'raise' for (let i = 0; i < 2; i++) { this.data.values.shift() } if (this.isTargeted) { this.data.values.shift() } this.data.mods = new Set() for (const mod of this.modifiers) { const modEnabled = this.data.values.shift() if (modEnabled) { this.data.mods.add(mod.id) } } if (this.isDamaging) { this.data.armorPiercing = this.data.values.shift() } this.data.range = this.data.values.shift() } async createSecondaryEffects (maintId) { const docs = [] for (const mod of this.modifiers) { if (this.data.mods.has(mod.id) && mod.effect) { const doc = this.createEffectDocument(mod.icon, mod.name, mod.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 } async createPrimaryEffect (maintId) { const doc = this.createEffectDocument(this.icon, this.effectName, this.getPrimaryEffectChanges()) doc.flags[moduleName].maintId = maintId doc.duration.seconds = 594 return doc } async createMaintainEffect (maintId) { const doc = this.createEffectDocument( MAINTAIN_ICON, `Maintaining ${this.name}`, []) doc.duration.rounds = this.duration doc.flags.swade.expiration = CONFIG.SWADE.CONST.STATUS_EFFECT_EXPIRATION.StartOfTurnPrompt 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) } // eslint-disable-next-line no-unused-vars async primaryDocForTarget(doc, target) { const newDoc = deepClone(doc) 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.mods.has('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) { if (this.data.mods.has(mod.id)) { total += mod.value } } if (this.targets.length > 1 && this.hasAdditionalRecipients) { total += (this.targets.length - 1) * this.additionalRecipientCost } total += this.data.range if (this.isDamaging) { total += this.data.ap } return total } get chatMessageEffects () { const list = [] if (this.hasAdditionalRecipients && this.targets.length > 1) { list.push(`${this.targets.length - 1} Additional Recipients`) } if (this.data.mods.has('adaptable')) { list.push('Different Trapping (Adaptable Caster)') } if (this.data.mods.has('fatigue')) { list.push('Fatigue (applied to targets') } if (this.data.mods.has('heavyweapon')) { list.push('Heavy Weapon') } if (this.data.mods.has('lingeringdamage')) { list.push('Lingering Damage') } if (this.data.mods.has('selective')) { list.push('Selective') } if (this.isDamaging && this.data.armorPiercing > 0) { list.push(`AP ${this.data.armorPiercing * 2}`) } if (this.data.range > 0) { list.push(`Range ×${this.data.range +1}`) } return list } get chatMessageText () { let text = `

Cast ${this.name}` if (this.targets.length > 0) { text += ` on ${this.targets.map(t => t.name).join(', ')}` } text += '

' const effects = this.chatMessageEffects if (effects.length > 0) { text += '
Other Effects:
' } 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 }); } }