Compare commits

...

258 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
92aced8f40 change some blindsense params 2024-01-30 19:31:54 -06:00
3964e87982 add changelog and version bump 2024-01-29 23:49:31 -06:00
8c7f7d2d1d update packages 2024-01-29 23:43:54 -06:00
b8936ab53f change setTokenVision macro to detect blindsense 2024-01-29 23:42:49 -06:00
a943106a49 add blindsense sensor mode 2024-01-29 23:27:25 -06:00
63b25faec0 bump to v2.3.3 2024-01-28 17:29:46 -06:00
2781de96da packs update 2024-01-28 17:26:41 -06:00
b37378d0fd update changelog 2024-01-23 00:32:54 -06:00
73c5ca6e3b v2.3.2 2024-01-23 00:30:47 -06:00
261759d514 add swade poison macros until swpf gets them 2024-01-23 00:29:46 -06:00
5514da4774 add dodge 2024-01-05 22:44:58 -06:00
1834fec172 Update modules.json for v2.3.1 2023-12-27 04:19:02 +00:00
3fe1df7c7f Merge pull request '2.31' (#42) from develop into main
Reviewed-on: #42
2023-12-27 04:13:59 +00:00
8c06f0e2a1 Prepare 2.3.1 2023-12-26 22:09:51 -06:00
6ed989c4bc several changes that were not properly split up
- Macro: Request fear check specialization macro
- Macro: Fear Table to call the new fearTable api endpoint
- API: rulesVersion property
- API: fearTable(actor) calls the relevant premium core rules module's fear
  table
- API: added requestFearRollFromTokens special helper
- Trait roll hooks for:
    - Glow/Shroud
    - Range modifiers
- added a summary chat message for the roll results to requested rolls.
- added a target number option to requested rolls.
2023-12-23 22:50:15 -06:00
a0b2df50c6 Merge pull request '2.3.0 module manifest' (#41) from develop into main
Reviewed-on: #41
2023-12-20 04:22:11 +00:00
c43fafa7df 2.3.0 module manifest 2023-12-19 22:21:06 -06:00
b50270a61f Merge pull request '2.3.0 release' (#40) from develop into main
Reviewed-on: #40
2023-12-20 04:19:29 +00:00
5ca758257e prep 2.3.0 2023-12-19 22:16:08 -06:00
55a123759f roll modifier hooks 2023-12-19 22:12:58 -06:00
d5cc256c4f set token vision macro 2023-12-18 23:39:49 -06:00
66ee916572 add quick damage roll macro 2023-12-18 15:48:11 -06:00
952511b182 overdue update of changelog 2023-12-18 12:31:31 -06:00
85923d9e8e unpacked source for packs 2023-12-18 12:13:27 -06:00
8f7881051b added docs for zombie 2023-12-17 18:44:22 -06:00
af70593334 add zombie to power effects 2023-12-17 17:57:43 -06:00
54da536518 untested addition to shape change and summon 2023-12-17 09:33:05 -06:00
ad42634bd7 Merge pull request 'prep 2.2.0 release' (#39) from develop into main
Reviewed-on: #39
2023-11-21 04:03:19 +00:00
eba1a7befc Merge pull request '2.2.0 release' (#38) from develop into main
Reviewed-on: #38
2023-11-21 04:02:08 +00:00
6e045714c8 prep 2.2.0 release 2023-11-20 21:57:06 -06:00
8f4e8a365d add packs changes 2023-11-20 21:54:23 -06:00
de690b1451 add changelog 2023-11-20 21:53:35 -06:00
3996687ad5 WIP roll request dialog 2023-11-19 11:34:42 -06:00
9eb7f4c5ea updated havoc 2023-11-18 23:58:09 -06:00
0fba8a73b0 add roll-requesting function 2023-11-18 19:28:12 -06:00
57b00686c8 WIP: add generalized roll requester 2023-11-12 22:44:31 -06:00
5002134e2c develop WIP with working havoc 2023-11-12 22:44:07 -06:00
f4495df534 add gear pack 2023-11-09 21:44:25 -06:00
274b63d470 Merge pull request 'Release version 2.1.0' (#37) from develop into main
Reviewed-on: #37
2023-11-08 05:38:19 +00:00
b2ff0b29b9 prep 2.1.0 release 2023-11-07 23:35:44 -06:00
a10291619c Merge pull request 'fix-mirror-self' (#36) from fix-mirror-self into develop
Reviewed-on: #36
2023-11-08 05:32:44 +00:00
3c0afc05d0 update documentation 2023-11-07 23:31:45 -06:00
a8ba0a64c0 update to support swids 2023-11-07 23:28:55 -06:00
a5373585fb make mirror self simpler 2023-11-07 22:52:44 -06:00
6ad1452922 Merge pull request 'Version 2.0.0' (#35) from develop into main
Reviewed-on: #35
2023-10-02 01:51:46 +00:00
9590fd8e0d version bump 2023-10-01 20:46:01 -05:00
b79ca1b4a6 packs + disguise 2023-10-01 20:45:16 -05:00
d9917ac5c8 change to game.modules.get(name) method 2023-10-01 12:07:27 -05:00
41e1ade442 add burrow and darksight 2023-10-01 11:48:49 -05:00
416661727b add sloth/speed 2023-09-30 12:31:20 -05:00
be55e94ea4 Merge remote-tracking branch 'origin/main' into develop 2023-09-25 22:39:57 -05:00
acba9d1f45 version bump 2023-09-26 03:39:20 +00:00
8869248ab8 packs update 2023-09-26 03:39:20 +00:00
ea4129c927 finish shape change functionality 2023-09-26 03:39:20 +00:00
f711e495a3 working shape change (needs warpgate update) 2023-09-26 03:39:20 +00:00
9e98425e63 shape change with popup side effects 2023-09-26 03:39:20 +00:00
bdc8a0bd83 make smite simpler 2023-09-26 03:39:20 +00:00
e16b0277a2 fix bug in player mutations not applying 2023-09-26 03:39:20 +00:00
8bc6f4c3dc Packs update 2023-09-26 03:39:20 +00:00
11f3317aeb Packs update 2023-09-26 03:39:20 +00:00
8f99a1a0ad lingering damage helper 2023-09-26 03:39:20 +00:00
3927291234 Add Arcane Protection 2023-09-26 03:39:20 +00:00
1d4223274c version bump 2023-09-25 22:38:31 -05:00
8750378977 Merge pull request 'shape-change' (#33) from shape-change into develop
Reviewed-on: #33
2023-09-26 03:37:39 +00:00
252 changed files with 24124 additions and 10831 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: {
}
}

6
.gitattributes vendored
View File

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

146
.gitignore vendored
View File

@ -1,132 +1,32 @@
# ---> Node # SPDX-FileCopyrightText: 2022 Johannes Loher
# Logs #
logs # SPDX-License-Identifier: MIT
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html) # IDE
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json .idea/
.vs/
# Runtime data # Node Modules
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/) # yarn2
web_modules/ .yarn/*
!.yarn/patches
!.yarn/releases
!.yarn/plugins
!.yarn/sdks
!.yarn/versions
.pnp.*
# TypeScript cache # Local configuration
*.tsbuildinfo foundryconfig.json
# Optional npm cache directory # Distribution files
.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
dist dist
# Gatsby files # ESLint
.cache/ .eslintcache
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output # Junit results
.vuepress/dist junit.xml
# 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

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

4
macros/powerMenu.js Normal file
View File

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

View File

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

43
macros/quickDamage.js Normal file
View File

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

42
macros/requestFearRoll.js Normal file
View File

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

95
macros/requestRoll.js Normal file
View File

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

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

156
macros/setTokenVision.js Normal file
View File

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

View File

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

View File

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

View File

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

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": { "devDependencies": {
"@league-of-foundry-developers/foundry-vtt-types": "^9.280.0", "@rollup/plugin-node-resolve": "^15.2.3",
"eslint": "^8.48.0", "@rollup/stream": "^3.0.1",
"eslint-config-standard": "^17.1.0", "@typhonjs-fvtt/eslint-config-foundry.js": "^0.8.0",
"eslint-plugin-import": "^2.28.1", "eslint": "^9.2.0",
"eslint-plugin-n": "^16.0.2", "eslint-config-prettier": "^10.1.5",
"eslint-plugin-promise": "^6.1.1", "eslint-plugin-prettier": "^5.1.3",
"typescript": "^5.2.2" "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 +0,0 @@
MANIFEST-000138

View File

@ -1,8 +0,0 @@
2023/09/25-22:33:46.979390 7fe7b968d700 Recovering log #135
2023/09/25-22:33:46.999374 7fe7b968d700 Delete type=3 #133
2023/09/25-22:33:46.999397 7fe7b968d700 Delete type=0 #135
2023/09/25-22:34:49.573980 7fe51bfff700 Level-0 table #141: started
2023/09/25-22:34:49.573997 7fe51bfff700 Level-0 table #141: 0 bytes OK
2023/09/25-22:34:49.583422 7fe51bfff700 Delete type=0 #139
2023/09/25-22:34:49.602253 7fe51bfff700 Manual compaction at level-0 from '!folders!0nDRFmMBs5DBJU9M' @ 72057594037927935 : 1 .. '!items!xA7qKMmugJv7z6j1' @ 0 : 0; will stop at (end)
2023/09/25-22:34:49.611494 7fe51bfff700 Manual compaction at level-1 from '!folders!0nDRFmMBs5DBJU9M' @ 72057594037927935 : 1 .. '!items!xA7qKMmugJv7z6j1' @ 0 : 0; will stop at (end)

View File

@ -1,15 +0,0 @@
2023/09/24-17:51:32.119358 7fe7b9e8e700 Recovering log #132
2023/09/24-17:51:32.127144 7fe7b9e8e700 Delete type=3 #131
2023/09/24-17:51:32.127174 7fe7b9e8e700 Delete type=0 #132
2023/09/25-22:27:28.458282 7fe51bfff700 Level-0 table #136: started
2023/09/25-22:27:28.461383 7fe51bfff700 Level-0 table #136: 5360 bytes OK
2023/09/25-22:27:28.464487 7fe51bfff700 Delete type=0 #134
2023/09/25-22:27:28.468877 7fe51bfff700 Manual compaction at level-0 from '!folders!0nDRFmMBs5DBJU9M' @ 72057594037927935 : 1 .. '!items!xA7qKMmugJv7z6j1' @ 0 : 0; will stop at (end)
2023/09/25-22:27:28.468974 7fe51bfff700 Manual compaction at level-1 from '!folders!0nDRFmMBs5DBJU9M' @ 72057594037927935 : 1 .. '!items!xA7qKMmugJv7z6j1' @ 0 : 0; will stop at '!items!vdYpJXIMdqUi7IDh' @ 170 : 1
2023/09/25-22:27:28.468980 7fe51bfff700 Compacting 1@1 + 1@2 files
2023/09/25-22:27:28.471549 7fe51bfff700 Generated table #137@1: 17 keys, 8737 bytes
2023/09/25-22:27:28.471557 7fe51bfff700 Compacted 1@1 + 1@2 files => 8737 bytes
2023/09/25-22:27:28.474310 7fe51bfff700 compacted to: files[ 0 0 1 0 0 0 0 ]
2023/09/25-22:27:28.474351 7fe51bfff700 Delete type=2 #136
2023/09/25-22:27:28.474385 7fe51bfff700 Delete type=2 #130
2023/09/25-22:27:28.482548 7fe51bfff700 Manual compaction at level-1 from '!items!vdYpJXIMdqUi7IDh' @ 170 : 1 .. '!items!xA7qKMmugJv7z6j1' @ 0 : 0; will stop at (end)

Binary file not shown.

Binary file not shown.

View File

@ -1 +0,0 @@
MANIFEST-000040

View File

@ -1,8 +0,0 @@
2023/09/25-22:33:47.001326 7fe7b9e8e700 Recovering log #38
2023/09/25-22:33:47.021271 7fe7b9e8e700 Delete type=0 #38
2023/09/25-22:33:47.021294 7fe7b9e8e700 Delete type=3 #36
2023/09/25-22:34:49.621185 7fe51bfff700 Level-0 table #43: started
2023/09/25-22:34:49.621202 7fe51bfff700 Level-0 table #43: 0 bytes OK
2023/09/25-22:34:49.630680 7fe51bfff700 Delete type=0 #41
2023/09/25-22:34:49.650161 7fe51bfff700 Manual compaction at level-0 from '!actors!U5v4gFHquo0Y1SAq' @ 72057594037927935 : 1 .. '!actors!U5v4gFHquo0Y1SAq' @ 0 : 0; will stop at (end)
2023/09/25-22:34:49.659391 7fe51bfff700 Manual compaction at level-1 from '!actors!U5v4gFHquo0Y1SAq' @ 72057594037927935 : 1 .. '!actors!U5v4gFHquo0Y1SAq' @ 0 : 0; will stop at (end)

View File

@ -1,8 +0,0 @@
2023/09/24-17:51:32.129349 7fe7ba68f700 Recovering log #35
2023/09/24-17:51:32.137024 7fe7ba68f700 Delete type=0 #35
2023/09/24-17:51:32.137042 7fe7ba68f700 Delete type=3 #34
2023/09/25-22:27:28.464580 7fe51bfff700 Level-0 table #39: started
2023/09/25-22:27:28.464601 7fe51bfff700 Level-0 table #39: 0 bytes OK
2023/09/25-22:27:28.468758 7fe51bfff700 Delete type=0 #37
2023/09/25-22:27:28.468891 7fe51bfff700 Manual compaction at level-0 from '!actors!U5v4gFHquo0Y1SAq' @ 72057594037927935 : 1 .. '!actors!U5v4gFHquo0Y1SAq' @ 0 : 0; will stop at (end)
2023/09/25-22:27:28.482519 7fe51bfff700 Manual compaction at level-1 from '!actors!U5v4gFHquo0Y1SAq' @ 72057594037927935 : 1 .. '!actors!U5v4gFHquo0Y1SAq' @ 0 : 0; will stop at (end)

Binary file not shown.

Binary file not shown.

View File

@ -1 +0,0 @@
MANIFEST-000168

View File

@ -1,8 +0,0 @@
2023/09/25-22:33:46.957940 7fe7b9e8e700 Recovering log #165
2023/09/25-22:33:46.977376 7fe7b9e8e700 Delete type=0 #165
2023/09/25-22:33:46.977395 7fe7b9e8e700 Delete type=3 #163
2023/09/25-22:34:49.583492 7fe51bfff700 Level-0 table #171: started
2023/09/25-22:34:49.583508 7fe51bfff700 Level-0 table #171: 0 bytes OK
2023/09/25-22:34:49.592987 7fe51bfff700 Delete type=0 #169
2023/09/25-22:34:49.611444 7fe51bfff700 Manual compaction at level-0 from '!folders!hIbrWxg1nDutCSwt' @ 72057594037927935 : 1 .. '!macros!wU2mAUnw3RW9qMT8' @ 0 : 0; will stop at (end)
2023/09/25-22:34:49.611542 7fe51bfff700 Manual compaction at level-1 from '!folders!hIbrWxg1nDutCSwt' @ 72057594037927935 : 1 .. '!macros!wU2mAUnw3RW9qMT8' @ 0 : 0; will stop at (end)

View File

@ -1,15 +0,0 @@
2023/09/24-17:51:32.110592 7fe7ba68f700 Recovering log #162
2023/09/24-17:51:32.118170 7fe7ba68f700 Delete type=3 #161
2023/09/24-17:51:32.118190 7fe7ba68f700 Delete type=0 #162
2023/09/25-22:27:28.431163 7fe51bfff700 Level-0 table #166: started
2023/09/25-22:27:28.433469 7fe51bfff700 Level-0 table #166: 724 bytes OK
2023/09/25-22:27:28.436317 7fe51bfff700 Delete type=0 #164
2023/09/25-22:27:28.445034 7fe51bfff700 Manual compaction at level-0 from '!folders!hIbrWxg1nDutCSwt' @ 72057594037927935 : 1 .. '!macros!wU2mAUnw3RW9qMT8' @ 0 : 0; will stop at (end)
2023/09/25-22:27:28.445112 7fe51bfff700 Manual compaction at level-1 from '!folders!hIbrWxg1nDutCSwt' @ 72057594037927935 : 1 .. '!macros!wU2mAUnw3RW9qMT8' @ 0 : 0; will stop at '!macros!8gxeYSUJ1FQhmJRw' @ 37 : 1
2023/09/25-22:27:28.445116 7fe51bfff700 Compacting 1@1 + 1@2 files
2023/09/25-22:27:28.449145 7fe51bfff700 Generated table #167@1: 19 keys, 4653 bytes
2023/09/25-22:27:28.449155 7fe51bfff700 Compacted 1@1 + 1@2 files => 4653 bytes
2023/09/25-22:27:28.452097 7fe51bfff700 compacted to: files[ 0 0 1 0 0 0 0 ]
2023/09/25-22:27:28.452136 7fe51bfff700 Delete type=2 #128
2023/09/25-22:27:28.452197 7fe51bfff700 Delete type=2 #166
2023/09/25-22:27:28.468818 7fe51bfff700 Manual compaction at level-1 from '!macros!8gxeYSUJ1FQhmJRw' @ 37 : 1 .. '!macros!wU2mAUnw3RW9qMT8' @ 0 : 0; will stop at (end)

Binary file not shown.

Binary file not shown.

View File

@ -1 +0,0 @@
MANIFEST-000168

View File

@ -1,8 +0,0 @@
2023/09/25-22:33:46.936754 7fe7b968d700 Recovering log #165
2023/09/25-22:33:46.956358 7fe7b968d700 Delete type=0 #165
2023/09/25-22:33:46.956379 7fe7b968d700 Delete type=3 #163
2023/09/25-22:34:49.564751 7fe51bfff700 Level-0 table #171: started
2023/09/25-22:34:49.564770 7fe51bfff700 Level-0 table #171: 0 bytes OK
2023/09/25-22:34:49.573732 7fe51bfff700 Delete type=0 #169
2023/09/25-22:34:49.573869 7fe51bfff700 Manual compaction at level-0 from '!journal!HbtPlHNFO1L6RVj0' @ 72057594037927935 : 1 .. '!journal.pages!Mw1g2Fx5dp4SoqVP.lhULHNp4gz9IjOR3' @ 0 : 0; will stop at (end)
2023/09/25-22:34:49.583466 7fe51bfff700 Manual compaction at level-1 from '!journal!HbtPlHNFO1L6RVj0' @ 72057594037927935 : 1 .. '!journal.pages!Mw1g2Fx5dp4SoqVP.lhULHNp4gz9IjOR3' @ 0 : 0; will stop at (end)

View File

@ -1,15 +0,0 @@
2023/09/24-17:51:32.100602 7fe7b9e8e700 Recovering log #162
2023/09/24-17:51:32.108259 7fe7b9e8e700 Delete type=3 #161
2023/09/24-17:51:32.108296 7fe7b9e8e700 Delete type=0 #162
2023/09/25-22:27:28.439755 7fe51bfff700 Level-0 table #166: started
2023/09/25-22:27:28.442121 7fe51bfff700 Level-0 table #166: 10961 bytes OK
2023/09/25-22:27:28.444862 7fe51bfff700 Delete type=0 #164
2023/09/25-22:27:28.445099 7fe51bfff700 Manual compaction at level-0 from '!journal!HbtPlHNFO1L6RVj0' @ 72057594037927935 : 1 .. '!journal.pages!Mw1g2Fx5dp4SoqVP.lhULHNp4gz9IjOR3' @ 0 : 0; will stop at (end)
2023/09/25-22:27:28.452245 7fe51bfff700 Manual compaction at level-1 from '!journal!HbtPlHNFO1L6RVj0' @ 72057594037927935 : 1 .. '!journal.pages!Mw1g2Fx5dp4SoqVP.lhULHNp4gz9IjOR3' @ 0 : 0; will stop at '!journal.pages!Mw1g2Fx5dp4SoqVP.lhULHNp4gz9IjOR3' @ 63 : 1
2023/09/25-22:27:28.452253 7fe51bfff700 Compacting 1@1 + 1@2 files
2023/09/25-22:27:28.455468 7fe51bfff700 Generated table #167@1: 7 keys, 4142 bytes
2023/09/25-22:27:28.455475 7fe51bfff700 Compacted 1@1 + 1@2 files => 4142 bytes
2023/09/25-22:27:28.458177 7fe51bfff700 compacted to: files[ 0 0 1 0 0 0 0 ]
2023/09/25-22:27:28.458213 7fe51bfff700 Delete type=2 #166
2023/09/25-22:27:28.458245 7fe51bfff700 Delete type=2 #96
2023/09/25-22:27:28.468848 7fe51bfff700 Manual compaction at level-1 from '!journal.pages!Mw1g2Fx5dp4SoqVP.lhULHNp4gz9IjOR3' @ 63 : 1 .. '!journal.pages!Mw1g2Fx5dp4SoqVP.lhULHNp4gz9IjOR3' @ 0 : 0; will stop at (end)

Binary file not shown.

80
release.sh Executable file
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,23 +0,0 @@
import { helpers } from './helpers.js'
import { shim, log } from './shim.js'
import { powerEffects } from './powerEffects.js'
export class api {
static registerFunctions () {
log('SWADE MB Helpers initialized')
api.globals()
}
static globals () {
globalThis.swadeMBHelpers = {
DEBUG: true,
powerEffects,
createEffectDocument: shim.createEffectDocument,
createMutationWithEffect: helpers.createMutationWithEffect,
defaultMutationOptions: helpers.defaultMutationOptions,
getActorFolderByPath: shim.getActorFolderByPath,
getActorsInFolder: shim.getActorsInFolder,
runOnTargetOrSelectedTokens: helpers.runOnTargetOrSelectedTokens
}
}
}

View File

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

View File

@ -1,20 +0,0 @@
import { api } from './api.js'
import { shapeChangeOnDismiss } from './powerEffects.js'
import { log } from './shim.js'
function _checkModule (name) {
if (!game.modules.get(name)?.active && game.user.isGM) {
let action = 'install and activate'
if (game.modules.get(name)) action = 'activate'
ui.notifications.error(
`SWADE MB Helpers requires the ${name} module. Please ${action} it.`)
}
}
Hooks.on('setup', api.registerFunctions)
Hooks.on('ready', () => {
_checkModule('warpgate')
log('Initialized SWADE MB Helpers')
warpgate.event.watch(warpgate.EVENT.DISMISS, shapeChangeOnDismiss)
})

View File

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

View File

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

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

111
src/config/torch_swade.json Normal file
View File

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

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

View File

@ -0,0 +1,112 @@
import { PowerEffect } from './basePowers.js';
export class ConfusionEffect extends PowerEffect {
get name() {
return 'Confusion';
}
get icon() {
return 'icons/magic/control/hypnosis-mesmerism-swirl.webp';
}
get duration() {
return 0;
}
get isTargeted() {
return true;
}
get usePrimaryEffect() {
return false;
}
get basePowerPoints() {
return 2;
}
get hasAoe() {
return true;
}
get modifiers() {
const mods = super.modifiers;
mods.push({
type: 'select',
default: 'mbt',
name: 'Area of Effect',
id: 'aoe',
epic: false,
choices: {
sbt: 'Small Blast Template',
mbt: 'Medium Blast Template',
lbt: 'Large Blast Template',
},
effects: { sbt: null, mbt: null, lbt: null },
values: { sbt: 0, mbt: 0, lbt: 1 },
});
mods.push({
name: 'Greater Confusion',
value: 2,
id: 'greater',
epic: true,
type: 'checkbox',
effect: false,
});
return mods;
}
get powerPoints() {
let total = super.powerPoints;
total += this.data.aoe === 'l' ? 1 : 0;
return total;
}
get menuButtons() {
const data = [
{ label: 'Apply with Distracted', value: 'distracted' },
{ label: 'Apply with Vulnerable', value: 'vulnerable' },
{ label: 'Apply with both (raise)', value: 'raise' },
{ label: 'Cancel', value: 'cancel' },
];
return data;
}
async parseValues() {
await super.parseValues();
this.data.distracted = this.data.button === 'distracted' || this.data.button === 'raise';
this.data.vulnerable = this.data.button === 'vulnerable' || this.data.button === 'raise';
}
get description() {
const size = this.data.aoe.toUpperCase();
let effect = 'Vulnerable';
if (this.data.raise) {
effect = 'both Distracted and Vulnerable';
} else if (this.data.distracted) {
effect = 'Distracted';
}
if (this.data.Greater) {
effect += ' as well as Shaken';
}
return (
super.description +
`
<p>The targets in the ${size} are ${effect}.</p>`
);
}
async createSecondaryEffects(maintId) {
const docs = await super.createSecondaryEffects(maintId);
if (this.data.distracted) {
PowerEffect.getStatus('SWADE.Distr', 'Distracted', false).then((v) => docs.push(v));
}
if (this.data.vulnerable) {
PowerEffect.getStatus('SWADE.Vuln', 'Vulnerable', false).then((v) => docs.push(v));
}
if (this.data.greater) {
PowerEffect.getStatus('SWADE.Shaken', 'Shaken', false).then((v) => docs.push(v));
}
return docs;
}
}

View File

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

View File

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

View File

@ -0,0 +1,64 @@
import { PowerEffect } from './basePowers.js';
export class CurseEffect extends PowerEffect {
get name() {
return 'Curse';
}
get icon() {
return 'icons/magic/control/voodoo-doll-pain-damage-purple.webp';
}
get duration() {
return 500 * 24 * 60 * 6;
}
get isTargeted() {
return true;
}
get oneTarget() {
return true;
}
get isRaisable() {
return false;
}
get basePowerPoints() {
return 5;
}
get modifiers() {
const mods = super.modifiers;
mods.push({
name: 'Turn to Stone',
type: 'checkbox',
value: 5,
id: 'turntostone',
epic: true,
effect: false,
});
return mods;
}
get description() {
let desc = super.description;
desc += `<p>The victim must defend with a Spirit roll opposed by the
caster's arcane skill roll. Failure means the victim suffers a level
of Fatigue immediately.</p>`;
if (this.data.turntostone) {
desc += `<p>On every following run the victim must make a Spirit roll
or take a level of Fatigue. When Incapacitated, the victim turns to
stone, with a Hardness equal to his Tougness.</p>`;
} else {
desc += `<p>At sunset every day, the victim suffers a level of Fatigue.
When Incapacitated by this, he makes a Vigor roll each day to avoid
death.</p>`;
}
desc += `<p><strong>Breaking the curse:</strong> The curse can be lifted by
the original caster at will, and ends if the caster is slain. Dispel at -2
also removes the curse, but each individual may only try once.</p>`;
return desc;
}
}

View File

@ -0,0 +1,126 @@
import { PowerEffect } from './basePowers.js';
export class DamageFieldEffect extends PowerEffect {
get name() {
return 'Damage Field';
}
get icon() {
return 'icons/magic/defensive/shield-barrier-blades-teal.webp';
}
get duration() {
return 5;
}
get basePowerPoints() {
return 4;
}
get isTargeted() {
return true;
}
get oneTarget() {
return true;
}
get isRaisable() {
return false;
}
get modifiers() {
const mods = super.modifiers;
mods.push({
type: 'checkbox',
name: 'Area of Effect',
value: 2,
id: 'aoe',
epic: false,
effect: false,
});
mods.push({
type: 'checkbox',
name: 'Damage',
value: 2,
id: 'damage',
epic: false,
effect: false,
});
mods.push({
type: 'checkbox',
name: 'Greater Damage Field',
value: 4,
id: 'greater',
epic: true,
effect: false,
});
mods.push({
type: 'checkbox',
name: 'Mobile',
value: 2,
id: 'mobile',
epic: false,
effect: false,
});
return mods;
}
get primaryEffectButtons() {
const buttons = super.primaryEffectButtons;
let damage = '2d4';
if (this.data.greater) {
damage = '3d6';
} else if (this.data.damage) {
damage = '2d6';
}
buttons.push({
label: `Damage (${damage})`,
type: 'damage',
formula: `${damage}x`,
});
return buttons;
}
get description() {
let desc = super.description;
let area = 'all adjacent creatures';
let damage = '2d4';
if (this.data.greater) {
damage = '3d6 (heavy weapon)';
} else if (this.data.damage) {
damage = '2d6';
}
if (this.data.aoe) {
area = 'all creatures within a MBT';
}
desc += `<p>At the end of the recipient's turn, ${area}
automatically take ${damage} damage.`;
if (this.data.mobile) {
desc += `The caster may detach the damage field from the recipient and
move it up to his Smarts die type each round, as a limited free action.`;
}
desc += '</p>';
return desc;
}
getPrimaryEffectChanges() {
const base = 'flags.swade.auras.damagefield';
const priority = 0;
const mode = foundry.CONST.ACTIVE_EFFECT_MODES.OVERRIDE;
const changes = [
{ key: `${base}.enabled`, value: true, priority, mode },
{ key: `${base}.walls`, value: true, priority, mode },
{ key: `${base}.color`, value: '#ffcc00', priority, mode },
{ key: `${base}.alpha`, value: 0.1, priority, mode },
{
key: `${base}.radius`,
value: this.data.aoe ? 1.5 : 0.5,
priority,
mode,
},
{ key: `${base}.visibleTo`, value: [-1, 0, 1], priority, mode },
];
return changes;
}
}

View File

@ -0,0 +1,69 @@
import { PowerEffect } from './basePowers.js';
export class DarksightEffect extends PowerEffect {
get name() {
return 'Darksight';
}
get icon() {
return 'icons/magic/perception/eye-ringed-glow-angry-small-teal.webp';
}
get duration() {
return 600;
}
get basePowerPoints() {
return 1;
}
get isTargeted() {
return true;
}
get hasAdditionalRecipients() {
return true;
}
get additionalRecipientCost() {
return 1;
}
get modifiers() {
const mods = super.modifiers;
mods.push({
name: 'Greater Darksight',
type: 'checkbox',
value: 2,
id: 'greater',
epic: true,
effect: false,
});
return mods;
}
get description() {
let desc = super.description;
desc += '<p>';
if (this.data.greater) {
desc += `Can see in all darkness, ignoring all illumination penalties and
4 points of penalties from invisible creatures`;
} else if (this.data.raise) {
desc += 'Can see in Pitch Darkness and ignore up to 6 points of illumination penalties';
} else {
desc += 'Can see in darkness and ignore 4 points of illumination penalties';
}
desc += '</p>';
return desc;
}
get effectName() {
if (this.data.greater) {
return 'Greater Darksight';
} else if (this.data.raise) {
return 'Major Darksight';
} else {
return 'Darksight';
}
}
}

View File

@ -0,0 +1,58 @@
import { PowerEffect } from './basePowers.js';
export class DeflectionEffect extends PowerEffect {
get name() {
return 'Deflection';
}
get icon() {
return 'icons/magic/defensive/shield-barrier-deflect-teal.webp';
}
get duration() {
return 5;
}
get basePowerPoints() {
return 2;
}
get isTargeted() {
return true;
}
get hasAdditionalRecipients() {
return true;
}
get additionalRecipientCost() {
return 1;
}
get menuButtons() {
const data = [
{ label: 'Melee', value: 'melee' },
{ label: 'Ranged', value: 'ranged' },
{ label: 'Raise (both)', value: 'raise' },
{ label: 'Cancel', value: 'cancel' },
];
return data;
}
async parseValues() {
await super.parseValues();
this.data.affects = this.data.button === 'raise' ? 'all' : this.data.button;
}
get effectName() {
return `Deflection (${this.data.affects})`;
}
get description() {
return (
super.description +
`<p>Attackers subtract -2 from ${this.data.affects}
attacks when targeting this creature.</p>`
);
}
}

View File

@ -0,0 +1,139 @@
import { PowerEffect } from './basePowers.js';
export class DetectConcealArcanaEffect extends PowerEffect {
get name() {
return 'Detect/Conceal Arcana';
}
get icon() {
return this.data.detect
? 'icons/magic/perception/third-eye-blue-red.webp'
: 'icons/magic/perception/silhouette-stealth-shadow.webp';
}
get duration() {
return this.data.detect ? 5 : 600;
}
get basePowerPoints() {
return 2;
}
get isTargeted() {
return true;
}
get hasAdditionalRecipients() {
return true;
}
get additionalRecipientCost() {
return (this.data?.aoe || 0) > 0 ? 0 : 1;
}
get modifiers() {
const mods = super.modifiers;
mods.push({
type: 'checkbox',
name: 'Alignment Sense (detect)',
value: 1,
id: 'alignment',
epic: false,
effect: false,
});
mods.push({
type: 'checkbox',
name: 'Identify (detect)',
value: 1,
id: 'identify',
epic: false,
effect: false,
});
mods.push({
type: 'checkbox',
name: 'Strong (conceal)',
value: 1,
id: 'strong',
epic: false,
effect: false,
});
mods.push({
sortOrder: -2,
name: 'Detect or Conceal?',
id: 'direction',
type: 'radio',
default: 'Detect',
epic: false,
choices: { Detect: 'Detect', Conceal: 'Conceal' },
effects: { Detect: null, Conceal: null },
values: { Detect: 0, Conceal: 0 },
});
mods.push({
name: 'Area of Effect (conceal)',
type: 'select',
default: 'none',
epic: false,
choices: { none: 'None', mbt: 'Medium Blast Template', lbt: 'Large Blast Template' },
effects: { none: null, mbt: null, lbt: null },
values: { none: 0, mbt: 1, lbt: 2 },
});
return mods;
}
get hasAoe() {
return true;
}
async parseValues() {
await super.parseValues();
this.data.detect = this.data.direction == 'Detect';
}
get powerPoints() {
return super.powerPoints + this.data.aoe;
}
get effectName() {
return `${this.data.detect ? 'Detect' : 'Conceal'} Arcana`;
}
get description() {
let desc = super.description;
if (this.data.detect) {
desc += `<p>The recipient can see and detect all supernatural persons,
objects, or effects in sight. This includes invisible foes, enchanted
objects, and so on.`;
if (this.data.raise) {
desc += `Since this was major Detect Arcana, the type of enchantments
is also known.`;
}
desc += `</p><p>If cast to learn more about a creature, the caster learns
active powers and arcane abilities.`;
if (this.data.raise) {
desc += `As major Detect in this mode, the caster also learns any
Weaknesses common to that creature type.`;
}
if (this.data.identify) {
desc += `<p>Items detected also give the recipient an idea of their
powers and how to activate them.</p>`;
}
if (this.data.alignment) {
desc += `<p>The recipient can also detect the presence and location
of supernatural good or evil within range, regardless of line of sight.</p>`;
}
desc += `</p><p><strong>Invisible Creatures:</strong> The recipient may
also ignore ${this.data.raise ? 'all' : 'up to 4 points of'} penalties
when attacking invisible or magically concealed foes.</p>`;
} else {
let area = 'one item or being';
if (this.data.aoe !== 0) {
area = `everything in a sphere the size of a
${this.data.aoe === 1 ? 'Medium' : 'Large'} Blast Template`;
}
desc += `<p>Conceal ${area} from the Detect Magic ability for
one hour. Attempts to <em>detect arcana</em> suffer a
${(this.data.strong ? -2 : 0) + this.data.raise ? -2 : -4} penalty.`;
}
return desc;
}
}

View File

@ -0,0 +1,56 @@
import { PowerEffect } from './basePowers.js';
export class DisguiseEffect extends PowerEffect {
get name() {
return 'Disguise';
}
get icon() {
return 'icons/equipment/head/mask-carved-wood-white.webp';
}
get duration() {
return 100;
}
get basePowerPoints() {
return 2;
}
get isTargeted() {
return true;
}
get hasAdditionalRecipients() {
return true;
}
get additionalRecipientCost() {
return 1;
}
get modifiers() {
return [
...super.modifiers,
{
name: 'Size',
type: 'checkbox',
value: 1,
id: 'size',
epic: false,
effect: false,
},
];
}
get description() {
const size = this.data.size ? 'within two sizes of' : 'of the same size as';
return (
super.description +
`
<p>Assume the appearance of another person ${size} the recipient. Anyone
who has cause to doubt the disguise may make a Notice roll at ${this.data.raise ? -4 : -2}
as a free action to see through the disguise.</p>`
);
}
}

133
src/module/powers/dispel.js Normal file
View File

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

View File

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

View File

@ -0,0 +1,83 @@
import { requestRollFromTokens } from '../helpers.js';
import { PowerEffect } from './basePowers.js';
export class DrainPowerPointsEffect extends PowerEffect {
get name() {
return 'Drain Power Points';
}
get icon() {
return 'icons/magic/control/debuff-energy-hold-teal-blue.webp';
}
get duration() {
return 0;
}
get basePowerPoints() {
return 2;
}
get usePrimaryEffect() {
return false;
}
get isTargeted() {
return true;
}
get isRaisable() {
return false;
}
get oneTarget() {
return true;
}
get modifiers() {
return [
...super.modifiers,
{
type: 'checkbox',
name: 'Power',
id: 'power',
epic: false,
effect: false,
value: 2,
},
{
type: 'number',
default: 4,
name: 'Opposed Target Number',
id: 'tn',
epic: false,
effect: false,
value: 0,
},
];
}
async sideEffects() {
await super.sideEffects();
const rollOpts = {
title: 'Resist Drain Power Points',
flavor: 'Resist the energy drain!',
mods: [{ label: 'Different Arcane Background', value: 2, ignore: true }],
targetNumber: this.data.tn,
};
await requestRollFromTokens(this.targets, 'ability', 'spirit', rollOpts);
}
get description() {
return (
super.description +
`
<p>An opposed roll of the caster's arcane activation roll vs the target's
Spirit. Caster's success means the victim loses
[[/r ${this.data.power ? '2' : '1'}d6]] power points. If the caster gets
a raise over the opponent, the stolen power points augment the caster's
own total.</p>
`
);
}
}

View File

@ -0,0 +1,97 @@
import { PowerEffect } from './basePowers.js';
export class ElementalManipulationEffect extends PowerEffect {
get name() {
return 'Elemental Manipulation';
}
get icon() {
return 'icons/magic/symbols/elements-air-earth-fire-water.webp';
}
get duration() {
return this.data?.weather ? 0 : 5;
}
get isTargeted() {
return this.data?.weather ? false : true;
}
get isRaisable() {
return true;
}
get usePrimaryEffect() {
return this.data?.weather ? false : true;
}
get oneTarget() {
return true;
}
get basePowerPoints() {
return 5;
}
get modifiers() {
return [
{
name: 'Power',
type: 'checkbox',
value: 3,
id: 'power',
epic: true,
effect: false,
},
{
name: 'Weather',
type: 'checkbox',
value: 5,
id: 'weather',
epic: true,
effect: false,
},
];
}
get primaryEffectButtons() {
const dmg = `${this.data.raise ? 3 : 2}d${this.data.power ? 6 : 4}`;
return [
...super.primaryEffectButtons,
{
type: 'damage',
label: `Damage (${dmg})`,
formula: `${dmg}x`,
},
];
}
get description() {
if (this.data.weather) {
return (
super.description +
`Bring or disperse rain, snow, sun, and wind in about a five mile radius.
This takes 10 minutes and lasts an hour.
`
);
}
let damage = `${this.data.raise ? 3 : 2}d${this.data.power ? 6 : 4}x`;
return (
super.description +
`
<p>Use the activation roll for:</p>
<ul>
<li><strong>Attack:</strong> activation roll is the attack roll,
[[/r ${damage}]] damage within Range.</li>
<li><strong>Move:</strong> move a cubic foot of air, earth, fire or water
(half that of stone) up to the caster's Smarts as a limited action.</li>
<li><strong>Push:</strong> Use the activation roll in place of Strength
for a Push.</li>
<li><strong>Special Effects:</strong> eg. purify a gallon of water,
or conjure a quart, fix breaks in stone, conjure a flame or spread
existing flame.</li>
</ul>
`
);
}
}

View File

@ -0,0 +1,105 @@
import { PowerEffect } from './basePowers.js';
export class EmpathyEffect extends PowerEffect {
get name() {
return 'Empathy';
}
get duration() {
return 5 * (this?.data?.duration ?? false ? 10 : 1);
}
get icon() {
return 'icons/skills/social/diplomacy-handshake-yellow.webp';
}
get hasAdditionalRecipients() {
return true;
}
get additionalRecipientCost() {
return 1;
}
get basePowerPoints() {
return 1;
}
get isTargeted() {
return true;
}
get modifiers() {
return [
...super.modifiers,
{
name: 'Charm',
type: 'checkbox',
id: 'charm',
value: 2,
epic: false,
effect: false,
},
{
name: 'Duration',
type: 'checkbox',
id: 'duration',
value: 1,
epic: false,
effect: false,
},
{
name: 'Truth',
type: 'checkbox',
id: 'truth',
value: 2,
epic: true,
effect: false,
},
];
}
get effectName() {
const extra = [];
if (this.data.charm) {
extra.push('Charm');
}
if (this.data.truth) {
extra.push('Truth');
}
const extraText = extra.length ? ` (${extra.join(', ')})` : '';
return this.name + extraText;
}
getMaintainEffectChanges() {
const mode = foundry.CONST.ACTIVE_EFFECT_MODES.ADD;
const value = this.data.raise ? 2 : 1;
return ['Intimidation', 'Persuasion', 'Performance', 'Taunt', 'Riding'].map(function (skill) {
return {
key: `@Skill{${skill}}[system.die.modifier]`,
priority: 0,
mode,
value,
};
});
}
get description() {
let text = super.description;
text += `
<p>Opposed by Spirit. If the target fails, caster gets
+${this.data.raise ? 2 : 1} to Intimidation, Persuasion, Performance,
Taunt or Riding (if target is an animal) rolls vs the target, except for
rolls to activate powers.</p>`;
if (this.data.charm) {
text += `<p>an Uncooperative target is made
${this.data.raise ? 'Friendly' : 'Cooperative'}.
The spell ends instantly if the caster's group attacks the victim's group.</p>`;
}
if (this.data.truth) {
text += '<p>The caster knows if the targets believe they are telling the truth.</p>';
}
return text;
}
}

View File

@ -0,0 +1,115 @@
import { moduleName } from '../globals.js';
import { PowerEffect } from './basePowers.js';
export class EntangleEffect extends PowerEffect {
get name() {
return 'Entangle';
}
get duration() {
return 0;
}
get icon() {
return 'icons/magic/nature/root-vine-entangled-humanoid.webp';
}
get isDamaging() {
return true;
}
get basePowerPoints() {
return 2;
}
get usePrimaryEffect() {
return false;
}
get isTargeted() {
return true;
}
get modifiers() {
return [
...super.modifiers,
{
type: 'select',
name: 'Damage',
id: 'damage',
epic: false,
default: 'none',
choices: {
none: 'None',
damage: 'Damage',
deadly: '⭐ Deadly',
},
effects: { none: null, damage: null, deadly: null },
values: { none: 0, damage: 2, deadly: 4 },
},
{
type: 'checkbox',
default: false,
name: 'Tough',
id: 'tough',
value: 1,
epic: false,
effect: false,
},
{
type: 'select',
default: 'none',
name: 'Area of Effect',
id: 'aoe',
epic: true,
choices: {
none: 'None',
mbt: 'Medium Blast Template',
lbt: 'Large Blast Template',
},
effects: { none: null, mbt: null, lbt: null },
values: { none: 0, mbt: 2, lbt: 3 },
},
];
}
async createSecondaryEffects(maintId) {
const docs = await super.createSecondaryEffects(maintId);
const docLabel = this.data.raise ? 'SWADE.Bound' : 'SWADE.Entangled';
const docName = this.data.raise ? 'Bound' : 'Entangled';
const doc = await PowerEffect.getStatus(docLabel, docName, false);
doc.flags = foundry.utils.mergeObject(doc.flags ?? {}, {
[moduleName]: {
powerEffect: true,
maintId,
},
});
if (this.data.damage !== 'none') {
const dmg = this.data.damage === 'deadly' ? '2d6' : '2d4';
doc.flags[moduleName].buttons = [
{
label: `Damage (${dmg})`,
type: 'damage',
formula: `${dmg}x`,
},
];
}
docs.push(doc);
return docs;
}
get description() {
let text = `
<p>Target(s) are restrained by something trapping-appropriate of Hardness
${this.data.tough ? 10 : 8}, and are ${this.data.raise ? 'Bound' : 'Entangled'}.
`;
if (this.data.damage !== 'none') {
text += `While restrained, victims take
${this.data.damage === 'deadly' ? '2d6' : '2d4'} damage at the end of
their turn.
`;
}
text += '</p>';
return super.description + text;
}
}

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