1763 lines
52 KiB
JavaScript
1763 lines
52 KiB
JavaScript
import { log, module } from './globals.js'
|
|
import { requestFearRollFromTokens, 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 warpgate.menu(
|
|
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) {
|
|
module.applyActiveEffects(target, this.effectDocs)
|
|
}
|
|
}
|
|
|
|
static modEffectDoc (icon, name, key, value, durationRounds) {
|
|
return module.createEffectDocument(icon, name, durationRounds, [
|
|
{
|
|
key,
|
|
mode: foundry.CONST.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) {
|
|
ui.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 = module.createEffectDocument(
|
|
'icons/magic/death/skull-poison-green.webp',
|
|
`Lingering Damage (${this.name})`,
|
|
1
|
|
)
|
|
doc.flags.swade.expiration = CONFIG.SWADE.CONST.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(
|
|
module.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: foundry.CONST.ACTIVE_EFFECT_MODES.ADD,
|
|
value: '-2',
|
|
priority: 0
|
|
}
|
|
]
|
|
this.effectDocs.push(
|
|
module.createEffectDocument(
|
|
icon, `minor Blindness (Vigor ${strong ? '-2 ' : ''}ends)`,
|
|
this.durationRounds, changes))
|
|
if (raise) {
|
|
this.effectDocs.push(
|
|
module.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(
|
|
module.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 = foundry.CONST.ACTIVE_EFFECT_MODES.ADD
|
|
const modValue = (direction === 'Boost' ? '+2' : '-2')
|
|
const minorEffect = module.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 = module.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(module.getStatus('SWADE.Distr', 'Distracted'))
|
|
}
|
|
if (this.buttons === 'vulnerable' || this.buttons === 'raise') {
|
|
this.effectDocs.push(module.getStatus('SWADE.Vuln', 'Vulnerable'))
|
|
}
|
|
if (greater) {
|
|
this.effectDocs.push(module.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(
|
|
module.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(
|
|
module.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(module.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 = module.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 = module.getStatus(effectSearch, effectName)
|
|
const extraIcon = 'icons/magic/nature/root-vine-barrier-wall-brown.webp'
|
|
const extraEffect = module.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 FearEffect extends TargetedPowerEffect {
|
|
get name () {
|
|
return 'Fear'
|
|
}
|
|
|
|
get baseDurationRounds () {
|
|
return 1
|
|
}
|
|
|
|
async prepResult () {
|
|
this.raise = (this.buttons === 'raise')
|
|
}
|
|
|
|
async applyResult () {
|
|
await super.applyResult()
|
|
await warpgate.wait(1000)
|
|
const options = {
|
|
title: 'Fear check!',
|
|
flavor: 'Failure: roll on the Fear Table if wildcard, Panicked if extra',
|
|
mods: []
|
|
}
|
|
if (this.raise) {
|
|
options.fear = '-2'
|
|
}
|
|
await requestFearRollFromTokens(this.targets, options)
|
|
}
|
|
}
|
|
|
|
class HavocEffect extends TargetedPowerEffect {
|
|
get name () {
|
|
return 'Havoc'
|
|
}
|
|
|
|
get baseDurationRounds () {
|
|
return 1
|
|
}
|
|
|
|
async prepResult () {
|
|
this.raise = (this.buttons === 'raise')
|
|
this.effectDocs.unshift(module.getStatus('SWADE.Distr', 'Distracted'))
|
|
}
|
|
|
|
async applyResult () {
|
|
await super.applyResult()
|
|
await warpgate.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 = module.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 = module.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 = module.getStatus('SWADE.Protection', 'Protection')
|
|
effect.duration = { rounds: this.durationRounds }
|
|
const mode = foundry.CONST.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 = module.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 = module.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) {
|
|
ui.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 = game.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: foundry.CONST.ACTIVE_EFFECT_MODES.ADD,
|
|
value: '+2',
|
|
priority: 0
|
|
})
|
|
}
|
|
}
|
|
this.effectDocs.push(
|
|
module.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 warpgate.spawnAt(
|
|
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: `<h2>${token.name}</h2>` })
|
|
const weapons = token.actor.items.filter(i => i.type === 'weapon').map(
|
|
i => { return { value: i.name, html: i.name } })
|
|
weapons.unshift({ value: '', html: '<i>None</i>' })
|
|
this.menuData.inputs.push({ type: 'select', label: token.name, options: weapons })
|
|
}
|
|
this.tokenWeapons = tokenWeapons
|
|
}
|
|
|
|
async prepResult () {
|
|
this.baseEffect = module.getStatus('SWADE.Smite', 'Smite')
|
|
}
|
|
|
|
async applyResult () {
|
|
const mode = foundry.CONST.ACTIVE_EFFECT_MODES.ADD
|
|
const raise = (this.buttons === 'raise')
|
|
const greater = !!this.inputs[this.inputIndex]
|
|
const changeValue = (greater ? (raise ? '+6' : '+4') : (raise ? '+4' : '+2'))
|
|
const changeKey = 'system.stats.globalMods.damage'
|
|
for (const token of this.targets) {
|
|
const weaponName = this.inputs[this.tokenWeapons[token.id]]
|
|
const effectName = `Smite (${weaponName})`
|
|
const changes = [
|
|
{ key: changeKey, mode, value: changeValue, priority: 0 }
|
|
]
|
|
this.baseEffect.changes = changes
|
|
this.baseEffect.name = effectName
|
|
await module.applyActiveEffects(token, [this.baseEffect].concat(this.effectDocs))
|
|
}
|
|
}
|
|
}
|
|
|
|
class SlothSpeedEffect extends TargetedPowerEffect {
|
|
get name () {
|
|
return 'Sloth/Speed'
|
|
}
|
|
|
|
get baseDurationRounds () {
|
|
if (this.inputs?.[this.inputIndex + 1] === true) {
|
|
return 1
|
|
}
|
|
return 5
|
|
}
|
|
|
|
async prepMenu () {
|
|
this.menuData.inputs = this.menuData.inputs.concat(
|
|
{ type: 'info', label: 'Sloth or Speed?' },
|
|
{ type: 'radio', label: 'Sloth', options: ['isSloth', true] },
|
|
{ type: 'radio', label: 'Speed', options: ['isSloth', false] },
|
|
{ type: 'checkbox', label: 'Dash (+2, speed only)', options: false },
|
|
{ type: 'checkbox', label: 'Quickness (+2, speed only)', options: false },
|
|
{ type: 'checkbox', label: 'Strong (+1, sloth only)', options: false }
|
|
)
|
|
}
|
|
|
|
async prepResult () {
|
|
const raise = (this.buttons === 'raise')
|
|
const isSloth = this.inputs[this.inputIndex + 1] === true
|
|
const icon = (isSloth
|
|
? 'icons/magic/control/debuff-chains-shackles-movement-blue.webp'
|
|
: 'icons/skills/movement/feet-winged-sandals-tan.webp'
|
|
)
|
|
const dash = !isSloth && !!this.inputs[this.inputIndex + 3]
|
|
const quickness = !isSloth && !!this.inputs[this.inputIndex + 4]
|
|
const strong = isSloth && !!this.inputs[this.inputIndex + 4]
|
|
const nameMods = []
|
|
if (raise) { nameMods.push('Major') }
|
|
if (dash) { nameMods.push('Dash') }
|
|
if (quickness) { nameMods.push('Quickness') }
|
|
if (strong) { nameMods.push('Strong') }
|
|
const nameModifier = (
|
|
`${nameMods.length > 0 ? ' (' : ''}` +
|
|
`${nameMods.join(', ')}${nameMods.length > 0 ? ')' : ''}`)
|
|
const name = `${isSloth ? 'Sloth' : 'Speed'}${nameModifier}`
|
|
const effect = module.createEffectDocument(
|
|
icon, name, this.durationRounds, [
|
|
{
|
|
key: 'system.stats.speed.value',
|
|
mode: foundry.CONST.ACTIVE_EFFECT_MODES.MULTIPLY,
|
|
value: (isSloth ? 0.5 : 2),
|
|
priority: 0
|
|
}])
|
|
this.effectDocs.push(effect)
|
|
}
|
|
}
|
|
|
|
class SummonEffect extends PowerEffect {
|
|
ICON = 'icons/magic/symbols/runes-triangle-blue.webp'
|
|
|
|
get actorFolderBase () {
|
|
return 'Summonables'
|
|
}
|
|
|
|
get actorFolder () {
|
|
return `${this.actorFolderBase}/${this.name}`
|
|
}
|
|
|
|
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 = module.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 = module.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...`
|
|
this.menuData.buttons = this.menuData.buttons.filter(b => b.value !== 'raise')
|
|
const actors = await this.prepActors()
|
|
if (Object.keys(actors).length < 1) {
|
|
ui.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 = game.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,
|
|
updateOpts: {
|
|
embedded: {
|
|
Item: {
|
|
renderSheet: null
|
|
}
|
|
}
|
|
},
|
|
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}`,
|
|
disposition: this.token.document.disposition,
|
|
sight: {
|
|
enabled: true
|
|
}
|
|
},
|
|
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 warpgate.menu(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 = module.createEffectDocument(
|
|
this.ICON, 'Increased Trait', this.durationRounds)
|
|
effectDoc.changes = modKeys.map(key => {
|
|
return {
|
|
key, mode: foundry.CONST.ACTIVE_EFFECT_MODES.ADD, value: '+2', priority: 0
|
|
}
|
|
})
|
|
this.spawnMutation.embedded.ActiveEffect[effectDoc.name] = effectDoc
|
|
}
|
|
|
|
async applyResult () {
|
|
await this.prepAdditional()
|
|
await warpgate.spawn(this.protoDoc, this.spawnMutation, {}, this.spawnOptions)
|
|
log('protoDoc', this.protoDoc)
|
|
log('spawnOptions', this.spawnOptions)
|
|
log('spawnMutation', this.spawnMutation)
|
|
}
|
|
}
|
|
|
|
class SummonAllyEffect extends SummonEffect {
|
|
get name () {
|
|
return 'Summon Ally'
|
|
}
|
|
|
|
async prepMenu () {
|
|
await super.prepMenu()
|
|
this.menuData.buttons = [
|
|
this.menuData.buttons[0],
|
|
{ label: 'Apply with Raise', value: 'raise' },
|
|
this.menuData.buttons[1]
|
|
]
|
|
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 warpgate.menu(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 = await 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
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
for (const effect of template.effects.values()) {
|
|
const doc = ActiveEffect.fromSource(effect)
|
|
this.spawnMutation.embedded.ActiveEffect[effect.name] = doc
|
|
}
|
|
}
|
|
|
|
async prepMirrorSelf () {
|
|
if (this.actor.name !== 'Mirror Self') {
|
|
return
|
|
}
|
|
const mirrorActor = this.token.actor
|
|
this.icon = mirrorActor.prototypeToken.texture.src
|
|
this.spawnMutation.actor.system = mirrorActor.system.clone({
|
|
wildcard: false,
|
|
'fatigue.value': 0,
|
|
'wounds.value': 0,
|
|
'wounds.max': 0,
|
|
'bennies.max': 0,
|
|
'bennies.value': 0
|
|
})
|
|
this.spawnMutation.actor.name = `Mirror ${mirrorActor.name}`
|
|
this.spawnMutation.actor.img = mirrorActor.img
|
|
this.spawnMutation.token.name = `Mirror ${this.token.name}`
|
|
this.spawnMutation.token.texture = {
|
|
src: this.token.document.texture.src,
|
|
scaleX: this.token.document.texture.scaleX * -1,
|
|
scaleY: this.token.document.texture.scaleY
|
|
}
|
|
this.spawnOptions.crosshairs.icon = this.icon
|
|
const effectChanges = []
|
|
for (const mirrorItem of mirrorActor.items) {
|
|
if (mirrorItem.type === 'power' &&
|
|
(mirrorItem.system?.swid === 'summon-ally' || mirrorItem.name === 'Summon Ally')) {
|
|
continue
|
|
}
|
|
if (['weapon', 'armor', 'consumable', 'gear'].includes(mirrorItem.type)) {
|
|
continue
|
|
}
|
|
this.spawnMutation.embedded.Item[mirrorItem.name] = await mirrorActor.getEmbeddedDocument(
|
|
'Item', mirrorItem.id)
|
|
if (mirrorItem.type === 'skill') {
|
|
effectChanges.push({
|
|
key: `@Skill{${mirrorItem.name}}[system.die.sides]`,
|
|
mode: foundry.CONST.ACTIVE_EFFECT_MODES.ADD,
|
|
value: '-2',
|
|
priority: 0
|
|
})
|
|
}
|
|
}
|
|
this.spawnMutation.embedded.ActiveEffect['Mirror Self'] =
|
|
module.createEffectDocument(this.ICON, 'Mirror Self',
|
|
this.durationRounds, effectChanges)
|
|
}
|
|
}
|
|
|
|
class SpiritualWeaponEffect extends SummonEffect {
|
|
get name () {
|
|
return 'Spiritual Weapon'
|
|
}
|
|
|
|
async prepMenu () {
|
|
await super.prepMenu()
|
|
this.menuData.buttons = [
|
|
this.menuData.buttons[0],
|
|
{ label: 'Apply with Raise', value: 'raise' },
|
|
this.menuData.buttons[1]
|
|
]
|
|
this.menuData.inputs.pop()
|
|
this.menuData.inputs.pop()
|
|
this.menuData.inputs[this.menuData.inputs.length - 1].label = 'Weapon to summon'
|
|
const arcaneSkills = this.token.actor.items
|
|
.filter(i => i.type === 'skill').filter(s => (
|
|
s.system.swid === 'faith' ||
|
|
s.system.swid === 'spellcasting' ||
|
|
s.system.swid === 'performance')).map(s => {
|
|
return {
|
|
value: s.system.swid,
|
|
html: s.name
|
|
}
|
|
})
|
|
this.menuData.inputs.push(
|
|
{ type: 'select', label: 'Arcane Skill', options: arcaneSkills }
|
|
)
|
|
}
|
|
|
|
async prepResult () {
|
|
this.raise = (this.buttons === 'raise')
|
|
this.actorId = this.inputs[this.inputIndex]
|
|
this.arcaneSkillSwid = this.inputs[this.inputIndex + 1]
|
|
this.actor = game.actors.get(this.actorId)
|
|
this.icon = 'icons/weapons/hammers/hammer-double-glowing-yellow.webp'
|
|
this.protoDoc = await this.actor.getTokenDocument()
|
|
this.spawnOptions = {
|
|
controllingActor: this.token.actor,
|
|
duplicates: 1,
|
|
updateOpts: {
|
|
embedded: {
|
|
Item: { renderSheet: null }
|
|
}
|
|
},
|
|
crosshairs: {
|
|
icon: this.icon,
|
|
label: `Summon ${this.actor.name}`,
|
|
drawOutline: true,
|
|
rememberControlled: true
|
|
}
|
|
}
|
|
this.spawnMutation = {
|
|
actor: {
|
|
name: `${this.token.name}'s ${this.actor.name}`,
|
|
system: {
|
|
wildcard: this.token.actor.system.wildcard,
|
|
attributes: {
|
|
spirit: {
|
|
die: {
|
|
sides: this.token.actor.system.attributes.spirit.die.sides,
|
|
modifier: this.token.actor.system.attributes.spirit.die.modifier
|
|
},
|
|
'wild-die': {
|
|
sides: this.token.actor.system.attributes.spirit['wild-die'].sides
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
token: {
|
|
actorLink: false,
|
|
name: `${this.token.name}'s ${this.protoDoc.name}`,
|
|
disposition: this.token.document.disposition,
|
|
sight: { enabled: false }
|
|
},
|
|
embedded: { ActiveEffect: {}, Item: {} }
|
|
}
|
|
const weaponItemId = this.actor.items.find(i =>
|
|
i.type === 'weapon' && i.system.swid === 'spiritual-weapon').id
|
|
const weaponDoc = this.actor.getEmbeddedDocument('Item', weaponItemId).clone()
|
|
const arcaneSkill = this.token.actor.items.find(i => i.type === 'skill' && i.system.swid === this.arcaneSkillSwid)
|
|
const arcaneDoc = this.token.actor.getEmbeddedDocument('Item', arcaneSkill.id).clone()
|
|
this.spawnMutation.embedded.Item[weaponDoc.name] = {
|
|
system: {
|
|
damage: '@spi+d4',
|
|
actions: {
|
|
trait: arcaneDoc.name
|
|
}
|
|
}
|
|
}
|
|
// weaponDoc.system.actions.trait = arcaneDoc.name
|
|
if (this.raise) {
|
|
this.spawnMutation.embedded.Item[weaponDoc.name].system.damage = '@spi+d6'
|
|
// weaponDoc.system.damage = '@spi+d6'
|
|
}
|
|
this.spawnMutation.embedded.Item[arcaneDoc.name] = arcaneDoc
|
|
// this.spawnMutation.embedded.Item[weaponDoc.name] = weaponDoc
|
|
for (const effectDocument of this.effectDocs) {
|
|
this.spawnMutation.embedded.ActiveEffect[effectDocument.name] = effectDocument
|
|
}
|
|
}
|
|
}
|
|
|
|
class SummonAnimalEffect extends SummonEffect {
|
|
get name () {
|
|
return 'Summon Animal'
|
|
}
|
|
}
|
|
|
|
class SummonMonsterEffect extends SummonEffect {
|
|
get name () {
|
|
return 'Summon Monster'
|
|
}
|
|
}
|
|
|
|
class SummonNaturesAllyEffect extends SummonEffect {
|
|
get name () {
|
|
return "Summon Nature's Ally"
|
|
}
|
|
}
|
|
|
|
class SummonPlanarAllyEffect extends SummonEffect {
|
|
get name () {
|
|
return 'Summon Planar Ally'
|
|
}
|
|
}
|
|
|
|
class SummonUndeadEffect extends SummonEffect {
|
|
get name () {
|
|
return 'Summon Undead'
|
|
}
|
|
}
|
|
|
|
class ZombieEffect extends SummonEffect {
|
|
get name () {
|
|
return 'Zombie'
|
|
}
|
|
|
|
async prepMenu () {
|
|
await super.prepMenu()
|
|
this.menuData.buttons = [
|
|
this.menuData.buttons[0],
|
|
{ label: 'Apply with Raise', value: 'raise' },
|
|
this.menuData.buttons[1]
|
|
]
|
|
this.menuData.inputs.pop()
|
|
this.menuData.inputs = this.menuData.inputs.concat([
|
|
{
|
|
type: 'checkbox',
|
|
label: 'Armed (Hand Weapon (Str+d6) or Ranged Weapon (2d6)',
|
|
options: false
|
|
}, {
|
|
type: 'checkbox',
|
|
label: '+2 Armor',
|
|
options: false
|
|
}, {
|
|
type: 'info',
|
|
label: 'Skeletal creatures +1 per zombie'
|
|
}
|
|
])
|
|
}
|
|
|
|
async prepResult () {
|
|
this.raise = (this.buttons === 'raise')
|
|
this.actorId = this.inputs[this.inputIndex]
|
|
this.number = this.inputs[this.inputIndex + 1]
|
|
this.actor = game.actors.get(this.actorId)
|
|
this.icon = this.actor.prototypeToken.texture.src
|
|
this.protoDoc = await this.actor.getTokenDocument()
|
|
this.increasedTrait = this.raise
|
|
this.armed = this.inputs[this.inputIndex + 2]
|
|
this.armor = this.inputs[this.inputIndex + 3]
|
|
this.spawnOptions = {
|
|
controllingActor: this.token.actor,
|
|
duplicates: this.number,
|
|
updateOpts: {
|
|
embedded: {
|
|
Item: { renderSheet: null }
|
|
}
|
|
},
|
|
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}`,
|
|
disposition: this.token.document.disposition,
|
|
sight: { enabled: true }
|
|
},
|
|
embedded: { ActiveEffect: {}, Item: {} }
|
|
}
|
|
if (this.armed && ('armed_template' in this.summonableActors)) {
|
|
const armedTemplate = this.summonableActors.armed_template
|
|
for (const item of armedTemplate.items) {
|
|
const itemDoc = await armedTemplate.getEmbeddedDocument('Item', item.id)
|
|
this.spawnMutation.embedded.Item[item.name] = itemDoc
|
|
}
|
|
}
|
|
if (this.armor) {
|
|
const effectDoc = module.createEffectDocument(
|
|
'icons/equipment/chest/breastplate-layered-leather-stitched.webp',
|
|
'Rotting Armor',
|
|
0)
|
|
delete effectDoc.duration
|
|
delete effectDoc.flags.swade.expiration
|
|
effectDoc.changes = [{
|
|
key: 'system.stats.toughness.armor',
|
|
mode: foundry.CONST.ACTIVE_EFFECT_MODES.ADD,
|
|
value: '+2',
|
|
priority: 0
|
|
}]
|
|
this.spawnMutation.embedded.ActiveEffect[effectDoc.name] = effectDoc
|
|
}
|
|
for (const effectDocument of this.effectDocs) {
|
|
this.spawnMutation.embedded.ActiveEffect[effectDocument.name] = effectDocument
|
|
}
|
|
}
|
|
|
|
async prepAdditional () {
|
|
if (!this.increasedTrait) {
|
|
return
|
|
}
|
|
const traitMenuOptions = {
|
|
title: `${this.name} Raise 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: 'Raise! Increase an attribute' }
|
|
],
|
|
buttons: [
|
|
{ label: 'Apply', value: 'apply' },
|
|
{ label: 'Increase no traits', value: 'cancel' }
|
|
]
|
|
}
|
|
traitMenuData.inputs = traitMenuData.inputs.concat(
|
|
attrList.map((x) => { return { type: 'radio', label: x, options: ['trait', false] } }))
|
|
traitMenuData.inputs.push({ type: 'header', label: 'Increase Skills (+1 each)' })
|
|
traitMenuData.inputs = traitMenuData.inputs.concat(
|
|
skillList.map((x) => { return { type: 'radio', label: x, options: ['trait', false] } }))
|
|
const { buttons, inputs } = await warpgate.menu(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 = module.createEffectDocument(
|
|
this.ICON, 'Increased Trait', this.durationRounds)
|
|
effectDoc.changes = modKeys.map(key => {
|
|
return {
|
|
key, mode: foundry.CONST.ACTIVE_EFFECT_MODES.ADD, value: '+2', priority: 0
|
|
}
|
|
})
|
|
this.spawnMutation.embedded.ActiveEffect[effectDoc.name] = effectDoc
|
|
}
|
|
}
|
|
|
|
const PowerClasses = {
|
|
'arcane protection': ArcaneProtectionEffect,
|
|
'arcane-protection': ArcaneProtectionEffect,
|
|
blast: BlastEffect,
|
|
blind: BlindEffect,
|
|
bolt: BoltEffect,
|
|
'boost/lower trait': BoostLowerTraitEffect,
|
|
'boostlower-trait': BoostLowerTraitEffect,
|
|
'boost trait': BoostLowerTraitEffect,
|
|
'boost-trait': BoostLowerTraitEffect,
|
|
burrow: BurrowEffect,
|
|
burst: BurstEffect,
|
|
'conceal arcana': DetectConcealArcanaEffect,
|
|
'conceal-arcana': DetectConcealArcanaEffect,
|
|
confusion: ConfusionEffect,
|
|
darksight: DarksightEffect,
|
|
deflection: DeflectionEffect,
|
|
'detect arcana': DetectConcealArcanaEffect,
|
|
'detect-arcana': DetectConcealArcanaEffect,
|
|
'detect/conceal aracana': DetectConcealArcanaEffect,
|
|
'detectconceal-aracana': DetectConcealArcanaEffect,
|
|
disguise: DisguiseEffect,
|
|
entangle: EntangleEffect,
|
|
fear: FearEffect,
|
|
havoc: HavocEffect,
|
|
intangibility: IntangibilityEffect,
|
|
invisibility: InvisibilityEffect,
|
|
'lower trait': BoostLowerTraitEffect,
|
|
'lower-trait': BoostLowerTraitEffect,
|
|
protection: ProtectionEffect,
|
|
'shape change': ShapeChangeEffect,
|
|
'shape-change': ShapeChangeEffect,
|
|
sloth: SlothSpeedEffect,
|
|
'sloth/speed': SlothSpeedEffect,
|
|
slothspeed: SlothSpeedEffect,
|
|
smite: SmiteEffect,
|
|
speed: SlothSpeedEffect,
|
|
'spiritual-weapon': SpiritualWeaponEffect,
|
|
'summon ally': SummonAllyEffect,
|
|
'summon-ally': SummonAllyEffect,
|
|
'summon animal': SummonAnimalEffect,
|
|
'summon-animal': SummonAnimalEffect,
|
|
'summon monster': SummonMonsterEffect,
|
|
'summon-monster': SummonMonsterEffect,
|
|
"summon nature's ally": SummonNaturesAllyEffect,
|
|
'summon-natures-ally': SummonNaturesAllyEffect,
|
|
'summon planar ally': SummonPlanarAllyEffect,
|
|
'summon-planar-ally': SummonPlanarAllyEffect,
|
|
'summon undead': SummonUndeadEffect,
|
|
'summon-undead': SummonUndeadEffect,
|
|
zombie: ZombieEffect
|
|
}
|
|
|
|
export async function powerEffects (options = {}) {
|
|
const token = 'token' in options ? options.token : []
|
|
if (token === undefined || token === null) {
|
|
ui.notifications.error('Please select one token')
|
|
return
|
|
}
|
|
|
|
const targets = 'targets' in options ? Array.from(options.targets) : []
|
|
const item = 'item' in options ? options.item : null
|
|
const name = options?.name || item?.system?.swid || 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
|
|
}
|
|
}
|
|
ui.notifications.error(`No power effect found for ${name}`)
|
|
}
|
|
|
|
export async function shapeChangeOnDismiss (data) {
|
|
if (game.user.id !== data.userId) { return }
|
|
const dismissedToken = data.actorData.prototypeToken
|
|
const flags = dismissedToken.flags['swade-mb-helpers']?.shapeChange
|
|
const srcTokenId = flags?.srcTokenId
|
|
if (!srcTokenId) { return }
|
|
const scene = game.scenes.get(data.sceneId)
|
|
const token = scene.tokens.get(srcTokenId)
|
|
if (!token) { return }
|
|
const saved = token.flags['swade-mb-helpers']?.shapeChange?.saved
|
|
if (saved) {
|
|
const update = {
|
|
alpha: saved.alpha,
|
|
hidden: saved.hidden,
|
|
x: dismissedToken.x,
|
|
y: dismissedToken.y,
|
|
elevation: dismissedToken.elevation
|
|
}
|
|
await token.update(update)
|
|
}
|
|
}
|