970 lines
34 KiB
JavaScript
970 lines
34 KiB
JavaScript
import { moduleName } from './globals.js'
|
|
import { PowerEffect } from './basePowers.js'
|
|
|
|
class ArcaneProtectionEffect extends PowerEffect {
|
|
get name () { return 'Arcane Protection' }
|
|
get duration () { return 5 }
|
|
get icon () { return 'icons/magic/defensive/shield-barrier-flaming-pentagon-blue.webp' }
|
|
get hasAdditionalRecipients () { return true }
|
|
get additionalRecipientCost () { return 1 }
|
|
get isTargeted () { return true }
|
|
get modifiers () {
|
|
const mods = super.modifiers
|
|
mods.push({
|
|
name: 'Greater Arcane Protection',
|
|
id: 'greater',
|
|
value: 2,
|
|
epic: true,
|
|
effect: false
|
|
})
|
|
return mods
|
|
}
|
|
get _penaltyAmount () {
|
|
return (this.data.raise ? -4 : -2) + (this.data.mods.has('greater') ? -2 : 0)
|
|
}
|
|
get description () {
|
|
let text = super.description
|
|
text += `<p>Hostile powers are at ${this._penaltyAmount} when
|
|
targeting or damaging the affected character.</p>`
|
|
return text
|
|
}
|
|
get effectName () {
|
|
const greater = this.data.mods.has('greater')
|
|
const raise = this.data.raise
|
|
const amount = this._penaltyAmount
|
|
return `${greater ? 'Greater ' : ''}Arcane Protection (${raise ? 'major, ' : ''}${amount})`
|
|
}
|
|
}
|
|
|
|
class BanishEffect extends PowerEffect {
|
|
get name () { return 'Banish' }
|
|
get duration () { return 0 }
|
|
get basePowerPoints () { return 3 }
|
|
get usePrimaryEffect () { return false }
|
|
get isTargeted () { return true }
|
|
get isRaisable () { return false }
|
|
get hasAoe () { return true }
|
|
get menuInputs () {
|
|
const inputs = super.menuInputs
|
|
inputs.push
|
|
inputs.push({ type: 'select', label: '⭐ Area of Effect',
|
|
options: [
|
|
{html: 'None', value: 0, selected: true},
|
|
{html: 'Small Blast Template (+1)', value: 1, selected: false},
|
|
{html: 'Medium Blast Template (+2)', value: 2, selected: false},
|
|
{html: 'Large Blast Template (+3)', value: 3, selected: false},
|
|
]})
|
|
return inputs
|
|
}
|
|
get powerPoints () {
|
|
let total = super.powerPoints
|
|
total += this.data.aoe
|
|
return total
|
|
}
|
|
get description () {
|
|
return super.description + `
|
|
<p>An opposed roll of the caster's skill vs the target's Spirit.
|
|
<strong>Success:</strong> Shaken, <strong>each Raise:</strong> 1 Wound
|
|
Incapacitation results in banishment to home plane.</p>
|
|
`
|
|
}
|
|
get chatMessageEffects () {
|
|
const list = super.chatMessageEffects
|
|
switch (this.data.aoe) {
|
|
case 0: break
|
|
case 1:
|
|
list.push('SBT')
|
|
break
|
|
case 2:
|
|
list.push('MBT')
|
|
break
|
|
case 3:
|
|
list.push('LBT')
|
|
break
|
|
}
|
|
return list
|
|
}
|
|
async parseValues () {
|
|
await super.parseValues()
|
|
this.data.aoe = this.data.values.shift()
|
|
}
|
|
}
|
|
|
|
class BarrierEffect extends PowerEffect {
|
|
get name () { return 'Barrier' }
|
|
get duration () { return 5 }
|
|
get icon () { return 'icons/environment/settlement/fence-stone-brick.webp' }
|
|
get isTargeted () { return false }
|
|
get isDamaging () { return true }
|
|
get basePowerPoints () { return 2 }
|
|
get usePrimaryEffect () { return false }
|
|
get modifiers () {
|
|
const mods = super.modifiers
|
|
mods.push({ name: 'Damage', id: 'damage', value: 1, epic: false, effect: false })
|
|
mods.push({ name: 'Damage (immaterial trapping)', id: 'damage', value: 0, epic: false, effect: false })
|
|
mods.push({ name: 'Deadly', id: 'deadly', value: 2, epic: true, effect: false })
|
|
mods.push({ name: 'Hardened', id: 'hardened', value: 1, epic: false, effect: false })
|
|
mods.push({ name: 'Shaped', id: 'shaped', value: 1, epic: false, effect: false })
|
|
mods.push({ name: 'Size', id: 'size', value: 1, epic: false, effect: false })
|
|
return mods
|
|
}
|
|
get _length () {
|
|
let height = 10
|
|
if (this.data.raise) { height *= 2 }
|
|
if (this.data.mods.has('size')) { height *= 2}
|
|
return `${height}" (${height*2} yards)`
|
|
}
|
|
get _height () {
|
|
return `${this.data.mods.has('size') ? '2" (4' : '1" (2'} yards`
|
|
}
|
|
get _hardness () {
|
|
return (this.data.raise ? 12 : 10) + (this.data.mods.has('hardened') ? 2 : 0)
|
|
}
|
|
get description () {
|
|
let text = super.description
|
|
text += `<p>A barrier ${this._height} tall and ${this._length} long, of hardness ${this._hardness}. `
|
|
if (this.data.mods.has('deadly')) {
|
|
text += 'It does 2d6 damage to anyone who contacts it. '
|
|
} else if (this.data.mods.has('damage')) {
|
|
text += 'It does 2d4 damage to anyone who contacts it. '
|
|
}
|
|
if (this.data.mods.has('shaped')) {
|
|
text += 'It was shaped into a circle, square, or rectangle. '
|
|
}
|
|
text += '</p>'
|
|
return text
|
|
}
|
|
}
|
|
|
|
class BeastFriendEffect extends PowerEffect {
|
|
get name () { return 'Beast Friend' }
|
|
get duration () { return (this.data.mods.has('duration') ? 30 : 10) * 6 * 60 }
|
|
get icon () { return 'icons/magic/nature/wolf-paw-glow-large-green.webp' }
|
|
get isTargeted () { return true }
|
|
get usePrimaryEffect () { return true }
|
|
get modifiers () {
|
|
const mods = super.modifiers
|
|
mods.push(
|
|
{ name: 'Bestiarium', value: 2, id: 'bestiarium', epic: true, effect: false },
|
|
{ name: 'Duration', value: 1, id: 'duration', epic: false, effect: false },
|
|
{ name: 'Mind Rider', value: 1, id: 'mindrider', epic: false, effect: false },
|
|
)
|
|
return mods
|
|
}
|
|
get menuInputs () {
|
|
const inputs = super.menuInputs
|
|
const pp = Math.max(
|
|
this.targets.
|
|
map(t => Math.max(t.actor.system.stats.size, 1)).
|
|
reduce((a,b) => a+b, 0),
|
|
1)
|
|
inputs.push({ type: 'number', label: 'Base power points', options: pp})
|
|
return inputs
|
|
}
|
|
async parseValues () {
|
|
await super.parseValues()
|
|
this.data.basePP = this.data.values.shift()
|
|
}
|
|
get basePowerPoints () { return this?.data?.basePP || 2 }
|
|
get description () {
|
|
let text = super.description
|
|
if (this.data.raise) {
|
|
text += '<p>Creatures will overcome instincts to follow orders.'
|
|
} else {
|
|
text += '<p>Creatures obey simple commands, subject to their insticts.'
|
|
}
|
|
if (this.data.mods.has('bestiarium')) {
|
|
text += ' The caster may even effect magical beasts.'
|
|
}
|
|
return text
|
|
}
|
|
}
|
|
|
|
class BlastEffect extends PowerEffect {
|
|
get name () { return 'Blast' }
|
|
get icon () { return 'icons/magic/fire/explosion-fireball-large-red-orange.webp' }
|
|
get duration () { return 0 }
|
|
get isTargeted () { return true }
|
|
get usePrimaryEffect () { return false }
|
|
get isDamaging () { return true }
|
|
get basePowerPoints () { return 3 }
|
|
get hasAoe () { return true }
|
|
get menuInputs () {
|
|
const inputs = super.menuInputs
|
|
inputs.push
|
|
inputs.push({ type: 'select', label: 'Area of Effect',
|
|
options: [
|
|
{html: 'Small Blast Template (0)', value: 's', selected: false},
|
|
{html: 'Medium Blast Template (0)', value: 'm', selected: true},
|
|
{html: 'Large Blast Template (+1)', value: 'l', selected: false},
|
|
]})
|
|
return inputs
|
|
}
|
|
get modifiers () {
|
|
const mods = super.modifiers
|
|
mods.push(
|
|
{ name: 'Damage', value: 2, id: 'damage', epic: false, effect: false },
|
|
{ name: 'Greater Blast', value: 4, id: 'greater', epic: true, effect: false },
|
|
)
|
|
return mods
|
|
}
|
|
get powerPoints () {
|
|
let total = super.powerPoints
|
|
total += (this.data.aoe === 'l' ? 1 : 0)
|
|
return total
|
|
}
|
|
async parseValues () {
|
|
await super.parseValues()
|
|
this.data.aoe = this.data.values.shift()
|
|
}
|
|
get description () {
|
|
const dmgDie = (
|
|
this.data.mods.has('greater') ? 4 :
|
|
(this.data.mods.has('damage') ? 3 : 2)) + (this.data.raise ? 1 : 0)
|
|
const size = (
|
|
this.data.aoe === 'l' ? 'LBT' : (this.data.aoe === 's' ? 'SBT' : 'MBT'))
|
|
return super.description + `
|
|
<p>The blast covers a ${size} and does ${dmgDie}d6 damage</p>`
|
|
}
|
|
}
|
|
|
|
class BlindEffect extends PowerEffect {
|
|
get name () { return 'Blind' }
|
|
get icon () { return 'icons/skills/wounds/injury-eyes-blood-red.webp' }
|
|
get duration () { return 0 }
|
|
get isTargeted () { return true }
|
|
get hasAoe () { return true }
|
|
get basePowerPoints () { return 2 }
|
|
getPrimaryEffectChanges () {
|
|
const changes = [
|
|
{ key: 'system.stats.globalMods.trait', value: -2,
|
|
priority: 0, mode: foundry.CONST.ACTIVE_EFFECT_MODES.ADD } ]
|
|
return changes
|
|
}
|
|
get description () {
|
|
return super.description +
|
|
`<p>${this.data.raise ? -4 : -2} penalty to all actions involving sight.</p>
|
|
<p>Shake off attempts at end of turns with a Vigor
|
|
${this.data.mods.has('strong') ? '-2 ' : ''}roll as a free action.
|
|
Success removes 2 points of penalties. A raise removes the effect.</p>`
|
|
}
|
|
async createSecondaryEffects (maintId) {
|
|
const docs = await super.createSecondaryEffects(maintId)
|
|
if (this.data.raise) {
|
|
const strong = this.data.mods.has('strong')
|
|
const doc = this.createEffectDocument(
|
|
this.icon, `Blinded (${strong ? 'Strong, ' : ''}Raise)`, [
|
|
{ key: 'system.stats.globalMods.trait', value: -2,
|
|
priority: 0, mode: foundry.CONST.ACTIVE_EFFECT_MODES.ADD }
|
|
])
|
|
doc.duration.seconds = 594
|
|
doc.description = this.description +
|
|
"<p>This is the raise effect which can be shaken off separately.</p>"
|
|
doc.flags[moduleName].maintId = maintId
|
|
docs.push(doc)
|
|
}
|
|
return docs
|
|
}
|
|
get modifiers () {
|
|
const mods = super.modifiers
|
|
mods.push(
|
|
{ name: 'Strong', value: 1, id: 'strong', epic: false, effect: false },
|
|
)
|
|
return mods
|
|
}
|
|
get menuInputs () {
|
|
const inputs = super.menuInputs
|
|
inputs.push
|
|
inputs.push({ type: 'select', label: 'Area of Effect',
|
|
options: [
|
|
{html: 'None', value: 0, selected: true},
|
|
{html: 'Medium Blast Template (+2)', value: 2, selected: false},
|
|
{html: 'Large Blast Template (+3)', value: 3, selected: false},
|
|
]})
|
|
return inputs
|
|
}
|
|
async parseValues () {
|
|
await super.parseValues()
|
|
this.data.aoe = this.data.values.shift()
|
|
}
|
|
get powerPoints () {
|
|
let total = super.powerPoints
|
|
total += this.data.aoe
|
|
return total
|
|
}
|
|
get effectName () {
|
|
const strong = this.data.mods.has('strong')
|
|
return `Blinded${strong ? ' (Strong)' : ''}`
|
|
}
|
|
get chatMessageEffects () {
|
|
const list = super.chatMessageEffects
|
|
switch (this.data.aoe) {
|
|
case 2: list.push('MBT'); break
|
|
case 3: list.push('LBT'); break
|
|
}
|
|
return list
|
|
}
|
|
}
|
|
|
|
class BoltEffect extends PowerEffect {
|
|
get name () { return 'Bolt' }
|
|
get icon () { return 'icons/magic/fire/explosion-fireball-large-red-orange.webp' }
|
|
get duration () { return 0 }
|
|
get isTargeted () { return true }
|
|
get isDamaging () { return true }
|
|
get basePowerPoints () { return 3 }
|
|
get usePrimaryEffect () { return false }
|
|
get modifiers () {
|
|
const mods = super.modifiers
|
|
mods.push(
|
|
{ name: 'Damage', value: 2, id: 'damage', epic: false, effect: false },
|
|
{ name: 'Disintegrate', value: 1, id: 'disintigrate', epic: true, effect: false },
|
|
{ name: 'Greater Bolt', value: 4, id: 'greater', epic: true, effect: false },
|
|
{ name: 'Rate of Fire', value: 2, id: 'rof', epic: true, effect: false },
|
|
)
|
|
return mods
|
|
}
|
|
get powerPoints () {
|
|
let total = super.powerPoints
|
|
total += (this.data.aoe === 'l' ? 1 : 0)
|
|
return total
|
|
}
|
|
get description () {
|
|
const dmgDie = (
|
|
this.data.mods.has('greater') ? 4 :
|
|
(this.data.mods.has('damage') ? 3 : 2)) + (this.data.raise ? 1 : 0)
|
|
let desc = super.description + '<p>'
|
|
if (this.data.mods.has('rof')) {
|
|
desc += `Up to two bolts (RoF 2) do ${dmgDie}d6 damage each.`
|
|
} else {
|
|
desc += `The bolt does ${dmgDie}d6 damage.`
|
|
}
|
|
if (this.data.mods.has('disintegrate')) {
|
|
desc += 'The bolt is <em>disintegrating</em>. If being used to break' +
|
|
' something, the damage dice can Ace. A creature Incapacitated by a ' +
|
|
'disintegrating bolt must make a Vigor roll or its body turns to dust'
|
|
}
|
|
desc += '</p>'
|
|
return desc
|
|
}
|
|
}
|
|
|
|
class BurrowEffect extends PowerEffect {
|
|
get name () { return 'Burrow' }
|
|
get duration () { return 5 }
|
|
get icon () { return 'icons/magic/earth/projectile-stone-landslide.webp' }
|
|
get hasAdditionalRecipients () { return true }
|
|
get additionalRecipientCost () { return 1 }
|
|
get basePowerPoints () { return 1 }
|
|
get isTargeted () { return true }
|
|
get modifiers () {
|
|
const mods = super.modifiers
|
|
mods.push(
|
|
{ name: 'Power',
|
|
id: 'power',
|
|
value: 1,
|
|
epic: false,
|
|
effect: false,
|
|
})
|
|
return mods
|
|
}
|
|
get effectName () {
|
|
return `${this.name} ${this.data.mods.has('power') ? '[Power] ' : ''}` +
|
|
`(${this.data.raise ? 'full' : 'half'} pace)`
|
|
}
|
|
get description() {
|
|
let text = super.description +
|
|
`<p>Meld into the ground. Move at ${this.data.raise ? 'full' : 'half'} pace. May not run.</p>`
|
|
if (this.data.mods.has('power')) {
|
|
text += `<p>Can <em>burrow</em> through solid stone, concrete, etc</p>`
|
|
}
|
|
return text
|
|
}
|
|
}
|
|
|
|
class BoostLowerTraitEffect extends PowerEffect {
|
|
get name () { return 'Boost/Lower Trait' }
|
|
get hasAdditionalRecipients () { return true }
|
|
get additionalRecipientCost () { return 2 }
|
|
get icon () {
|
|
return (this?.data?.direction === 'Boost' ?
|
|
'icons/magic/life/cross-embers-glow-yellow-purple.webp' :
|
|
'icons/magic/movement/chevrons-down-yellow.webp'
|
|
)
|
|
}
|
|
get duration () {
|
|
return (this?.data?.direction === 'Boost' ? 5 : 0)
|
|
}
|
|
get isTargeted () { return true }
|
|
get basePowerPoints () { return 3 }
|
|
getPrimaryEffectChanges () {
|
|
let modValue = '2'
|
|
if (this.data.raise && this.data.direction === 'Boost') {
|
|
modValue = '4'
|
|
}
|
|
modValue = (this.data.direction === 'Boost' ? '+' : '-') + modValue
|
|
const changes = [
|
|
{ key: this.data.trait.diekey, value: modValue,
|
|
priority: 0, mode: foundry.CONST.ACTIVE_EFFECT_MODES.ADD } ]
|
|
if (this.data.direction === 'Lower' && this.data.mods.has('greater')) {
|
|
changes.push({
|
|
key: this.data.trait.modkey, mode: foundry.CONST.ACTIVE_EFFECT_MODES.ADD,
|
|
value: -2, priority: 0})
|
|
}
|
|
return changes
|
|
}
|
|
async createSecondaryEffects (maintId) {
|
|
const docs = await super.createSecondaryEffects(maintId)
|
|
if (this.data.raise && this.data.direction === 'Lower') {
|
|
const name = 'major ' + this.effectName
|
|
const modValue = (this.data.direction === 'Boost' ? '+2': '-2')
|
|
const changes = [
|
|
{ key: this.data.trait.diekey, value: modValue,
|
|
priority: 0, mode: foundry.CONST.ACTIVE_EFFECT_MODES.ADD } ]
|
|
const doc = this.createEffectDocument(this.icon, name, changes)
|
|
doc.duration.seconds = 594
|
|
doc.description = this.description +
|
|
"<p>This is the raise effect which can be shaken off separately.</p>"
|
|
doc.flags[moduleName].maintId = maintId
|
|
docs.push(doc)
|
|
}
|
|
return docs
|
|
}
|
|
get effectName () {
|
|
let name = `${this.data.direction} ${this.data.trait.name}`
|
|
const nameMods = []
|
|
if (this.data.mods.has('greater')) {
|
|
nameMods.push('Greater')
|
|
}
|
|
if (this.data.direction === 'Lower' && this.data.mods.has('strong')) {
|
|
nameMods.push('Strong')
|
|
}
|
|
if (nameMods.length > 0) {
|
|
name += ` (${nameMods.join(', ')})`
|
|
}
|
|
return name
|
|
}
|
|
get description () {
|
|
let desc = super.description
|
|
const amount = `${this.data.raise ? 2 : 1} die type${this.data.raise ? 's' :''}`
|
|
desc += `<p>${this.data.direction === 'Boost' ? 'Raise' : 'Lower'} the
|
|
target's ${this.data.trait.name} die type ${amount}.`
|
|
if (this.data.mods.has('greater')) {
|
|
if (this.data.direction === 'Boost') {
|
|
desc += ` Additionally, the target gains a free ${this.data.trait.name}
|
|
reroll once per ${this.data.raise ? 'action' : 'round'}.`
|
|
} else {
|
|
desc += ` Additionally, the target suffers a -2 penalty to their
|
|
${this.data.trait.name} rolls.`
|
|
}
|
|
}
|
|
desc += '</p>'
|
|
if (this.data.direction === 'Lower') {
|
|
desc += `<p>At the end of the target's following turns, they attempt to shake off
|
|
the affect with a Spirit${this.data.mods.has('strong') ? ' -2' : ''}
|
|
roll as a free action. Success reduces the effect one die type. A raise
|
|
completely shakes off the effect.</p>`
|
|
}
|
|
return desc
|
|
}
|
|
get modifiers () {
|
|
const mods = super.modifiers
|
|
mods.push(
|
|
{name: 'Greater Boost/Lower Trailt', value: 2, id: 'greater', epic: true, effect: false})
|
|
mods.push(
|
|
{ name: 'Strong (lower only)', value: 1, id: 'strong', epic: false, effect: false }
|
|
)
|
|
return mods
|
|
}
|
|
get menuInputs () {
|
|
const inputs = super.menuInputs
|
|
let traitOptions = ['Agility', 'Smarts', 'Spirit', 'Strength', 'Vigor']
|
|
const allSkills = new Set()
|
|
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.add(name)
|
|
}
|
|
}
|
|
}
|
|
traitOptions = traitOptions.concat(Array.from(allSkills).sort())
|
|
this.data.traits = traits
|
|
inputs.push({type: 'select', label: 'Trait', options: traitOptions})
|
|
inputs.push({type: 'info', label: 'Boost or Lower?'})
|
|
inputs.push({type: 'radio', label: 'Boost', options: ['isBoost', true]})
|
|
inputs.push({type: 'radio', label: 'Lower', options: ['isBoost', false]})
|
|
return inputs
|
|
}
|
|
async parseValues () {
|
|
await super.parseValues()
|
|
this.data.trait = this.data.traits[this.data.values.shift()]
|
|
this.data.values.shift()
|
|
this.data.direction = this.data.values.shift() ? 'Boost' : 'Lower'
|
|
}
|
|
get powerPoints () {
|
|
let total = super.powerPoints
|
|
return total
|
|
}
|
|
}
|
|
|
|
class BurstEffect extends PowerEffect {
|
|
get name () { return 'Blast' }
|
|
get icon () { return 'icons/magic/sonic/projectile-shock-wave-blue.webp' }
|
|
get duration () { return 0 }
|
|
get isTargeted () { return true }
|
|
get usePrimaryEffect () { return false }
|
|
get isDamaging () { return true }
|
|
get basePowerPoints () { return 3 }
|
|
get hasAoe () { return true }
|
|
get modifiers () {
|
|
const mods = super.modifiers
|
|
mods.push(
|
|
{ name: 'Damage', value: 2, id: 'damage', epic: false, effect: false },
|
|
{ name: 'Greater Burst', value: 4, id: 'greater', epic: true, effect: false },
|
|
)
|
|
return mods
|
|
}
|
|
get description () {
|
|
const dmgDie = (
|
|
this.data.mods.has('greater') ? 4 :
|
|
(this.data.mods.has('damage') ? 3 : 2)) + (this.data.raise ? 1 : 0)
|
|
return super.description + `
|
|
<p>The blast covers a Cone or Stream template and does ${dmgDie}d6 damage</p>`
|
|
}
|
|
}
|
|
|
|
class ConfusionEffect extends PowerEffect {
|
|
get name () { return 'Confusion' }
|
|
get icon () { return 'icons/magic/control/hypnosis-mesmerism-swirl.webp' }
|
|
get duration () { return 0 }
|
|
get isTargeted () { return true }
|
|
get usePrimaryEffect () { return false }
|
|
get basePowerPoints () { return 2 }
|
|
get hasAoe () { return true }
|
|
get menuInputs () {
|
|
const inputs = super.menuInputs
|
|
inputs.push
|
|
inputs.push({ type: 'select', label: 'Area of Effect',
|
|
options: [
|
|
{html: 'Small Blast Template (0)', value: 's', selected: false},
|
|
{html: 'Medium Blast Template (0)', value: 'm', selected: true},
|
|
{html: 'Large Blast Template (+1)', value: 'l', selected: false},
|
|
]})
|
|
return inputs
|
|
}
|
|
get modifiers () {
|
|
const mods = super.modifiers
|
|
mods.push(
|
|
{ name: 'Greater Confusion', value: 2, id: 'greater', epic: true, effect: false },
|
|
)
|
|
return mods
|
|
}
|
|
get powerPoints () {
|
|
let total = super.powerPoints
|
|
total += (this.data.aoe === 'l' ? 1 : 0)
|
|
return total
|
|
}
|
|
get menuButtons () {
|
|
const data = [
|
|
{ label: 'Apply with Distracted', value: 'distracted' },
|
|
{ label: 'Apply with Vulnerable', value: 'vulnerable' },
|
|
{ label: 'Apply with both (raise)', value: 'raise' },
|
|
{ label: 'Cancel', value: 'cancel' },
|
|
]
|
|
return data
|
|
}
|
|
async parseValues () {
|
|
await super.parseValues()
|
|
this.data.distracted = this.data.button === 'distracted' || this.data.button === 'raise'
|
|
this.data.vulnerable = this.data.button === 'vulnerable' || this.data.button === 'raise'
|
|
this.data.aoe = this.data.values.shift()
|
|
}
|
|
get description () {
|
|
const size = (
|
|
this.data.aoe === 'l' ? 'LBT' : (this.data.aoe === 's' ? 'SBT' : 'MBT'))
|
|
let effect = 'Vulnerable'
|
|
if (this.data.raise) {
|
|
effect = 'both Distracted and Vulnerable'
|
|
} else if (this.data.distracted) {
|
|
effect = 'Distracted'
|
|
}
|
|
if (this.data.mods.has('Greater')) {
|
|
effect += ' as well as Shaken'
|
|
}
|
|
return super.description + `
|
|
<p>The targets in the ${size} are ${effect}.</p>`
|
|
}
|
|
async createSecondaryEffects (maintId) {
|
|
const docs = await super.createSecondaryEffects(maintId)
|
|
if (this.data.distracted) {
|
|
PowerEffect.getStatus('SWADE.Distr', 'Distracted', false).then(v => docs.push(v))
|
|
}
|
|
if (this.data.distracted) {
|
|
PowerEffect.getStatus('SWADE.Vuln', 'Vulnerable', false).then(v => docs.push(v))
|
|
}
|
|
if (this.data.mods.has('greater')) {
|
|
PowerEffect.getStatus('SWADE.Shaken', 'Shaken', false).then(v => docs.push(v))
|
|
}
|
|
return docs
|
|
}
|
|
}
|
|
|
|
class CurseEffect extends PowerEffect {
|
|
get name () { return 'Curse' }
|
|
get icon () { return 'icons/magic/control/voodoo-doll-pain-damage-purple.webp' }
|
|
get duration () { return 500*24*60*6 }
|
|
get isTargeted () { return true }
|
|
get oneTarget () { return true }
|
|
get isRaisable () { return false }
|
|
get basePowerPoints () { return 5 }
|
|
get modifiers () {
|
|
const mods = super.modifiers
|
|
mods.push({
|
|
name: 'Turn to Stone', value: 5, id: 'turntostone', epic: true, effect: false
|
|
})
|
|
return mods
|
|
}
|
|
get description () {
|
|
let desc = super.description
|
|
desc += `<p>The victim must defend with a Spirit roll opposed by the
|
|
caster's arcane skill roll. Failure means the victim suffers a level
|
|
of Fatigue immediately.</p>`
|
|
if (this.data.mods.has('turntostone')) {
|
|
desc += `<p>On every following run the victim must make a Spirit roll
|
|
or take a level of Fatigue. When Incapacitated, the victim turns to
|
|
stone, with a Hardness equal to his Tougness.</p>`
|
|
} else {
|
|
desc += `<p>At sunset every day, the victim suffers a level of Fatigue.
|
|
When Incapacitated by this, he makes a Vigor roll each day to avoid
|
|
death.</p>`
|
|
}
|
|
desc += `<p><strong>Breaking the curse:</strong> The curse can be lifted by
|
|
the original caster at will, and ends if the caster is slain. Dispel at -2
|
|
also removes the curse, but each individual may only try once.</p>`
|
|
return desc
|
|
}
|
|
}
|
|
|
|
class DamageFieldEffect extends PowerEffect {
|
|
get name () { return 'Damage Field' }
|
|
get icon () { return 'icons/magic/defensive/shield-barrier-blades-teal.webp' }
|
|
get duration () { return 5 }
|
|
get basePowerPoints () { return 4 }
|
|
get isTargeted () { return true }
|
|
get oneTarget () { return true }
|
|
get isRaisable () { return false }
|
|
get modifiers () {
|
|
const mods = super.modifiers
|
|
mods.push({ name: 'Area of Effect', value: 2, id: 'aoe', epic: false, effect: false })
|
|
mods.push({ name: 'Damage', value: 2, id: 'damage', epic: false, effect: false })
|
|
mods.push({ name: 'Greater Damage Field', value: 4, id: 'greater', epic: true, effect: false })
|
|
mods.push({ name: 'Mobile', value: 2, id: 'mobile', epic: false, effect: false })
|
|
return mods
|
|
}
|
|
get description () {
|
|
let desc = super.description
|
|
let area = 'all adjacent creatures'
|
|
let damage = '2d4'
|
|
if (this.data.mods.has('greater')) {
|
|
damage = '3d6 (heavy weapon)'
|
|
} else if (this.data.mods.has('damage')) {
|
|
damage = '2d6'
|
|
}
|
|
if (this.data.mods.has('aoe')) {
|
|
area = 'all creatures within a MBT'
|
|
}
|
|
desc += `<p>At the end of the recipient's turn, ${area}
|
|
automatically take ${damage} damage.`
|
|
if (this.data.mods.has('mobile')) {
|
|
desc += `The caster may detach the damage field from the recipient and
|
|
move it up to his Smarts die type each round, as a limited free action.`
|
|
}
|
|
desc += '</p>'
|
|
return desc
|
|
}
|
|
getPrimaryEffectChanges () {
|
|
const base = 'flags.swade.auras.damagefield'
|
|
const priority = 0
|
|
const mode = foundry.CONST.ACTIVE_EFFECT_MODES.OVERRIDE
|
|
const changes = [
|
|
{ key: `${base}.enabled`, value: true, priority, mode },
|
|
{ key: `${base}.walls`, value: true, priority, mode },
|
|
{ key: `${base}.color`, value: '#ffcc00', priority, mode },
|
|
{ key: `${base}.alpha`, value: 0.1, priority, mode },
|
|
{ key: `${base}.radius`, value: (this.data.mods.has('aoe') ? 1.5 : 0.5), priority, mode },
|
|
{ key: `${base}.visibleTo`, value: [-1, 0, 1], priority, mode },
|
|
]
|
|
return changes
|
|
}
|
|
}
|
|
|
|
class DarksightEffect extends PowerEffect {
|
|
get name () { return 'Darksight' }
|
|
get icon () { return 'icons/magic/perception/eye-ringed-glow-angry-small-teal.webp' }
|
|
get duration () { return 600 }
|
|
get basePowerPoints () { return 1 }
|
|
get isTargeted () { return true }
|
|
get hasAdditionalRecipients () { return true }
|
|
get additionalRecipientCost () { return 1 }
|
|
get modifiers () {
|
|
const mods = super.modifiers
|
|
mods.push({name: 'Greater Darksight', value: 2, id: 'greater', epic: true, effect: false})
|
|
return mods
|
|
}
|
|
get description () {
|
|
let desc = super.description
|
|
desc += '<p>'
|
|
if (this.data.mods.has('greater')) {
|
|
desc += `Can see in all darkness, ignoring all illumination penalties and
|
|
4 points of penalties from invisible creatures`
|
|
} else if (this.data.raise) {
|
|
desc += 'Can see in Pitch Darkness and ignore up to 6 points of illumination penalties'
|
|
} else {
|
|
desc += 'Can see in darkness and ignore 4 points of illumination penalties'
|
|
}
|
|
desc += '</p>'
|
|
return desc
|
|
}
|
|
get effectName () {
|
|
if (this.data.mods.has('greater')) {
|
|
return 'Greater Darksight'
|
|
} else if (this.data.raise) {
|
|
return 'Major Darksight'
|
|
} else {
|
|
return 'Darksignt'
|
|
}
|
|
}
|
|
}
|
|
|
|
class DeflectionEffect extends PowerEffect {
|
|
get name () { return 'Deflection' }
|
|
get icon () { return 'icons/magic/defensive/shield-barrier-deflect-teal.webp' }
|
|
get duration () { return 5 }
|
|
get basePowerPoints () { return 2 }
|
|
get isTargeted () { return true }
|
|
get hasAdditionalRecipients () { return true }
|
|
get additionalRecipientCost () { return 1 }
|
|
get menuButtons () {
|
|
const data = [
|
|
{ label: 'Melee', value: 'melee' },
|
|
{ label: 'Ranged', value: 'vulnerable' },
|
|
{ label: 'Raise (both)', value: 'raise' },
|
|
{ label: 'Cancel', value: 'cancel' },
|
|
]
|
|
return data
|
|
}
|
|
async parseValues () {
|
|
await super.parseValues()
|
|
this.data.affects = this.data.button === 'raise' ? 'all' : this.data.button
|
|
}
|
|
get effectName () {
|
|
return `Deflection (${this.data.affects})`
|
|
}
|
|
get description () {
|
|
return super.description + `<p>Attackers subtract -2 from ${this.data.affects}
|
|
attacks when targeting this creature.</p>`
|
|
}
|
|
}
|
|
|
|
class DetectConcealArcanaEffect extends PowerEffect {
|
|
get name () { return 'Detect/Conceal Arcana' }
|
|
get icon () { return this.data.detect ?
|
|
'icons/magic/perception/third-eye-blue-red.webp' :
|
|
'icons/magic/perception/silhouette-stealth-shadow.webp' }
|
|
get duration () { return this.data.detect ? 5 : 600 }
|
|
get basePowerPoints () { return 2 }
|
|
get isTargeted () { return true }
|
|
get hasAdditionalRecipients () { return true }
|
|
get additionalRecipientCost () { return (this.data?.aoe || 0) > 0 ? 0 : 1 }
|
|
get modifiers () {
|
|
const mods = super.modifiers
|
|
mods.push({name: 'Alignment Sense (detect)', value: 1, id: 'alignment', epic: false, effect: false})
|
|
mods.push({name: 'Identify (detect)', value: 1, id: 'identify', epic: false, effect: false})
|
|
mods.push({name: 'Strong (conceal)', value: 1, id: 'strong', epic: false, effect: false})
|
|
return mods
|
|
}
|
|
get hasAoe () { return true }
|
|
get menuInputs () {
|
|
const inputs = super.menuInputs
|
|
inputs.push({ type: 'select', label: 'Area of Effect (conceal)',
|
|
options: [
|
|
{html: 'None', value: 0, selected: true},
|
|
{html: 'Medium Blast Template (+1)', value: 1, selected: false},
|
|
{html: 'Large Blast Template (+2)', value: 2, selected: false},
|
|
]})
|
|
inputs.push({type: 'info', label: 'Detect or Conceal?'})
|
|
inputs.push({type: 'radio', label: 'Detect', options: ['isDetect', true]})
|
|
inputs.push({type: 'radio', label: 'Conceal', options: ['isDetect', false]})
|
|
return inputs
|
|
}
|
|
async parseValues () {
|
|
await super.parseValues()
|
|
this.data.aoe = this.data.values.shift()
|
|
this.data.values.shift()
|
|
this.data.detect = this.data.values.shift()
|
|
}
|
|
get powerPoints () {
|
|
return super.powerPoints + this.data.aoe
|
|
}
|
|
get effectName () {
|
|
return `${this.data.detect ? 'Detect' : 'Conceal'} Arcana`
|
|
}
|
|
get description () {
|
|
let desc = super.description
|
|
if (this.data.detect) {
|
|
desc += `<p>The recipient can see and detect all supernatural persons,
|
|
objects, or effects in sight. This includes invisible foes, enchanted
|
|
objects, and so on.`
|
|
if (this.data.raise) {
|
|
desc += `Since this was major Detect Arcana, the type of enchantments
|
|
is also known.`
|
|
}
|
|
desc+= `</p><p>If cast to learn more about a creature, the caster learns
|
|
active powers and arcane abilities.`
|
|
if (this.data.raise) {
|
|
desc += `As major Detect in this mode, the caster also learns any
|
|
Weaknesses common to that creature type.`
|
|
}
|
|
if (this.data.mods.has('identify')) {
|
|
desc += `<p>Items detected also give the recipient an idea of their
|
|
powers and how to activate them.</p>`
|
|
}
|
|
if (this.data.mods.has('alignment')) {
|
|
desc += `<p>The recipient can also detect the presence and location
|
|
of supernatural good or evil within range, regardless of line of sight.</p>`
|
|
}
|
|
desc += `</p><p><strong>Invisible Creatures:</strong> The recipient may
|
|
also ignore ${this.data.raise ? 'all' : 'up to 4 points of'} penalties
|
|
when attacking invisible or magically concealed foes.</p>`
|
|
} else {
|
|
let area = 'one item or being'
|
|
if (this.data.aoe !== 0) {
|
|
area = `everything in a sphere the size of a
|
|
${this.data.aoe === 1 ? 'Medium' : 'Large'} Blast Template`
|
|
}
|
|
desc += `<p>Conceal ${area} from the Detect Magic ability for
|
|
one hour. Attempts to <em>detect arcana</em> suffer a
|
|
${(this.data.mods.has('strong') ? -2 : 0) + this.data.raise ? -2 : -4} penalty.`
|
|
}
|
|
return desc
|
|
}
|
|
}
|
|
|
|
class DisguiseEffect extends PowerEffect {
|
|
get name () { return 'Disguise' }
|
|
get icon () { return 'icons/equipment/head/mask-carved-wood-white.webp' }
|
|
get duration () { return 100 }
|
|
get basePowerPoints () { return 2 }
|
|
get isTargeted () { return true }
|
|
get hasAdditionalRecipients () { return true }
|
|
get additionalRecipientCost () { return 1 }
|
|
get modifiers () {
|
|
return [
|
|
...super.modifiers,
|
|
{name: 'Size', value: 1, id: 'size', epic: false, effect: false}
|
|
]
|
|
}
|
|
get description () {
|
|
let size = this.data.mods.has('size') ? 'of the same size as' : 'within two sizes of'
|
|
return super.description + `
|
|
<p>Assume the appearance of another person ${size} the recipient. Anyone
|
|
who has cause to doubt the disguise may make a Notice roll at ${this.data.raise ? -4 : -4}
|
|
as a free action to see through the disguise.</p>`
|
|
}
|
|
}
|
|
|
|
const PowerClasses = {
|
|
"arcane-protection": ArcaneProtectionEffect,
|
|
banish: BanishEffect,
|
|
barrier: BarrierEffect,
|
|
"beast-friend": BeastFriendEffect,
|
|
blast: BlastEffect,
|
|
blind: BlindEffect,
|
|
bolt: BoltEffect,
|
|
"boost-lower-trait": BoostLowerTraitEffect,
|
|
"boost-trait": BoostLowerTraitEffect,
|
|
burrow: BurrowEffect,
|
|
burst: BurstEffect,
|
|
confusion: ConfusionEffect,
|
|
curse: CurseEffect,
|
|
"damage-field": DamageFieldEffect,
|
|
darksight: DarksightEffect,
|
|
deflection: DeflectionEffect,
|
|
"detect-conceal-arcana": DetectConcealArcanaEffect,
|
|
"disguise" : DisguiseEffect,
|
|
"lower-trait": BoostLowerTraitEffect,
|
|
}
|
|
|
|
/* ---------------------------------------------------------------- */
|
|
|
|
export async function powerEffectManagementHook(effect, data, userId) {
|
|
if (game.user.id !== userId) {
|
|
return
|
|
}
|
|
const maintId = effect.getFlag(moduleName, 'maintainingId')
|
|
if (!maintId) {
|
|
return
|
|
}
|
|
const mutateOptions = {
|
|
permanent: true,
|
|
comparisonKeys: {
|
|
ActiveEffect: 'id'
|
|
}
|
|
}
|
|
const targetIds = effect.getFlag(moduleName, 'targetIds') || []
|
|
for (const targetId of targetIds) {
|
|
const mutation = {
|
|
embedded: { ActiveEffect: {} }
|
|
}
|
|
const target = canvas.tokens.get(targetId)
|
|
if (!target) {
|
|
continue
|
|
}
|
|
const effects = target.actor.effects.filter(
|
|
e => e.getFlag(moduleName, 'maintId') === maintId)
|
|
for (const effect of effects) {
|
|
mutation.embedded.ActiveEffect[effect.id] = warpgate.CONST.DELETE
|
|
}
|
|
mutateOptions.description = `${effect.parent.name} is no longer ${effect.name} on ${target.name}`
|
|
await warpgate.mutate(target.document, mutation, {}, mutateOptions)
|
|
}
|
|
}
|
|
|
|
export async function powers (options = {}) {
|
|
const token = 'token' in options ? options.token : null
|
|
if (token === undefined || token === null) {
|
|
ui.notifications.error('Please select one token to be the caster')
|
|
return
|
|
}
|
|
|
|
const targets = 'targets' in options ? Array.from(options.targets) : []
|
|
const item = 'item' in options ? options.item : null
|
|
const swid = options?.name || item?.system.swid || null
|
|
|
|
if (swid in PowerClasses) {
|
|
const runner = new PowerClasses[swid](token, targets)
|
|
runner.powerEffect()
|
|
return
|
|
}
|
|
ui.notifications.error(`No power effect found for ${name}`)
|
|
}
|
|
|