swade-mb-helpers/scripts/basePowers.js

380 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 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})
}
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` },
]
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: 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' },
{ label: 'Apply with Raise', value: 'raise' },
{ 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
}
async createPrimaryEffect (maintId) {
const doc = this.createEffectDocument(this.icon, this.effectName,
this.getPrimaryEffectChanges())
doc.flags[moduleName].maintId = maintId
doc.duration.seconds = 594
return doc
}
async createMaintainEffect (maintId) {
const doc = this.createEffectDocument(
MAINTAIN_ICON, `Maintaining ${this.name}`, [])
doc.duration.rounds = this.duration
doc.flags.swade.expiration = CONFIG.SWADE.CONST.STATUS_EFFECT_EXPIRATION.StartOfTurnPrompt
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)
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.ap
}
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.targets.length > 0) {
text += ` on ${this.targets.map(t => t.name).join(', ')}`
}
text += '</p>'
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 });
}
}