Compare commits

...

194 Commits

Author SHA1 Message Date
4d1a4268bc update module for version bump 2025-12-13 11:14:39 -06:00
b9e6fc5075 pack file updates 2025-12-13 11:03:54 -06:00
56233e7c10 update roll helpers for swade 5.1.0 2025-12-07 23:09:36 -06:00
694a09d603 update release script 2025-09-07 16:48:23 -05:00
e662242000 css fixes for power form 2025-06-11 22:29:00 -05:00
3187d625f0 update summonCosts application 2025-06-09 22:12:42 -05:00
cba485285d update module.json with new pack 2025-06-09 21:01:06 -05:00
b7b8b5c54a changed setting to a drop down of actor packs 2025-06-08 23:26:03 -05:00
59f9022e43 update summon cost application to use compendium 2025-06-08 23:07:08 -05:00
0153896eeb add item to powers classes if present 2025-06-08 17:31:25 -05:00
7775932702 additional style updates 2025-06-08 17:28:22 -05:00
c670bd5dc6 update warriors gift to use compendium 2025-06-08 17:28:05 -05:00
03787ee314 move to single compendium for actors
Changes to update summon ally.
2025-06-08 15:21:38 -05:00
469fad5ba5 css fixes for power dialog 2025-06-08 14:52:27 -05:00
b41fd8833b summon ally reworked to use transient actors 2025-06-03 23:59:44 -05:00
0560cc5778 partial summon power update 2025-06-03 21:18:59 -05:00
f396636713 summon ally - mirror self fixes 2025-06-01 17:26:29 -05:00
c7193b3306 update summon powers to use sequencer crosshairs 2025-06-01 16:25:25 -05:00
15436de599 changelog updates 2025-05-31 21:42:07 -05:00
298d892844 converted powers dialog to applicationv2 2025-05-31 01:02:47 -05:00
a47b765c6f start converting main power dialog to applicationv2 2025-05-29 00:12:41 -05:00
669c3799a0 powers appv2 updates (dialog) 2025-05-29 00:12:23 -05:00
be74c79b35 macro cleanup DialogV2 2025-05-28 22:37:59 -05:00
f900cb7d53 move various power dialogs to DialogV2 2025-05-28 21:44:43 -05:00
a2fa9d3b05 v13 repack 2025-05-28 21:42:23 -05:00
7240fcf041 v13 unpack 2025-05-28 21:41:45 -05:00
e18419575c add settings registration 2025-05-27 23:08:01 -05:00
ec4f3e6c63 node dependencies upgrade 2025-05-27 23:07:21 -05:00
9e09eaecbb fix darksight effect name 2025-05-01 20:56:36 -05:00
d3274905c1 version bump 2025-01-27 21:04:15 -06:00
78ce8661a4 add additional maintain effect icons 2025-01-27 21:03:05 -06:00
d8b9f554ed add different movement modes for fly and burrow powers 2025-01-27 20:56:11 -06:00
1352d62c4f update CHANGELOG and module version 2025-01-26 23:53:57 -06:00
ba9eac716d make deflection modifiers more descriptive 2025-01-26 23:48:41 -06:00
cd8b5ac7eb clean up requested roll chat card 2025-01-26 23:48:07 -06:00
f93418009d fix power effect creation 2025-01-26 20:59:20 -06:00
a0bc314e0d switch to new method of finding fear dialog 2025-01-26 20:57:50 -06:00
89c357d6da update version for release 2025-01-11 13:11:37 -06:00
88eac4f241 changelog updates 2025-01-11 13:02:46 -06:00
281ab0712e modify blindsense vision mode for more contrast 2025-01-11 13:01:59 -06:00
5427d738ff fix pace based powers to work with swade 4.2 2025-01-11 12:51:02 -06:00
1c4595c72f node package updates 2025-01-11 12:49:48 -06:00
1b4e07ef65 fix typo 2024-08-11 14:22:31 -05:00
f2a088390f version bump for release 2024-08-11 14:19:05 -05:00
26be798ae2 pack update for release 2024-08-11 14:18:24 -05:00
93d13a0b47 move effect description to description field
Since Visual Active Effects did away with the special collapsable
description field, moved the active effect description for power effects
to the effect's 'description' field.
2024-08-11 13:56:01 -05:00
8e9827e5ae Fix protection and other maintained status effects
Protection and other system status effects still had a duration when
being maintained.

Fixes #44
2024-08-11 13:40:19 -05:00
abfed3204b update module data for 3.1.1 2024-07-26 14:47:09 -05:00
22f6a49c65 update changelog for 3.1.1 2024-07-26 14:44:37 -05:00
4372c5460a forgot changelog edit 2024-07-26 14:42:25 -05:00
969dab0a3e Add shape change ability power effect 2024-07-15 22:13:22 -05:00
3a91311887 update module to 3.1.0 2024-07-14 12:19:50 -05:00
4e37f910a1 suppress render sheet on updating embedded 2024-07-09 23:26:49 -05:00
141b06adea more foundry.utils moves 2024-07-09 20:51:57 -05:00
e240bb5bf1 update module verified to v12, add sequencer dependency 2024-07-09 20:51:27 -05:00
2765a53815 fix status effect finder 2024-07-09 20:36:50 -05:00
06f26c264c foundry.utils.mergeObject 2024-07-09 20:28:04 -05:00
d14de3d385 formdataextended doesn't have toObject 2024-07-09 20:17:07 -05:00
39c2ce8d70 foundry v12 ownership const name change 2024-07-09 20:16:40 -05:00
2ef597dc8c foundry v12 - update deepClone 2024-07-09 20:15:39 -05:00
65d8ec2d39 3.0.2 release. 2024-06-24 21:28:15 -05:00
8e206d1180 fix for effects expiring at start of character's turn 2024-06-21 16:26:04 -05:00
2721eb4a1e update module.json with version bump 2024-06-18 10:59:17 -05:00
f9824ad02e add judgement power effect 2024-06-18 10:49:37 -05:00
b1a128b692 add release script 2024-06-12 23:18:49 -05:00
318cc36348 update changelog and module.json 2024-06-12 20:19:42 -05:00
225b081f3c accidental commit of dist.tgz 2024-06-12 09:56:31 -05:00
8f557297eb changelog and module.json update 2024-06-11 22:58:07 -05:00
551161c6df add power effects and documentation to packs 2024-06-11 22:54:08 -05:00
946158a145 move dist directory 2024-06-11 22:15:41 -05:00
43cfa125ea power menu selection complete 2024-06-11 09:26:44 -05:00
94ee4243fa warrior's gift will still show name if no template 2024-06-11 09:08:31 -05:00
18cd705626 add power menu, part 1 2024-06-11 00:05:04 -05:00
5111bd830c add martial flexibility 2024-06-10 23:32:01 -05:00
557cb6e546 macro to add effects to powers 2024-06-10 16:49:22 -05:00
cc14b28d40 add summon effects 2024-06-09 17:59:37 -05:00
d520f8e137 add alarm to sound/silence 2024-06-09 14:27:27 -05:00
613552af4e add spiritual weapon to smite 2024-06-09 14:14:29 -05:00
c68c55bfc7 add lock/unlock, magic jar, shield other 2024-06-08 23:36:15 -05:00
fe0e889c02 add create pit 2024-06-08 23:00:53 -05:00
20871bcdef zombie complete + summon costs setup macro 2024-06-08 22:20:48 -05:00
5cb15bcedd zombie and summonable refactoring 2024-06-08 15:57:50 -05:00
5965b89b66 working zombie needs powerpoints 2024-06-08 10:45:20 -05:00
929cdda415 summon mods for zombie pt 1 2024-06-08 00:27:30 -05:00
57e6ab1bd3 add wish 2024-06-07 16:23:39 -05:00
1cb7abb7f5 add warrior's gift 2024-06-07 00:25:42 -05:00
d5300a845b typo fix 2024-06-06 16:58:07 -05:00
9ad0a23c33 Wall Walker 2024-06-06 11:34:52 -05:00
4959a380ab status fix for deprecation 2024-06-05 23:42:42 -05:00
ba99dc08e2 add timestop 2024-06-05 17:02:42 -05:00
f4bfa13cab add teleport 2024-06-04 23:59:19 -05:00
e016437b73 add telekinesis 2024-06-04 23:30:25 -05:00
a6e2bc7bdc summon ally 2024-06-04 19:47:22 -05:00
6fe743130d summon part 2 2024-06-04 00:01:41 -05:00
4bdd6176e8 add stun and summons part 1 2024-06-03 00:03:02 -05:00
ac70a99b99 add speak language 2024-06-02 22:28:27 -05:00
b646524a97 add sound/silence 2024-06-02 21:30:53 -05:00
805f50c631 add smite and fix VAE usage 2024-06-02 17:05:45 -05:00
a8354b4983 add baleful polymorph 2024-06-02 15:30:47 -05:00
d6581e16d4 add slumber 2024-06-02 00:16:41 -05:00
140381e1d1 add sloth/speed 2024-06-01 23:57:57 -05:00
0c0fb1a3a8 shape change done 2024-06-01 16:37:36 -05:00
4ffed673db shape change fix powers and skill issue 2024-05-30 15:29:39 -05:00
5c9556471e shape change part 3 2024-05-30 12:33:01 -05:00
8aac513792 help for spawns, shape change pt 3 2024-05-28 23:29:09 -05:00
e438fd36e8 shape change part 2 2024-05-27 23:53:39 -05:00
08d2be4ea0 shape change part 1 2024-05-27 00:25:02 -05:00
06306d8959 add scrying, and buttons to blind and boost/lower 2024-05-26 23:26:27 -05:00
267e892f27 add resurrection and sanctuary 2024-05-26 18:12:06 -05:00
1207b9c1bd use hash of name to assign maintain icon 2024-05-26 17:25:18 -05:00
61e78936a7 randomize maintain effects 2024-05-26 17:06:29 -05:00
7c28c43bd9 add relief 2024-05-26 16:55:47 -05:00
e0cd1c640c add puppet 2024-05-25 23:18:51 -05:00
864acdd722 add protection 2024-05-25 23:00:28 -05:00
f80e551835 add object reading, planar binding, plane shift 2024-05-25 15:44:50 -05:00
1cd4a08990 add mind reading and mind wipe 2024-05-25 11:13:02 -05:00
4908c6d4a7 mind reading 2024-05-24 23:22:02 -05:00
27989ffdfb add mind link 2024-05-23 17:04:29 -05:00
ef1eb940d0 add locate 2024-05-23 15:25:29 -05:00
d7f6582d41 add light/darkness 2024-05-22 22:43:17 -05:00
3d6a561929 add illusion, invisibility, intangibility 2024-05-21 23:49:40 -05:00
b4fd301a4f add healing 2024-05-21 13:12:45 -05:00
3062e1bfcb havoc 2024-05-20 23:54:32 -05:00
d85c11088a added VAE to recommended modules 2024-05-20 23:27:07 -05:00
ae3bc394e7 add growth shrink 2024-05-20 23:01:25 -05:00
36520fcee2 growthshrink pt1 2024-05-20 18:12:05 -05:00
4b087916b3 fix fly 2024-05-20 08:30:08 -05:00
0d74ae787d add fly 2024-05-19 23:49:01 -05:00
c76ce1ff7c add fear 2024-05-19 23:30:07 -05:00
53563af7d0 add farsight 2024-05-19 23:11:33 -05:00
b6a42818c8 add environmental protection 2024-05-19 20:37:47 -05:00
909457d1dd Add VAE helper buttons for bound/entangled 2024-05-19 19:43:55 -05:00
6a7dd696ca add entangle 2024-05-19 18:44:20 -05:00
e92a2c3424 move config dir into src 2024-05-19 18:44:11 -05:00
2dda9d76a4 empathy 2024-05-19 00:33:37 -05:00
caee516d48 add conjure item, more buttons 2024-05-18 23:55:51 -05:00
0d41527a5e buttons and descriptions for VAR module, added elemental manipulation 2024-05-18 22:34:27 -05:00
2870b6b587 add drain power points 2024-05-18 15:52:26 -05:00
db471b0cb9 add dispel and divination 2024-05-18 14:13:12 -05:00
b6c16ed0f5 remove warpgate dependency 2024-05-18 00:03:02 -05:00
a1246bf758 remove warpgate from fear roll and token vision macros 2024-05-16 23:23:47 -05:00
3357145ea9 add socket based active effect handling for powers 2024-05-15 23:53:36 -05:00
4c00d7116e remove warpgate active effect handling from power management 2024-05-15 23:34:02 -05:00
5151634e8b remove warpgate from powers handling 2024-05-15 22:15:48 -05:00
486a7d4167 add disguise 2024-05-15 22:03:35 -05:00
1877b048d2 add detect-conceal arcana 2024-05-15 21:54:38 -05:00
8c373fcc8b add deflection 2024-05-15 21:37:29 -05:00
57caeccad2 add darksight 2024-05-15 21:30:40 -05:00
2caa2a4633 add damage field 2024-05-15 21:22:43 -05:00
dea2fa95c4 add confusion and curse 2024-05-14 23:47:12 -05:00
7ee2f966c0 add burst 2024-05-14 23:26:41 -05:00
f50a81a329 burrow and boost lower trait 2024-05-14 23:17:04 -05:00
cf30214502 add reorged bolt effect 2024-05-14 15:04:22 -05:00
eaf5b8a066 add reorged blind power 2024-05-14 14:10:33 -05:00
6ec4d9019e more linting and power conversion 2024-05-13 23:25:16 -05:00
8f015bc05c add resist roll to banish 2024-05-13 19:56:38 -05:00
ae7295028d re-enable sockets 2024-05-13 19:16:27 -05:00
f962ee785e fixes for banish 2024-05-13 18:29:54 -05:00
b5b0ad01f6 finish base power overhaul 2024-05-12 23:19:36 -05:00
c8f27770b6 reorg part 2 2024-05-12 18:10:35 -05:00
53d30e80e9 testing changes to compendium 2024-05-11 22:35:02 -05:00
99657ea07f pack updates 2024-05-11 20:54:19 -05:00
a3fdf1c7f2 massive reorg to new project structure 2024-05-11 20:49:16 -05:00
4c93c0be94 adding webpack 2024-05-11 19:35:16 -05:00
46feb5705e prettier and eslint formatting 2024-05-11 19:35:06 -05:00
d4f155adc2 add disguise 2024-05-07 21:34:08 -05:00
4d44b6c31c add detect/conceal arcana 2024-05-07 19:36:21 -05:00
eccb4778cb add new deflection effect 2024-05-07 13:29:59 -05:00
13e6c9bcf7 add darksight 2024-05-07 13:18:07 -05:00
312f2a88fc add damage field 2024-05-06 17:08:57 -05:00
69bea4e77d add boost lower and curse 2024-05-05 23:05:04 -05:00
e6f37b173f boost/lower trait 2024-05-05 15:47:44 -05:00
dd4735d50a refactoring the refactoring 2024-05-04 19:26:52 -05:00
ca7acd7e9d changing how effect descriptions work 2024-05-03 17:13:14 -05:00
5dfce6cbcc add power effect descriptions, blind, blast 2024-05-03 00:09:42 -05:00
6100f425ed maintained powers use power icon if no primary effect 2024-05-02 15:03:19 -05:00
ba7f3302c1 Add Banish and Barrier to new powers 2024-05-01 22:45:02 -05:00
13f93898d6 new arcane protection 2024-05-01 21:53:08 -05:00
8e50b478ce update recommended token variant mapping config 2024-05-01 21:52:37 -05:00
d99d0c0728 base powers feature complete 2024-05-01 21:14:27 -05:00
c679304ae2 add additional recipients to power structure 2024-05-01 15:17:12 -05:00
baa9d51c18 WIP commit for additional recipients 2024-04-30 23:55:52 -05:00
592d0c5406 additional power refinement 2024-04-30 23:01:42 -05:00
2aa7af28a5 power overhaul part 2 2024-04-29 23:59:38 -05:00
9333be95fd powers v2 part 1 2024-04-29 00:04:14 -05:00
b4089e4b71 temp commit 2024-04-28 21:43:26 -05:00
391b76ccbd remove shim, better linting 2024-04-28 21:39:08 -05:00
6fb821cc2a v2.4.3 2024-04-21 14:54:24 -05:00
df607d4d66 collected updates 2024-04-21 13:43:02 -05:00
b5f0794866 version bump 2024-02-25 22:28:25 -06:00
14c988c528 add action deck macros 2024-02-25 22:27:02 -06:00
a3733fa4cc add spiritual weapon summon 2024-02-13 22:29:48 -06:00
6b5b1195ab version bump 2024-02-11 16:25:31 -06:00
4de06dac03 add MATT request macros 2024-02-11 16:18:54 -06:00
267 changed files with 20637 additions and 12955 deletions

9
.editorconfig Normal file
View 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

View File

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

146
.gitignore vendored
View File

@ -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
View File

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

1
.nvmrc Normal file
View File

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

8
.prettierignore Normal file
View 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
View 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,
};

View File

@ -5,6 +5,166 @@ 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).
## [4.1.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

View File

@ -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/

View File

@ -1,660 +0,0 @@
{
"globalMappings": [
{
"imgName": "",
"imgSrc": "",
"priority": 50,
"config": {},
"overlay": true,
"alwaysOn": false,
"overlayConfig": {
"underlay": false,
"bottom": false,
"top": false,
"inheritTint": false,
"linkRotation": true,
"animation": {
"relative": false,
"rotate": false,
"duration": 30000,
"clockwise": true
},
"linkMirror": true,
"linkScale": true,
"linkOpacity": false,
"loop": true,
"playOnce": false,
"img": "modules/JB2A_DnD5e/Library/Generic/Item/GlintMany01_01_Regular_Yellow_200x200.webm",
"alpha": 1,
"tint": "",
"offsetX": 0,
"offsetY": 0.38,
"scaleX": 0.4,
"scaleY": 0.4,
"angle": 0,
"filter": "DropShadowFilter",
"filterOptions": {
"rotation": 45,
"distance": 7,
"color": 0,
"alpha": 0.84,
"shadowOnly": false,
"blur": 2,
"quality": 0
},
"alwaysVisible": false,
"limitedUsers": [],
"limitOnHover": false,
"limitOnControl": false,
"text": {
"text": "Shaken",
"fontFamily": "Signika",
"fontSize": 36,
"letterSpacing": 0,
"fill": "#FFFFFF",
"dropShadow": "true",
"strokeThickness": 1,
"stroke": "#111111",
"curve": {
"radius": 0,
"invert": null
}
},
"id": "mwFtNKpD"
},
"targetActors": null,
"label": "Shaken",
"expression": "Shaken",
"id": "mwFtNKpD"
},
{
"imgName": "",
"imgSrc": "",
"priority": 50,
"config": {},
"overlay": true,
"alwaysOn": false,
"overlayConfig": {
"underlay": false,
"bottom": false,
"top": false,
"inheritTint": false,
"linkRotation": true,
"animation": {
"relative": false,
"rotate": true,
"duration": 30000,
"clockwise": true
},
"linkMirror": true,
"linkScale": true,
"linkOpacity": false,
"loop": true,
"playOnce": false,
"img": "modules/JB2A_DnD5e/Library/Generic/Particles/ParticlesInward02_04_Regular_GreenYellow_400x400.webm",
"alpha": 1,
"tint": "",
"offsetX": 0,
"offsetY": 0,
"scaleX": 1.5,
"scaleY": 1.5,
"angle": 0,
"filter": "DropShadowFilter",
"filterOptions": {
"rotation": 45,
"distance": 7,
"color": 0,
"alpha": 0.84,
"shadowOnly": false,
"blur": 2,
"quality": 0
},
"alwaysVisible": false,
"limitedUsers": [],
"limitOnHover": false,
"limitOnControl": false,
"text": {
"text": "",
"fontFamily": "Signika",
"fontSize": 36,
"letterSpacing": 0,
"fill": "#FFFFFF",
"dropShadow": "true",
"strokeThickness": 1,
"stroke": "#111111",
"curve": {
"radius": 0,
"invert": null
}
},
"id": "toTYr3DQ"
},
"targetActors": null,
"label": "Distracted",
"expression": "Distracted",
"id": "toTYr3DQ"
},
{
"imgName": "",
"imgSrc": "",
"priority": 50,
"config": {},
"overlay": true,
"alwaysOn": false,
"overlayConfig": {
"underlay": false,
"bottom": false,
"top": false,
"inheritTint": false,
"linkRotation": true,
"animation": {
"relative": false,
"rotate": false,
"duration": 5200,
"clockwise": false
},
"linkMirror": true,
"linkScale": true,
"linkOpacity": false,
"loop": true,
"playOnce": false,
"img": "modules/JB2A_DnD5e/Library/Generic/Marker/MarkerShieldCracked_02_Regular_Purple_400x400.webm",
"alpha": 1,
"tint": "",
"offsetX": 0,
"offsetY": 0,
"scaleX": 1.5,
"scaleY": 1.5,
"angle": 0,
"filter": "OutlineFilter",
"filterOptions": {
"thickness": 1,
"color": 0,
"quality": 0.1
},
"alwaysVisible": false,
"limitedUsers": [],
"limitOnHover": false,
"limitOnControl": false,
"text": {
"text": "",
"fontFamily": "Signika",
"fontSize": 36,
"letterSpacing": 0,
"fill": "#FFFFFF",
"dropShadow": "true",
"strokeThickness": 1,
"stroke": "#111111",
"curve": {
"radius": 0,
"invert": null
}
},
"id": "J4GrRaxL"
},
"targetActors": null,
"label": "Vulnerable",
"expression": "Vulnerable",
"id": "J4GrRaxL"
},
{
"imgName": "",
"imgSrc": "",
"priority": 50,
"config": {},
"overlay": true,
"alwaysOn": false,
"overlayConfig": {
"underlay": false,
"bottom": false,
"top": false,
"inheritTint": false,
"linkRotation": true,
"animation": {
"relative": false,
"rotate": false,
"duration": 5000,
"clockwise": true
},
"linkMirror": true,
"linkScale": true,
"linkOpacity": false,
"loop": true,
"playOnce": false,
"img": "modules/JB2A_DnD5e/Library/Generic/UI/IconStun_01_Regular_Purple_200x200.webm",
"alpha": 1,
"tint": "",
"offsetX": 0,
"offsetY": 0,
"scaleX": 0.77,
"scaleY": 0.77,
"angle": 0,
"filter": "OutlineFilter",
"filterOptions": {
"thickness": 1,
"color": 0,
"quality": 0.1
},
"alwaysVisible": false,
"limitedUsers": [],
"limitOnHover": false,
"limitOnControl": false,
"text": {
"text": "",
"fontFamily": "Signika",
"fontSize": 36,
"letterSpacing": 0,
"fill": "#FFFFFF",
"dropShadow": "true",
"strokeThickness": 1,
"stroke": "#111111",
"curve": {
"radius": 0,
"invert": null
}
},
"id": "nOfPMsQp"
},
"targetActors": null,
"label": "Stunned",
"expression": "Stunned",
"id": "nOfPMsQp"
},
{
"imgName": "",
"imgSrc": "",
"priority": 50,
"config": {},
"overlay": true,
"alwaysOn": false,
"overlayConfig": {
"underlay": false,
"bottom": false,
"top": false,
"inheritTint": false,
"linkRotation": true,
"animation": {
"relative": false,
"rotate": false,
"duration": 5000,
"clockwise": true
},
"linkMirror": true,
"linkScale": true,
"linkOpacity": false,
"loop": true,
"playOnce": false,
"img": "modules/JB2A_DnD5e/Library/1st_Level/Entangle/Entangle_01_Green_400x400.webm",
"alpha": 1,
"tint": "",
"offsetX": 0,
"offsetY": 0,
"scaleX": 1,
"scaleY": 1,
"angle": 0,
"filter": "DropShadowFilter",
"filterOptions": {
"rotation": 45,
"distance": 2,
"color": 2367281,
"alpha": 0.84,
"shadowOnly": false,
"blur": 2,
"quality": 0
},
"alwaysVisible": false,
"limitedUsers": [],
"limitOnHover": false,
"limitOnControl": false,
"text": {
"text": "",
"fontFamily": "Signika",
"fontSize": 36,
"letterSpacing": 0,
"fill": "#FFFFFF",
"dropShadow": "true",
"strokeThickness": 1,
"stroke": "#111111",
"curve": {
"radius": 0,
"invert": null
}
},
"id": "rrdhKai4"
},
"targetActors": null,
"label": "Entangled",
"expression": "Entangled",
"id": "rrdhKai4"
},
{
"imgName": "",
"imgSrc": "",
"priority": 50,
"config": {},
"overlay": true,
"alwaysOn": false,
"overlayConfig": {
"underlay": false,
"bottom": false,
"top": false,
"inheritTint": false,
"linkRotation": true,
"animation": {
"relative": false,
"rotate": false,
"duration": 5000,
"clockwise": true
},
"linkMirror": true,
"linkScale": true,
"linkOpacity": false,
"loop": true,
"playOnce": false,
"img": "modules/JB2A_DnD5e/Library/Generic/Marker/MarkerChainStandard01_01_Regular_Red_Loop_400x400.webm",
"alpha": 1,
"tint": "",
"offsetX": 0,
"offsetY": 0,
"scaleX": 1.75,
"scaleY": 1.75,
"angle": 0,
"filter": "DropShadowFilter",
"filterOptions": {
"rotation": 45,
"distance": 2,
"color": 2367281,
"alpha": 0.84,
"shadowOnly": false,
"blur": 2,
"quality": 0
},
"alwaysVisible": false,
"limitedUsers": [],
"limitOnHover": false,
"limitOnControl": false,
"text": {
"text": "",
"fontFamily": "Signika",
"fontSize": 36,
"letterSpacing": 0,
"fill": "#FFFFFF",
"dropShadow": "true",
"strokeThickness": 1,
"stroke": "#111111",
"curve": {
"radius": 0,
"invert": null
}
},
"id": "5RYi2X9W"
},
"targetActors": null,
"label": "Bound",
"expression": "Bound",
"id": "5RYi2X9W"
},
{
"imgName": "",
"imgSrc": "",
"priority": 50,
"config": {
"tv_script": {
"onApply": "",
"onRemove": "",
"tmfxPreset": "dropshadow"
}
},
"overlay": false,
"alwaysOn": false,
"overlayConfig": {
"id": "KtequnXd"
},
"targetActors": null,
"label": "Flying",
"expression": "Flying",
"id": "KtequnXd"
},
{
"imgName": "",
"imgSrc": "",
"priority": 50,
"config": {
"tv_script": {
"onApply": "",
"onRemove": "",
"tmfxPreset": "glow"
}
},
"overlay": false,
"alwaysOn": false,
"overlayConfig": {
"id": "k4boMPSb"
},
"targetActors": null,
"label": "Conviction",
"expression": "Conviction",
"id": "k4boMPSb"
},
{
"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,
"overlayConfig": {
"underlay": false,
"bottom": false,
"top": false,
"inheritTint": false,
"linkRotation": true,
"animation": {
"relative": false,
"rotate": true,
"duration": 5000,
"clockwise": true
},
"linkMirror": true,
"linkScale": true,
"linkOpacity": false,
"loop": true,
"playOnce": false,
"img": "modules/JB2A_DnD5e/Library/Cantrip/Dancing_Lights/DancingLights_01_Yellow_200x200.webm",
"alpha": 1,
"tint": "",
"offsetX": 0,
"offsetY": 0.52,
"scaleX": 0.41,
"scaleY": 0.41,
"angle": 0,
"filter": "NONE",
"alwaysVisible": false,
"limitedUsers": [],
"limitOnHover": false,
"limitOnControl": false,
"text": {
"text": "",
"fontFamily": "Signika",
"fontSize": 36,
"letterSpacing": 0,
"fill": "#FFFFFF",
"dropShadow": "true",
"strokeThickness": 1,
"stroke": "#111111",
"curve": {
"radius": 0,
"invert": null
}
},
"id": "eO68BGDl"
},
"targetActors": null,
"label": "Glow",
"expression": "Glow",
"id": "eO68BGDl"
},
{
"imgName": "",
"imgSrc": "",
"priority": 50,
"config": {
"texture": {
"tint": "#c0bfbc"
},
"tv_script": {
"onApply": "",
"onRemove": "",
"tmfxPreset": "smoke"
}
},
"overlay": false,
"alwaysOn": false,
"overlayConfig": {
"id": "BP0Xx8wD"
},
"targetActors": null,
"label": "Shroud",
"expression": "Shroud",
"id": "BP0Xx8wD"
},
{
"imgName": "",
"imgSrc": "",
"priority": 50,
"config": {},
"overlay": true,
"alwaysOn": false,
"overlayConfig": {
"underlay": false,
"bottom": false,
"top": false,
"inheritTint": false,
"linkRotation": true,
"animation": {
"relative": false,
"rotate": false,
"duration": 5000,
"clockwise": true
},
"linkMirror": true,
"linkScale": true,
"linkOpacity": false,
"loop": true,
"playOnce": false,
"img": "modules/JB2A_DnD5e/Library/Generic/Marker/MarkerShield_03_Regular_Green_400x400.webm",
"alpha": 1,
"tint": "",
"offsetX": 0,
"offsetY": 0,
"scaleX": 2,
"scaleY": 2,
"angle": 0,
"filter": "OutlineFilter",
"filterOptions": {
"thickness": 1,
"color": 0,
"quality": 0.1
},
"alwaysVisible": false,
"limitedUsers": [],
"limitOnHover": false,
"limitOnControl": false,
"text": {
"text": "",
"fontFamily": "Signika",
"fontSize": 36,
"letterSpacing": 0,
"fill": "#FFFFFF",
"dropShadow": "true",
"strokeThickness": 1,
"stroke": "#111111",
"curve": {
"radius": 0,
"invert": null
}
},
"id": "gRwsZcZK"
},
"targetActors": null,
"label": "Deflection (melee) || Deflection (range) || Deflection (raise)",
"expression": "Deflection (melee) || Deflection (range) || Deflection (raise)",
"id": "gRwsZcZK"
},
{
"imgName": "",
"imgSrc": "",
"priority": 50,
"config": {},
"overlay": true,
"alwaysOn": false,
"overlayConfig": {
"underlay": false,
"bottom": false,
"top": false,
"inheritTint": false,
"linkRotation": true,
"animation": {
"relative": false,
"rotate": false,
"duration": 5000,
"clockwise": true
},
"linkMirror": true,
"linkScale": true,
"linkOpacity": false,
"loop": true,
"playOnce": false,
"img": "modules/JB2A_DnD5e/Library/1st_Level/Shield/Shield_01_Regular_Blue_Loop_400x400.webm",
"alpha": 1,
"tint": "",
"offsetX": 0,
"offsetY": 0,
"scaleX": 1.6,
"scaleY": 1.6,
"angle": 0,
"filter": "OutlineFilter",
"filterOptions": {
"thickness": 2.9,
"color": 0,
"quality": 0.1
},
"alwaysVisible": false,
"limitedUsers": [],
"limitOnHover": false,
"limitOnControl": false,
"text": {
"text": "",
"fontFamily": "Signika",
"fontSize": 36,
"letterSpacing": 0,
"fill": "#FFFFFF",
"dropShadow": "true",
"strokeThickness": 1,
"stroke": "#111111",
"curve": {
"radius": 0,
"invert": null
}
},
"id": "1po9hq1m"
},
"targetActors": null,
"label": "Protection",
"expression": "Protection",
"id": "1po9hq1m"
}
]
}

23
eslint.config.js Normal file
View 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
View 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
View 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
View 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.`);
}
}
}

View 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);
}
}

View File

@ -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>'
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,
});
}

4
macros/powerMenu.js Normal file
View File

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

View File

@ -1,5 +1,5 @@
new Dialog({
title: 'Damage Roll Configuration',
new foundry.applications.api.DialogV2({
window: { title: "Damage Roll Configuration" },
content: `
<form>
<div class="form-group">
@ -16,24 +16,28 @@ new Dialog({
</div>
</form>
`,
buttons: {
ok: {
label: 'Roll Damage',
callback: (html) => {
const damageRoll = html.find('input[name="damageRoll"]').val()
let flavor = html.find('input[name="flavor"]').val()
const ap = parseInt(html.find('input[name="ap"]').val()) || 0
const options = {}
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
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 })
}
new CONFIG.Dice.DamageRoll(damageRoll, null, options).toMessage({ flavor });
},
cancel: {
label: 'Cancel'
}
}
}).render(true)
},
{
action: "cancel",
label: "Cancel",
},
],
}).render(true);

View File

@ -1,39 +1,42 @@
const requestFearRollFromTokens = game.modules.get('swade-mb-helpers').api.requestRollFromTokens
const requestFearRollFromTokens = game.modules.get('swade-mb-helpers').api.requestFearRollFromTokens;
async function main() {
let tokens = Array.from(game.user.targets)
let tokens = Array.from(game.user.targets);
if (tokens.length < 1) {
tokens = canvas.tokens.controlled
tokens = canvas.tokens.controlled;
}
if (tokens.length < 1) {
ui.notifications.error('Please target or select some tokens')
return
ui.notifications.error('Please target or select some tokens');
return;
}
const menuData = {
inputs: [
{ type: 'info', label: `Requesting Fear roll from ${tokens.map(t => t.name).join(', ')}` },
{ type: 'number', label: 'Fear Check Penalty', options: 0 }
],
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: [
{ label: 'Request roll', value: 'ok', default: true },
{ label: 'Cancel', value: 'cancel' }
]
}
const menuConfig = {
title: 'Request Fear roll...'
}
const result = await warpgate.menu(menuData, menuConfig)
if (result.buttons !== 'ok') {
return
}
console.log(result)
const fear = result.inputs[1] || 0
const targetNumber = 4
const options = { targetNumber, fear }
requestFearRollFromTokens(tokens, options)
{
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()
main();

View File

@ -1,72 +1,95 @@
const requestRollFromTokens = game.modules.get('swade-mb-helpers').api.requestRollFromTokens
const requestRollFromTokens = game.modules.get('swade-mb-helpers').api.requestRollFromTokens;
async function main() {
let tokens = Array.from(game.user.targets)
let tokens = Array.from(game.user.targets);
if (tokens.length < 1) {
tokens = canvas.tokens.controlled
tokens = canvas.tokens.controlled;
}
if (tokens.length < 1) {
ui.notifications.error('Please target or select some tokens')
return
ui.notifications.error('Please target or select some tokens');
return;
}
const menuData = {
inputs: [
{ type: 'info', label: `Requesting roll from ${tokens.map(t => t.name).join(', ')}` },
{
type: 'select',
label: 'Trait to roll',
options: []
},
{ type: 'number', label: 'Roll Modifier', options: 0 },
{ type: 'text', label: 'Roll Modifier Description', options: 'Roll Modifier' },
{ type: 'number', label: 'Target Number', options: 4 }
],
buttons: [
{ label: 'Request roll', value: 'ok', default: true },
{ label: 'Cancel', value: 'cancel' }
]
}
const menuConfig = {
title: 'Request roll...'
}
for (const attribute of ['Agility', 'Smarts', 'Spirit', 'Strength', 'Vigor']) {
menuData.inputs[1].options.push(
{ html: `Attribute | ${attribute}`, value: `a|${attribute}` }
)
}
const skillSet = new Set()
const attributes = ['Agility', 'Smarts', 'Spirit', 'Strength', 'Vigor'];
const skillSet = new Set();
for (const token of tokens) {
const skills = token.actor.items.filter(i => i.type === 'skill' &&
!['Untrained', 'Unskilled Attempt'].includes(i.name))
for (const skill of skills) {
skillSet.add(skill.name)
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);
}
}
for (const skill of Array.from(skillSet).sort()) {
menuData.inputs[1].options.push(
{ html: `Skill | ${skill}`, value: `s|${skill}` })
}
menuData.inputs[1].options.push(
{ html: 'Skill | Untrained', value: 's|NOSKILL' })
const result = await warpgate.menu(menuData, menuConfig)
if (result.buttons !== 'ok') {
return
}
console.log(result)
const rollMod = result.inputs[2]
const rollModDesc = result.inputs[3]
const rollParts = result.inputs[1].split('|')
const rollType = (rollParts[0] === 'a' ? 'attribute' : 'skill')
const rollDesc = rollParts[1]
const targetNumber = result.inputs[4] || 4
const options = { targetNumber }
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 }]
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);
}
requestRollFromTokens(tokens, rollType, rollDesc, options)
}
main()
main();

37
macros/requestRollMATT.js Normal file
View 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()

View File

@ -1,123 +1,156 @@
const argBright = typeof args === 'undefined' ? null : args.length > 0 ? args[0] : null
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 BRIGHT_LEVELS = ['bright', 'dim', 'dark', 'pitchdark'];
const THRESHOLDS = {
dim: 0.4,
dark: 0.6,
pitchdark: 0.8
}
pitchdark: 0.8,
};
const RANGES = {
basic: {
bright: 25,
dim: 25,
dark: 10,
pitchdark: 0
pitchdark: 0,
},
lowlight: {
bright: 25,
dim: 25,
dark: 10,
pitchdark: 0
pitchdark: 0,
},
darkvision: {
bright: 25,
dim: 25,
dark: 10,
pitchdark: 10
pitchdark: 10,
},
nightvision: {
bright: 200,
dim: 200,
dark: 200,
pitchdark: 200
pitchdark: 200,
},
blindsense: {
bright: 5,
dim: 5,
dark: 5,
pitchdark: 5
}
}
pitchdark: 5,
},
};
const SIGHT_NAMES = {
lowlight: 'low-light-vision',
darkvision: 'darkvision',
nightvision: 'night-vision',
blindsense: 'blindsense'
}
blindsense: 'blindsense',
};
const SIGHT_MODES = {
lowlight: 'lowlight',
darkvision: 'darkvision',
nightvision: 'darkvision',
basic: 'basic',
blindsense: 'blindsense'
}
blindsense: 'blindsense',
};
function findAbility(token, swid) {
return token.actor.items.find(i => i.type === 'ability' && i.system.swid === 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]
const scene = game.scenes.current;
let sceneBright = BRIGHT_LEVELS[0];
if (scene.darkness > THRESHOLDS.pitchdark) {
sceneBright = BRIGHT_LEVELS[3]
sceneBright = BRIGHT_LEVELS[3];
} else if (scene.darkness > THRESHOLDS.dark) {
sceneBright = BRIGHT_LEVELS[2]
sceneBright = BRIGHT_LEVELS[2];
} else if (scene.darkness > THRESHOLDS.dim) {
sceneBright = BRIGHT_LEVELS[1]
sceneBright = BRIGHT_LEVELS[1];
}
let bright = sceneBright
let bright = sceneBright;
if (argBright && BRIGHT_LEVELS.includes(argBright)) {
bright = argBright
bright = argBright;
}
const menuData = {
inputs: [
{ type: 'radio', label: 'Bright Light', options: ['bright', bright === BRIGHT_LEVELS[0]] },
{ type: 'radio', label: 'Dim Light', options: ['bright', bright === BRIGHT_LEVELS[1]] },
{ type: 'radio', label: 'Dark', options: ['bright', bright === BRIGHT_LEVELS[2]] },
{ type: 'radio', label: 'Pitch Dark', options: ['bright', bright === BRIGHT_LEVELS[3]] }
],
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: [
{ label: 'Select Scene Brightness', value: 'ok', default: true },
{ label: 'Cancel', value: 'cancel' }
]
}
const menuConfig = { title: 'Select scene brightness' }
const result = await warpgate.menu(menuData, menuConfig)
if (result.buttons !== 'ok') { return }
{
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);
for (let i = 0; i < 4; i++) {
if (result.inputs[i]) { bright = BRIGHT_LEVELS[i] }
}
for (const tokenId of scene.tokens.map(t => t.id)) {
const token = scene.tokens.get(tokenId)
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
console.log(`Skipping ${token.name}, vision not enabled`);
continue;
// don't set sight on a token where it's not enabled
}
let sightType = 'basic'
let sightType = 'basic';
for (const sight in SIGHT_NAMES) {
if (findAbility(token, SIGHT_NAMES[sight])) {
sightType = sight
sightType = sight;
}
}
const range = RANGES[sightType][bright]
const sightMode = SIGHT_MODES[sightType]
const visionModeData = CONFIG.Canvas.visionModes[sightMode].vision.defaults
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)
await token.update(data)
'sight.contrast': visionModeData.contrast,
};
console.log(`Updating ${token.name}:`, sightType, bright, data);
token.update(data);
}
},
},
{ action: "cancel", label: 'Cancel' },
],
}).render(true);
}
main()
main();

14722
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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.

View File

@ -1,5 +0,0 @@
2024/01/29-23:43:16.099904 7f72097bd700 Delete type=3 #1
2024/01/29-23:43:16.103068 7f6f6bfff700 Level-0 table #5: started
2024/01/29-23:43:16.110852 7f6f6bfff700 Level-0 table #5: 15710 bytes OK
2024/01/29-23:43:16.121887 7f6f6bfff700 Delete type=0 #3
2024/01/29-23:43:16.122061 7f6f6bfff700 Manual compaction at level-0 from '!folders!0nDRFmMBs5DBJU9M' @ 72057594037927935 : 1 .. '!items.effects!RC1Nz6iph8wPPK1B.g9W5hJisq3MsCpZW' @ 0 : 0; will stop at (end)

Binary file not shown.

Binary file not shown.

View File

@ -1,5 +0,0 @@
2024/01/29-23:43:17.011739 7fcd23fff700 Delete type=3 #1
2024/01/29-23:43:17.014256 7fcd237fe700 Level-0 table #5: started
2024/01/29-23:43:17.021608 7fcd237fe700 Level-0 table #5: 6787 bytes OK
2024/01/29-23:43:17.030434 7fcd237fe700 Delete type=0 #3
2024/01/29-23:43:17.030575 7fcd237fe700 Manual compaction at level-0 from '!items!JWyBQe4tnOYljFAF' @ 72057594037927935 : 1 .. '!items!tWWSfEMmLmws6Yb1' @ 0 : 0; will stop at (end)

Binary file not shown.

Binary file not shown.

View File

@ -1,5 +0,0 @@
2024/01/29-23:43:17.871203 7fcc2b7fe700 Delete type=3 #1
2024/01/29-23:43:17.874557 7fcc2a542700 Level-0 table #5: started
2024/01/29-23:43:17.881745 7fcc2a542700 Level-0 table #5: 1751 bytes OK
2024/01/29-23:43:17.891265 7fcc2a542700 Delete type=0 #3
2024/01/29-23:43:17.891403 7fcc2a542700 Manual compaction at level-0 from '!actors!U5v4gFHquo0Y1SAq' @ 72057594037927935 : 1 .. '!actors!U5v4gFHquo0Y1SAq' @ 0 : 0; will stop at (end)

Binary file not shown.

Binary file not shown.

View File

@ -1,5 +0,0 @@
2024/01/29-23:43:18.604510 7f21da7be700 Delete type=3 #1
2024/01/29-23:43:18.607134 7f21d8fbb700 Level-0 table #5: started
2024/01/29-23:43:18.630768 7f21d8fbb700 Level-0 table #5: 18825 bytes OK
2024/01/29-23:43:18.639775 7f21d8fbb700 Delete type=0 #3
2024/01/29-23:43:18.639913 7f21d8fbb700 Manual compaction at level-0 from '!folders!A3iVDJD2cTuTLpBu' @ 72057594037927935 : 1 .. '!macros!wU2mAUnw3RW9qMT8' @ 0 : 0; will stop at (end)

Binary file not shown.

View File

@ -1,25 +0,0 @@
{
"name": "Blind",
"type": "script",
"author": "R9ZgY0IvWl8ovIuT",
"img": "icons/svg/blind.svg",
"scope": "global",
"command": "game.modules.get('swade-mb-helpers').api.powerEffects({\n token,\n targets: game.user.targets,\n name: 'Blind'\n})",
"flags": {},
"_stats": {
"systemId": "swade",
"systemVersion": "3.1.4",
"coreVersion": "11.311",
"createdTime": 1678165762773,
"modifiedTime": 1696209757148,
"lastModifiedBy": "sVoCvBU1knmXzoYe"
},
"_id": "e9HvLMtaDw2qpcE8",
"folder": "hIbrWxg1nDutCSwt",
"sort": 700000,
"ownership": {
"default": 0,
"R9ZgY0IvWl8ovIuT": 3
},
"_key": "!macros!e9HvLMtaDw2qpcE8"
}

View File

@ -1,25 +0,0 @@
{
"name": "Boost/Lower Trait",
"type": "script",
"author": "R9ZgY0IvWl8ovIuT",
"img": "icons/magic/life/cross-embers-glow-yellow-purple.webp",
"scope": "global",
"command": "game.modules.get('swade-mb-helpers').api.powerEffects({\n token,\n targets: game.user.targets,\n name: 'Boost/Lower Trait'\n})",
"flags": {},
"_stats": {
"systemId": "swade",
"systemVersion": "3.1.4",
"coreVersion": "11.311",
"createdTime": 1677458254287,
"modifiedTime": 1696209757148,
"lastModifiedBy": "sVoCvBU1knmXzoYe"
},
"_id": "1AqIFHAcX5TRdE8X",
"folder": "hIbrWxg1nDutCSwt",
"sort": 2100000,
"ownership": {
"default": 0,
"R9ZgY0IvWl8ovIuT": 3
},
"_key": "!macros!1AqIFHAcX5TRdE8X"
}

View File

@ -1,27 +0,0 @@
{
"name": "Burrow",
"type": "script",
"author": "sVoCvBU1knmXzoYe",
"img": "icons/magic/earth/projectile-stone-landslide.webp",
"scope": "global",
"command": "game.modules.get('swade-mb-helpers').api.powerEffects({\n token,\n targets: game.user.targets,\n name: 'Burrow'\n})",
"folder": "hIbrWxg1nDutCSwt",
"ownership": {
"default": 0,
"sVoCvBU1knmXzoYe": 3
},
"flags": {
"core": {}
},
"_stats": {
"systemId": "swade",
"systemVersion": "3.1.4",
"coreVersion": "11.311",
"createdTime": 1696179835774,
"modifiedTime": 1696209757148,
"lastModifiedBy": "sVoCvBU1knmXzoYe"
},
"_id": "e4LvHlTNDy5zcGIG",
"sort": 400000,
"_key": "!macros!e4LvHlTNDy5zcGIG"
}

View File

@ -1,25 +0,0 @@
{
"name": "Confusion",
"type": "script",
"author": "R9ZgY0IvWl8ovIuT",
"img": "icons/magic/control/hypnosis-mesmerism-swirl.webp",
"scope": "global",
"command": "game.modules.get('swade-mb-helpers').api.powerEffects({\n token,\n targets: game.user.targets,\n name: 'Confusion'\n})",
"flags": {},
"_stats": {
"systemId": "swade",
"systemVersion": "3.1.4",
"coreVersion": "11.311",
"createdTime": 1678082334572,
"modifiedTime": 1696209757148,
"lastModifiedBy": "sVoCvBU1knmXzoYe"
},
"_id": "L2IstecV7ivcrgUI",
"folder": "hIbrWxg1nDutCSwt",
"sort": 2000000,
"ownership": {
"default": 0,
"R9ZgY0IvWl8ovIuT": 3
},
"_key": "!macros!L2IstecV7ivcrgUI"
}

View File

@ -1,27 +0,0 @@
{
"name": "Darksight",
"type": "script",
"author": "sVoCvBU1knmXzoYe",
"img": "icons/magic/perception/eye-ringed-glow-angry-small-teal.webp",
"scope": "global",
"command": "game.modules.get('swade-mb-helpers').api.powerEffects({\n token,\n targets: game.user.targets,\n name: 'Darksight'\n})",
"folder": "hIbrWxg1nDutCSwt",
"ownership": {
"default": 0,
"sVoCvBU1knmXzoYe": 3
},
"flags": {
"core": {}
},
"_stats": {
"systemId": "swade",
"systemVersion": "3.1.4",
"coreVersion": "11.311",
"createdTime": 1696179835774,
"modifiedTime": 1696209757148,
"lastModifiedBy": "sVoCvBU1knmXzoYe"
},
"_id": "YIdF96EfItR641oP",
"sort": 300000,
"_key": "!macros!YIdF96EfItR641oP"
}

View File

@ -1,25 +0,0 @@
{
"name": "Deflection",
"type": "script",
"scope": "global",
"author": "R9ZgY0IvWl8ovIuT",
"img": "icons/magic/defensive/shield-barrier-deflect-teal.webp",
"command": "game.modules.get('swade-mb-helpers').api.powerEffects({\n token,\n targets: game.user.targets,\n name: 'Deflection'\n})",
"flags": {},
"_stats": {
"systemId": "swade",
"systemVersion": "3.1.4",
"coreVersion": "11.311",
"createdTime": 1677645552357,
"modifiedTime": 1696209757148,
"lastModifiedBy": "sVoCvBU1knmXzoYe"
},
"_id": "HYTiftQW0pwwOQGH",
"folder": "hIbrWxg1nDutCSwt",
"sort": 1900000,
"ownership": {
"default": 0,
"R9ZgY0IvWl8ovIuT": 3
},
"_key": "!macros!HYTiftQW0pwwOQGH"
}

View File

@ -1,27 +0,0 @@
{
"name": "Detect/Conceal Arcana",
"type": "script",
"author": "sVoCvBU1knmXzoYe",
"img": "icons/magic/perception/third-eye-blue-red.webp",
"scope": "global",
"command": "game.modules.get('swade-mb-helpers').api.powerEffects({\n token,\n targets: game.user.targets,\n name: 'Detect/Conceal Arcana'\n})",
"folder": "hIbrWxg1nDutCSwt",
"ownership": {
"default": 0,
"sVoCvBU1knmXzoYe": 3
},
"flags": {
"core": {}
},
"_stats": {
"systemId": "swade",
"systemVersion": "3.1.4",
"coreVersion": "11.311",
"createdTime": 1696208623170,
"modifiedTime": 1696209757148,
"lastModifiedBy": "sVoCvBU1knmXzoYe"
},
"_id": "stHHxnYfGW0X1l5R",
"sort": 600000,
"_key": "!macros!stHHxnYfGW0X1l5R"
}

View File

@ -1,27 +0,0 @@
{
"name": "Disguise",
"type": "script",
"author": "sVoCvBU1knmXzoYe",
"img": "icons/skills/social/diplomacy-peace-alliance.webp",
"scope": "global",
"command": "game.modules.get('swade-mb-helpers').api.powerEffects({\n token,\n targets: game.user.targets,\n name: 'Disguise'\n})",
"folder": "hIbrWxg1nDutCSwt",
"ownership": {
"default": 0,
"sVoCvBU1knmXzoYe": 3
},
"flags": {
"core": {}
},
"_stats": {
"systemId": "swade",
"systemVersion": "3.1.4",
"coreVersion": "11.311",
"createdTime": 1696208623170,
"modifiedTime": 1696209757148,
"lastModifiedBy": "sVoCvBU1knmXzoYe"
},
"_id": "MYfrVZpLSrpp0vYW",
"sort": 200000,
"_key": "!macros!MYfrVZpLSrpp0vYW"
}

View File

@ -1,19 +0,0 @@
{
"name": "Effect Macros",
"sorting": "a",
"folder": null,
"type": "Macro",
"_id": "hIbrWxg1nDutCSwt",
"sort": 0,
"color": null,
"flags": {},
"_stats": {
"systemId": "swade",
"systemVersion": "3.2.2",
"coreVersion": "11.315",
"createdTime": 1695251622720,
"modifiedTime": 1700436446749,
"lastModifiedBy": "R9ZgY0IvWl8ovIuT"
},
"_key": "!folders!hIbrWxg1nDutCSwt"
}

View File

@ -1,25 +0,0 @@
{
"name": "Entangle",
"type": "script",
"scope": "global",
"author": "R9ZgY0IvWl8ovIuT",
"img": "icons/magic/nature/root-vine-barrier-wall-brown.webp",
"command": "game.modules.get('swade-mb-helpers').api.powerEffects({\n token,\n targets: game.user.targets,\n name: 'Entangle'\n})",
"flags": {},
"_stats": {
"systemId": "swade",
"systemVersion": "3.1.4",
"coreVersion": "11.311",
"createdTime": 1678164427219,
"modifiedTime": 1696209757148,
"lastModifiedBy": "sVoCvBU1knmXzoYe"
},
"_id": "2TOeRNCJT3T2px8D",
"folder": "hIbrWxg1nDutCSwt",
"sort": 1800000,
"ownership": {
"default": 0,
"R9ZgY0IvWl8ovIuT": 3
},
"_key": "!macros!2TOeRNCJT3T2px8D"
}

File diff suppressed because one or more lines are too long

View File

@ -1,27 +0,0 @@
{
"name": "Havoc",
"type": "script",
"scope": "global",
"author": "R9ZgY0IvWl8ovIuT",
"img": "icons/magic/air/air-burst-spiral-yellow.webp",
"command": "game.modules.get('swade-mb-helpers').api.powerEffects({\n token,\n targets: game.user.targets,\n name: 'Havoc'\n})",
"flags": {
"core": {}
},
"_stats": {
"systemId": "swade",
"systemVersion": "3.2.2",
"coreVersion": "11.315",
"createdTime": 1678164427219,
"modifiedTime": 1700436564699,
"lastModifiedBy": "R9ZgY0IvWl8ovIuT"
},
"folder": "hIbrWxg1nDutCSwt",
"ownership": {
"default": 0,
"R9ZgY0IvWl8ovIuT": 3
},
"_id": "0CalvjuxtMvY2enn",
"sort": 0,
"_key": "!macros!0CalvjuxtMvY2enn"
}

View File

@ -1,25 +0,0 @@
{
"name": "Intangibility",
"type": "script",
"author": "R9ZgY0IvWl8ovIuT",
"img": "icons/magic/control/debuff-energy-hold-levitate-blue-yellow.webp",
"scope": "global",
"command": "game.modules.get('swade-mb-helpers').api.powerEffects({\n token,\n targets: game.user.targets,\n name: 'Intangibility'\n})",
"flags": {},
"_stats": {
"systemId": "swade",
"systemVersion": "3.1.4",
"coreVersion": "11.311",
"createdTime": 1678168528898,
"modifiedTime": 1696209757148,
"lastModifiedBy": "sVoCvBU1knmXzoYe"
},
"_id": "OMDjgWLJyE9BJAwT",
"folder": "hIbrWxg1nDutCSwt",
"sort": 1700000,
"ownership": {
"default": 0,
"R9ZgY0IvWl8ovIuT": 3
},
"_key": "!macros!OMDjgWLJyE9BJAwT"
}

View File

@ -1,25 +0,0 @@
{
"name": "Invisibility",
"type": "script",
"author": "R9ZgY0IvWl8ovIuT",
"img": "icons/svg/invisible.svg",
"scope": "global",
"command": "game.modules.get('swade-mb-helpers').api.powerEffects({\n token,\n targets: game.user.targets,\n name: 'Invisibility'\n})",
"flags": {},
"_stats": {
"systemId": "swade",
"systemVersion": "3.1.4",
"coreVersion": "11.311",
"createdTime": 1678168163811,
"modifiedTime": 1696209757148,
"lastModifiedBy": "sVoCvBU1knmXzoYe"
},
"_id": "11GOryzx2Q8MXbT6",
"folder": "hIbrWxg1nDutCSwt",
"sort": 1600000,
"ownership": {
"default": 0,
"R9ZgY0IvWl8ovIuT": 3
},
"_key": "!macros!11GOryzx2Q8MXbT6"
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,25 +0,0 @@
{
"name": "Protection",
"type": "script",
"scope": "global",
"author": "R9ZgY0IvWl8ovIuT",
"img": "systems/swade/assets/icons/status/status_protection.svg",
"command": "game.modules.get('swade-mb-helpers').api.powerEffects({\n token,\n targets: game.user.targets,\n name: 'Protection'\n})",
"flags": {},
"_stats": {
"systemId": "swade",
"systemVersion": "3.1.4",
"coreVersion": "11.311",
"createdTime": 1677630174987,
"modifiedTime": 1696209757148,
"lastModifiedBy": "sVoCvBU1knmXzoYe"
},
"_id": "YETsNWOWfIxyLPdC",
"folder": "hIbrWxg1nDutCSwt",
"sort": 1500000,
"ownership": {
"default": 0,
"R9ZgY0IvWl8ovIuT": 3
},
"_key": "!macros!YETsNWOWfIxyLPdC"
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,25 +0,0 @@
{
"folder": "hIbrWxg1nDutCSwt",
"name": "Shape Change",
"type": "script",
"_id": "8gxeYSUJ1FQhmJRw",
"author": "sVoCvBU1knmXzoYe",
"img": "icons/magic/symbols/runes-star-blue.webp",
"scope": "global",
"command": "game.modules.get('swade-mb-helpers').api.powerEffects({\n token,\n targets: game.user.targets,\n name: 'Shape Change'\n})",
"sort": 100000,
"ownership": {
"default": 0,
"sVoCvBU1knmXzoYe": 3
},
"flags": {},
"_stats": {
"systemId": "swade",
"systemVersion": "3.1.4",
"coreVersion": "11.311",
"createdTime": 1695618313958,
"modifiedTime": 1696209757148,
"lastModifiedBy": "sVoCvBU1knmXzoYe"
},
"_key": "!macros!8gxeYSUJ1FQhmJRw"
}

View File

@ -1,27 +0,0 @@
{
"name": "Sloth/Speed",
"type": "script",
"author": "sVoCvBU1knmXzoYe",
"img": "icons/skills/movement/feet-winged-sandals-tan.webp",
"scope": "global",
"command": "game.modules.get('swade-mb-helpers').api.powerEffects({\n token,\n targets: game.user.targets,\n name: 'Sloth/Speed'\n})",
"flags": {
"core": {}
},
"_stats": {
"systemId": "swade",
"systemVersion": "3.1.4",
"coreVersion": "11.311",
"createdTime": 1677996503821,
"modifiedTime": 1696209757148,
"lastModifiedBy": "sVoCvBU1knmXzoYe"
},
"folder": "hIbrWxg1nDutCSwt",
"ownership": {
"default": 0,
"sVoCvBU1knmXzoYe": 3
},
"_id": "uWF4I2mnDkV8NZ6j",
"sort": 500000,
"_key": "!macros!uWF4I2mnDkV8NZ6j"
}

View File

@ -1,27 +0,0 @@
{
"name": "Smite",
"type": "script",
"scope": "global",
"author": "R9ZgY0IvWl8ovIuT",
"img": "systems/swade/assets/icons/status/status_smite.svg",
"command": "game.modules.get('swade-mb-helpers').api.powerEffects({\n token,\n targets: game.user.targets,\n name: 'Smite'\n})",
"flags": {
"core": {}
},
"_stats": {
"systemId": "swade",
"systemVersion": "3.1.4",
"coreVersion": "11.311",
"createdTime": 1677548889704,
"modifiedTime": 1696209757148,
"lastModifiedBy": "sVoCvBU1knmXzoYe"
},
"_id": "C1vGk7AKQDpcvKyP",
"folder": "hIbrWxg1nDutCSwt",
"sort": 1400000,
"ownership": {
"default": 0,
"R9ZgY0IvWl8ovIuT": 3
},
"_key": "!macros!C1vGk7AKQDpcvKyP"
}

View File

@ -1,25 +0,0 @@
{
"name": "Summon Ally",
"type": "script",
"author": "R9ZgY0IvWl8ovIuT",
"img": "icons/magic/symbols/runes-star-orange.webp",
"scope": "global",
"command": "game.modules.get('swade-mb-helpers').api.powerEffects({\n token,\n targets: game.user.targets,\n name: 'Summon Ally'\n})",
"flags": {},
"_stats": {
"systemId": "swade",
"systemVersion": "3.1.4",
"coreVersion": "11.311",
"createdTime": 1677996503821,
"modifiedTime": 1696209757148,
"lastModifiedBy": "sVoCvBU1knmXzoYe"
},
"_id": "RV09eJi9iG5bfupo",
"folder": "hIbrWxg1nDutCSwt",
"sort": 1000000,
"ownership": {
"default": 0,
"R9ZgY0IvWl8ovIuT": 3
},
"_key": "!macros!RV09eJi9iG5bfupo"
}

View File

@ -1,25 +0,0 @@
{
"name": "Summon Animal",
"type": "script",
"_id": "745gcs8ytsCLPXe1",
"author": "R9ZgY0IvWl8ovIuT",
"img": "icons/magic/symbols/mask-yellow-orange.webp",
"scope": "global",
"command": "game.modules.get('swade-mb-helpers').api.powerEffects({\n token,\n targets: game.user.targets,\n name: 'Summon Animal'\n})",
"folder": "hIbrWxg1nDutCSwt",
"flags": {},
"_stats": {
"systemId": "swade",
"systemVersion": "3.1.4",
"coreVersion": "11.311",
"createdTime": 1694403406793,
"modifiedTime": 1696209757148,
"lastModifiedBy": "sVoCvBU1knmXzoYe"
},
"sort": 800000,
"ownership": {
"default": 0,
"R9ZgY0IvWl8ovIuT": 3
},
"_key": "!macros!745gcs8ytsCLPXe1"
}

View File

@ -1,25 +0,0 @@
{
"name": "Summon Monster",
"type": "script",
"_id": "V8r5hugGBQfqlhYt",
"author": "R9ZgY0IvWl8ovIuT",
"img": "icons/magic/symbols/mask-metal-silver-white.webp",
"scope": "global",
"command": "game.modules.get('swade-mb-helpers').api.powerEffects({\n token,\n targets: game.user.targets,\n name: 'Summon Monster'\n})",
"folder": "hIbrWxg1nDutCSwt",
"flags": {},
"_stats": {
"systemId": "swade",
"systemVersion": "3.1.4",
"coreVersion": "11.311",
"createdTime": 1694403446899,
"modifiedTime": 1696209757148,
"lastModifiedBy": "sVoCvBU1knmXzoYe"
},
"sort": 1300000,
"ownership": {
"default": 0,
"R9ZgY0IvWl8ovIuT": 3
},
"_key": "!macros!V8r5hugGBQfqlhYt"
}

View File

@ -1,25 +0,0 @@
{
"name": "Summon Nature's Ally",
"type": "script",
"_id": "wU2mAUnw3RW9qMT8",
"author": "R9ZgY0IvWl8ovIuT",
"img": "icons/magic/symbols/mask-yellow-orange.webp",
"scope": "global",
"command": "game.modules.get('swade-mb-helpers').api.powerEffects({\n token,\n targets: game.user.targets,\n name: 'Summon Nature's Ally'\n})",
"folder": "hIbrWxg1nDutCSwt",
"flags": {},
"_stats": {
"systemId": "swade",
"systemVersion": "3.1.4",
"coreVersion": "11.311",
"createdTime": 1694403243667,
"modifiedTime": 1696209757148,
"lastModifiedBy": "sVoCvBU1knmXzoYe"
},
"sort": 900000,
"ownership": {
"default": 0,
"R9ZgY0IvWl8ovIuT": 3
},
"_key": "!macros!wU2mAUnw3RW9qMT8"
}

View File

@ -1,25 +0,0 @@
{
"name": "Summon Planar Ally",
"type": "script",
"_id": "jACgJo0HAmkyzFjZ",
"author": "R9ZgY0IvWl8ovIuT",
"img": "icons/magic/symbols/runes-star-orange-purple.webp",
"scope": "global",
"command": "game.modules.get('swade-mb-helpers').api.powerEffects({\n token,\n targets: game.user.targets,\n name: 'Summon Planar Ally'\n})",
"folder": "hIbrWxg1nDutCSwt",
"flags": {},
"_stats": {
"systemId": "swade",
"systemVersion": "3.1.4",
"coreVersion": "11.311",
"createdTime": 1694403168266,
"modifiedTime": 1696209757148,
"lastModifiedBy": "sVoCvBU1knmXzoYe"
},
"sort": 1200000,
"ownership": {
"default": 0,
"R9ZgY0IvWl8ovIuT": 3
},
"_key": "!macros!jACgJo0HAmkyzFjZ"
}

View File

@ -1,25 +0,0 @@
{
"name": "Summon Undead",
"type": "script",
"_id": "eeX3Hoy2Uxo5BeUC",
"author": "R9ZgY0IvWl8ovIuT",
"img": "icons/magic/symbols/star-yellow.webp",
"scope": "global",
"command": "game.modules.get('swade-mb-helpers').api.powerEffects({\n token,\n targets: game.user.targets,\n name: 'Summon Undead'\n})",
"folder": "hIbrWxg1nDutCSwt",
"flags": {},
"_stats": {
"systemId": "swade",
"systemVersion": "3.1.4",
"coreVersion": "11.311",
"createdTime": 1694404089533,
"modifiedTime": 1696209757148,
"lastModifiedBy": "sVoCvBU1knmXzoYe"
},
"sort": 1100000,
"ownership": {
"default": 0,
"R9ZgY0IvWl8ovIuT": 3
},
"_key": "!macros!eeX3Hoy2Uxo5BeUC"
}

View File

@ -1,27 +0,0 @@
{
"name": "Zombie",
"type": "script",
"author": "sVoCvBU1knmXzoYe",
"img": "icons/magic/death/hand-dirt-undead-zombie.webp",
"scope": "global",
"command": "game.modules.get('swade-mb-helpers').api.powerEffects({\n token,\n targets: game.user.targets,\n name: 'Zombie'\n})",
"folder": "hIbrWxg1nDutCSwt",
"flags": {
"core": {}
},
"_stats": {
"systemId": "swade",
"systemVersion": "3.2.5",
"coreVersion": "11.315",
"createdTime": 1694404089533,
"modifiedTime": 1702859394353,
"lastModifiedBy": "sVoCvBU1knmXzoYe"
},
"ownership": {
"default": 0,
"sVoCvBU1knmXzoYe": 3
},
"_id": "mdci1DSM3UTaLzrb",
"sort": 0,
"_key": "!macros!mdci1DSM3UTaLzrb"
}

Binary file not shown.

View File

@ -1,5 +0,0 @@
2024/01/29-23:43:19.357120 7f7d00fbc700 Delete type=3 #1
2024/01/29-23:43:19.359763 7f7a63fff700 Level-0 table #5: started
2024/01/29-23:43:19.367310 7f7a63fff700 Level-0 table #5: 16288 bytes OK
2024/01/29-23:43:19.376324 7f7a63fff700 Delete type=0 #3
2024/01/29-23:43:19.376468 7f7a63fff700 Manual compaction at level-0 from '!journal!HbtPlHNFO1L6RVj0' @ 72057594037927935 : 1 .. '!journal.pages!w4TImRTAiNiqDWeL.vQhO6BVdKZOubTUQ' @ 0 : 0; will stop at (end)

Binary file not shown.

File diff suppressed because one or more lines are too long

80
release.sh Executable file
View 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
View 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()],
});

View File

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

View File

@ -1,122 +0,0 @@
import { shim } from './shim.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 = shim.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 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 = shim.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 = shim.warpgateUtil.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(shim.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: [...shim.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>`,
rolls: []
}
for (const result of results) {
const token = shim.game.scenes.get(result.sceneId).tokens.get(result.tokenId)
const roll = (
result.result instanceof shim.CONFIG.Dice.SwadeRoll
? result.result
: shim.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>')
if (roll) {
messageData.rolls.unshift(roll)
}
}
messageData.content += '</tbody></table>'
shim.ChatMessage.create(messageData, {})
return results
}
export async function requestTokenRoll (
sceneId, tokenId, rollType, rollDesc, options
) {
const scene = shim.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 }
}

View File

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

View File

@ -1,213 +0,0 @@
import { log, shim } from './shim.js'
export async function preTraitRollModifiers (actor, trait, roll, modifiers, options) {
const targets = Array.from(shim.targets)
const token = shim.canvas.tokens.controlled.length > 0 ? shim.canvas.tokens.controlled[0] : null
// log('ACTOR', actor)
// log('TOKEN', token)
// log('TRAIT', trait)
// log('ROLL', roll)
// log('MODIFIERS', modifiers)
// log('OPTIONS', options)
// log('TARGET', targets)
if (targets.some(target => target.actor.system.status.isVulnerable)) {
modifiers.push({ label: 'Target is Vulnerable', value: '+2', ignore: false })
}
if (targets.some(
target => target.actor.effects.filter(
e => !e.disabled && e.name.toLowerCase().includes('deflection')).length > 0)
) {
modifiers.push({ label: 'Target has Deflection', value: '-2', ignore: false })
}
if (targets.some(
target => target.actor.effects.filter(
e => !e.disabled && e.name.toLowerCase().includes('glow')).length > 0)
) {
modifiers.push({
label: 'Glowing target (negate 1 point of illumination penalty)',
value: '+1',
ignore: true
})
}
if (targets.some(
target => target.actor.effects.filter(
e => !e.disabled && e.name.toLowerCase().includes('shroud')).length > 0)
) {
modifiers.push({
label: 'Shrouded target',
value: '-1',
ignore: false
})
}
if (targets.length === 1 && token) {
const target = targets[0]
_addArcaneModifiers(target, modifiers)
_addRangeModifiers(token, target, options, modifiers)
const scaleMod = calcScaleMod(token, target)
if (scaleMod !== 0) {
modifiers.push({ label: 'Scale', value: scaleMod, ignore: false })
}
if (target.actor.items.find(e => e.type === 'edge' && e.system.swid === 'dodge')) {
modifiers.push({ label: 'Dodge', value: -2, ignore: true })
}
if (trait?.type === 'skill' && trait?.system?.swid === 'fighting') {
const gangUpBonus = calcGangup(token, target)
if (gangUpBonus > 0) {
modifiers.push({ label: 'Gang Up', value: gangUpBonus, ignore: false })
}
}
}
}
export async function preDamageRollModifiers (actor, item, roll, modifiers, options) {
const targets = Array.from(shim.targets)
const token = shim.canvas.tokens.controlled.length > 0 ? shim.canvas.tokens.controlled[0] : null
// log('ACTOR', actor)
// log('TOKEN', token)
// log('ITEM', item)
// log('ROLL', roll)
// log('MODIFIERS', modifiers)
// log('OPTIONS', options)
// log('TARGET', targets)
if (targets.length === 1 && token) {
const target = targets[0]
_addArcaneModifiers(target, modifiers)
const weaknesses = target.actor.items.filter(
i => i.type === 'ability' && i.system.swid.toLowerCase().includes('weakness'))
if (weaknesses.length > 0) {
modifiers.push(...weaknesses.map(i => { return { label: i.name, value: '+4', ignore: true } }))
}
const resistances = target.actor.items.filter(
i => i.type === 'ability' && i.system.swid.toLowerCase().includes('resistance'))
if (resistances.length > 0) {
modifiers.push(...resistances.map(i => { return { label: i.name, value: '-4', ignore: true } }))
}
if (_findItem(token.actor, 'ability', 'pack-tactics')) {
const gangupBonus = calcGangup(token, target)
if (gangupBonus > 0) {
modifiers.push({ label: 'Gang Up (Pack Tactics)', value: gangupBonus, ignore: false })
}
}
}
}
function _addRangeModifiers (token, target, options, modifiers) {
if (options?.item?.type !== 'weapon' || !options?.item?.system?.range.includes('/')) {
return
}
const ranges = options.item.system.range.split('/').map(x => parseInt(x))
const distance = getDistance(token, target)
const rollmods = shim.CONFIG.SWADE.prototypeRollGroups.find(g => g.name === 'Range').modifiers
log('ITEM RANGES:', ranges)
if (distance <= ranges[0]) {
// nothing here
} else if (ranges.length >= 2 && distance <= ranges[1]) {
modifiers.push(rollmods[0])
} else if (ranges.length >= 3 && distance <= ranges[2]) {
modifiers.push(rollmods[1])
} else {
modifiers.push(rollmods[2]) // extreme range
}
}
function _addArcaneModifiers (target, modifiers) {
if (_findItem(target.actor, 'edge', 'improved-arcane-resistance')) {
modifiers.push({ label: 'Arcane Resistance', value: '-4', ignore: true })
} else if (_findItem(target.actor, 'edge', 'arcane-resistance')) {
modifiers.push({ label: 'Arcane Resistance', value: '-2', ignore: true })
}
const effect = target.actor.effects.find(
e => !e.disabled && e.name.toLowerCase().includes('arcane protection'))
if (effect) {
const effectName = effect.name.toLowerCase()
const effectMod = (
-2 +
(effectName.includes('major') ? -2 : 0) +
(effectName.includes('greater') ? -2 : 0)
)
modifiers.push({ label: 'Target Arcane Protection', value: effectMod, ignore: true })
}
}
function getScaleDistanceMod (token) {
const scale = token.actor.system.stats.scale
return (scale > 0 ? (scale / 2) : 0)
}
function getDistance (origin, target) {
const ray = new Ray(origin, target)
const originScale = getScaleDistanceMod(origin)
const targetScale = getScaleDistanceMod(target)
const distance = shim.canvas.grid.measureDistances([{ ray }], { gridSpaces: true })[0]
return distance - (originScale + targetScale)
}
function withinRange (origin, target, range) {
const distance = getDistance(origin, target)
return range >= distance
}
function _findItem (actor, type, swid) {
return actor.items.find(i => i.type === type && i.system.swid === swid)
}
function calcScaleMod (attacker, target) {
const attackerScale = attacker.actor.system.stats.scale
const targetScale = target.actor.system.stats.scale
const attackerHasSwat = !!_findItem(attacker.actor, 'ability', 'swat')
let modifier = targetScale - attackerScale
if (attackerHasSwat && modifier < 0) {
modifier = Math.min(modifier + 4, 0)
}
return modifier
}
function calcGangup (attacker, target, debug) {
debug = (typeof debug === 'undefined') ? false : debug
const range = 1.2
let modifier = 0
if (_findItem(target.actor, 'edge', 'improved-block')) {
modifier = -2
} else if (_findItem(target.actor, 'edge', 'block')) {
modifier = -1
}
const attackerHasFormationFighter = !!(_findItem(attacker.actor, 'edge', 'formation-fighter'))
const withinRangeOfToken = shim.canvas.tokens.placeables.filter(t =>
t.id !== attacker.id &&
t.id !== target.id &&
t.actor.system.status.isStunned === false &&
t.visible &&
withinRange(target, t, range)
)
const attackerAllies = withinRangeOfToken.filter(
t => t.document.disposition === attacker.document.disposition)
const targetAllies = withinRangeOfToken.filter(
t => t.document.disposition === target.document.disposition &&
withinRange(attacker, t, range)
)
const attackersWithFormationFighter = attackerAllies.filter(
t => !!_findItem(t.actor, 'edge', 'formation-fighter'))
const attackerCount = attackerAllies.length
const attackerFormationBonus = (
(attackerCount > 0 && attackerHasFormationFighter ? 1 : 0) +
attackersWithFormationFighter.length
)
const defenderCount = targetAllies.length
const gangUp = Math.max(
0,
Math.min(
4,
attackerCount + attackerFormationBonus - defenderCount + modifier))
if (debug) {
log('GANG UP | Attacker:', attacker)
log('GANG UP | Target:', target)
log('GANG UP | Others within range:', withinRangeOfToken)
log('GANG UP | Attacker Allies:', attackerCount)
log('GANG UP | Attacker Formation Bonus:', attackerFormationBonus)
log('GANG UP | Effective Defender Allies:', defenderCount)
log('GANG UP | Target Block Modifier:', modifier)
log('GANG UP | Total Bonus:', gangUp)
}
return gangUp
}

View File

@ -1,215 +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 CONFIG () {
return CONFIG
}
static get Actor () {
return Actor
}
static get ChatMessage () {
return ChatMessage
}
static get game () {
return game
}
static get canvas () {
return game.canvas
}
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 users () {
return game.users
}
static get notifications () {
return ui.notifications
}
static get actors () {
return game.actors
}
static get scenes () {
return game.scenes
}
static _socket = null
static get socket () {
return shim._socket
}
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 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 wait (ms) {
return warpgate.wait(ms)
}
static warpgateMenu (menuData, menuOptions) {
return warpgate.menu(menuData, menuOptions)
}
static warpgateSpawn (...args) {
return warpgate.spawn(...args)
}
static warpgateSpawnAt (...args) {
return warpgate.spawnAt(...args)
}
static get warpgateUtil () {
return warpgate.util
}
static get fearTableHelper () {
switch (shim.rulesVersion) {
case 'swade': return coreFearDialog
case 'swpf': return swpfFearDialog
}
throw new ReferenceError('No premium module active. No fear table found')
}
static getActorFolderByPath (path) {
const names = path.split('/')
if (names[0] === '') {
names.shift()
}
let name = names.shift()
let folder = shim.folders.filter(
f => f.type === 'Actor' && !f.folder
).find(f => f.name === name)
if (!folder) { return undefined }
while (names.length > 0) {
name = names.shift()
folder = folder.children.find(c => c.folder.name === name)
if (!folder) { return undefined }
folder = folder.folder
}
return folder
}
static getActorsInFolder (inFolder) {
const prefixStack = ['']
const actors = {}
const folderStack = [inFolder]
while (folderStack.length > 0) {
const prefix = prefixStack.shift()
const folder = folderStack.shift()
for (const actor of folder.contents) {
if (shim.user.isGM ||
actor.testUserPermission(
shim.user, CONST.FOUNDRY.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER)
) {
actors[`${prefix}${actor.name}`] = actor
}
}
for (const child of folder.children) {
const newPrefix = `${prefix}${child.folder.name} | `
prefixStack.push(newPrefix)
folderStack.push(child.folder)
}
}
return actors
}
}
export function log (...args) {
console.log('SWADE MB HELPERS |', ...args)
}

View File

@ -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
}
]
}

6
src/lang/en.json Normal file
View 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)."
}

View File

@ -1,21 +1,25 @@
{
"id": "swade-mb-helpers",
"title": "SWADE Helpers (MB)",
"version": "2.3.5",
"description": "Mike's collection of swade helpers",
"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"
],
"socket": true,
"packs": [
{
"name": "module-docs",
@ -61,6 +65,17 @@
"ASSISTANT": "OWNER"
}
},
{
"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",
@ -84,7 +99,8 @@
"helper-macros",
"helper-actors",
"Common Actions",
"swade-mb-gear"
"swade-mb-gear",
"power-actors"
]
}
],
@ -95,31 +111,29 @@
"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",
"id": "socketlib",
"type": "module",
"manifest": "https://github.com/trioderegion/warpgate/releases/latest/download/module.json",
"compatibility": {
"verified": "1.16.2"
}
"compatibility": {}
},
{
"id": "socketlib",
"id": "tcal",
"type": "module",
"compatibility": {}
},
{
"id": "sequencer",
"type": "module",
"compatibility": {}
}
],
"recommends": [
{
"id": "token-variants",
"type": "module",
"compatibility": {}
},
{
"id": "torch",
"type": "module",
@ -127,13 +141,25 @@
},
{
"id": "JB2A_DnD5e",
"type": "module"
"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
View 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
View 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
View 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 }],
});
},
});
}
}

View 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})`;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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
View 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
View 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;
}
}

View 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];
}
}

View 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;
}
}

View 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>`;
}
}

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