reorg part 2
This commit is contained in:
parent
53d30e80e9
commit
c8f27770b6
@ -1,7 +1,3 @@
|
||||
# SPDX-FileCopyrightText: 2022 Johannes Loher
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
root = true
|
||||
|
||||
[*]
|
||||
|
||||
@ -1,8 +1,3 @@
|
||||
// SPDX-FileCopyrightText: 2022 Johannes Loher
|
||||
// SPDX-FileCopyrightText: 2022 David Archibald
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
module.exports = {
|
||||
parserOptions: {
|
||||
ecmaVersion: 2020,
|
||||
@ -14,16 +9,13 @@ module.exports = {
|
||||
browser: true,
|
||||
},
|
||||
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'@typhonjs-fvtt/eslint-config-foundry.js/0.8.0',
|
||||
'plugin:prettier/recommended',
|
||||
],
|
||||
extends: ['eslint:recommended', '@typhonjs-fvtt/eslint-config-foundry.js/0.8.0', 'plugin:prettier/recommended'],
|
||||
|
||||
plugins: [],
|
||||
|
||||
rules: {
|
||||
// Specify any specific ESLint rules.
|
||||
'no-undef': 'error',
|
||||
'no-unused-vars': 'warning',
|
||||
},
|
||||
|
||||
overrides: [
|
||||
|
||||
@ -8,10 +8,10 @@
|
||||
}
|
||||
],
|
||||
"url": "https://git.bloy.org/foundryvtt/swade-mb-helpers",
|
||||
"version": "0.0.0",
|
||||
"version": "2.4.3",
|
||||
"compatibility": {
|
||||
"minimum": "10",
|
||||
"verified": "10"
|
||||
"minimum": "11",
|
||||
"verified": "11"
|
||||
},
|
||||
"scripts": [],
|
||||
"esmodules": [
|
||||
|
||||
@ -1,23 +1,23 @@
|
||||
import { log, moduleHelpers } from './globals.js'
|
||||
import { requestFearRollFromTokens, requestRollFromTokens } from './helpers.js'
|
||||
import { powerEffects } from './powerEffects.js'
|
||||
import { powers } from './powers.js'
|
||||
import { log, moduleHelpers } from './globals.js';
|
||||
import { requestFearRollFromTokens, requestRollFromTokens } from './helpers.js';
|
||||
import { powerEffects } from './powerEffects.js';
|
||||
import { powers } from './powers/powers.js';
|
||||
|
||||
export class api {
|
||||
static registerFunctions () {
|
||||
log('SWADE MB Helpers initialized')
|
||||
api.globals()
|
||||
static registerFunctions() {
|
||||
log('SWADE MB Helpers initialized');
|
||||
api.globals();
|
||||
}
|
||||
|
||||
static globals () {
|
||||
const moduleName = 'swade-mb-helpers'
|
||||
static globals() {
|
||||
const moduleName = 'swade-mb-helpers';
|
||||
game.modules.get(moduleName).api = {
|
||||
rulesVersion: moduleHelpers.rulesVersion,
|
||||
fearTable: moduleHelpers.fearTableHelper,
|
||||
powerEffects,
|
||||
powers,
|
||||
requestRollFromTokens,
|
||||
requestFearRollFromTokens
|
||||
}
|
||||
requestFearRollFromTokens,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,542 +0,0 @@
|
||||
/* 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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -1,112 +0,0 @@
|
||||
/* globals ColorAdjustmentsSamplerShader */
|
||||
/* globals socketlib */
|
||||
/* globals VisionMode */
|
||||
/* globals warpgate */
|
||||
import { api } from './api.js'
|
||||
import { requestTokenRoll } from './helpers.js'
|
||||
import { preDamageRollModifiers, preTraitRollModifiers } from './rollHelpers.js'
|
||||
import { shapeChangeOnDismiss } from './powerEffects.js'
|
||||
import { log, moduleHelpers } from './globals.js'
|
||||
import { powerEffectManagementHook } from './powers.js'
|
||||
|
||||
function _checkModule (name) {
|
||||
if (!game.modules.get(name)?.active && game.user.isGM) {
|
||||
let action = 'install and activate'
|
||||
if (game.modules.get(name)) action = 'activate'
|
||||
ui.notifications.error(
|
||||
`SWADE MB Helpers requires the ${name} module. Please ${action} it.`)
|
||||
}
|
||||
}
|
||||
|
||||
Hooks.on('setup', api.registerFunctions)
|
||||
|
||||
Hooks.on('init', () => {
|
||||
log('INIT VISION')
|
||||
CONFIG.Canvas.visionModes.basic = new VisionMode({
|
||||
id: 'basic',
|
||||
label: 'VISION.ModeBasicVision',
|
||||
canvas: {
|
||||
shader: ColorAdjustmentsSamplerShader,
|
||||
uniforms: { contrast: 0, saturation: -0.85, brightness: -1.0 }
|
||||
},
|
||||
lighting: {
|
||||
background: { visibility: VisionMode.LIGHTING_VISIBILITY.REQUIRED }
|
||||
},
|
||||
vision: {
|
||||
darkness: { adaptive: false },
|
||||
defaults: { attenuation: 0, contrast: 0, saturation: -0.85, brightness: -1.0 },
|
||||
preferred: true
|
||||
}
|
||||
})
|
||||
CONFIG.Canvas.visionModes.darkvision = new VisionMode({
|
||||
id: 'darkvision',
|
||||
label: 'VISION.ModeDarkvision',
|
||||
canvas: {
|
||||
shader: ColorAdjustmentsSamplerShader,
|
||||
uniforms: { contrast: 0, saturation: 0, brightness: 0.75, tint: [0.8, 0.8, 1.0] }
|
||||
},
|
||||
lighting: {
|
||||
background: { visibility: VisionMode.LIGHTING_VISIBILITY.REQUIRED },
|
||||
levels: {
|
||||
[VisionMode.LIGHTING_LEVELS.DIM]: VisionMode.LIGHTING_LEVELS.BRIGHT
|
||||
}
|
||||
},
|
||||
vision: {
|
||||
darkness: { adaptive: false },
|
||||
defaults: { attenuation: 0.1, contrast: 0, saturation: 0, brightness: 0.75 },
|
||||
preferred: false
|
||||
}
|
||||
})
|
||||
CONFIG.Canvas.visionModes.lowlight = new VisionMode({
|
||||
id: 'lowlight',
|
||||
label: 'Low Light Vision',
|
||||
canvas: {
|
||||
shader: ColorAdjustmentsSamplerShader,
|
||||
uniforms: { contrast: 0, saturation: -0.5, brightness: -0.2 }
|
||||
},
|
||||
lighting: {
|
||||
background: { visibility: VisionMode.LIGHTING_VISIBILITY.REQUIRED }
|
||||
},
|
||||
vision: {
|
||||
darkness: { adaptive: false },
|
||||
defaults: { attenuation: 0.1, contrast: 0, saturation: -0.5, brightness: -0.2 },
|
||||
preferred: false
|
||||
}
|
||||
})
|
||||
CONFIG.Canvas.visionModes.blindsense = new VisionMode({
|
||||
id: 'blindsense',
|
||||
label: 'Blindsense',
|
||||
canvas: {
|
||||
shader: ColorAdjustmentsSamplerShader,
|
||||
uniforms: { contrast: 0, saturation: -0.5, brightness: -0.2 }
|
||||
},
|
||||
lighting: {
|
||||
background: { visibility: VisionMode.LIGHTING_VISIBILITY.DISABLED },
|
||||
coloration: { visibility: VisionMode.LIGHTING_VISIBILITY.DISABLED },
|
||||
illumination: { visibility: VisionMode.LIGHTING_VISIBILITY.DISABLED }
|
||||
},
|
||||
vision: {
|
||||
darkness: { adaptive: false },
|
||||
defaults: { attenuation: 0.3, contrast: -0.5, saturation: 0.75, brightness: 0.5 },
|
||||
preferred: false
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
Hooks.on('swadePreRollAttribute', preTraitRollModifiers)
|
||||
Hooks.on('swadePreRollSkill', preTraitRollModifiers)
|
||||
Hooks.on('swadeRollDamage', preDamageRollModifiers)
|
||||
Hooks.on('deleteActiveEffect', powerEffectManagementHook)
|
||||
|
||||
Hooks.on('ready', () => {
|
||||
_checkModule('warpgate')
|
||||
_checkModule('socketlib')
|
||||
log('Initialized SWADE MB Helpers')
|
||||
warpgate.event.watch(warpgate.EVENT.DISMISS, shapeChangeOnDismiss)
|
||||
})
|
||||
|
||||
Hooks.on('socketlib.ready', () => {
|
||||
const _socket = socketlib.registerModule('swade-mb-helpers')
|
||||
_socket.register('requestTokenRoll', requestTokenRoll)
|
||||
moduleHelpers._socket = _socket
|
||||
})
|
||||
1637
src/module/powers.js
1637
src/module/powers.js
@ -1,1637 +0,0 @@
|
||||
/* globals warpgate */
|
||||
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({
|
||||
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({
|
||||
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({
|
||||
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 () {
|
||||
const 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({
|
||||
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 () {
|
||||
const 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 efct of effects) {
|
||||
mutation.embedded.ActiveEffect[efct.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}`)
|
||||
}
|
||||
59
src/module/powers/arcaneProtection.js
Normal file
59
src/module/powers/arcaneProtection.js
Normal file
@ -0,0 +1,59 @@
|
||||
import { PowerEffect } from './basePowers.js';
|
||||
|
||||
export 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,
|
||||
type: 'checkbox',
|
||||
default: false,
|
||||
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})`;
|
||||
}
|
||||
}
|
||||
86
src/module/powers/banish.js
Normal file
86
src/module/powers/banish.js
Normal file
@ -0,0 +1,86 @@
|
||||
import { PowerEffect } from './basePowers.js';
|
||||
|
||||
export 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({
|
||||
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();
|
||||
}
|
||||
}
|
||||
618
src/module/powers/basePowers.js
Normal file
618
src/module/powers/basePowers.js
Normal file
@ -0,0 +1,618 @@
|
||||
/* globals warpgate */
|
||||
import { templates } from '../preloadTemplates.js';
|
||||
import { moduleName } from '../globals.js';
|
||||
|
||||
const MAINTAIN_ICON = 'icons/magic/symbols/runes-star-blue.webp';
|
||||
|
||||
export class PowerFormApplication extends FormApplication {
|
||||
constructor(powerEffect) {
|
||||
super();
|
||||
this.powerEffect = powerEffect;
|
||||
}
|
||||
|
||||
static get defaultOptions() {
|
||||
return mergeObject(super.defaultOptions, {
|
||||
classes: ['sheet', 'mbSwadePowerEffectsForm'],
|
||||
popOut: true,
|
||||
template: templates['powerDialog.html'],
|
||||
id: ['mbSwadePowerEffectsApplication'],
|
||||
title: 'Power Effects',
|
||||
width: 350,
|
||||
});
|
||||
}
|
||||
|
||||
static sortMods(a, b) {
|
||||
if (a.isGlobal !== b.isGlobal) {
|
||||
return a.isGlobal ? -1 : 1;
|
||||
}
|
||||
if (a.type !== b.type) {
|
||||
return a.type === 'checkbox' ? -1 : 1;
|
||||
}
|
||||
if ((a.sortOrder ?? 0) !== (b.sortOrder ?? 0)) {
|
||||
return (a.sortOrder ?? 0) < (b.sortOrder ?? 0) ? -1 : 1;
|
||||
}
|
||||
return a.name === b.name ? 0 : a.name < b.name ? -1 : 1;
|
||||
}
|
||||
|
||||
getData() {
|
||||
let modifiers = deepClone(this.powerEffect.modifiers);
|
||||
modifiers.sort(PowerFormApplication.sortMods);
|
||||
for (const modifier of modifiers) {
|
||||
modifier.isCheckbox = modifier.type === 'checkbox';
|
||||
modifier.isSelect = modifier.type === 'select';
|
||||
modifier.isRadio = modifier.type === 'radio';
|
||||
modifier.isNumber = modifier.type === 'number';
|
||||
modifier.isText = modifier.type === 'text';
|
||||
if (modifier.isSelect || modifier.isRadio) {
|
||||
for (const choice in modifier.choices) {
|
||||
let val = '';
|
||||
if (modifier.values[choice] !== 0) {
|
||||
val = ` (${modifier.values[choice] > 0 ? '+' : ''}${modifier.values[choice]})`;
|
||||
}
|
||||
modifier.choices[choice] = `${modifier.choices[choice]}${val}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
const data = {
|
||||
name: this.powerEffect.name,
|
||||
icon: this.powerEffect.icon,
|
||||
basePowerPoints: this.powerEffect.basePowerPoints,
|
||||
modifiers,
|
||||
additionalRecipientCost: 0,
|
||||
targets: [],
|
||||
buttons: this.powerEffect.menuButtons,
|
||||
};
|
||||
if (this.powerEffect.isTargeted) {
|
||||
data.targets = this.powerEffect.targets.map((t) => t.name);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
async _updateObject(ev, formData) {
|
||||
formData.submit = ev?.submitter?.value ?? 'cancel';
|
||||
console.log(ev, formData);
|
||||
}
|
||||
}
|
||||
|
||||
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',
|
||||
type: 'checkbox',
|
||||
default: false,
|
||||
id: 'adaptable',
|
||||
value: 1,
|
||||
epic: false,
|
||||
effect: false,
|
||||
isGlobal: true,
|
||||
});
|
||||
mods.push({
|
||||
name: 'Fatigue',
|
||||
type: 'checkbox',
|
||||
default: false,
|
||||
id: 'fatigue',
|
||||
value: 2,
|
||||
epic: false,
|
||||
effect: false,
|
||||
isGlobal: true,
|
||||
});
|
||||
mods.push({
|
||||
name: 'Glow/Shroud',
|
||||
id: 'glowshroud',
|
||||
type: 'radio',
|
||||
isGlobal: true,
|
||||
default: 'none',
|
||||
choices: { none: 'None', glow: 'Glow', shroud: 'Shroud' },
|
||||
values: { none: 0, glow: 1, shroud: 1 },
|
||||
effects: {
|
||||
none: null,
|
||||
glow: {
|
||||
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,
|
||||
},
|
||||
],
|
||||
},
|
||||
shroud: {
|
||||
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,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
epic: false,
|
||||
effect: true,
|
||||
});
|
||||
if (this.isDamaging) {
|
||||
mods.push({
|
||||
name: 'Lingering Damage',
|
||||
id: 'lingeringdamage',
|
||||
type: 'checkbox',
|
||||
default: false,
|
||||
value: 2,
|
||||
epic: false,
|
||||
effect: false,
|
||||
isGlobal: true,
|
||||
});
|
||||
mods.push({
|
||||
name: 'Heavy Weapon',
|
||||
id: 'heavyweapon',
|
||||
value: 2,
|
||||
epic: false,
|
||||
effect: false,
|
||||
type: 'checkbox',
|
||||
default: false,
|
||||
isGlobal: true,
|
||||
});
|
||||
}
|
||||
mods.push({
|
||||
name: 'Hinder/Hurry',
|
||||
id: 'hinderhurry',
|
||||
type: 'radio',
|
||||
default: false,
|
||||
value: 1,
|
||||
epic: false,
|
||||
effect: true,
|
||||
choices: { none: 'None', hinder: 'Hinder', hurry: 'Hurry' },
|
||||
values: { none: 0, hinder: 1, hurry: 1 },
|
||||
isGlobal: true,
|
||||
effects: {
|
||||
none: null,
|
||||
hinder: {
|
||||
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,
|
||||
},
|
||||
],
|
||||
},
|
||||
hurry: {
|
||||
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.hasAoe) {
|
||||
mods.push({
|
||||
type: 'checkbox',
|
||||
default: false,
|
||||
name: 'Selective',
|
||||
id: 'selective',
|
||||
value: 1,
|
||||
epic: false,
|
||||
effect: false,
|
||||
isGlobal: true,
|
||||
});
|
||||
}
|
||||
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: {},
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
let app = new PowerFormApplication(this).render(true);
|
||||
}
|
||||
|
||||
async applyEffect() {
|
||||
log('Power Effect called');
|
||||
log(this);
|
||||
}
|
||||
|
||||
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 },
|
||||
);
|
||||
}
|
||||
}
|
||||
1452
src/module/powers/powers.js
Normal file
1452
src/module/powers/powers.js
Normal file
@ -0,0 +1,1452 @@
|
||||
/* globals warpgate */
|
||||
import { moduleName } from '../globals.js';
|
||||
import { PowerEffect } from './basePowers.js';
|
||||
import { ArcaneProtectionEffect } from './arcaneProtection.js';
|
||||
import { BanishEffect } from './banish.js';
|
||||
|
||||
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({
|
||||
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({
|
||||
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() {
|
||||
const 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({
|
||||
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() {
|
||||
const 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 efct of effects) {
|
||||
mutation.embedded.ActiveEffect[efct.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.render();
|
||||
return;
|
||||
}
|
||||
ui.notifications.error(`No power effect found for ${name}`);
|
||||
}
|
||||
@ -1,11 +1,15 @@
|
||||
// SPDX-FileCopyrightText: 2022 Johannes Loher
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
const _templatePaths = ['powerDialog.html'];
|
||||
|
||||
export async function preloadTemplates() {
|
||||
const templatePaths = [
|
||||
// Add paths to "modules/swade-mb-helpers/templates"
|
||||
];
|
||||
|
||||
return loadTemplates(templatePaths);
|
||||
return loadTemplates(_templatePaths.map((f) => `modules/swade-mb-helpers/templates/${f}`));
|
||||
}
|
||||
|
||||
function templateMap() {
|
||||
const templates = {};
|
||||
for (const file of _templatePaths) {
|
||||
templates[file] = `modules/swade-mb-helpers/templates/${file}`;
|
||||
}
|
||||
return templates;
|
||||
}
|
||||
|
||||
export const templates = templateMap();
|
||||
|
||||
@ -5,13 +5,13 @@
|
||||
// Import JavaScript modules
|
||||
import { registerSettings } from './settings.js';
|
||||
import { preloadTemplates } from './preloadTemplates.js';
|
||||
import { api } from './api.js'
|
||||
import { initVisionModes } from './visionModes.js'
|
||||
import { requestTokenRoll } from './helpers.js'
|
||||
import { preDamageRollModifiers, preTraitRollModifiers } from './rollHelpers.js'
|
||||
import { shapeChangeOnDismiss } from './powerEffects.js'
|
||||
import { log, moduleHelpers } from './globals.js'
|
||||
import { powerEffectManagementHook } from './powers.js'
|
||||
import { api } from './api.js';
|
||||
import { initVisionModes } from './visionModes.js';
|
||||
import { requestTokenRoll } from './helpers.js';
|
||||
import { preDamageRollModifiers, preTraitRollModifiers } from './rollHelpers.js';
|
||||
import { shapeChangeOnDismiss } from './powerEffects.js';
|
||||
import { log, moduleHelpers } from './globals.js';
|
||||
import { powerEffectManagementHook } from './powers/powers.js';
|
||||
|
||||
// Initialize module
|
||||
Hooks.once('init', async () => {
|
||||
@ -30,36 +30,34 @@ Hooks.once('init', async () => {
|
||||
|
||||
// Setup module
|
||||
Hooks.once('setup', async () => {
|
||||
api.registerFunctions()
|
||||
api.registerFunctions();
|
||||
// Do anything after initialization but before
|
||||
// ready
|
||||
});
|
||||
|
||||
// When ready
|
||||
Hooks.once('ready', async () => {
|
||||
_checkModule('warpgate')
|
||||
_checkModule('socketlib')
|
||||
log('Initialized SWADE MB Helpers')
|
||||
warpgate.event.watch(warpgate.EVENT.DISMISS, shapeChangeOnDismiss)
|
||||
_checkModule('warpgate');
|
||||
_checkModule('socketlib');
|
||||
log('Initialized SWADE MB Helpers');
|
||||
warpgate.event.watch(warpgate.EVENT.DISMISS, shapeChangeOnDismiss);
|
||||
});
|
||||
|
||||
|
||||
function _checkModule (name) {
|
||||
function _checkModule(name) {
|
||||
if (!game.modules.get(name)?.active && game.user.isGM) {
|
||||
let action = 'install and activate'
|
||||
if (game.modules.get(name)) action = 'activate'
|
||||
ui.notifications.error(
|
||||
`SWADE MB Helpers requires the ${name} module. Please ${action} it.`)
|
||||
let action = 'install and activate';
|
||||
if (game.modules.get(name)) action = 'activate';
|
||||
ui.notifications.error(`SWADE MB Helpers requires the ${name} module. Please ${action} it.`);
|
||||
}
|
||||
}
|
||||
|
||||
Hooks.on('swadePreRollAttribute', preTraitRollModifiers)
|
||||
Hooks.on('swadePreRollSkill', preTraitRollModifiers)
|
||||
Hooks.on('swadeRollDamage', preDamageRollModifiers)
|
||||
Hooks.on('deleteActiveEffect', powerEffectManagementHook)
|
||||
Hooks.on('swadePreRollAttribute', preTraitRollModifiers);
|
||||
Hooks.on('swadePreRollSkill', preTraitRollModifiers);
|
||||
Hooks.on('swadeRollDamage', preDamageRollModifiers);
|
||||
Hooks.on('deleteActiveEffect', powerEffectManagementHook);
|
||||
|
||||
Hooks.once('socketlib.ready', () => {
|
||||
const _socket = socketlib.registerModule('swade-mb-helpers')
|
||||
_socket.register('requestTokenRoll', requestTokenRoll)
|
||||
moduleHelpers._socket = _socket
|
||||
})
|
||||
const _socket = socketlib.registerModule('swade-mb-helpers');
|
||||
_socket.register('requestTokenRoll', requestTokenRoll);
|
||||
moduleHelpers._socket = _socket;
|
||||
});
|
||||
|
||||
27
src/templates/powerDialog.html
Normal file
27
src/templates/powerDialog.html
Normal file
@ -0,0 +1,27 @@
|
||||
<form class="flexcol">
|
||||
<header class="sheet-header flexrow">
|
||||
<img src="{{icon}}" height="64" width="64" title="{{name}} Effect" alt=" " />
|
||||
<section class="flexcol">
|
||||
<h2>{{name}} Effect</h2>
|
||||
<p>Apply the affects of {{name}}.</p>
|
||||
</section>
|
||||
</header>
|
||||
<p>
|
||||
{{#if targets.length}} <strong>Targets</strong>: {{#each targets}}{{#if @index}}, {{/if}}{{this}}{{/each}} {{/if}}
|
||||
</p>
|
||||
{{#each modifiers}}
|
||||
<div class="form-group">
|
||||
{{#if isCheckbox}}
|
||||
<input type="checkbox" name="{{id}}" {{checked default}} />
|
||||
<label for="{{id}}">{{#if epic}}⭐ {{/if}}{{name}} ({{numberFormat value decimals=0 sign=true}})</label>
|
||||
{{/if}} {{#if isRadio}}
|
||||
<label>{{#if epic}}⭐ {{/if}}{{name}}:</label>
|
||||
{{radioBoxes id choices checked=default}} {{/if}}
|
||||
</div>
|
||||
{{/each}}
|
||||
<footer class="sheet-footer flexrow">
|
||||
{{#each buttons}}
|
||||
<button type="submit" name="submit" value="{{value}}">{{label}}</button>
|
||||
{{/each}}
|
||||
</footer>
|
||||
</form>
|
||||
Loading…
x
Reference in New Issue
Block a user