Compare commits

..

No commits in common. "main" and "fix-mirror-self" have entirely different histories.

252 changed files with 10982 additions and 24096 deletions

View File

@ -1,9 +0,0 @@
root = true
[*]
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2
charset = utf-8
trim_trailing_whitespace = true

15
.eslintrc.js Normal file
View File

@ -0,0 +1,15 @@
module.exports = {
env: {
browser: true,
es2021: true
},
extends: 'standard',
overrides: [
],
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module'
},
rules: {
}
}

6
.gitattributes vendored
View File

@ -1,8 +1,4 @@
packs/**/*.ldb binary packs/** binary
packs/**/MANIFEST-* binary
packs/**/CURRENT binary
packs/**/LOCK binary
packs/**/LOG* binary
*.webp filter=lfs diff=lfs merge=lfs -text *.webp filter=lfs diff=lfs merge=lfs -text
*.jpeg filter=lfs diff=lfs merge=lfs -text *.jpeg filter=lfs diff=lfs merge=lfs -text
*.jpg filter=lfs diff=lfs merge=lfs -text *.jpg filter=lfs diff=lfs merge=lfs -text

146
.gitignore vendored
View File

@ -1,32 +1,132 @@
# SPDX-FileCopyrightText: 2022 Johannes Loher # ---> Node
# # Logs
# SPDX-License-Identifier: MIT logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# IDE # Diagnostic reports (https://nodejs.org/api/report.html)
.idea/ report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
.vs/
# Node Modules # Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/ node_modules/
npm-debug.log jspm_packages/
# yarn2 # Snowpack dependency directory (https://snowpack.dev/)
.yarn/* web_modules/
!.yarn/patches
!.yarn/releases
!.yarn/plugins
!.yarn/sdks
!.yarn/versions
.pnp.*
# Local configuration # TypeScript cache
foundryconfig.json *.tsbuildinfo
# Distribution files # Optional npm cache directory
dist .npm
# ESLint # Optional eslint cache
.eslintcache .eslintcache
# Junit results # Optional stylelint cache
junit.xml .stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
swade-mb-helpers.lock

View File

@ -1,5 +0,0 @@
{
"flags": {
"gulpfile": "gulpfile.mjs"
}
}

1
.nvmrc
View File

@ -1 +0,0 @@
lts/*

View File

@ -1,8 +0,0 @@
# SPDX-FileCopyrightText: 2022 Johannes Loher
#
# SPDX-License-Identifier: MIT
dist
package-lock.json
.pnp.cjs
.pnp.js

View File

@ -1,11 +0,0 @@
// SPDX-FileCopyrightText: 2022 Johannes Loher
//
// SPDX-License-Identifier: MIT
module.exports = {
semi: true,
trailingComma: 'all',
singleQuote: true,
printWidth: 120,
tabWidth: 2,
};

View File

@ -5,270 +5,9 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [4.1.0] ## [Unreleased]
### Added ## 2.0.0
- added suppression of system gang up calculation
- added correction for gang up for formation fighter
- added swat correction for scale penalty
### Changed
- updated roll modifiers for SWADE version 5.1.0
- updated pack tactics gang up to use same calculation as system
### Removed
- removed roll modifiers handled by the system:
- range check
- vulnerable target
- dodge check
## [4.0.0]
### Added
- Added example morphables and summonables actor compendia.
### Changed
- Updated for Foundry v13
- Updated for SWADE 5.0
- Updated powerEffect application and other dialogs to ApplicationV2
- BREAKING CHANGE: removed Portal dependency for summons
- BREAKING CHANGE: added Sequencer dependency for summons
### Fixed
- Fixed Darksight effect name.
- Summon Ally: Mirror Self improvements - remove changesets and grants from
copied items.
## [3.1.5] 2025-01-27
### Changed
- Added flying pace support to Fly power
- Added burrowing pace support to Burrow power
- Added additional icon choices to the list for maintain effects
## [3.1.4] 2025-01-26
### Fixed
- Fixed module loding with swade core module and swpf due to fear macro changes
- Fixed power effect creation for SWADE system 4.2
### Changed
- Clean up of requested roll GM chat card
- Make Deflection roll helper modifiers more descriptive (melee, ranged, all)
## [3.1.3] 2025-01-11
### Fixed
- Updated hurry/hinder and sloth/speed power effects to work with
`system.pace` for SWADE 4.2 compatibility.
### Changed
- updated settings for Blindsense to a more pleasing look.
## [3.1.2] 2024-08-11
### Fixed
- #44: protection effect and other default system effects still had duration
when added by the power effect, instead of maintaining their own effect.
- Since VAE did away with the collapsable extra description, moved the spell
description to the main 'description' field of active effects.
## [3.1.1] 2024-07-26
- Added: 'Shape Change Ability' power effect
## [3.1.0] 2024-07-14
- v12 compatibility
- bugfixes for v12
## [3.0.2] 2024-06-24
### Fixed
- Fixed bug in which effects would disappear on the target's next turn
### Added
- Eidolon summon effect and helper action
- Companion "summon" effect and helper action
## [3.0.1] 2024-06-18
### Added
- Added Inquisitor's Judgement power effect
## [3.0.0] 2024-06-12
### Added
- Optional Visual Active Effect integration for power descriptions
- Add VAE helper buttons for breaking free from Bound/Entangled
- Added Portal dependency
### Changed
- Refactor and redo of powers handling
- maintained powers
- powerpoint calculation
- more powers
- Refactor of distribution
- Code is minified into one file for faster loading
### Removed
- Removed warpgate dependency
## [2.4.3] 2024-04-21
### Added
- Added token varient art suggested effect mappings
### Changed
- Added elevation component to distance calculations for gang up
- Added check for incapacitated or defeated tokens to gang up check
- Minor cosmetic change to 'Draw from Action Deck' macro
## [2.4.2] 2024-02-25
### Added
- Added Macro Manager macro for all Power Effects
- Added Macro Manager macro for all helper macros
- Added Draw from Action Deck macro
- Added Shuffle Action Deck macro
## [2.4.1] 2024-02-13
### Added
- Added support for SWPF Smite's Spiritual Weapon (from the APG)
## [2.4.0] 2024-02-11
### Added
- Added Monk's Active Tile Trigger versions of the request roll macros.
## [2.3.5] 2024-01-30
### Changed
- Changed visual parameters for blindsense
## [2.3.4] 2024-01-29
### Added
- Added additional vision mode: blindsense
### Changed
- Changed setTokenVison module to detect blindsense
## [2.3.3] 2024-01-28
### Added
- Added mutagen action
## [2.3.2] 2024-01-23
### Added
- Grabbed poison macros from SWADE for use in SWPF until I can write something
else or they appear in SWPF.
### Changed
- Added Dodge as a detected edge on a target for roll modifiers
## [2.3.1] 2023-12-26
### Added
- Data file for the Torch module
- Import file for Token Variant Art's global effect mappings
- Macro: Request fear check specialization macro
- Macro: Fear Table to call the new fearTable api endpoint
- API: rulesVersion property
- API: fearTable(actor) calls the relevant premium core rules module's fear
table
- API: added requestFearRollFromTokens special helper
- Trait roll hooks for:
- Glow/Shroud
- Range modifiers
### Changed
- added a summary chat message for the roll results to requested rolls.
- added a target number option to requested rolls.
## [2.3.0] 2023-12-19
### Added
- Trait and Damage Roll hooks to look for and apply modifiers for target
conditions:
- Vulnerable
- Deflection
- Arcane Protection
- Arcane Resistance
- Scale
- Gang Up
- Resistences and Weaknesses
- New Macro: Set token vision
- New Common Action: Illumination (for the darkness penalty effects)
- New macro: Quick Damage Roll
- New Vision mode: Low Light Vision
- Power Effect for Zombie
- Sample fixed request roll macro
### Changed
- Vision mode visual effects changed for Basic Vision and Darkvision
- Shape Change and Summon both set the disposition for their new tokens
- Shape Change and Summon both set vision to enabled for their new tokens
## [2.2.0]
### Added
- Power Effect for Havoc
- Power Effect Macro for Havoc
- Power Effect Action for Havoc
- New Macro: Request Roll
- NEW DEPENDENCY: socketlib
- Documentation:
- API Documentation
- Request Roll macro documentation
## [2.1.0]
### Changed
- Changed the Summon Ally power effect macro to handle Mirror Self a little
cleaner
- Changed the power effect macro to consider swids in addition to the item
name.
- Updates to documentation
## [2.0.0]
### Changed ### Changed
@ -285,7 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Shape Change - Shape Change
- Sloth/Speed - Sloth/Speed
## [1.2.0] ## 1.2.0
### Changed ### Changed
@ -294,20 +33,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- incorporated glow, shroud, hurry, and hinder power modifiers into power - incorporated glow, shroud, hurry, and hinder power modifiers into power
effects effects
## [1.1.0] ## 1.1.0
### Added ### Added
- gold calculator macro for SWPF gold items - gold calculator macro for SWPF gold items
- Actions for common rolls with links to SWPF rules - Actions for common rolls with links to SWPF rules
## [1.0.1] ## 1.0.1
### Fixed ### Fixed
- Summon macro now spawns tokens with prototype token's actual dimensions - Summon macro now spawns tokens with prototype token's actual dimensions
## [1.0.0] ## 1.0.0
### Added ### Added
@ -322,7 +61,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Protection and Smite macros now linked to swade system effects - Protection and Smite macros now linked to swade system effects
## [0.9.0] ## 0.9.0
- Initial 'public' release - Initial 'public' release
@ -330,20 +69,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- DEPENDENCY: warpgate - DEPENDENCY: warpgate
- macro helpers for the following effects: - macro helpers for the following effects:
- Boost/Lower Trait - Boost/Lower Trait
- Deflection - Deflection
- Glow - Glow
- Hinder - Hinder
- Hurry - Hurry
- Lingering Damage - Lingering Damage
- Protection - Protection
- Shroud - Shroud
- Smite - Smite
- Summon - Summon
- API helpers: - API helpers:
- `createEffectDocument` - `createEffectDocument`
- `createMutationWithEffect` - `createMutationWithEffect`
- `defaultMutationOptions` - `defaultMutationOptions`
- `getActorFolderByPath` - `getActorFolderByPath`
- `getActorsInFolder` - `getActorsInFolder`
- `runOnTargetOrSelectedTokens` - `runOnTargetOrSelectedTokens`

View File

@ -3,76 +3,3 @@
This is a series of helper macros and other helpers to help run SWADE and This is a series of helper macros and other helpers to help run SWADE and
Savage Pathfinder games with some minimal help remembering token state, without Savage Pathfinder games with some minimal help remembering token state, without
going overboard on the automation. going overboard on the automation.
## Development
### Prerequisites
In order to build this module, recent versions of `node` and `npm` are
required. Most likely, using `yarn` also works, but only `npm` is officially
supported. We recommend using the latest lts version of `node`. If you use `nvm`
to manage your `node` versions, you can simply run
```
nvm install
```
in the project's root directory.
You also need to install the project's dependencies. To do so, run
```
npm install
```
### Building
You can build the project by running
```
npm run build
```
Alternatively, you can run
```
npm run build:watch
```
to watch for changes and automatically build as necessary.
### Linking the built project to Foundry VTT
In order to provide a fluent development experience, it is recommended to link
the built module to your local Foundry VTT installation's data folder. In
order to do so, first add a file called `foundryconfig.json` to the project root
with the following content:
```
{
"dataPath": ["/absolute/path/to/your/FoundryVTT"]
}
```
(if you are using Windows, make sure to use `\` as a path separator instead of
`/`)
Then run
```
npm run link-project
```
On Windows, creating symlinks requires administrator privileges, so
unfortunately you need to run the above command in an administrator terminal for
it to work.
You can also link to multiple data folders by specifying multiple paths in the
`dataPath` array.
## Licensing
This project is being developed under the terms of the
[LIMITED LICENSE AGREEMENT FOR MODULE DEVELOPMENT] for Foundry Virtual Tabletop.
[LIMITED LICENSE AGREEMENT FOR MODULE DEVELOPMENT]: https://foundryvtt.com/article/license/

View File

@ -1,23 +0,0 @@
import globals from "globals"
import js from "@eslint/js";
import typhonJs from "@typhonjs-fvtt/eslint-config-foundry.js"
export default [
js.configs.recommended,
{
languageOptions: {
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
},
globals: {
...globals.browser,
...typhonJs.globals,
}
},
ignores: ['dist/*', './macros/*'],
rules: {
'no-unused-vars': 'warn',
'no-undef': 'error',
},
}
];

View File

@ -1,12 +0,0 @@
#!/bin/sh
curdir=$(realpath $(dirname $0))
package=$(basename ${curdir})
fvtt package workon ${package}
for p in ${curdir}/dist/swade-mb-helpers/packs/*; do
package=$(basename $p)
rm $p/*
fvtt package pack -n ${package} --inputDirectory ${curdir}/src/packsrc/${package} --clean
done
rsync -rpa ${curdir}/dist/swade-mb-helpers/packs/ ${curdir}/src/packs --delete

View File

@ -1,11 +0,0 @@
#!/bin/sh
curdir=$(realpath $(dirname $0))
package=$(basename ${curdir})
fvtt package workon ${package}
for p in ${curdir}/dist/swade-mb-helpers/packs/*; do
package=$(basename $p)
mkdir -p ${curdir}/src/packsrc/${package}
fvtt package unpack -n ${package} --outputDirectory ${curdir}/src/packsrc/${package}
done

View File

@ -1,183 +0,0 @@
import fs from 'fs-extra';
import gulp from 'gulp';
import sass from 'gulp-dart-sass';
import sourcemaps from 'gulp-sourcemaps';
import path from 'node:path';
import buffer from 'vinyl-buffer';
import source from 'vinyl-source-stream';
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
import rollupStream from '@rollup/stream';
import rollupConfig from './rollup.config.mjs';
/********************/
/* CONFIGURATION */
/********************/
const packageId = 'swade-mb-helpers';
const sourceDirectory = './src';
const distDirectory = './dist/swade-mb-helpers';
const stylesDirectory = `${sourceDirectory}/styles`;
const stylesExtension = 'scss';
const sourceFileExtension = 'js';
const staticFiles = ['assets', 'config', 'fonts', 'lang', 'templates'];
const runtimeStaticFiles = ['packs', 'module.json'];
/********************/
/* BUILD */
/********************/
let cache;
/**
* Build the distributable JavaScript code
*/
function buildCode() {
return rollupStream({ ...rollupConfig(), cache })
.on('bundle', (bundle) => {
cache = bundle;
})
.pipe(source(`${packageId}.js`))
.pipe(buffer())
.pipe(sourcemaps.init({ loadMaps: true }))
.pipe(sourcemaps.write('.'))
.pipe(gulp.dest(`${distDirectory}/module`));
}
/**
* Build style sheets
*/
function buildStyles() {
return gulp
.src(`${stylesDirectory}/${packageId}.${stylesExtension}`)
.pipe(sass().on('error', sass.logError))
.pipe(gulp.dest(`${distDirectory}/styles`));
}
/**
* Copy static files
*/
async function copyFiles() {
for (const file of staticFiles) {
if (fs.existsSync(`${sourceDirectory}/${file}`)) {
await fs.copy(`${sourceDirectory}/${file}`, `${distDirectory}/${file}`);
}
}
}
/**
* Copy runtime files
*/
async function copyRuntimeFiles() {
for (const file of runtimeStaticFiles) {
if (fs.existsSync(`${sourceDirectory}/${file}`)) {
await fs.copy(`${sourceDirectory}/${file}`, `${distDirectory}/${file}`);
}
}
}
/**
* Watch for changes for each build step
*/
export function watch() {
gulp.watch(`${sourceDirectory}/**/*.${sourceFileExtension}`, { ignoreInitial: false }, buildCode);
gulp.watch(`${stylesDirectory}/**/*.${stylesExtension}`, { ignoreInitial: false }, buildStyles);
gulp.watch(
staticFiles.map((file) => `${sourceDirectory}/${file}`),
{ ignoreInitial: false },
copyFiles,
);
}
export const build = gulp.series(clean, gulp.parallel(buildCode, buildStyles, copyRuntimeFiles, copyFiles));
/********************/
/* CLEAN */
/********************/
/**
* Remove built files from `dist` folder while ignoring source files
*/
export async function clean() {
const files = [...runtimeStaticFiles, 'module'];
if (fs.existsSync(`${stylesDirectory}/${packageId}.${stylesExtension}`)) {
files.push('styles');
}
console.log(' ', 'Files to clean:');
console.log(' ', files.join('\n '));
for (const filePath of files) {
await fs.remove(`${distDirectory}/${filePath}`);
}
}
/********************/
/* LINK */
/********************/
/**
* Get the data paths of Foundry VTT based on what is configured in `foundryconfig.json`
*/
function getDataPaths() {
const config = fs.readJSONSync('foundryconfig.json');
const dataPath = config?.dataPath;
if (dataPath) {
const dataPaths = Array.isArray(dataPath) ? dataPath : [dataPath];
return dataPaths.map((dataPath) => {
if (typeof dataPath !== 'string') {
throw new Error(
`Property dataPath in foundryconfig.json is expected to be a string or an array of strings, but found ${dataPath}`,
);
}
if (!fs.existsSync(path.resolve(dataPath))) {
throw new Error(`The dataPath ${dataPath} does not exist on the file system`);
}
return path.resolve(dataPath);
});
} else {
throw new Error('No dataPath defined in foundryconfig.json');
}
}
/**
* Link build to User Data folder
*/
export async function link() {
let destinationDirectory;
if (fs.existsSync(path.resolve(sourceDirectory, 'module.json'))) {
destinationDirectory = 'modules';
} else {
throw new Error('Could not find module.json');
}
const linkDirectories = getDataPaths().map((dataPath) =>
path.resolve(dataPath, 'Data', destinationDirectory, packageId),
);
const argv = yargs(hideBin(process.argv)).option('clean', {
alias: 'c',
type: 'boolean',
default: false,
}).argv;
const clean = argv.c;
for (const linkDirectory of linkDirectories) {
if (clean) {
console.log(`Removing build in ${linkDirectory}.`);
await fs.remove(linkDirectory);
} else if (!fs.existsSync(linkDirectory)) {
console.log(`Linking dist to ${linkDirectory}.`);
await fs.ensureDir(path.resolve(linkDirectory, '..'));
await fs.symlink(path.resolve(distDirectory), linkDirectory);
} else {
console.log(`Skipped linking to ${linkDirectory}, as it already exists.`);
}
}
}

View File

@ -1,28 +0,0 @@
const tokens = canvas.tokens.controlled;
const actors = tokens.map((t) => t.actor);
const EFFECT_OBJECT = {
name: 'Effect',
type: 'macro',
dice: null,
resourcesUsed: null,
modifier: '',
override: '',
uuid: 'Compendium.swade-mb-helpers.helper-macros.Macro.AjuA11hQ48UJNwlH',
macroActor: 'self',
isHeavyWeapon: false,
};
for (const actor of actors) {
const updates = [];
for (const power of actor.items.filter((i) => i.type === 'power')) {
if (Object.values(power.system.actions.additional).find((action) => action.name === 'Effect')) {
continue;
}
const _id = power.id;
const additional = foundry.utils.deepClone(power.system.actions.additional);
additional[foundry.utils.randomID(8)] = foundry.utils.deepClone(EFFECT_OBJECT);
updates.push({ _id, 'system.actions.additional': additional });
}
if (updates.length > 0) {
actor.updateEmbeddedDocuments('Item', updates);
}
}

45
macros/blind.js Normal file
View File

@ -0,0 +1,45 @@
swadeMBHelpers.runOnTargetOrSelectedTokens(main)
async function main (tokens) {
const tokenList = tokens.map(t => t.name).join(', ')
const menuOptions = {
title: 'Blind',
defaultButton: 'Cancel',
options: {}
}
const menuData = {
inputs: [
{ type: 'header', label: 'Blind' },
{ type: 'info', label: `Apply Blind to ${tokenList}` },
{ type: 'checkbox', label: 'Strong', options: false }
],
buttons: [
{ label: 'Apply', value: 'apply' },
{ label: 'Raise', value: 'raise' },
{ label: 'Cancel', value: 'cancel' }
]
}
const { buttons, inputs } = await warpgate.menu(menuData, menuOptions)
if (buttons && buttons !== 'cancel') {
const options = {
raise: (buttons === 'raise'),
strong: (!!inputs[2])
}
await createEffect(tokens, options)
}
}
async function createEffect (tokens, options) {
const effectDetails = (options.raise ? '-4' : '-2')
const effectEnd = (options.strong ? 'Vigor -2' : 'Vigor')
const effectName = `Blind (${effectDetails}) ${effectEnd} ends`
const baseEffect = CONFIG.statusEffects.find(se => se.label === 'EFFECT.StatusBlind')
for (const token of tokens) {
const mutate = swadeMBHelpers.createMutationWithEffect(baseEffect.icon, effectName, 1, [])
mutate.embedded.ActiveEffect[effectName].flags.core = { statusId: baseEffect.id }
const mutateOptions = swadeMBHelpers.defaultMutationOptions('Blind')
await warpgate.mutate(token.document, mutate, {}, mutateOptions)
}
}

133
macros/boost-lower-trait.js Normal file
View File

@ -0,0 +1,133 @@
swadeMBHelpers.runOnTargetOrSelectedTokens(main)
async function main (tokens) {
let traitOptions = [
'Agility', 'Smarts', 'Spirit', 'Strength', 'Vigor'
]
const allSkills = []
const traits = {}
for (const traitName of traitOptions) {
const lower = traitName.toLowerCase()
traits[traitName] = {
name: traitName,
type: 'attribute',
modkey: `system.attributes.${lower}.die.modifier`,
diekey: `system.attributes.${lower}.die.sides`
}
}
const tokenList = tokens.map(t => t.name).join(', ')
for (const token of tokens) {
const skills = token.actor.items.filter(item => item.type === 'skill')
for (const skill of skills) {
const name = skill.name
traits[name] = {
type: 'skill',
name,
modkey: `@Skill{${name}}[system.die.modifier]`,
diekey: `@Skill{${name}}[system.die.sides]`
}
if (name !== 'Unskilled' && !allSkills.find(v => v === name)) {
allSkills.push(name)
}
}
}
traitOptions = traitOptions.concat(allSkills.sort())
const menuOptions = {
title: 'Boost/Lower Trait',
defaultButton: 'Cancel',
options: {}
}
const menuData = {
inputs: [
{ type: 'header', label: 'Boost/Lower Trait' },
{ type: 'info', label: `Affected Tokens ${tokenList}` },
{ type: 'select', label: 'Trait', options: traitOptions },
{ type: 'info', label: 'Boost or Lower?' },
{ type: 'radio', label: 'Boost', options: ['isBoost', true] },
{ type: 'radio', label: 'Lower', options: ['isBoost', false] },
{ type: 'checkbox', label: 'Greater', options: false },
{ type: 'checkbox', label: 'Strong (lower only)', options: false }
],
buttons: [
{ label: 'Apply', value: 'apply' },
{ label: 'Apply with raise', value: 'raise' },
{ label: 'Cancel', value: 'cancel' }
]
}
const { buttons, inputs } = await warpgate.menu(menuData, menuOptions)
if (buttons && buttons !== 'cancel') {
const options = {
action: buttons,
name: inputs[2],
trait: traits[inputs[2]],
direction: inputs[4] || inputs[5],
greater: inputs[6],
strong: inputs[7]
}
createEffect(tokens, options)
}
}
const UPICON = 'icons/magic/life/cross-embers-glow-yellow-purple.webp'
const DOWNICON = 'icons/magic/movement/chevrons-down-yellow.webp'
async function createEffect (tokens, options) {
const raise = (options.action === 'raise')
const icon = (options.direction === 'Boost' ? UPICON : DOWNICON)
let namePart = `${options.direction} ${options.trait.name}`
const mode = CONST.ACTIVE_EFFECT_MODES.ADD
const mods = []
if (options.strong && options.direction === 'Lower') {
mods.push('strong')
}
if (options.greater) {
mods.push('greater')
}
if (mods.length > 0) {
namePart = `${namePart} (${mods.join(', ')})`
}
const minorEffect = swadeMBHelpers.createEffectDocument(
icon, `minor ${namePart}`, 5, [
{
key: options.trait.diekey,
mode,
value: (options.direction === 'Boost' ? '+2' : '-2'),
priority: 0
}
]
)
const majorEffect = swadeMBHelpers.createEffectDocument(
icon, `major ${namePart}`, 5, [
{
key: options.trait.diekey,
mode,
value: (options.direction === 'Boost' ? '+2' : '-2'),
priority: 0
}
]
)
if (options.direction === 'Lower' && options.greater === 'Greater') {
minorEffect.changes.push({
key: options.trait.modkey,
mode,
value: '-2',
priority: 0
})
}
const mutate = {
embedded: {
ActiveEffect: {
}
}
}
mutate.embedded.ActiveEffect[minorEffect.id] = minorEffect
if (raise) {
mutate.embedded.ActiveEffect[majorEffect.id] = majorEffect
}
const mutateOptions = swadeMBHelpers.defaultMutationOptions(namePart)
for (const token of tokens) {
await warpgate.mutate(token.document, mutate, {}, mutateOptions)
}
}

62
macros/confusion.js Normal file
View File

@ -0,0 +1,62 @@
swadeMBHelpers.runOnTargetOrSelectedTokens(main)
async function main (tokens) {
const tokenList = tokens.map(t => t.name).join(', ')
const menuOptions = {
title: 'Confusion',
defaultButton: 'Cancel',
options: {}
}
const menuData = {
inputs: [
{ type: 'header', label: 'Confusion' },
{ type: 'info', label: `Apply Confusion to ${tokenList}` },
{ type: 'checkbox', label: 'Greater (adds shaken)', options: false }
],
buttons: [
{ label: 'Distracted', value: 'distracted' },
{ label: 'Vulnerable', value: 'vulnerable' },
{ label: 'Raise (both)', value: 'raise' },
{ label: 'Cancel', value: 'cancel' }
]
}
const { buttons, inputs } = await warpgate.menu(menuData, menuOptions)
const greater = (inputs[2] === 'Greater (adds shaken)')
if (buttons && buttons !== 'cancel') {
await createEffect(tokens, buttons, greater)
}
}
function getStatus (label, name) {
const effect = JSON.parse(JSON.stringify(
CONFIG.statusEffects.find(se => se.label === label)))
effect.label = name
effect.flags.core = { statusId: effect.id }
effect.id = name
return effect
}
async function createEffect (tokens, choice, greater) {
const effects = []
if (choice === 'distracted' || choice === 'raise') {
effects.push(getStatus('SWADE.Distr', 'Distracted'))
}
if (choice === 'vulnerable' || choice === 'raise') {
effects.push(getStatus('SWADE.Vuln', 'Vulnerable'))
}
if (greater) {
effects.push(getStatus('SWADE.Shaken', 'Shaken'))
}
for (const token of tokens) {
const mutateOptions = swadeMBHelpers.defaultMutationOptions('Confusion')
const mutate = {
embedded: { ActiveEffect: {} }
}
for (const effect of effects) {
mutate.embedded.ActiveEffect[effect.id] = effect
}
await warpgate.mutate(token.document, mutate, {}, mutateOptions)
}
}

37
macros/deflection.js Normal file
View File

@ -0,0 +1,37 @@
swadeMBHelpers.runOnTargetOrSelectedTokens(main)
async function main (tokens) {
const tokenList = tokens.map(t => t.name).join(', ')
const dialogOptions = {
title: 'Deflection',
content: `Apply <em>Deflection</em> to ${tokenList}`,
default: 'cancel',
buttons: [
{ label: 'Apply (melee)', value: 'melee' },
{ label: 'Apply (ranged)', value: 'ranged' },
{ label: 'Apply with raise (both)', value: 'raise' },
{ label: 'Cancel', value: 'cancel' }
]
}
const choice = await warpgate.buttonDialog(dialogOptions)
if (choice && choice !== 'cancel') {
await createEffect(tokens, choice)
}
}
async function createEffect (tokens, choice) {
const icon = 'icons/magic/defensive/shield-barrier-deflect-teal.webp'
let effectName = 'Deflection'
if (choice === 'raise') {
effectName = `${effectName} (all)`
} else {
effectName = `${effectName} (${choice})`
}
for (const token of tokens) {
const mutate = swadeMBHelpers.createMutationWithEffect(icon, effectName, 5, [])
const mutateOptions = swadeMBHelpers.defaultMutationOptions(effectName)
await warpgate.mutate(token.document, mutate, {}, mutateOptions)
}
}

74
macros/entangle.js Normal file
View File

@ -0,0 +1,74 @@
swadeMBHelpers.runOnTargetOrSelectedTokens(main)
async function main (tokens) {
const tokenList = tokens.map(t => t.name).join(', ')
const menuOptions = {
title: 'Entangle',
defaultButton: 'Cancel',
options: {}
}
const menuData = {
inputs: [
{ type: 'header', label: 'Entangle' },
{ type: 'info', label: `Apply Entangle to ${tokenList}` },
{ type: 'radio', label: 'Not Damaging', options: ['dmg', true] },
{ type: 'radio', label: 'Damaging', options: ['dmg', false] },
{ type: 'radio', label: 'Deadly', options: ['dmg', false] },
{ type: 'checkbox', label: 'Tough', options: false }
],
buttons: [
{ label: 'Entangled', value: 'apply' },
{ label: 'Bound (raise)', value: 'raise' },
{ label: 'Cancel', value: 'cancel' }
]
}
const { buttons, inputs } = await warpgate.menu(menuData, menuOptions)
if (buttons && buttons !== 'cancel') {
const options = {
apply: (buttons === 'raise' ? 'bound' : 'entangled'),
damage: (inputs[3] ? '2d4' : (inputs[4] ? '2d6' : null)),
tough: (!!inputs[5])
}
await createEffect(tokens, options)
}
}
function getStatus (label, name) {
const effect = JSON.parse(JSON.stringify(
CONFIG.statusEffects.find(se => se.label === label)))
effect.label = name
if (!effect.flags) {
effect.flags = {}
}
effect.flags.core = { statusId: effect.id }
effect.id = name
return effect
}
async function createEffect (tokens, options) {
const effectSearch = (options.apply === 'bound' ? 'SWADE.Bound' : 'SWADE.Entangled')
const effectName = (options.apply === 'bound' ? 'Bound' : 'Entangled')
const effect = getStatus(effectSearch, effectName)
const extraIcon = 'icons/magic/nature/root-vine-barrier-wall-brown.webp'
const extraEffect = swadeMBHelpers.createEffectDocument(extraIcon, 'Entangle Modifier', 1, [])
if (options.damage) {
extraEffect.id = `${extraEffect.id} - ${options.damage} dmg`
extraEffect.label = `${extraEffect.label} - ${options.damage} dmg`
}
if (options.tough) {
extraEffect.id = `Tough ${extraEffect.id}`
extraEffect.label = `Tough ${extraEffect.label}`
}
for (const token of tokens) {
const mutateOptions = swadeMBHelpers.defaultMutationOptions('Entangle')
const mutate = { embedded: { ActiveEffect: {} } }
mutate.embedded.ActiveEffect[effect.id] = effect
if (options.damage || options.tough) {
mutate.embedded.ActiveEffect[extraEffect.id] = extraEffect
}
await warpgate.mutate(token.document, mutate, {}, mutateOptions)
}
}

58
macros/glow.js Normal file
View File

@ -0,0 +1,58 @@
swadeMBHelpers.runOnTargetOrSelectedTokens(main)
async function main (tokens) {
const tokenList = tokens.map(t => t.name).join(', ')
const dialogOptions = {
title: 'Glow',
content: `Apply <em>Glow</em> to ${tokenList}`,
default: 'cancel',
buttons: [
{ label: 'OK', value: 'ok' },
{ label: 'Mutate token lighting', value: 'mutate' },
{ label: 'Cancel', value: 'cancel' }
]
}
const choice = await warpgate.buttonDialog(dialogOptions)
if (choice === 'ok' || choice === 'mutate') {
await createEffect(tokens, choice)
}
}
async function createEffect (tokens, choice) {
const icon = 'icons/magic/light/explosion-star-blue-large.webp'
const effectName = 'Glow'
for (const token of tokens) {
const mutate = swadeMBHelpers.createMutationWithEffect(icon, effectName, 5, [])
const mutateOptions = swadeMBHelpers.defaultMutationOptions(effectName)
await warpgate.mutate(token.document, mutate, {}, mutateOptions)
if (choice === 'mutate') {
const mutate2 = {
token: {
light: {
alpha: 0.5,
angle: 360,
attenuation: 0.5,
animation: {
intensity: 5,
reverse: false,
speed: 5,
type: 'starlight'
},
bright: 0,
color: '#0f3fff',
coloration: 1,
contrast: 0,
dim: 0.5,
luminosity: 0.5,
saturation: 0,
shadows: 0
}
}
}
mutateOptions.permanent = false
await warpgate.mutate(token.document, mutate2, {}, mutateOptions)
}
}
}

View File

@ -1,33 +1,33 @@
let tokens = []; let tokens = []
if (canvas.tokens.controlled.length > 0) { if (canvas.tokens.controlled.length > 0) {
tokens = canvas.tokens.controlled; tokens = canvas.tokens.controlled
} }
if (tokens.length > 0) { if (tokens.length > 0) {
main(tokens); main(tokens)
} else { } else {
ui.notifications.error('Please select or target a token'); ui.notifications.error('Please select or target a token')
} }
async function main(tokens) { async function main (tokens) {
const currencies = ['Copper', 'Silver', 'Gold', 'Platinum']; const currencies = ['Copper', 'Silver', 'Gold', 'Platinum']
let template = '<div><table><thead><tr><th>Actor</th><th>Currency</th></tr></thead><tbody>'; let template = '<div><table><thead><tr><th>Actor</th><th>Currency</th></tr></thead><tbody>'
const fmtOptions = { const fmtOptions = {
minimumIntegerDigits: 1, minimumIntegerDigits: 1,
minimumFractionDigits: 2, minimumFractionDigits: 2,
maximumFractionDigits: 2, maximumFractionDigits: 2
};
const fmt = Intl.NumberFormat('en-US', fmtOptions);
for (const token of tokens) {
const actor = token.actor;
let total = 0;
for (const item of actor.items.filter((i) => currencies.indexOf(i.name) > -1)) {
total += item.system.price * item.system.quantity;
}
template += `<tr><td>${actor.name}</td><td>${fmt.format(total)}</td></tr>`;
} }
template += '</thead></tbody>'; const fmt = Intl.NumberFormat('en-US', fmtOptions)
foundry.applications.api.DialogV2.prompt({ for (const token of tokens) {
window: { title: 'Currency Totals' }, const actor = token.actor
content: template, let total = 0
}); for (const item of actor.items.filter(i => currencies.indexOf(i.name) > -1)) {
total += item.system.price * item.system.quantity
}
template += `<tr><td>${actor.name}</td><td>${fmt.format::(total)}</td></tr>`
}
template += '</thead></tbody>'
Dialog.prompt({
title: 'Currency Totals',
content: template
})
} }

38
macros/hinder.js Normal file
View File

@ -0,0 +1,38 @@
swadeMBHelpers.runOnTargetOrSelectedTokens(main)
async function main (tokens) {
const tokenList = tokens.map(t => t.name).join(', ')
const dialogOptions = {
title: 'Hinder',
content: `Apply <em>Hinder</em> to ${tokenList}`,
default: 'cancel',
buttons: [
{ label: 'OK', value: 'ok' },
{ label: 'Cancel', value: 'cancel' }
]
}
const choice = await warpgate.buttonDialog(dialogOptions, 'column')
if (choice === 'ok') {
await createEffect(tokens, choice)
}
}
async function createEffect (tokens, choice) {
const icon = 'icons/magic/movement/abstract-ribbons-red-orange.webp'
const effectName = 'Hinder'
const changes = [
{
key: 'system.stats.speed.value',
mode: foundry.CONST.ACTIVE_EFFECT_MODES.ADD,
value: -2,
priority: 0
}
]
for (const token of tokens) {
const mutate = swadeMBHelpers.createMutationWithEffect(icon, effectName, 5, changes)
const mutateOptions = swadeMBHelpers.defaultMutationOptions(effectName)
await warpgate.mutate(token.document, mutate, {}, mutateOptions)
}
}

38
macros/hurry.js Normal file
View File

@ -0,0 +1,38 @@
swadeMBHelpers.runOnTargetOrSelectedTokens(main)
async function main (tokens) {
const tokenList = tokens.map(t => t.name).join(', ')
const dialogOptions = {
title: 'Hurry',
content: `Apply <em>Hurry</em> to ${tokenList}`,
default: 'cancel',
buttons: [
{ label: 'OK', value: 'ok' },
{ label: 'Cancel', value: 'cancel' }
]
}
const choice = await warpgate.buttonDialog(dialogOptions, 'column')
if (choice === 'ok') {
await createEffect(tokens, choice)
}
}
async function createEffect (tokens, choice) {
const icon = 'icons/skills/movement/feet-winged-boots-blue.webp'
const effectName = 'Hurry'
const changes = [
{
key: 'system.stats.speed.value',
mode: foundry.CONST.ACTIVE_EFFECT_MODES.ADD,
value: 2,
priority: 0
}
]
for (const token of tokens) {
const mutate = swadeMBHelpers.createMutationWithEffect(icon, effectName, 5, changes)
const mutateOptions = swadeMBHelpers.defaultMutationOptions(effectName)
await warpgate.mutate(token.document, mutate, {}, mutateOptions)
}
}

41
macros/intangibility.js Normal file
View File

@ -0,0 +1,41 @@
swadeMBHelpers.runOnTargetOrSelectedTokens(main)
async function main (tokens) {
const tokenList = tokens.map(t => t.name).join(', ')
const menuOptions = {
title: 'Intangibility',
defaultButton: 'Cancel',
options: {}
}
const menuData = {
inputs: [
{ type: 'header', label: 'Intangibility' },
{ type: 'info', label: `Apply Intangibility to ${tokenList}` },
{ type: 'checkbox', label: 'Duration', options: false }
],
buttons: [
{ label: 'Apply', value: 'apply' },
{ label: 'Cancel', value: 'cancel' }
]
}
const { buttons, inputs } = await warpgate.menu(menuData, menuOptions)
if (buttons && buttons !== 'cancel') {
const options = {
duration: (!!inputs[2])
}
await createEffect(tokens, options)
}
}
async function createEffect (tokens, options) {
const effectName = 'Intangibility'
const duration = (options.duration ? 5 * 6 * 60 : 5)
const icon = 'icons/magic/control/debuff-energy-hold-levitate-blue-yellow.webp'
for (const token of tokens) {
const mutate = swadeMBHelpers.createMutationWithEffect(icon, effectName, duration, [])
const mutateOptions = swadeMBHelpers.defaultMutationOptions('Intangibility')
await warpgate.mutate(token.document, mutate, {}, mutateOptions)
}
}

44
macros/invisibility.js Normal file
View File

@ -0,0 +1,44 @@
swadeMBHelpers.runOnTargetOrSelectedTokens(main)
async function main (tokens) {
const tokenList = tokens.map(t => t.name).join(', ')
const menuOptions = {
title: 'Invisibility',
defaultButton: 'Cancel',
options: {}
}
const menuData = {
inputs: [
{ type: 'header', label: 'Invisibility' },
{ type: 'info', label: `Apply Invisibility to ${tokenList}` },
{ type: 'checkbox', label: 'Duration', options: false }
],
buttons: [
{ label: 'Apply', value: 'apply' },
{ label: 'Raise', value: 'raise' },
{ label: 'Cancel', value: 'cancel' }
]
}
const { buttons, inputs } = await warpgate.menu(menuData, menuOptions)
if (buttons && buttons !== 'cancel') {
const options = {
raise: (buttons === 'raise'),
duration: (!!inputs[2])
}
await createEffect(tokens, options)
}
}
async function createEffect (tokens, options) {
const effectName = `${options.raise ? 'major' : 'minor'} Invisibility`
const baseEffect = CONFIG.statusEffects.find(se => se.label === 'EFFECT.StatusInvisible')
const duration = (options.duration ? 5 * 6 * 60 : 5)
for (const token of tokens) {
const mutate = swadeMBHelpers.createMutationWithEffect(baseEffect.icon, effectName, duration, [])
mutate.embedded.ActiveEffect[effectName].flags.core = { statusId: baseEffect.id }
const mutateOptions = swadeMBHelpers.defaultMutationOptions('Invisibility')
await warpgate.mutate(token.document, mutate, {}, mutateOptions)
}
}

View File

@ -0,0 +1,32 @@
swadeMBHelpers.runOnTargetOrSelectedTokens(main)
async function main (tokens) {
const tokenList = tokens.map(t => t.name).join(', ')
const dialogOptions = {
title: 'Lingering Damage',
content: `Apply <em>Lingering Damage</em> to ${tokenList}`,
default: 'cancel',
buttons: [
{ label: 'Ok', value: 'ok' },
{ label: 'Cancel', value: 'cancel' }
]
}
const choice = await warpgate.buttonDialog(dialogOptions)
if (choice === 'ok') {
await createEffect(tokens)
}
}
async function createEffect (tokens) {
const icon = 'icons/magic/acid/dissolve-arm-flesh.webp'
const effectName = 'Lingering Damage'
for (const token of tokens) {
const mutate = swadeMBHelpers.createMutationWithEffect(icon, effectName, 1, [])
mutate.embedded.ActiveEffect['Lingering Damage'].flags.swade.expiration =
CONFIG.SWADE.CONST.STATUS_EFFECT_EXPIRATION.StartOfTurnPrompt
const mutateOptions = swadeMBHelpers.defaultMutationOptions(effectName)
await warpgate.mutate(token.document, mutate, {}, mutateOptions)
}
}

View File

@ -1,4 +0,0 @@
game.modules.get('swade-mb-helpers').api.powerEffectsMenu({
token,
targets: game.user.targets,
});

45
macros/protection.js Normal file
View File

@ -0,0 +1,45 @@
swadeMBHelpers.runOnTargetOrSelectedTokens(main)
async function main (tokens) {
const tokenList = tokens.map(t => t.name).join(', ')
const dialogOptions = {
title: 'Protection',
content: `Apply <em>Protection</em> to ${tokenList}`,
default: 'cancel',
buttons: [
{ label: 'Apply (+2 armor)', value: 'apply' },
{ label: 'Apply with raise (+2 toughness)', value: 'raise' },
{ label: 'Cancel', value: 'cancel' }
]
}
const choice = await warpgate.buttonDialog(dialogOptions)
if (choice && choice !== 'cancel') {
await createEffect(tokens, choice)
}
}
async function createEffect (tokens, choice) {
const baseEffect = CONFIG.statusEffects.find(se => se.label === 'SWADE.Protection')
const changes = [
{
key: 'system.stats.toughness.armor',
mode: foundry.CONST.ACTIVE_EFFECT_MODES.UPGRADE,
value: 2,
priority: 0
}
]
let effectName = 'minor Protection'
if (choice === 'raise') {
changes[0].key = 'system.stats.toughness.value'
effectName = 'major Protection'
}
for (const token of tokens) {
const mutate = swadeMBHelpers.createMutationWithEffect(
baseEffect.icon, effectName, 5, changes)
mutate.embedded.ActiveEffect[effectName].flags.core = { statusId: baseEffect.id }
const mutateOptions = swadeMBHelpers.defaultMutationOptions(effectName)
await warpgate.mutate(token.document, mutate, {}, mutateOptions)
}
}

View File

@ -1,43 +0,0 @@
new foundry.applications.api.DialogV2({
window: { title: "Damage Roll Configuration" },
content: `
<form>
<div class="form-group">
<label>Damage Roll:</label>
<input type="text" name="damageRoll" value="2d4x" />
</div>
<div class="form-group">
<label>AP:</label>
<input type="number" name="ap" value="0" />
</div>
<div class="form-group">
<label>Flavor:</label>
<input type="text" name="flavor" value="" />
</div>
</form>
`,
buttons: [
{
action: "ok",
label: "Roll Damage",
callback: (event, button, dialog) => {
const form = new foundry.applications.ux.FormDataExtended(button.form);
console.log(form)
const damageRoll = form.object.damageRoll;
let flavor = form.object.flavor;
const ap = parseInt(form.object.ap) || 0;
const options = {};
if (ap > 0) {
flavor = `${flavor ? flavor + " - " : ""}AP: ${ap}`
options.ap = ap;
}
// Perform the damage roll and send the message
new CONFIG.Dice.DamageRoll(damageRoll, null, options).toMessage({ flavor });
},
},
{
action: "cancel",
label: "Cancel",
},
],
}).render(true);

View File

@ -1,42 +0,0 @@
const requestFearRollFromTokens = game.modules.get('swade-mb-helpers').api.requestFearRollFromTokens;
async function main() {
let tokens = Array.from(game.user.targets);
if (tokens.length < 1) {
tokens = canvas.tokens.controlled;
}
if (tokens.length < 1) {
ui.notifications.error('Please target or select some tokens');
return;
}
new foundry.applications.api.DialogV2({
window: { title: 'Request Fear roll...' },
content: `
<form>
<p>Requesting Fear roll from ${tokens.map((t) => t.name).join(', ')}.</p>
<div class="form-group">
<label>Fear Check Penalty
<input type="number" value="0" name="fear">
</label>
</div>
</form>`,
buttons: [
{
action: "submit",
label: 'Request Roll',
callback: (event, button, dialog) => {
formdata = new foundry.applications.ux.FormDataExtended(button.form).object
const fear = parseInt(formdata.fear) || 0;
const options = { targetNumber: 4, fear };
requestFearRollFromTokens(tokens, options);
},
},
{
action: "cancel", label: 'Cancel',
},
],
}).render(true);
}
main();

View File

@ -1,95 +0,0 @@
const requestRollFromTokens = game.modules.get('swade-mb-helpers').api.requestRollFromTokens;
async function main() {
let tokens = Array.from(game.user.targets);
if (tokens.length < 1) {
tokens = canvas.tokens.controlled;
}
if (tokens.length < 1) {
ui.notifications.error('Please target or select some tokens');
return;
}
const attributes = ['Agility', 'Smarts', 'Spirit', 'Strength', 'Vigor'];
const skillSet = new Set();
for (const token of tokens) {
const tokenSkills = token.actor.items.filter(
(i) => i.type === 'skill' && !['Untrained', 'Untrained Attempt'].includes(i.name),
);
for (const skill of tokenSkills) {
skillSet.add(skill.name);
}
}
const attributeOptions = attributes
.map(
(a) => `
<option ${a === 'Agility' ? 'selected ' : ''} value="a|${a}">${a}</option>`,
)
.join('');
const skillOptions = Array.from(skillSet)
.sort()
.map(
(s) => `
<option value="s|${s}">${s}</option>`,
)
.join('');
const content = `
<form>
<p>Requesting roll from ${tokens.map((t) => t.name).join(', ')}.</p>
<div class="form-group">
<label for="trait">Trait to roll</label>
<select name="trait">
<optgroup label="Attributes">${attributeOptions}</optgroup>
<optgroup label="Skills">
${skillOptions}
<option value="s|NOSKILL">Untrained</option>
</optgroup>
</select>
</div>
<div class="form-group">
<label for="mod">Roll Modifier:</label>
<input type="number" value="0" name="mod">
</div>
<div class="form-group">
<label for="modDesc">Roll Modifier Description:</label>
<input type="text" value="Roll Modifier" name="modDesc">
</div>
<div class="form-group">
<label for="tn">Target Number</label>
<input type="number" value="4" name="tn">
</div>
</form>
`;
const buttons = [
{
action: "submit",
label: 'Request Roll',
callback: (event, button, dialog) => {
const form = button.form;
const formDataObject = new foundry.applications.ux.FormDataExtended(form).object;
console.log(formDataObject);
const rollMod = parseInt(formDataObject.mod);
const rollModDesc = formDataObject.modDesc;
const rollParts = formDataObject.trait.split('|');
const rollType = rollParts[0] === 'a' ? 'attribute' : 'skill';
const rollDesc = rollParts[1];
const targetNumber = parseInt(formDataObject.tn);
const options = { targetNumber };
if (rollMod !== 0) {
options.mods = [{ label: rollModDesc, value: rollMod }];
}
requestRollFromTokens(tokens, rollType, rollDesc, options);
},
},
{
action: "cancel", label: 'Cancel',
},
];
new foundry.applications.api.DialogV2({
window { title: 'Request roll' },
content,
buttons,
}).render(true);
}
main();

View File

@ -1,37 +0,0 @@
// This will request rolls from the tile's current collection, assuming
// that collection is tokens.
// call this from MATT's Run Macro with the following arguments,
// 1. roll type: "attribute" or "skill" (in double quotes)
// 2. roll description: attribute or skill name as you want
// it to appear in the request title (in double quotes), eg "Strength"
// or "Common Knowledge"
// 3... paired arguments, each pair a modifier and a description,
// eg: '-2 "Noxious Fog" +1 "Bless Aura"'
// so an entire arguments box in MATT may look like this:
// "skill" "Common Knowledge" -2 "Ugly Wallpaper" +1 "Rousing Speech"
const requestRollFromTokens = game.modules.get('swade-mb-helpers').api.requestRollFromTokens
const tokens = arguments[0].value.tokens.map(t => canvas.tokens.get(t.id))
const rolldata = args
async function main () {
if (tokens.length < 1) {
return
}
const rollType = rolldata.shift()
const rollDesc = rolldata.shift()
const options = { targetNumber: 4 }
const mods = []
while (rolldata.length > 0) {
const value = Number(rolldata.shift())
const label = rolldata.shift()
mods.push({ label, value })
}
if (mods.length > 0) {
options.mods = mods
}
requestRollFromTokens(tokens, rollType, rollDesc, options)
}
main()

View File

@ -1,156 +0,0 @@
const argBright = typeof args === 'undefined' ? null : args.length > 0 ? args[0] : null;
// argument can be one of 'bright', 'dim', 'dark', 'pitchdark'. Other values
// will guess based on scene darkness
const BRIGHT_LEVELS = ['bright', 'dim', 'dark', 'pitchdark'];
const THRESHOLDS = {
dim: 0.4,
dark: 0.6,
pitchdark: 0.8,
};
const RANGES = {
basic: {
bright: 25,
dim: 25,
dark: 10,
pitchdark: 0,
},
lowlight: {
bright: 25,
dim: 25,
dark: 10,
pitchdark: 0,
},
darkvision: {
bright: 25,
dim: 25,
dark: 10,
pitchdark: 10,
},
nightvision: {
bright: 200,
dim: 200,
dark: 200,
pitchdark: 200,
},
blindsense: {
bright: 5,
dim: 5,
dark: 5,
pitchdark: 5,
},
};
const SIGHT_NAMES = {
lowlight: 'low-light-vision',
darkvision: 'darkvision',
nightvision: 'night-vision',
blindsense: 'blindsense',
};
const SIGHT_MODES = {
lowlight: 'lowlight',
darkvision: 'darkvision',
nightvision: 'darkvision',
basic: 'basic',
blindsense: 'blindsense',
};
function findAbility(token, swid) {
return token.actor.items.find((i) => i.type === 'ability' && i.system.swid === swid);
}
async function main() {
const scene = game.scenes.current;
let sceneBright = BRIGHT_LEVELS[0];
if (scene.darkness > THRESHOLDS.pitchdark) {
sceneBright = BRIGHT_LEVELS[3];
} else if (scene.darkness > THRESHOLDS.dark) {
sceneBright = BRIGHT_LEVELS[2];
} else if (scene.darkness > THRESHOLDS.dim) {
sceneBright = BRIGHT_LEVELS[1];
}
let bright = sceneBright;
if (argBright && BRIGHT_LEVELS.includes(argBright)) {
bright = argBright;
}
new foundry.applications.api.DialogV2({
window: { title: 'Select scene brightness' },
content: `
<form>
<h2>Set token vision</h2>
<p>All tokens with vision will be adjusted</p>
<div class="form-group">
<label class="checkbox">
<input type="radio" name="bright" value="${BRIGHT_LEVELS[0]}"
${bright === BRIGHT_LEVELS[0] ? 'checked' : ''}/>
Bright Light
</label>
</div>
<div class="form-group">
<label class="checkbox">
<input type="radio" name="bright" value="${BRIGHT_LEVELS[1]}"
${bright === BRIGHT_LEVELS[1] ? 'checked' : ''}/>
Dim Light
</label>
</div>
<div class="form-group">
<label class="checkbox">
<input type="radio" name="bright" value="${BRIGHT_LEVELS[2]}"
${bright === BRIGHT_LEVELS[2] ? 'checked' : ''}/>
Dark
</label>
</div>
<div class="form-group">
<label class="checkbox">
<input type="radio" name="bright" value="${BRIGHT_LEVELS[3]}"
${bright === BRIGHT_LEVELS[3] ? 'checked' : ''}/>
Pitch Dark
</label>
</div>
</form>
`,
buttons: [
{
action: "submit",
label: 'Select scene Brightness',
value: 'ok',
callback: (event, button, dialog) => {
const form = button.form;
const formDataObject = new foundry.applications.ux.FormDataExtended(form).object;
console.log('form data', formDataObject, form);
bright = formDataObject.bright;
for (const tokenId of scene.tokens.map((t) => t.id)) {
const token = scene.tokens.get(tokenId);
if (!token.sight.enabled) {
console.log(`Skipping ${token.name}, vision not enabled`);
continue;
// don't set sight on a token where it's not enabled
}
let sightType = 'basic';
for (const sight in SIGHT_NAMES) {
if (findAbility(token, SIGHT_NAMES[sight])) {
sightType = sight;
}
}
const range = RANGES[sightType][bright];
const sightMode = SIGHT_MODES[sightType];
const visionModeData = CONFIG.Canvas.visionModes[sightMode].vision.defaults;
const data = {
'sight.range': range,
'sight.visionMode': sightMode,
'sight.attenuation': visionModeData.attenuation,
'sight.brightness': visionModeData.brightness,
'sight.saturation': visionModeData.saturation,
'sight.contrast': visionModeData.contrast,
};
console.log(`Updating ${token.name}:`, sightType, bright, data);
token.update(data);
}
},
},
{ action: "cancel", label: 'Cancel' },
],
}).render(true);
}
main();

58
macros/shroud.js Normal file
View File

@ -0,0 +1,58 @@
swadeMBHelpers.runOnTargetOrSelectedTokens(main)
async function main (tokens) {
const tokenList = tokens.map(t => t.name).join(', ')
const dialogOptions = {
title: 'Shroud',
content: `Apply <em>Shroud</em> to ${tokenList}`,
default: 'cancel',
buttons: [
{ label: 'OK', value: 'ok' },
{ label: 'Mutate token lighting', value: 'mutate' },
{ label: 'Cancel', value: 'cancel' }
]
}
const choice = await warpgate.buttonDialog(dialogOptions)
if (choice === 'ok' || choice === 'mutate') {
await createEffect(tokens, choice)
}
}
async function createEffect (tokens, choice) {
const icon = 'icons/magic/perception/silhouette-stealth-shadow.webp'
const effectName = 'Shroud'
for (const token of tokens) {
const mutate = swadeMBHelpers.createMutationWithEffect(icon, effectName, 5, [])
const mutateOptions = swadeMBHelpers.defaultMutationOptions(effectName)
await warpgate.mutate(token.document, mutate, {}, mutateOptions)
if (choice === 'mutate') {
const mutate2 = {
token: {
light: {
alpha: 0.5,
angle: 360,
attenuation: 0.1,
animation: {
intensity: 5,
reverse: false,
speed: 5,
type: 'roiling'
},
bright: 0,
color: null,
coloration: 0,
contrast: 0,
dim: 0.1,
luminosity: -0.15,
saturation: 0,
shadows: 0.25
}
}
}
mutateOptions.permanent = false
await warpgate.mutate(token.document, mutate2, {}, mutateOptions)
}
}
}

74
macros/smite.js Normal file
View File

@ -0,0 +1,74 @@
swadeMBHelpers.runOnTargetOrSelectedTokens(main)
async function main (tokens) {
const tokenList = tokens.map(t => t.name).join(', ')
const menuOptions = {
title: 'Smite',
defaultButton: 'Cancel',
options: {}
}
const menuData = {
inputs: [
{ type: 'header', label: 'Smite' },
{ type: 'info', label: `Apply Smite to ${tokenList}` },
{ type: 'checkbox', label: 'Greater', options: false }
],
buttons: [
{ label: 'Apply', value: 'apply' },
{ label: 'Apply with Raise', value: 'raise' },
{ label: 'Cancel', value: 'cancel' }
]
}
const tokenWeapons = {}
let index = 2
for (const token of tokens) {
index += 2
tokenWeapons[token.id] = index
menuData.inputs.push({ type: 'info', label: `<h2>${token.name}</h2>` })
const weapons = token.actor.items.filter(i => i.type === 'weapon').map(
i => { return { value: i.name, html: i.name } })
weapons.unshift({ value: '', html: '<i>None</i>' })
menuData.inputs.push({ type: 'select', label: token.name, options: weapons })
}
const { buttons, inputs } = await warpgate.menu(menuData, menuOptions)
for (const tokenId in tokenWeapons) {
tokenWeapons[tokenId] = inputs[tokenWeapons[tokenId]]
}
const greater = (inputs[2] === 'Greater')
if (buttons && buttons !== 'cancel') {
await createEffect(tokens, tokenWeapons, buttons, greater)
}
}
async function createEffect (tokens, tokenWeapons, choice, greater) {
const baseEffect = CONFIG.statusEffects.find(se => se.label === 'SWADE.Smite')
const effectIcon = baseEffect.icon
let changeValue = (choice === 'raise' ? '+4' : '+2')
if (greater) {
changeValue = (choice === 'raise' ? '+6' : '+4')
}
for (const token of tokens) {
const weaponName = tokenWeapons[token.id]
const weaponId = token.actor.items.getName(weaponName)?.id
const changeKey = `@Weapon{${weaponName}}[system.damage]`
if (!weaponId) {
continue
}
const effectName = `${choice === 'raise' ? 'major' : 'minor'} Smite${greater ? '(greater)' : ''} (${weaponName})`
const changes = [
{
key: changeKey,
mode: foundry.CONST.ACTIVE_EFFECT_MODES.ADD,
value: changeValue,
priority: 0
}
]
const mutate = swadeMBHelpers.createMutationWithEffect(
effectIcon, effectName, 5, changes)
mutate.embedded.ActiveEffect[effectName].flags.core = { statusId: baseEffect.id }
const mutateOptions = swadeMBHelpers.defaultMutationOptions(effectName)
await warpgate.mutate(token.document, mutate, {}, mutateOptions)
}
}

74
macros/summon.js Normal file
View File

@ -0,0 +1,74 @@
const ACTORFOLDER = 'Summonables'
const SUMMONICON = 'icons/magic/symbols/runes-star-orange.webp'
swadeMBHelpers.runOnTargetOrSelectedTokens(main)
async function main (tokens) {
const token = tokens[0]
const tokenList = token.name
const folder = swadeMBHelpers.getActorFolderByPath(ACTORFOLDER)
const actors = swadeMBHelpers.getActorsInFolder(folder)
const menuOptions = {
title: 'Summon Creature',
defaultButton: 'cancel',
options: {}
}
const menuData = {
inputs: [
{ type: 'header', label: 'Summon Creature' },
{ type: 'info', label: `${tokenList} is summoning` },
{
type: 'select',
label: 'Ally to summon',
options: Object.keys(actors).sort().map(k => { return { value: actors[k].id, html: k } })
},
{ type: 'number', label: 'Number to spawn (+half base cost per)', options: 1 }
],
buttons: [
{ label: 'Apply', value: 'apply' },
{ label: 'Apply with raise', value: 'raise' },
{ label: 'Cancel', value: 'cancel' }
]
}
const { buttons, inputs } = await warpgate.menu(menuData, menuOptions)
if (buttons && buttons !== 'cancel') {
const summonData = {
raise: (buttons === 'raise'),
actorId: inputs[2],
number: inputs[3]
}
summonData.actor = game.actors.get(summonData.actorId)
summonData.actorName = summonData.actor.name
summonData.icon = summonData.actor.prototypeToken.texture.src
summonData.token = summonData.actor.prototypeToken
doWork(summonData, token)
}
}
async function doWork (summonData, token) {
console.log('Summon ', token, summonData)
const effectName = `Summoned ${summonData.actorName} (${summonData.number})`
const tokenEffectMutation = swadeMBHelpers.createMutationWithEffect(SUMMONICON, effectName, 5, [])
const mutateOptions = swadeMBHelpers.defaultMutationOptions(effectName)
await warpgate.mutate(token.document, tokenEffectMutation, {}, mutateOptions)
const spawnOptions = {
controllingActor: token.actor,
duplicates: summonData.number,
comparisonKeys: { ActiveEffect: 'label' },
crosshairs: {
icon: summonData.icon,
label: `Summon ${summonData.actorName}`,
drawOutline: false,
rememberControlled: true
}
}
const spawnMutation = {
token: {
actorLink: false,
name: `${token.name}'s ${summonData.token.name}`
}
}
await warpgate.spawn(summonData.actorName, spawnMutation, {}, spawnOptions)
}

View File

@ -1,24 +1,19 @@
{ {
"id": "swade-mb-helpers", "id": "swade-mb-helpers",
"title": "SWADE Helpers (MB)", "title": "SWADE Helpers (MB)",
"description": "Mike's collection of SWADE helpers", "description": "Mike's collection of swade helpers",
"version": "2.0.0",
"authors": [ "authors": [
{ {
"name": "Mike", "name": "Mike"
"flags": {}
} }
], ],
"url": "https://git.bloy.org/foundryvtt/swade-mb-helpers",
"version": "4.1.0",
"compatibility": { "compatibility": {
"minimum": "13", "minimum": "11",
"verified": "13" "verified": "11"
}, },
"esmodules": [ "esmodules": [
"module/swade-mb-helpers.js" "scripts/module.js"
],
"styles": [
"styles/swade-mb-helpers.css"
], ],
"packs": [ "packs": [
{ {
@ -44,7 +39,7 @@
} }
}, },
{ {
"name": "CommonActions", "name": "Common Actions",
"label": "SWADE MB Common Actions", "label": "SWADE MB Common Actions",
"path": "packs/common-actions", "path": "packs/common-actions",
"type": "Item", "type": "Item",
@ -59,33 +54,11 @@
"label": "SWADE MB Helper Actors", "label": "SWADE MB Helper Actors",
"path": "packs/helper-actors", "path": "packs/helper-actors",
"type": "Actor", "type": "Actor",
"system": "swade",
"ownership": { "ownership": {
"PLAYER": "OBSERVER", "PLAYER": "OBSERVER",
"ASSISTANT": "OWNER" "ASSISTANT": "OWNER"
} },
}, "system": "swade"
{
"name": "power-actors",
"label": "SWADE MB Example Power Actors",
"path": "packs/power-actors",
"type": "Actor",
"system": "swade",
"ownership": {
"PLAYER": "OBSERVER",
"ASSISTANT": "OWNER"
}
},
{
"name": "swade-mb-gear",
"label": "SWADE MB Gear",
"path": "packs/gear",
"type": "Item",
"system": "swade",
"ownership": {
"PLAYER": "OBSERVER",
"ASSISTANT": "OWNER"
}
} }
], ],
"packFolders": [ "packFolders": [
@ -98,9 +71,7 @@
"module-docs", "module-docs",
"helper-macros", "helper-macros",
"helper-actors", "helper-actors",
"Common Actions", "Common Actions"
"swade-mb-gear",
"power-actors"
] ]
} }
], ],
@ -111,55 +82,36 @@
"type": "system", "type": "system",
"manifest": "https://gitlab.com/api/v4/projects/16269883/packages/generic/swade/latest/system.json", "manifest": "https://gitlab.com/api/v4/projects/16269883/packages/generic/swade/latest/system.json",
"compatibility": { "compatibility": {
"minimum": "5.1.0", "verified": "2.2.5"
"verified": "5.1.0"
} }
} }
], ],
"requires": [ "requires": [
{ {
"id": "socketlib", "id": "warpgate",
"type": "module", "type": "module",
"compatibility": {} "manifest": "https://github.com/trioderegion/warpgate/releases/latest/download/module.json",
}, "compatibility": {
{ "verified": "1.16.2"
"id": "tcal", }
"type": "module",
"compatibility": {}
},
{
"id": "sequencer",
"type": "module",
"compatibility": {}
} }
], ],
"recommends": [ "recommends": [
{
"id": "token-variants",
"type": "module",
"compatibility": {}
},
{ {
"id": "torch", "id": "torch",
"type": "module", "type": "module",
"compatibility": {} "compatibility": {}
},
{
"id": "JB2A_DnD5e",
"type": "module",
"compatibility": {}
},
{
"id": "visual-active-effects",
"type": "module",
"compatibility": {}
} }
] ]
}, },
"languages": [ "url": "https://git.bloy.org/foundryvtt/swade-mb-helpers",
{ "manifest": "https://git.bloy.org/foundryvtt/swade-mb-helpers/raw/branch/main/module.json",
"lang": "en", "download": "https://git.bloy.org/foundryvtt/swade-mb-helpers/archive/main.zip",
"name": "English", "license": "./LICENSE",
"path": "lang/en.json", "readme": "./README.md"
"flags": {}
}
],
"socket": true,
"manifest": "https://git.bloy.org/foundryvtt/swade-mb-helpers/releases/download/latest/module.json",
"download": "https://git.bloy.org/foundryvtt/swade-mb-helpers/releases/download/latest/swade-mb-helpers.zip"
} }

14692
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,53 +1,11 @@
{ {
"private": true,
"name": "swade-mb-helpers",
"version": "2.4.3",
"description": "Mike's Collection of swade helpers",
"license": "ALL RIGHTS RESERVED",
"homepage": "https://git.bloy.org/foundryvtt/swade-mb-helpers",
"contributors": [
{
"name": "Mike"
}
],
"type": "module",
"scripts": {
"build": "gulp build",
"build:watch": "gulp watch",
"link-project": "gulp link",
"clean": "gulp clean",
"clean:link": "gulp link --clean",
"lint": "eslint --ext .js,.cjs,.mjs .",
"lint:fix": "eslint --ext .js,.cjs,.mjs --fix .",
"format": "prettier --write \"./**/*.(js|cjs|mjs|json|yml|scss)\"",
"postinstall": "husky install"
},
"devDependencies": { "devDependencies": {
"@rollup/plugin-node-resolve": "^15.2.3", "@league-of-foundry-developers/foundry-vtt-types": "^9.280.0",
"@rollup/stream": "^3.0.1", "eslint": "^8.48.0",
"@typhonjs-fvtt/eslint-config-foundry.js": "^0.8.0", "eslint-config-standard": "^17.1.0",
"eslint": "^9.2.0", "eslint-plugin-import": "^2.28.1",
"eslint-config-prettier": "^10.1.5", "eslint-plugin-n": "^16.0.2",
"eslint-plugin-prettier": "^5.1.3", "eslint-plugin-promise": "^6.1.1",
"fs-extra": "^11.2.0", "typescript": "^5.2.2"
"gulp": "^5.0.0",
"gulp-dart-sass": "^1.1.0",
"gulp-sourcemaps": "^2.6.5",
"husky": "^9.0.11",
"lint-staged": "^16.1.0",
"prettier": "^3.2.5",
"prettier-eslint": "^16.3.0",
"prettier-eslint-cli": "^8.0.1",
"rollup": "^2.79.2",
"typescript": "^5.7.3",
"typescript-language-server": "^4.3.3",
"vinyl-buffer": "^1.0.1",
"vinyl-source-stream": "^2.0.0",
"vscode-langservers-extracted": "^4.10.0",
"yargs": "^18.0.0"
},
"lint-staged": {
"*.(js|cjs|mjs)": "eslint --fix",
"*.(json|yml|scss)": "prettier --write"
} }
} }

Binary file not shown.

View File

@ -0,0 +1 @@
MANIFEST-000170

8
packs/common-actions/LOG Normal file
View File

@ -0,0 +1,8 @@
2023/11/07-23:29:20.343335 7f46667fc640 Recovering log #167
2023/11/07-23:29:20.345329 7f46667fc640 Delete type=0 #167
2023/11/07-23:29:20.345336 7f46667fc640 Delete type=3 #165
2023/11/07-23:29:29.393563 7f46649f4640 Level-0 table #173: started
2023/11/07-23:29:29.393574 7f46649f4640 Level-0 table #173: 0 bytes OK
2023/11/07-23:29:29.394638 7f46649f4640 Delete type=0 #171
2023/11/07-23:29:29.396575 7f46649f4640 Manual compaction at level-0 from '!folders!0nDRFmMBs5DBJU9M' @ 72057594037927935 : 1 .. '!items!xA7qKMmugJv7z6j1' @ 0 : 0; will stop at (end)
2023/11/07-23:29:29.396609 7f46649f4640 Manual compaction at level-1 from '!folders!0nDRFmMBs5DBJU9M' @ 72057594037927935 : 1 .. '!items!xA7qKMmugJv7z6j1' @ 0 : 0; will stop at (end)

View File

@ -0,0 +1,15 @@
2023/11/07-17:50:41.783626 7f4666ffd640 Recovering log #163
2023/11/07-17:50:41.785537 7f4666ffd640 Delete type=0 #163
2023/11/07-17:50:41.785550 7f4666ffd640 Delete type=3 #161
2023/11/07-23:29:14.520879 7f46649f4640 Level-0 table #168: started
2023/11/07-23:29:14.522237 7f46649f4640 Level-0 table #168: 19378 bytes OK
2023/11/07-23:29:14.523105 7f46649f4640 Delete type=0 #166
2023/11/07-23:29:14.524999 7f46649f4640 Manual compaction at level-0 from '!folders!0nDRFmMBs5DBJU9M' @ 72057594037927935 : 1 .. '!items!xA7qKMmugJv7z6j1' @ 0 : 0; will stop at (end)
2023/11/07-23:29:14.526907 7f46649f4640 Manual compaction at level-1 from '!folders!0nDRFmMBs5DBJU9M' @ 72057594037927935 : 1 .. '!items!xA7qKMmugJv7z6j1' @ 0 : 0; will stop at '!items!xA7qKMmugJv7z6j1' @ 219 : 1
2023/11/07-23:29:14.526908 7f46649f4640 Compacting 1@1 + 1@2 files
2023/11/07-23:29:14.527704 7f46649f4640 Generated table #169@1: 21 keys, 9211 bytes
2023/11/07-23:29:14.527711 7f46649f4640 Compacted 1@1 + 1@2 files => 9211 bytes
2023/11/07-23:29:14.528471 7f46649f4640 compacted to: files[ 0 0 1 0 0 0 0 ]
2023/11/07-23:29:14.528489 7f46649f4640 Delete type=2 #168
2023/11/07-23:29:14.528516 7f46649f4640 Delete type=2 #160
2023/11/07-23:29:14.541956 7f46649f4640 Manual compaction at level-1 from '!items!xA7qKMmugJv7z6j1' @ 219 : 1 .. '!items!xA7qKMmugJv7z6j1' @ 0 : 0; will stop at (end)

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1 @@
MANIFEST-000069

8
packs/helper-actors/LOG Normal file
View File

@ -0,0 +1,8 @@
2023/11/07-23:29:20.346979 7f4665ffb640 Recovering log #66
2023/11/07-23:29:20.349569 7f4665ffb640 Delete type=3 #64
2023/11/07-23:29:20.349612 7f4665ffb640 Delete type=0 #66
2023/11/07-23:29:29.395544 7f46649f4640 Level-0 table #72: started
2023/11/07-23:29:29.395555 7f46649f4640 Level-0 table #72: 0 bytes OK
2023/11/07-23:29:29.396547 7f46649f4640 Delete type=0 #70
2023/11/07-23:29:29.396590 7f46649f4640 Manual compaction at level-0 from '!actors!U5v4gFHquo0Y1SAq' @ 72057594037927935 : 1 .. '!actors!U5v4gFHquo0Y1SAq' @ 0 : 0; will stop at (end)
2023/11/07-23:29:29.396624 7f46649f4640 Manual compaction at level-1 from '!actors!U5v4gFHquo0Y1SAq' @ 72057594037927935 : 1 .. '!actors!U5v4gFHquo0Y1SAq' @ 0 : 0; will stop at (end)

View File

@ -0,0 +1,15 @@
2023/11/07-17:50:41.786664 7f4665ffb640 Recovering log #62
2023/11/07-17:50:41.788546 7f4665ffb640 Delete type=0 #62
2023/11/07-17:50:41.788559 7f4665ffb640 Delete type=3 #60
2023/11/07-23:29:14.523160 7f46649f4640 Level-0 table #67: started
2023/11/07-23:29:14.524102 7f46649f4640 Level-0 table #67: 1737 bytes OK
2023/11/07-23:29:14.524964 7f46649f4640 Delete type=0 #65
2023/11/07-23:29:14.525003 7f46649f4640 Manual compaction at level-0 from '!actors!U5v4gFHquo0Y1SAq' @ 72057594037927935 : 1 .. '!actors!U5v4gFHquo0Y1SAq' @ 0 : 0; will stop at (end)
2023/11/07-23:29:14.525014 7f46649f4640 Manual compaction at level-1 from '!actors!U5v4gFHquo0Y1SAq' @ 72057594037927935 : 1 .. '!actors!U5v4gFHquo0Y1SAq' @ 0 : 0; will stop at '!actors!U5v4gFHquo0Y1SAq' @ 2 : 1
2023/11/07-23:29:14.525016 7f46649f4640 Compacting 1@1 + 1@2 files
2023/11/07-23:29:14.526092 7f46649f4640 Generated table #68@1: 1 keys, 1737 bytes
2023/11/07-23:29:14.526097 7f46649f4640 Compacted 1@1 + 1@2 files => 1737 bytes
2023/11/07-23:29:14.526856 7f46649f4640 compacted to: files[ 0 0 1 0 0 0 0 ]
2023/11/07-23:29:14.526872 7f46649f4640 Delete type=2 #5
2023/11/07-23:29:14.526891 7f46649f4640 Delete type=2 #67
2023/11/07-23:29:14.541950 7f46649f4640 Manual compaction at level-1 from '!actors!U5v4gFHquo0Y1SAq' @ 2 : 1 .. '!actors!U5v4gFHquo0Y1SAq' @ 0 : 0; will stop at (end)

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1 @@
MANIFEST-000199

8
packs/helper-macros/LOG Normal file
View File

@ -0,0 +1,8 @@
2023/11/07-23:29:20.340266 7f4665ffb640 Recovering log #197
2023/11/07-23:29:20.342378 7f4665ffb640 Delete type=0 #197
2023/11/07-23:29:20.342408 7f4665ffb640 Delete type=3 #195
2023/11/07-23:29:29.392493 7f46649f4640 Level-0 table #202: started
2023/11/07-23:29:29.392505 7f46649f4640 Level-0 table #202: 0 bytes OK
2023/11/07-23:29:29.393529 7f46649f4640 Delete type=0 #200
2023/11/07-23:29:29.394661 7f46649f4640 Manual compaction at level-0 from '!folders!hIbrWxg1nDutCSwt' @ 72057594037927935 : 1 .. '!macros!wU2mAUnw3RW9qMT8' @ 0 : 0; will stop at (end)
2023/11/07-23:29:29.396569 7f46649f4640 Manual compaction at level-1 from '!folders!hIbrWxg1nDutCSwt' @ 72057594037927935 : 1 .. '!macros!wU2mAUnw3RW9qMT8' @ 0 : 0; will stop at (end)

View File

@ -0,0 +1,8 @@
2023/11/07-17:50:41.780718 7f4665ffb640 Recovering log #193
2023/11/07-17:50:41.782723 7f4665ffb640 Delete type=3 #191
2023/11/07-17:50:41.782737 7f4665ffb640 Delete type=0 #193
2023/11/07-23:29:14.517951 7f46649f4640 Level-0 table #198: started
2023/11/07-23:29:14.517966 7f46649f4640 Level-0 table #198: 0 bytes OK
2023/11/07-23:29:14.518928 7f46649f4640 Delete type=0 #196
2023/11/07-23:29:14.520867 7f46649f4640 Manual compaction at level-0 from '!folders!hIbrWxg1nDutCSwt' @ 72057594037927935 : 1 .. '!macros!wU2mAUnw3RW9qMT8' @ 0 : 0; will stop at (end)
2023/11/07-23:29:14.524993 7f46649f4640 Manual compaction at level-1 from '!folders!hIbrWxg1nDutCSwt' @ 72057594037927935 : 1 .. '!macros!wU2mAUnw3RW9qMT8' @ 0 : 0; will stop at (end)

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1 @@
MANIFEST-000197

8
packs/module-docs/LOG Normal file
View File

@ -0,0 +1,8 @@
2023/11/07-23:29:20.336710 7f46667fc640 Recovering log #194
2023/11/07-23:29:20.338673 7f46667fc640 Delete type=0 #194
2023/11/07-23:29:20.338682 7f46667fc640 Delete type=3 #192
2023/11/07-23:29:29.390257 7f46649f4640 Level-0 table #200: started
2023/11/07-23:29:29.390274 7f46649f4640 Level-0 table #200: 0 bytes OK
2023/11/07-23:29:29.391163 7f46649f4640 Delete type=0 #198
2023/11/07-23:29:29.392479 7f46649f4640 Manual compaction at level-0 from '!journal!HbtPlHNFO1L6RVj0' @ 72057594037927935 : 1 .. '!journal.pages!Mw1g2Fx5dp4SoqVP.lhULHNp4gz9IjOR3' @ 0 : 0; will stop at (end)
2023/11/07-23:29:29.393557 7f46649f4640 Manual compaction at level-1 from '!journal!HbtPlHNFO1L6RVj0' @ 72057594037927935 : 1 .. '!journal.pages!Mw1g2Fx5dp4SoqVP.lhULHNp4gz9IjOR3' @ 0 : 0; will stop at (end)

15
packs/module-docs/LOG.old Normal file
View File

@ -0,0 +1,15 @@
2023/11/07-17:50:41.777838 7f4666ffd640 Recovering log #190
2023/11/07-17:50:41.779631 7f4666ffd640 Delete type=0 #190
2023/11/07-17:50:41.779649 7f4666ffd640 Delete type=3 #188
2023/11/07-23:29:14.515315 7f46649f4640 Level-0 table #195: started
2023/11/07-23:29:14.516677 7f46649f4640 Level-0 table #195: 9634 bytes OK
2023/11/07-23:29:14.517872 7f46649f4640 Delete type=0 #193
2023/11/07-23:29:14.518953 7f46649f4640 Manual compaction at level-0 from '!journal!HbtPlHNFO1L6RVj0' @ 72057594037927935 : 1 .. '!journal.pages!Mw1g2Fx5dp4SoqVP.lhULHNp4gz9IjOR3' @ 0 : 0; will stop at (end)
2023/11/07-23:29:14.518974 7f46649f4640 Manual compaction at level-1 from '!journal!HbtPlHNFO1L6RVj0' @ 72057594037927935 : 1 .. '!journal.pages!Mw1g2Fx5dp4SoqVP.lhULHNp4gz9IjOR3' @ 0 : 0; will stop at '!journal.pages!Mw1g2Fx5dp4SoqVP.lhULHNp4gz9IjOR3' @ 98 : 1
2023/11/07-23:29:14.518976 7f46649f4640 Compacting 1@1 + 1@2 files
2023/11/07-23:29:14.519779 7f46649f4640 Generated table #196@1: 6 keys, 4503 bytes
2023/11/07-23:29:14.519792 7f46649f4640 Compacted 1@1 + 1@2 files => 4503 bytes
2023/11/07-23:29:14.520666 7f46649f4640 compacted to: files[ 0 0 1 0 0 0 0 ]
2023/11/07-23:29:14.520737 7f46649f4640 Delete type=2 #167
2023/11/07-23:29:14.520824 7f46649f4640 Delete type=2 #195
2023/11/07-23:29:14.524986 7f46649f4640 Manual compaction at level-1 from '!journal.pages!Mw1g2Fx5dp4SoqVP.lhULHNp4gz9IjOR3' @ 98 : 1 .. '!journal.pages!Mw1g2Fx5dp4SoqVP.lhULHNp4gz9IjOR3' @ 0 : 0; will stop at (end)

Binary file not shown.

View File

@ -1,80 +0,0 @@
#!/bin/bash
curdir=$(realpath $(dirname $0))
version=$1
echo $version
if [ -z "$version" ]; then
echo "must give a version specification."
exit 1
fi
echo Tagging git release...
git tag -a -m $version $version
git push origin ${version}
echo Tagged git release $(git describe)
read -r -d '' release_body <<EOF
{
"body": "${version}",
"draft": true,
"name": "${version}",
"prerelease": false,
"tag_name": "${version}",
"target_commitish": "${version}"
}
EOF
releaseId=$(curl -n -X 'POST' \
'https://git.bloy.org/api/v1/repos/foundryvtt/swade-mb-helpers/releases' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d "${release_body}" \
| jq '.id')
(cd $curdir/dist; rm -f swade-mb-helpers.zip; zip -r swade-mb-helpers.zip swade-mb-helpers)
# curl -n -X 'POST' \
# "https://git.bloy.org/api/v1/repos/foundryvtt/swade-mb-helpers/releases/${releaseId}/assets?name=module.json" \
# -H 'accept: application/json' \
# -T ./dist/swade-mb-helpers/module.json
# curl -n -X 'POST' \
# "https://git.bloy.org/api/v1/repos/foundryvtt/swade-mb-helpers/releases/${releaseId}/assets?name=swade-mb-helpers.zip" \
# -H 'accept: application/json' \
# -T ./dist/swade-mb-helpers.zip
echo
echo "Updating module.json"
curl -n -X 'POST' \
"https://git.bloy.org/api/v1/repos/foundryvtt/swade-mb-helpers/releases/${releaseId}/assets?name=module.json" \
-H 'accept: application/json' \
-H 'Content-Type: multipart/form-data' \
-F 'attachment=@dist/swade-mb-helpers/module.json;type=application/json'
echo
echo "Updating swade-mb-helpers.zip"
curl -n -X 'POST' \
"https://git.bloy.org/api/v1/repos/foundryvtt/swade-mb-helpers/releases/${releaseId}/assets?name=swade-mb-helpers.zip" \
-H 'accept: application/json' \
-H 'Content-Type: multipart/form-data' \
-F 'attachment=@dist/swade-mb-helpers.zip;type=application/x-zip-compressed'
read -r -d '' patch_body <<EOF
{
"body": "${version}",
"draft": false,
"name": "${version}",
"prerelease": false,
"tag_name": "${version}",
"target_commitish": "${version}"
}
EOF
echo
echo "setting to not draft"
curl -n -X 'PATCH' \
"https://git.bloy.org/api/v1/repos/foundryvtt/swade-mb-helpers/releases/${releaseId}" \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d "${patch_body}" \

View File

@ -1,16 +0,0 @@
// SPDX-FileCopyrightText: 2022 Johannes Loher
// SPDX-FileCopyrightText: 2022 David Archibald
//
// SPDX-License-Identifier: MIT
import { nodeResolve } from '@rollup/plugin-node-resolve';
export default () => ({
input: 'src/module/swade-mb-helpers.js',
output: {
dir: 'dist/swade-mb-helpers/module',
format: 'es',
sourcemap: true,
},
plugins: [nodeResolve()],
});

18
scripts/api.js Normal file
View File

@ -0,0 +1,18 @@
import { helpers } from './helpers.js'
import { shim, log } from './shim.js'
import { powerEffects } from './powerEffects.js'
export class api {
static registerFunctions () {
log('SWADE MB Helpers initialized')
api.globals()
}
static globals () {
const moduleName = 'swade-mb-helpers'
game.modules.get(moduleName).api = {
DEBUG: true,
powerEffects
}
}
}

92
scripts/helpers.js Normal file
View File

@ -0,0 +1,92 @@
import { CONST, shim } from './shim.js'
export class helpers {
static runOnTargetOrSelectedTokens (runFunc) {
let tokens = []
const targets = Array.from(shim.targets)
if (targets.length > 0) {
tokens = targets
} else if (shim.controlled.length > 0) {
tokens = shim.controlled
}
if (tokens.length > 0) {
runFunc(tokens)
} else {
shim.notifications.error('Please select or target a token')
}
}
static createEffectDocument (icon, name, durationRounds, changes) {
const effectData = {
icon,
name,
duration: { rounds: durationRounds },
flags: {
swade: {
expiration: CONST.SWADE.STATUS_EFFECT_EXPIRATION.EndOfTurnPrompt,
loseTurnOnHold: true
}
},
changes
}
return effectData
}
static createMutationWithEffect (icon, name, durationRounds, changes) {
const effect = helpers.createEffectDocument(icon, name, durationRounds, changes)
const mutate = {
embedded: { ActiveEffect: {} }
}
mutate.embedded.ActiveEffect[name] = effect
return mutate
}
static defaultMutationOptions (name) {
const mutateOptions = {
name,
permanent: true,
description: name
}
return mutateOptions
}
static getActorFolderByPath (path) {
const names = path.split('/')
if (names[0] === '') {
names.shift()
}
let name = names.shift()
let folder = shim.folders.find(f => f.name === name && !f.folder)
while (names.length > 0) {
name = names.shift()
folder = folder.children.find(c => c.folder.name === name)
folder = folder.folder
}
return folder
}
static getActorsInFolder (inFolder) {
const prefixStack = ['']
const actors = {}
const folderStack = [inFolder]
while (folderStack.length > 0) {
const prefix = prefixStack.shift()
const folder = folderStack.shift()
for (const actor of folder.contents) {
if (
shim.user.isGM || actor.testUserPermission(
shim.user, CONST.FOUNDRY.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER)
) {
actors[`${prefix}${actor.name}`] = actor
}
}
for (const child of folder.children) {
const newPrefix = `${prefix}${child.folder.name} | `
prefixStack.push(newPrefix)
folderStack.push(child.folder)
}
}
return actors
}
}

22
scripts/module.js Normal file
View File

@ -0,0 +1,22 @@
import { api } from './api.js'
import { shapeChangeOnDismiss } from './powerEffects.js'
import { log } from './shim.js'
const moduleName = 'swade-mb-helpers'
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('ready', () => {
_checkModule('warpgate')
log('Initialized SWADE MB Helpers')
warpgate.event.watch(warpgate.EVENT.DISMISS, shapeChangeOnDismiss)
})

1421
scripts/powerEffects.js Normal file
View File

@ -0,0 +1,1421 @@
import { CONST, log, shim } from './shim.js'
class PowerEffect {
constructor (token, targets) {
this.token = token
this.targets = targets
this.effectDocs = []
this.menuData = {
inputs: [
{ type: 'header', label: `${this.name} Effect` },
{ type: 'info', label: `Apply ${this.name} Effect` },
{ type: 'header', label: 'Global Modifiers' },
{ type: 'checkbox', label: 'Glow (+1)' },
{ type: 'checkbox', label: 'Shroud (+1)' },
{ type: 'checkbox', label: 'Hinder (+1)' },
{ type: 'checkbox', label: 'Hurry (+1)' },
{ type: 'header', label: '---------------' }
],
buttons: [
{ label: 'Apply', value: 'apply' },
{ label: 'Apply with Raise', value: 'raise' },
{ label: 'Cancel', value: 'cancel' }
]
}
this.menuOptions = {
title: `${this.name} Effect`,
defaultButton: 'Cancel',
options: {}
}
this.inputs = []
this.buttons = null
}
get name () {
return 'Unknown Power'
}
get durationRounds () {
return this.baseDurationRounds
}
get baseDurationRounds () {
return 5
}
async powerEffect () {
try {
await this.prepMenu()
} catch (e) {
log('Error preparing menu for power effect: ' + e.toString())
return
}
const { buttons, inputs } = await shim.warpgateMenu(
this.menuData, this.menuOptions)
this.buttons = buttons
this.inputs = inputs
if (this.buttons && this.buttons !== 'cancel') {
this.globalModifierEffects()
await this.prepResult()
await this.applyResult()
}
}
async prepMenu () {
}
async prepResult () {
}
async applyResult () {
for (const target of this.targets) {
shim.applyActiveEffects(target, this.effectDocs)
}
}
static modEffectDoc (icon, name, key, value, durationRounds) {
return shim.createEffectDocument(icon, name, durationRounds, [
{
key,
mode: CONST.FOUNDRY.ACTIVE_EFFECT_MODES.ADD,
value,
priority: 0
}
])
}
static glow (durationRounds) {
return PowerEffect.modEffectDoc(
'icons/magic/light/orb-shadow-blue.webp',
'Glow', '@Skill{Stealth}[system.die.modifier]', -2, durationRounds)
}
static shroud (durationRounds) {
return PowerEffect.modEffectDoc(
'icons/magic/perception/shadow-stealth-eyes-purple.webp',
'Shroud', '@Skill{Stealth}[system.die.modifier]', 1, durationRounds)
}
static hinder (durationRounds) {
return PowerEffect.modEffectDoc(
'icons/magic/control/debuff-chains-shackle-movement-red.webp',
'Hinder', 'system.stats.speed.value', -2, durationRounds)
}
static hurry (durationRounds) {
return PowerEffect.modEffectDoc(
'icons/skills/movement/feet-winged-sandals-tan.webp',
'Hurry', 'system.stats.speed.value', 2, durationRounds)
}
globalModifierEffects () {
this.inputIndex = 8
if (this.inputs[3]) { // glow
this.effectDocs.push(PowerEffect.glow(this.durationRounds))
}
if (this.inputs[4]) { // shroud
this.effectDocs.push(PowerEffect.shroud(this.durationRounds))
}
if (this.inputs[5]) { // hinder
this.effectDocs.push(PowerEffect.hinder(this.durationRounds))
}
if (this.inputs[6]) { // hurry
this.effectDocs.push(PowerEffect.hurry(this.durationRounds))
}
}
}
class TargetedPowerEffect extends PowerEffect {
constructor (token, targets) {
super(token, targets)
const targetList = this.targets.map(t => t.name).join(', ')
this.menuData.inputs[1] = {
type: 'info',
label: `Apply ${this.name} Effect to ${targetList}`
}
}
async powerEffect () {
if (this.targets.length < 1) {
shim.notifications.error(`No target selected for ${this.name}`)
return
}
super.powerEffect()
}
}
class LingeringDamagePowerEffect extends TargetedPowerEffect {
get baseDurationRounds () {
return 1
}
async prepMenu () {
this.menuData.inputs.splice(this.menuData.inputs.length - 1, 0, {
type: 'checkbox', label: 'Lingering Damage (+2)'
})
}
globalModifierEffects () {
super.globalModifierEffects()
this.inputIndex += 1
if (this.inputs[7]) { // lingering damage
const doc = shim.createEffectDocument(
'icons/magic/death/skull-poison-green.webp',
`Lingering Damage (${this.name})`,
1
)
doc.flags.swade.expiration = CONST.SWADE.STATUS_EFFECT_EXPIRATION.StartOfTurnPrompt
this.effectDocs.push(doc)
}
}
}
class ArcaneProtectionEffect extends TargetedPowerEffect {
get name () {
return 'Arcane Protection'
}
get baseDurationRounds () {
return 5
}
async prepMenu () {
this.menuData.inputs.push(
{ type: 'checkbox', label: 'Greater', options: false })
}
async prepResult () {
const greater = !!this.inputs[this.inputIndex]
const raise = this.buttons === 'raise'
const amount = (raise ? -4 : -2) + (greater ? -2 : 0)
const icon = 'icons/magic/defensive/shield-barrier-flaming-pentagon-blue.webp'
const name = `${greater ? 'Greater ' : ''}Arcane Protection (${raise ? 'major, ' : ''}${amount})`
this.effectDocs.push(
shim.createEffectDocument(icon, name, this.durationRounds, []))
}
}
class BlastEffect extends LingeringDamagePowerEffect {
get name () {
return 'Blast'
}
}
class BlindEffect extends TargetedPowerEffect {
async prepMenu (token, targets) {
this.menuData.inputs.push({
type: 'checkbox',
label: 'Strong (+1 point)',
options: false
})
}
get name () {
return 'Blind'
}
get baseDurationRounds () {
return 1
}
async prepResult () {
const raise = (this.buttons === 'raise')
const strong = !!this.inputs[this.inputIndex]
const icon = 'icons/skills/wounds/injury-eyes-blood-red.webp'
const changes = [
{
key: 'system.stats.globalMods.trait',
mode: CONST.FOUNDRY.ACTIVE_EFFECT_MODES.ADD,
value: '-2',
priority: 0
}
]
this.effectDocs.push(
shim.createEffectDocument(
icon, `minor Blindness (Vigor ${strong ? '-2 ' : ''}ends)`,
this.durationRounds, changes))
if (raise) {
this.effectDocs.push(
shim.createEffectDocument(
icon, `major Blindness (Vigor ${strong ? '-2 ' : ''}ends)`,
this.durationRounds, changes)
)
}
}
}
class BurrowEffect extends TargetedPowerEffect {
get name () { return 'Burrow' }
get baseDurationRounds () { return 5 }
async prepResult () {
const raise = (this.buttons === 'raise')
const icon = 'icons/magic/earth/projectile-stone-landslide.webp'
this.effectDocs.push(
shim.createEffectDocument(
icon,
`${raise ? 'major' : 'minor'} ${this.name}`,
this.durationRounds,
[])
)
}
}
class BoltEffect extends LingeringDamagePowerEffect {
get name () {
return 'Bolt'
}
}
class BoostLowerTraitEffect extends TargetedPowerEffect {
get name () {
return 'Boost/Lower Trait'
}
get baseDurationRounds () {
if (!this.inputs) {
return 1
}
if (this.inputs[this.inputs.length - 4]) { // Boost
return 5
}
return 1 // Lower
}
async prepMenu () {
let traitOptions = ['Agility', 'Smarts', 'Spirit', 'Strength', 'Vigor']
const allSkills = []
const traits = {}
for (const traitName of traitOptions) {
const lower = traitName.toLowerCase()
traits[traitName] = {
name: traitName,
type: 'attribute',
modkey: `system.attributes.${lower}.die.modifier`,
diekey: `system.attributes.${lower}.die.sides`
}
}
for (const token of this.targets) {
const skills = token.actor.items.filter(item => item.type === 'skill')
for (const skill of skills) {
const name = skill.name
traits[name] = {
name,
type: 'skill',
modkey: `@Skill{${name}}[system.die.modifier]`,
diekey: `@Skill{${name}}[system.die.sides]`
}
if (name !== 'Unskilled' && !allSkills.find(v => v === name)) {
allSkills.push(name)
}
}
traitOptions = traitOptions.concat(allSkills.sort())
}
this.menuData.inputs = this.menuData.inputs.concat(
{ type: 'select', label: 'Trait', options: traitOptions },
{ type: 'info', label: 'Boost or Lower?' },
{ type: 'radio', label: 'Boost', options: ['isBoost', true] },
{ type: 'radio', label: 'Lower', options: ['isBoost', false] },
{ type: 'checkbox', label: 'Greater', options: false },
{ type: 'checkbox', label: 'Strong (lower only)', options: false }
)
this.traits = traits
}
async prepResult () {
const raise = (this.buttons === 'raise')
const direction = this.inputs[this.inputs.length - 4] ? 'Boost' : 'Lower'
const durationRounds = (direction === 'Boost' ? 5 : 1)
const icon = (direction === 'Boost'
? 'icons/magic/life/cross-embers-glow-yellow-purple.webp'
: 'icons/magic/movement/chevrons-down-yellow.webp')
const trait = this.traits[this.inputs[this.inputIndex]]
const greater = !!this.inputs[this.inputIndex + 4]
const strong = !!this.inputs[this.inputIndex + 5]
let namePart = `${direction} ${trait.name}`
const mods = []
if (direction === 'Lower') {
mods.push(`Spirit${strong ? '-2' : ''} ends`)
}
if (greater) {
mods.push('greater')
}
if (mods.length > 0) {
namePart = `${namePart} (${mods.join(', ')})`
}
const mode = CONST.FOUNDRY.ACTIVE_EFFECT_MODES.ADD
const modValue = (direction === 'Boost' ? '+2' : '-2')
const minorEffect = shim.createEffectDocument(
icon, `minor ${namePart}`, durationRounds, [
{ key: trait.diekey, mode, value: modValue, priority: 0 }
])
if (direction === 'Lower' && greater) {
minorEffect.changes.push({ key: trait.modkey, mode, value: modValue, priority: 0 })
}
const majorEffect = shim.createEffectDocument(
icon, `major ${namePart}`, durationRounds, [
{ key: trait.diekey, mode, value: modValue, priority: 0 }
])
this.effectDocs.push(minorEffect)
if (raise) { this.effectDocs.push(majorEffect) }
}
}
class BurstEffect extends LingeringDamagePowerEffect {
get name () {
return 'Burst'
}
}
class ConfusionEffect extends TargetedPowerEffect {
get name () {
return 'Confusion'
}
get baseDurationRounds () {
return 1
}
async prepMenu () {
this.menuData.inputs.push(
{ type: 'checkbox', label: 'Greater (adds Shaken)', options: false })
this.menuData.buttons = [
{ label: 'Distracted', value: 'distracted' },
{ label: 'Vulnerable', value: 'vulnerable' },
{ label: 'Raise (both)', value: 'raise' },
{ label: 'Cancel', value: 'cancel' }
]
}
async prepResult () {
const greater = !!this.inputs[this.inputIndex]
if (this.buttons === 'distracted' || this.buttons === 'raise') {
this.effectDocs.push(shim.getStatus('SWADE.Distr', 'Distracted'))
}
if (this.buttons === 'vulnerable' || this.buttons === 'raise') {
this.effectDocs.push(shim.getStatus('SWADE.Vuln', 'Vulnerable'))
}
if (greater) {
this.effectDocs.push(shim.getStatus('SWADE.Shaken', 'Shaken'))
}
}
}
class DarksightEffect extends TargetedPowerEffect {
get name () { return 'Darksight' }
get baseDurationRounds () { return 600 }
async prepMenu () {
this.menuData.inputs.push(
{ type: 'checkbox', label: '⭐ Greater (+2)', options: false })
}
async prepResult () {
const raise = this.buttons === 'raise'
const greater = !!this.inputs[this.inputIndex]
const icon = 'icons/magic/perception/eye-ringed-glow-angry-small-teal.webp'
this.effectDocs.push(
shim.createEffectDocument(
icon,
`${raise ? 'major' : 'minor'} ${this.name}${greater ? ' (greater)' : ''}`,
this.durationRounds,
[])
)
}
}
class DisguiseEffect extends TargetedPowerEffect {
get name () { return 'Disguise' }
get baseDurationRounds () { return 100 }
async prepResult () {
const raise = this.buttons === 'raise'
const icon = 'icons/skills/social/diplomacy-peace-alliance.webp'
this.effectDocs.push(
shim.createEffectDocument(
icon,
`${raise ? 'major' : 'minor'} ${this.name}`,
this.durationRounds,
[])
)
}
}
class DeflectionEffect extends TargetedPowerEffect {
get name () {
return 'Deflection'
}
get baseDurationRounds () {
return 5
}
async prepMenu () {
this.menuData.buttons = [
{ label: 'Melee', value: 'melee' },
{ label: 'Ranged', value: 'ranged' },
{ label: 'Raise (both)', value: 'raise' },
{ label: 'Cancel', value: 'cancel' }
]
}
async prepResult () {
const effectName = `Deflection (${this.buttons === 'raise' ? 'all' : this.buttons})`
const icon = 'icons/magic/defensive/shield-barrier-deflect-teal.webp'
this.effectDocs.push(shim.createEffectDocument(icon, effectName, this.durationRounds))
}
}
class DetectConcealArcanaEffect extends TargetedPowerEffect {
get name () { return 'Detect/Conceal Arcana' }
get baseDurationRounds () {
if (this.inputs?.[this.inputIndex + 2] === true) {
return 600
}
return 5
}
async prepMenu () {
this.menuData.inputs = this.menuData.inputs.concat(
{ type: 'info', label: 'Detect or Conceal?' },
{ type: 'radio', label: 'Detect', options: ['isDetect', true] },
{ type: 'radio', label: 'Conceal', options: ['isDetect', false] },
{ type: 'checkbox', label: 'Strong (+1, conceal only)', options: false }
)
}
async prepResult () {
const raise = (this.buttons === 'raise')
const isDetect = this.inputs[this.inputIndex + 1] === true
const strong = !isDetect && !!this.inputs[this.inputIndex + 3]
const icon = (isDetect
? 'icons/magic/perception/third-eye-blue-red.webp'
: 'icons/magic/perception/silhouette-stealth-shadow.webp')
const name = `${raise ? 'major ' : ''}${isDetect ? 'Detect' : 'Conceal'} Arcana${strong ? ' (strong)' : ''}`
const effect = shim.createEffectDocument(icon, name, this.durationRounds, [])
this.effectDocs.push(effect)
}
}
class EntangleEffect extends TargetedPowerEffect {
get name () {
return 'Entangle'
}
get baseDurationRounds () {
return 1
}
async prepMenu () {
this.menuData.inputs = this.menuData.inputs.concat([
{ type: 'radio', label: 'Not Damaging', options: ['dmg', true] },
{ type: 'radio', label: 'Damaging', options: ['dmg', false] },
{ type: 'radio', label: 'Deadly', options: ['dmg', false] },
{ type: 'checkbox', label: 'Tough', options: false }
])
this.menuData.buttons = [
{ label: 'Entangled', value: 'apply' },
{ label: 'Bound (raise)', value: 'raise' },
{ label: 'Cancel', value: 'cancel' }
]
}
async prepResult () {
const damage = (this.inputs[this.inputIndex + 1]
? '2d4'
: (this.inputs[this.inputIndex + 2] ? '2d6' : null))
const tough = !!this.inputs[this.inputIndex + 3]
const effectSearch = (this.buttons === 'raise' ? 'SWADE.Bound' : 'SWADE.Entangled')
const effectName = (this.buttons === 'raise' ? 'Bound' : 'Entangled')
const effect = shim.getStatus(effectSearch, effectName)
const extraIcon = 'icons/magic/nature/root-vine-barrier-wall-brown.webp'
const extraEffect = shim.createEffectDocument(extraIcon,
'Entangle Modifier', this.durationRounds, [])
if (damage) {
extraEffect.name = `${extraEffect.name} - ${damage} dmg`
}
if (tough) {
extraEffect.name = `Tough ${extraEffect.name}`
}
this.effectDocs.push(effect)
if (damage || tough) {
this.effectDocs.push(extraEffect)
}
}
}
class IntangibilityEffect extends TargetedPowerEffect {
get name () {
return 'Intangility'
}
get baseDurationRounds () {
if (!this.inputs) {
return 5
}
if (this.inputs[this.inputs.length - 1]) { // Duration
return 50
}
return 5 // no duration
}
async prepMenu () {
this.menuData.inputs.push({ type: 'checkbox', label: 'Duration', options: false })
this.menuData.buttons = [
{ label: 'Apply', value: 'apply' },
{ label: 'Cancel', value: 'cancel' }
]
}
async prepResult () {
const icon = 'icons/magic/control/debuff-energy-hold-levitate-blue-yellow.webp'
const effect = shim.createEffectDocument(icon, this.name, this.durationRounds, [])
this.effectDocs.push(effect)
}
}
class InvisibilityEffect extends TargetedPowerEffect {
get name () {
return 'Invisiblity'
}
get baseDurationRounds () {
if (!this.inputs) {
return 5
}
if (this.inputs[this.inputs.length - 1]) { // Duration
return 50
}
return 5 // no duration
}
async prepMenu () {
this.menuData.inputs.push({ type: 'checkbox', label: 'Duration', options: false })
}
async prepResult () {
const effect = shim.getStatus('EFFECT.StatusInvisible', 'Invisible')
effect.duration = { rounds: this.durationRounds }
this.effectDocs.push(effect)
}
}
class ProtectionEffect extends TargetedPowerEffect {
get name () {
return 'Protection'
}
get baseDurationRounds () {
return 5
}
async prepMenu () {
this.menuData.buttons = [
{ label: 'Apply (+2 armor)', value: 'apply' },
{ label: 'Apply with raise (+2 toughness)', value: 'raise' },
{ label: 'Cancel', value: 'cancel' }
]
}
async prepResult () {
const effect = shim.getStatus('SWADE.Protection', 'Protection')
effect.duration = { rounds: this.durationRounds }
const mode = CONST.FOUNDRY.ACTIVE_EFFECT_MODES.ADD
effect.changes = [
{ key: 'system.stats.toughness.armor', mode, value: 2, priority: 0 }
]
if (this.buttons === 'raise') {
effect.changes[0].key = 'system.stats.toughness.value'
}
this.effectDocs.push(effect)
}
}
class ShapeChangeEffect extends TargetedPowerEffect {
get actorFolderBase () {
return 'Morphables'
}
get tempActorFolder () {
return `${this.actorFolderBase}/Changed`
}
get actorFolder () {
return `${this.actorFolderBase}/${this.name}`
}
get name () {
return 'Shape Change'
}
get baseDurationRounds () {
if (this.increasedDuration ?? false) {
return 50
}
return 5
}
async prepFolders () {
const folders = []
const folderNames = [
this.actorFolder,
`${this.actorFolder} - Default`,
`${this.actorFolder}/Default`,
`${this.actorFolder} - ${this.token.name}`,
`${this.actorFolder} - ${this.token.actor.name}`,
`${this.actorFolder}/${this.token.name}`,
`${this.actorFolder}/${this.token.actor.name}`
]
for (const folderName of folderNames) {
const folder = shim.getActorFolderByPath(folderName)
if (folder) {
log(`Found actor folder ${folderName}`)
folders.push(folder)
}
}
if (folders.length > 1) {
folders.shift()
}
return folders
}
async prepActors () {
const folders = await this.prepFolders()
const actors = {}
for (const folder of folders) {
const folderActors = shim.getActorsInFolder(folder)
for (const key in folderActors) {
actors[key] = folderActors[key]
}
}
return actors
}
async prepMenu () {
const actors = await this.prepActors()
this.cancel = false
if (Object.keys(actors).length < 1) {
shim.notifications.error('No summonables found')
this.cancel = true
}
function actorData (key) {
return {
value: actors[key].id,
html: key
}
}
this.summonableActors = actors
this.menuData.inputs = this.menuData.inputs.concat([
{
type: 'select',
label: 'Turn into creature',
options: Object.keys(actors).filter(
k => !k.includes('_template')).sort().map(actorData)
}, {
type: 'checkbox',
label: 'Duration (+1, rounds to minutes)',
options: false
}
])
}
async prepResult () {
this.raise = (this.buttons === 'raise')
this.actorId = (this.inputs[this.inputIndex])
this.increasedDuration = (!!this.inputs[this.inputIndex + 1])
this.actor = shim.actors.get(this.actorId)
this.icon = this.targets[0].document.texture.src
const targetActor = this.targets[0].actor
this.protoDoc = await this.actor.getTokenDocument()
this.spawnOptions = {
controllingActor: this.targets[0].actor,
duplicates: 1,
updateOpts: {
embedded: {
Item: {
renderSheet: null
}
}
},
crosshairs: {
rememberControlled: true
}
}
const effectChanges = []
if (this.raise) {
for (const stat of ['vigor', 'strength']) {
effectChanges.push({
key: `system.attributes.${stat}.die.sides`,
mode: CONST.FOUNDRY.ACTIVE_EFFECT_MODES.ADD,
value: '+2',
priority: 0
})
}
}
this.effectDocs.push(
shim.createEffectDocument(
this.icon,
`Shape Change into ${this.protoDoc.name}`,
this.durationRounds, effectChanges)
)
this.spawnMutation = {
actor: {
name: `${this.targets[0].actor.name} (${this.actor.name} form)`,
system: {
attributes: {
smarts: { die: targetActor.system.attributes.smarts.die },
spirit: { die: targetActor.system.attributes.spirit.die }
},
wildcard: targetActor.system.wildcard
}
},
token: {
flags: {
'swade-mb-helpers.shapeChange.srcTokenId': this.targets[0].id
},
actorLink: false,
name: `${this.targets[0].name} (${this.protoDoc.name} form) `,
elevation: this.targets[0].document.elevation,
disposition: this.targets[0].document.disposition
},
embedded: { ActiveEffect: {}, Item: {} }
}
for (const doc of this.effectDocs) {
this.spawnMutation.embedded.ActiveEffect[doc.name] = doc
}
for (const doc of this.targets[0].actor.effects) {
this.spawnMutation.embedded.ActiveEffect[doc.name] = this.targets[0].actor.getEmbeddedDocument('ActiveEffect', doc.id)
}
for (const item of targetActor.items) {
if (item.type === 'skill' && ['smarts', 'spirit'].includes(item.system.attribute)) {
const doc = await this.targets[0].actor.getEmbeddedDocument('Item', item.id)
this.spawnMutation.embedded.Item[item.name] = doc
}
if (['power', 'edge', 'hindrance', 'action'].includes(item.type)) {
const doc = await this.targets[0].actor.getEmbeddedDocument('Item', item.id)
this.spawnMutation.embedded.Item[item.name] = doc
}
}
}
async applyResult () {
log('protoDoc', this.protoDoc)
log('spawnOptions', this.spawnOptions)
log('spawnMutation', this.spawnMutation)
const newTokenId = (await shim.warpgateSpawnAt(
this.targets[0].center,
this.protoDoc,
this.spawnMutation,
{},
this.spawnOptions
))[0]
await this.targets[0].document.setFlag('swade-mb-helpers', 'shapeChange', {
toId: newTokenId,
saved: {
alpha: this.targets[0].document.alpha,
hidden: this.targets[0].document.hidden,
x: this.targets[0].document.x,
y: this.targets[0].document.y,
elevation: this.targets[0].document.elevation
}
})
await this.targets[0].document.update({
hidden: true,
alpha: 0.05
})
}
}
class SmiteEffect extends TargetedPowerEffect {
get name () {
return 'Smite'
}
get baseDurationRounds () {
return 5
}
async prepMenu () {
this.menuData.inputs.push({
type: 'checkbox', label: 'Greater', options: false
})
const tokenWeapons = {}
let index = this.menuData.inputs.length - 1
for (const token of this.targets) {
index += 2
tokenWeapons[token.id] = index
this.menuData.inputs.push({ type: 'info', label: `<h2>${token.name}</h2>` })
const weapons = token.actor.items.filter(i => i.type === 'weapon').map(
i => { return { value: i.name, html: i.name } })
weapons.unshift({ value: '', html: '<i>None</i>' })
this.menuData.inputs.push({ type: 'select', label: token.name, options: weapons })
}
this.tokenWeapons = tokenWeapons
}
async prepResult () {
this.baseEffect = shim.getStatus('SWADE.Smite', 'Smite')
}
async applyResult () {
const mode = CONST.FOUNDRY.ACTIVE_EFFECT_MODES.ADD
const raise = (this.buttons === 'raise')
const greater = !!this.inputs[this.inputIndex]
const changeValue = (greater ? (raise ? '+6' : '+4') : (raise ? '+4' : '+2'))
const changeKey = 'system.stats.globalMods.damage'
for (const token of this.targets) {
const weaponName = this.inputs[this.tokenWeapons[token.id]]
const effectName = `Smite (${weaponName})`
const changes = [
{ key: changeKey, mode, value: changeValue, priority: 0 }
]
this.baseEffect.changes = changes
this.baseEffect.name = effectName
await shim.applyActiveEffects(token, [this.baseEffect].concat(this.effectDocs))
}
}
}
class SlothSpeedEffect extends TargetedPowerEffect {
get name () {
return 'Sloth/Speed'
}
get baseDurationRounds () {
if (this.inputs?.[this.inputIndex + 1] === true) {
return 1
}
return 5
}
async prepMenu () {
this.menuData.inputs = this.menuData.inputs.concat(
{ type: 'info', label: 'Sloth or Speed?' },
{ type: 'radio', label: 'Sloth', options: ['isSloth', true] },
{ type: 'radio', label: 'Speed', options: ['isSloth', false] },
{ type: 'checkbox', label: 'Dash (+2, speed only)', options: false },
{ type: 'checkbox', label: 'Quickness (+2, speed only)', options: false },
{ type: 'checkbox', label: 'Strong (+1, sloth only)', options: false }
)
}
async prepResult () {
const raise = (this.buttons === 'raise')
const isSloth = this.inputs[this.inputIndex + 1] === true
const icon = (isSloth
? 'icons/magic/control/debuff-chains-shackles-movement-blue.webp'
: 'icons/skills/movement/feet-winged-sandals-tan.webp'
)
const dash = !isSloth && !!this.inputs[this.inputIndex + 3]
const quickness = !isSloth && !!this.inputs[this.inputIndex + 4]
const strong = isSloth && !!this.inputs[this.inputIndex + 4]
const nameMods = []
if (raise) { nameMods.push('Major') }
if (dash) { nameMods.push('Dash') }
if (quickness) { nameMods.push('Quickness') }
if (strong) { nameMods.push('Strong') }
const nameModifier = (
`${nameMods.length > 0 ? ' (' : ''}` +
`${nameMods.join(', ')}${nameMods.length > 0 ? ')' : ''}`)
const name = `${isSloth ? 'Sloth' : 'Speed'}${nameModifier}`
const effect = shim.createEffectDocument(
icon, name, this.durationRounds, [
{
key: 'system.stats.speed.value',
mode: CONST.FOUNDRY.ACTIVE_EFFECT_MODES.MULTIPLY,
value: (isSloth ? 0.5 : 2),
priority: 0
}])
this.effectDocs.push(effect)
}
}
class SummonEffect extends PowerEffect {
ICON = 'icons/magic/symbols/runes-triangle-blue.webp'
get actorFolderBase () {
return 'Summonables'
}
get actorFolder () {
return `${this.actorFolderBase}/${this.name}`
}
get name () {
return 'Summon Creature'
}
get baseDurationRounds () {
return 5
}
async prepFolders () {
const folders = []
const folderNames = [
this.actorFolder,
`${this.actorFolder} - Default`,
`${this.actorFolder}/Default`,
`${this.actorFolder} - ${this.token.name}`,
`${this.actorFolder} - ${this.token.actor.name}`,
`${this.actorFolder}/${this.token.name}`,
`${this.actorFolder}/${this.token.actor.name}`
]
for (const folderName of folderNames) {
const folder = shim.getActorFolderByPath(folderName)
if (folder) {
log(`Found actor folder ${folderName}`)
folders.push(folder)
}
}
if (folders.length > 1) {
folders.shift()
}
return folders
}
async prepActors () {
const folders = await this.prepFolders()
const actors = {}
for (const folder of folders) {
const folderActors = shim.getActorsInFolder(folder)
for (const key in folderActors) {
actors[key] = folderActors[key]
}
}
return actors
}
async prepMenu () {
this.menuData.inputs[1].label = `${this.token.name} is summoning...`
const actors = await this.prepActors()
if (Object.keys(actors).length < 1) {
shim.notifications.error('No summonables found')
throw new Error('No summonables found')
}
function actorData (key) {
return {
value: actors[key].id,
html: key
}
}
this.summonableActors = actors
this.menuData.inputs = this.menuData.inputs.concat([
{
type: 'select',
label: 'Creature to summon',
options: Object.keys(actors).filter(
k => !k.includes('_template')).sort().map(actorData)
}, {
type: 'number',
label: 'Number to spawn (+half base cost per)',
options: 1
}, {
type: 'checkbox',
label: 'Add Increased Trait(s)? (+1 per trait)',
options: false
}
])
}
async prepResult () {
this.raise = (this.buttons === 'raise')
this.actorId = (this.inputs[this.inputIndex])
this.number = (this.inputs[this.inputIndex + 1])
this.actor = shim.actors.get(this.actorId)
this.icon = this.actor.prototypeToken.texture.src
this.protoDoc = await this.actor.getTokenDocument()
this.increasedTrait = !!(this.inputs[this.inputIndex + 2])
this.inputIndex += 3
this.spawnOptions = {
controllingActor: this.token.actor,
duplicates: this.number,
updateOpts: {
embedded: {
Item: {
renderSheet: null
}
}
},
crosshairs: {
icon: this.icon,
label: `Summon ${this.actor.name}`,
drawOutline: true,
rememberControlled: true
}
}
this.spawnMutation = {
actor: {
name: `${this.token.name}'s ${this.actor.name}`
},
token: {
actorLink: false,
name: `${this.token.name}'s ${this.protoDoc.name}`
},
embedded: { ActiveEffect: {}, Item: {} }
}
if (this.raise && ('raise_template' in this.summonableActors)) {
const raiseTemplate = this.summonableActors.raise_template
for (const item of raiseTemplate.items) {
const raiseItemDoc = await raiseTemplate.getEmbeddedDocument('Item', item.id)
this.spawnMutation.embedded.Item[item.name] = raiseItemDoc
}
}
for (const effectDocument of this.effectDocs) {
this.spawnMutation.embedded.ActiveEffect[effectDocument.name] = effectDocument
}
}
async prepAdditional () {
if (!this.increasedTrait) {
return
}
const traitMenuOptions = {
title: `${this.name} Summon Trait Increase`,
defaultButton: 'Cancel',
options: {}
}
const skillSet = new Set()
for (const skill of this.actor.items.filter(i => i.type === 'skill')) {
skillSet.add(skill.name)
}
for (const item of Object.values(this.spawnMutation.embedded.Item).filter(i => i.type === 'skill')) {
skillSet.add(item.name)
}
const skillList = Array.from(skillSet)
const attrList = ['Agility', 'Smarts', 'Spirit', 'Strength', 'Vigor']
skillList.sort()
const traitMenuData = {
inputs: [
{ type: 'header', label: 'Increase Attributes (+1 each)' }
],
buttons: [
{ label: 'Apply', value: 'apply' },
{ label: 'Increase no traits', value: 'cancel' }
]
}
traitMenuData.inputs = traitMenuData.inputs.concat(
attrList.map((x) => { return { type: 'checkbox', label: x, options: false } }))
traitMenuData.inputs.push({ type: 'header', label: 'Increase Skills (+1 each)' })
traitMenuData.inputs = traitMenuData.inputs.concat(
skillList.map((x) => { return { type: 'checkbox', label: x, options: false } }))
const { buttons, inputs } = await shim.warpgateMenu(traitMenuData, traitMenuOptions)
if (!buttons || buttons === 'cancel') {
return
}
const modKeys = []
for (let i = 0; i < attrList.length; i++) {
if (inputs[i + 1]) {
modKeys.push(`system.attributes.${attrList[i].toLowerCase()}.die.sides`)
}
}
for (let i = 0; i < skillList.length; i++) {
if (inputs[i + 7]) {
modKeys.push(`@Skill{${skillList[i]}}[system.die.sides]`)
}
}
const effectDoc = shim.createEffectDocument(
this.ICON, 'Increased Trait', this.durationRounds)
effectDoc.changes = modKeys.map(key => {
return {
key, mode: CONST.FOUNDRY.ACTIVE_EFFECT_MODES.ADD, value: '+2', priority: 0
}
})
this.spawnMutation.embedded.ActiveEffect[effectDoc.name] = effectDoc
}
async applyResult () {
await this.prepAdditional()
await shim.warpgateSpawn(this.protoDoc, this.spawnMutation, {}, this.spawnOptions)
log('protoDoc', this.protoDoc)
log('spawnOptions', this.spawnOptions)
log('spawnMutation', this.spawnMutation)
}
}
class SummonAllyEffect extends SummonEffect {
get name () {
return 'Summon Ally'
}
async prepMenu () {
await super.prepMenu()
this.menuData.inputs = this.menuData.inputs.concat([
{
type: 'checkbox',
label: 'Bite/Claw (+1)',
options: false
}, {
type: 'checkbox',
label: 'Up to 3 Combat Edges (+1 per)',
options: false
}, {
type: 'checkbox',
label: 'Flight (+3)',
options: false
}
])
}
async prepResult () {
await super.prepResult()
this.biteClaw = !!(this.inputs[this.inputIndex])
this.combatEdge = !!(this.inputs[this.inputIndex + 1])
this.flight = !!(this.inputs[this.inputIndex + 2])
await this.prepMirrorSelf()
}
async prepAdditional () {
await super.prepAdditional()
await this.prepBiteClaw()
await this.prepFlight()
await this.prepCombatEdge()
}
async prepCombatEdge () {
if (!this.combatEdge || !('combat-edge_template' in this.summonableActors)) {
return
}
const template = this.summonableActors['combat-edge_template']
const edges = template.items.filter(i => i.type === 'edge').map(i => i.name)
edges.sort()
edges.unshift('None')
const edgeMenuData = {
inputs: [
{ type: 'header', label: 'Choose Edges (+1 per choice)' },
{ type: 'select', label: 'Edge 1', options: edges },
{ type: 'select', label: 'Edge 2', options: edges },
{ type: 'select', label: 'Edge 3', options: edges }
],
buttons: [
{ label: 'Apply', value: 'apply' },
{ label: 'Add no edges', value: 'cancel' }
]
}
const edgeMenuOptions = {
title: `${this.name} Combat Edge Selection`,
defaultButton: 'Cancel',
options: {}
}
const { buttons, inputs } = await shim.warpgateMenu(edgeMenuData, edgeMenuOptions)
if (!buttons || buttons === 'cancel') {
return
}
for (let i = 1; i <= 3; i++) {
if (inputs[i] === 'None') {
continue
}
const edge = template.items.getName(inputs[i])
if (edge) {
const doc = await template.getEmbeddedDocument('Item', edge.id)
this.spawnMutation.embedded.Item[edge.name] = doc
}
}
}
async prepBiteClaw () {
if (!this.biteClaw || !('bite-claw_template' in this.summonableActors)) {
return
}
const template = this.summonableActors['bite-claw_template']
for (const item of template.items) {
const doc = await template.getEmbeddedDocument('Item', item.id)
this.spawnMutation.embedded.Item[item.name] = doc
}
}
async prepFlight () {
if (!this.flight || !('flight_template' in this.summonableActors)) {
return
}
const template = this.summonableActors.flight_template
for (const item of template.items) {
const doc = await template.getEmbeddedDocument('Item', item.id)
this.spawnMutation.embedded.Item[item.name] = doc
}
for (const effect of template.effects.values()) {
const doc = shim.ActiveEffect.fromSource(effect)
this.spawnMutation.embedded.ActiveEffect[effect.name] = doc
}
}
async prepMirrorSelf () {
if (this.actor.name !== 'Mirror Self') {
return
}
const mirrorActor = this.token.actor
this.icon = mirrorActor.prototypeToken.texture.src
this.spawnMutation.actor.system = mirrorActor.system.clone({
wildcard: false,
'fatigue.value': 0,
'wounds.value': 0,
'wounds.max': 0,
'bennies.max': 0,
'bennies.value': 0
})
this.spawnMutation.actor.name = `Mirror ${mirrorActor.name}`
this.spawnMutation.actor.img = mirrorActor.img
this.spawnMutation.token.name = `Mirror ${this.token.name}`
this.spawnMutation.token.texture = {
src: this.token.document.texture.src,
scaleX: this.token.document.texture.scaleX * -1,
scaleY: this.token.document.texture.scaleY
}
this.spawnOptions.crosshairs.icon = this.icon
const effectChanges = []
for (const mirrorItem of mirrorActor.items) {
if (mirrorItem.type === 'power' &&
(mirrorItem.system?.swid === 'summon-ally' || mirrorItem.name === 'Summon Ally')) {
continue
}
if (['weapon', 'armor', 'consumable', 'gear'].includes(mirrorItem.type)) {
continue
}
this.spawnMutation.embedded.Item[mirrorItem.name] = await mirrorActor.getEmbeddedDocument(
'Item', mirrorItem.id)
if (mirrorItem.type === 'skill') {
effectChanges.push({
key: `@Skill{${mirrorItem.name}}[system.die.sides]`,
mode: CONST.FOUNDRY.ACTIVE_EFFECT_MODES.ADD,
value: '-2',
priority: 0
})
}
}
this.spawnMutation.embedded.ActiveEffect['Mirror Self'] =
shim.createEffectDocument(this.ICON, 'Mirror Self',
this.durationRounds, effectChanges)
}
}
class SummonAnimalEffect extends SummonEffect {
get name () {
return 'Summon Animal'
}
}
class SummonMonsterEffect extends SummonEffect {
get name () {
return 'Summon Monster'
}
}
class SummonNaturesAllyEffect extends SummonEffect {
get name () {
return "Summon Nature's Ally"
}
}
class SummonPlanarAllyEffect extends SummonEffect {
get name () {
return 'Summon Planar Ally'
}
}
class SummonUndeadEffect extends SummonEffect {
get name () {
return 'Summon Undead'
}
}
const PowerClasses = {
'arcane protection': ArcaneProtectionEffect,
'arcane-protection': ArcaneProtectionEffect,
blast: BlastEffect,
blind: BlindEffect,
bolt: BoltEffect,
'boost/lower trait': BoostLowerTraitEffect,
'boostlower-trait': BoostLowerTraitEffect,
'boost trait': BoostLowerTraitEffect,
'boost-trait': BoostLowerTraitEffect,
burrow: BurrowEffect,
burst: BurstEffect,
'conceal arcana': DetectConcealArcanaEffect,
'conceal-arcana': DetectConcealArcanaEffect,
confusion: ConfusionEffect,
darksight: DarksightEffect,
deflection: DeflectionEffect,
'detect arcana': DetectConcealArcanaEffect,
'detect-arcana': DetectConcealArcanaEffect,
'detect/conceal aracana': DetectConcealArcanaEffect,
'detectconceal-aracana': DetectConcealArcanaEffect,
disguise: DisguiseEffect,
entangle: EntangleEffect,
intangibility: IntangibilityEffect,
invisibility: InvisibilityEffect,
'lower trait': BoostLowerTraitEffect,
'lower-trait': BoostLowerTraitEffect,
protection: ProtectionEffect,
'shape change': ShapeChangeEffect,
'shape-change': ShapeChangeEffect,
sloth: SlothSpeedEffect,
'sloth/speed': SlothSpeedEffect,
slothspeed: SlothSpeedEffect,
smite: SmiteEffect,
speed: SlothSpeedEffect,
'summon ally': SummonAllyEffect,
'summon-ally': SummonAllyEffect,
'summon animal': SummonAnimalEffect,
'summon-animal': SummonAnimalEffect,
'summon monster': SummonMonsterEffect,
'summon-monster': SummonMonsterEffect,
"summon nature's ally": SummonNaturesAllyEffect,
'summon-natures-ally': SummonNaturesAllyEffect,
'summon planar ally': SummonPlanarAllyEffect,
'summon-planar-ally': SummonPlanarAllyEffect,
'summon undead': SummonUndeadEffect,
'summon-undead': SummonUndeadEffect,
zombie: SummonUndeadEffect
}
export async function powerEffects (options = {}) {
const token = 'token' in options ? options.token : []
if (token === undefined || token === null) {
shim.notifications.error('Please select one token')
return
}
const targets = 'targets' in options ? Array.from(options.targets) : []
const item = 'item' in options ? options.item : null
const name = options?.name || item?.system?.swid || item?.name || null
const lcName = name.toLowerCase()
for (const name in PowerClasses) {
if (lcName.includes(name)) {
const runner = new PowerClasses[name](token, targets)
runner.powerEffect()
return
}
}
shim.notifications.error(`No power effect found for ${name}`)
}
export async function shapeChangeOnDismiss (data) {
if (shim.user.id !== data.userId) { return }
const dismissedToken = data.actorData.prototypeToken
const flags = dismissedToken.flags['swade-mb-helpers']?.shapeChange
const srcTokenId = flags?.srcTokenId
if (!srcTokenId) { return }
const scene = shim.scenes.get(data.sceneId)
const token = scene.tokens.get(srcTokenId)
if (!token) { return }
const saved = token.flags['swade-mb-helpers']?.shapeChange?.saved
if (saved) {
const update = {
alpha: saved.alpha,
hidden: saved.hidden,
x: dismissedToken.x,
y: dismissedToken.y,
elevation: dismissedToken.elevation
}
await token.update(update)
}
}

163
scripts/shim.js Normal file
View File

@ -0,0 +1,163 @@
export class CONST {
static get SWADE () {
return CONFIG.SWADE.CONST
}
static get FOUNDRY () {
return foundry.CONST
}
static get WARPGATE () {
return warpgate.CONST
}
}
export class shim {
static get ActiveEffect () {
return ActiveEffect
}
static get Actor () {
return Actor
}
static get folders () {
return game.folders
}
static get controlled () {
return canvas.tokens.controlled
}
static get targets () {
return game.user.targets
}
static get user () {
return game.user
}
static get notifications () {
return ui.notifications
}
static get actors () {
return game.actors
}
static get scenes () {
return game.scenes
}
static mergeObject (...args) {
return mergeObject(...args)
}
static getStatus (label, name, favorite = true) {
const effect = JSON.parse(JSON.stringify(
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
}
static createEffectDocument (icon, name, durationRounds, changes = null) {
if (changes === null) {
changes = []
}
return {
icon,
name,
duration: { rounds: durationRounds },
changes,
flags: {
swade: {
favorite: true,
expiration: CONST.SWADE.STATUS_EFFECT_EXPIRATION.EndOfTurnPrompt
}
}
}
}
static async applyActiveEffects (token, effectDocuments) {
const mutation = {
embedded: { ActiveEffect: {} }
}
const mutateOptions = {
permanent: true,
description: effectDocuments[effectDocuments.length - 1]?.name
}
for (const effectDocument of effectDocuments) {
mutation.embedded.ActiveEffect[effectDocument.name] = effectDocument
}
await warpgate.mutate(token.document, mutation, {}, mutateOptions)
}
static warpgateMenu (menuData, menuOptions) {
return warpgate.menu(menuData, menuOptions)
}
static warpgateSpawn (...args) {
return warpgate.spawn(...args)
}
static warpgateSpawnAt (...args) {
return warpgate.spawnAt(...args)
}
static getActorFolderByPath (path) {
const names = path.split('/')
if (names[0] === '') {
names.shift()
}
let name = names.shift()
let folder = shim.folders.filter(
f => f.type === 'Actor' && !f.folder
).find(f => f.name === name)
if (!folder) { return undefined }
while (names.length > 0) {
name = names.shift()
folder = folder.children.find(c => c.folder.name === name)
if (!folder) { return undefined }
folder = folder.folder
}
return folder
}
static getActorsInFolder (inFolder) {
const prefixStack = ['']
const actors = {}
const folderStack = [inFolder]
while (folderStack.length > 0) {
const prefix = prefixStack.shift()
const folder = folderStack.shift()
for (const actor of folder.contents) {
if (shim.user.isGM ||
actor.testUserPermission(
shim.user, CONST.FOUNDRY.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER)
) {
actors[`${prefix}${actor.name}`] = actor
}
}
for (const child of folder.children) {
const newPrefix = `${prefix}${child.folder.name} | `
prefixStack.push(newPrefix)
folderStack.push(child.folder)
}
}
return actors
}
}
export function log (...args) {
console.log('SWADE MB HELPERS |', ...args)
}

View File

@ -1,1283 +0,0 @@
{
"globalMappings": [
{
"id": "02qnAqdH",
"label": "Arcane Protection",
"expression": "Arcane Protection (-2) || Arcane Protection (major, -4) || Greater Arcane Protection (-4) || Greater Arcane Protection (major, -6)",
"codeExp": "",
"imgName": "",
"imgSrc": "",
"priority": 50,
"config": {},
"overlay": true,
"alwaysOn": false,
"disabled": false,
"overlayConfig": {
"id": "02qnAqdH",
"html": {
"template": "",
"style": "",
"listeners": ""
},
"parentID": "",
"ui": false,
"underlay": false,
"bottom": false,
"top": false,
"inheritTint": false,
"linkRotation": true,
"animation": {
"relative": false,
"rotate": false,
"duration": "5000",
"clockwise": true
},
"linkMirror": true,
"linkScale": false,
"linkDimensionsX": true,
"linkDimensionsY": true,
"linkOpacity": false,
"linkStageScale": false,
"loop": true,
"playOnce": false,
"img": "modules/JB2A_DnD5e/Library/Generic/Template/Circle/OutPulse/OutPulse_02_Regular_BlueWhite_Burst_600x600.webm",
"repeating": false,
"alpha": "1",
"tint": "",
"interpolateColor": {
"color2": "",
"prc": ""
},
"width": "1",
"height": "1",
"scaleX": "0.18",
"scaleY": "0.18",
"angle": "0",
"pOffsetX": "",
"pOffsetY": "",
"offsetX": "0",
"offsetY": "0",
"anchor": {
"x": 0.5,
"y": 0.5
},
"filter": "NONE",
"alwaysVisible": false,
"limitedToOwner": false,
"limitedUsers": [],
"limitOnHover": false,
"limitOnHighlight": false,
"limitOnControl": false,
"limitOnHUD": false,
"limitOnTarget": false,
"limitOnAnyTarget": false,
"limitOnEffect": "",
"limitOnProperty": "",
"text": {
"text": "",
"repeating": false,
"fontFamily": "Signika",
"fill": "#ffffff",
"interpolateColor": {
"color2": "",
"prc": ""
},
"fontSize": "36",
"fontWeight": "normal",
"align": "center",
"letterSpacing": "0",
"dropShadow": "true",
"strokeThickness": "1",
"stroke": "#111111",
"wordWrap": false,
"wordWrapWidth": "200",
"breakWords": false,
"maxHeight": "0",
"curve": {
"angle": "0",
"radius": "0",
"invert": false
}
},
"effect": "",
"imgLinked": false,
"interactivity": [],
"label": "Arcane Protection"
},
"group": "Default",
"i": 0
},
{
"id": "rrdhKai4",
"label": "Bound or Entangled",
"expression": "Bound||Entangled",
"codeExp": "",
"imgName": "",
"imgSrc": "",
"priority": 50,
"config": {},
"overlay": true,
"alwaysOn": false,
"disabled": false,
"overlayConfig": {
"id": "rrdhKai4",
"html": {
"template": "",
"style": "",
"listeners": ""
},
"parentID": "",
"ui": false,
"underlay": false,
"bottom": false,
"top": false,
"inheritTint": false,
"linkRotation": false,
"animation": {
"relative": false,
"rotate": false,
"duration": "5000",
"clockwise": true
},
"linkMirror": true,
"linkScale": false,
"linkDimensionsX": true,
"linkDimensionsY": true,
"linkOpacity": true,
"linkStageScale": false,
"loop": true,
"playOnce": false,
"img": "modules/JB2A_DnD5e/Library/1st_Level/Entangle/Entangle_01_Green_400x400.webm",
"repeating": false,
"alpha": "0.75",
"tint": "",
"interpolateColor": {
"color2": "",
"prc": ""
},
"width": "1",
"height": "1",
"scaleX": "0.27",
"scaleY": "0.27",
"angle": "0",
"pOffsetX": "",
"pOffsetY": "",
"offsetX": "0",
"offsetY": "0",
"anchor": {
"x": 0.5,
"y": 0.5
},
"filter": "NONE",
"alwaysVisible": false,
"limitedToOwner": false,
"limitedUsers": [],
"limitOnHover": false,
"limitOnHighlight": false,
"limitOnControl": false,
"limitOnHUD": false,
"limitOnTarget": false,
"limitOnAnyTarget": false,
"limitOnEffect": "",
"limitOnProperty": "",
"text": {
"text": "",
"repeating": false,
"fontFamily": "Signika",
"fill": "#FFFFFF",
"interpolateColor": {
"color2": "",
"prc": ""
},
"fontSize": "36",
"fontWeight": "normal",
"align": "center",
"letterSpacing": "0",
"dropShadow": "true",
"strokeThickness": "1",
"stroke": "#111111",
"wordWrap": false,
"wordWrapWidth": "200",
"breakWords": false,
"maxHeight": "0",
"curve": {
"angle": "0",
"radius": "0",
"invert": false
}
},
"shapes": [
{
"shape": {
"type": "rectangle",
"x": "0",
"y": "0",
"width": "100",
"height": "100",
"radius": "0"
},
"label": "",
"line": {
"width": "1",
"color": "#000000",
"alpha": "1"
},
"fill": {
"color": "#ffffff",
"alpha": "1",
"interpolateColor": {
"color2": "",
"prc": ""
}
},
"repeating": false
}
],
"effect": "",
"imgLinked": false,
"interactivity": [],
"label": "Bound or Entangled"
},
"group": "Default",
"i": 1
},
{
"id": "5EAavaok",
"label": "Conviction",
"expression": "actor.system.details.conviction.active=\"true\"",
"codeExp": "",
"imgName": "",
"imgSrc": "",
"priority": 50,
"config": {},
"overlay": true,
"alwaysOn": false,
"disabled": false,
"overlayConfig": {
"id": "5EAavaok",
"html": {
"template": "",
"style": "",
"listeners": ""
},
"parentID": "",
"ui": false,
"underlay": false,
"bottom": false,
"top": false,
"inheritTint": false,
"linkRotation": true,
"animation": {
"relative": false,
"rotate": false,
"duration": "5000",
"clockwise": true
},
"linkMirror": true,
"linkScale": false,
"linkDimensionsX": true,
"linkDimensionsY": true,
"linkOpacity": true,
"linkStageScale": false,
"loop": true,
"playOnce": false,
"img": "modules/JB2A_DnD5e/Library/1st_Level/Bless/Bless_01_Regular_Yellow_Loop_400x400.webm",
"repeating": false,
"alpha": "1",
"tint": "",
"interpolateColor": {
"color2": "",
"prc": ""
},
"width": "1",
"height": "1",
"scaleX": ".4",
"scaleY": ".4",
"angle": "1",
"pOffsetX": "",
"pOffsetY": "",
"offsetX": "0",
"offsetY": "0",
"anchor": {
"x": 0.5,
"y": 0.5
},
"filter": "DropShadowFilter",
"filterOptions": {
"rotation": 45,
"distance": 0,
"color": "#000000",
"alpha": 0.86,
"shadowOnly": false,
"blur": 2.3,
"quality": 0
},
"alwaysVisible": false,
"limitedToOwner": false,
"limitedUsers": [],
"limitOnHover": false,
"limitOnHighlight": false,
"limitOnControl": false,
"limitOnHUD": false,
"limitOnTarget": false,
"limitOnAnyTarget": false,
"limitOnEffect": "",
"limitOnProperty": "",
"text": {
"text": "",
"repeating": false,
"fontFamily": "Signika",
"fill": "#ffffff",
"interpolateColor": {
"color2": "",
"prc": ""
},
"fontSize": "36",
"fontWeight": "normal",
"align": "center",
"letterSpacing": "0",
"dropShadow": "true",
"strokeThickness": "1",
"stroke": "#111111",
"wordWrap": false,
"wordWrapWidth": "200",
"breakWords": false,
"maxHeight": "0",
"curve": {
"angle": "0",
"radius": "0",
"invert": false
}
},
"effect": "",
"imgLinked": false,
"interactivity": [],
"label": "Conviction"
},
"group": "Default",
"i": 2
},
{
"id": "gRwsZcZK",
"label": "Deflection",
"expression": "Deflection (melee) || Deflection (range) || Deflection (all)",
"codeExp": "",
"imgName": "",
"imgSrc": "",
"priority": 50,
"config": {},
"overlay": true,
"alwaysOn": false,
"disabled": false,
"overlayConfig": {
"id": "gRwsZcZK",
"html": {
"template": "",
"style": "",
"listeners": ""
},
"parentID": "",
"ui": false,
"underlay": false,
"bottom": false,
"top": false,
"inheritTint": false,
"linkRotation": true,
"animation": {
"relative": false,
"rotate": false,
"duration": "5000",
"clockwise": true
},
"linkMirror": true,
"linkScale": false,
"linkDimensionsX": true,
"linkDimensionsY": true,
"linkOpacity": true,
"linkStageScale": false,
"loop": true,
"playOnce": false,
"img": "modules/JB2A_DnD5e/Library/Generic/Marker/MarkerShield_03_Regular_Green_400x400.webm",
"repeating": false,
"alpha": "1",
"tint": "",
"interpolateColor": {
"color2": "",
"prc": ""
},
"width": "1",
"height": "1",
"scaleX": "0.45",
"scaleY": "0.45",
"angle": "0",
"pOffsetX": "",
"pOffsetY": "",
"offsetX": "0",
"offsetY": "0",
"anchor": {
"x": 0.5,
"y": 0.5
},
"filter": "OutlineFilter",
"filterOptions": {
"thickness": 1,
"color": "#000000",
"quality": 0.1
},
"alwaysVisible": false,
"limitedToOwner": false,
"limitedUsers": [],
"limitOnHover": false,
"limitOnHighlight": false,
"limitOnControl": false,
"limitOnHUD": false,
"limitOnTarget": false,
"limitOnAnyTarget": false,
"limitOnEffect": "",
"limitOnProperty": "",
"text": {
"text": "",
"repeating": false,
"fontFamily": "Signika",
"fill": "#FFFFFF",
"interpolateColor": {
"color2": "",
"prc": ""
},
"fontSize": "36",
"fontWeight": "normal",
"align": "center",
"letterSpacing": "0",
"dropShadow": "true",
"strokeThickness": "1",
"stroke": "#111111",
"wordWrap": false,
"wordWrapWidth": "200",
"breakWords": false,
"maxHeight": "0",
"curve": {
"angle": "0",
"radius": "0",
"invert": false
}
},
"effect": "",
"imgLinked": false,
"interactivity": [],
"label": "Deflection"
},
"group": "Default",
"i": 3
},
{
"id": "toTYr3DQ",
"label": "Distracted",
"expression": "Distracted && \\!Stunned",
"codeExp": "",
"imgName": "",
"imgSrc": "",
"priority": 50,
"config": {},
"overlay": true,
"alwaysOn": false,
"disabled": false,
"overlayConfig": {
"id": "toTYr3DQ",
"html": {
"template": "",
"style": "",
"listeners": ""
},
"parentID": "",
"ui": false,
"underlay": false,
"bottom": false,
"top": false,
"inheritTint": false,
"linkRotation": true,
"animation": {
"relative": false,
"rotate": true,
"duration": "30000",
"clockwise": true
},
"linkMirror": true,
"linkScale": false,
"linkDimensionsX": true,
"linkDimensionsY": true,
"linkOpacity": true,
"linkStageScale": false,
"loop": true,
"playOnce": false,
"img": "modules/JB2A_DnD5e/Library/Generic/Particles/ParticlesInward02_04_Regular_GreenYellow_400x400.webm",
"repeating": false,
"alpha": "1",
"tint": "",
"interpolateColor": {
"color2": "",
"prc": ""
},
"width": "1",
"height": "1",
"scaleX": ".3",
"scaleY": ".3",
"angle": "0",
"pOffsetX": "",
"pOffsetY": "",
"offsetX": "0",
"offsetY": "0",
"anchor": {
"x": 0.5,
"y": 0.5
},
"filter": "OutlineFilter",
"filterOptions": {
"thickness": 1,
"color": "#000000",
"quality": 0
},
"alwaysVisible": false,
"limitedToOwner": false,
"limitedUsers": [],
"limitOnHover": false,
"limitOnHighlight": false,
"limitOnControl": false,
"limitOnHUD": false,
"limitOnTarget": false,
"limitOnAnyTarget": false,
"limitOnEffect": "",
"limitOnProperty": "",
"text": {
"text": "",
"repeating": false,
"fontFamily": "Signika",
"fill": "#FFFFFF",
"interpolateColor": {
"color2": "",
"prc": ""
},
"fontSize": "36",
"fontWeight": "normal",
"align": "center",
"letterSpacing": "0",
"dropShadow": "true",
"strokeThickness": "1",
"stroke": "#111111",
"wordWrap": false,
"wordWrapWidth": "200",
"breakWords": false,
"maxHeight": "0",
"curve": {
"angle": "0",
"radius": "0",
"invert": false
}
},
"effect": "",
"imgLinked": false,
"interactivity": [],
"label": "Distracted"
},
"group": "Default",
"i": 4
},
{
"id": "KtequnXd",
"label": "Flying",
"expression": "Flying",
"codeExp": "",
"imgName": "",
"imgSrc": "",
"priority": 50,
"config": {
"tv_script": {
"onApply": "",
"onRemove": "",
"tmfxPreset": "dropshadow"
}
},
"overlay": false,
"alwaysOn": false,
"disabled": false,
"overlayConfig": {
"id": "KtequnXd",
"label": "Flying"
},
"group": "Default",
"i": 5
},
{
"id": "eO68BGDl",
"label": "Glow",
"expression": "Glow",
"codeExp": "",
"imgName": "",
"imgSrc": "",
"priority": 50,
"config": {
"light": {
"dim": 0.25,
"bright": 0,
"color": "#1c71d8",
"alpha": 0.4,
"animation": {
"type": "sunburst",
"speed": 3,
"intensity": 1
}
}
},
"overlay": true,
"alwaysOn": false,
"disabled": false,
"overlayConfig": {
"id": "eO68BGDl",
"html": {
"template": "",
"style": "",
"listeners": ""
},
"parentID": "",
"ui": false,
"underlay": false,
"bottom": false,
"top": false,
"inheritTint": false,
"linkRotation": true,
"animation": {
"relative": false,
"rotate": true,
"duration": "5000",
"clockwise": false
},
"linkMirror": true,
"linkScale": false,
"linkDimensionsX": true,
"linkDimensionsY": true,
"linkOpacity": false,
"linkStageScale": false,
"loop": true,
"playOnce": false,
"img": "modules/JB2A_DnD5e/Library/Cantrip/Dancing_Lights/DancingLights_01_Yellow_200x200.webm",
"repeating": false,
"alpha": "1",
"tint": "",
"interpolateColor": {
"color2": "",
"prc": ""
},
"width": "1",
"height": "1",
"scaleX": "0.15",
"scaleY": "0.15",
"angle": "0",
"pOffsetX": "",
"pOffsetY": "",
"offsetX": "0",
"offsetY": "0.52",
"anchor": {
"x": 0.5,
"y": 0.5
},
"filter": "NONE",
"alwaysVisible": false,
"limitedToOwner": false,
"limitedUsers": [],
"limitOnHover": false,
"limitOnHighlight": false,
"limitOnControl": false,
"limitOnHUD": false,
"limitOnTarget": false,
"limitOnAnyTarget": false,
"limitOnEffect": "",
"limitOnProperty": "",
"text": {
"text": "",
"repeating": false,
"fontFamily": "Signika",
"fill": "#FFFFFF",
"interpolateColor": {
"color2": "",
"prc": ""
},
"fontSize": "36",
"fontWeight": "normal",
"align": "center",
"letterSpacing": "0",
"dropShadow": "true",
"strokeThickness": "1",
"stroke": "#111111",
"wordWrap": false,
"wordWrapWidth": "200",
"breakWords": false,
"maxHeight": "0",
"curve": {
"angle": "0",
"radius": "0",
"invert": false
}
},
"effect": "",
"imgLinked": false,
"interactivity": [],
"label": "Glow"
},
"group": "Default",
"i": 6
},
{
"id": "1po9hq1m",
"label": "Protection",
"expression": "Protection",
"codeExp": "",
"imgName": "",
"imgSrc": "",
"priority": 50,
"config": {},
"overlay": true,
"alwaysOn": false,
"disabled": false,
"overlayConfig": {
"id": "1po9hq1m",
"html": {
"template": "",
"style": "",
"listeners": ""
},
"parentID": "",
"ui": false,
"underlay": false,
"bottom": false,
"top": false,
"inheritTint": false,
"linkRotation": true,
"animation": {
"relative": false,
"rotate": false,
"duration": "5000",
"clockwise": true
},
"linkMirror": true,
"linkScale": false,
"linkDimensionsX": true,
"linkDimensionsY": true,
"linkOpacity": false,
"linkStageScale": false,
"loop": true,
"playOnce": false,
"img": "modules/JB2A_DnD5e/Library/1st_Level/Shield/Shield_01_Regular_Blue_Loop_400x400.webm",
"repeating": false,
"alpha": "1",
"tint": "",
"interpolateColor": {
"color2": "",
"prc": ""
},
"width": "1",
"height": "1",
"scaleX": ".35",
"scaleY": ".35",
"angle": "0",
"pOffsetX": "",
"pOffsetY": "",
"offsetX": "0",
"offsetY": "0",
"anchor": {
"x": 0.5,
"y": 0.5
},
"filter": "NONE",
"alwaysVisible": false,
"limitedToOwner": false,
"limitedUsers": [],
"limitOnHover": false,
"limitOnHighlight": false,
"limitOnControl": false,
"limitOnHUD": false,
"limitOnTarget": false,
"limitOnAnyTarget": false,
"limitOnEffect": "",
"limitOnProperty": "",
"text": {
"text": "",
"repeating": false,
"fontFamily": "Signika",
"fill": "#FFFFFF",
"interpolateColor": {
"color2": "",
"prc": ""
},
"fontSize": "36",
"fontWeight": "normal",
"align": "center",
"letterSpacing": "0",
"dropShadow": "true",
"strokeThickness": "1",
"stroke": "#111111",
"wordWrap": false,
"wordWrapWidth": "200",
"breakWords": false,
"maxHeight": "0",
"curve": {
"angle": "0",
"radius": "0",
"invert": false
}
},
"effect": "",
"imgLinked": false,
"interactivity": [],
"label": "Protection"
},
"group": "Default",
"i": 7
},
{
"id": "mwFtNKpD",
"label": "Shaken",
"expression": "Shaken",
"codeExp": "",
"imgName": "",
"imgSrc": "",
"priority": 50,
"config": {},
"overlay": true,
"alwaysOn": false,
"disabled": false,
"overlayConfig": {
"id": "mwFtNKpD",
"html": {
"template": "",
"style": "",
"listeners": ""
},
"parentID": "",
"ui": false,
"underlay": false,
"bottom": false,
"top": false,
"inheritTint": false,
"linkRotation": true,
"animation": {
"relative": false,
"rotate": false,
"duration": "30000",
"clockwise": true
},
"linkMirror": true,
"linkScale": false,
"linkDimensionsX": true,
"linkDimensionsY": true,
"linkOpacity": false,
"linkStageScale": false,
"loop": true,
"playOnce": false,
"img": "modules/JB2A_DnD5e/Library/Generic/Template/Circle/TemplateStunCircle_01_Regular_Purple_800x800.webm",
"repeating": false,
"alpha": "0.5",
"tint": "",
"interpolateColor": {
"color2": "",
"prc": ""
},
"width": "1",
"height": "1",
"scaleX": ".15",
"scaleY": ".15",
"angle": "0",
"pOffsetX": "",
"pOffsetY": "",
"offsetX": "0",
"offsetY": "0",
"anchor": {
"x": 0.5,
"y": 0.5
},
"filter": "DropShadowFilter",
"filterOptions": {
"rotation": 45,
"distance": 7,
"color": "#000000",
"alpha": 0.84,
"shadowOnly": false,
"blur": 2,
"quality": 0
},
"alwaysVisible": false,
"limitedToOwner": false,
"limitedUsers": [],
"limitOnHover": false,
"limitOnHighlight": false,
"limitOnControl": false,
"limitOnHUD": false,
"limitOnTarget": false,
"limitOnAnyTarget": false,
"limitOnEffect": "",
"limitOnProperty": "",
"text": {
"text": "Shaken",
"repeating": false,
"fontFamily": "Signika",
"fill": "#FFFFFF",
"interpolateColor": {
"color2": "",
"prc": ""
},
"fontSize": "36",
"fontWeight": "normal",
"align": "center",
"letterSpacing": "0",
"dropShadow": "true",
"strokeThickness": "1",
"stroke": "#111111",
"wordWrap": false,
"wordWrapWidth": "200",
"breakWords": false,
"maxHeight": "0",
"curve": {
"angle": "0",
"radius": "0",
"invert": false
}
},
"effect": "",
"imgLinked": false,
"interactivity": [],
"label": "Shaken"
},
"group": "Default",
"i": 8
},
{
"id": "BP0Xx8wD",
"label": "Shroud",
"expression": "Shroud",
"codeExp": "",
"imgName": "",
"imgSrc": "",
"priority": 50,
"config": {},
"overlay": true,
"alwaysOn": false,
"disabled": false,
"overlayConfig": {
"id": "BP0Xx8wD",
"html": {
"template": "",
"style": "",
"listeners": ""
},
"parentID": "",
"ui": false,
"underlay": false,
"bottom": false,
"top": false,
"inheritTint": false,
"linkRotation": true,
"animation": {
"relative": false,
"rotate": false,
"duration": "5000",
"clockwise": true
},
"linkMirror": true,
"linkScale": false,
"linkDimensionsX": true,
"linkDimensionsY": true,
"linkOpacity": true,
"linkStageScale": false,
"loop": true,
"playOnce": false,
"img": "modules/JB2A_DnD5e/Library/2nd_Level/Darkness/Opacities/Darkness_01_Black_75OPA_600x600.webm",
"repeating": false,
"alpha": "1",
"tint": "",
"interpolateColor": {
"color2": "",
"prc": ""
},
"width": "1",
"height": "1",
"scaleX": "0.17",
"scaleY": "0.17",
"angle": "0",
"pOffsetX": "",
"pOffsetY": "",
"offsetX": "0",
"offsetY": "0",
"anchor": {
"x": 0.5,
"y": 0.5
},
"filter": "NONE",
"alwaysVisible": false,
"limitedToOwner": false,
"limitedUsers": [],
"limitOnHover": false,
"limitOnHighlight": false,
"limitOnControl": false,
"limitOnHUD": false,
"limitOnTarget": false,
"limitOnAnyTarget": false,
"limitOnEffect": "",
"limitOnProperty": "",
"text": {
"text": "",
"repeating": false,
"fontFamily": "Signika",
"fill": "#ffffff",
"interpolateColor": {
"color2": "",
"prc": ""
},
"fontSize": "36",
"fontWeight": "normal",
"align": "center",
"letterSpacing": "0",
"dropShadow": "true",
"strokeThickness": "1",
"stroke": "#111111",
"wordWrap": false,
"wordWrapWidth": "200",
"breakWords": false,
"maxHeight": "0",
"curve": {
"angle": "0",
"radius": "0",
"invert": false
}
},
"effect": "",
"imgLinked": false,
"interactivity": [],
"label": "Shroud"
},
"group": "Default",
"i": 9
},
{
"id": "nOfPMsQp",
"label": "Stunned",
"expression": "Stunned",
"codeExp": "",
"imgName": "",
"imgSrc": "",
"priority": 50,
"config": {},
"overlay": true,
"alwaysOn": false,
"disabled": false,
"overlayConfig": {
"id": "nOfPMsQp",
"html": {
"template": "",
"style": "",
"listeners": ""
},
"parentID": "",
"ui": false,
"underlay": false,
"bottom": false,
"top": false,
"inheritTint": false,
"linkRotation": true,
"animation": {
"relative": false,
"rotate": false,
"duration": "5000",
"clockwise": true
},
"linkMirror": true,
"linkScale": false,
"linkDimensionsX": true,
"linkDimensionsY": true,
"linkOpacity": false,
"linkStageScale": false,
"loop": true,
"playOnce": false,
"img": "modules/JB2A_DnD5e/Library/Generic/UI/IconStun_01_Regular_Purple_200x200.webm",
"repeating": false,
"alpha": "1",
"tint": "",
"interpolateColor": {
"color2": "",
"prc": ""
},
"width": "1",
"height": "1",
"scaleX": "0.4",
"scaleY": "0.4",
"angle": "0",
"pOffsetX": "",
"pOffsetY": "",
"offsetX": "0",
"offsetY": "0",
"anchor": {
"x": 0.5,
"y": 0.5
},
"filter": "OutlineFilter",
"filterOptions": {
"thickness": 1,
"color": "#000000",
"quality": 0.1
},
"alwaysVisible": false,
"limitedToOwner": false,
"limitedUsers": [],
"limitOnHover": false,
"limitOnHighlight": false,
"limitOnControl": false,
"limitOnHUD": false,
"limitOnTarget": false,
"limitOnAnyTarget": false,
"limitOnEffect": "",
"limitOnProperty": "",
"text": {
"text": "",
"repeating": false,
"fontFamily": "Signika",
"fill": "#FFFFFF",
"interpolateColor": {
"color2": "",
"prc": ""
},
"fontSize": "36",
"fontWeight": "normal",
"align": "center",
"letterSpacing": "0",
"dropShadow": "true",
"strokeThickness": "1",
"stroke": "#111111",
"wordWrap": false,
"wordWrapWidth": "200",
"breakWords": false,
"maxHeight": "0",
"curve": {
"angle": "0",
"radius": "0",
"invert": false
}
},
"effect": "",
"imgLinked": false,
"interactivity": [],
"label": "Stunned"
},
"group": "Default",
"i": 10
},
{
"id": "J4GrRaxL",
"label": "Vulnerable",
"expression": "Vulnerable && \\!Stunned",
"codeExp": "",
"imgName": "",
"imgSrc": "",
"priority": 50,
"config": {},
"overlay": true,
"alwaysOn": true,
"disabled": false,
"overlayConfig": {
"id": "J4GrRaxL",
"html": {
"template": "",
"style": "",
"listeners": ""
},
"parentID": "",
"ui": false,
"underlay": false,
"bottom": false,
"top": false,
"inheritTint": false,
"linkRotation": true,
"animation": {
"relative": false,
"rotate": false,
"duration": "5200",
"clockwise": false
},
"linkMirror": true,
"linkScale": false,
"linkDimensionsX": true,
"linkDimensionsY": true,
"linkOpacity": false,
"linkStageScale": false,
"loop": true,
"playOnce": false,
"img": "modules/JB2A_DnD5e/Library/Generic/Marker/MarkerShieldCracked_03_Regular_Purple_400x400.webm",
"repeating": false,
"alpha": "1",
"tint": "",
"interpolateColor": {
"color2": "",
"prc": ""
},
"width": "1",
"height": "1",
"scaleX": ".36",
"scaleY": ".36",
"angle": "0",
"pOffsetX": "",
"pOffsetY": "",
"offsetX": "0",
"offsetY": "0",
"anchor": {
"x": 0.5,
"y": 0.5
},
"filter": "OutlineFilter",
"filterOptions": {
"thickness": 1,
"color": "#000000",
"quality": 0.1
},
"alwaysVisible": false,
"limitedToOwner": false,
"limitedUsers": [],
"limitOnHover": false,
"limitOnHighlight": false,
"limitOnControl": false,
"limitOnHUD": false,
"limitOnTarget": false,
"limitOnAnyTarget": false,
"limitOnEffect": "",
"limitOnProperty": "",
"text": {
"text": "",
"repeating": false,
"fontFamily": "Signika",
"fill": "#FFFFFF",
"interpolateColor": {
"color2": "",
"prc": ""
},
"fontSize": "36",
"fontWeight": "normal",
"align": "center",
"letterSpacing": "0",
"dropShadow": "true",
"strokeThickness": "1",
"stroke": "#111111",
"wordWrap": false,
"wordWrapWidth": "200",
"breakWords": false,
"maxHeight": "0",
"curve": {
"angle": "0",
"radius": "0",
"invert": false
}
},
"effect": "",
"imgLinked": false,
"interactivity": [],
"label": "Vulnerable"
},
"group": "Default",
"i": 11
}
]
}

View File

@ -1,111 +0,0 @@
{
"swade": {
"system": "swade",
"topology": "standard",
"quantity" : "quantity",
"aliases": {
"Candle (1 hr, 2\" radius)": "Candle",
"Torch (1 hour, 4\" radius)": "Torch",
"Lantern, bullseye (10\" cone)": "Lantern, bullseye",
"Lantern, hooded (6\" radius)": "Lantern, hooded"
},
"sources": {
"Candle (1 hr, 2\" radius)": {
"states": 2,
"light": [
{
"bright": 0.5, "dim": 2, "angle": 360, "color": "#e68805", "alpha": 0.15,
"animation": { "type": "torch", "speed": 2, "intensity": 4, "reverse": false }
}
]
},
"Everburning Torch": {
"states": 2,
"light": [
{
"bright": 0.5, "dim": 4, "angle": 360, "color": "#4dfbc2", "alpha": 0.15,
"animation": { "type": "torch", "speed": 2, "intensity": 4, "reverse": false }
}
]
},
"Lantern, bullseye (10\" cone)": {
"states": 3,
"light": [
{
"bright": 4, "dim": 10, "angle": 180, "color": "#e68805", "alpha": 0.15,
"animation": { "type": "torch", "speed": 2, "intensity": 4, "reverse": false }
},
{
"bright": 0, "dim": 3, "angle": 180, "color": "#e68805", "alpha": 0.15,
"animation": { "type": "torch", "speed": 2, "intensity": 4, "reverse": false }
}
]
},
"Lantern, hooded (6\" radius)": {
"states": 2,
"light": [
{
"bright": 3, "dim": 6, "angle": 360, "color": "#e68805", "alpha": 0.15,
"animation": { "type": "torch", "speed": 2, "intensity": 4, "reverse": false }
}
]
},
"Torch (1 hour, 4\" radius)": {
"states": 2,
"light": [
{
"bright": 0.5, "dim": 4, "angle": 360, "color": "#e68805", "alpha": 0.15,
"animation": { "type": "torch", "speed": 2, "intensity": 5, "reverse": false }
}
]
},
"Sunrod": {
"states": 2,
"light": [
{
"bright": 6, "dim": 12, "angle": 360, "color": "#f9e380", "alpha": 0.15,
"animation": { "type": "sunburst", "speed": 1, "intensity": 3, "reverse": false }
}
]
},
"Lamp, Small (3\" radius)": {
"states": 2,
"light": [
{
"bright": 1, "dim": 3, "angle": 360, "color": "#e9c40c", "alpha": 0.15,
"animation": { "type": "torch", "speed": 1, "intensity": 3, "reverse": false }
}
]
},
"Light / Darkness": {
"states": 3,
"light": [
{
"bright": 1, "dim": 3, "angle": 360, "color": "#e9c40c", "alpha": 0.15,
"animation": { "type": "torch", "speed": 1, "intensity": 3, "reverse": false }
},
{
"bright": 3, "dim": 10, "angle": 120, "color": "#e68805", "alpha": 0.15,
"animation": { "type": "torch", "speed": 2, "intensity": 4, "reverse": false }
}
]
},
"Lantern of Revealing": {
"light": [
{
"bright": 1, "dim": 5, "angle": 360, "color": "#a80092", "alpha": 0.15,
"animation": { "type": "starlight", "speed": 1, "intensity": 3, "reverse": false }
}
]
},
"Robe of Scintillating Colors": {
"light": [
{
"bright": 3, "dim": 5, "angle": 360, "color": "#888888", "alpha": 0.15,
"animation": { "type": "rainbowswirl", "speed": 1, "intensity": 2, "reverse": false }
}
]
}
}
}
}

View File

@ -1,6 +0,0 @@
{
"mbhelpers.settings.powerActorsCompendiumName": "Power Actors Compendium",
"mbhelpers.settings.powerActorsCompendiumHint": "Identifier of a compendium that holds all the actor helpers for powers. See the documentation for details on the structure of this compendium.",
"mbhelpers.settings.powersJournalName": "Powers Journal",
"mbhelpers.settings.powersJournalHint": "UUID of a helper journal for actor-based powers (summonables and morphables)."
}

View File

@ -1,23 +0,0 @@
import { log, moduleHelpers } from './globals.js';
import { requestFearRollFromTokens, requestRollFromTokens } from './helpers.js';
import { powers, PowerClasses, powerEffectsMenu } from './powers/powers.js';
import { setSummonCosts } from './powers/summonSupport.js';
export class api {
static registerFunctions() {
log('SWADE MB Helpers initialized');
const moduleName = 'swade-mb-helpers';
const mbSwadeApi = {
fearTable: moduleHelpers.fearTableHelper,
powerEffects: powers,
PowerClasses,
powerEffectsMenu,
requestFearRollFromTokens,
requestRollFromTokens,
rulesVersion: moduleHelpers.rulesVersion,
setSummonCosts,
};
game.modules.get(moduleName).api = mbSwadeApi;
game.mbSwade = mbSwadeApi;
}
}

View File

@ -1,95 +0,0 @@
export const moduleName = 'swade-mb-helpers';
export const settingKeys = {
powerActorsCompendium: 'powerActorsCompendium',
};
export function log(...args) {
console.log('SWADE MB HELPERS |', ...args);
}
export class moduleHelpers {
static _socket = null;
static get socket() {
return moduleHelpers._socket;
}
static getSetting(key) {
return game.settings.get(moduleName, key);
}
static async setSetting(key, value) {
return game.settings.get(moduleName, key, value);
}
static async registerSetting(key, metadata) {
return game.settings.register(moduleName, key, metadata);
}
static get rulesVersion() {
if (game.modules.get('swpf-core-rules')?.active) {
return 'swpf';
}
if (game.modules.get('swade-core-rules')?.active) {
return 'swade';
}
return 'system';
}
static get useVAE() {
return !!game.modules.get('visual-active-effects')?.active;
}
static getActorFolderByPath(path) {
const names = path.split('/');
if (names[0] === '') {
names.shift();
}
let name = names.shift();
let folder = game.folders.filter((f) => f.type === 'Actor' && !f.folder).find((f) => f.name === name);
if (!folder) {
return undefined;
}
while (names.length > 0) {
name = names.shift();
folder = folder.children.find((c) => c.folder.name === name);
if (!folder) {
return undefined;
}
folder = folder.folder;
}
return folder;
}
static getActorsInFolder(inFolder) {
const prefixStack = [''];
const actors = {};
const folderStack = [inFolder];
while (folderStack.length > 0) {
const prefix = prefixStack.shift();
const folder = folderStack.shift();
for (const actor of folder.contents) {
if (game.user.isGM || actor.testUserPermission(game.user, foundry.CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER)) {
actors[`${prefix}${actor.name}`] = actor;
}
}
for (const child of folder.children) {
const newPrefix = `${prefix}${child.folder.name} | `;
prefixStack.push(newPrefix);
folderStack.push(child.folder);
}
}
return actors;
}
static get fearTableHelper() {
switch (moduleHelpers.rulesVersion) {
case 'swade':
return game.swadeCore.macros.coreFearDialog; // defined as global by the swade module
case 'swpf':
return game.swpfCore.macros.coreFearDialog; // defined as global by the swpf module
}
throw new ReferenceError('No premium module active. No fear table found');
}
}

View File

@ -1,206 +0,0 @@
import { moduleHelpers } from './globals.js';
export async function requestFearRollFromTokens(tokens, options = {}) {
// tokens: list of tokens to request the roll from
// options:
// title: tile for the roll dialog. Will have "- {{ token name }}" appended
// flavour: flavor text for the roll card. Defaults to title
// fear: value of the fear modifier. Defaults to 0. Positive number.
const requestingUser = game.user;
const title = options?.title || `${requestingUser.name} requests a Fear check`;
const flavour = options?.flavour || options?.flavor || title;
const fear = options.fear || 0;
const rollOpts = {
title,
flavour,
mods: [{ label: 'Fear Penalty', value: Math.abs(fear) * -1, ignore: false }],
};
return requestRollFromTokens(tokens, 'attribute', 'spirit', rollOpts);
}
export function firstOwner(doc) {
// lifted from warpgate
// https://github.com/trioderegion/warpgate/blob/master/src/scripts/module.js
if (!doc) return undefined;
const corrected =
doc instanceof TokenDocument
? doc.actor
: doc instanceof foundry.canvas.placeables.Token
? doc.document.actor
: doc;
const permissionObject = foundry.utils.getProperty(corrected ?? {}, 'ownership');
const playerOwners = Object.entries(permissionObject)
.filter(([id, level]) => !game.users.get(id)?.isGM && game.users.get(id)?.active && level === 3)
.map(([id]) => id);
if (playerOwners.length > 0) {
return game.users.get(playerOwners[0]);
}
return firstGM();
}
export function firstGM() {
// lifted from warpgate
// https://github.com/trioderegion/warpgate/blob/master/src/scripts/module.js
return game.users?.find((u) => u.isGM && u.active);
}
export async function requestRollFromTokens(tokens, rollType, rollDesc, options = {}) {
// tokens: list of tokens to request a roll from
// rollType: 'attribute' or 'skill
// rollDesc: name of attribute or skill
// options:
// title: title for the roll dialog. Will have "- {{ token name }}"
// appended
// flavour: flavor text for the roll card. Defaults to title
// targetNumber: defaults to 4
// mods: list of modifiers {label: "", value: 0, ignore: false}
// modCallback: callback function that takes a token and returns a list of
// modifiers in the same format as modifiers, above
const requestingUser = game.user;
const title = options?.title || `${requestingUser.name} requests a ${rollDesc} roll`;
const flavour = options?.flavour || options?.flavor || title;
const targetNumber = options?.targetNumber || 4;
const promises = [];
for (const token of tokens) {
const owner = firstOwner(token.document);
const rollOpts = {
title: `${title} - ${token.name}`,
targetNumber,
flavour,
};
const additionalMods = [];
if ('mods' in options) {
for (const mod of options.mods) {
additionalMods.push(mod);
}
}
if ('modCallback' in options) {
const tokenMods = await options.modCallback(token);
for (const tm of tokenMods) {
additionalMods.push(tm);
}
}
if (additionalMods.length > 0) {
rollOpts.additionalMods = additionalMods;
}
promises.push(
moduleHelpers.socket.executeAsUser(
requestTokenRoll,
owner.id,
token.scene.id,
token.id,
rollType,
rollDesc,
rollOpts,
),
);
}
const results = (await Promise.allSettled(promises)).map((r) => r.value);
const contentExtra = targetNumber === 4 ? '' : ` vs TN: ${targetNumber}`;
const messageData = {
flavor: flavour,
speaker: { alias: 'Requested Roll Results' },
whisper: [...ChatMessage.getWhisperRecipients('GM'), requestingUser],
content: `<p>Results of ${rollDesc[0].toUpperCase()}${rollDesc.slice(1)} roll${contentExtra}:</p>
<table><thead><tr><th>Token</th><th>Roll</th><th>Result</th></tr></thead><tbody>`,
};
for (const result of results) {
const token = game.scenes.get(result.sceneId).tokens.get(result.tokenId);
const roll =
result.result instanceof CONFIG.Dice.SwadeRoll
? result.result
: CONFIG.Dice[result.result.class].fromData(result.result);
roll.targetNumber = targetNumber;
let textResult = '';
if (roll.successes === -1) {
textResult = 'CRITICAL FAILURE';
} else if (roll.successes === 0) {
textResult = 'failed';
} else if (roll.successes === 1) {
textResult = 'success';
} else {
textResult = `success and ${roll.successes - 1} raise${roll.successes > 2 ? 's' : ''}`;
}
messageData.content +=
'<tr>' +
`<th>${token.name}</th>` +
`<td>${roll ? roll.total : '<i>Canceled</i>'}</td>` +
`<td>${textResult}</td>` +
'</tr>';
}
messageData.content += '</tbody></table>';
ChatMessage.create(messageData, {});
return results;
}
export async function requestTokenRoll(sceneId, tokenId, rollType, rollDesc, options) {
const scene = game.scenes.get(sceneId);
const token = scene.tokens.get(tokenId);
let rollFunc = 'rollAttribute';
let rollId = rollDesc.toLowerCase();
if (rollType === 'skill') {
rollFunc = 'rollSkill';
rollId = token.actor.items
.filter((i) => i.type === 'skill')
.find((i) => i.system.swid === rollDesc.toLowerCase() || i.name.toLowerCase() === rollDesc.toLowerCase())?.id;
}
const result = await token.actor[rollFunc](rollId, options);
return { sceneId, tokenId, result };
}
function _getSceneToken(sceneId, tokenId) {
const scene = game.scenes.get(sceneId);
const token = scene.tokens.get(tokenId);
return token;
}
export async function addActiveEffectsToToken(sceneId, tokenId, effectDocuments) {
const token = _getSceneToken(sceneId, tokenId);
await token.actor.createEmbeddedDocuments('ActiveEffect', effectDocuments);
}
export async function deleteActiveEffectsFromToken(sceneId, tokenId, effectIds) {
const token = _getSceneToken(sceneId, tokenId);
await token.actor.deleteEmbeddedDocuments('ActiveEffect', effectIds);
}
export async function addItemsToToken(sceneId, tokenId, itemDocuments) {
const token = _getSceneToken(sceneId, tokenId);
await token.actor.createEmbeddedDocuments('Item', itemDocuments, { renderSheet: false });
}
export async function deleteItemsFromActor(actorUuid, itemIds) {
const actor = await fromUuid(actorUuid);
await actor.deleteEmbeddedDocuments('Item', itemIds);
}
export async function updateOwnedToken(sceneId, tokenId, updates, options = {}) {
const token = _getSceneToken(sceneId, tokenId);
return token.update(updates, options);
}
export async function deleteToken(sceneId, tokenId) {
const token = _getSceneToken(sceneId, tokenId);
return token.delete();
}
export function SwadeVAEbuttons(effect, buttons) {
if (['Bound', 'Entangled'].includes(effect?.name)) {
buttons.push({
label: 'Break Free (Athletics)',
callback: function () {
const skillId = effect.parent.items.find((i) => i.type === 'skill' && i.system.swid === 'athletics')?.id;
effect.parent.rollSkill(skillId, { flavor: 'Breaking Free' });
},
});
buttons.push({
label: 'Break Free (Strength -2)',
callback: function () {
effect.parent.rollAttribute('strength', {
flavor: 'Breaking Free',
additionalMods: [{ label: 'Breaking Free with Strength', value: -2 }],
});
},
});
}
}

View File

@ -1,63 +0,0 @@
import { PowerEffect } from './basePowers.js';
export class ArcaneProtectionEffect extends PowerEffect {
get name() {
return 'Arcane Protection';
}
get basePowerPoints() {
return 1;
}
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.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.greater;
const raise = this.data.raise;
const amount = this._penaltyAmount;
return `${greater ? 'Greater ' : ''}Arcane Protection (${raise ? 'major, ' : ''}${amount})`;
}
}

View File

@ -1,196 +0,0 @@
import { moduleHelpers, moduleName } from '../globals.js';
import { firstOwner, updateOwnedToken } from '../helpers.js';
import { ActorFolderEffect } from './basePowers.js';
export class BalefulPolymorphEffect extends ActorFolderEffect {
get actorFolderBase() {
return 'Morphables';
}
get name() {
return 'Baleful Polymorph';
}
get icon() {
return 'icons/magic/control/silhouette-hold-change-blue.webp';
}
get duration() {
return this.data.duration ? 50 : 5;
}
get isTargeted() {
return true;
}
get oneTarget() {
return true;
}
get isRaisable() {
return true;
}
get basePowerPoints() {
return 3;
}
get hasRange() {
return false;
}
actorValue(actor) {
const size = actor.system.stats.size;
const targetSize = this.targets[0].actor.system.stats.size;
return Math.abs(targetSize - size);
}
get modifiers() {
return [
...super.modifiers,
{
name: 'Duration',
type: 'checkbox',
value: 2,
id: 'duration',
epic: true,
effect: false,
},
{
name: 'Victim critical failure',
type: 'checkbox',
default: false,
id: 'critfail',
epic: false,
value: 0,
effect: false,
},
];
}
async parseValues() {
await super.parseValues();
this.target = this?.targets?.[0];
this.data.actorUpdates = {
name: `${this.target.actor.name} (${this.targetActor.name} form)`,
system: {
wildcard: this.target.actor.system.wildcard,
attributes: {},
},
};
const attrList = ['spirit'];
if (!this.data.critfail) {
attrList.push('smarts');
}
for (const stat of attrList) {
this.data.actorUpdates.system.attributes[stat] = {
die: this.target.actor.system.attributes[stat].die,
'wild-die': this.target.actor.system.attributes[stat]['wild-die'],
};
}
this.data.tokenUpdates = {
flags: {
[moduleName]: {
'shapeChange.srcTokenUuid': this.target.document.uuid,
'shapeChange.srcTokenId': this.target.document.id,
'shapeChange.srcTokenSceneId': this.target.scene.id,
},
},
actorLink: false,
name: `${this.target.name} (${this.targetActor.prototypeToken.name} form)`,
disposition: this.target.document.disposition,
sight: {
enabled: true,
},
};
this.data.embeddedUpdates = {
ActiveEffect: {},
Item: {},
};
for (const effect of this.target.actor.effects) {
const doc = foundry.utils.deepClone(await this.target.actor.getEmbeddedDocument('ActiveEffect', effect.id));
this.data.embeddedUpdates.ActiveEffect[effect.name] = doc;
}
}
async spawn() {
const target = this.target.document;
const size = target.parent.dimensions.size;
const protoWidth = this.targetActor.prototypeToken.width;
const protoHeight = this.targetActor.prototypeToken.height;
this.targetTokenDoc.updateSource({
x: target.x - ((protoWidth - target.width) * size) / 2,
y: target.y - ((protoHeight - target.height) * size) / 2,
elevation: target.elevation,
hidden: target.hidden,
});
return this.source.scene.createEmbeddedDocuments('Token', [this.targetTokenDoc]);
}
async apply() {
await super.apply();
const maintainDoc = await this.createMaintainEffect(this.data.maintId);
maintainDoc.flags[moduleName].targetIds = this.data.spawned.map((t) => t.id);
maintainDoc.flags[moduleName].shapeChangeSourceId = this.target.id;
maintainDoc.flags[moduleName].shapeChangeTempTokenId = this.data.spawned[0].id;
let maintainer = this.source;
if (this.source.id === this.target.id) {
maintainer = this.data.spawned[0];
}
await this.applyActiveEffects(maintainer, [maintainDoc]);
}
get effectName() {
return `Baleful Polymorph into ${this.targetActor.prototypeToken.name}`;
}
get spawnUpdates() {
const updates = super.spawnUpdates;
foundry.utils.mergeObject(updates.actor, this.data.actorUpdates);
foundry.utils.mergeObject(updates.token, this.data.tokenUpdates);
foundry.utils.mergeObject(updates.embedded, this.data.embeddedUpdates);
return updates;
}
async sideEffects() {
const owner = firstOwner(this.target);
moduleHelpers.socket.executeAsUser(
updateOwnedToken,
owner.id,
this.target.document.parent.id,
this.target.document.id,
{ hidden: true, x: 0, y: 0 },
{ animate: false },
);
}
get description() {
let desc = super.description;
desc += `<p>On losing an opposed roll vs the victim's Spirit, the victim
is transformed, taking the form and abilities of a <em>${this.targetActor.name}</em>
but retaining their ${this.data.critfail ? '' : 'Smarts and '}Spirit. `;
if (this.data.critfail) {
desc += `The victim believes they <strong>are</strong> the animal for
the duration of the power.`;
}
desc += `</p><p>The victim may attempt to shake off the effect with a Spirit
roll at ${this.data.raise ? -4 : -2} at the end of subsequent turns.`;
return desc;
}
get primaryEffectButtons() {
const buttons = super.primaryEffectButtons;
const modvalue = this.data.raise ? -4 : -2;
const mods = [];
mods.push({ label: 'Strong', value: modvalue });
buttons.push({
label: `Shake off (Spirit ${modvalue})`,
type: 'trait',
rollType: 'attribute',
rollDesc: 'Spirit',
flavor: 'Success shakes off the effects of sloth',
mods,
});
return buttons;
}
}

View File

@ -1,92 +0,0 @@
import { PowerEffect } from './basePowers.js';
import { requestRollFromTokens } from '../helpers.js';
export class BanishEffect extends PowerEffect {
get name() {
return 'Banish';
}
get duration() {
return 0;
}
get icon() {
return 'icons/magic/control/sihouette-hold-beam-green.webp';
}
get basePowerPoints() {
return 3;
}
get usePrimaryEffect() {
return false;
}
get isTargeted() {
return true;
}
get isRaisable() {
return false;
}
get hasAoe() {
return true;
}
get modifiers() {
const mods = super.modifiers;
mods.push({
type: 'number',
default: 4,
name: 'Opposed Target Number',
id: 'tn',
epic: false,
effect: false,
value: 0,
});
mods.push({
type: 'select',
default: 'none',
name: 'Area of Effect',
id: 'aoe',
epic: true,
choices: {
none: 'None',
sbt: 'Small Blast Template',
mbt: 'Medium Blast Template',
lbt: 'Large Blast Template',
},
effects: { none: null, sbt: null, mbt: null, lbt: null },
values: { none: 0, sbt: 1, mbt: 2, lbt: 3 },
});
return mods;
}
async sideEffects() {
await super.sideEffects();
const rollOpts = {
title: 'Spirit roll to resist banishment',
flavor: 'Roll Spirit to resist banishment!',
targetNumber: this.data.tn,
};
await requestRollFromTokens(this.targets, 'ability', 'spirit', rollOpts);
}
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;
if (this.data.aoe !== 'none') {
list.push(this.data.aoe.toUpperCase());
}
return list;
}
}

View File

@ -1,120 +0,0 @@
import { PowerEffect } from './basePowers.js';
export 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({
type: 'select',
name: 'Damage',
id: 'damage',
epic: false,
choices: {
none: 'None',
damage: 'Damage',
immaterial: 'Damage (immaterial)',
deadly: '⭐ Deadly',
},
effects: { none: null, damage: null, immaterial: null, deadly: null },
values: { none: 0, damage: 1, immaterial: 0, deadly: 2 },
});
mods.push({
type: 'checkbox',
name: 'Hardened',
id: 'hardened',
value: 1,
epic: false,
effect: false,
});
mods.push({
type: 'checkbox',
name: 'Shaped',
id: 'shaped',
value: 1,
epic: false,
effect: false,
});
mods.push({
type: 'checkbox',
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.size) {
height *= 2;
}
return `${height}" (${height * 2} yards)`;
}
get _height() {
return `${this.data.size ? '2" (4' : '1" (2'} yards)`;
}
get _hardness() {
return (this.data.raise ? 12 : 10) + (this.data.hardened ? 2 : 0);
}
get maintEffectButtons() {
const buttons = super.primaryEffectButtons;
if (this.data.damage != 'none') {
const damage = this.data.damage === 'deadly' ? '2d6' : '2d4';
buttons.push({
label: `Damage (${damage})`,
type: 'damage',
formula: damage + 'x',
});
}
return buttons;
}
get description() {
let text = super.description;
text += `<p>A barrier ${this._height} tall and ${this._length} long, of hardness ${this._hardness}. `;
if (this.data.damage === 'deadly') {
text += 'It does 2d6 damage to anyone who contacts it. ';
} else if (this.data.damage != 'none') {
text += 'It does 2d4 damage to anyone who contacts it. ';
}
if (this.data.shaped) {
text += 'It was shaped into a circle, square, or rectangle. ';
}
text += '</p>';
return text;
}
}

View File

@ -1,1076 +0,0 @@
import { moduleName, moduleHelpers, log, settingKeys } from '../globals.js';
import { firstOwner, addActiveEffectsToToken } from '../helpers.js';
import { templates } from '../preloadTemplates.js';
const MAINTAIN_ICONS = [
'icons/magic/symbols/rune-sigil-black-pink.webp',
'icons/magic/symbols/rune-sigil-green-purple.webp',
'icons/magic/symbols/rune-sigil-hook-white-red.webp',
'icons/magic/symbols/rune-sigil-red-orange.webp',
'icons/magic/symbols/rune-sigil-rough-white-teal.webp',
'icons/magic/symbols/rune-sigil-white-pink.webp',
'icons/magic/symbols/runes-star-blue.webp',
'icons/magic/symbols/runes-star-magenta.webp',
'icons/magic/symbols/runes-star-orange-purple.webp',
'icons/magic/symbols/runes-star-orange.webp',
'icons/magic/symbols/runes-star-pentagon-blue.webp',
'icons/magic/symbols/runes-star-pentagon-magenta.webp',
'icons/magic/symbols/runes-star-pentagon-orange-purple.webp',
'icons/magic/symbols/runes-star-pentagon-orange.webp',
'icons/magic/symbols/runes-triangle-blue.webp',
'icons/magic/symbols/runes-triangle-magenta.webp',
'icons/magic/symbols/runes-triangle-orange-purple.webp',
'icons/magic/symbols/runes-triangle-orange.webp',
'icons/magic/symbols/triangle-glow-purple.webp',
'icons/magic/symbols/triangle-glowing-green.webp',
];
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
function _hashCode(str) {
let hash = 0;
if (str.length === 0) {
return hash;
}
for (let i = 0; i < str.length; i++) {
const c = str.charCodeAt(i);
hash = (hash << 5) - hash + c;
hash |= 0;
}
return Math.abs(hash);
}
export class PowerFormApplication extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(powerEffect) {
const name = powerEffect.name.replaceAll(/[^a-zA-Z]/g, '');
const id = `${PowerFormApplication.DEFAULT_OPTIONS.id}${name}`;
super({ id });
this.powerEffect = powerEffect;
}
static DEFAULT_OPTIONS = {
id: 'mbSwadePowerEffectsApplicationV2',
form: {
handler: PowerFormApplication.#onSubmit,
closeOnSubmit: true,
},
tag: 'form',
position: {
width: 600,
height: 'auto',
},
classes: ['mbSwade', 'mbSwadeForm', 'mbSwadePowerEffectsForm'],
window: {
icon: 'fa-solid fa-hand-sparkles',
title: 'Apply Effect',
},
};
static PARTS = {
header: {
template: templates['dialogHeader.html'],
classes: ['mbSwade', 'mbSwadeDialogHeader', 'mbSwadePowerEffectsHeader'],
},
body: {
template: templates['powerDialog.html'],
classes: ['mbSwade', 'mbSwadePowerEffectsBody', 'scrollable'],
},
footer: {
template: 'templates/generic/form-footer.hbs',
},
};
static sortMods(a, b) {
if (a.isGlobal !== b.isGlobal) {
return a.isGlobal ? -1 : 1;
}
if ((a.sortOrder ?? 0) !== (b.sortOrder ?? 0)) {
return (a.sortOrder ?? 0) < (b.sortOrder ?? 0) ? -1 : 1;
}
if (a.type !== b.type) {
return a.type === 'checkbox' ? -1 : 1;
}
return a.name === b.name ? 0 : a.name < b.name ? -1 : 1;
}
async _prepareContext() {
await this.powerEffect.init();
let modifiers = foundry.utils.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.isNumber) {
modifier.step = modifier?.step ?? 1;
}
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}`;
}
}
if (modifier.isRadio) {
for (const choice in modifier.choices) {
let val = modifier.choices[choice];
modifier.choices[choice] = { text: val, checked: choice == modifier.default };
}
}
}
const data = {
name: this.powerEffect.name,
formId: foundry.utils.randomID(),
headerTitle: `${this.powerEffect.name} Effect`,
headerSubtitle: `Apply the effects from ${this.powerEffect.name}`,
icon: this.powerEffect.icon,
basePowerPoints: this.powerEffect.basePowerPoints,
modifiers,
recipients: {
cost: 0,
number: 0,
total: 0,
},
extraDescription: this.powerEffect.extraDescription,
targets: [],
buttons: this.powerEffect.menuButtons,
};
for (let button of data.buttons) {
button.action = button.value;
button.type = button.value === 'cancel' ? 'cancel' : 'submit';
}
if (this.powerEffect.isTargeted) {
if (this.powerEffect.oneTarget) {
data.targets = [this.powerEffect.targets?.[0]?.name ?? '<em>No Target Selected!</em>'];
} else {
data.targets = this.powerEffect.targets.map((t) => t.name);
}
}
if (this.powerEffect.hasAdditionalRecipients && this.powerEffect.targets.length > 1) {
data.recipients.cost = this.powerEffect.additionalRecipientCost;
data.recipients.count = this.powerEffect.additionalRecipientCount;
data.recipients.total = data.recipients.cost * data.recipients.count;
data.recipients.epic = this.powerEffect.additionalRecipientsIsEpic;
data.recipients.text = this.powerEffect.additionalRecipientText;
}
log('DATA', data);
return data;
}
static async #onSubmit(event, form, formData) {
log(this.formData);
formData.object.submit = event?.submitter?.dataset?.action ?? 'cancel';
if (formData.object.submit !== 'cancel') {
this.powerEffect.formData = formData.object;
this.powerEffect.applyEffect();
}
}
}
export class PowerEffect {
constructor(token, targets, item) {
this.source = token;
this.targets = targets;
this.item = item;
this.data = {};
}
async init() {
log('Power Effect', this.name, 'Init');
}
static async getStatus(label, name, favorite = true) {
const effect = foundry.utils.deepClone(CONFIG.statusEffects.find((se) => se.label === label || se.name === label));
effect.name = 'name' in effect ? effect.name : effect.label;
effect.duration = {};
if (!('flags' in effect)) {
effect.flags = {};
}
effect.flags.swade = {};
if (favorite) {
effect.flags.swade.favorite = true;
}
effect.statuses ??= [];
effect.statuses.push(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: {},
system: {
loseTurnOnHold: false,
expiration: null,
},
flags: {
[moduleName]: {
powerEffect: true,
},
swade: {
loseTurnOnHold: false,
expiration: null,
},
},
};
}
async applyActiveEffects(token, effectDocuments) {
const owner = firstOwner(token);
await moduleHelpers.socket.executeAsUser(
addActiveEffectsToToken,
owner.id,
token?.scene?.id ?? token.parent.id,
token.id,
effectDocuments,
);
}
get name() {
return 'Unknown Power';
}
get effectName() {
return this.name;
}
get extraDescription() {
return '';
}
get icon() {
return 'icons/magic/symbols/question-stone-yellow.webp';
}
get duration() {
return 5;
}
get basePowerPoints() {
return 0;
}
get usePrimaryEffect() {
return true;
}
get isDamaging() {
return false;
}
get hasAdditionalRecipients() {
return false;
}
get additionalRecipientsIsEpic() {
return false;
}
get additionalRecipientText() {
return 'Additional Recipients';
}
get additionalRecipientCount() {
if (!this.hasAdditionalRecipients) {
return 0;
}
return Math.max(0, this.targets.length - 1);
}
get additionalRecipientCost() {
return 0;
}
get isTargeted() {
return false;
}
get oneTarget() {
return false;
}
get hasRange() {
return true;
}
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: {
name: '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: {
name: '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,
});
if (this.isDamaging) {
mods.push({
name: 'Armor Piercing',
id: 'ap',
type: 'select',
default: 'none',
choices: { none: 'None', 2: 'AP 2', 4: 'AP 4', 6: 'AP 6' },
values: { none: 0, 2: 1, 4: 2, 6: 3 },
effects: { none: null, 2: null, 4: null, 6: null },
epic: false,
isGlobal: true,
});
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: 'none',
value: 1,
epic: false,
choices: { none: 'None', hinder: 'Hinder', hurry: 'Hurry' },
values: { none: 0, hinder: 1, hurry: 1 },
isGlobal: true,
effects: {
none: null,
hinder: {
name: 'Hinder',
icon: 'icons/magic/control/debuff-chains-shackle-movement-red.webp',
changes: [
{
key: 'system.pace',
value: -2,
priority: 0,
mode: foundry.CONST.ACTIVE_EFFECT_MODES.ADD,
},
],
},
hurry: {
name: 'Hurry',
icon: 'icons/skills/movement/feet-winged-sandals-tan.webp',
changes: [
{
key: 'system.pace',
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,
});
}
if (this.hasRange) {
mods.push({
type: 'select',
default: 0,
name: 'Range',
id: 'range',
choices: {
normal: 'Normal Range',
x2: 'Range ×2',
x3: 'Range ×3',
},
values: {
normal: 0,
x2: 1,
x3: 2,
},
isGlobal: true,
effects: { normal: null, x2: null, x3: null },
});
}
return mods;
}
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;
}
render() {
new PowerFormApplication(this).render(true);
}
async applyEffect() {
await this.parseValues();
await this.apply();
await this.chatMessage();
await this.sideEffects();
}
async parseValues() {
this.data.raise = this.formData.submit === 'raise';
this.data.button = this.formData.submit;
this.data.maintId = foundry.utils.randomID();
for (const mod of this.modifiers) {
this.data[mod.id] = this.formData[mod.id];
}
}
enhanceSecondaryEffect(maintId, doc) {
doc.statuses = doc.statuses ?? [];
doc.statuses.push('powerEffect');
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;
doc.system.expiration = doc.flags.swade.expiration;
} else {
doc.flags[moduleName].maintId = maintId;
if (moduleHelpers.useVAE) {
doc.flags['visual-active-effects'] = {
data: {
inclusion: 1,
},
};
} else {
doc.duration.seconds = 594;
}
}
return doc;
}
async createSecondaryEffects(maintId) {
const docs = [];
for (const mod of this.modifiers) {
const modValue = this.data[mod.id];
if (modValue && (mod?.effect || (mod?.effects?.[modValue] ?? false))) {
const icon = 'effects' in mod ? mod.effects[modValue].icon : mod.icon;
const name = 'effects' in mod ? mod.effects[modValue].name : mod.name;
const changes = 'effects' in mod ? mod.effects[modValue].changes : mod.changes;
const doc = this.enhanceSecondaryEffect(maintId, this.createEffectDocument(icon, name, changes));
const desc = 'effects' in mod ? mod.effects?.[modValue]?.description : mod.description;
if (desc) {
doc.description += desc;
}
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;
}
getMaintainEffectChanges() {
const changes = [
{
key: 'flags.swade-mb-helpers.powerMaintained',
value: 1,
priority: 0,
mode: foundry.CONST.ACTIVE_EFFECT_MODES.OVERRIDE,
},
];
return changes;
}
get description() {
return '';
}
get primaryEffectButtons() {
// button objects should have a label and a type.
// type should have one of the following, with the associated additional
// fields:
// roll:
// formula: dice formula eg '3d6 + 3'
// flavor: flavor text (optional)
// trait:
// rollType: 'attribute' or 'skill
// rollDesc: name or swid of the attribute or skill
// flavor: flavor text (optional)
// mods: list of mods { label, value, ignore }
// damage:
// formula: dice formula for example '1d4x[Blades]'
// ap: optional, a positive integer or 0, armor piercing
// flavor: flavor text (optional)
// callback:
// callback: the function callback to run, takes a token as an argument
return [];
}
get maintEffectButtons() {
// see the comment for primaryEffectButtons
return [];
}
get basePrimaryEffect() {
return this.createEffectDocument(this.icon, this.effectName, this.getPrimaryEffectChanges());
}
async createPrimaryEffect(maintId) {
const doc = this.basePrimaryEffect;
if (moduleHelpers.useVAE) {
doc.flags['visual-active-effects'] = {
data: {
inclusion: 1,
},
};
}
doc.description += this.description;
doc.statuses = doc.statuses ?? [];
doc.statuses.push('powerEffect');
doc.flags[moduleName].maintId = maintId;
const effectButtons = this.primaryEffectButtons;
if (effectButtons.length > 0) {
doc.flags[moduleName].buttons = effectButtons;
}
return doc;
}
async createMaintainEffect(maintId) {
let icon = MAINTAIN_ICONS[_hashCode(this.name) % MAINTAIN_ICONS.length];
if (!this.usePrimaryEffect) {
icon = this.icon;
}
const doc = this.createEffectDocument(icon, `Maintaining ${this.effectName}`, this.getMaintainEffectChanges());
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.system.expiration = doc.flags.swade.expiration;
doc.system.loseTurnOnHold = true;
doc.flags[moduleName].maintainingId = maintId;
if (this.isTargeted) {
doc.flags[moduleName].targetIds = this.targets.map((t) => t.id);
} else {
doc.flags[moduleName].targetIds = [this.source.id];
}
doc.statuses = doc.statuses ?? [];
doc.statuses.push('powerMaintainEffect');
const effectButtons = this.maintEffectButtons;
if (effectButtons.length > 0) {
doc.flags[moduleName].buttons = effectButtons;
}
return doc;
}
// eslint-disable-next-line no-unused-vars
async secondaryDocsForTarget(docs, target) {
return foundry.utils.deepClone(docs);
}
async primaryDocForTarget(doc, target) {
const newDoc = foundry.utils.deepClone(doc);
newDoc.flags[moduleName].maintainingId = doc.flags[moduleName].maintId;
newDoc.flags[moduleName].targetIds = [target.id];
return newDoc;
}
async apply() {
const maintId = this.data.maintId;
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);
}
}
} else {
const targetDocs = await this.secondaryDocsForTarget(secondaryDocs, this.source);
if (targetDocs.length > 0) {
await this.applyActiveEffects(this.source, targetDocs);
}
}
if (this.duration > 0) {
await this.applyActiveEffects(this.source, [maintainDoc]);
}
}
async sideEffects() {
if (this.data.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) {
const modValue = this.data[mod.id];
if (modValue) {
if ('values' in mod) {
total += mod.values[modValue];
} else {
total += mod.value;
}
}
}
total += this.additionalRecipientCost * this.additionalRecipientCount;
return total;
}
get chatMessageEffects() {
const list = [];
if (this.hasAdditionalRecipients && this.targets.length > 1) {
list.push(`${this.targets.length - 1} Additional Recipients`);
}
if (this.data.adaptable) {
list.push('Different Trapping (Adaptable Caster)');
}
if (this.data.fatigue) {
list.push('Fatigue (applied to targets)');
}
if (this.data.heavyweapon) {
list.push('Heavy Weapon');
}
if (this.data.lingeringdamage) {
list.push('Lingering Damage');
}
if (this.data.selective) {
list.push('Selective');
}
if (this.isDamaging && this.data.ap > 0) {
list.push(`AP ${this.data.ap}`);
}
if (this.data.range ?? 'none' != 'none') {
list.push(`Range ${this.data.range}`);
}
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 open><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 },
);
}
}
export class ActorFolderEffect extends PowerEffect {
async init() {
await super.init();
const extraFields = ['system.stats.size', 'flags.swade-mb-helpers.summonData'];
this.packActors = await ActorFolderEffect.actorFolderPack.getIndex({ fields: extraFields });
this.data.actors = this.prepActors();
}
get actorFolderBase() {
return '';
}
static get actorFolderPack() {
const packId = moduleHelpers.getSetting(settingKeys.powerActorsCompendium);
const pack = game.packs.get(packId);
if (!pack) {
return undefined;
}
return game.user.isGM || pack.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER)
? pack
: undefined;
}
get actorFolder() {
return `${this.actorFolderBase}/${this.name}`;
}
static getPackFolderByPath(path) {
const names = path.split('/');
if (names[0] === '') {
names.shift();
}
let name = names.shift();
if (!ActorFolderEffect.actorFolderPack) {
return undefined;
}
let folder = ActorFolderEffect.actorFolderPack.folders
.filter((f) => f.type === 'Actor' && !f.folder)
.find((f) => f.name === name);
if (!folder) {
return undefined;
}
while (names.length > 0) {
name = names.shift();
folder = folder.children.find((c) => c.folder.name === name);
if (!folder) {
return undefined;
}
folder = folder.folder;
}
return folder;
}
static getPackActorsInFolder(inFolder) {
const prefixStack = [''];
const actors = {};
const folderStack = [inFolder];
while (folderStack.length > 0) {
const prefix = prefixStack.shift();
const folder = folderStack.shift();
for (const actor of folder.contents) {
actors[`${prefix}${actor.name}`] = actor;
}
for (const child of folder.children) {
const newPrefix = `${prefix}${child.folder.name} | `;
prefixStack.push(newPrefix);
folderStack.push(child.folder);
}
}
return actors;
}
prepFolders() {
const folders = [];
const folderNames = [
this.actorFolder,
`${this.actorFolder} - Default`,
`${this.actorFolder}/Default`,
`${this.actorFolder} - ${this.source.name}`,
`${this.actorFolder} - ${this.source.actor.name}`,
`${this.actorFolder}/${this.source.name}`,
`${this.actorFolder}/${this.source.actor.name}`,
];
for (const folderName of folderNames) {
const folder = ActorFolderEffect.getPackFolderByPath(folderName);
if (folder) {
log(`Found actor folder ${folderName}`);
folders.push(folder);
}
}
if (folders.length > 1) {
folders.shift();
}
return folders;
}
prepActors() {
const folders = this.prepFolders();
const actors = {};
for (const folder of folders) {
const folderActors = ActorFolderEffect.getPackActorsInFolder(folder);
for (const key in folderActors) {
actors[key] = folderActors[key];
}
}
return actors;
}
// eslint-disable-next-line no-unused-vars
actorValue(actor) {
return 0;
}
getActors() {
const choices = {};
const effects = {};
const values = {};
Object.keys(this.data.actors)
.filter((k) => !k.includes('_template'))
.sort()
.forEach((key) => {
const id = this.data.actors[key].uuid;
choices[id] = key;
effects[id] = null;
values[id] = this.actorValue(this.data.actors[key]);
});
return { choices, effects, values };
}
get modifiers() {
const { choices, effects, values } = this.getActors();
return [
...super.modifiers,
{
name: 'Select Creature',
id: 'actorId',
type: 'select',
choices,
effects,
values,
epic: false,
effect: false,
},
];
}
get spawnUpdates() {
const updates = {
actor: {},
token: {
actorLink: false,
},
embedded: {
ActiveEffect: {},
Item: {},
},
};
return updates;
}
#documentFinder(documentType, oldDoc, newDoc) {
if (documentType === 'Item') {
return oldDoc.name.toLowerCase() === newDoc.name.toLowerCase() && oldDoc.type === newDoc.type;
}
return oldDoc.name.toLowerCase() === newDoc.name.toLowerCase();
}
async updateEmbedded(actor, newDocs) {
const adds = {};
const updates = {};
for (const documentType of Object.keys(newDocs ?? {})) {
const collection = actor.getEmbeddedCollection(documentType);
adds[documentType] = [];
updates[documentType] = [];
log('docType', documentType);
for (const newDocKey in newDocs[documentType]) {
log('newDocKey', newDocKey);
const newDoc = newDocs[documentType][newDocKey].toObject();
const oldDoc = collection.find((doc) => this.#documentFinder(documentType, doc, newDoc));
if (newDoc.type === 'power' && newDoc?.system?.choiceSets?.length > 0) {
newDoc.system.choiceSets = [];
}
if (oldDoc) {
const _id = oldDoc.id;
updates[documentType].push({ ...newDoc, _id });
} else {
adds[documentType].push(newDoc);
}
}
const updateOpts = {};
updateOpts.mbItemCreationSource = moduleName;
if (documentType === 'Item') {
updateOpts.renderSheet = false;
}
try {
if (adds[documentType].length > 0) {
actor.createEmbeddedDocuments(documentType, adds[documentType], updateOpts);
}
} catch (e) {
log('ERROR', e);
}
try {
if (updates[documentType].length > 0) {
actor.updateEmbeddedDocuments(documentType, updates[documentType], updateOpts);
}
} catch (e) {
log('ERROR', e);
}
}
}
async parseValues() {
await super.parseValues();
this.data.maintId = foundry.utils.randomID();
this.targetActor = await game.tcal.importTransientActor(this.data.actorId, { preferExisting: true });
this.targetTokenDoc = await this.targetActor.getTokenDocument();
const perm = CONST?.DOCUMENT_PERMISSION_LEVELS?.OWNER ?? CONST?.DOCUMENT_OWNERSHIP_LEVELS?.OWNER;
const sourceUpdates = {
delta: {
ownership: {
[game.user.id]: perm,
},
},
};
this.targetTokenDoc.updateSource(sourceUpdates);
}
async spawn() {
this.targetTokenDoc.updateSource({
x: this.source.x,
y: this.source.y,
elevation: this.source.elevation,
});
return this.source.scene.createEmbeddedDocuments('Token', [this.targetTokenDoc]);
}
async apply() {
this.data.spawned = await this.spawn();
const updates = this.spawnUpdates;
const secondaryDocs = await this.createSecondaryEffects(this.data.maintId);
const primaryDoc = await this.createPrimaryEffect(this.data.maintId);
const promises = [];
for (const token of this.data.spawned) {
if (updates?.token) {
promises.push(token.update(updates.token));
}
if (updates?.actor) {
promises.push(token.actor.update(updates.actor));
}
if (updates?.embedded) {
promises.push(this.updateEmbedded(token.actor, updates.embedded));
}
const activeEffects = await this.secondaryDocsForTarget(secondaryDocs, token);
activeEffects.push(await this.primaryDocForTarget(primaryDoc, token));
promises.push(this.applyActiveEffects(token, activeEffects));
}
await Promise.all(promises);
}
async createPrimaryEffect(maintId) {
const doc = await super.createPrimaryEffect(maintId);
doc.flags[moduleName].spawnedTempToken = true;
return doc;
}
async sideEffects() {
if (this.data.fatigue) {
for (const target of this.data.spawned) {
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);
}
}
}
}
}
export function embeddedHelperHook(item, data, options) {
if (options?.mbItemCreationSource === moduleName) {
options.renderSheet = false;
}
}

View File

@ -1,90 +0,0 @@
import { PowerEffect } from './basePowers.js';
export class BeastFriendEffect extends PowerEffect {
get name() {
return 'Beast Friend';
}
get duration() {
return (this.data.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;
const ppDefault = Math.max(
this.targets.map((t) => Math.max(t.actor.system.stats.size, 1)).reduce((a, b) => a + b, 0),
1,
);
mods.push(
{
name: 'Bestiarium',
type: 'checkbox',
value: 2,
id: 'bestiarium',
epic: true,
effect: false,
},
{
name: 'Duration',
type: 'checkbox',
value: 1,
id: 'duration',
epic: false,
effect: false,
},
{
name: 'Mind Rider',
type: 'checkbox',
value: 1,
id: 'mindrider',
epic: false,
effect: false,
},
{
name: 'Base power points',
type: 'number',
value: 0,
default: ppDefault,
id: 'basePP',
epic: false,
effect: false,
},
);
return mods;
}
get basePowerPoints() {
return (
this.data.basePP ??
Math.max(
this.targets.map((t) => Math.max(t.actor.system.stats.size, 1)).reduce((a, b) => a + b, 0),
1,
)
);
}
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.bestiarium) {
text += ' The caster may even effect magical beasts.';
}
return text;
}
}

View File

@ -1,70 +0,0 @@
import { PowerEffect } from './basePowers.js';
export 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 modifiers() {
const mods = super.modifiers;
mods.push(
{
name: 'Area of Effect',
id: 'aoe',
type: 'select',
default: 'mbt',
choices: {
sbt: 'Small Blast Template',
mbt: 'Medium Blast Template',
lbt: 'Large Blast Template',
},
effects: { sbt: null, mbt: null, lbt: null },
values: { sbt: 0, mbt: 0, lbt: 1 },
epic: false,
},
{ name: 'Damage', value: 2, id: 'damage', epic: false, effect: false },
{
name: 'Greater Blast',
value: 4,
id: 'greater',
epic: true,
effect: false,
},
);
return mods;
}
get description() {
const dmgDie = (this.data.greater ? 4 : this.data.damage ? 3 : 2) + (this.data.raise ? 1 : 0);
const size = this.data.aoe.toUpperCase();
return super.description + `<p>The blast covers a ${size} and does ${dmgDie}d6 damage</p>`;
}
}

View File

@ -1,126 +0,0 @@
import { moduleName } from '../globals.js';
import { PowerEffect } from './basePowers.js';
export 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.strong ? '-2 ' : ''}
roll as a free action. Success removes 2 points of penalties.
A raise removes the effect.</p>`
);
}
get primaryEffectButtons() {
const buttons = super.primaryEffectButtons;
const mods = [];
if (this.data.strong) {
mods.push({ label: 'Strong', value: -2 });
}
buttons.push({
label: `Shake off (Vigor${this.data.strong ? ' -2' : ''})`,
type: 'trait',
rollType: 'attribute',
rollDesc: 'Vigor',
flavor: 'Success shakes off one level, Raise shakes off two',
mods,
});
return buttons;
}
async createSecondaryEffects(maintId) {
const docs = await super.createSecondaryEffects(maintId);
if (this.data.raise) {
const strong = this.data.strong;
let 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 = this.enhanceSecondaryEffect(maintId, doc);
doc.description = this.description + '<p>This is the raise effect which can be shaken off separately.</p>';
docs.push(doc);
}
return docs;
}
get modifiers() {
const mods = super.modifiers;
mods.push({
name: 'Strong',
type: 'checkbox',
value: 1,
id: 'strong',
epic: false,
effect: false,
});
mods.push({
type: 'select',
default: 'none',
name: 'Area of Effect',
id: 'aoe',
epic: true,
choices: {
none: 'None',
mbt: 'Medium Blast Template',
lbt: 'Large Blast Template',
},
effects: { none: null, mbt: null, lbt: null },
values: { none: 0, mbt: 2, lbt: 3 },
});
return mods;
}
get effectName() {
const strong = this.data.strong;
return `Blinded${strong ? ' (Strong)' : ''}`;
}
get chatMessageEffects() {
const list = super.chatMessageEffects;
if (this.data.aoe !== 'none') {
list.push(this.data.aoe.toUpperCase());
}
return list;
}
}

View File

@ -1,94 +0,0 @@
import { PowerEffect } from './basePowers.js';
export class BoltEffect extends PowerEffect {
get name() {
return 'Bolt';
}
get icon() {
return 'icons/skills/ranged/tracers-triple-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(
{
type: 'checkbox',
name: 'Damage',
value: 2,
id: 'damage',
epic: false,
effect: false,
},
{
type: 'checkbox',
name: 'Disintegrate',
value: 1,
id: 'disintigrate',
epic: true,
effect: false,
},
{
name: 'Greater Bolt',
type: 'checkbox',
value: 4,
id: 'greater',
epic: true,
effect: false,
},
{
name: 'Rate of Fire',
type: 'checkbox',
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.greater ? 4 : this.data.damage ? 3 : 2) + (this.data.raise ? 1 : 0);
let desc = super.description + '<p>';
if (this.data.rof) {
desc += `Up to two bolts (RoF 2) do ${dmgDie}d6 damage each.`;
} else {
desc += `The bolt does ${dmgDie}d6 damage.`;
}
if (this.data.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;
}
}

View File

@ -1,221 +0,0 @@
import { moduleName } from '../globals.js';
import { PowerEffect } from './basePowers.js';
export 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) {
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.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.enhanceSecondaryEffect(maintId, this.createEffectDocument(this.icon, name, changes));
doc.description = this.description + '<p>This is the raise effect which can be shaken off separately.</p>';
docs.push(doc);
}
return docs;
}
get effectName() {
let name = `${this.data.direction} ${this.data.trait.name}`;
const nameMods = [];
if (this.data.greater) {
nameMods.push('Greater');
}
if (this.data.direction === 'Lower' && this.data.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.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.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 primaryEffectButtons() {
const buttons = super.primaryEffectButtons;
if (this.data.direction === 'Lower') {
const mods = [];
if (this.data.strong) {
mods.push({ label: 'Strong', value: -2 });
}
buttons.push({
label: `Shake off (Spirit${this.data.strong ? ' -2' : ''})`,
type: 'trait',
rollType: 'attribute',
rollDesc: 'Spirit',
flavor: 'Success shakes off one level, Raise shakes off two',
mods,
});
}
return buttons;
}
get modifiers() {
const mods = super.modifiers;
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());
const traitChoices = {};
const traitValues = {};
const traitEffects = {};
for (const trait of traitOptions) {
traitChoices[trait] = trait;
traitValues[trait] = 0;
traitEffects[trait] = null;
}
this.data.traits = traits;
mods.push({
sortOrder: -2,
name: 'Boost or Lower?',
id: 'direction',
type: 'radio',
default: 'Boost',
epic: false,
choices: { Boost: 'Boost', Lower: 'Lower' },
effects: { Boost: null, Lower: null },
values: { Boost: 0, Lower: 0 },
});
mods.push({
sortOrder: -1,
name: 'Trait',
id: 'traitName',
type: 'select',
epic: false,
choices: traitChoices,
values: traitValues,
effects: traitEffects,
});
mods.push({
name: 'Greater Boost/Lower Trailt',
type: 'checkbox',
value: 2,
id: 'greater',
epic: true,
effect: false,
});
mods.push({
name: 'Strong (lower only)',
type: 'checkbox',
value: 1,
id: 'strong',
epic: false,
effect: false,
});
return mods;
}
async parseValues() {
await super.parseValues();
this.data.trait = this.data.traits[this.data.traitName];
}
}

View File

@ -1,78 +0,0 @@
import { PowerEffect } from './basePowers.js';
export 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',
type: 'checkbox',
id: 'power',
value: 1,
epic: false,
effect: false,
});
return mods;
}
getPrimaryEffectChanges() {
return [
...super.getPrimaryEffectChanges(),
{
key: 'system.pace.burrow',
value: 6,
mode: CONST.ACTIVE_EFFECT_MODES.OVERRIDE,
priority: 0,
},
];
}
async primaryDocForTarget(doc, target) {
const newDoc = await super.primaryDocForTarget(doc, target);
var pace = target.actor.system.pace[target.actor.system.pace.base];
pace = this.data.raise ? pace : pace / 2;
newDoc.changes[newDoc.changes.length - 1].value = pace;
return newDoc;
}
get effectName() {
return `${this.name} ${this.data.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.power) {
text += '<p>Can <em>burrow</em> through solid stone, concrete, etc</p>';
}
return text;
}
}

View File

@ -1,63 +0,0 @@
import { PowerEffect } from './basePowers.js';
export class BurstEffect extends PowerEffect {
get name() {
return 'Burst';
}
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',
type: 'checkbox',
value: 2,
id: 'damage',
epic: false,
effect: false,
},
{
name: 'Greater Burst',
type: 'checkbox',
value: 4,
id: 'greater',
epic: true,
effect: false,
},
);
return mods;
}
get description() {
const dmgDie = (this.data.greater ? 4 : this.data.damage ? 3 : 2) + (this.data.raise ? 1 : 0);
return super.description + `<p>The burst covers a Cone or Stream template and does ${dmgDie}d6 damage</p>`;
}
}

View File

@ -1,112 +0,0 @@
import { PowerEffect } from './basePowers.js';
export 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 modifiers() {
const mods = super.modifiers;
mods.push({
type: 'select',
default: 'mbt',
name: 'Area of Effect',
id: 'aoe',
epic: false,
choices: {
sbt: 'Small Blast Template',
mbt: 'Medium Blast Template',
lbt: 'Large Blast Template',
},
effects: { sbt: null, mbt: null, lbt: null },
values: { sbt: 0, mbt: 0, lbt: 1 },
});
mods.push({
name: 'Greater Confusion',
value: 2,
id: 'greater',
epic: true,
type: 'checkbox',
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';
}
get description() {
const size = this.data.aoe.toUpperCase();
let effect = 'Vulnerable';
if (this.data.raise) {
effect = 'both Distracted and Vulnerable';
} else if (this.data.distracted) {
effect = 'Distracted';
}
if (this.data.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.vulnerable) {
PowerEffect.getStatus('SWADE.Vuln', 'Vulnerable', false).then((v) => docs.push(v));
}
if (this.data.greater) {
PowerEffect.getStatus('SWADE.Shaken', 'Shaken', false).then((v) => docs.push(v));
}
return docs;
}
}

View File

@ -1,100 +0,0 @@
import { PowerEffect } from './basePowers.js';
export class ConjureItemEffect extends PowerEffect {
get name() {
return 'Conjure Item';
}
get icon() {
return 'icons/commodities/tech/cog-steel-grey.webp';
}
get duration() {
return 0;
}
get isTargeted() {
return false;
}
get isRaisable() {
return true;
}
get basePowerPoints() {
return 2;
}
get modifiers() {
return [
{
name: 'Weight in pounds OR daily rations',
type: 'number',
default: 1,
value: 0,
id: 'weight',
epic: false,
effect: false,
},
{
name: 'Complete Set',
type: 'checkbox',
default: false,
value: 1,
id: 'complete',
epic: false,
effect: false,
},
{
name: 'Create Food and Water (Special)',
type: 'checkbox',
default: false,
value: 0,
id: 'rations',
epic: false,
effect: false,
},
{
name: 'Durable (+1 per pound)',
type: 'checkbox',
default: false,
value: 0,
id: 'durable',
epic: false,
effect: false,
},
];
}
get powerPoints() {
if (this.data.rations) {
return this.data.weight;
}
return this.data.weight * (this.data.durable ? 3 : 2) + (this.data.complete ? 1 : 0);
}
get description() {
if (this.data.rations) {
return (
super.description +
`<p>Conjure enough food and drink to feed ${this.data.weight} size 0
humanoid${this.data.weight > 1 ? 's' : ''} for 1 day. The food decays
and is inedible after 24 hours if not consumed.</p>`
);
}
let desc = super.description + `<p>Conjure an item up to ${this.data.weight} in pounds. `;
if (this.data.raise) {
desc += 'It is a more durable item than usual for its type. ';
}
if (this.data.complete) {
desc += 'Whatever is conjured is a complete set. ';
}
if (this.data.durable) {
desc += 'The item remains until it is dispelled or dismissed by the caster. ';
} else {
desc += 'The item lasts for one hour. ';
}
desc += 'Once it is dismissed or expires, the item fades from existance.</p>';
return desc;
}
}

View File

@ -1,109 +0,0 @@
import { PowerEffect } from './basePowers.js';
export class CreatePitEffect extends PowerEffect {
get name() {
return 'Create Pit';
}
get duration() {
return 5;
}
get icon() {
return 'icons/environment/traps/spike-skull-white-brown.webp';
}
get isTargeted() {
return false;
}
get basePowerPoints() {
return 2;
}
get usePrimaryEffect() {
return false;
}
get modifiers() {
return [
...super.modifiers,
{
name: 'Soft Ground',
id: 'soft',
type: 'checkbox',
default: false,
value: 1,
epic: false,
effect: false,
},
{
name: 'Spiked',
id: 'spiked',
type: 'checkbox',
default: false,
value: 1,
epic: false,
effect: false,
},
{
name: 'Deep',
id: 'deep',
type: 'checkbox',
default: false,
value: 2,
epic: false,
effect: false,
},
];
}
get damage() {
let dice = 2;
let mod = 2;
if (this.data.deep) {
dice += 2;
mod = 4;
}
if (this.data.spiked) {
dice += 1;
}
return { dice, mod };
}
get description() {
let text = super.description;
const deep = this.data.deep ? '8" (16 yards)' : '4" (8 yards)';
const damage = this.damage;
text += `<p>An extradimension pit appears as a hole the size of an MBT,
${deep} deep.
`;
if (this.data.spiked) {
text += 'The bottom is covered in spikes.';
}
text += `</p>
<p>Anyone in or adjacent to the area must make an Evasion roll
${this.data.raise ? '(at -2 from the raise)' : ''} or fall in.
`;
if (this.data.soft) {
text += 'The bottom is soft and does no damage.';
} else {
text += `Those who fall in take ${damage.dice}d6+${damage.mod} damage.`;
}
return text;
}
get maintEffectButtons() {
const damage = this.damage;
const buttons = super.maintEffectButtons;
if (!this.data.soft) {
buttons.push({
label: `Falling Damage (${damage.dice}d6+${damage.mod})`,
type: 'damage',
flavor: `Falling Damage${this.data.spiked ? ' with spikes!' : ''}`,
formula: `${damage.dice}d6x+${damage.mod}`,
});
}
return buttons;
}
}

View File

@ -1,64 +0,0 @@
import { PowerEffect } from './basePowers.js';
export 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',
type: 'checkbox',
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.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;
}
}

View File

@ -1,126 +0,0 @@
import { PowerEffect } from './basePowers.js';
export 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({
type: 'checkbox',
name: 'Area of Effect',
value: 2,
id: 'aoe',
epic: false,
effect: false,
});
mods.push({
type: 'checkbox',
name: 'Damage',
value: 2,
id: 'damage',
epic: false,
effect: false,
});
mods.push({
type: 'checkbox',
name: 'Greater Damage Field',
value: 4,
id: 'greater',
epic: true,
effect: false,
});
mods.push({
type: 'checkbox',
name: 'Mobile',
value: 2,
id: 'mobile',
epic: false,
effect: false,
});
return mods;
}
get primaryEffectButtons() {
const buttons = super.primaryEffectButtons;
let damage = '2d4';
if (this.data.greater) {
damage = '3d6';
} else if (this.data.damage) {
damage = '2d6';
}
buttons.push({
label: `Damage (${damage})`,
type: 'damage',
formula: `${damage}x`,
});
return buttons;
}
get description() {
let desc = super.description;
let area = 'all adjacent creatures';
let damage = '2d4';
if (this.data.greater) {
damage = '3d6 (heavy weapon)';
} else if (this.data.damage) {
damage = '2d6';
}
if (this.data.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.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.aoe ? 1.5 : 0.5,
priority,
mode,
},
{ key: `${base}.visibleTo`, value: [-1, 0, 1], priority, mode },
];
return changes;
}
}

View File

@ -1,69 +0,0 @@
import { PowerEffect } from './basePowers.js';
export 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',
type: 'checkbox',
value: 2,
id: 'greater',
epic: true,
effect: false,
});
return mods;
}
get description() {
let desc = super.description;
desc += '<p>';
if (this.data.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.greater) {
return 'Greater Darksight';
} else if (this.data.raise) {
return 'Major Darksight';
} else {
return 'Darksight';
}
}
}

View File

@ -1,58 +0,0 @@
import { PowerEffect } from './basePowers.js';
export 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: 'ranged' },
{ 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>`
);
}
}

View File

@ -1,139 +0,0 @@
import { PowerEffect } from './basePowers.js';
export 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({
type: 'checkbox',
name: 'Alignment Sense (detect)',
value: 1,
id: 'alignment',
epic: false,
effect: false,
});
mods.push({
type: 'checkbox',
name: 'Identify (detect)',
value: 1,
id: 'identify',
epic: false,
effect: false,
});
mods.push({
type: 'checkbox',
name: 'Strong (conceal)',
value: 1,
id: 'strong',
epic: false,
effect: false,
});
mods.push({
sortOrder: -2,
name: 'Detect or Conceal?',
id: 'direction',
type: 'radio',
default: 'Detect',
epic: false,
choices: { Detect: 'Detect', Conceal: 'Conceal' },
effects: { Detect: null, Conceal: null },
values: { Detect: 0, Conceal: 0 },
});
mods.push({
name: 'Area of Effect (conceal)',
type: 'select',
default: 'none',
epic: false,
choices: { none: 'None', mbt: 'Medium Blast Template', lbt: 'Large Blast Template' },
effects: { none: null, mbt: null, lbt: null },
values: { none: 0, mbt: 1, lbt: 2 },
});
return mods;
}
get hasAoe() {
return true;
}
async parseValues() {
await super.parseValues();
this.data.detect = this.data.direction == 'Detect';
}
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.identify) {
desc += `<p>Items detected also give the recipient an idea of their
powers and how to activate them.</p>`;
}
if (this.data.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.strong ? -2 : 0) + this.data.raise ? -2 : -4} penalty.`;
}
return desc;
}
}

View File

@ -1,56 +0,0 @@
import { PowerEffect } from './basePowers.js';
export 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',
type: 'checkbox',
value: 1,
id: 'size',
epic: false,
effect: false,
},
];
}
get description() {
const size = this.data.size ? 'within two sizes of' : 'of the same size as';
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 : -2}
as a free action to see through the disguise.</p>`
);
}
}

View File

@ -1,133 +0,0 @@
import { PowerEffect } from './basePowers.js';
export class DispelEffect extends PowerEffect {
get name() {
return 'Dispel';
}
get icon() {
return 'icons/magic/symbols/triangle-glowing-green.webp';
}
get duration() {
return this?.data?.antiMagic ? 5 : 0;
}
get usePrimaryEffect() {
return !!this?.data?.antiMagic;
}
get basePowerPoints() {
return 1;
}
get isTargeted() {
return true;
}
get hasAoe() {
return true;
}
get modifiers() {
return [
...super.modifiers,
{
name: 'Anti-Magic Field',
type: 'checkbox',
value: 8,
id: 'antiMagic',
epic: true,
effect: false,
},
{
type: 'select',
default: 'none',
name: 'Area of Effect',
id: 'aoe',
epic: false,
choices: {
none: 'None',
sbt: 'Small Blast Template',
mbt: 'Medium Blast Template',
lbt: 'Large Blast Template',
},
effects: { none: null, sbt: null, mbt: null, lbt: null },
values: { none: 0, sbt: 1, mbt: 2, lbt: 3 },
},
{
name: 'Disenchant',
type: 'checkbox',
value: 1,
id: 'disenchant',
epic: true,
effect: false,
},
{
name: 'Multiple Powers',
type: 'checkbox',
value: 8,
id: 'multiple',
epic: false,
effect: false,
},
{
name: 'Remove Curse',
type: 'checkbox',
value: 2,
id: 'removeCurse',
epic: true,
effect: false,
},
];
}
get effectName() {
return this.data.antiMagic ? 'Anti-Magic Field' : super.effectName;
}
getPrimaryEffectChanges() {
if (this.data.antiMagic) {
const base = 'flags.swade.auras.antiMagicField';
const priority = 0;
const mode = foundry.CONST.ACTIVE_EFFECT_MODES.OVERRIDE;
return [
{ key: `${base}.enabled`, value: true, priority, mode },
{ key: `${base}.walls`, value: true, priority, mode },
{ key: `${base}.color`, value: '#ff00cc', priority, mode },
{ key: `${base}.alpha`, value: 0.1, priority, mode },
{ key: `${base}.radius`, value: 1.5, priority, mode },
{ key: `${base}.visibleTo`, value: [-1, 0, 1], priority, mode },
];
}
return super.getPrimaryEffectChanges();
}
get description() {
let text = super.description;
if (this.data.antiMagic) {
text += `<p>Magic items, effects, and powers within the anti magic field
have no effect. Summoned creatures must make a Spirit roll each round or
or take a wound.</p>`;
return text;
}
const multi = this.data.multiple || this.data.aoe;
const affected = `${multi ? 'all' : 'a single'}
${this.data.disenchant ? 'magic item' : 'power'}${multi ? 's' : ''}
${this.data.aoe === 'none' ? 'cast by or on the recipient' : 'within a ' + this.data.aoe.toUpperCase()}`;
text += `<p>Attempt to dispel ${affected}. `;
if (this.data.disenchant) {
text += `The item(s) magical abilities are negated for ${this.data.raise ? 'two rounds' : 'one round'}`;
} else {
text += `Each target must make an opposed arcane skill (spirit for Mystic Powers)
roll or have the power(s) end immediately.`;
}
if (this.data.removeCurse) {
text += `The normal -2 penalty to remove a curse is ignored.`;
} else {
text += `If the effect is a Curse, there is a -2 penalty to the dispeller's roll`;
}
text += '</p>';
return text;
}
}

View File

@ -1,70 +0,0 @@
import { PowerEffect } from './basePowers.js';
export class DivinationEffect extends PowerEffect {
get name() {
return 'Divination';
}
get icon() {
return 'icons/magic/perception/orb-crystal-ball-scrying.webp';
}
get duration() {
return 0;
}
get isTargeted() {
return false;
}
get isRaisable() {
return true;
}
get basePowerPoints() {
return 5;
}
get modifiers() {
return [
{
name: 'Power (sacred ground only)',
type: 'checkbox',
value: 5,
id: 'power',
epic: true,
effect: false,
},
{
name: 'Sacred Ground',
type: 'checkbox',
value: 0,
id: 'sacred',
epic: false,
effect: false,
},
];
}
get description() {
let desc = super.description;
desc += `<p>A brief conversation with a summoned spirit or entity. `;
if (this.data.sacred) {
desc += `There is a +2 to the roll due to being on sacred ground for the
summoned entity. `;
}
if (this.data.raise) {
desc += `The entity will be generally helpful and more direct than usual
in answering questions.i `;
} else {
desc += `The entity will answer questions to the best of its ability, but
usually in a vague or symbolic manner.i `;
}
if (this.data.sacred && this.data.power) {
desc += `The entity will also offer unsolicted advice or information,
according to its nature.`;
}
desc += '</p>';
return desc;
}
}

Some files were not shown because too many files have changed in this diff Show More