diff --git a/scripts/powerEffects.js b/scripts/powerEffects.js index 36ce2dd..c29242a 100644 --- a/scripts/powerEffects.js +++ b/scripts/powerEffects.js @@ -209,7 +209,7 @@ const PowerMenus = { entangle: function (token, targets) { if (targets.length < 1) { - shim.notifications.error('No target selected for Deflection') + shim.notifications.error('No target selected for Entangle') return null } const { menuOptions, menuData } = baseMenu('Entangle', targets) @@ -225,6 +225,17 @@ const PowerMenus = { { label: 'Cancel', value: 'cancel' } ] return { menuOptions, menuData, extra: {} } + }, + + intangibility: function (token, targets) { + if (targets.length < 1) { + shim.notifications.error('No target selected for Intangibility') + return null + } + const { menuOptions, menuData } = baseMenu('Intangibility', targets) + menuData.inputs.push({ type: 'checkbox', label: 'Duration', options: false }) + menuData.buttons.splice(1, 1) // delete the raise button + return { menuOptions, menuData, extra: {} } } } @@ -353,24 +364,405 @@ const PowerHandlers = { } } -export async function powerEffects (options = {}) { - // options available - 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) +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 + } - const lcName = name.toLowerCase() - if (lcName in PowerMenus && lcName in PowerHandlers) { - const data = PowerMenus[lcName](token, targets) - if (data === null) { return } - const { menuOptions, menuData, extra } = data - const { buttons, inputs } = await shim.warpgateMenu(menuData, menuOptions) - if (buttons && buttons !== 'cancel') { - await PowerHandlers[lcName](token, targets, buttons, inputs, extra) + get name () { + return 'Unknown Power' + } + + get durationRounds () { + return 5 + } + + async powerEffect () { + this.prepMenu() + 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 + } + + 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' } + ] + } + + 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 + } + + prepMenu () { + this.menuData.buttons = [ + { label: 'Melee', value: 'melee' }, + { label: 'Ranged', value: 'ranged' }, + { label: 'Raise (both)', value: 'raise' }, + { label: 'Cancel', value: 'cancel' } + ] + } + + 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 + } + + 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' } + ] + } + + 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) + } + } +} + +const PowerClasses = { + blind: BlindEffect, + 'boost/lower trait': BoostLowerTraitEffect, + 'boost trait': BoostLowerTraitEffect, + confusion: ConfusionEffect, + deflection: DeflectionEffect, + entangle: EntangleEffect, + 'lower trait': BoostLowerTraitEffect +} + +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}`) +}