543 lines
13 KiB
JavaScript
543 lines
13 KiB
JavaScript
/* globals warpgate */
|
||
import { moduleName } from './globals.js'
|
||
|
||
const MAINTAIN_ICON = 'icons/magic/symbols/runes-star-blue.webp'
|
||
|
||
export class PowerEffect {
|
||
constructor (token, targets) {
|
||
this.source = token
|
||
this.targets = targets
|
||
this.data = {}
|
||
}
|
||
|
||
static async getStatus (label, name, favorite = true) {
|
||
const effect = deepClone(
|
||
CONFIG.statusEffects.find((se) => se.label === label)
|
||
)
|
||
effect.name = 'name' in effect ? effect.name : effect.label
|
||
if (!('flags' in effect)) {
|
||
effect.flags = {}
|
||
}
|
||
if (favorite) {
|
||
if (!('swade' in effect.flags)) {
|
||
effect.flags.swade = {}
|
||
}
|
||
effect.flags.swade.favorite = true
|
||
}
|
||
effect.flags.core = { statusId: effect.id }
|
||
return effect
|
||
}
|
||
|
||
createEffectDocument (icon, name, changes = null) {
|
||
if (changes === null) {
|
||
changes = []
|
||
}
|
||
return {
|
||
icon,
|
||
name,
|
||
changes,
|
||
description: `<p>From <strong>${this.source.name}</strong> casting <em>${this.name}</em></p>`,
|
||
duration: { rounds: 99 },
|
||
flags: {
|
||
[moduleName]: {
|
||
powerEffect: true
|
||
},
|
||
swade: {
|
||
loseTurnOnHold: false,
|
||
expiration:
|
||
CONFIG.SWADE.CONST.STATUS_EFFECT_EXPIRATION.StartOfTurnAuto
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
async applyActiveEffects (token, effectDocuments) {
|
||
const mutation = {
|
||
embedded: { ActiveEffect: {} }
|
||
}
|
||
const mutateOptions = {
|
||
permanent: true,
|
||
description: `${this.source.name} applying ${effectDocuments[effectDocuments.length - 1]?.name} to ${token.name}`
|
||
}
|
||
for (const effectDocument of effectDocuments) {
|
||
mutation.embedded.ActiveEffect[effectDocument.name] = effectDocument
|
||
}
|
||
await warpgate.mutate(token.document, mutation, {}, mutateOptions)
|
||
}
|
||
|
||
get name () {
|
||
return 'Unknown Power'
|
||
}
|
||
|
||
get effectName () {
|
||
return this.name
|
||
}
|
||
|
||
get icon () {
|
||
return 'icons/magic/symbols/question-stone-yellow.webp'
|
||
}
|
||
|
||
get duration () {
|
||
return 5
|
||
}
|
||
|
||
get basePowerPoints () {
|
||
return 0
|
||
}
|
||
|
||
get usePrimaryEffect () {
|
||
return true
|
||
}
|
||
|
||
get hasAdditionalRecipients () {
|
||
return false
|
||
}
|
||
|
||
get isDamaging () {
|
||
return false
|
||
}
|
||
|
||
get additionalRecipientCost () {
|
||
return 0
|
||
}
|
||
|
||
get isTargeted () {
|
||
return false
|
||
}
|
||
|
||
get oneTarget () {
|
||
return false
|
||
}
|
||
|
||
get isRaisable () {
|
||
return true
|
||
}
|
||
|
||
get hasAoe () {
|
||
return false
|
||
}
|
||
|
||
get modifiers () {
|
||
const mods = []
|
||
mods.push({
|
||
name: 'Adaptable Caster',
|
||
id: 'adaptable',
|
||
value: 1,
|
||
epic: false,
|
||
effect: false
|
||
})
|
||
mods.push({
|
||
name: 'Fatigue',
|
||
id: 'fatigue',
|
||
value: 2,
|
||
epic: false,
|
||
effect: false
|
||
})
|
||
mods.push({
|
||
name: 'Glow',
|
||
id: 'glow',
|
||
value: 1,
|
||
epic: false,
|
||
effect: true,
|
||
icon: 'icons/magic/light/orb-shadow-blue.webp',
|
||
changes: [
|
||
{
|
||
key: '@Skill{Stealth}[system.die.modifier]',
|
||
value: -2,
|
||
priority: 0,
|
||
mode: foundry.CONST.ACTIVE_EFFECT_MODES.ADD
|
||
}
|
||
]
|
||
})
|
||
mods.push({
|
||
name: 'Shroud',
|
||
id: 'shroud',
|
||
value: 1,
|
||
epic: false,
|
||
effect: true,
|
||
icon: 'icons/magic/perception/shadow-stealth-eyes-purple.webp',
|
||
changes: [
|
||
{
|
||
key: '@Skill{Stealth}[system.die.modifier]',
|
||
value: 1,
|
||
priority: 0,
|
||
mode: foundry.CONST.ACTIVE_EFFECT_MODES.ADD
|
||
}
|
||
]
|
||
})
|
||
if (this.isDamaging) {
|
||
mods.push({
|
||
name: 'Heavy Weapon',
|
||
id: 'heavyweapon',
|
||
value: 2,
|
||
epic: false,
|
||
effect: false
|
||
})
|
||
}
|
||
mods.push({
|
||
name: 'Hinder',
|
||
id: 'hinder',
|
||
value: 1,
|
||
epic: false,
|
||
effect: true,
|
||
icon: 'icons/magic/control/debuff-chains-shackle-movement-red.webp',
|
||
changes: [
|
||
{
|
||
key: 'system.stats.speed.value',
|
||
value: -2,
|
||
priority: 0,
|
||
mode: foundry.CONST.ACTIVE_EFFECT_MODES.ADD
|
||
}
|
||
]
|
||
})
|
||
mods.push({
|
||
name: 'Hurry',
|
||
id: 'hurry',
|
||
value: 1,
|
||
epic: false,
|
||
effect: true,
|
||
icon: 'icons/skills/movement/feet-winged-sandals-tan.webp',
|
||
changes: [
|
||
{
|
||
key: 'system.stats.speed.value',
|
||
value: 2,
|
||
priority: 0,
|
||
mode: foundry.CONST.ACTIVE_EFFECT_MODES.ADD
|
||
}
|
||
]
|
||
})
|
||
if (this.isDamaging) {
|
||
mods.push({
|
||
name: 'Lingering Damage',
|
||
id: 'lingeringdamage',
|
||
value: 2,
|
||
epic: false,
|
||
effect: false
|
||
})
|
||
}
|
||
if (this.hasAoe) {
|
||
mods.push({
|
||
name: 'Selective',
|
||
id: 'selective',
|
||
value: 1,
|
||
epic: false,
|
||
effect: false
|
||
})
|
||
}
|
||
return mods
|
||
}
|
||
|
||
get menuData () {
|
||
return {
|
||
inputs: this.menuInputs,
|
||
buttons: this.menuButtons
|
||
}
|
||
}
|
||
|
||
get menuInputs () {
|
||
const inputs = [
|
||
{ type: 'header', label: `${this.name} Effect` },
|
||
{
|
||
type: 'info',
|
||
label: `Apply ${this.name} Effect (base cost ${this.basePowerPoints} pp)`
|
||
}
|
||
]
|
||
if (this.isTargeted) {
|
||
let label = `<strong>Targets:</strong> ${this.targets.map((t) => t.name).join(',')}`
|
||
if (this.targets.length > 1 && this.hasAdditionalRecipients) {
|
||
label +=
|
||
` (${this.targets.length - 1} additional recipients ` +
|
||
`+${this.additionalRecipientCost} ea.)`
|
||
}
|
||
inputs.push({ type: 'info', label })
|
||
}
|
||
for (const mod of this.modifiers) {
|
||
inputs.push({
|
||
type: 'checkbox',
|
||
label:
|
||
`${mod.epic ? '⭐ ' : ''}${mod.name} ` +
|
||
`(${mod.value >= 0 ? '+' : ''}${mod.value})`
|
||
})
|
||
}
|
||
if (this.isDamaging) {
|
||
inputs.push({
|
||
type: 'select',
|
||
label: 'Armor Piercing',
|
||
options: [
|
||
{ html: 'None', value: 0, selected: true },
|
||
{ html: 'AP 2 (+1)', value: 1, selected: false },
|
||
{ html: 'AP 4 (+2)', value: 2, selected: false },
|
||
{ html: 'AP 6 (+3)', value: 3, selected: false }
|
||
]
|
||
})
|
||
}
|
||
inputs.push({
|
||
type: 'select',
|
||
label: 'Range',
|
||
options: [
|
||
{ html: 'Normal Range', value: 0, selected: true },
|
||
{ html: 'Range ×2 (+1)', value: 1, selected: false },
|
||
{ html: 'Range ×3 (+2)', value: 2, selected: false }
|
||
]
|
||
})
|
||
return inputs
|
||
}
|
||
|
||
get menuButtons () {
|
||
const data = [{ label: 'Apply', value: 'apply' }]
|
||
if (this.isRaisable) {
|
||
data.push({ label: 'Apply with Raise', value: 'raise' })
|
||
}
|
||
data.push({ label: 'Cancel', value: 'cancel' })
|
||
|
||
return data
|
||
}
|
||
|
||
get menuOptions () {
|
||
return {
|
||
title: `${this.name} Effect`,
|
||
defaultButton: 'Cancel',
|
||
options: {}
|
||
}
|
||
}
|
||
|
||
async powerEffect () {
|
||
const { buttons, inputs } = await warpgate.menu(
|
||
this.menuData,
|
||
this.menuOptions
|
||
)
|
||
if (buttons && buttons !== 'cancel') {
|
||
this.data.button = buttons
|
||
this.data.values = inputs
|
||
await this.parseValues()
|
||
await this.apply()
|
||
await this.sideEffects()
|
||
await this.chatMessage()
|
||
}
|
||
}
|
||
|
||
async parseValues () {
|
||
this.data.rawValues = deepClone(this.data.values)
|
||
this.data.raise = this.data.button === 'raise'
|
||
for (let i = 0; i < 2; i++) {
|
||
this.data.values.shift()
|
||
}
|
||
if (this.isTargeted) {
|
||
this.data.values.shift()
|
||
}
|
||
this.data.mods = new Set()
|
||
for (const mod of this.modifiers) {
|
||
const modEnabled = this.data.values.shift()
|
||
if (modEnabled) {
|
||
this.data.mods.add(mod.id)
|
||
}
|
||
}
|
||
if (this.isDamaging) {
|
||
this.data.armorPiercing = this.data.values.shift()
|
||
}
|
||
this.data.range = this.data.values.shift()
|
||
}
|
||
|
||
async createSecondaryEffects (maintId) {
|
||
const docs = []
|
||
for (const mod of this.modifiers) {
|
||
if (this.data.mods.has(mod.id) && mod.effect) {
|
||
const doc = this.createEffectDocument(mod.icon, mod.name, mod.changes)
|
||
if (this.duration === 0 && !this.usePrimaryEffect) {
|
||
// set secondary effects of instant spells to expire on victim's next
|
||
// turn
|
||
doc.duration.rounds = 1
|
||
doc.flags.swade.expiration =
|
||
CONFIG.SWADE.CONST.STATUS_EFFECT_EXPIRATION.EndOfTurnAuto
|
||
} else {
|
||
doc.duration.seconds = 594
|
||
doc.flags[moduleName].maintId = maintId
|
||
}
|
||
docs.push(doc)
|
||
}
|
||
}
|
||
return docs
|
||
}
|
||
|
||
getPrimaryEffectChanges () {
|
||
const changes = [
|
||
{
|
||
key: 'flags.swade-mb-helpers.powerAffected',
|
||
value: 1,
|
||
priority: 0,
|
||
mode: foundry.CONST.ACTIVE_EFFECT_MODES.OVERRIDE
|
||
}
|
||
]
|
||
return changes
|
||
}
|
||
|
||
get description () {
|
||
return ''
|
||
}
|
||
|
||
async createPrimaryEffect (maintId) {
|
||
const doc = this.createEffectDocument(
|
||
this.icon,
|
||
this.effectName,
|
||
this.getPrimaryEffectChanges()
|
||
)
|
||
doc.description += this.description
|
||
doc.flags[moduleName].maintId = maintId
|
||
doc.duration.seconds = 594
|
||
return doc
|
||
}
|
||
|
||
async createMaintainEffect (maintId) {
|
||
let icon = MAINTAIN_ICON
|
||
if (!this.usePrimaryEffect) {
|
||
icon = this.icon
|
||
}
|
||
const doc = this.createEffectDocument(
|
||
icon,
|
||
`Maintaining ${this.effectName}`,
|
||
[]
|
||
)
|
||
doc.duration.rounds = this.duration
|
||
doc.description += this.description
|
||
doc.flags.swade.expiration =
|
||
CONFIG.SWADE.CONST.STATUS_EFFECT_EXPIRATION.EndOfTurnPrompt
|
||
doc.flags.swade.loseTurnOnHold = true
|
||
doc.flags[moduleName].maintainingId = maintId
|
||
doc.flags[moduleName].targetIds = this.targets.map((t) => t.id)
|
||
return doc
|
||
}
|
||
|
||
// eslint-disable-next-line no-unused-vars
|
||
async secondaryDocsForTarget (docs, target) {
|
||
return deepClone(docs)
|
||
}
|
||
|
||
// eslint-disable-next-line no-unused-vars
|
||
async primaryDocForTarget (doc, target) {
|
||
const newDoc = deepClone(doc)
|
||
newDoc.flags[moduleName].maintainingId = doc.flags[moduleName].maintId
|
||
newDoc.flags[moduleName].targetIds = [target.id]
|
||
return newDoc
|
||
}
|
||
|
||
async apply () {
|
||
const maintId = randomID()
|
||
const secondaryDocs = await this.createSecondaryEffects(maintId)
|
||
const primaryDoc = await this.createPrimaryEffect(maintId)
|
||
const maintainDoc = await this.createMaintainEffect(maintId)
|
||
if (this.isTargeted) {
|
||
for (const target of this.targets) {
|
||
const targetDocs = await this.secondaryDocsForTarget(
|
||
secondaryDocs,
|
||
target
|
||
)
|
||
if (this.duration > 0 || this.usePrimaryEffect) {
|
||
targetDocs.push(await this.primaryDocForTarget(primaryDoc, target))
|
||
}
|
||
if (targetDocs.length > 0) {
|
||
await this.applyActiveEffects(target, targetDocs)
|
||
}
|
||
}
|
||
}
|
||
if (this.duration > 0) {
|
||
await this.applyActiveEffects(this.source, [maintainDoc])
|
||
}
|
||
}
|
||
|
||
async sideEffects () {
|
||
if (this.data.mods.has('fatigue') && this.isTargeted) {
|
||
for (const target of this.targets) {
|
||
const actor = target.actor
|
||
const update = {
|
||
system: {
|
||
fatigue: {
|
||
value: actor.system.fatigue.value + 1
|
||
}
|
||
}
|
||
}
|
||
if (actor.system.fatigue.value < actor.system.fatigue.max) {
|
||
await actor.update(update)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
get powerPoints () {
|
||
let total = this.basePowerPoints
|
||
for (const mod of this.modifiers) {
|
||
if (this.data.mods.has(mod.id)) {
|
||
total += mod.value
|
||
}
|
||
}
|
||
if (this.targets.length > 1 && this.hasAdditionalRecipients) {
|
||
total += (this.targets.length - 1) * this.additionalRecipientCost
|
||
}
|
||
total += this.data.range
|
||
if (this.isDamaging) {
|
||
total += this.data.armorPiercing
|
||
}
|
||
return total
|
||
}
|
||
|
||
get chatMessageEffects () {
|
||
const list = []
|
||
if (this.hasAdditionalRecipients && this.targets.length > 1) {
|
||
list.push(`${this.targets.length - 1} Additional Recipients`)
|
||
}
|
||
if (this.data.mods.has('adaptable')) {
|
||
list.push('Different Trapping (Adaptable Caster)')
|
||
}
|
||
if (this.data.mods.has('fatigue')) {
|
||
list.push('Fatigue (applied to targets')
|
||
}
|
||
if (this.data.mods.has('heavyweapon')) {
|
||
list.push('Heavy Weapon')
|
||
}
|
||
if (this.data.mods.has('lingeringdamage')) {
|
||
list.push('Lingering Damage')
|
||
}
|
||
if (this.data.mods.has('selective')) {
|
||
list.push('Selective')
|
||
}
|
||
if (this.isDamaging && this.data.armorPiercing > 0) {
|
||
list.push(`AP ${this.data.armorPiercing * 2}`)
|
||
}
|
||
if (this.data.range > 0) {
|
||
list.push(`Range ×${this.data.range + 1}`)
|
||
}
|
||
return list
|
||
}
|
||
|
||
get chatMessageText () {
|
||
let text = `<p>Cast ${this.name}`
|
||
if (this.isTargeted && this.targets.length > 0) {
|
||
text += ` on ${this.targets.map((t) => t.name).join(', ')}`
|
||
}
|
||
text += '</p>'
|
||
const desc = this.description
|
||
if (desc) {
|
||
text += `<details><summary>Description</summary>${desc}</details>`
|
||
}
|
||
const effects = this.chatMessageEffects
|
||
if (effects.length > 0) {
|
||
text +=
|
||
'<details><summary>Other Effects:</summary><ul><li>' +
|
||
effects.join('</li><li>') +
|
||
'</li></ul></details>'
|
||
}
|
||
return text
|
||
}
|
||
|
||
async chatMessage () {
|
||
return ChatMessage.create(
|
||
{
|
||
flavor: `Calculated cost: ${this.powerPoints} pp`,
|
||
speaker: ChatMessage.getSpeaker(this.source.actor),
|
||
content: this.chatMessageText,
|
||
whisper: ChatMessage.getWhisperRecipients('GM', game.user.name)
|
||
},
|
||
{ chatBubble: false }
|
||
)
|
||
}
|
||
}
|