import { CONST, log, shim } from './shim.js' class PowerEffect { constructor (token, targets) { this.token = token this.targets = targets this.effectDocs = [] this.menuData = { inputs: [ { type: 'header', label: `${this.name} Effect` }, { type: 'info', label: `Apply ${this.name} Effect` }, { type: 'header', label: 'Global Modifiers' }, { type: 'checkbox', label: 'Glow (+1)' }, { type: 'checkbox', label: 'Shroud (+1)' }, { type: 'checkbox', label: 'Hinder (+1)' }, { type: 'checkbox', label: 'Hurry (+1)' }, { type: 'header', label: '---------------' } ], buttons: [ { label: 'Apply', value: 'apply' }, { label: 'Apply with Raise', value: 'raise' }, { label: 'Cancel', value: 'cancel' } ] } this.menuOptions = { title: `${this.name} Effect`, defaultButton: 'Cancel', options: {} } this.inputs = [] this.buttons = null } get name () { return 'Unknown Power' } get durationRounds () { return this.baseDurationRounds } get baseDurationRounds () { return 5 } async powerEffect () { try { await this.prepMenu() } catch (e) { log('Error preparing menu for power effect: ' + e.toString()) return } const { buttons, inputs } = await shim.warpgateMenu( this.menuData, this.menuOptions) this.buttons = buttons this.inputs = inputs if (this.buttons && this.buttons !== 'cancel') { this.globalModifierEffects() await this.prepResult() await this.applyResult() } } async prepResult () { } async applyResult () { for (const target of this.targets) { shim.applyActiveEffects(target, this.effectDocs) } } static modEffectDoc (icon, name, key, value, durationRounds) { return shim.createEffectDocument(icon, name, durationRounds, [ { key, mode: CONST.FOUNDRY.ACTIVE_EFFECT_MODES.ADD, value, priority: 0 } ]) } static glow (durationRounds) { return PowerEffect.modEffectDoc( 'icons/magic/light/orb-shadow-blue.webp', 'Glow', '@Skill{Stealth}[system.die.modifier]', -2, durationRounds) } static shroud (durationRounds) { return PowerEffect.modEffectDoc( 'icons/magic/perception/shadow-stealth-eyes-purple.webp', 'Shroud', '@Skill{Stealth}[system.die.modifier]', 1, durationRounds) } static hinder (durationRounds) { return PowerEffect.modEffectDoc( 'icons/magic/control/debuff-chains-shackle-movement-red.webp', 'Hinder', 'system.stats.speed.value', -2, durationRounds) } static hurry (durationRounds) { return PowerEffect.modEffectDoc( 'icons/skills/movement/feet-winged-sandals-tan.webp', 'Hurry', 'system.stats.speed.value', 2, durationRounds) } globalModifierEffects () { this.inputIndex = 8 if (this.inputs[3]) { // glow this.effectDocs.push(PowerEffect.glow(this.durationRounds)) } if (this.inputs[4]) { // shroud this.effectDocs.push(PowerEffect.shroud(this.durationRounds)) } if (this.inputs[5]) { // hinder this.effectDocs.push(PowerEffect.hinder(this.durationRounds)) } if (this.inputs[6]) { // hurry this.effectDocs.push(PowerEffect.hurry(this.durationRounds)) } } } class TargetedPowerEffect extends PowerEffect { constructor (token, targets) { super(token, targets) const targetList = this.targets.map(t => t.name).join(', ') this.menuData.inputs[1] = { type: 'info', label: `Apply ${this.name} Effect to ${targetList}` } } async powerEffect () { if (this.targets.length < 1) { shim.notifications.error(`No target selected for ${this.name}`) return } super.powerEffect() } } class BlindEffect extends TargetedPowerEffect { async prepMenu (token, targets) { this.menuData.inputs.push({ type: 'checkbox', label: 'Strong (+1 point)', options: false }) } get name () { return 'Blind' } get baseDurationRounds () { return 1 } async prepResult () { const raise = (this.buttons === 'raise') const strong = !!this.inputs[this.inputIndex] const icon = 'icons/skills/wounds/injury-eyes-blood-red.webp' const changes = [ { key: 'system.stats.globalMods.trait', mode: CONST.FOUNDRY.ACTIVE_EFFECT_MODES.ADD, value: '-2', priority: 0 } ] this.effectDocs.push( shim.createEffectDocument( icon, `minor Blindness (Vigor ${strong ? '-2 ' : ''}ends)`, this.durationRounds, changes)) if (raise) { this.effectDocs.push( shim.createEffectDocument( icon, `major Blindness (Vigor ${strong ? '-2 ' : ''}ends)`, this.durationRounds, changes) ) } } } class BoostLowerTraitEffect extends TargetedPowerEffect { get name () { return 'Boost/Lower Trait' } get baseDurationRounds () { if (!this.inputs) { return 1 } if (this.inputs[this.inputs.length - 4]) { // Boost return 5 } return 1 // Lower } async prepMenu () { let traitOptions = ['Agility', 'Smarts', 'Spirit', 'Strength', 'Vigor'] const allSkills = [] const traits = {} for (const traitName of traitOptions) { const lower = traitName.toLowerCase() traits[traitName] = { name: traitName, type: 'attribute', modkey: `system.attributes.${lower}.die.modifier`, diekey: `system.attributes.${lower}.die.sides` } } for (const token of this.targets) { const skills = token.actor.items.filter(item => item.type === 'skill') for (const skill of skills) { const name = skill.name traits[name] = { name, type: 'skill', modkey: `@Skill{${name}}[system.die.modifier]`, diekey: `@Skill{${name}}[system.die.sides]` } if (name !== 'Unskilled' && !allSkills.find(v => v === name)) { allSkills.push(name) } } traitOptions = traitOptions.concat(allSkills.sort()) } this.menuData.inputs = this.menuData.inputs.concat( { type: 'select', label: 'Trait', options: traitOptions }, { type: 'info', label: 'Boost or Lower?' }, { type: 'radio', label: 'Boost', options: ['isBoost', true] }, { type: 'radio', label: 'Lower', options: ['isBoost', false] }, { type: 'checkbox', label: 'Greater', options: false }, { type: 'checkbox', label: 'Strong (lower only)', options: false } ) this.traits = traits } async prepResult () { const raise = (this.buttons === 'raise') const direction = this.inputs[this.inputs.length - 4] ? 'Boost' : 'Lower' const durationRounds = (direction === 'Boost' ? 5 : 1) const icon = (direction === 'Boost' ? 'icons/magic/life/cross-embers-glow-yellow-purple.webp' : 'icons/magic/movement/chevrons-down-yellow.webp') const trait = this.traits[this.inputs[this.inputIndex]] const greater = !!this.inputs[this.inputIndex + 4] const strong = !!this.inputs[this.inputIndex + 5] let namePart = `${direction} ${trait.name}` const mods = [] if (direction === 'Lower') { mods.push(`Spirit${strong ? '-2' : ''} ends`) } if (greater) { mods.push('greater') } if (mods.length > 0) { namePart = `${namePart} (${mods.join(', ')})` } const mode = CONST.FOUNDRY.ACTIVE_EFFECT_MODES.ADD const modValue = (direction === 'Boost' ? '+2' : '-2') const minorEffect = shim.createEffectDocument( icon, `minor ${namePart}`, durationRounds, [ { key: trait.diekey, mode, value: modValue, priority: 0 } ]) if (direction === 'Lower' && greater) { minorEffect.changes.push({ key: trait.modkey, mode, value: modValue, priority: 0 }) } const majorEffect = shim.createEffectDocument( icon, `major ${namePart}`, durationRounds, [ { key: trait.diekey, mode, value: modValue, priority: 0 } ]) this.effectDocs.push(minorEffect) if (raise) { this.effectDocs.push(majorEffect) } } } class ConfusionEffect extends TargetedPowerEffect { get name () { return 'Confusion' } get baseDurationRounds () { return 1 } async prepMenu () { this.menuData.inputs.push( { type: 'checkbox', label: 'Greater (adds Shaken)', options: false }) this.menuData.buttons = [ { label: 'Distracted', value: 'distracted' }, { label: 'Vulnerable', value: 'vulnerable' }, { label: 'Raise (both)', value: 'raise' }, { label: 'Cancel', value: 'cancel' } ] } async prepResult () { const greater = !!this.inputs[this.inputIndex] if (this.buttons === 'distracted' || this.buttons === 'raise') { this.effectDocs.push(shim.getStatus('SWADE.Distr', 'Distracted')) } if (this.buttons === 'vulnerable' || this.buttons === 'raise') { this.effectDocs.push(shim.getStatus('SWADE.Vuln', 'Vulnerable')) } if (greater) { this.effectDocs.push(shim.getStatus('SWADE.Shaken', 'Shaken')) } } } class DeflectionEffect extends TargetedPowerEffect { get name () { return 'Deflection' } get baseDurationRounds () { return 5 } async prepMenu () { this.menuData.buttons = [ { label: 'Melee', value: 'melee' }, { label: 'Ranged', value: 'ranged' }, { label: 'Raise (both)', value: 'raise' }, { label: 'Cancel', value: 'cancel' } ] } async prepResult () { const effectName = `Deflection (${this.buttons === 'raise' ? 'all' : this.buttons})` const icon = 'icons/magic/defensive/shield-barrier-deflect-teal.webp' this.effectDocs.push(shim.createEffectDocument(icon, effectName, this.durationRounds)) } } class EntangleEffect extends TargetedPowerEffect { get name () { return 'Entangle' } get baseDurationRounds () { return 1 } async prepMenu () { this.menuData.inputs = this.menuData.inputs.concat([ { type: 'radio', label: 'Not Damaging', options: ['dmg', true] }, { type: 'radio', label: 'Damaging', options: ['dmg', false] }, { type: 'radio', label: 'Deadly', options: ['dmg', false] }, { type: 'checkbox', label: 'Tough', options: false } ]) this.menuData.buttons = [ { label: 'Entangled', value: 'apply' }, { label: 'Bound (raise)', value: 'raise' }, { label: 'Cancel', value: 'cancel' } ] } async prepResult () { const damage = (this.inputs[this.inputIndex + 1] ? '2d4' : (this.inputs[this.inputIndex + 2] ? '2d6' : null)) const tough = !!this.inputs[this.inputIndex + 3] const effectSearch = (this.buttons === 'raise' ? 'SWADE.Bound' : 'SWADE.Entangled') const effectName = (this.buttons === 'raise' ? 'Bound' : 'Entangled') const effect = shim.getStatus(effectSearch, effectName) const extraIcon = 'icons/magic/nature/root-vine-barrier-wall-brown.webp' const extraEffect = shim.createEffectDocument(extraIcon, 'Entangle Modifier', this.durationRounds, []) if (damage) { extraEffect.name = `${extraEffect.name} - ${damage} dmg` } if (tough) { extraEffect.name = `Tough ${extraEffect.name}` } this.effectDocs.push(effect) if (damage || tough) { this.effectDocs.push(extraEffect) } } } class IntangibilityEffect extends TargetedPowerEffect { get name () { return 'Intangility' } get baseDurationRounds () { if (!this.inputs) { return 5 } if (this.inputs[this.inputs.length - 1]) { // Duration return 50 } return 5 // no duration } async prepMenu () { this.menuData.inputs.push({ type: 'checkbox', label: 'Duration', options: false }) this.menuData.buttons = [ { label: 'Apply', value: 'apply' }, { label: 'Cancel', value: 'cancel' } ] } async prepResult () { const icon = 'icons/magic/control/debuff-energy-hold-levitate-blue-yellow.webp' const effect = shim.createEffectDocument(icon, this.name, this.durationRounds, []) this.effectDocs.push(effect) } } class InvisibilityEffect extends TargetedPowerEffect { get name () { return 'Invisiblity' } get baseDurationRounds () { if (!this.inputs) { return 5 } if (this.inputs[this.inputs.length - 1]) { // Duration return 50 } return 5 // no duration } async prepMenu () { this.menuData.inputs.push({ type: 'checkbox', label: 'Duration', options: false }) } async prepResult () { const effect = shim.getStatus('EFFECT.StatusInvisible', 'Invisible') effect.duration = { rounds: this.durationRounds } this.effectDocs.push(effect) } } class ProtectionEffect extends TargetedPowerEffect { get name () { return 'Protection' } get baseDurationRounds () { return 5 } async prepMenu () { this.menuData.buttons = [ { label: 'Apply (+2 armor)', value: 'apply' }, { label: 'Apply with raise (+2 toughness)', value: 'raise' }, { label: 'Cancel', value: 'cancel' } ] } async prepResult () { const effect = shim.getStatus('SWADE.Protection', 'Protection') effect.duration = { rounds: this.durationRounds } const mode = CONST.FOUNDRY.ACTIVE_EFFECT_MODES.ADD effect.changes = [ { key: 'system.stats.toughness.armor', mode, value: 2, priority: 0 } ] if (this.buttons === 'raise') { effect.changes[0].key = 'system.stats.toughness.value' } this.effectDocs.push(effect) } } class SmiteEffect extends TargetedPowerEffect { get name () { return 'Smite' } get baseDurationRounds () { return 5 } async prepMenu () { this.menuData.inputs.push({ type: 'checkbox', label: 'Greater', options: false }) const tokenWeapons = {} let index = this.menuData.inputs.length - 1 for (const token of this.targets) { index += 2 tokenWeapons[token.id] = index this.menuData.inputs.push({ type: 'info', label: `

${token.name}

` }) const weapons = token.actor.items.filter(i => i.type === 'weapon').map( i => { return { value: i.name, html: i.name } }) weapons.unshift({ value: '', html: 'None' }) this.menuData.inputs.push({ type: 'select', label: token.name, options: weapons }) } this.tokenWeapons = tokenWeapons } async prepResult () { this.baseEffect = shim.getStatus('SWADE.Smite', 'Smite') } async applyResult () { const mode = CONST.FOUNDRY.ACTIVE_EFFECT_MODES.ADD const raise = (this.buttons === 'raise') const greater = !!this.inputs[this.inputIndex] const changeValue = (greater ? (raise ? '+6' : '+4') : (raise ? '+4' : '+2')) for (const token of this.targets) { const weaponName = this.inputs[this.tokenWeapons[token.id]] const weaponId = token.actor.items.getName(weaponName)?.id const changeKey = `@Weapon{${weaponName}}[system.actions.dmgMod]` if (!weaponId) { continue } const effectName = `${this.buttons === 'raise' ? 'major' : 'minor'} Smite${greater ? ' (greater)' : ''} (${weaponName})` const changes = [ { key: changeKey, mode, value: changeValue, priority: 0 } ] this.baseEffect.changes = changes this.baseEffect.name = effectName console.log(token, weaponName, weaponId, effectName, changeKey) await shim.applyActiveEffects(token, [this.baseEffect].concat(this.effectDocs)) } } } class SummonEffect extends PowerEffect { ICON = 'icons/magic/symbols/runes-triangle-blue.webp' get actorFolder () { return 'Summonables' } get name () { return 'Summon Creature' } get baseDurationRounds () { return 5 } async prepFolders () { const folders = [] const folderNames = [ this.actorFolder, `${this.actorFolder} - Default`, `${this.actorFolder}/Default`, `${this.actorFolder} - ${this.token.name}`, `${this.actorFolder} - ${this.token.actor.name}`, `${this.actorFolder}/${this.token.name}`, `${this.actorFolder}/${this.token.actor.name}` ] for (const folderName of folderNames) { const folder = shim.getActorFolderByPath(folderName) if (folder) { log(`Found actor folder ${folderName}`) folders.push(folder) } } if (folders.length > 1) { folders.shift() } return folders } async prepActors () { const folders = await this.prepFolders() const actors = {} for (const folder of folders) { const folderActors = shim.getActorsInFolder(folder) for (const key in folderActors) { actors[key] = folderActors[key] } } return actors } async prepMenu () { this.menuData.inputs[1].label = `${this.token.name} is summoning...` const actors = await this.prepActors() if (Object.keys(actors).length < 1) { shim.notifications.error('No summonables found') throw new Error('No summonables found') } function actorData (key) { return { value: actors[key].id, html: key } } this.summonableActors = actors this.menuData.inputs = this.menuData.inputs.concat([ { type: 'select', label: 'Creature to summon', options: Object.keys(actors).filter( k => !k.includes('_template')).sort().map(actorData) }, { type: 'number', label: 'Number to spawn (+half base cost per)', options: 1 }, { type: 'checkbox', label: 'Add Increased Trait(s)? (+1 per trait)', options: false } ]) } async prepResult () { this.raise = (this.buttons === 'raise') this.actorId = (this.inputs[this.inputIndex]) this.number = (this.inputs[this.inputIndex + 1]) this.actor = shim.actors.get(this.actorId) this.icon = this.actor.prototypeToken.texture.src this.protoDoc = await this.actor.getTokenDocument() this.increasedTrait = !!(this.inputs[this.inputIndex + 2]) this.inputIndex += 3 this.spawnOptions = { controllingActor: this.token.actor, duplicates: this.number, crosshairs: { icon: this.icon, label: `Summon ${this.actor.name}`, drawOutline: true, rememberControlled: true } } this.spawnMutation = { actor: { name: `${this.token.name}'s ${this.actor.name}` }, token: { actorLink: false, name: `${this.token.name}'s ${this.protoDoc.name}` }, embedded: { ActiveEffect: {}, Item: {} } } if (this.raise && ('raise_template' in this.summonableActors)) { const raiseTemplate = this.summonableActors.raise_template for (const item of raiseTemplate.items) { const raiseItemDoc = await raiseTemplate.getEmbeddedDocument('Item', item.id) this.spawnMutation.embedded.Item[item.name] = raiseItemDoc } } for (const effectDocument of this.effectDocs) { this.spawnMutation.embedded.ActiveEffect[effectDocument.name] = effectDocument } } async prepAdditional () { if (!this.increasedTrait) { return } const traitMenuOptions = { title: `${this.name} Summon Trait Increase`, defaultButton: 'Cancel', options: {} } const skillSet = new Set() for (const skill of this.actor.items.filter(i => i.type === 'skill')) { skillSet.add(skill.name) } for (const item of Object.values(this.spawnMutation.embedded.Item).filter(i => i.type === 'skill')) { skillSet.add(item.name) } const skillList = Array.from(skillSet) const attrList = ['Agility', 'Smarts', 'Spirit', 'Strength', 'Vigor'] skillList.sort() const traitMenuData = { inputs: [ { type: 'header', label: 'Increase Attributes (+1 each)' } ], buttons: [ { label: 'Apply', value: 'apply' }, { label: 'Increase no traits', value: 'cancel' } ] } traitMenuData.inputs = traitMenuData.inputs.concat( attrList.map((x) => { return { type: 'checkbox', label: x, options: false } })) traitMenuData.inputs.push({ type: 'header', label: 'Increase Skills (+1 each)' }) traitMenuData.inputs = traitMenuData.inputs.concat( skillList.map((x) => { return { type: 'checkbox', label: x, options: false } })) const { buttons, inputs } = await shim.warpgateMenu(traitMenuData, traitMenuOptions) if (!buttons || buttons === 'cancel') { return } const modKeys = [] for (let i = 0; i < attrList.length; i++) { if (inputs[i + 1]) { modKeys.push(`system.attributes.${attrList[i].toLowerCase()}.die.sides`) } } for (let i = 0; i < skillList.length; i++) { if (inputs[i + 7]) { modKeys.push(`@Skill{${skillList[i]}}[system.die.sides]`) } } const effectDoc = shim.createEffectDocument( this.ICON, 'Increased Trait', this.durationRounds) effectDoc.changes = modKeys.map(key => { return { key, mode: CONST.FOUNDRY.ACTIVE_EFFECT_MODES.ADD, value: '+2', priority: 0 } }) this.spawnMutation.embedded.ActiveEffect[effectDoc.name] = effectDoc } async applyResult () { await this.prepAdditional() await shim.warpgateSpawn(this.protoDoc, this.spawnMutation, {}, this.spawnOptions) } } class SummonAllyEffect extends SummonEffect { get name () { return 'Summon Ally' } get actorFolder () { return `${super.actorFolder}/Summon Ally` } async prepMenu () { await super.prepMenu() this.menuData.inputs = this.menuData.inputs.concat([ { type: 'checkbox', label: 'Bite/Claw (+1)', options: false }, { type: 'checkbox', label: 'Up to 3 Combat Edges (+1 per)', options: false }, { type: 'checkbox', label: 'Flight (+3)', options: false } ]) } async prepResult () { await super.prepResult() this.biteClaw = !!(this.inputs[this.inputIndex]) this.combatEdge = !!(this.inputs[this.inputIndex + 1]) this.flight = !!(this.inputs[this.inputIndex + 2]) await this.prepMirrorSelf() } async prepAdditional () { await super.prepAdditional() await this.prepBiteClaw() await this.prepFlight() await this.prepCombatEdge() } async prepCombatEdge () { if (!this.combatEdge || !('combat-edge_template' in this.summonableActors)) { return } const template = this.summonableActors['combat-edge_template'] const edges = template.items.filter(i => i.type === 'edge').map(i => i.name) edges.sort() edges.unshift('None') const edgeMenuData = { inputs: [ { type: 'header', label: 'Choose Edges (+1 per choice)' }, { type: 'select', label: 'Edge 1', options: edges }, { type: 'select', label: 'Edge 2', options: edges }, { type: 'select', label: 'Edge 3', options: edges } ], buttons: [ { label: 'Apply', value: 'apply' }, { label: 'Add no edges', value: 'cancel' } ] } const edgeMenuOptions = { title: `${this.name} Combat Edge Selection`, defaultButton: 'Cancel', options: {} } const { buttons, inputs } = await shim.warpgateMenu(edgeMenuData, edgeMenuOptions) if (!buttons || buttons === 'cancel') { return } for (let i = 1; i <= 3; i++) { if (inputs[i] === 'None') { continue } const edge = template.items.getName(inputs[i]) if (edge) { const doc = template.getEmbeddedDocument('Item', edge.id) this.spawnMutation.embedded.Item[edge.name] = doc } } } async prepBiteClaw () { if (!this.biteClaw || !('bite-claw_template' in this.summonableActors)) { return } const template = this.summonableActors['bite-claw_template'] for (const item of template.items) { const doc = await template.getEmbeddedDocument('Item', item.id) this.spawnMutation.embedded.Item[item.name] = doc log(`Added ${item.name} to spawn mutation`) } } async prepFlight () { if (!this.flight || !('flight_template' in this.summonableActors)) { return } const template = this.summonableActors.flight_template for (const item of template.items) { const doc = await template.getEmbeddedDocument('Item', item.id) this.spawnMutation.embedded.Item[item.name] = doc log(`Added ${item.name} to spawn mutation`) } for (const effect of template.effects.values()) { const doc = shim.ActiveEffect.fromSource(effect) this.spawnMutation.embedded.ActiveEffect[effect.name] = doc log(`Added ${effect.name} to spawn mutation`) } } async prepMirrorSelf () { if (this.actor.name !== 'Mirror Self') { return } const actorDoc = this.token.actor.clone({ 'system.wildcard': false, 'system.fatigue.value': 0, 'system.wounds.value': 0, 'system.wounds.max': 0, 'system.bennies.max': 0, 'system.bennies.value': 0 }) await shim.mergeObject( this.spawnMutation.actor, { name: `Mirrored ${this.token.actor.name}`, img: this.token.actor.img, system: actorDoc.system }, { inplace: true, recursive: true } ) await shim.mergeObject( this.spawnMutation.token, { actorLink: false, disposition: this.token.document.disposition, light: this.token.document.light, sight: this.token.document.sight, name: `Mirrored ${this.token.name}`, texture: this.token.document.texture }, { inplace: true, recursive: true }) this.spawnMutation.token.texture.scaleX = -1 * this.token.document.texture.scaleX const effectChanges = [] for (const item of this.token.actor.items) { this.spawnMutation.embedded.Item[item.name] = this.token.actor.getEmbeddedDocument('Item', item.id) if (item.type === 'skill') { effectChanges.push({ key: `@Skill{${item.name}}[system.die.sides]`, mode: CONST.FOUNDRY.ACTIVE_EFFECT_MODES.ADD, value: '-2', priority: 0 }) } } this.spawnMutation.embedded.ActiveEffect['Mirror Self'] = shim.createEffectDocument(this.ICON, 'Mirror Self', this.durationRounds, effectChanges) } } const PowerClasses = { blind: BlindEffect, 'boost/lower trait': BoostLowerTraitEffect, 'boost trait': BoostLowerTraitEffect, confusion: ConfusionEffect, deflection: DeflectionEffect, entangle: EntangleEffect, intangibility: IntangibilityEffect, invisibility: InvisibilityEffect, 'lower trait': BoostLowerTraitEffect, protection: ProtectionEffect, smite: SmiteEffect, 'summon ally': SummonAllyEffect } export async function powerEffects (options = {}) { const token = 'token' in options ? options.token : [] const targets = 'targets' in options ? Array.from(options.targets) : [] const item = 'item' in options ? options.item : null const name = 'name' in options ? options.name : (item !== null ? item.name : null) const lcName = name.toLowerCase() for (const name in PowerClasses) { if (lcName.includes(name)) { const runner = new PowerClasses[name](token, targets) runner.powerEffect() return } } shim.notifications.error(`No power effect found for ${name}`) }