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 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 durationRounds () { 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 durationRounds () { 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 durationRounds () { 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 durationRounds () { 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 durationRounds () { 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 durationRounds () { 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 durationRounds () { 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 durationRounds () { 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 durationRounds () { 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: `