import { CONST, log, shim } from './shim.js' import { requestRollFromTokens } from './helpers.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 prepMenu () { } 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 LingeringDamagePowerEffect extends TargetedPowerEffect { get baseDurationRounds () { return 1 } async prepMenu () { this.menuData.inputs.splice(this.menuData.inputs.length - 1, 0, { type: 'checkbox', label: 'Lingering Damage (+2)' }) } globalModifierEffects () { super.globalModifierEffects() this.inputIndex += 1 if (this.inputs[7]) { // lingering damage const doc = shim.createEffectDocument( 'icons/magic/death/skull-poison-green.webp', `Lingering Damage (${this.name})`, 1 ) doc.flags.swade.expiration = CONST.SWADE.STATUS_EFFECT_EXPIRATION.StartOfTurnPrompt this.effectDocs.push(doc) } } } class ArcaneProtectionEffect extends TargetedPowerEffect { get name () { return 'Arcane Protection' } get baseDurationRounds () { return 5 } async prepMenu () { this.menuData.inputs.push( { type: 'checkbox', label: 'Greater', options: false }) } async prepResult () { const greater = !!this.inputs[this.inputIndex] const raise = this.buttons === 'raise' const amount = (raise ? -4 : -2) + (greater ? -2 : 0) const icon = 'icons/magic/defensive/shield-barrier-flaming-pentagon-blue.webp' const name = `${greater ? 'Greater ' : ''}Arcane Protection (${raise ? 'major, ' : ''}${amount})` this.effectDocs.push( shim.createEffectDocument(icon, name, this.durationRounds, [])) } } class BlastEffect extends LingeringDamagePowerEffect { get name () { return 'Blast' } } 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 BurrowEffect extends TargetedPowerEffect { get name () { return 'Burrow' } get baseDurationRounds () { return 5 } async prepResult () { const raise = (this.buttons === 'raise') const icon = 'icons/magic/earth/projectile-stone-landslide.webp' this.effectDocs.push( shim.createEffectDocument( icon, `${raise ? 'major' : 'minor'} ${this.name}`, this.durationRounds, []) ) } } class BoltEffect extends LingeringDamagePowerEffect { get name () { return 'Bolt' } } 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 BurstEffect extends LingeringDamagePowerEffect { get name () { return 'Burst' } } 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 DarksightEffect extends TargetedPowerEffect { get name () { return 'Darksight' } get baseDurationRounds () { return 600 } async prepMenu () { this.menuData.inputs.push( { type: 'checkbox', label: '⭐ Greater (+2)', options: false }) } async prepResult () { const raise = this.buttons === 'raise' const greater = !!this.inputs[this.inputIndex] const icon = 'icons/magic/perception/eye-ringed-glow-angry-small-teal.webp' this.effectDocs.push( shim.createEffectDocument( icon, `${raise ? 'major' : 'minor'} ${this.name}${greater ? ' (greater)' : ''}`, this.durationRounds, []) ) } } class DisguiseEffect extends TargetedPowerEffect { get name () { return 'Disguise' } get baseDurationRounds () { return 100 } async prepResult () { const raise = this.buttons === 'raise' const icon = 'icons/skills/social/diplomacy-peace-alliance.webp' this.effectDocs.push( shim.createEffectDocument( icon, `${raise ? 'major' : 'minor'} ${this.name}`, this.durationRounds, []) ) } } 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 DetectConcealArcanaEffect extends TargetedPowerEffect { get name () { return 'Detect/Conceal Arcana' } get baseDurationRounds () { if (this.inputs?.[this.inputIndex + 2] === true) { return 600 } return 5 } async prepMenu () { this.menuData.inputs = this.menuData.inputs.concat( { type: 'info', label: 'Detect or Conceal?' }, { type: 'radio', label: 'Detect', options: ['isDetect', true] }, { type: 'radio', label: 'Conceal', options: ['isDetect', false] }, { type: 'checkbox', label: 'Strong (+1, conceal only)', options: false } ) } async prepResult () { const raise = (this.buttons === 'raise') const isDetect = this.inputs[this.inputIndex + 1] === true const strong = !isDetect && !!this.inputs[this.inputIndex + 3] const icon = (isDetect ? 'icons/magic/perception/third-eye-blue-red.webp' : 'icons/magic/perception/silhouette-stealth-shadow.webp') const name = `${raise ? 'major ' : ''}${isDetect ? 'Detect' : 'Conceal'} Arcana${strong ? ' (strong)' : ''}` const effect = shim.createEffectDocument(icon, name, this.durationRounds, []) this.effectDocs.push(effect) } } 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 HavocEffect extends TargetedPowerEffect { get name () { return 'Havoc' } get baseDurationRounds () { return 1 } async prepResult () { this.raise = (this.buttons === 'raise') this.effectDocs.unshift(shim.getStatus('SWADE.Distr', 'Distracted')) } async applyResult () { await super.applyResult() await shim.wait(1000) const resistMods = function (token) { const mods = [] if (token.actor.effects.find(e => e.name === 'Flying')) { mods.push({ label: 'Flying', value: -2 }) } return mods } const options = { title: 'Resisting Havoc!', flavour: 'Havoc!', mods: [], modCallback: resistMods } if (this.raise) { options.mods.push({ label: 'vs. Raise', value: -2 }) } await requestRollFromTokens(this.targets, 'attribute', 'strength', options) } } 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 ShapeChangeEffect extends TargetedPowerEffect { get actorFolderBase () { return 'Morphables' } get tempActorFolder () { return `${this.actorFolderBase}/Changed` } get actorFolder () { return `${this.actorFolderBase}/${this.name}` } get name () { return 'Shape Change' } get baseDurationRounds () { if (this.increasedDuration ?? false) { return 50 } 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 () { const actors = await this.prepActors() this.cancel = false if (Object.keys(actors).length < 1) { shim.notifications.error('No summonables found') this.cancel = true } function actorData (key) { return { value: actors[key].id, html: key } } this.summonableActors = actors this.menuData.inputs = this.menuData.inputs.concat([ { type: 'select', label: 'Turn into creature', options: Object.keys(actors).filter( k => !k.includes('_template')).sort().map(actorData) }, { type: 'checkbox', label: 'Duration (+1, rounds to minutes)', options: false } ]) } async prepResult () { this.raise = (this.buttons === 'raise') this.actorId = (this.inputs[this.inputIndex]) this.increasedDuration = (!!this.inputs[this.inputIndex + 1]) this.actor = shim.actors.get(this.actorId) this.icon = this.targets[0].document.texture.src const targetActor = this.targets[0].actor this.protoDoc = await this.actor.getTokenDocument() this.spawnOptions = { controllingActor: this.targets[0].actor, duplicates: 1, updateOpts: { embedded: { Item: { renderSheet: null } } }, crosshairs: { rememberControlled: true } } const effectChanges = [] if (this.raise) { for (const stat of ['vigor', 'strength']) { effectChanges.push({ key: `system.attributes.${stat}.die.sides`, mode: CONST.FOUNDRY.ACTIVE_EFFECT_MODES.ADD, value: '+2', priority: 0 }) } } this.effectDocs.push( shim.createEffectDocument( this.icon, `Shape Change into ${this.protoDoc.name}`, this.durationRounds, effectChanges) ) this.spawnMutation = { actor: { name: `${this.targets[0].actor.name} (${this.actor.name} form)`, system: { attributes: { smarts: { die: targetActor.system.attributes.smarts.die }, spirit: { die: targetActor.system.attributes.spirit.die } }, wildcard: targetActor.system.wildcard } }, token: { flags: { 'swade-mb-helpers.shapeChange.srcTokenId': this.targets[0].id }, actorLink: false, name: `${this.targets[0].name} (${this.protoDoc.name} form) `, elevation: this.targets[0].document.elevation, disposition: this.targets[0].document.disposition, sight: { enabled: true } }, embedded: { ActiveEffect: {}, Item: {} } } for (const doc of this.effectDocs) { this.spawnMutation.embedded.ActiveEffect[doc.name] = doc } for (const doc of this.targets[0].actor.effects) { this.spawnMutation.embedded.ActiveEffect[doc.name] = this.targets[0].actor.getEmbeddedDocument('ActiveEffect', doc.id) } for (const item of targetActor.items) { if (item.type === 'skill' && ['smarts', 'spirit'].includes(item.system.attribute)) { const doc = await this.targets[0].actor.getEmbeddedDocument('Item', item.id) this.spawnMutation.embedded.Item[item.name] = doc } if (['power', 'edge', 'hindrance', 'action'].includes(item.type)) { const doc = await this.targets[0].actor.getEmbeddedDocument('Item', item.id) this.spawnMutation.embedded.Item[item.name] = doc } } } async applyResult () { log('protoDoc', this.protoDoc) log('spawnOptions', this.spawnOptions) log('spawnMutation', this.spawnMutation) const newTokenId = (await shim.warpgateSpawnAt( this.targets[0].center, this.protoDoc, this.spawnMutation, {}, this.spawnOptions ))[0] await this.targets[0].document.setFlag('swade-mb-helpers', 'shapeChange', { toId: newTokenId, saved: { alpha: this.targets[0].document.alpha, hidden: this.targets[0].document.hidden, x: this.targets[0].document.x, y: this.targets[0].document.y, elevation: this.targets[0].document.elevation } }) await this.targets[0].document.update({ hidden: true, alpha: 0.05 }) } } 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: `