Compare commits
258 Commits
shape-chan
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d1a4268bc | |||
| b9e6fc5075 | |||
| 56233e7c10 | |||
| 694a09d603 | |||
| e662242000 | |||
| 3187d625f0 | |||
| cba485285d | |||
| b7b8b5c54a | |||
| 59f9022e43 | |||
| 0153896eeb | |||
| 7775932702 | |||
| c670bd5dc6 | |||
| 03787ee314 | |||
| 469fad5ba5 | |||
| b41fd8833b | |||
| 0560cc5778 | |||
| f396636713 | |||
| c7193b3306 | |||
| 15436de599 | |||
| 298d892844 | |||
| a47b765c6f | |||
| 669c3799a0 | |||
| be74c79b35 | |||
| f900cb7d53 | |||
| a2fa9d3b05 | |||
| 7240fcf041 | |||
| e18419575c | |||
| ec4f3e6c63 | |||
| 9e09eaecbb | |||
| d3274905c1 | |||
| 78ce8661a4 | |||
| d8b9f554ed | |||
| 1352d62c4f | |||
| ba9eac716d | |||
| cd8b5ac7eb | |||
| f93418009d | |||
| a0bc314e0d | |||
| 89c357d6da | |||
| 88eac4f241 | |||
| 281ab0712e | |||
| 5427d738ff | |||
| 1c4595c72f | |||
| 1b4e07ef65 | |||
| f2a088390f | |||
| 26be798ae2 | |||
| 93d13a0b47 | |||
| 8e9827e5ae | |||
| abfed3204b | |||
| 22f6a49c65 | |||
| 4372c5460a | |||
| 969dab0a3e | |||
| 3a91311887 | |||
| 4e37f910a1 | |||
| 141b06adea | |||
| e240bb5bf1 | |||
| 2765a53815 | |||
| 06f26c264c | |||
| d14de3d385 | |||
| 39c2ce8d70 | |||
| 2ef597dc8c | |||
| 65d8ec2d39 | |||
| 8e206d1180 | |||
| 2721eb4a1e | |||
| f9824ad02e | |||
| b1a128b692 | |||
| 318cc36348 | |||
| 225b081f3c | |||
| 8f557297eb | |||
| 551161c6df | |||
| 946158a145 | |||
| 43cfa125ea | |||
| 94ee4243fa | |||
| 18cd705626 | |||
| 5111bd830c | |||
| 557cb6e546 | |||
| cc14b28d40 | |||
| d520f8e137 | |||
| 613552af4e | |||
| c68c55bfc7 | |||
| fe0e889c02 | |||
| 20871bcdef | |||
| 5cb15bcedd | |||
| 5965b89b66 | |||
| 929cdda415 | |||
| 57e6ab1bd3 | |||
| 1cb7abb7f5 | |||
| d5300a845b | |||
| 9ad0a23c33 | |||
| 4959a380ab | |||
| ba99dc08e2 | |||
| f4bfa13cab | |||
| e016437b73 | |||
| a6e2bc7bdc | |||
| 6fe743130d | |||
| 4bdd6176e8 | |||
| ac70a99b99 | |||
| b646524a97 | |||
| 805f50c631 | |||
| a8354b4983 | |||
| d6581e16d4 | |||
| 140381e1d1 | |||
| 0c0fb1a3a8 | |||
| 4ffed673db | |||
| 5c9556471e | |||
| 8aac513792 | |||
| e438fd36e8 | |||
| 08d2be4ea0 | |||
| 06306d8959 | |||
| 267e892f27 | |||
| 1207b9c1bd | |||
| 61e78936a7 | |||
| 7c28c43bd9 | |||
| e0cd1c640c | |||
| 864acdd722 | |||
| f80e551835 | |||
| 1cd4a08990 | |||
| 4908c6d4a7 | |||
| 27989ffdfb | |||
| ef1eb940d0 | |||
| d7f6582d41 | |||
| 3d6a561929 | |||
| b4fd301a4f | |||
| 3062e1bfcb | |||
| d85c11088a | |||
| ae3bc394e7 | |||
| 36520fcee2 | |||
| 4b087916b3 | |||
| 0d74ae787d | |||
| c76ce1ff7c | |||
| 53563af7d0 | |||
| b6a42818c8 | |||
| 909457d1dd | |||
| 6a7dd696ca | |||
| e92a2c3424 | |||
| 2dda9d76a4 | |||
| caee516d48 | |||
| 0d41527a5e | |||
| 2870b6b587 | |||
| db471b0cb9 | |||
| b6c16ed0f5 | |||
| a1246bf758 | |||
| 3357145ea9 | |||
| 4c00d7116e | |||
| 5151634e8b | |||
| 486a7d4167 | |||
| 1877b048d2 | |||
| 8c373fcc8b | |||
| 57caeccad2 | |||
| 2caa2a4633 | |||
| dea2fa95c4 | |||
| 7ee2f966c0 | |||
| f50a81a329 | |||
| cf30214502 | |||
| eaf5b8a066 | |||
| 6ec4d9019e | |||
| 8f015bc05c | |||
| ae7295028d | |||
| f962ee785e | |||
| b5b0ad01f6 | |||
| c8f27770b6 | |||
| 53d30e80e9 | |||
| 99657ea07f | |||
| a3fdf1c7f2 | |||
| 4c93c0be94 | |||
| 46feb5705e | |||
| d4f155adc2 | |||
| 4d44b6c31c | |||
| eccb4778cb | |||
| 13e6c9bcf7 | |||
| 312f2a88fc | |||
| 69bea4e77d | |||
| e6f37b173f | |||
| dd4735d50a | |||
| ca7acd7e9d | |||
| 5dfce6cbcc | |||
| 6100f425ed | |||
| ba7f3302c1 | |||
| 13f93898d6 | |||
| 8e50b478ce | |||
| d99d0c0728 | |||
| c679304ae2 | |||
| baa9d51c18 | |||
| 592d0c5406 | |||
| 2aa7af28a5 | |||
| 9333be95fd | |||
| b4089e4b71 | |||
| 391b76ccbd | |||
| 6fb821cc2a | |||
| df607d4d66 | |||
| b5f0794866 | |||
| 14c988c528 | |||
| a3733fa4cc | |||
| 6b5b1195ab | |||
| 4de06dac03 | |||
| 92aced8f40 | |||
| 3964e87982 | |||
| 8c7f7d2d1d | |||
| b8936ab53f | |||
| a943106a49 | |||
| 63b25faec0 | |||
| 2781de96da | |||
| b37378d0fd | |||
| 73c5ca6e3b | |||
| 261759d514 | |||
| 5514da4774 | |||
| 1834fec172 | |||
| 3fe1df7c7f | |||
| 8c06f0e2a1 | |||
| 6ed989c4bc | |||
| a0b2df50c6 | |||
| c43fafa7df | |||
| b50270a61f | |||
| 5ca758257e | |||
| 55a123759f | |||
| d5cc256c4f | |||
| 66ee916572 | |||
| 952511b182 | |||
| 85923d9e8e | |||
| 8f7881051b | |||
| af70593334 | |||
| 54da536518 | |||
| ad42634bd7 | |||
| eba1a7befc | |||
| 6e045714c8 | |||
| 8f4e8a365d | |||
| de690b1451 | |||
| 3996687ad5 | |||
| 9eb7f4c5ea | |||
| 0fba8a73b0 | |||
| 57b00686c8 | |||
| 5002134e2c | |||
| f4495df534 | |||
| 274b63d470 | |||
| b2ff0b29b9 | |||
| a10291619c | |||
| 3c0afc05d0 | |||
| a8ba0a64c0 | |||
| a5373585fb | |||
| 6ad1452922 | |||
| 9590fd8e0d | |||
| b79ca1b4a6 | |||
| d9917ac5c8 | |||
| 41e1ade442 | |||
| 416661727b | |||
| be55e94ea4 | |||
| acba9d1f45 | |||
| 8869248ab8 | |||
| ea4129c927 | |||
| f711e495a3 | |||
| 9e98425e63 | |||
| bdc8a0bd83 | |||
| e16b0277a2 | |||
| 8bc6f4c3dc | |||
| 11f3317aeb | |||
| 8f99a1a0ad | |||
| 3927291234 | |||
| 1d4223274c | |||
| 8750378977 |
9
.editorconfig
Normal file
9
.editorconfig
Normal file
@ -0,0 +1,9 @@
|
||||
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
15
.eslintrc.js
@ -1,15 +0,0 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
browser: true,
|
||||
es2021: true
|
||||
},
|
||||
extends: 'standard',
|
||||
overrides: [
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module'
|
||||
},
|
||||
rules: {
|
||||
}
|
||||
}
|
||||
6
.gitattributes
vendored
6
.gitattributes
vendored
@ -1,4 +1,8 @@
|
||||
packs/** binary
|
||||
packs/**/*.ldb binary
|
||||
packs/**/MANIFEST-* binary
|
||||
packs/**/CURRENT binary
|
||||
packs/**/LOCK binary
|
||||
packs/**/LOG* binary
|
||||
*.webp filter=lfs diff=lfs merge=lfs -text
|
||||
*.jpeg filter=lfs diff=lfs merge=lfs -text
|
||||
*.jpg filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
146
.gitignore
vendored
146
.gitignore
vendored
@ -1,132 +1,32 @@
|
||||
# ---> Node
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
# SPDX-FileCopyrightText: 2022 Johannes Loher
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
# IDE
|
||||
.idea/
|
||||
.vs/
|
||||
|
||||
# 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/
|
||||
jspm_packages/
|
||||
npm-debug.log
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
# yarn2
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/releases
|
||||
!.yarn/plugins
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
.pnp.*
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
# Local configuration
|
||||
foundryconfig.json
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
.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
|
||||
# Distribution files
|
||||
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
|
||||
# ESLint
|
||||
.eslintcache
|
||||
|
||||
# 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
|
||||
# Junit results
|
||||
junit.xml
|
||||
|
||||
5
.gulp.json
Normal file
5
.gulp.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"flags": {
|
||||
"gulpfile": "gulpfile.mjs"
|
||||
}
|
||||
}
|
||||
8
.prettierignore
Normal file
8
.prettierignore
Normal file
@ -0,0 +1,8 @@
|
||||
# SPDX-FileCopyrightText: 2022 Johannes Loher
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
dist
|
||||
package-lock.json
|
||||
.pnp.cjs
|
||||
.pnp.js
|
||||
11
.prettierrc.cjs
Normal file
11
.prettierrc.cjs
Normal file
@ -0,0 +1,11 @@
|
||||
// SPDX-FileCopyrightText: 2022 Johannes Loher
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
module.exports = {
|
||||
semi: true,
|
||||
trailingComma: 'all',
|
||||
singleQuote: true,
|
||||
printWidth: 120,
|
||||
tabWidth: 2,
|
||||
};
|
||||
322
CHANGELOG.md
322
CHANGELOG.md
@ -5,9 +5,287 @@ 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/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
## [4.1.0]
|
||||
|
||||
## 1.2.0
|
||||
### Added
|
||||
|
||||
- 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
|
||||
|
||||
- BREAKING CHANGE: changed from creating a global `swadeMBHelpers` to requiring
|
||||
`game.modules.get('swade-mb-helpers').api` instead
|
||||
- FLOW change: Smite's modifier will still show the weapon affected, but the
|
||||
modifier is a global modifier that can be ignored in the roll dialog or the
|
||||
effect can be turned off.
|
||||
- Added the following Power Effects
|
||||
- Burrow
|
||||
- Darksight
|
||||
- Detect/Conceal Arcana
|
||||
- Disguise
|
||||
- Shape Change
|
||||
- Sloth/Speed
|
||||
|
||||
## [1.2.0]
|
||||
|
||||
### Changed
|
||||
|
||||
@ -16,20 +294,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
|
||||
effects
|
||||
|
||||
## 1.1.0
|
||||
## [1.1.0]
|
||||
|
||||
### Added
|
||||
|
||||
- gold calculator macro for SWPF gold items
|
||||
- Actions for common rolls with links to SWPF rules
|
||||
|
||||
## 1.0.1
|
||||
## [1.0.1]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Summon macro now spawns tokens with prototype token's actual dimensions
|
||||
|
||||
## 1.0.0
|
||||
## [1.0.0]
|
||||
|
||||
### Added
|
||||
|
||||
@ -44,7 +322,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
|
||||
|
||||
## 0.9.0
|
||||
## [0.9.0]
|
||||
|
||||
- Initial 'public' release
|
||||
|
||||
@ -52,20 +330,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
- DEPENDENCY: warpgate
|
||||
- macro helpers for the following effects:
|
||||
- Boost/Lower Trait
|
||||
- Deflection
|
||||
- Glow
|
||||
- Hinder
|
||||
- Hurry
|
||||
- Lingering Damage
|
||||
- Protection
|
||||
- Shroud
|
||||
- Smite
|
||||
- Summon
|
||||
- Boost/Lower Trait
|
||||
- Deflection
|
||||
- Glow
|
||||
- Hinder
|
||||
- Hurry
|
||||
- Lingering Damage
|
||||
- Protection
|
||||
- Shroud
|
||||
- Smite
|
||||
- Summon
|
||||
- API helpers:
|
||||
- `createEffectDocument`
|
||||
- `createMutationWithEffect`
|
||||
- `defaultMutationOptions`
|
||||
- `getActorFolderByPath`
|
||||
- `getActorsInFolder`
|
||||
- `runOnTargetOrSelectedTokens`
|
||||
- `createEffectDocument`
|
||||
- `createMutationWithEffect`
|
||||
- `defaultMutationOptions`
|
||||
- `getActorFolderByPath`
|
||||
- `getActorsInFolder`
|
||||
- `runOnTargetOrSelectedTokens`
|
||||
|
||||
73
README.md
73
README.md
@ -3,3 +3,76 @@
|
||||
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
|
||||
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/
|
||||
|
||||
23
eslint.config.js
Normal file
23
eslint.config.js
Normal file
@ -0,0 +1,23 @@
|
||||
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',
|
||||
},
|
||||
}
|
||||
];
|
||||
12
fvtt-pack.sh
Executable file
12
fvtt-pack.sh
Executable file
@ -0,0 +1,12 @@
|
||||
#!/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
|
||||
11
fvtt-unpack.sh
Executable file
11
fvtt-unpack.sh
Executable file
@ -0,0 +1,11 @@
|
||||
#!/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
|
||||
183
gulpfile.mjs
Normal file
183
gulpfile.mjs
Normal file
@ -0,0 +1,183 @@
|
||||
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.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
28
macros/addEffectToPowers.js
Normal file
28
macros/addEffectToPowers.js
Normal file
@ -0,0 +1,28 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -1,45 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@ -1,133 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@ -1,62 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@ -1,37 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@ -1,74 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@ -1,58 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,33 +1,33 @@
|
||||
let tokens = []
|
||||
let tokens = [];
|
||||
if (canvas.tokens.controlled.length > 0) {
|
||||
tokens = canvas.tokens.controlled
|
||||
tokens = canvas.tokens.controlled;
|
||||
}
|
||||
if (tokens.length > 0) {
|
||||
main(tokens)
|
||||
main(tokens);
|
||||
} else {
|
||||
ui.notifications.error('Please select or target a token')
|
||||
ui.notifications.error('Please select or target a token');
|
||||
}
|
||||
|
||||
async function main (tokens) {
|
||||
const currencies = ['Copper', 'Silver', 'Gold', 'Platinum']
|
||||
let template = '<div><table><thead><tr><th>Actor</th><th>Currency</th></tr></thead><tbody>'
|
||||
async function main(tokens) {
|
||||
const currencies = ['Copper', 'Silver', 'Gold', 'Platinum'];
|
||||
let template = '<div><table><thead><tr><th>Actor</th><th>Currency</th></tr></thead><tbody>';
|
||||
const fmtOptions = {
|
||||
minimumIntegerDigits: 1,
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
}
|
||||
const fmt = Intl.NumberFormat('en-US', fmtOptions)
|
||||
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
|
||||
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 += `<tr><td>${actor.name}</td><td>${fmt.format(total)}</td></tr>`;
|
||||
}
|
||||
template += '</thead></tbody>'
|
||||
Dialog.prompt({
|
||||
title: 'Currency Totals',
|
||||
content: template
|
||||
})
|
||||
template += '</thead></tbody>';
|
||||
foundry.applications.api.DialogV2.prompt({
|
||||
window: { title: 'Currency Totals' },
|
||||
content: template,
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,38 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@ -1,38 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@ -1,41 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@ -1,44 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@ -1,32 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
4
macros/powerMenu.js
Normal file
4
macros/powerMenu.js
Normal file
@ -0,0 +1,4 @@
|
||||
game.modules.get('swade-mb-helpers').api.powerEffectsMenu({
|
||||
token,
|
||||
targets: game.user.targets,
|
||||
});
|
||||
@ -1,45 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
43
macros/quickDamage.js
Normal file
43
macros/quickDamage.js
Normal file
@ -0,0 +1,43 @@
|
||||
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);
|
||||
42
macros/requestFearRoll.js
Normal file
42
macros/requestFearRoll.js
Normal file
@ -0,0 +1,42 @@
|
||||
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();
|
||||
95
macros/requestRoll.js
Normal file
95
macros/requestRoll.js
Normal file
@ -0,0 +1,95 @@
|
||||
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();
|
||||
37
macros/requestRollMATT.js
Normal file
37
macros/requestRollMATT.js
Normal file
@ -0,0 +1,37 @@
|
||||
// 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()
|
||||
156
macros/setTokenVision.js
Normal file
156
macros/setTokenVision.js
Normal file
@ -0,0 +1,156 @@
|
||||
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();
|
||||
@ -1,58 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,74 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@ -1,74 +0,0 @@
|
||||
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)
|
||||
}
|
||||
14718
package-lock.json
generated
14718
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
56
package.json
56
package.json
@ -1,11 +1,53 @@
|
||||
{
|
||||
"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": {
|
||||
"@league-of-foundry-developers/foundry-vtt-types": "^9.280.0",
|
||||
"eslint": "^8.48.0",
|
||||
"eslint-config-standard": "^17.1.0",
|
||||
"eslint-plugin-import": "^2.28.1",
|
||||
"eslint-plugin-n": "^16.0.2",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"typescript": "^5.2.2"
|
||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||
"@rollup/stream": "^3.0.1",
|
||||
"@typhonjs-fvtt/eslint-config-foundry.js": "^0.8.0",
|
||||
"eslint": "^9.2.0",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"fs-extra": "^11.2.0",
|
||||
"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.
@ -1 +0,0 @@
|
||||
MANIFEST-000138
|
||||
@ -1,8 +0,0 @@
|
||||
2023/09/25-22:33:46.979390 7fe7b968d700 Recovering log #135
|
||||
2023/09/25-22:33:46.999374 7fe7b968d700 Delete type=3 #133
|
||||
2023/09/25-22:33:46.999397 7fe7b968d700 Delete type=0 #135
|
||||
2023/09/25-22:34:49.573980 7fe51bfff700 Level-0 table #141: started
|
||||
2023/09/25-22:34:49.573997 7fe51bfff700 Level-0 table #141: 0 bytes OK
|
||||
2023/09/25-22:34:49.583422 7fe51bfff700 Delete type=0 #139
|
||||
2023/09/25-22:34:49.602253 7fe51bfff700 Manual compaction at level-0 from '!folders!0nDRFmMBs5DBJU9M' @ 72057594037927935 : 1 .. '!items!xA7qKMmugJv7z6j1' @ 0 : 0; will stop at (end)
|
||||
2023/09/25-22:34:49.611494 7fe51bfff700 Manual compaction at level-1 from '!folders!0nDRFmMBs5DBJU9M' @ 72057594037927935 : 1 .. '!items!xA7qKMmugJv7z6j1' @ 0 : 0; will stop at (end)
|
||||
@ -1,15 +0,0 @@
|
||||
2023/09/24-17:51:32.119358 7fe7b9e8e700 Recovering log #132
|
||||
2023/09/24-17:51:32.127144 7fe7b9e8e700 Delete type=3 #131
|
||||
2023/09/24-17:51:32.127174 7fe7b9e8e700 Delete type=0 #132
|
||||
2023/09/25-22:27:28.458282 7fe51bfff700 Level-0 table #136: started
|
||||
2023/09/25-22:27:28.461383 7fe51bfff700 Level-0 table #136: 5360 bytes OK
|
||||
2023/09/25-22:27:28.464487 7fe51bfff700 Delete type=0 #134
|
||||
2023/09/25-22:27:28.468877 7fe51bfff700 Manual compaction at level-0 from '!folders!0nDRFmMBs5DBJU9M' @ 72057594037927935 : 1 .. '!items!xA7qKMmugJv7z6j1' @ 0 : 0; will stop at (end)
|
||||
2023/09/25-22:27:28.468974 7fe51bfff700 Manual compaction at level-1 from '!folders!0nDRFmMBs5DBJU9M' @ 72057594037927935 : 1 .. '!items!xA7qKMmugJv7z6j1' @ 0 : 0; will stop at '!items!vdYpJXIMdqUi7IDh' @ 170 : 1
|
||||
2023/09/25-22:27:28.468980 7fe51bfff700 Compacting 1@1 + 1@2 files
|
||||
2023/09/25-22:27:28.471549 7fe51bfff700 Generated table #137@1: 17 keys, 8737 bytes
|
||||
2023/09/25-22:27:28.471557 7fe51bfff700 Compacted 1@1 + 1@2 files => 8737 bytes
|
||||
2023/09/25-22:27:28.474310 7fe51bfff700 compacted to: files[ 0 0 1 0 0 0 0 ]
|
||||
2023/09/25-22:27:28.474351 7fe51bfff700 Delete type=2 #136
|
||||
2023/09/25-22:27:28.474385 7fe51bfff700 Delete type=2 #130
|
||||
2023/09/25-22:27:28.482548 7fe51bfff700 Manual compaction at level-1 from '!items!vdYpJXIMdqUi7IDh' @ 170 : 1 .. '!items!xA7qKMmugJv7z6j1' @ 0 : 0; will stop at (end)
|
||||
Binary file not shown.
Binary file not shown.
@ -1 +0,0 @@
|
||||
MANIFEST-000040
|
||||
@ -1,8 +0,0 @@
|
||||
2023/09/25-22:33:47.001326 7fe7b9e8e700 Recovering log #38
|
||||
2023/09/25-22:33:47.021271 7fe7b9e8e700 Delete type=0 #38
|
||||
2023/09/25-22:33:47.021294 7fe7b9e8e700 Delete type=3 #36
|
||||
2023/09/25-22:34:49.621185 7fe51bfff700 Level-0 table #43: started
|
||||
2023/09/25-22:34:49.621202 7fe51bfff700 Level-0 table #43: 0 bytes OK
|
||||
2023/09/25-22:34:49.630680 7fe51bfff700 Delete type=0 #41
|
||||
2023/09/25-22:34:49.650161 7fe51bfff700 Manual compaction at level-0 from '!actors!U5v4gFHquo0Y1SAq' @ 72057594037927935 : 1 .. '!actors!U5v4gFHquo0Y1SAq' @ 0 : 0; will stop at (end)
|
||||
2023/09/25-22:34:49.659391 7fe51bfff700 Manual compaction at level-1 from '!actors!U5v4gFHquo0Y1SAq' @ 72057594037927935 : 1 .. '!actors!U5v4gFHquo0Y1SAq' @ 0 : 0; will stop at (end)
|
||||
@ -1,8 +0,0 @@
|
||||
2023/09/24-17:51:32.129349 7fe7ba68f700 Recovering log #35
|
||||
2023/09/24-17:51:32.137024 7fe7ba68f700 Delete type=0 #35
|
||||
2023/09/24-17:51:32.137042 7fe7ba68f700 Delete type=3 #34
|
||||
2023/09/25-22:27:28.464580 7fe51bfff700 Level-0 table #39: started
|
||||
2023/09/25-22:27:28.464601 7fe51bfff700 Level-0 table #39: 0 bytes OK
|
||||
2023/09/25-22:27:28.468758 7fe51bfff700 Delete type=0 #37
|
||||
2023/09/25-22:27:28.468891 7fe51bfff700 Manual compaction at level-0 from '!actors!U5v4gFHquo0Y1SAq' @ 72057594037927935 : 1 .. '!actors!U5v4gFHquo0Y1SAq' @ 0 : 0; will stop at (end)
|
||||
2023/09/25-22:27:28.482519 7fe51bfff700 Manual compaction at level-1 from '!actors!U5v4gFHquo0Y1SAq' @ 72057594037927935 : 1 .. '!actors!U5v4gFHquo0Y1SAq' @ 0 : 0; will stop at (end)
|
||||
Binary file not shown.
Binary file not shown.
@ -1 +0,0 @@
|
||||
MANIFEST-000168
|
||||
@ -1,8 +0,0 @@
|
||||
2023/09/25-22:33:46.957940 7fe7b9e8e700 Recovering log #165
|
||||
2023/09/25-22:33:46.977376 7fe7b9e8e700 Delete type=0 #165
|
||||
2023/09/25-22:33:46.977395 7fe7b9e8e700 Delete type=3 #163
|
||||
2023/09/25-22:34:49.583492 7fe51bfff700 Level-0 table #171: started
|
||||
2023/09/25-22:34:49.583508 7fe51bfff700 Level-0 table #171: 0 bytes OK
|
||||
2023/09/25-22:34:49.592987 7fe51bfff700 Delete type=0 #169
|
||||
2023/09/25-22:34:49.611444 7fe51bfff700 Manual compaction at level-0 from '!folders!hIbrWxg1nDutCSwt' @ 72057594037927935 : 1 .. '!macros!wU2mAUnw3RW9qMT8' @ 0 : 0; will stop at (end)
|
||||
2023/09/25-22:34:49.611542 7fe51bfff700 Manual compaction at level-1 from '!folders!hIbrWxg1nDutCSwt' @ 72057594037927935 : 1 .. '!macros!wU2mAUnw3RW9qMT8' @ 0 : 0; will stop at (end)
|
||||
@ -1,15 +0,0 @@
|
||||
2023/09/24-17:51:32.110592 7fe7ba68f700 Recovering log #162
|
||||
2023/09/24-17:51:32.118170 7fe7ba68f700 Delete type=3 #161
|
||||
2023/09/24-17:51:32.118190 7fe7ba68f700 Delete type=0 #162
|
||||
2023/09/25-22:27:28.431163 7fe51bfff700 Level-0 table #166: started
|
||||
2023/09/25-22:27:28.433469 7fe51bfff700 Level-0 table #166: 724 bytes OK
|
||||
2023/09/25-22:27:28.436317 7fe51bfff700 Delete type=0 #164
|
||||
2023/09/25-22:27:28.445034 7fe51bfff700 Manual compaction at level-0 from '!folders!hIbrWxg1nDutCSwt' @ 72057594037927935 : 1 .. '!macros!wU2mAUnw3RW9qMT8' @ 0 : 0; will stop at (end)
|
||||
2023/09/25-22:27:28.445112 7fe51bfff700 Manual compaction at level-1 from '!folders!hIbrWxg1nDutCSwt' @ 72057594037927935 : 1 .. '!macros!wU2mAUnw3RW9qMT8' @ 0 : 0; will stop at '!macros!8gxeYSUJ1FQhmJRw' @ 37 : 1
|
||||
2023/09/25-22:27:28.445116 7fe51bfff700 Compacting 1@1 + 1@2 files
|
||||
2023/09/25-22:27:28.449145 7fe51bfff700 Generated table #167@1: 19 keys, 4653 bytes
|
||||
2023/09/25-22:27:28.449155 7fe51bfff700 Compacted 1@1 + 1@2 files => 4653 bytes
|
||||
2023/09/25-22:27:28.452097 7fe51bfff700 compacted to: files[ 0 0 1 0 0 0 0 ]
|
||||
2023/09/25-22:27:28.452136 7fe51bfff700 Delete type=2 #128
|
||||
2023/09/25-22:27:28.452197 7fe51bfff700 Delete type=2 #166
|
||||
2023/09/25-22:27:28.468818 7fe51bfff700 Manual compaction at level-1 from '!macros!8gxeYSUJ1FQhmJRw' @ 37 : 1 .. '!macros!wU2mAUnw3RW9qMT8' @ 0 : 0; will stop at (end)
|
||||
Binary file not shown.
Binary file not shown.
@ -1 +0,0 @@
|
||||
MANIFEST-000168
|
||||
@ -1,8 +0,0 @@
|
||||
2023/09/25-22:33:46.936754 7fe7b968d700 Recovering log #165
|
||||
2023/09/25-22:33:46.956358 7fe7b968d700 Delete type=0 #165
|
||||
2023/09/25-22:33:46.956379 7fe7b968d700 Delete type=3 #163
|
||||
2023/09/25-22:34:49.564751 7fe51bfff700 Level-0 table #171: started
|
||||
2023/09/25-22:34:49.564770 7fe51bfff700 Level-0 table #171: 0 bytes OK
|
||||
2023/09/25-22:34:49.573732 7fe51bfff700 Delete type=0 #169
|
||||
2023/09/25-22:34:49.573869 7fe51bfff700 Manual compaction at level-0 from '!journal!HbtPlHNFO1L6RVj0' @ 72057594037927935 : 1 .. '!journal.pages!Mw1g2Fx5dp4SoqVP.lhULHNp4gz9IjOR3' @ 0 : 0; will stop at (end)
|
||||
2023/09/25-22:34:49.583466 7fe51bfff700 Manual compaction at level-1 from '!journal!HbtPlHNFO1L6RVj0' @ 72057594037927935 : 1 .. '!journal.pages!Mw1g2Fx5dp4SoqVP.lhULHNp4gz9IjOR3' @ 0 : 0; will stop at (end)
|
||||
@ -1,15 +0,0 @@
|
||||
2023/09/24-17:51:32.100602 7fe7b9e8e700 Recovering log #162
|
||||
2023/09/24-17:51:32.108259 7fe7b9e8e700 Delete type=3 #161
|
||||
2023/09/24-17:51:32.108296 7fe7b9e8e700 Delete type=0 #162
|
||||
2023/09/25-22:27:28.439755 7fe51bfff700 Level-0 table #166: started
|
||||
2023/09/25-22:27:28.442121 7fe51bfff700 Level-0 table #166: 10961 bytes OK
|
||||
2023/09/25-22:27:28.444862 7fe51bfff700 Delete type=0 #164
|
||||
2023/09/25-22:27:28.445099 7fe51bfff700 Manual compaction at level-0 from '!journal!HbtPlHNFO1L6RVj0' @ 72057594037927935 : 1 .. '!journal.pages!Mw1g2Fx5dp4SoqVP.lhULHNp4gz9IjOR3' @ 0 : 0; will stop at (end)
|
||||
2023/09/25-22:27:28.452245 7fe51bfff700 Manual compaction at level-1 from '!journal!HbtPlHNFO1L6RVj0' @ 72057594037927935 : 1 .. '!journal.pages!Mw1g2Fx5dp4SoqVP.lhULHNp4gz9IjOR3' @ 0 : 0; will stop at '!journal.pages!Mw1g2Fx5dp4SoqVP.lhULHNp4gz9IjOR3' @ 63 : 1
|
||||
2023/09/25-22:27:28.452253 7fe51bfff700 Compacting 1@1 + 1@2 files
|
||||
2023/09/25-22:27:28.455468 7fe51bfff700 Generated table #167@1: 7 keys, 4142 bytes
|
||||
2023/09/25-22:27:28.455475 7fe51bfff700 Compacted 1@1 + 1@2 files => 4142 bytes
|
||||
2023/09/25-22:27:28.458177 7fe51bfff700 compacted to: files[ 0 0 1 0 0 0 0 ]
|
||||
2023/09/25-22:27:28.458213 7fe51bfff700 Delete type=2 #166
|
||||
2023/09/25-22:27:28.458245 7fe51bfff700 Delete type=2 #96
|
||||
2023/09/25-22:27:28.468848 7fe51bfff700 Manual compaction at level-1 from '!journal.pages!Mw1g2Fx5dp4SoqVP.lhULHNp4gz9IjOR3' @ 63 : 1 .. '!journal.pages!Mw1g2Fx5dp4SoqVP.lhULHNp4gz9IjOR3' @ 0 : 0; will stop at (end)
|
||||
Binary file not shown.
80
release.sh
Executable file
80
release.sh
Executable file
@ -0,0 +1,80 @@
|
||||
#!/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}" \
|
||||
16
rollup.config.mjs
Normal file
16
rollup.config.mjs
Normal file
@ -0,0 +1,16 @@
|
||||
// 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()],
|
||||
});
|
||||
@ -1,23 +0,0 @@
|
||||
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 () {
|
||||
globalThis.swadeMBHelpers = {
|
||||
DEBUG: true,
|
||||
powerEffects,
|
||||
createEffectDocument: shim.createEffectDocument,
|
||||
createMutationWithEffect: helpers.createMutationWithEffect,
|
||||
defaultMutationOptions: helpers.defaultMutationOptions,
|
||||
getActorFolderByPath: shim.getActorFolderByPath,
|
||||
getActorsInFolder: shim.getActorsInFolder,
|
||||
runOnTargetOrSelectedTokens: helpers.runOnTargetOrSelectedTokens
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,92 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@ -1,20 +0,0 @@
|
||||
import { api } from './api.js'
|
||||
import { shapeChangeOnDismiss } from './powerEffects.js'
|
||||
import { log } from './shim.js'
|
||||
|
||||
function _checkModule (name) {
|
||||
if (!game.modules.get(name)?.active && game.user.isGM) {
|
||||
let action = 'install and activate'
|
||||
if (game.modules.get(name)) action = 'activate'
|
||||
ui.notifications.error(
|
||||
`SWADE MB Helpers requires the ${name} module. Please ${action} it.`)
|
||||
}
|
||||
}
|
||||
|
||||
Hooks.on('setup', api.registerFunctions)
|
||||
|
||||
Hooks.on('ready', () => {
|
||||
_checkModule('warpgate')
|
||||
log('Initialized SWADE MB Helpers')
|
||||
warpgate.event.watch(warpgate.EVENT.DISMISS, shapeChangeOnDismiss)
|
||||
})
|
||||
@ -1,1256 +0,0 @@
|
||||
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 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 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 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 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 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()
|
||||
log('protoDoc', this.protoDoc)
|
||||
log('spawnOptions', this.spawnOptions)
|
||||
log('spawnMutation', this.spawnMutation)
|
||||
await shim.warpgateSpawn(this.protoDoc, this.spawnMutation, {}, this.spawnOptions)
|
||||
}
|
||||
}
|
||||
|
||||
class SummonAllyEffect extends SummonEffect {
|
||||
get name () {
|
||||
return 'Summon Ally'
|
||||
}
|
||||
|
||||
get mirrorFolder () {
|
||||
return `${this.actorFolderBase}/Mirror Selves`
|
||||
}
|
||||
|
||||
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 actorFolder = shim.getActorFolderByPath(this.mirrorFolder)
|
||||
const oldActor = actorFolder.contents.find(a => a.name === `Mirror ${this.token.name}`)
|
||||
if (oldActor) {
|
||||
await oldActor.delete()
|
||||
}
|
||||
const actorDoc = this.token.actor.clone({
|
||||
type: 'npc',
|
||||
name: `Mirror ${this.token.actor.name}`,
|
||||
folder: actorFolder.id,
|
||||
'system.wildcard': false,
|
||||
'system.fatigue.value': 0,
|
||||
'system.wounds.value': 0,
|
||||
'system.wounds.max': 0,
|
||||
'system.bennies.max': 0,
|
||||
'system.bennies.value': 0,
|
||||
'prototypeToken.actorLink': false,
|
||||
'prototypeToken.name': `Mirror ${this.token.name}`,
|
||||
'prototypeToken.texture.scaleX': this.token.document.texture.scaleX * -1
|
||||
})
|
||||
const mirrorActor = this.actor
|
||||
this.actor = await shim.Actor.create(actorDoc)
|
||||
this.actorId = this.actor.id
|
||||
this.icon = this.actor.prototypeToken.texture.src
|
||||
this.protoDoc = await this.actor.getTokenDocument()
|
||||
this.spawnOptions.crosshairs.icon = this.icon
|
||||
for (const mirrorItem of mirrorActor.items) {
|
||||
this.spawnMutation.embedded.Item[mirrorItem.name] =
|
||||
await mirrorActor.getEmbeddedDocument('Item', mirrorItem.id)
|
||||
}
|
||||
this.spawnMutation.embedded.Item['Summon Ally'] = CONST.WARPGATE.DELETE
|
||||
const effectChanges = []
|
||||
for (const item of this.token.actor.items.filter(i => i.type === 'skill')) {
|
||||
effectChanges.push({
|
||||
key: `@Skill{${item.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,
|
||||
blast: BlastEffect,
|
||||
blind: BlindEffect,
|
||||
bolt: BoltEffect,
|
||||
'boost/lower trait': BoostLowerTraitEffect,
|
||||
'boost trait': BoostLowerTraitEffect,
|
||||
burst: BurstEffect,
|
||||
confusion: ConfusionEffect,
|
||||
deflection: DeflectionEffect,
|
||||
entangle: EntangleEffect,
|
||||
intangibility: IntangibilityEffect,
|
||||
invisibility: InvisibilityEffect,
|
||||
'lower trait': BoostLowerTraitEffect,
|
||||
protection: ProtectionEffect,
|
||||
'shape change': ShapeChangeEffect,
|
||||
smite: SmiteEffect,
|
||||
'summon ally': SummonAllyEffect,
|
||||
'summon animal': SummonAnimalEffect,
|
||||
'summon monster': SummonMonsterEffect,
|
||||
"summon nature's ally": SummonNaturesAllyEffect,
|
||||
'summon planar ally': SummonPlanarAllyEffect,
|
||||
'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 = 'name' in options ? options.name : (item !== null ? 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
163
scripts/shim.js
@ -1,163 +0,0 @@
|
||||
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)
|
||||
}
|
||||
1283
src/config/token-variants-global-mappings.json
Executable file
1283
src/config/token-variants-global-mappings.json
Executable file
@ -0,0 +1,1283 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
]
|
||||
}
|
||||
111
src/config/torch_swade.json
Normal file
111
src/config/torch_swade.json
Normal file
@ -0,0 +1,111 @@
|
||||
{
|
||||
"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 }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
6
src/lang/en.json
Normal file
6
src/lang/en.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"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)."
|
||||
}
|
||||
@ -1,19 +1,24 @@
|
||||
{
|
||||
"id": "swade-mb-helpers",
|
||||
"title": "SWADE Helpers (MB)",
|
||||
"description": "Mike's collection of swade helpers",
|
||||
"version": "1.2.0",
|
||||
"description": "Mike's collection of SWADE helpers",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Mike"
|
||||
"name": "Mike",
|
||||
"flags": {}
|
||||
}
|
||||
],
|
||||
"url": "https://git.bloy.org/foundryvtt/swade-mb-helpers",
|
||||
"version": "4.1.0",
|
||||
"compatibility": {
|
||||
"minimum": "11",
|
||||
"verified": "11"
|
||||
"minimum": "13",
|
||||
"verified": "13"
|
||||
},
|
||||
"esmodules": [
|
||||
"scripts/module.js"
|
||||
"module/swade-mb-helpers.js"
|
||||
],
|
||||
"styles": [
|
||||
"styles/swade-mb-helpers.css"
|
||||
],
|
||||
"packs": [
|
||||
{
|
||||
@ -39,7 +44,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Common Actions",
|
||||
"name": "CommonActions",
|
||||
"label": "SWADE MB Common Actions",
|
||||
"path": "packs/common-actions",
|
||||
"type": "Item",
|
||||
@ -54,11 +59,33 @@
|
||||
"label": "SWADE MB Helper Actors",
|
||||
"path": "packs/helper-actors",
|
||||
"type": "Actor",
|
||||
"system": "swade",
|
||||
"ownership": {
|
||||
"PLAYER": "OBSERVER",
|
||||
"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": [
|
||||
@ -71,7 +98,9 @@
|
||||
"module-docs",
|
||||
"helper-macros",
|
||||
"helper-actors",
|
||||
"Common Actions"
|
||||
"Common Actions",
|
||||
"swade-mb-gear",
|
||||
"power-actors"
|
||||
]
|
||||
}
|
||||
],
|
||||
@ -82,36 +111,55 @@
|
||||
"type": "system",
|
||||
"manifest": "https://gitlab.com/api/v4/projects/16269883/packages/generic/swade/latest/system.json",
|
||||
"compatibility": {
|
||||
"verified": "2.2.5"
|
||||
"minimum": "5.1.0",
|
||||
"verified": "5.1.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requires": [
|
||||
{
|
||||
"id": "warpgate",
|
||||
"type": "module",
|
||||
"manifest": "https://github.com/trioderegion/warpgate/releases/latest/download/module.json",
|
||||
"compatibility": {
|
||||
"verified": "1.16.2"
|
||||
}
|
||||
}
|
||||
],
|
||||
"recommends": [
|
||||
{
|
||||
"id": "token-variants",
|
||||
"id": "socketlib",
|
||||
"type": "module",
|
||||
"compatibility": {}
|
||||
},
|
||||
{
|
||||
"id": "tcal",
|
||||
"type": "module",
|
||||
"compatibility": {}
|
||||
},
|
||||
{
|
||||
"id": "sequencer",
|
||||
"type": "module",
|
||||
"compatibility": {}
|
||||
}
|
||||
],
|
||||
"recommends": [
|
||||
{
|
||||
"id": "torch",
|
||||
"type": "module",
|
||||
"compatibility": {}
|
||||
},
|
||||
{
|
||||
"id": "JB2A_DnD5e",
|
||||
"type": "module",
|
||||
"compatibility": {}
|
||||
},
|
||||
{
|
||||
"id": "visual-active-effects",
|
||||
"type": "module",
|
||||
"compatibility": {}
|
||||
}
|
||||
]
|
||||
},
|
||||
"url": "https://git.bloy.org/foundryvtt/swade-mb-helpers",
|
||||
"manifest": "https://git.bloy.org/foundryvtt/swade-mb-helpers/raw/branch/main/module.json",
|
||||
"download": "https://git.bloy.org/foundryvtt/swade-mb-helpers/archive/main.zip",
|
||||
"license": "./LICENSE",
|
||||
"readme": "./README.md"
|
||||
"languages": [
|
||||
{
|
||||
"lang": "en",
|
||||
"name": "English",
|
||||
"path": "lang/en.json",
|
||||
"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"
|
||||
}
|
||||
23
src/module/api.js
Normal file
23
src/module/api.js
Normal file
@ -0,0 +1,23 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
95
src/module/globals.js
Normal file
95
src/module/globals.js
Normal file
@ -0,0 +1,95 @@
|
||||
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');
|
||||
}
|
||||
}
|
||||
206
src/module/helpers.js
Normal file
206
src/module/helpers.js
Normal file
@ -0,0 +1,206 @@
|
||||
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 }],
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
63
src/module/powers/arcaneProtection.js
Normal file
63
src/module/powers/arcaneProtection.js
Normal file
@ -0,0 +1,63 @@
|
||||
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})`;
|
||||
}
|
||||
}
|
||||
196
src/module/powers/balefulPolymorph.js
Normal file
196
src/module/powers/balefulPolymorph.js
Normal file
@ -0,0 +1,196 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
92
src/module/powers/banish.js
Normal file
92
src/module/powers/banish.js
Normal file
@ -0,0 +1,92 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
120
src/module/powers/barrier.js
Normal file
120
src/module/powers/barrier.js
Normal file
@ -0,0 +1,120 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
1076
src/module/powers/basePowers.js
Normal file
1076
src/module/powers/basePowers.js
Normal file
@ -0,0 +1,1076 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
90
src/module/powers/beastFriend.js
Normal file
90
src/module/powers/beastFriend.js
Normal file
@ -0,0 +1,90 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
70
src/module/powers/blast.js
Normal file
70
src/module/powers/blast.js
Normal file
@ -0,0 +1,70 @@
|
||||
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>`;
|
||||
}
|
||||
}
|
||||
126
src/module/powers/blind.js
Normal file
126
src/module/powers/blind.js
Normal file
@ -0,0 +1,126 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
94
src/module/powers/bolt.js
Normal file
94
src/module/powers/bolt.js
Normal file
@ -0,0 +1,94 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
221
src/module/powers/boostLowerTrait.js
Normal file
221
src/module/powers/boostLowerTrait.js
Normal file
@ -0,0 +1,221 @@
|
||||
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];
|
||||
}
|
||||
}
|
||||
78
src/module/powers/burrow.js
Normal file
78
src/module/powers/burrow.js
Normal file
@ -0,0 +1,78 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
63
src/module/powers/burst.js
Normal file
63
src/module/powers/burst.js
Normal file
@ -0,0 +1,63 @@
|
||||
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>`;
|
||||
}
|
||||
}
|
||||
112
src/module/powers/confusion.js
Normal file
112
src/module/powers/confusion.js
Normal file
@ -0,0 +1,112 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
100
src/module/powers/conjureItem.js
Normal file
100
src/module/powers/conjureItem.js
Normal file
@ -0,0 +1,100 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
109
src/module/powers/createPit.js
Normal file
109
src/module/powers/createPit.js
Normal file
@ -0,0 +1,109 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
64
src/module/powers/curse.js
Normal file
64
src/module/powers/curse.js
Normal file
@ -0,0 +1,64 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
126
src/module/powers/damageField.js
Normal file
126
src/module/powers/damageField.js
Normal file
@ -0,0 +1,126 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
69
src/module/powers/darksight.js
Normal file
69
src/module/powers/darksight.js
Normal file
@ -0,0 +1,69 @@
|
||||
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';
|
||||
}
|
||||
}
|
||||
}
|
||||
58
src/module/powers/deflection.js
Normal file
58
src/module/powers/deflection.js
Normal file
@ -0,0 +1,58 @@
|
||||
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>`
|
||||
);
|
||||
}
|
||||
}
|
||||
139
src/module/powers/detectConcealArcana.js
Normal file
139
src/module/powers/detectConcealArcana.js
Normal file
@ -0,0 +1,139 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
56
src/module/powers/disguise.js
Normal file
56
src/module/powers/disguise.js
Normal file
@ -0,0 +1,56 @@
|
||||
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>`
|
||||
);
|
||||
}
|
||||
}
|
||||
133
src/module/powers/dispel.js
Normal file
133
src/module/powers/dispel.js
Normal file
@ -0,0 +1,133 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
70
src/module/powers/divination.js
Normal file
70
src/module/powers/divination.js
Normal file
@ -0,0 +1,70 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
83
src/module/powers/drainPowerPoints.js
Normal file
83
src/module/powers/drainPowerPoints.js
Normal file
@ -0,0 +1,83 @@
|
||||
import { requestRollFromTokens } from '../helpers.js';
|
||||
import { PowerEffect } from './basePowers.js';
|
||||
|
||||
export class DrainPowerPointsEffect extends PowerEffect {
|
||||
get name() {
|
||||
return 'Drain Power Points';
|
||||
}
|
||||
|
||||
get icon() {
|
||||
return 'icons/magic/control/debuff-energy-hold-teal-blue.webp';
|
||||
}
|
||||
|
||||
get duration() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
get basePowerPoints() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
get usePrimaryEffect() {
|
||||
return false;
|
||||
}
|
||||
|
||||
get isTargeted() {
|
||||
return true;
|
||||
}
|
||||
|
||||
get isRaisable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
get oneTarget() {
|
||||
return true;
|
||||
}
|
||||
|
||||
get modifiers() {
|
||||
return [
|
||||
...super.modifiers,
|
||||
{
|
||||
type: 'checkbox',
|
||||
name: 'Power',
|
||||
id: 'power',
|
||||
epic: false,
|
||||
effect: false,
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
default: 4,
|
||||
name: 'Opposed Target Number',
|
||||
id: 'tn',
|
||||
epic: false,
|
||||
effect: false,
|
||||
value: 0,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
async sideEffects() {
|
||||
await super.sideEffects();
|
||||
const rollOpts = {
|
||||
title: 'Resist Drain Power Points',
|
||||
flavor: 'Resist the energy drain!',
|
||||
mods: [{ label: 'Different Arcane Background', value: 2, ignore: true }],
|
||||
targetNumber: this.data.tn,
|
||||
};
|
||||
await requestRollFromTokens(this.targets, 'ability', 'spirit', rollOpts);
|
||||
}
|
||||
|
||||
get description() {
|
||||
return (
|
||||
super.description +
|
||||
`
|
||||
<p>An opposed roll of the caster's arcane activation roll vs the target's
|
||||
Spirit. Caster's success means the victim loses
|
||||
[[/r ${this.data.power ? '2' : '1'}d6]] power points. If the caster gets
|
||||
a raise over the opponent, the stolen power points augment the caster's
|
||||
own total.</p>
|
||||
`
|
||||
);
|
||||
}
|
||||
}
|
||||
97
src/module/powers/elementalManipulation.js
Normal file
97
src/module/powers/elementalManipulation.js
Normal file
@ -0,0 +1,97 @@
|
||||
import { PowerEffect } from './basePowers.js';
|
||||
|
||||
export class ElementalManipulationEffect extends PowerEffect {
|
||||
get name() {
|
||||
return 'Elemental Manipulation';
|
||||
}
|
||||
|
||||
get icon() {
|
||||
return 'icons/magic/symbols/elements-air-earth-fire-water.webp';
|
||||
}
|
||||
|
||||
get duration() {
|
||||
return this.data?.weather ? 0 : 5;
|
||||
}
|
||||
|
||||
get isTargeted() {
|
||||
return this.data?.weather ? false : true;
|
||||
}
|
||||
|
||||
get isRaisable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
get usePrimaryEffect() {
|
||||
return this.data?.weather ? false : true;
|
||||
}
|
||||
|
||||
get oneTarget() {
|
||||
return true;
|
||||
}
|
||||
|
||||
get basePowerPoints() {
|
||||
return 5;
|
||||
}
|
||||
|
||||
get modifiers() {
|
||||
return [
|
||||
{
|
||||
name: 'Power',
|
||||
type: 'checkbox',
|
||||
value: 3,
|
||||
id: 'power',
|
||||
epic: true,
|
||||
effect: false,
|
||||
},
|
||||
{
|
||||
name: 'Weather',
|
||||
type: 'checkbox',
|
||||
value: 5,
|
||||
id: 'weather',
|
||||
epic: true,
|
||||
effect: false,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
get primaryEffectButtons() {
|
||||
const dmg = `${this.data.raise ? 3 : 2}d${this.data.power ? 6 : 4}`;
|
||||
return [
|
||||
...super.primaryEffectButtons,
|
||||
{
|
||||
type: 'damage',
|
||||
label: `Damage (${dmg})`,
|
||||
formula: `${dmg}x`,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
get description() {
|
||||
if (this.data.weather) {
|
||||
return (
|
||||
super.description +
|
||||
`Bring or disperse rain, snow, sun, and wind in about a five mile radius.
|
||||
This takes 10 minutes and lasts an hour.
|
||||
`
|
||||
);
|
||||
}
|
||||
let damage = `${this.data.raise ? 3 : 2}d${this.data.power ? 6 : 4}x`;
|
||||
return (
|
||||
super.description +
|
||||
`
|
||||
<p>Use the activation roll for:</p>
|
||||
<ul>
|
||||
<li><strong>Attack:</strong> activation roll is the attack roll,
|
||||
[[/r ${damage}]] damage within Range.</li>
|
||||
<li><strong>Move:</strong> move a cubic foot of air, earth, fire or water
|
||||
(half that of stone) up to the caster's Smarts as a limited action.</li>
|
||||
<li><strong>Push:</strong> Use the activation roll in place of Strength
|
||||
for a Push.</li>
|
||||
<li><strong>Special Effects:</strong> eg. purify a gallon of water,
|
||||
or conjure a quart, fix breaks in stone, conjure a flame or spread
|
||||
existing flame.</li>
|
||||
</ul>
|
||||
`
|
||||
);
|
||||
}
|
||||
}
|
||||
105
src/module/powers/empathy.js
Normal file
105
src/module/powers/empathy.js
Normal file
@ -0,0 +1,105 @@
|
||||
import { PowerEffect } from './basePowers.js';
|
||||
|
||||
export class EmpathyEffect extends PowerEffect {
|
||||
get name() {
|
||||
return 'Empathy';
|
||||
}
|
||||
|
||||
get duration() {
|
||||
return 5 * (this?.data?.duration ?? false ? 10 : 1);
|
||||
}
|
||||
|
||||
get icon() {
|
||||
return 'icons/skills/social/diplomacy-handshake-yellow.webp';
|
||||
}
|
||||
|
||||
get hasAdditionalRecipients() {
|
||||
return true;
|
||||
}
|
||||
|
||||
get additionalRecipientCost() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
get basePowerPoints() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
get isTargeted() {
|
||||
return true;
|
||||
}
|
||||
|
||||
get modifiers() {
|
||||
return [
|
||||
...super.modifiers,
|
||||
{
|
||||
name: 'Charm',
|
||||
type: 'checkbox',
|
||||
id: 'charm',
|
||||
value: 2,
|
||||
epic: false,
|
||||
effect: false,
|
||||
},
|
||||
{
|
||||
name: 'Duration',
|
||||
type: 'checkbox',
|
||||
id: 'duration',
|
||||
value: 1,
|
||||
epic: false,
|
||||
effect: false,
|
||||
},
|
||||
{
|
||||
name: 'Truth',
|
||||
type: 'checkbox',
|
||||
id: 'truth',
|
||||
value: 2,
|
||||
epic: true,
|
||||
effect: false,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
get effectName() {
|
||||
const extra = [];
|
||||
if (this.data.charm) {
|
||||
extra.push('Charm');
|
||||
}
|
||||
if (this.data.truth) {
|
||||
extra.push('Truth');
|
||||
}
|
||||
const extraText = extra.length ? ` (${extra.join(', ')})` : '';
|
||||
return this.name + extraText;
|
||||
}
|
||||
|
||||
getMaintainEffectChanges() {
|
||||
const mode = foundry.CONST.ACTIVE_EFFECT_MODES.ADD;
|
||||
const value = this.data.raise ? 2 : 1;
|
||||
return ['Intimidation', 'Persuasion', 'Performance', 'Taunt', 'Riding'].map(function (skill) {
|
||||
return {
|
||||
key: `@Skill{${skill}}[system.die.modifier]`,
|
||||
priority: 0,
|
||||
mode,
|
||||
value,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
get description() {
|
||||
let text = super.description;
|
||||
text += `
|
||||
<p>Opposed by Spirit. If the target fails, caster gets
|
||||
+${this.data.raise ? 2 : 1} to Intimidation, Persuasion, Performance,
|
||||
Taunt or Riding (if target is an animal) rolls vs the target, except for
|
||||
rolls to activate powers.</p>`;
|
||||
if (this.data.charm) {
|
||||
text += `<p>an Uncooperative target is made
|
||||
${this.data.raise ? 'Friendly' : 'Cooperative'}.
|
||||
The spell ends instantly if the caster's group attacks the victim's group.</p>`;
|
||||
}
|
||||
if (this.data.truth) {
|
||||
text += '<p>The caster knows if the targets believe they are telling the truth.</p>';
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
}
|
||||
115
src/module/powers/entangle.js
Normal file
115
src/module/powers/entangle.js
Normal file
@ -0,0 +1,115 @@
|
||||
import { moduleName } from '../globals.js';
|
||||
import { PowerEffect } from './basePowers.js';
|
||||
|
||||
export class EntangleEffect extends PowerEffect {
|
||||
get name() {
|
||||
return 'Entangle';
|
||||
}
|
||||
|
||||
get duration() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
get icon() {
|
||||
return 'icons/magic/nature/root-vine-entangled-humanoid.webp';
|
||||
}
|
||||
|
||||
get isDamaging() {
|
||||
return true;
|
||||
}
|
||||
|
||||
get basePowerPoints() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
get usePrimaryEffect() {
|
||||
return false;
|
||||
}
|
||||
|
||||
get isTargeted() {
|
||||
return true;
|
||||
}
|
||||
|
||||
get modifiers() {
|
||||
return [
|
||||
...super.modifiers,
|
||||
{
|
||||
type: 'select',
|
||||
name: 'Damage',
|
||||
id: 'damage',
|
||||
epic: false,
|
||||
default: 'none',
|
||||
choices: {
|
||||
none: 'None',
|
||||
damage: 'Damage',
|
||||
deadly: '⭐ Deadly',
|
||||
},
|
||||
effects: { none: null, damage: null, deadly: null },
|
||||
values: { none: 0, damage: 2, deadly: 4 },
|
||||
},
|
||||
{
|
||||
type: 'checkbox',
|
||||
default: false,
|
||||
name: 'Tough',
|
||||
id: 'tough',
|
||||
value: 1,
|
||||
epic: false,
|
||||
effect: false,
|
||||
},
|
||||
{
|
||||
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 },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
async createSecondaryEffects(maintId) {
|
||||
const docs = await super.createSecondaryEffects(maintId);
|
||||
const docLabel = this.data.raise ? 'SWADE.Bound' : 'SWADE.Entangled';
|
||||
const docName = this.data.raise ? 'Bound' : 'Entangled';
|
||||
const doc = await PowerEffect.getStatus(docLabel, docName, false);
|
||||
doc.flags = foundry.utils.mergeObject(doc.flags ?? {}, {
|
||||
[moduleName]: {
|
||||
powerEffect: true,
|
||||
maintId,
|
||||
},
|
||||
});
|
||||
if (this.data.damage !== 'none') {
|
||||
const dmg = this.data.damage === 'deadly' ? '2d6' : '2d4';
|
||||
doc.flags[moduleName].buttons = [
|
||||
{
|
||||
label: `Damage (${dmg})`,
|
||||
type: 'damage',
|
||||
formula: `${dmg}x`,
|
||||
},
|
||||
];
|
||||
}
|
||||
docs.push(doc);
|
||||
return docs;
|
||||
}
|
||||
|
||||
get description() {
|
||||
let text = `
|
||||
<p>Target(s) are restrained by something trapping-appropriate of Hardness
|
||||
${this.data.tough ? 10 : 8}, and are ${this.data.raise ? 'Bound' : 'Entangled'}.
|
||||
`;
|
||||
if (this.data.damage !== 'none') {
|
||||
text += `While restrained, victims take
|
||||
${this.data.damage === 'deadly' ? '2d6' : '2d4'} damage at the end of
|
||||
their turn.
|
||||
`;
|
||||
}
|
||||
text += '</p>';
|
||||
return super.description + text;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user