Compare commits

...
Sign in to create a new pull request.

152 commits

Author SHA1 Message Date
CK
2212e3c7d8
Merge pull request #239 from unrealkakeman89/Develop
Develop
2021-07-07 09:24:23 -04:00
CK
8c0ad582f7
Merge pull request #238 from unrealkakeman89/prettier
Prettier setup
2021-07-07 09:22:51 -04:00
TJ
584767b352 Formatted js files 2021-07-06 19:57:18 -05:00
TJ
d1b123100e Merge branch 'Develop' into prettier 2021-07-06 19:37:55 -05:00
Jacob Lucas
0a9c9f8ef0 Removed debug flag that spammed the console 2021-07-07 00:40:00 +01:00
CK
ee7418f552
Merge pull request #236 from unrealkakeman89/Develop
Develop
2021-07-06 06:10:24 -04:00
CK
c44ad926a5
Merge pull request #235 from unrealkakeman89/importer-fixes
Update calls to 0.8 api specifications
2021-07-05 10:44:40 -04:00
TJ
10ee20354b Update calls to 0.8 api specifications 2021-07-04 23:13:42 -05:00
CK
7214e3d260
Merge pull request #234 from unrealkakeman89/Develop
Various powercasting bugs
2021-07-02 20:46:59 -04:00
Jacob Lucas
97df236b54 Incremented version 2021-07-03 01:44:38 +01:00
Jacob Lucas
cac466462b Removed unnecessary calculation 2021-07-03 01:42:08 +01:00
Jacob Lucas
063d529f09 Fixed bug that prevented NPCs power DCs from having values 2021-07-03 01:41:26 +01:00
Jacob Lucas
b343a06ef6 Fixed bug that caused power points to not have the mods 2021-07-03 01:40:48 +01:00
CK
fc09308d11
Merge pull request #233 from unrealkakeman89/Develop
Develop
2021-07-01 21:54:52 -04:00
Jacob Lucas
7a18055c18 Bump max version to latest 2021-07-02 02:49:05 +01:00
Jacob Lucas
e271c41239 Fixed error that caused max force & tech points to evaluate to NaN 2021-07-02 02:48:55 +01:00
CK
5d879f99e2
Merge pull request #232 from unrealkakeman89/Develop
Develop
2021-07-01 16:31:37 -04:00
CK
df44ad0635
Merge pull request #231 from unrealkakeman89/professorbunbury-sw5e
"My Dudes" Update - 7/1/2021
2021-07-01 16:28:54 -04:00
Professor Bunbury
aff43d3e98 "My Dudes" Update - 7/1/2021
^ Updates Astrotech Engineering archetype in Archetypes compendium.
2021-07-01 16:05:37 -04:00
CK
211201caea Shoulder Cannon added 2021-06-30 17:10:13 -04:00
CK
7a29dfe600
Merge pull request #230 from unrealkakeman89/professorbunbury-sw5e
"My Dudes" Update - 6/24/2021
2021-06-29 15:35:12 -04:00
Professor Bunbury
c467738845 "My Dudes" Update - 6/24/2021
+ Adds new Force and Tech Powers and associated artwork to Force Powers and Tech Powers compendia.
2021-06-29 15:33:17 -04:00
CK
8a0940ccce Update Weapons Artwork pt2 2021-06-29 10:17:00 -04:00
CK
3b4300a8eb
Merge pull request #229 from unrealkakeman89/ammo-fix
Fix for blaster ammunition
2021-06-29 10:02:22 -04:00
CK
55bbb95cfb Updated Weapons Artwork 2021-06-29 10:02:02 -04:00
TJ
62e31afff2 Fix for blaster ammunition 2021-06-28 19:15:31 -05:00
TJ
da5223cab8 Prettier settings 2021-06-28 18:54:49 -05:00
CK
db286f7883
Merge pull request #228 from unrealkakeman89/split-powercasting
Split powercasting
2021-06-28 17:30:16 -04:00
supervj
25684173fa Update entity.js
looks good to me
2021-06-28 17:14:03 -04:00
supervj
74d841e9e1 split powercasting
Split where powercasting is done so that it can be changed by DAE.
2021-06-28 12:57:06 -04:00
Jacob Lucas
53064c0e09 Removed strange C from sidebar of power sheet when concentration is on 2021-06-24 19:08:37 +01:00
Jacob Lucas
9a21ce2b2a Fixed broken json for power cell 2021-06-24 15:57:23 +01:00
Jacob Lucas
29a639ff90 Removed unnecessary imports which also caused system to fail to load 2021-06-23 18:31:19 +01:00
CK
f18e537561
Merge pull request #227 from burndaflame/master
Convulsion is a 3rd level power
2021-06-23 13:08:14 -04:00
CK
bac8e3d642
Merge pull request #224 from unrealkakeman89/power-cell-fix
Always create a new power cell
2021-06-23 13:04:18 -04:00
CK
65594f62a3
Merge pull request #223 from unrealkakeman89/professorbunbury-sw5e
"My Dudes" Update - 6/17/2021
2021-06-23 13:03:45 -04:00
Jacob Lucas
e30d823225 Removed unnecessary additions to migration 2021-06-23 17:22:28 +01:00
burndaflame
17de2a89c2 Convulsion is a 3rd level power 2021-06-23 18:18:37 +02:00
TJ
95b2b1e39c Merge branch 'Develop' into 1.3.5-dev 2021-06-23 10:59:17 -05:00
supervj
fe520f2c0d Update class.html
forgot to hit save apparently, although this is all commented out anyway.
2021-06-23 03:01:59 -04:00
supervj
9a86bf7857 Finish core upgrade to 1.3.5
Filled in some missing pieces in html for core upgrades.  Looked mostly good on both Cyr and Jacob's accounts.

I had a few questions about differences that were added from DND5e, they are as follows:

less\original\npc.less
	line 34 - is the "li" before .creature-type necessary, not in dnd5e

module\item\entity.js
	line 685 - dnd is game.user._id, we have game.user.data._id

module\pixi\ability-template.js
	line 22- dnd is game.user._id, we have game.user.data._id

templates\chat\item-card.html
	line 1- dnd has actor._id, we have actor.data._id
2021-06-23 02:53:39 -04:00
TJ
88f5c0cbed Update missed config tags 2021-06-21 18:31:37 -05:00
TJ
c0e71fe0f3 Changes for 1.3.5 2021-06-21 18:27:34 -05:00
TJ
ffffe5da52 Always create a new power cell 2021-06-21 17:39:10 -05:00
Professor Bunbury
cd9bdf61d2 "My Dudes" Update - 6/17/2021
+ Adds Teräs Käsi Order and associated artwork to Archetypes compendium.
2021-06-18 19:31:42 -04:00
CK
e2f002292b
Merge pull request #222 from unrealkakeman89/professorbunbury-sw5e
Correction to Species Compendium
2021-06-18 09:11:40 -04:00
Professor Bunbury
6c2d89ee82 Correction to Species Compendium
^ Updates Anzellan to correct size.
2021-06-17 20:14:21 -04:00
CK
76ef89b518
Merge pull request #221 from unrealkakeman89/Develop-0.8
Updated to 0.8
2021-06-16 13:02:20 -04:00
Jacob Lucas
b414abbb81 Removed evidence of starships from the compendium 2021-06-16 17:58:35 +01:00
CK
53a845feb7
Add Italian 2021-06-16 12:49:54 -04:00
CK
7134c4ac07
add Italian 2021-06-16 12:42:59 -04:00
Jacob Lucas
585de42a46 Removed evidence of Starships 2021-06-16 17:24:34 +01:00
CK
2007d116a2
Merge pull request #219 from unrealkakeman89/professorbunbury-sw5e
"My Dudes" Update - 6/10/2021
2021-06-16 11:57:51 -04:00
Jacob Lucas
104e49615d Updated to 1.3.3, started removing evidence of statships 2021-06-13 04:25:56 +01:00
Professor Bunbury
6041564835 "My Dudes" Update - 6/10/2021
+ Adds Nikto and associated artwork to Species compendium.
^ Updates Half-Human inherited traits in Species compendium.
2021-06-12 12:15:40 -04:00
CK
64bae2140c
Merge pull request #218 from burndaflame/patch-1
now ignoring ´.DS_Store´ file of mac-os
2021-06-09 13:02:47 -04:00
burndaflame
c3cbc96499
now ignoring ´.DS_Store´ file of mac-os 2021-06-09 19:01:07 +02:00
Jacob Lucas
37a3e83f3a Potentially updated Migration 2021-06-09 02:29:03 +01:00
Jacob Lucas
db5e90281c Updated version for compatibility with modules 2021-06-04 23:21:52 +01:00
Jacob Lucas
92bf020cdf General Cleanup
CSS
 - Removed unnecessary units on 0 values
 - Replaced invalid css values (such as line-height default) with valid equivalents
Templates
 - Added missing closing tags
 - Fixed incorrect closing tags
 - Removed unnecessary closing tags
2021-06-04 22:20:48 +01:00
Jacob Lucas
d0e0dda2b3 Patched issue that meant that all powers were not being prepared 2021-06-04 22:20:48 +01:00
Jacob Lucas
b0c928c691 Patched issue that prevented the old actor sheet from opening 2021-06-04 22:20:48 +01:00
Jacob Lucas
c454c035a3 Fixed new character sheet's send language to chat function
- Cleaned up unused variables
 - Updated _id get to 0.8 standard
 - Patched fetching languages failing
 - Patched blind and gm chat option not doing anything while simultaneously calling a non existent function and dying
 - Patched self chat option still sending to everyone
2021-06-04 22:20:48 +01:00
Jacob Lucas
f839166082 Added changes from DND5e 1.3.3 2021-06-04 22:20:48 +01:00
Jacob Lucas
3cfee9dd81 Added changes to packs 2021-06-04 22:20:48 +01:00
Jacob Lucas
6295de9fd6 Cleaned up actor 2021-06-04 22:20:48 +01:00
Jacob Lucas
2a7e1c419e Updated to DND5e 1.3.2
Things unfinished:
 - Migration
 - The update adds new sections to the class sheet to allow some light customisation, this hasn't been included, but could be extended for the sake of dynamic classes with automatic class features and more
 - The French
 - The packs have not yet been updated, meaning due to the addition of a progression field to the class item, classes now don't set force or tech points
 - I updated the function calls in starships, but I didn't update it very thoroughly, it'll need checking
 - I only did a little testing
 - There has since been updates to DND5e that hasn't made it to release that patch bugs, those should be implemented
Things changed from base 5e:
 - Short rests and long rests were merged into one function, this needed some rewrites to account for force and tech points, and for printing the correct message
Extra Comments:
 - Unfinished code exists for automatic spell scrolls, this could be extended for single use force or tech powers
 - Weapon proficiencies probably need revising
 - Elven accuracy, halfling lucky, and reliable talent are present in the roll logic, this probably needs revising for sw5e
 - SW5e has a variant rule that permits force powers of any alignment to use either charisma or wisdom, that could be implemented
 - SW5e's version of gritty realism, [Longer Rests](https://sw5e.com/rules/variantRules/Longer%20Rests) differs from base dnd, this could be implemented
 - Extra ideas I've had while looking through the code can be found in Todos next to the ideas relevant context
2021-06-04 22:20:48 +01:00
CK
aa07380c57
Merge pull request #214 from unrealkakeman89/professorbunbury-sw5e
"My Dudes" Update, 6/3/2021
2021-06-04 17:18:27 -04:00
CK
4f3f22f3bc
Merge pull request #213 from ExileofBrokenSky/Develop
Operative: Bolstering Practice class features added
2021-06-04 17:18:18 -04:00
Professor Bunbury
1b8b8204e5 "My Dudes" Update, 6/3/2021
+ Adds Clone background and associated artwork to the Backgrounds compendium.
2021-06-04 15:00:43 -04:00
Michael Burgess
8c74aa67a1 Operative: Bolstering Practice class features added
Also updated the GUERRILLA’S EXPLOIT to current
2021-05-30 12:14:12 -04:00
Kakeman89
c7c9bc3b5d Merge branch 'professorbunbury-sw5e' into Develop 2021-05-28 10:18:30 -04:00
Professor Bunbury
709ad758dc Multi/Class Proficiencies
+ Adds base Class and Multiclass proficiencies to the Class Features compendium.
2021-05-28 09:17:00 -04:00
CK
3d0f869356
Merge pull request #211 from unrealkakeman89/button-duplication-fix
Fixes button duplication
2021-05-28 08:05:33 -04:00
CK
8c93b090b4
Merge pull request #210 from unrealkakeman89/professorbunbury-sw5e
"My Dudes" Update - 5/27/2021
2021-05-28 08:05:22 -04:00
TJ
60fca48e8c Fixes button duplication 2021-05-27 22:59:31 -05:00
Professor Bunbury
3ccf80d442 "My Dudes" Update - 5/27/2021
+ Adds Bolstering Practice and associated artwork to the Archetypes compendium.
2021-05-27 13:46:19 -04:00
CK
0f53fdde5f
Merge pull request #209 from jtljac/Develop
Modified DAE of Alert feat to use the Alert feat flag, rather than adding 5 to the AC manually, and DAE of the Droid Class II species to set the Armour integration flag
2021-05-26 13:06:17 -04:00
Jacob
f0c4f9c5d5 Edited Droid class II species DAE to set armour integration flag 2021-05-26 16:55:31 +01:00
Jacob
c0cfcda102 Edited Alert Feat DAE to set the alert feat flag
Instead of manually adding 5 to the AC
2021-05-26 16:54:35 +01:00
CK
078ad2584a
Combat Enhancements fix
By Jacob
2021-05-25 14:41:21 -04:00
CK
7200a9e2f0
Combat Enhancements fix
By Jacob
2021-05-25 14:40:26 -04:00
CK
7d589c7e2f Add Weapons artwork
Added missing artwork and updated some current artwork courtesy of WhtWlf
2021-05-20 09:57:32 -04:00
CK
cf57bdbc9e
Merge pull request #204 from unrealkakeman89/professorbunbury-sw5e
"My Dudes" Update - 5/13/2021
2021-05-19 13:27:24 -04:00
Professor Bunbury
ab420f5400 "My Dudes" Update - 5/13/2021
+ Adds Advozse and associated artwork to the Species compendium.
2021-05-19 12:07:59 -04:00
CK
84ab6cf478
Merge pull request #203 from unrealkakeman89/dependabot/npm_and_yarn/hosted-git-info-2.8.9
Bump hosted-git-info from 2.8.8 to 2.8.9
2021-05-12 08:52:14 -04:00
dependabot[bot]
d39fa6acf2
Bump hosted-git-info from 2.8.8 to 2.8.9
Bumps [hosted-git-info](https://github.com/npm/hosted-git-info) from 2.8.8 to 2.8.9.
- [Release notes](https://github.com/npm/hosted-git-info/releases)
- [Changelog](https://github.com/npm/hosted-git-info/blob/v2.8.9/CHANGELOG.md)
- [Commits](https://github.com/npm/hosted-git-info/compare/v2.8.8...v2.8.9)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-11 22:42:24 +00:00
CK
0a4b6de0fa
Merge pull request #202 from unrealkakeman89/hotfix-ammo-usage
Fix for blasters not consuming ammo
2021-05-09 19:58:43 -04:00
CK
b7b4fa0c94
Merge pull request #200 from unrealkakeman89/fix-for-favorite-text
Fix for favorite text
2021-05-09 19:58:27 -04:00
TJ
c33982f97c Fix for blasters not consuming ammo 2021-05-09 18:53:05 -05:00
TJ
59c733735c Fix for favorite text 2021-05-07 16:57:58 -05:00
CK
1e251a27b1
Merge pull request #199 from unrealkakeman89/professorbunbury-sw5e
"My Dudes" Update 5/6/2021
2021-05-06 15:53:36 -04:00
Professor Bunbury
a78aa37f7c "My Dudes" Update 5/6/2021
+ Adds Mikkian and associated artwork to Species compendium.
2021-05-06 14:10:27 -04:00
CK
bf2f09381e
Merge pull request #197 from unrealkakeman89/Develop-VJ
Revert back to hp instead of hull/shld for compatibility
2021-05-05 11:40:33 -04:00
supervj
0607152f51 Revert back to hp instead of hull/shld for compatibility
Left the dice separated out since hit die are controlled by character level.
2021-05-05 01:37:44 -04:00
CK
27c9dd4f3e
Merge pull request #196 from unrealkakeman89/Develop-VJ
Add More Starship structure for future use
2021-05-03 15:59:53 -04:00
supervj
d0eae64241 Created Starship size as "class"
This will be used to automatically upgrade Tier and Dice.  Need to build in logic still
2021-05-02 00:53:46 -04:00
supervj
6ecf1e7b96 Split Hull and Shield
Made new properties instead of using the Character values since there is already logic built into them that is fighting what we want to do.
Also added Power dice structure
2021-05-01 23:43:24 -04:00
CK
4bbd3e1cbb
Merge pull request #195 from ExileofBrokenSky/Develop
Added archetype class features
2021-04-30 11:42:08 -04:00
Michael Burgess
ce29cf57be Added archetype class features for Triage Technique and Path of Meditation 2021-04-30 11:39:50 -04:00
supervj
692538f0c2 Test for the Prof
Here are my changes, not sure if it will help.
2021-04-29 16:56:30 -04:00
CK
97afabb3e0
Merge pull request #194 from unrealkakeman89/professorbunbury-sw5e
"My Dudes" Updates 4/22-4/29/2021
2021-04-29 15:50:32 -04:00
Professor Bunbury
1df6ccb1c9 "My Dudes" Updates 4/22-4/29/2021
+ Adds Triage Technique (Scout) to Archetypes compendium.
+ Adds Path of Meditation (Sentinel) to Archetypes compendium.
+ Adds new archetypes artwork.
^ Updates Archetypes links on Scout and Sentinel classes.
2021-04-29 15:49:44 -04:00
supervj
a04a66ab6d
Merge pull request #193 from unrealkakeman89/Develop
Update Develop-VJ to Develop
2021-04-23 20:03:40 -04:00
supervj
47cfad4624 Update template.json
Add crawl roll turn movement values.
Removed some duplicate values.
Ran Prettier so I could read it easier.
2021-04-21 22:49:13 -04:00
CK
d60e1fbcfa
Merge pull request #192 from unrealkakeman89/professorbunbury-sw5e
SotG Update #1.2
2021-04-20 23:42:01 -04:00
CK
90a45dab55
Merge branch 'Develop' into professorbunbury-sw5e 2021-04-20 23:41:51 -04:00
Professor Bunbury
14f4e06788 SotG Update #1.2
+ Adds Starship Features compendium
^ Adds major updates to Starship sheet and related item compendia
^ Adds Mechanist Technique to Archetypes compendium
2021-04-17 13:26:13 -04:00
supervj
9de6a8f5c0 Keep fields from being wiped out
updated the template.json to have the fields that were being wiped out.  If they don't exist they will be removed when moving to/from a compendium and when duplicating the item.
2021-04-16 02:05:53 -04:00
CK
20f47110cd
Merge pull request #190 from unrealkakeman89/character-importer-updates
Character importer updates
2021-04-13 23:14:23 -04:00
supervj
4d562d07d0
Merge pull request #191 from unrealkakeman89/Develop
Update Develop-VJ branch to match Develop
2021-04-13 23:08:28 -04:00
supervj
fa7b03109f Add Skills to Starships
Added skills to the starship sheets and made them rollable.

Sorry about the direct commit to Develop, but I forgot to branch it off...
2021-04-13 23:01:03 -04:00
TJ
eaac412cb3 Merge clean up 2021-04-13 21:39:53 -05:00
TJ
3eec52f647 Merge branch 'Develop' into character-importer-updates 2021-04-13 21:35:34 -05:00
CK
a28cbc5a8a
Merge pull request #189 from ellimist25/master
Skills & Multiclass imports
2021-04-12 12:30:07 -04:00
Mike Magarino
c7ef91e9ed Merge branch 'Develop' 2021-04-12 11:49:17 -04:00
CK
a5b0faae77
Merge pull request #188 from unrealkakeman89/professorbunbury-sw5e
"My Dudes" Update: 4/8/2021
2021-04-09 14:02:00 -04:00
Professor Bunbury
e711ff0f7c "My Dudes" Update: 4/8/2021
+ Adds Anomid and associated artwork to Species compendium.
+ Adds Vintian and associated artwork to Species compendium.
2021-04-08 12:48:33 -04:00
CK
fb0b489c82
Merge pull request #187 from unrealkakeman89/professorbunbury-sw5e
SotG Update #1.1 (Quick Fixes)
2021-04-07 17:58:38 -04:00
Professor Bunbury
a7089373dd Merge branch 'professorbunbury-sw5e' of https://github.com/unrealkakeman89/sw5e into professorbunbury-sw5e 2021-04-07 14:11:00 -04:00
Professor Bunbury
f393670528 SotG Update #1.1 (Quick Fixes)
^ Updates entity.js and starship.js to fix drag-and-drop and NaN errors and prepare for Starship Features.
^ Updates en.json, config.js, sw5e.js, and template.json to prepare for Starship Features.
2021-04-07 14:10:56 -04:00
CK
3f8a314b94
Merge pull request #186 from unrealkakeman89/professorbunbury-sw5e
SotG Update #1
2021-04-06 16:08:05 -04:00
professorbunbury
1ad1e6976f Regenerate css 2021-04-06 20:07:18 +00:00
Professor Bunbury
c793949b37 SotG Update #1
+ Adds compendia for Deployments, Deployment Features, Starship Armor, Starship Equipment, Starship Weapons, and Ventures and associated artwork
+ Adds Starship actor sheet (very, very rough draft, somewhat unpredictable, not fully functional)
+ Adds function to Character sheet to collapse/expand Feature tab rows (major assist from Cyr)
+ Adds function to Character sheet to increment/decrement class levels directly from character sheet (another major assist from Cyr)
2021-04-06 16:03:48 -04:00
Kakeman89
01d7de2e46 Merge branch 'Develop' of https://github.com/ExileofBrokenSky/sw5e into Develop 2021-04-01 12:04:21 -04:00
CK
4f0c6addcf
Merge pull request #185 from unrealkakeman89/dependabot/npm_and_yarn/y18n-3.2.2
Bump y18n from 3.2.1 to 3.2.2
2021-04-01 11:42:36 -04:00
dependabot[bot]
7c03cd4b04
Bump y18n from 3.2.1 to 3.2.2
Bumps [y18n](https://github.com/yargs/y18n) from 3.2.1 to 3.2.2.
- [Release notes](https://github.com/yargs/y18n/releases)
- [Changelog](https://github.com/yargs/y18n/blob/master/CHANGELOG.md)
- [Commits](https://github.com/yargs/y18n/commits)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-31 17:09:05 +00:00
Michael Burgess
0558cdec49 Update classfeatures.db
Added Fighter - Exhibitionist class features,
updated Scout class features with icons, added scout routines
updated Sentinel class features with icons, added force-empowered options, added ideals.
2021-03-29 15:05:49 -04:00
TJ
e25140b529 Add skill proficiencies 2021-03-27 23:17:31 -05:00
TJ
e942a9b803 Added item quantity handling 2021-03-27 23:00:57 -05:00
TJ
4b1b3bbeed Fix for species data 2021-03-27 22:14:57 -05:00
TJ
9d6fabe8c2 Add adventuring gear 2021-03-27 21:48:58 -05:00
TJ
bd94d75086 Add weapons and armors 2021-03-27 16:14:50 -05:00
TJ
16d01207a7 Added tech powers 2021-03-27 15:38:37 -05:00
TJ
5477f9371d Add force powers 2021-03-27 15:23:27 -05:00
TJ
b057026328 Remove built in species bonuses 2021-03-27 14:45:05 -05:00
CK
28d9290959
Merge pull request #182 from unrealkakeman89/professorbunbury-sw5e
"My Dudes" Update: 3/25/2021
2021-03-26 17:41:41 -04:00
Professor Bunbury
3297d9bd8c Merge branch 'professorbunbury-sw5e' of https://github.com/unrealkakeman89/sw5e into professorbunbury-sw5e 2021-03-25 13:50:31 -04:00
Professor Bunbury
08d62d1e85 "My Dudes" Update: 3/25/2021
^ Archetypes compendium to include Exhibition Specialist and associated artwork.
^ Classes compendium to include link to Exhibition Specialist archetype.
2021-03-25 13:50:27 -04:00
TJ
9bbc9af285 Add species 2021-03-24 19:55:33 -05:00
supervj
56e6640f38
Merge pull request #174 from unrealkakeman89/Develop
update develop-VJ with Develop branch
2021-03-17 06:16:09 -04:00
CK
fee77e2172
Merge pull request #172 from unrealkakeman89/Develop
Develop
2021-03-16 10:15:16 -04:00
CK
87d615babc
Merge pull request #155 from ExileofBrokenSky/ExileOfBrokenSky
updated operative
2021-03-11 15:00:27 -05:00
ellimist25
9dfa851016 Merge remote-tracking branch 'upstream/Develop' 2021-03-05 21:49:19 -05:00
Mags
5138feed4e
Merge pull request #1 from ellimist25/character-sheet-importer-v2
Character sheet importer v2
2021-03-05 21:35:34 -05:00
Michael Burgess
04dcaf332d Added Guardian archetype: Aqinos Form
updated icons for scholar and made individual entries for discoveries and maneuvers
added (Maneuver) to fighter and scholar maneuver names, for ease of searching all maneuvers in the directory
2021-03-05 13:09:52 -05:00
Michael Burgess
4ee235566d updated operative
expanded options into class features for each
exploit including skills exploits
visited your mom
individualized the previously shared operative and monk ability score increases
added midi-qol superSaver effect to Evasion (Operative)
2021-02-25 10:40:41 -05:00
Mike Magarino
abc65220ec skills and multiclass working 2021-01-26 10:11:52 -05:00
Mike Magarino
826f042dbb skills and multiclass working 2021-01-26 10:09:28 -05:00
Mike Magarino
6f90f19ad1 Update characterImporter.js 2021-01-26 02:00:30 -05:00
Mike Magarino
52fd477d39 skills v2 2021-01-26 01:24:16 -05:00
Mike Magarino
f4af3aad45 skills v1 2021-01-26 00:33:21 -05:00
235 changed files with 20889 additions and 12172 deletions

3
.gitignore vendored
View file

@ -22,6 +22,9 @@
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid* hs_err_pid*
# Mac-OS file
.DS_Store
# IDE Folders # IDE Folders
.idea/ .idea/
.vs/ .vs/

14
.prettierrc Normal file
View file

@ -0,0 +1,14 @@
{
"printWidth": 120,
"tabWidth": 4,
"useTabs": false,
"semi": true,
"singleQuote": false,
"quoteProps": "consistent",
"jsxSingleQuote": false,
"trailingComma": "none",
"bracketSpacing": false,
"jsxBracketSameLine": false,
"arrowParens": "always",
"endOfLine": "lf"
}

View file

@ -1,2 +1,3 @@
{ {
"editor.formatOnSave": true
} }

View file

@ -8,19 +8,19 @@ const less = require("gulp-less");
const SW5E_LESS = ["less/**/*.less"]; const SW5E_LESS = ["less/**/*.less"];
function compileLESS() { function compileLESS() {
return gulp.src("less/original/sw5e.less").pipe(less()).pipe(gulp.dest("./")); return gulp.src("less/original/sw5e.less").pipe(less()).pipe(gulp.dest("./"));
} }
function compileGlobalLess() { function compileGlobalLess() {
return gulp.src("less/update/sw5e-global.less").pipe(less()).pipe(gulp.dest("./")); return gulp.src("less/update/sw5e-global.less").pipe(less()).pipe(gulp.dest("./"));
} }
function compileLightLess() { function compileLightLess() {
return gulp.src("less/update/sw5e-light.less").pipe(less()).pipe(gulp.dest("./")); return gulp.src("less/update/sw5e-light.less").pipe(less()).pipe(gulp.dest("./"));
} }
function compileDarkLess() { function compileDarkLess() {
return gulp.src("less/update/sw5e-dark.less").pipe(less()).pipe(gulp.dest("./")); return gulp.src("less/update/sw5e-dark.less").pipe(less()).pipe(gulp.dest("./"));
} }
const css = gulp.series(compileLESS, compileGlobalLess, compileLightLess, compileDarkLess); const css = gulp.series(compileLESS, compileGlobalLess, compileLightLess, compileDarkLess);
@ -30,7 +30,7 @@ const css = gulp.series(compileLESS, compileGlobalLess, compileLightLess, compil
/* ----------------------------------------- */ /* ----------------------------------------- */
function watchUpdates() { function watchUpdates() {
gulp.watch(SW5E_LESS, css); gulp.watch(SW5E_LESS, css);
} }
/* ----------------------------------------- */ /* ----------------------------------------- */

View file

@ -1,6 +1,7 @@
{ {
"ACTOR.TypeCharacter": "Player Character", "ACTOR.TypeCharacter": "Player Character",
"ACTOR.TypeNpc": "Non-Player Character", "ACTOR.TypeNpc": "Non-Player Character",
"ACTOR.TypeStarship": "Starship",
"ACTOR.TypeVehicle": "Vehicle", "ACTOR.TypeVehicle": "Vehicle",
"ITEM.TypeArchetype": "Archetype", "ITEM.TypeArchetype": "Archetype",
"ITEM.TypeBackground": "Background", "ITEM.TypeBackground": "Background",
@ -8,15 +9,22 @@
"ITEM.TypeClass": "Class", "ITEM.TypeClass": "Class",
"ITEM.TypeClassfeature": "Class Feature", "ITEM.TypeClassfeature": "Class Feature",
"ITEM.TypeConsumable": "Consumable", "ITEM.TypeConsumable": "Consumable",
"ITEM.TypeDeployment": "Deployment",
"ITEM.TypeDeploymentfeature": "Deployment Feature",
"ITEM.TypeEquipment": "Equipment", "ITEM.TypeEquipment": "Equipment",
"ITEM.TypeFeat": "Feat", "ITEM.TypeFeat": "Feat",
"ITEM.TypeFightingmastery": "FightingMastery", "ITEM.TypeFightingmastery": "Fighting Mastery",
"ITEM.TypeFightingstyle": "Fighting Style", "ITEM.TypeFightingstyle": "Fighting Style",
"ITEM.TypeLightsaberform": "Lightsaber Form", "ITEM.TypeLightsaberform": "Lightsaber Form",
"ITEM.TypeLoot": "Loot", "ITEM.TypeLoot": "Loot",
"ITEM.TypePower": "Power", "ITEM.TypePower": "Power",
"ITEM.TypeSpecies": "Species", "ITEM.TypeSpecies": "Species",
"ITEM.TypeStarshipfeature": "Starship Feature",
"ITEM.TypeStarshipfeaturePl": "Starship Features",
"ITEM.TypeStarshipmod": "Starship Modification",
"ITEM.TypeStarshipmodPl": "Starship Modifications",
"ITEM.TypeTool": "Tool", "ITEM.TypeTool": "Tool",
"ITEM.TypeVenture": "Venture",
"ITEM.TypeWeapon": "Weapon", "ITEM.TypeWeapon": "Weapon",
"SETTINGS.5eAllowPolymorphingL": "Allow players to polymorph their own actors.", "SETTINGS.5eAllowPolymorphingL": "Allow players to polymorph their own actors.",
"SETTINGS.5eAllowPolymorphingN": "Allow Polymorphing", "SETTINGS.5eAllowPolymorphingN": "Allow Polymorphing",
@ -35,11 +43,13 @@
"SETTINGS.5eInitTBN": "Initiative Dexterity Tiebreaker", "SETTINGS.5eInitTBN": "Initiative Dexterity Tiebreaker",
"SETTINGS.5eNoExpL": "Remove experience bars from character sheets.", "SETTINGS.5eNoExpL": "Remove experience bars from character sheets.",
"SETTINGS.5eNoExpN": "Disable Experience Tracking", "SETTINGS.5eNoExpN": "Disable Experience Tracking",
"SETTINGS.5eReset": "Reset",
"SETTINGS.5eRestEpic": "Epic Heroism (LR: 1 hour, SR: 1 min)", "SETTINGS.5eRestEpic": "Epic Heroism (LR: 1 hour, SR: 1 min)",
"SETTINGS.5eRestGritty": "Gritty Realism (LR: 7 days, SR: 8 hours)", "SETTINGS.5eRestGritty": "Gritty Realism (LR: 7 days, SR: 8 hours)",
"SETTINGS.5eRestL": "Configure which rest variant should be used for games within this system.", "SETTINGS.5eRestL": "Configure which rest variant should be used for games within this system.",
"SETTINGS.5eRestN": "Rest Variant", "SETTINGS.5eRestN": "Rest Variant",
"SETTINGS.5eRestPHB": "Player's Handbook (LR: 8 hours, SR: 1 hour)", "SETTINGS.5eRestPHB": "Player's Handbook (LR: 8 hours, SR: 1 hour)",
"SETTINGS.5eUndoChanges": "Undo Changes",
"SETTINGS.SWColorDark": "Dark Theme", "SETTINGS.SWColorDark": "Dark Theme",
"SETTINGS.SWColorL": "Set the color theme of the game", "SETTINGS.SWColorL": "Set the color theme of the game",
"SETTINGS.SWColorLight": "Light Theme", "SETTINGS.SWColorLight": "Light Theme",
@ -70,6 +80,7 @@
"SW5E.AbilityUseCast": "Cast Power", "SW5E.AbilityUseCast": "Cast Power",
"SW5E.AbilityUseChargedHint": "This {type} is charged and ready to use!", "SW5E.AbilityUseChargedHint": "This {type} is charged and ready to use!",
"SW5E.AbilityUseChargesLabel": "{value} Charges", "SW5E.AbilityUseChargesLabel": "{value} Charges",
"SW5E.AbilityUseConfig": "Usage Configuration",
"SW5E.AbilityUseConsumableChargeHint": "Using this {type} will consume 1 charge of {value} remaining.", "SW5E.AbilityUseConsumableChargeHint": "Using this {type} will consume 1 charge of {value} remaining.",
"SW5E.AbilityUseConsumableDestroyHint": "Using this {type} will consume its final charge and it will be destroyed.", "SW5E.AbilityUseConsumableDestroyHint": "Using this {type} will consume its final charge and it will be destroyed.",
"SW5E.AbilityUseConsumableLabel": "{max} per {per}", "SW5E.AbilityUseConsumableLabel": "{max} per {per}",
@ -96,7 +107,9 @@
"SW5E.ActionUtil": "Utility", "SW5E.ActionUtil": "Utility",
"SW5E.ActionWarningNoItem": "The requested item {item} no longer exists on Actor {name}", "SW5E.ActionWarningNoItem": "The requested item {item} no longer exists on Actor {name}",
"SW5E.ActionWarningNoToken": "You must have one or more controlled Tokens in order to use this option.", "SW5E.ActionWarningNoToken": "You must have one or more controlled Tokens in order to use this option.",
"SW5E.ActorWarningInvalidItem": "{itemType} items cannot be added to a {actorType}.",
"SW5E.Add": "Add", "SW5E.Add": "Add",
"SW5E.AddEmbeddedItemPromptHint": "Do you want to add these items to your character sheet?",
"SW5E.AdditionalNotes": "Additional Notes", "SW5E.AdditionalNotes": "Additional Notes",
"SW5E.Advantage": "Advantage", "SW5E.Advantage": "Advantage",
"SW5E.Alignment": "Alignment", "SW5E.Alignment": "Alignment",
@ -110,6 +123,7 @@
"SW5E.AlignmentND": "Neutral Dark", "SW5E.AlignmentND": "Neutral Dark",
"SW5E.AlignmentNL": "Neutral Light", "SW5E.AlignmentNL": "Neutral Light",
"SW5E.Appearance": "Appearance", "SW5E.Appearance": "Appearance",
"SW5E.Apply": "Apply",
"SW5E.ArchetypeName": "Archetype Name", "SW5E.ArchetypeName": "Archetype Name",
"SW5E.Archetypes": "Archetypes", "SW5E.Archetypes": "Archetypes",
"SW5E.ArmorClass": "Armor Class", "SW5E.ArmorClass": "Armor Class",
@ -179,6 +193,9 @@
"SW5E.BonusSaveForm": "Update Bonuses", "SW5E.BonusSaveForm": "Update Bonuses",
"SW5E.BonusTechPowerDC": "Global Tech Power DC Bonus", "SW5E.BonusTechPowerDC": "Global Tech Power DC Bonus",
"SW5E.BonusTitle": "Configure Actor Bonuses", "SW5E.BonusTitle": "Configure Actor Bonuses",
"SW5E.BurnFuel": "Burn",
"SW5E.CapacityMultiplier": "Capacity Multiplier",
"SW5E.CentStorageCapacity": "Central Storage Capacity",
"SW5E.ChallengeRating": "Challenge Rating", "SW5E.ChallengeRating": "Challenge Rating",
"SW5E.Charged": "Charged", "SW5E.Charged": "Charged",
"SW5E.Charges": "Charges", "SW5E.Charges": "Charges",
@ -188,9 +205,14 @@
"SW5E.ChatContextHealing": "Apply Healing", "SW5E.ChatContextHealing": "Apply Healing",
"SW5E.ChatFlavor": "Chat Message Flavor", "SW5E.ChatFlavor": "Chat Message Flavor",
"SW5E.ClassLevels": "Class Levels", "SW5E.ClassLevels": "Class Levels",
"SW5E.ClassMakeOriginal": "Original Class",
"SW5E.ClassMakeOriginalHint": "First class taken by character used to determine certain class traits when multiclassing.",
"SW5E.ClassName": "Class Name", "SW5E.ClassName": "Class Name",
"SW5E.ClassOriginal": "Original Class",
"SW5E.ClassSaves": "Saving Throws",
"SW5E.ClassSkillsChosen": "Chosen Class Skills", "SW5E.ClassSkillsChosen": "Chosen Class Skills",
"SW5E.ClassSkillsNumber": "Number of Starting Skills", "SW5E.ClassSkillsNumber": "Number of Starting Skills",
"SW5E.Collapse": "Collapse/Expand",
"SW5E.ComponentMaterial": "Material", "SW5E.ComponentMaterial": "Material",
"SW5E.ComponentSomatic": "Somatic", "SW5E.ComponentSomatic": "Somatic",
"SW5E.ComponentVerbal": "Verbal", "SW5E.ComponentVerbal": "Verbal",
@ -249,6 +271,32 @@
"SW5E.CoverHalf": "Half", "SW5E.CoverHalf": "Half",
"SW5E.CoverThreeQuarters": "Three Quarters", "SW5E.CoverThreeQuarters": "Three Quarters",
"SW5E.CoverTotal": "Total", "SW5E.CoverTotal": "Total",
"SW5E.CreatureAberration": "Aberration",
"SW5E.CreatureAberrationPl": "Aberrations",
"SW5E.CreatureBeast": "Beast",
"SW5E.CreatureBeastPl": "Beasts",
"SW5E.CreatureConstruct": "Construct",
"SW5E.CreatureConstructPl": "Constructs",
"SW5E.CreatureDroid": "Droid",
"SW5E.CreatureDroidPl": "Droids",
"SW5E.CreatureForceEntity": "Force Entity",
"SW5E.CreatureForceEntityPl": "Force Entities",
"SW5E.CreatureHumanoid": "Humanoid",
"SW5E.CreatureHumanoidPl": "Humanoids",
"SW5E.CreaturePlant": "Plant",
"SW5E.CreaturePlantPl": "Plants",
"SW5E.CreatureSwarm": "Swarm",
"SW5E.CreatureSwarmPhrase": "Swarm of {size} {type}",
"SW5E.CreatureSwarmSize": "Swarm Size",
"SW5E.CreatureType": "Creature Type",
"SW5E.CreatureTypeConfig": "Configure Creature Type",
"SW5E.CreatureTypeSelectorCustom": "Custom Type",
"SW5E.CreatureTypeSelectorSubtype": "Subtype",
"SW5E.CreatureTypeTitle": "Configure Creature Type",
"SW5E.CreatureUndead": "Undead",
"SW5E.CreatureUndeadPl": "Undead",
"SW5E.CrewCap": "Crew Capacity",
"SW5E.Crewed": "Crewed",
"SW5E.Critical": "Critical", "SW5E.Critical": "Critical",
"SW5E.CriticalHit": "Critical Hit", "SW5E.CriticalHit": "Critical Hit",
"SW5E.Currency": "Currency", "SW5E.Currency": "Currency",
@ -282,8 +330,11 @@
"SW5E.DeathSavingThrow": "Death Saving Throw", "SW5E.DeathSavingThrow": "Death Saving Throw",
"SW5E.Default": "Default", "SW5E.Default": "Default",
"SW5E.DefaultAbilityCheck": "Default Ability Check", "SW5E.DefaultAbilityCheck": "Default Ability Check",
"SW5E.Deployment": "Deployment",
"SW5E.DeploymentPl": "Deployments",
"SW5E.description": "A comprehensive game system for running games of Star Wars 5th Edition in the Foundry VTT environment.", "SW5E.description": "A comprehensive game system for running games of Star Wars 5th Edition in the Foundry VTT environment.",
"SW5E.Description": "Description", "SW5E.Description": "Description",
"SW5E.DestructionSave": "Destruction Saves",
"SW5E.Details": "Details", "SW5E.Details": "Details",
"SW5E.Dimensions": "Dimensions", "SW5E.Dimensions": "Dimensions",
"SW5E.Disadvantage": "Disadvantage", "SW5E.Disadvantage": "Disadvantage",
@ -292,38 +343,55 @@
"SW5E.DistMi": "Miles", "SW5E.DistMi": "Miles",
"SW5E.DistSelf": "Self", "SW5E.DistSelf": "Self",
"SW5E.DistTouch": "Touch", "SW5E.DistTouch": "Touch",
"SW5E.DmgRed": "Damage Reduction",
"SW5E.Duration": "Duration", "SW5E.Duration": "Duration",
"SW5E.EffectsCategoryTemporary": "Temporary Effects",
"SW5E.EffectsCategoryPassive": "Passive Effects",
"SW5E.EffectsCategoryInactive": "Inactive Effects",
"SW5E.EffectCreate": "Create Effect", "SW5E.EffectCreate": "Create Effect",
"SW5E.EffectDelete": "Delete Effect", "SW5E.EffectDelete": "Delete Effect",
"SW5E.EffectEdit": "Edit Effect", "SW5E.EffectEdit": "Edit Effect",
"SW5E.EffectInactive": "Inactive Effects",
"SW5E.EffectNew": "New Effect",
"SW5E.EffectPassive": "Passive Effects",
"SW5E.Effects": "Effects", "SW5E.Effects": "Effects",
"SW5E.EffectTemporary": "Temporary Effects",
"SW5E.EffectsCategoryInactive": "Inactive Effects",
"SW5E.EffectsCategoryPassive": "Passive Effects",
"SW5E.EffectsCategoryTemporary": "Temporary Effects",
"SW5E.EffectToggle": "Toggle Effect", "SW5E.EffectToggle": "Toggle Effect",
"SW5E.Engine": "Engine",
"SW5E.EnginePl": "Engines",
"SW5E.EquipmentBonus": "Magical Bonus", "SW5E.EquipmentBonus": "Magical Bonus",
"SW5E.EquipmentClothing": "Clothing", "SW5E.EquipmentClothing": "Clothing",
"SW5E.EquipmentHeavy": "Heavy Armor", "SW5E.EquipmentHeavy": "Heavy Armor",
"SW5E.EquipmentHyperdrive": "Hyperdrive",
"SW5E.EquipmentLight": "Light Armor", "SW5E.EquipmentLight": "Light Armor",
"SW5E.EquipmentMedium": "Medium Armor", "SW5E.EquipmentMedium": "Medium Armor",
"SW5E.EquipmentNatural": "Natural Armor", "SW5E.EquipmentNatural": "Natural Armor",
"SW5E.EquipmentPowerCoupling": "Power Coupling",
"SW5E.EquipmentReactor": "Reactor",
"SW5E.EquipmentShield": "Shield", "SW5E.EquipmentShield": "Shield",
"SW5E.EquipmentShieldProficiency": "Shields", "SW5E.EquipmentShieldProficiency": "Shields",
"SW5E.EquipmentStarshipArmor": "Starship Armor",
"SW5E.EquipmentStarshipShield": "Starship Shield",
"SW5E.EquipmentTrinket": "Trinket", "SW5E.EquipmentTrinket": "Trinket",
"SW5E.EquipmentVehicle": "Vehicle Equipment", "SW5E.EquipmentVehicle": "Vehicle Equipment",
"SW5E.Equipped": "Equipped", "SW5E.Equipped": "Equipped",
"SW5E.Exhaustion": "Exhaustion", "SW5E.Exhaustion": "Exhaustion",
"SW5E.Expand": "Expand",
"SW5E.Expertise": "Expertise", "SW5E.Expertise": "Expertise",
"SW5E.Favorites": "Favoris", "SW5E.Favorites": "Favorites",
"SW5E.FavoritesAndNotes": "Favorites & Notes", "SW5E.FavoritesAndNotes": "Favorites & Notes",
"SW5E.Feats": "Feats",
"SW5E.FeatureActionRecharge": "Action Recharge", "SW5E.FeatureActionRecharge": "Action Recharge",
"SW5E.FeatureActive": "Active Abilities", "SW5E.FeatureActive": "Active Abilities",
"SW5E.FeatureAdd": "Create Feature", "SW5E.FeatureAdd": "Create Feature",
"SW5E.FeatureAttack": "Feature Attack", "SW5E.FeatureAttack": "Feature Attack",
"SW5E.FeatureCollapse": "Collapse Feature",
"SW5E.FeatureExpand": "Expand Feature",
"SW5E.FeaturePassive": "Passive Abilities", "SW5E.FeaturePassive": "Passive Abilities",
"SW5E.FeatureRechargeOn": "Recharge On", "SW5E.FeatureRechargeOn": "Recharge On",
"SW5E.FeatureRechargeResult": "1d6 Result", "SW5E.FeatureRechargeResult": "1d6 Result",
"SW5E.Features": "Features", "SW5E.Features": "Features",
"SW5E.FeatureType": "Feature Type",
"SW5E.FeatureUsage": "Feature Usage", "SW5E.FeatureUsage": "Feature Usage",
"SW5E.FeetAbbr": "ft.", "SW5E.FeetAbbr": "ft.",
"SW5E.Filter": "Filter", "SW5E.Filter": "Filter",
@ -339,9 +407,9 @@
"SW5E.FlagsArmorIntegration": "Armor Integration", "SW5E.FlagsArmorIntegration": "Armor Integration",
"SW5E.FlagsArmorIntegrationHint": "You cannot wear armor, but you can have the armor professionally integrated into your chassis over the course of a long rest. This work must be done by someone proficient with astrotechs implements. You must be proficient in armor in order to have it integrated.", "SW5E.FlagsArmorIntegrationHint": "You cannot wear armor, but you can have the armor professionally integrated into your chassis over the course of a long rest. This work must be done by someone proficient with astrotechs implements. You must be proficient in armor in order to have it integrated.",
"SW5E.FlagsBusinessSavvy": "Business Savvy", "SW5E.FlagsBusinessSavvy": "Business Savvy",
"SW5E.FlagsBusinessSavvyHint":"Whenever you make a Charisma (Persuasion) check involving haggling you are considered to have expertise in the Persuasion skill.", "SW5E.FlagsBusinessSavvyHint": "Whenever you make a Charisma (Persuasion) check involving haggling you are considered to have expertise in the Persuasion skill.",
"SW5E.FlagsCannibalize": "Cannibalize", "SW5E.FlagsCannibalize": "Cannibalize",
"SW5E.FlagsCannibalizeHint":"If you spend at least 1 minute devouring the corpse of a beast or humanoid, you gain temporary hit points equal to your Constitution modifier. Once you've used this feature, you must complete a short or long rest before you can use it again.", "SW5E.FlagsCannibalizeHint": "If you spend at least 1 minute devouring the corpse of a beast or humanoid, you gain temporary hit points equal to your Constitution modifier. Once you've used this feature, you must complete a short or long rest before you can use it again.",
"SW5E.FlagsClosedMind": "Closed Mind", "SW5E.FlagsClosedMind": "Closed Mind",
"SW5E.FlagsClosedMindHint": "Members of your species have a natural attunement to the Force, which makes them resistant to its powers. You have advantage on Wisdom and Charisma saving throws against force powers.", "SW5E.FlagsClosedMindHint": "Members of your species have a natural attunement to the Force, which makes them resistant to its powers. You have advantage on Wisdom and Charisma saving throws against force powers.",
"SW5E.FlagsCrudeWeaponSpecialists": "Crude Weapon Specialists", "SW5E.FlagsCrudeWeaponSpecialists": "Crude Weapon Specialists",
@ -359,7 +427,7 @@
"SW5E.FlagsForceContention": "Force Contention", "SW5E.FlagsForceContention": "Force Contention",
"SW5E.FlagsForceContentionHint": "Due to their unique physiology, members of your species exhibit a hardiness that allows them to overcome use of the Force. You have advantage on Strength and Constitution saving throws against force powers.", "SW5E.FlagsForceContentionHint": "Due to their unique physiology, members of your species exhibit a hardiness that allows them to overcome use of the Force. You have advantage on Strength and Constitution saving throws against force powers.",
"SW5E.FlagsForceInsensitive": "Force Insensitive", "SW5E.FlagsForceInsensitive": "Force Insensitive",
"SW5E.FlagsForceInsensitiveHint" : "While droids can be manipulated by many force powers, they cannot sense the Force. You can not use force powers or take levels in forcecasting classes.", "SW5E.FlagsForceInsensitiveHint": "While droids can be manipulated by many force powers, they cannot sense the Force. You can not use force powers or take levels in forcecasting classes.",
"SW5E.FlagsForeignBiology": "Foreign Biology", "SW5E.FlagsForeignBiology": "Foreign Biology",
"SW5E.FlagsForeignBiologyHint": "You wear a breathing apparatus because many atmospheres in the galaxy differ from that of your species' homeworld. If your apparatus is removed while you are in such an environment, you lose consciousness.", "SW5E.FlagsForeignBiologyHint": "You wear a breathing apparatus because many atmospheres in the galaxy differ from that of your species' homeworld. If your apparatus is removed while you are in such an environment, you lose consciousness.",
"SW5E.FlagsFuryOfTheSmall": "Fury of the Small", "SW5E.FlagsFuryOfTheSmall": "Fury of the Small",
@ -382,7 +450,7 @@
"SW5E.FlagsMaintenanceMode": "Maintenance Mode", "SW5E.FlagsMaintenanceMode": "Maintenance Mode",
"SW5E.FlagsMaintenanceModeHint": "Rather than sleep, you enter an inactive state to perform routine maintenance for 4 hours each day. You have disadvantage on Wisdom (Perception) checks while performing maintenance.", "SW5E.FlagsMaintenanceModeHint": "Rather than sleep, you enter an inactive state to perform routine maintenance for 4 hours each day. You have disadvantage on Wisdom (Perception) checks while performing maintenance.",
"SW5E.FlagsMaskOfTheWild": "Mask of the Wild", "SW5E.FlagsMaskOfTheWild": "Mask of the Wild",
"SW5E.FlagsMaskOfTheWildHint":"You can attempt to hide even when you are only lightly obscured by foliage, heavy rain, falling snow, mist, and other natural phenomena.", "SW5E.FlagsMaskOfTheWildHint": "You can attempt to hide even when you are only lightly obscured by foliage, heavy rain, falling snow, mist, and other natural phenomena.",
"SW5E.FlagsMeleeCriticalDice": "Melee Critical Damage Dice", "SW5E.FlagsMeleeCriticalDice": "Melee Critical Damage Dice",
"SW5E.FlagsMeleeCriticalDiceHint": "A number of additional damage dice added to melee weapon critical hits.", "SW5E.FlagsMeleeCriticalDiceHint": "A number of additional damage dice added to melee weapon critical hits.",
"SW5E.FlagsMultipleHearts": "Multiple Hearts", "SW5E.FlagsMultipleHearts": "Multiple Hearts",
@ -453,19 +521,31 @@
"SW5E.Flaws": "Flaws", "SW5E.Flaws": "Flaws",
"SW5E.ForcePowerbook": "Force Powers", "SW5E.ForcePowerbook": "Force Powers",
"SW5E.Formula": "Formula", "SW5E.Formula": "Formula",
"SW5E.FuelCapacity": "Fuel Capacity",
"SW5E.FuelCostPerUnit": "Fuel Cost per Unit",
"SW5E.FuelCostsMod": "Fuel Costs Modifier",
"SW5E.GrantedAbilities": "Granted Abilities", "SW5E.GrantedAbilities": "Granted Abilities",
"SW5E.HalfProficient": "Half Proficient", "SW5E.HalfProficient": "Half Proficient",
"SW5E.HardpointSizeMod": "Hardpoint Size Modifier",
"SW5E.Healing": "Healing", "SW5E.Healing": "Healing",
"SW5E.HealingTemp": "Healing (Temporary)", "SW5E.HealingTemp": "Healing (Temporary)",
"SW5E.Health": "Health", "SW5E.Health": "Health",
"SW5E.HealthConditions": "Health Conditions", "SW5E.HealthConditions": "Health Conditions",
"SW5E.HealthFormula": "Health Formula", "SW5E.HealthFormula": "Health Formula",
"SW5E.HitDice": "Hit Dice", "SW5E.HitDice": "Hit Dice",
"SW5E.HitDiceConfig": "Adjust Hit Dice",
"SW5E.HitDiceConfigHint": "Adjust remaining hit dice levels for each class.",
"SW5E.HitDiceMax": "Maximum Hit Dice",
"SW5E.HitDiceRemaining": "Remaining Hit Dice",
"SW5E.HitDiceRoll": "Roll Hit Dice", "SW5E.HitDiceRoll": "Roll Hit Dice",
"SW5E.HitDiceUsed": "Hit Dice Used", "SW5E.HitDiceUsed": "Hit Dice Used",
"SW5E.HitDiceWarn": "{name} has no available {formula} Hit Dice remaining!", "SW5E.HitDiceWarn": "{name} has no available {formula} Hit Dice remaining!",
"SW5E.HP": "Health", "SW5E.HP": "Health",
"SW5E.HPFormula": "Health Formula", "SW5E.HPFormula": "Health Formula",
"SW5E.HullDice": "Hull Dice",
"SW5E.HullPoints": "Hull Points",
"SW5E.HullPointsFormula": "Hull Points Formula",
"SW5E.HyperdriveClass": "Hyperdrive Class",
"SW5E.Ideals": "Ideals", "SW5E.Ideals": "Ideals",
"SW5E.Identified": "Identified", "SW5E.Identified": "Identified",
"SW5E.Initiative": "Initiative", "SW5E.Initiative": "Initiative",
@ -509,6 +589,7 @@
"SW5E.ItemTypeArchetype": "Archetype", "SW5E.ItemTypeArchetype": "Archetype",
"SW5E.ItemTypeBackground": "Background", "SW5E.ItemTypeBackground": "Background",
"Sw5E.ItemTypeBackgroundPl": "Backgrounds", "Sw5E.ItemTypeBackgroundPl": "Backgrounds",
"SW5E.ItemTypeBackpack": "Container",
"SW5E.ItemTypeClass": "Class", "SW5E.ItemTypeClass": "Class",
"SW5E.ItemTypeClassFeat": "Class Feature", "SW5E.ItemTypeClassFeat": "Class Feature",
"SW5E.ItemTypeClassFeats": "Class Features", "SW5E.ItemTypeClassFeats": "Class Features",
@ -517,6 +598,10 @@
"SW5E.ItemTypeConsumablePl": "Consumables", "SW5E.ItemTypeConsumablePl": "Consumables",
"SW5E.ItemTypeContainer": "Container", "SW5E.ItemTypeContainer": "Container",
"SW5E.ItemTypeContainerPl": "Containers", "SW5E.ItemTypeContainerPl": "Containers",
"SW5E.ItemTypeDeployment": "Deployment",
"SW5E.ItemTypeDeploymentFeature": "Deployment Feature",
"SW5E.ItemTypeDeploymentFeaturePl": "Deployment Features",
"SW5E.ItemTypeDeploymentPl": "Deployments",
"SW5E.ItemTypeEquipment": "Equipment", "SW5E.ItemTypeEquipment": "Equipment",
"SW5E.ItemTypeEquipmentPl": "Equipment", "SW5E.ItemTypeEquipmentPl": "Equipment",
"SW5E.ItemTypeFeat": "Feat", "SW5E.ItemTypeFeat": "Feat",
@ -533,13 +618,19 @@
"SW5E.ItemTypePowerPl": "Powers", "SW5E.ItemTypePowerPl": "Powers",
"SW5E.ItemTypeSpecies": "Species", "SW5E.ItemTypeSpecies": "Species",
"SW5E.ItemTypeSpeciesPl": "Species", "SW5E.ItemTypeSpeciesPl": "Species",
"SW5E.ItemTypeStarshipMod": "Starship Modification",
"SW5E.ItemTypeStarshipModPl": "Starship Modifications",
"SW5E.ItemTypeTool": "Tool", "SW5E.ItemTypeTool": "Tool",
"SW5E.ItemTypeToolPl": "Tools", "SW5E.ItemTypeToolPl": "Tools",
"SW5E.ItemTypeVenture": "Venture",
"SW5E.ItemTypeVenturePl": "Ventures",
"SW5E.ItemTypeWeapon": "Weapon", "SW5E.ItemTypeWeapon": "Weapon",
"SW5E.ItemTypeWeaponPl": "Weapons", "SW5E.ItemTypeWeaponPl": "Weapons",
"SW5E.ItemWeaponAttack": "Weapon Attack", "SW5E.ItemWeaponAttack": "Weapon Attack",
"SW5E.ItemWeaponDetails": "Weapon Details", "SW5E.ItemWeaponDetails": "Weapon Details",
"SW5E.ItemWeaponProperties": "Weapon Properties", "SW5E.ItemWeaponProperties": "Weapon Properties",
"SW5E.ItemWeaponSize": "Starship Weapon Size",
"SW5E.ItemWeaponSizePl": "Starship Weapon Sizes",
"SW5E.ItemWeaponStatus": "Weapon Status", "SW5E.ItemWeaponStatus": "Weapon Status",
"SW5E.ItemWeaponType": "Weapon Type", "SW5E.ItemWeaponType": "Weapon Type",
"SW5E.ItemWeaponUsage": "Weapon Usage", "SW5E.ItemWeaponUsage": "Weapon Usage",
@ -665,6 +756,7 @@
"SW5E.LongRest": "Long Rest", "SW5E.LongRest": "Long Rest",
"SW5E.LongRestEpic": "Long Rest (1 hour)", "SW5E.LongRestEpic": "Long Rest (1 hour)",
"SW5E.LongRestGritty": "Long Rest (7 days)", "SW5E.LongRestGritty": "Long Rest (7 days)",
"SW5E.LongRestHint": "Take a long rest? On a long rest you will recover hit points, half your maximum hit dice, class resources, limited use item charges, and power points.",
"SW5E.LongRestNormal": "Long Rest (8 hours)", "SW5E.LongRestNormal": "Long Rest (8 hours)",
"SW5E.LongRestOvernight": "Long Rest (New Day)", "SW5E.LongRestOvernight": "Long Rest (New Day)",
"SW5E.LongRestResult": "{name} takes a long rest.", "SW5E.LongRestResult": "{name} takes a long rest.",
@ -684,18 +776,25 @@
"SW5E.LongRestResultTP": "{name} takes a long rest and recovers {tech} Tech Points.", "SW5E.LongRestResultTP": "{name} takes a long rest and recovers {tech} Tech Points.",
"SW5E.LongRestResultTPHD": "{name} takes a long rest and recovers {tech} Tech Points and {dice} Hit Dice.", "SW5E.LongRestResultTPHD": "{name} takes a long rest and recovers {tech} Tech Points and {dice} Hit Dice.",
"SW5E.Max": "Max", "SW5E.Max": "Max",
"SW5E.ModCap": "Modification Capacity",
"SW5E.Modifier": "Modifier", "SW5E.Modifier": "Modifier",
"SW5E.Movement": "Movement", "SW5E.Movement": "Movement",
"SW5E.MovementBurrow": "Burrow", "SW5E.MovementBurrow": "Burrow",
"SW5E.MovementClimb": "Climb", "SW5E.MovementClimb": "Climb",
"SW5E.MovementConfig": "Configure Movement Speed", "SW5E.MovementConfig": "Configure Movement Speed",
"SW5E.MovementConfigHint": "Configure the movement speed and special movement attributes of this creature.", "SW5E.MovementConfigHint": "Configure the movement speed and special movement attributes of this creature.",
"SW5E.MovementCrawl": "Crawl",
"SW5E.MovementFly": "Fly", "SW5E.MovementFly": "Fly",
"SW5E.MovementHover": "Hover", "SW5E.MovementHover": "Hover",
"SW5E.MovementRoll": "Roll",
"SW5E.MovementSpace": "Space Flight",
"SW5E.MovementSwim": "Swim", "SW5E.MovementSwim": "Swim",
"SW5E.MovementTurn": "Turning",
"SW5E.MovementUnits": "Units", "SW5E.MovementUnits": "Units",
"SW5E.MovementWalk": "Walk", "SW5E.MovementWalk": "Walk",
"SW5E.Name": "Character Name", "SW5E.Name": "Character Name",
"SW5E.NewDay": "Is New Day?",
"SW5E.NewDayHint": "Recover limited use abilities which recharge \"per day\"?",
"SW5E.NoCharges": "No Charges", "SW5E.NoCharges": "No Charges",
"SW5E.None": "None", "SW5E.None": "None",
"SW5E.NoPowerLevels": "This character has no powercaster levels, but you may add powers manually.", "SW5E.NoPowerLevels": "This character has no powercaster levels, but you may add powers manually.",
@ -748,7 +847,12 @@
"SW5E.PowerCreate": "Create Power", "SW5E.PowerCreate": "Create Power",
"SW5E.PowerDC": "Power DC", "SW5E.PowerDC": "Power DC",
"SW5E.PowerDetails": "Power Details", "SW5E.PowerDetails": "Power Details",
"SW5E.PowerDiceRecovery": "Power Dice Recovery",
"SW5E.PowerDie": "Power Die",
"SW5E.PowerDieAlloc": "Power Die Allocation",
"SW5E.PowerDiePl": "Power Dice",
"SW5E.PowerEffects": "Power Effects", "SW5E.PowerEffects": "Power Effects",
"SW5E.PowerfulCritical": "Powerful Critical",
"SW5E.PowerLevel": "Power Level", "SW5E.PowerLevel": "Power Level",
"SW5E.PowerLevel0": "At-Will", "SW5E.PowerLevel0": "At-Will",
"SW5E.PowerLevel1": "1st Level", "SW5E.PowerLevel1": "1st Level",
@ -778,6 +882,7 @@
"SW5E.PowerProgression": "Power Progression", "SW5E.PowerProgression": "Power Progression",
"SW5E.PowerProgSct": "Scout", "SW5E.PowerProgSct": "Scout",
"SW5E.PowerProgSnt": "Sentinel", "SW5E.PowerProgSnt": "Sentinel",
"SW5E.PowerRouting": "Power Routing",
"SW5E.PowerSchool": "Power School", "SW5E.PowerSchool": "Power School",
"SW5E.PowersKnown": "Powers Known", "SW5E.PowersKnown": "Powers Known",
"SW5E.PowerTarget": "Power Target", "SW5E.PowerTarget": "Power Target",
@ -789,21 +894,29 @@
"SW5E.Proficient": "Proficient", "SW5E.Proficient": "Proficient",
"SW5E.Quantity": "Quantity", "SW5E.Quantity": "Quantity",
"SW5E.Range": "Range", "SW5E.Range": "Range",
"SW5E.Rank": "Rank",
"SW5E.RankPl": "Ranks",
"SW5E.Rarity": "Rarity", "SW5E.Rarity": "Rarity",
"SW5E.Reaction": "Reaction", "SW5E.Reaction": "Reaction",
"SW5E.ReactionPl": "Reactions", "SW5E.ReactionPl": "Reactions",
"SW5E.Recharge": "Recharge", "SW5E.Recharge": "Recharge",
"SW5E.Refitting": "Refitting",
"SW5E.Refuel": "Refuel",
"SW5E.RegenerationRateCoefficient": "Regeneration Rate Coefficient",
"SW5E.RequiredMaterials": "Required Materials", "SW5E.RequiredMaterials": "Required Materials",
"SW5E.Requirements": "Requirements", "SW5E.Requirements": "Requirements",
"SW5E.ResourcesAndTraits": "Resources & Traits",
"SW5E.ResourcePrimary": "Resource 1", "SW5E.ResourcePrimary": "Resource 1",
"SW5E.ResourcesAndTraits": "Resources & Traits",
"SW5E.ResourceSecondary": "Resource 2", "SW5E.ResourceSecondary": "Resource 2",
"SW5E.ResourceTertiary": "Resource 3", "SW5E.ResourceTertiary": "Resource 3",
"SW5E.Rest": "Rest",
"SW5E.RestL": "L. Rest", "SW5E.RestL": "L. Rest",
"SW5E.RestS": "S. Rest", "SW5E.RestS": "S. Rest",
"SW5E.Ritual": "Ritual", "SW5E.Ritual": "Ritual",
"SW5E.Role": "Role",
"SW5E.RolePl": "Roles",
"SW5E.Roll": "Roll", "SW5E.Roll": "Roll",
"SW5E.RollExample": "e.g. +1d4", "SW5E.RollExample": "e.g. 1d4",
"SW5E.RollMode": "Roll Mode", "SW5E.RollMode": "Roll Mode",
"SW5E.RollSituationalBonus": "Situational Bonus?", "SW5E.RollSituationalBonus": "Situational Bonus?",
"SW5E.Save": "Save", "SW5E.Save": "Save",
@ -816,6 +929,7 @@
"SW5E.SchoolLgt": "Light", "SW5E.SchoolLgt": "Light",
"SW5E.SchoolTec": "Tech", "SW5E.SchoolTec": "Tech",
"SW5E.SchoolUni": "Universal", "SW5E.SchoolUni": "Universal",
"SW5E.SelectItemsPromptTitle": "Select Items",
"SW5E.SenseBlindsight": "Blindsight", "SW5E.SenseBlindsight": "Blindsight",
"SW5E.SenseBS": "Blindsight", "SW5E.SenseBS": "Blindsight",
"SW5E.SenseDarkvision": "Darkvision", "SW5E.SenseDarkvision": "Darkvision",
@ -834,6 +948,10 @@
"SW5E.SheetClassNPC": "Default NPC Sheet", "SW5E.SheetClassNPC": "Default NPC Sheet",
"SW5E.SheetClassNPCOld": "Old NPC Sheet", "SW5E.SheetClassNPCOld": "Old NPC Sheet",
"SW5E.SheetClassVehicle": "Default Vehicle Sheet", "SW5E.SheetClassVehicle": "Default Vehicle Sheet",
"SW5E.ShieldDice": "Shield Dice",
"SW5E.ShieldPoints": "Shield Points",
"SW5E.ShieldPointsFormula": "Shield Points Formula",
"SW5E.ShieldRegen": "Regen",
"SW5E.ShortRest": "Short Rest", "SW5E.ShortRest": "Short Rest",
"SW5E.ShortRestEpic": "Short Rest (5 minutes)", "SW5E.ShortRestEpic": "Short Rest (5 minutes)",
"SW5E.ShortRestGritty": "Short Rest (8 hours)", "SW5E.ShortRestGritty": "Short Rest (8 hours)",
@ -873,6 +991,7 @@
"SW5E.SkillSte": "Stealth", "SW5E.SkillSte": "Stealth",
"SW5E.SkillSur": "Survival", "SW5E.SkillSur": "Survival",
"SW5E.SkillTec": "Technology", "SW5E.SkillTec": "Technology",
"SW5E.Skip": "Skip",
"SW5E.Slots": "Slots", "SW5E.Slots": "Slots",
"SW5E.Source": "Source", "SW5E.Source": "Source",
"SW5E.Special": "Special", "SW5E.Special": "Special",
@ -882,8 +1001,62 @@
"SW5E.SpeciesTraits": "Species Traits", "SW5E.SpeciesTraits": "Species Traits",
"SW5E.Speed": "Speed", "SW5E.Speed": "Speed",
"SW5E.SpeedSpecial": "Special Movement", "SW5E.SpeedSpecial": "Special Movement",
"SW5E.StarshipAmbassador": "Ambassador",
"SW5E.StarshipArmorandShieldProps": "Starship Armor & Shield Properties",
"SW5E.StarshipArmorandShields": "Starship Armor and Shields",
"SW5E.StarshipBattleship": "Battleship",
"SW5E.StarshipBlockadeShip": "Blockade Ship",
"SW5E.StarshipBomber": "Bomber",
"SW5E.StarshipCarrier": "Carrier",
"SW5E.StarshipColonizer": "Colonizer",
"SW5E.StarshipCommandShip": "Command Ship",
"SW5E.StarshipCorvette": "Corvette",
"SW5E.StarshipCourier": "Courier",
"SW5E.StarshipCruiser": "Cruiser",
"SW5E.StarshipEquipment": "Starship Equipment",
"SW5E.StarshipEquipmentProps": "Starship Equipment Properties",
"SW5E.StarshipExplorer": "Explorer",
"SW5E.StarshipfeaturePl": "Starship Features",
"SW5E.StarshipFlagship": "Flagship",
"SW5E.StarshipFreighter": "Freighter",
"SW5E.StarshipGunboat": "Gunboat",
"SW5E.StarshipIndustrialCenter": "Industrial Center",
"SW5E.StarshipInterceptor": "Interceptor",
"SW5E.StarshipInterdictor": "Interdictor",
"SW5E.StarshipJuggernaut": "Juggernaut",
"SW5E.StarshipMissileBoat": "Missile Boat",
"SW5E.StarshipMobileMetropolis": "Mobile Metropolis",
"SW5E.StarshipmodPl": "Starship Modifications",
"SW5E.StarshipNavigator": "Navigator",
"SW5E.StarshipPicketShip": "Picket Ship",
"SW5E.StarshipResearcher": "Researcher",
"SW5E.StarshipScout": "Scout",
"SW5E.StarshipScrambler": "Scrambler",
"SW5E.StarshipShipsTender": "Ship's Tender",
"SW5E.StarshipShuttle": "Shuttle",
"SW5E.StarshipSkillAst": "Astrogation",
"SW5E.StarshipSkillBst": "Boost",
"SW5E.StarshipSkillDat": "Data",
"SW5E.StarshipSkillHid": "Hide",
"SW5E.StarshipSkillImp": "Impress",
"SW5E.StarshipSkillInt": "Interfere",
"SW5E.StarshipSkillMan": "Maneuvering",
"SW5E.StarshipSkillMen": "Menace",
"SW5E.StarshipSkillPat": "Patch",
"SW5E.StarshipSkillPrb": "Probe",
"SW5E.StarshipSkillRam": "Ram",
"SW5E.StarshipSkillReg": "Regulation",
"SW5E.StarshipSkillScn": "Scan",
"SW5E.StarshipSkillSwn": "Swindle",
"SW5E.StarshipStrikeFighter": "Strike Fighter",
"SW5E.StarshipTier": "Tier",
"SW5E.StarshipWarship": "Warship",
"SW5E.StarshipYacht": "Yacht",
"SW5E.StealthDisadvantage": "Stealth Disadvantage", "SW5E.StealthDisadvantage": "Stealth Disadvantage",
"SW5E.SuiteCap": "Suite Capacity",
"SW5E.Supply": "Supply", "SW5E.Supply": "Supply",
"SW5E.SysStorageCapacity": "System Storage Capacity",
"SW5E.SystemDrainage": "System Drainage",
"SW5E.Target": "Target", "SW5E.Target": "Target",
"SW5E.TargetAlly": "Ally", "SW5E.TargetAlly": "Ally",
"SW5E.TargetCone": "Cone", "SW5E.TargetCone": "Cone",
@ -899,6 +1072,7 @@
"SW5E.TargetSpace": "Space", "SW5E.TargetSpace": "Space",
"SW5E.TargetSphere": "Sphere", "SW5E.TargetSphere": "Sphere",
"SW5E.TargetSquare": "Square", "SW5E.TargetSquare": "Square",
"SW5E.TargetStarship": "Starship",
"SW5E.TargetWall": "Wall", "SW5E.TargetWall": "Wall",
"SW5E.TargetWeapon": "Weapon", "SW5E.TargetWeapon": "Weapon",
"SW5E.TargetWidth": "Line Width", "SW5E.TargetWidth": "Line Width",
@ -954,7 +1128,8 @@
"SW5E.TraitToolProf": "Tool Proficiencies", "SW5E.TraitToolProf": "Tool Proficiencies",
"SW5E.TraitWeaponProf": "Weapon Proficiencies", "SW5E.TraitWeaponProf": "Weapon Proficiencies",
"SW5E.Type": "Type", "SW5E.Type": "Type",
"SW5E.Unequipped": "Not Equipped", "SW5E.Uncrewed": "Uncrewed",
"SW5E.Unequipped": "Unequipped",
"SW5E.UniversalPowerDC": "Universal Power DC", "SW5E.UniversalPowerDC": "Universal Power DC",
"SW5E.Unlimited": "Unlimited", "SW5E.Unlimited": "Unlimited",
"SW5E.Usage": "Usage", "SW5E.Usage": "Usage",
@ -978,50 +1153,85 @@
"SW5E.VersatileDamage": "Versatile Damage", "SW5E.VersatileDamage": "Versatile Damage",
"SW5E.VsDC": "vs DC.", "SW5E.VsDC": "vs DC.",
"SW5E.WeaponAmmo": "Ammunition", "SW5E.WeaponAmmo": "Ammunition",
"SW5E.WeaponBlasterPistolProficiency": "Blaster Pistol",
"SW5E.WeaponChakramProficiency": "Chakrams",
"SW5E.WeaponDoubleBladeProficiency": "Doubleblade",
"SW5E.WeaponDoubleSaberProficiency": "Doublesaber",
"SW5E.WeaponDoubleShotoProficiency": "Doubleshoto",
"SW5E.WeaponDoubleSwordProficiency": "Doublesword",
"SW5E.WeaponHiddenBladeProficiency": "Hidden Blade",
"SW5E.WeaponImprov": "Improvised", "SW5E.WeaponImprov": "Improvised",
"SW5E.WeaponImprovisedProficiency": "Improvised Weapons",
"SW5E.WeaponLightFoilProficiency": "Lightfoil",
"SW5E.WeaponLightRingProficiency": "Light Ring",
"SW5E.WeaponMartialB": "Martial Blaster", "SW5E.WeaponMartialB": "Martial Blaster",
"SW5E.WeaponMartialBlasterProficiency": "Martial Blasters",
"SW5E.WeaponMartialLightweaponProficiency": "Martial Lightweapons",
"SW5E.WeaponMartialLW": "Martial Lightweapon", "SW5E.WeaponMartialLW": "Martial Lightweapon",
"SW5E.WeaponMartialProficiency": "Martial Weapons", "SW5E.WeaponMartialProficiency": "Martial Weapons",
"SW5E.WeaponMartialVibroweaponProficiency": "Martial Vibroweapons",
"SW5E.WeaponMartialVW": "Martial Vibroweapon", "SW5E.WeaponMartialVW": "Martial Vibroweapon",
"SW5E.WeaponNatural": "Natural", "SW5E.WeaponNatural": "Natural",
"SW5E.WeaponNaturalProficiency": "Natural Weapons",
"SW5E.WeaponPrimarySW": "Primary (Starship)",
"SW5E.WeaponPropertiesAmm": "Ammunition", "SW5E.WeaponPropertiesAmm": "Ammunition",
"SW5E.WeaponPropertiesAut": "Auto", "SW5E.WeaponPropertiesAut": "Auto",
"SW5E.WeaponPropertiesBur": "Burst", "SW5E.WeaponPropertiesBur": "Burst",
"SW5E.WeaponPropertiesCon": "Constitution",
"SW5E.WeaponPropertiesDef": "Defensive", "SW5E.WeaponPropertiesDef": "Defensive",
"SW5E.WeaponPropertiesDex": "Dexterity Rqmt.", "SW5E.WeaponPropertiesDex": "Dexterity Rqt",
"SW5E.WeaponPropertiesDgd": "Disguised", "SW5E.WeaponPropertiesDgd": "Disguised",
"SW5E.WeaponPropertiesDir": "Dire", "SW5E.WeaponPropertiesDir": "Dire",
"SW5E.WeaponPropertiesDis": "Disintegrate", "SW5E.WeaponPropertiesDis": "Disintegrate",
"SW5E.WeaponPropertiesDou": "Double", "SW5E.WeaponPropertiesDou": "Double",
"SW5E.WeaponPropertiesDpt": "Disruptive", "SW5E.WeaponPropertiesDpt": "Disruptive",
"SW5E.WeaponPropertiesDrm": "Disarming", "SW5E.WeaponPropertiesDrm": "Disarming",
"SW5E.WeaponPropertiesExp": "Explosive",
"SW5E.WeaponPropertiesFin": "Finesse", "SW5E.WeaponPropertiesFin": "Finesse",
"SW5E.WeaponPropertiesFix": "Fixed", "SW5E.WeaponPropertiesFix": "Fixed",
"SW5E.WeaponPropertiesFoc": "Focus", "SW5E.WeaponPropertiesFoc": "Focus",
"SW5E.WeaponPropertiesHid": "Hidden", "SW5E.WeaponPropertiesHid": "Hidden",
"SW5E.WeaponPropertiesHom": "Homing",
"SW5E.WeaponPropertiesHvy": "Heavy", "SW5E.WeaponPropertiesHvy": "Heavy",
"SW5E.WeaponPropertiesIon": "Ionizing",
"SW5E.WeaponPropertiesKen": "Keen", "SW5E.WeaponPropertiesKen": "Keen",
"SW5E.WeaponPropertiesLgt": "Light", "SW5E.WeaponPropertiesLgt": "Light",
"SW5E.WeaponPropertiesLum": "Luminous", "SW5E.WeaponPropertiesLum": "Luminous",
"SW5E.WeaponPropertiesMig": "Mighty", "SW5E.WeaponPropertiesMig": "Mighty",
"SW5E.WeaponPropertiesMlt": "Melt",
"SW5E.WeaponPropertiesOvr": "Overheat",
"SW5E.WeaponPropertiesPic": "Piercing", "SW5E.WeaponPropertiesPic": "Piercing",
"SW5E.WeaponPropertiesPow": "Power",
"SW5E.WeaponPropertiesRan": "Range", "SW5E.WeaponPropertiesRan": "Range",
"SW5E.WeaponPropertiesRap": "Rapid", "SW5E.WeaponPropertiesRap": "Rapid",
"SW5E.WeaponPropertiesRch": "Reach", "SW5E.WeaponPropertiesRch": "Reach",
"SW5E.WeaponPropertiesRel": "Reload", "SW5E.WeaponPropertiesRel": "Reload",
"SW5E.WeaponPropertiesRet": "Returning", "SW5E.WeaponPropertiesRet": "Returning",
"SW5E.WeaponPropertiesSat": "Saturate",
"SW5E.WeaponPropertiesShk": "Shocking", "SW5E.WeaponPropertiesShk": "Shocking",
"SW5E.WeaponPropertiesSil": "Silent", "SW5E.WeaponPropertiesSil": "Silent",
"SW5E.WeaponPropertiesSpc": "Special", "SW5E.WeaponPropertiesSpc": "Special",
"SW5E.WeaponPropertiesStr": "Strength Rqmt.", "SW5E.WeaponPropertiesStr": "Strength Rqt",
"SW5E.WeaponPropertiesThr": "Thrown", "SW5E.WeaponPropertiesThr": "Thrown",
"SW5E.WeaponPropertiesTwo": "Two-Handed", "SW5E.WeaponPropertiesTwo": "Two-Handed",
"SW5E.WeaponPropertiesVer": "Versatile", "SW5E.WeaponPropertiesVer": "Versatile",
"SW5E.WeaponPropertiesVic": "Vicious", "SW5E.WeaponPropertiesVic": "Vicious",
"SW5E.WeaponPropertiesZon": "Zone",
"SW5E.WeaponQuaternarySW": "Quaternary (Starship)",
"SW5E.WeaponSaberWhipProficiency": "Saberwhip",
"SW5E.WeaponSecondarySW": "Secondary (Starship)",
"SW5E.WeaponSiege": "Siege", "SW5E.WeaponSiege": "Siege",
"SW5E.WeaponSimpleB": "Simple Blaster", "SW5E.WeaponSimpleB": "Simple Blaster",
"SW5E.WeaponSimpleBlasterProficiency": "Simple Blasters",
"SW5E.WeaponSimpleLightweaponProficiency": "Simple Lightweapons",
"SW5E.WeaponSimpleLW": "Simple Lightweapon", "SW5E.WeaponSimpleLW": "Simple Lightweapon",
"SW5E.WeaponSimpleProficiency": "Simple Weapons", "SW5E.WeaponSimpleProficiency": "Simple Weapons",
"SW5E.WeaponSimpleVibroweaponProficiency": "Simple Vibroweapons",
"SW5E.WeaponSimpleVW": "Simple Vibroweapon", "SW5E.WeaponSimpleVW": "Simple Vibroweapon",
"SW5E.WeaponSizeAbb": "Size",
"SW5E.WeaponTechbladeProficiency": "Techblades",
"SW5E.WeaponTertiarySW": "Tertiary (Starship)",
"SW5E.WeaponVibrorapierProficiency": "Vibrorapier",
"SW5E.WeaponVibrowhipProficiency": "Vibrowhip",
"SW5E.Weight": "Weight" "SW5E.Weight": "Weight"
} }

View file

@ -192,8 +192,8 @@
"SW5E.ClassSkillsChosen": "Compétences de classe choisies", "SW5E.ClassSkillsChosen": "Compétences de classe choisies",
"SW5E.ClassSkillsNumber": "Nombre de compétences de départ", "SW5E.ClassSkillsNumber": "Nombre de compétences de départ",
"SW5E.ComponentMaterial": "Matérielle", "SW5E.ComponentMaterial": "Matérielle",
"SW5E.ComponentSomatic": "Somatique", "SW5E.ComponentSomatic": "Somatique",
"SW5E.ComponentVerbal": "Verbale", "SW5E.ComponentVerbal": "Verbale",
"SW5E.ConBlinded": "Aveuglé", "SW5E.ConBlinded": "Aveuglé",
"SW5E.Concentrate": "Concentration", "SW5E.Concentrate": "Concentration",
"SW5E.Concentrated": "Concentré", "SW5E.Concentrated": "Concentré",
@ -222,7 +222,7 @@
"SW5E.ConsumableForce": "Points de Force", "SW5E.ConsumableForce": "Points de Force",
"SW5E.ConsumableLastChargeWarn": "C'est la dernière charge de cette unité et sa consommation réduira également la quantité de l'article de 1.", "SW5E.ConsumableLastChargeWarn": "C'est la dernière charge de cette unité et sa consommation réduira également la quantité de l'article de 1.",
"SW5E.ConsumableMedpac": "Medipack", "SW5E.ConsumableMedpac": "Medipack",
"SW5E.ConsumablePoison": "Poison", "SW5E.ConsumablePoison": "Poison",
"SW5E.ConsumableTech": "Points de Tech", "SW5E.ConsumableTech": "Points de Tech",
"SW5E.ConsumableTechnology": "Technologie", "SW5E.ConsumableTechnology": "Technologie",
"SW5E.ConsumableTrinket": "Babiole", "SW5E.ConsumableTrinket": "Babiole",
@ -233,7 +233,7 @@
"SW5E.ConsumeAmmunition": "Munition", "SW5E.ConsumeAmmunition": "Munition",
"SW5E.ConsumeAttribute": "Capacité", "SW5E.ConsumeAttribute": "Capacité",
"SW5E.ConsumeCharges": "Charge", "SW5E.ConsumeCharges": "Charge",
"SW5E.Consumed": "Consommé", "SW5E.Consumed": "Consommé",
"SW5E.ConsumeMaterial": "Matériel", "SW5E.ConsumeMaterial": "Matériel",
"SW5E.ConsumeRecharge": "Consommer la recharge ?", "SW5E.ConsumeRecharge": "Consommer la recharge ?",
"SW5E.ConsumeResource": "Consommer la ressource ?", "SW5E.ConsumeResource": "Consommer la ressource ?",
@ -256,10 +256,10 @@
"SW5E.CurrencyConvertHint": "Convertir toutes les devises transportées dans la catégorie la plus élevée possible afin de réduire la quantité de pièces transportées par le personnage. Attention, cette action ne peut être annulée.", "SW5E.CurrencyConvertHint": "Convertir toutes les devises transportées dans la catégorie la plus élevée possible afin de réduire la quantité de pièces transportées par le personnage. Attention, cette action ne peut être annulée.",
"SW5E.CurrencyGC": "Credits", "SW5E.CurrencyGC": "Credits",
"SW5E.Damage": "Dégâts", "SW5E.Damage": "Dégâts",
"SW5E.DamageAcid": "Acide", "SW5E.DamageAcid": "Acide",
"SW5E.DamageCold": "Froid", "SW5E.DamageCold": "Froid",
"SW5E.DamageEnergy": "Energy", "SW5E.DamageEnergy": "Energy",
"SW5E.DamageFire": "Feu", "SW5E.DamageFire": "Feu",
"SW5E.DamageForce": "Force", "SW5E.DamageForce": "Force",
"SW5E.DamageIon": "Ionique", "SW5E.DamageIon": "Ionique",
"SW5E.DamageKinetic": "Cinétique", "SW5E.DamageKinetic": "Cinétique",
@ -339,9 +339,9 @@
"SW5E.FlagsArmorIntegration": "Intégration d'armure", "SW5E.FlagsArmorIntegration": "Intégration d'armure",
"SW5E.FlagsArmorIntegrationHint": "Chaque fois que vous effectuez un test de Charisme (Persuasion) impliquant du marchandage, vous êtes considéré comme ayant une expertise dans la compétence Persuasion.", "SW5E.FlagsArmorIntegrationHint": "Chaque fois que vous effectuez un test de Charisme (Persuasion) impliquant du marchandage, vous êtes considéré comme ayant une expertise dans la compétence Persuasion.",
"SW5E.FlagsBusinessSavvy": "Sens des affaires", "SW5E.FlagsBusinessSavvy": "Sens des affaires",
"SW5E.FlagsBusinessSavvyHint":"Chaque fois que vous effectuez un test de Charisme (Persuasion) impliquant du marchandage, vous êtes considéré comme ayant une expertise dans la compétence Persuasion.", "SW5E.FlagsBusinessSavvyHint": "Chaque fois que vous effectuez un test de Charisme (Persuasion) impliquant du marchandage, vous êtes considéré comme ayant une expertise dans la compétence Persuasion.",
"SW5E.FlagsCannibalize": "Cannibale", "SW5E.FlagsCannibalize": "Cannibale",
"SW5E.FlagsCannibalizeHint":"Si vous passez au moins 1 minute à dévorer le cadavre d'une bête ou d'un humanoïde, vous gagnez des points de vie temporaires égaux à votre modificateur de Constitution. Une fois que vous avez utilisé cette fonctionnalité, vous devez effectuer un repos court ou long avant de pouvoir l'utiliser à nouveau.", "SW5E.FlagsCannibalizeHint": "Si vous passez au moins 1 minute à dévorer le cadavre d'une bête ou d'un humanoïde, vous gagnez des points de vie temporaires égaux à votre modificateur de Constitution. Une fois que vous avez utilisé cette fonctionnalité, vous devez effectuer un repos court ou long avant de pouvoir l'utiliser à nouveau.",
"SW5E.FlagsClosedMind": "Esprit Fortifié", "SW5E.FlagsClosedMind": "Esprit Fortifié",
"SW5E.FlagsClosedMindHint": "Les membres de votre espèce ont une harmonisation naturelle avec la Force, ce qui les rend résistants à ses pouvoirs. Vous avez l'avantage aux jets de sauvegarde de Sagesse et de Charisme contre les pouvoirs de force.", "SW5E.FlagsClosedMindHint": "Les membres de votre espèce ont une harmonisation naturelle avec la Force, ce qui les rend résistants à ses pouvoirs. Vous avez l'avantage aux jets de sauvegarde de Sagesse et de Charisme contre les pouvoirs de force.",
"SW5E.FlagsCrudeWeaponSpecialists": "Spécialiste des armes improvisées", "SW5E.FlagsCrudeWeaponSpecialists": "Spécialiste des armes improvisées",
@ -359,7 +359,7 @@
"SW5E.FlagsForceContention": "Force Contention", "SW5E.FlagsForceContention": "Force Contention",
"SW5E.FlagsForceContentionHint": "En raison de leur physiologie unique, les membres de votre espèce présentent une rusticité qui leur permet de surmonter l'utilisation de la Force. Vous avez un avantage aux jets de sauvegarde de Force et de Constitution contre les pouvoirs de la force.", "SW5E.FlagsForceContentionHint": "En raison de leur physiologie unique, les membres de votre espèce présentent une rusticité qui leur permet de surmonter l'utilisation de la Force. Vous avez un avantage aux jets de sauvegarde de Force et de Constitution contre les pouvoirs de la force.",
"SW5E.FlagsForceInsensitive": "Insensible à la Force", "SW5E.FlagsForceInsensitive": "Insensible à la Force",
"SW5E.FlagsForceInsensitiveHint" : "Bien que les droïdes puissent être manipulés par de nombreux pouvoirs de la force, ils ne peuvent pas sentir la Force. Vous ne pouvez pas utiliser de pouvoirs de la force ou prendre des niveaux dans les classes d'utilisateurs de la force.", "SW5E.FlagsForceInsensitiveHint": "Bien que les droïdes puissent être manipulés par de nombreux pouvoirs de la force, ils ne peuvent pas sentir la Force. Vous ne pouvez pas utiliser de pouvoirs de la force ou prendre des niveaux dans les classes d'utilisateurs de la force.",
"SW5E.FlagsForeignBiology": "Biologie Étrangère", "SW5E.FlagsForeignBiology": "Biologie Étrangère",
"SW5E.FlagsForeignBiologyHint": "Vous portez un appareil respiratoire parce que de nombreuses atmosphères dans la galaxie diffèrent de celle de votre monde natal. Si votre appareil est retiré alors que vous êtes dans un tel environnement, vous perdez connaissance.", "SW5E.FlagsForeignBiologyHint": "Vous portez un appareil respiratoire parce que de nombreuses atmosphères dans la galaxie diffèrent de celle de votre monde natal. Si votre appareil est retiré alors que vous êtes dans un tel environnement, vous perdez connaissance.",
"SW5E.FlagsFuryOfTheSmall": "Rage des Petits", "SW5E.FlagsFuryOfTheSmall": "Rage des Petits",
@ -382,7 +382,7 @@
"SW5E.FlagsMaintenanceMode": "Maintenance Mode", "SW5E.FlagsMaintenanceMode": "Maintenance Mode",
"SW5E.FlagsMaintenanceModeHint": "Plutôt que de dormir, vous entrez dans un état inactif pour effectuer une maintenance de routine pendant 4 heures par jour. Vous avez un désavantage aux tests de Sagesse (Perception) lors de la maintenance.", "SW5E.FlagsMaintenanceModeHint": "Plutôt que de dormir, vous entrez dans un état inactif pour effectuer une maintenance de routine pendant 4 heures par jour. Vous avez un désavantage aux tests de Sagesse (Perception) lors de la maintenance.",
"SW5E.FlagsMaskOfTheWild": "Cachette Naturelle", "SW5E.FlagsMaskOfTheWild": "Cachette Naturelle",
"SW5E.FlagsMaskOfTheWildHint":"Vous pouvez tenter de vous cacher dans une zone à visibilité réduite, comme en présence de branchages, de forte pluie, de neige qui tombe, de brume ou autre phénomène naturel.", "SW5E.FlagsMaskOfTheWildHint": "Vous pouvez tenter de vous cacher dans une zone à visibilité réduite, comme en présence de branchages, de forte pluie, de neige qui tombe, de brume ou autre phénomène naturel.",
"SW5E.FlagsMeleeCriticalDice": "Dé de dégâts pour les coups critiques au corps-à-corps", "SW5E.FlagsMeleeCriticalDice": "Dé de dégâts pour les coups critiques au corps-à-corps",
"SW5E.FlagsMeleeCriticalDiceHint": "Un certain nombre de dés de dégâts supplémentaires ont été ajoutés aux coups critiques des armes de corps-à-corps.", "SW5E.FlagsMeleeCriticalDiceHint": "Un certain nombre de dés de dégâts supplémentaires ont été ajoutés aux coups critiques des armes de corps-à-corps.",
"SW5E.FlagsMultipleHearts": "Plusieurs Coeurs", "SW5E.FlagsMultipleHearts": "Plusieurs Coeurs",
@ -812,8 +812,8 @@
"SW5E.SavingThrow": "Jet de sauvegarde", "SW5E.SavingThrow": "Jet de sauvegarde",
"SW5E.ScalingFormula": "Formule d'évolution", "SW5E.ScalingFormula": "Formule d'évolution",
"SW5E.SchoolDrk": "Obscur", "SW5E.SchoolDrk": "Obscur",
"SW5E.SchoolEnh": "Amélioration", "SW5E.SchoolEnh": "Amélioration",
"SW5E.SchoolLgt": "Lumineux", "SW5E.SchoolLgt": "Lumineux",
"SW5E.SchoolTec": "Technologie", "SW5E.SchoolTec": "Technologie",
"SW5E.SchoolUni": "Universel", "SW5E.SchoolUni": "Universel",
"SW5E.SenseBlindsight": "Vision aveugle", "SW5E.SenseBlindsight": "Vision aveugle",

1027
lang/it.json Normal file

File diff suppressed because it is too large Load diff

View file

@ -71,7 +71,7 @@
} }
// Movement Configuration // Movement Configuration
.movement { .movement, .hit-dice {
h4.attribute-name { h4.attribute-name {
position: relative; position: relative;
} }
@ -655,6 +655,15 @@
// Empty powerbook controls // Empty powerbook controls
.powerbook-empty .item-controls { flex: 1; } .powerbook-empty .item-controls { flex: 1; }
/* ----------------------------------------- */
/* Features Tab */
/* ----------------------------------------- */
// Original class icon
.features i.original-class {
color: #4b4a44
}
/* ----------------------------------------- */ /* ----------------------------------------- */
/* TinyMCE */ /* TinyMCE */
/* ----------------------------------------- */ /* ----------------------------------------- */
@ -678,4 +687,4 @@
overflow-y: auto; overflow-y: auto;
scrollbar-width: thin; scrollbar-width: thin;
} }
} }

View file

@ -10,9 +10,7 @@
.sw5e { .sw5e {
.window-content { .window-content {
background: @sheetBackground;
font-size: 13px; font-size: 13px;
color: @colorDark;
} }
/* ----------------------------------------- */ /* ----------------------------------------- */
@ -44,6 +42,8 @@
select:disabled, select:disabled,
textarea:disabled { textarea:disabled {
color: @colorOlive; color: @colorOlive;
border: 1px solid transparent !important;
outline: none !important;
&:hover, &:hover,
&:focus { &:focus {
box-shadow: none !important; box-shadow: none !important;
@ -58,28 +58,6 @@
border: @borderGroove; border: @borderGroove;
} }
// Checkbox Labels
// TODO: THIS CAN BE MOSTLY REMOVED NOW THAT IT IS IN CORE, see core forms.less
label.checkbox {
flex: auto;
padding: 0;
margin: 0;
height: 22px;
line-height: 22px;
font-size: 11px;
> input[type="checkbox"] {
width: 16px;
height: 16px;
margin: 0 2px 0 0;
position: relative;
top: 4px;
}
&.right > input[type="checkbox"] {
margin: 0 0 0 2px;
}
}
/* Form Groups */ /* Form Groups */
.form-group { .form-group {
label { label {
@ -98,11 +76,12 @@
// Stacked Groups // Stacked Groups
.form-group.stacked { .form-group.stacked {
label { > label {
flex: 0 0 100%; flex: 0 0 100%;
margin: 0; margin: 0;
} }
label.checkbox { label.checkbox,
label.radio {
flex: auto; flex: auto;
text-align: left; text-align: left;
} }
@ -131,6 +110,34 @@
} }
/* ----------------------------------------- */
/* Hit Dice Config Sheet Specifically */
/* ----------------------------------------- */
.sw5e.hd-config {
.form-group {
button.increment, button.decrement {
flex: 0 0 1rem;
line-height: 1rem;
}
button.decrement {
margin-right: 0;
}
span.sep {
margin: 0;
}
input {
flex: 0 0 2rem;
text-align: center;
margin-left: 2px;
margin-right: 2px;
}
}
}
/* ----------------------------------------- */ /* ----------------------------------------- */
/* Entity Sheets Specifically */ /* Entity Sheets Specifically */
/* ----------------------------------------- */ /* ----------------------------------------- */
@ -475,7 +482,7 @@
/* Trait Selector /* Trait Selector
/* ----------------------------------------- */ /* ----------------------------------------- */
#trait-selector { .trait-selector {
.trait-list { .trait-list {
list-style: none; list-style: none;
margin: 0; margin: 0;
@ -488,6 +495,59 @@
} }
} }
/* ----------------------------------------- */
/* Actor Type Config Sheet Specifically */
/* ----------------------------------------- */
.actor-type {
.trait-list {
display: flex;
flex-wrap: wrap;
li {
flex-basis: 50%;
flex-grow: 1;
}
li.form-group {
flex-basis: 100%;
}
}
label.radio {
display: flex;
flex: auto;
font-size: 12px;
line-height: 20px;
font-weight: normal;
> input[type="radio"] {
margin: 0 5px 0 0;
}
}
li.custom-type input[type="radio"] {
display: none;
}
}
/* ----------------------------------------- */
/* Add Feature Prompt Specifically */
/* ----------------------------------------- */
.sw5e.select-items-prompt {
.dialog-content {
margin-bottom: 1em;
}
.items-list {
margin-top: 0.5em;
}
.item-name > label, .item-image, input {
cursor: pointer;
}
.item-name > label {
align-items: center;
}
}
/* ----------------------------------------- */ /* ----------------------------------------- */
/* HUD /* HUD
/* ----------------------------------------- */ /* ----------------------------------------- */

View file

@ -89,7 +89,7 @@
// Custom Resources // Custom Resources
.resource .attribute-value { .resource .attribute-value {
input { > input {
flex: 0 0 25%; flex: 0 0 25%;
} }
label.recharge { label.recharge {
@ -99,6 +99,7 @@
font-size: 11px; font-size: 11px;
text-align: center; text-align: center;
color: @colorOlive; color: @colorOlive;
align-items: center;
input[type="checkbox"] { input[type="checkbox"] {
height: 14px; height: 14px;
width: 14px; width: 14px;

View file

@ -106,17 +106,17 @@
&:nth-child(even) { &:nth-child(even) {
width: 150px; width: 150px;
margin: 0.5em 0.5em; margin: 0.5em 0.5em;
padding: 0px 10px 0px 10px; padding: 0 10px 0 10px;
text-align: left; text-align: left;
} }
} }
thead { thead {
border-bottom: 0px; border-bottom: 0;
} }
th { th {
color: #000000; color: #000000;
text-shadow: none; text-shadow: none;
border-bottom: 0px; border-bottom: 0;
background-color: #bdc8cc; background-color: #bdc8cc;
text-transform: none; text-transform: none;
font-weight: bold; font-weight: bold;
@ -129,7 +129,7 @@
&:nth-child(even) { &:nth-child(even) {
width: 150px; width: 150px;
margin: 0.5em 0.5em; margin: 0.5em 0.5em;
padding: 0px 10px 0px 10px; padding: 0 10px 0 10px;
text-align: left; text-align: left;
} }
} }
@ -137,7 +137,7 @@
.medtable { .medtable {
table { table {
width: 500px; width: 500px;
border: 0px; border: 0;
margin: 0.5em 0.5em; margin: 0.5em 0.5em;
} }
td { td {
@ -149,17 +149,17 @@
&:nth-child(even) { &:nth-child(even) {
width: 450px; width: 450px;
margin: 0.5em 0.5em; margin: 0.5em 0.5em;
padding: 0px 10px 0px 0px; padding: 0 10px 0 0;
text-align: left; text-align: left;
} }
} }
thead { thead {
border-bottom: 0px; border-bottom: 0;
} }
th { th {
color: #000000; color: #000000;
text-shadow: none; text-shadow: none;
border-bottom: 0px; border-bottom: 0;
background-color: #bdc8cc; background-color: #bdc8cc;
text-transform: none; text-transform: none;
font-weight: bold; font-weight: bold;
@ -174,8 +174,8 @@
} }
.classtable { .classtable {
blockquote { blockquote {
border-left: 0px; border-left: 0;
border-right: 0px; border-right: 0;
background-color: #bdc8cc; background-color: #bdc8cc;
width: 600px; width: 600px;
h3 { h3 {
@ -189,8 +189,8 @@
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
background: rgba(0, 0, 0, 0.05); background: rgba(0, 0, 0, 0.05);
border-left: 0px; border-left: 0;
border-right: 0px; border-right: 0;
border-top: 0; border-top: 0;
border-bottom: 0; border-bottom: 0;
margin: 0.5em 0; margin: 0.5em 0;
@ -200,7 +200,7 @@
thead { thead {
color: #000000; color: #000000;
text-shadow: none; text-shadow: none;
border-bottom: 0px; border-bottom: 0;
background-color: #bdc8cc; background-color: #bdc8cc;
text-transform: none; text-transform: none;
font-style: normal; font-style: normal;
@ -209,7 +209,7 @@
th { th {
color: #000000; color: #000000;
text-shadow: none; text-shadow: none;
border-bottom: 0px; border-bottom: 0;
background-color: #bdc8cc; background-color: #bdc8cc;
text-transform: none; text-transform: none;
font-style: normal; font-style: normal;
@ -246,7 +246,7 @@
width: 100%; width: 100%;
line-height: 18px; line-height: 18px;
margin-bottom: 15px; margin-bottom: 15px;
border: 0 0 0 0; border: 0;
border-bottom: none; border-bottom: none;
overflow-x: auto; overflow-x: auto;
tbody { tbody {

View file

@ -30,5 +30,28 @@
.summary { .summary {
font-size: 18px; font-size: 18px;
li.creature-type {
display: flex;
justify-content: space-between;
width: 1em;
padding: 0 3px;
span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.config-button {
display: none;
font-size: 12px;
font-weight: normal;
line-height: 2em;
}
&:hover .config-button {
display: block;
}
}
} }
} }

View file

@ -140,6 +140,7 @@
height: auto; height: auto;
.russoOne(17px); .russoOne(17px);
line-height: 24px; line-height: 24px;
width: 100%;
} }
.proficiency { .proficiency {
@ -184,7 +185,7 @@
display: inline-block; display: inline-block;
text-align: right; text-align: right;
padding: 0px 3px; padding: 0 3px;
&:last-child { &:last-child {
text-align: left; text-align: left;
@ -779,7 +780,7 @@
display: block; display: block;
width: 100%; width: 100%;
text-align: right; text-align: right;
padding: 0px 3px; padding: 0 3px;
&:last-child { &:last-child {
text-align: left; text-align: left;
} }
@ -955,7 +956,7 @@
display: block; display: block;
width: 100%; width: 100%;
text-align: right; text-align: right;
padding: 0px 3px; padding: 0 3px;
&:last-child { &:last-child {
text-align: left; text-align: left;
} }
@ -1053,10 +1054,35 @@
h1.character-name { h1.character-name {
align-self: auto; align-self: auto;
} }
.npc-size { .npc-size, .creature-type {
.russoOne(18px); .russoOne(18px);
line-height: 28px; line-height: 28px;
} }
div.creature-type {
display: flex;
justify-content: space-between;
padding: 1px 4px;
border: 1px solid transparent;
overflow-x: auto;
span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.config-button {
display: none;
font-size: 12px;
font-weight: normal;
line-height: 2em;
}
&:hover .config-button {
display: block;
}
}
.attributes { .attributes {
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, 1fr);
footer { footer {

View file

@ -408,6 +408,9 @@
&.npc { &.npc {
.swalt-sheet { .swalt-sheet {
header { header {
div.creature-type:hover {
border-color: @inputBorderFocus;
}
.experience { .experience {
color: @actorProficiencyTextColor; color: @actorProficiencyTextColor;
} }

View file

@ -1,4 +1,4 @@
input[type="text"], input[type="number"], input[type="password"], input[type="date"], input[type="time"], select, textarea { input[type="text"], input[type="number"], input[type="password"], input[type="date"], input[type="time"], select, textarea, .roundTransition {
border-radius: 4px; border-radius: 4px;
transition: all 0.3s; transition: all 0.3s;
&:hover { &:hover {

View file

@ -166,6 +166,12 @@
.token-name { .token-name {
text-shadow: none; text-shadow: none;
} }
.ce-image-wrapper {
.token-image {
width: auto;
height: auto;
}
}
h4 { h4 {
color: @colorBlack; color: @colorBlack;
} }
@ -225,7 +231,7 @@
padding-bottom: 4px; padding-bottom: 4px;
.folder { .folder {
& > .folder-header { & > .folder-header {
line-height: default; line-height: initial;
padding: 0 0 0 8px; padding: 0 0 0 8px;
position: relative; position: relative;
border: none; border: none;
@ -379,4 +385,4 @@
padding: 0 8px; padding: 0 8px;
margin: 0 0 8px; margin: 0 0 8px;
} }
} }

View file

@ -302,7 +302,7 @@
} }
.folder { .folder {
& > .folder-header { & > .folder-header {
line-height: default; line-height: initial;
padding: 0 0 0 8px; padding: 0 0 0 8px;
position: relative; position: relative;
border: none; border: none;

File diff suppressed because it is too large Load diff

2126
module/actor/old_entity.js Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -6,136 +6,154 @@ import ActorSheet5e from "./base.js";
* @extends {ActorSheet5e} * @extends {ActorSheet5e}
*/ */
export default class ActorSheet5eNPCNew extends ActorSheet5e { export default class ActorSheet5eNPCNew extends ActorSheet5e {
/** @override */
/** @override */ get template() {
get template() { if (!game.user.isGM && this.actor.limited) return "systems/sw5e/templates/actors/newActor/limited-sheet.html";
if ( !game.user.isGM && this.actor.limited ) return "systems/sw5e/templates/actors/newActor/limited-sheet.html"; return `systems/sw5e/templates/actors/newActor/npc-sheet.html`;
return `systems/sw5e/templates/actors/newActor/npc-sheet.html`; }
} /** @override */
/** @override */ static get defaultOptions() {
static get defaultOptions() { return mergeObject(super.defaultOptions, {
return mergeObject(super.defaultOptions, { classes: ["sw5e", "sheet", "actor", "npc"],
classes: ["sw5e", "sheet", "actor", "npc"], width: 800,
width: 800, tabs: [
tabs: [{ {
navSelector: ".root-tabs", navSelector: ".root-tabs",
contentSelector: ".sheet-body", contentSelector: ".sheet-body",
initial: "attributes" initial: "attributes"
}], }
}); ]
} });
/* -------------------------------------------- */
/**
* Organize Owned Items for rendering the NPC sheet
* @private
*/
_prepareItems(data) {
// Categorize Items as Features and Powers
const features = {
weapons: { label: game.i18n.localize("SW5E.AttackPl"), items: [] , hasActions: true, dataset: {type: "weapon", "weapon-type": "natural"} },
actions: { label: game.i18n.localize("SW5E.ActionPl"), items: [] , hasActions: true, dataset: {type: "feat", "activation.type": "action"} },
passive: { label: game.i18n.localize("SW5E.Features"), items: [], dataset: {type: "feat"} },
equipment: { label: game.i18n.localize("SW5E.Inventory"), items: [], dataset: {type: "loot"}}
};
// Start by classifying items into groups for rendering
let [forcepowers, techpowers, other] = data.items.reduce((arr, item) => {
item.img = item.img || DEFAULT_TOKEN;
item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1);
item.hasUses = item.data.uses && (item.data.uses.max > 0);
item.isOnCooldown = item.data.recharge && !!item.data.recharge.value && (item.data.recharge.charged === false);
item.isDepleted = item.isOnCooldown && (item.data.uses.per && (item.data.uses.value > 0));
item.hasTarget = !!item.data.target && !(["none",""].includes(item.data.target.type));
if ( item.type === "power" && ["lgt", "drk", "uni"].includes(item.data.school) ) arr[0].push(item);
else if ( item.type === "power" && ["tec"].includes(item.data.school) ) arr[1].push(item);
else arr[2].push(item);
return arr;
}, [[], [], []]);
// Apply item filters
forcepowers = this._filterItems(forcepowers, this._filters.forcePowerbook);
techpowers = this._filterItems(techpowers, this._filters.techPowerbook);
other = this._filterItems(other, this._filters.features);
// Organize Powerbook
const forcePowerbook = this._preparePowerbook(data, forcepowers, "uni");
const techPowerbook = this._preparePowerbook(data, techpowers, "tec");
// Organize Features
for ( let item of other ) {
if ( item.type === "weapon" ) features.weapons.items.push(item);
else if ( item.type === "feat" ) {
if ( item.data.activation.type ) features.actions.items.push(item);
else features.passive.items.push(item);
}
else features.equipment.items.push(item);
} }
// Assign and return /* -------------------------------------------- */
data.features = Object.values(features);
data.forcePowerbook = forcePowerbook;
data.techPowerbook = techPowerbook;
}
/** @override */
static unsupportedItemTypes = new Set(["class"]);
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @override */ /**
getData() { * Organize Owned Items for rendering the NPC sheet
const data = super.getData(); * @private
*/
_prepareItems(data) {
// Categorize Items as Features and Powers
const features = {
weapons: {
label: game.i18n.localize("SW5E.AttackPl"),
items: [],
hasActions: true,
dataset: {"type": "weapon", "weapon-type": "natural"}
},
actions: {
label: game.i18n.localize("SW5E.ActionPl"),
items: [],
hasActions: true,
dataset: {"type": "feat", "activation.type": "action"}
},
passive: {label: game.i18n.localize("SW5E.Features"), items: [], dataset: {type: "feat"}},
equipment: {label: game.i18n.localize("SW5E.Inventory"), items: [], dataset: {type: "loot"}}
};
// Challenge Rating // Start by classifying items into groups for rendering
const cr = parseFloat(data.data.details.cr || 0); let [forcepowers, techpowers, other] = data.items.reduce(
const crLabels = {0: "0", 0.125: "1/8", 0.25: "1/4", 0.5: "1/2"}; (arr, item) => {
data.labels["cr"] = cr >= 1 ? String(cr) : crLabels[cr] || 1; item.img = item.img || CONST.DEFAULT_TOKEN;
return data; item.isStack = Number.isNumeric(item.data.quantity) && item.data.quantity !== 1;
} item.hasUses = item.data.uses && item.data.uses.max > 0;
item.isOnCooldown =
item.data.recharge && !!item.data.recharge.value && item.data.recharge.charged === false;
item.isDepleted = item.isOnCooldown && item.data.uses.per && item.data.uses.value > 0;
item.hasTarget = !!item.data.target && !["none", ""].includes(item.data.target.type);
if (item.type === "power" && ["lgt", "drk", "uni"].includes(item.data.school)) arr[0].push(item);
else if (item.type === "power" && ["tec"].includes(item.data.school)) arr[1].push(item);
else arr[2].push(item);
return arr;
},
[[], [], []]
);
/* -------------------------------------------- */ // Apply item filters
/* Object Updates */ forcepowers = this._filterItems(forcepowers, this._filters.forcePowerbook);
/* -------------------------------------------- */ techpowers = this._filterItems(techpowers, this._filters.techPowerbook);
other = this._filterItems(other, this._filters.features);
/** @override */ // Organize Powerbook
_updateObject(event, formData) { const forcePowerbook = this._preparePowerbook(data, forcepowers, "uni");
const techPowerbook = this._preparePowerbook(data, techpowers, "tec");
// Format NPC Challenge Rating // Organize Features
const crs = {"1/8": 0.125, "1/4": 0.25, "1/2": 0.5}; for (let item of other) {
let crv = "data.details.cr"; if (item.type === "weapon") features.weapons.items.push(item);
let cr = formData[crv]; else if (item.type === "feat") {
cr = crs[cr] || parseFloat(cr); if (item.data.activation.type) features.actions.items.push(item);
if ( cr ) formData[crv] = cr < 1 ? cr : parseInt(cr); else features.passive.items.push(item);
} else features.equipment.items.push(item);
}
// Parent ActorSheet update steps // Assign and return
super._updateObject(event, formData); data.features = Object.values(features);
} data.forcePowerbook = forcePowerbook;
data.techPowerbook = techPowerbook;
}
/* -------------------------------------------- */ /* -------------------------------------------- */
/* Event Listeners and Handlers */
/* -------------------------------------------- */
/** @override */ /** @inheritdoc */
activateListeners(html) { getData(options) {
super.activateListeners(html); const data = super.getData(options);
html.find(".health .rollable").click(this._onRollHPFormula.bind(this));
}
/* -------------------------------------------- */ // Challenge Rating
const cr = parseFloat(data.data.details.cr || 0);
const crLabels = {0: "0", 0.125: "1/8", 0.25: "1/4", 0.5: "1/2"};
data.labels["cr"] = cr >= 1 ? String(cr) : crLabels[cr] || 1;
/** // Creature Type
* Handle rolling NPC health values using the provided formula data.labels["type"] = this.actor.labels.creatureType;
* @param {Event} event The original click event return data;
* @private }
*/
_onRollHPFormula(event) { /* -------------------------------------------- */
event.preventDefault(); /* Object Updates */
const formula = this.actor.data.data.attributes.hp.formula; /* -------------------------------------------- */
if ( !formula ) return;
const hp = new Roll(formula).roll().total; /** @override */
AudioHelper.play({src: CONFIG.sounds.dice}); async _updateObject(event, formData) {
this.actor.update({"data.attributes.hp.value": hp, "data.attributes.hp.max": hp}); // Format NPC Challenge Rating
} const crs = {"1/8": 0.125, "1/4": 0.25, "1/2": 0.5};
let crv = "data.details.cr";
let cr = formData[crv];
cr = crs[cr] || parseFloat(cr);
if (cr) formData[crv] = cr < 1 ? cr : parseInt(cr);
// Parent ActorSheet update steps
return super._updateObject(event, formData);
}
/* -------------------------------------------- */
/* Event Listeners and Handlers */
/* -------------------------------------------- */
/** @override */
activateListeners(html) {
super.activateListeners(html);
html.find(".health .rollable").click(this._onRollHPFormula.bind(this));
}
/* -------------------------------------------- */
/**
* Handle rolling NPC health values using the provided formula
* @param {Event} event The original click event
* @private
*/
_onRollHPFormula(event) {
event.preventDefault();
const formula = this.actor.data.data.attributes.hp.formula;
if (!formula) return;
const hp = new Roll(formula).roll().total;
AudioHelper.play({src: CONFIG.sounds.dice});
this.actor.update({"data.attributes.hp.value": hp, "data.attributes.hp.max": hp});
}
} }

View file

@ -0,0 +1,169 @@
import ActorSheet5e from "./base.js";
/**
* An Actor sheet for starships in the SW5E system.
* Extends the base ActorSheet5e class.
* @extends {ActorSheet5e}
*/
export default class ActorSheet5eStarship extends ActorSheet5e {
/** @override */
get template() {
if (!game.user.isGM && this.actor.limited) return "systems/sw5e/templates/actors/newActor/limited-sheet.html";
return `systems/sw5e/templates/actors/newActor/starship.html`;
}
/** @override */
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
classes: ["sw5e", "sheet", "actor", "starship"],
width: 800,
tabs: [
{
navSelector: ".root-tabs",
contentSelector: ".sheet-body",
initial: "attributes"
}
]
});
}
/* -------------------------------------------- */
/**
* Organize Owned Items for rendering the starship sheet
* @private
*/
_prepareItems(data) {
// Categorize Items as Features and Powers
const features = {
weapons: {
label: game.i18n.localize("SW5E.ItemTypeWeaponPl"),
items: [],
hasActions: true,
dataset: {"type": "weapon", "weapon-type": "natural"}
},
passive: {label: game.i18n.localize("SW5E.Features"), items: [], dataset: {type: "feat"}},
equipment: {label: game.i18n.localize("SW5E.StarshipEquipment"), items: [], dataset: {type: "equipment"}},
starshipfeatures: {
label: game.i18n.localize("SW5E.StarshipfeaturePl"),
items: [],
hasActions: true,
dataset: {type: "starshipfeature"}
},
starshipmods: {
label: game.i18n.localize("SW5E.StarshipmodPl"),
items: [],
hasActions: false,
dataset: {type: "starshipmod"}
}
};
// Start by classifying items into groups for rendering
let [forcepowers, techpowers, other] = data.items.reduce(
(arr, item) => {
item.img = item.img || CONST.DEFAULT_TOKEN;
item.isStack = Number.isNumeric(item.data.quantity) && item.data.quantity !== 1;
item.hasUses = item.data.uses && item.data.uses.max > 0;
item.isOnCooldown =
item.data.recharge && !!item.data.recharge.value && item.data.recharge.charged === false;
item.isDepleted = item.isOnCooldown && item.data.uses.per && item.data.uses.value > 0;
item.hasTarget = !!item.data.target && !["none", ""].includes(item.data.target.type);
if (item.type === "power" && ["lgt", "drk", "uni"].includes(item.data.school)) arr[0].push(item);
else if (item.type === "power" && ["tec"].includes(item.data.school)) arr[1].push(item);
else arr[2].push(item);
return arr;
},
[[], [], []]
);
// Apply item filters
forcepowers = this._filterItems(forcepowers, this._filters.forcePowerbook);
techpowers = this._filterItems(techpowers, this._filters.techPowerbook);
other = this._filterItems(other, this._filters.features);
// Organize Powerbook
// const forcePowerbook = this._preparePowerbook(data, forcepowers, "uni");
// const techPowerbook = this._preparePowerbook(data, techpowers, "tec");
// Organize Features
for (let item of other) {
if (item.type === "weapon") features.weapons.items.push(item);
else if (item.type === "feat") {
if (item.data.activation.type) features.actions.items.push(item);
else features.passive.items.push(item);
} else if (item.type === "starshipfeature") {
features.starshipfeatures.items.push(item);
} else if (item.type === "starshipmod") {
features.starshipmods.items.push(item);
} else features.equipment.items.push(item);
}
// Assign and return
data.features = Object.values(features);
// data.forcePowerbook = forcePowerbook;
// data.techPowerbook = techPowerbook;
}
/* -------------------------------------------- */
/** @override */
getData(options) {
const data = super.getData(options);
// Add Size info
data.isTiny = data.actor.data.traits.size === "tiny";
data.isSmall = data.actor.data.traits.size === "sm";
data.isMedium = data.actor.data.traits.size === "med";
data.isLarge = data.actor.data.traits.size === "lg";
data.isHuge = data.actor.data.traits.size === "huge";
data.isGargantuan = data.actor.data.traits.size === "grg";
// Challenge Rating
const cr = parseFloat(data.data.details.cr || 0);
const crLabels = {0: "0", 0.125: "1/8", 0.25: "1/4", 0.5: "1/2"};
data.labels["cr"] = cr >= 1 ? String(cr) : crLabels[cr] || 1;
return data;
}
/* -------------------------------------------- */
/* Object Updates */
/* -------------------------------------------- */
/** @override */
async _updateObject(event, formData) {
// Format NPC Challenge Rating
const crs = {"1/8": 0.125, "1/4": 0.25, "1/2": 0.5};
let crv = "data.details.cr";
let cr = formData[crv];
cr = crs[cr] || parseFloat(cr);
if (cr) formData[crv] = cr < 1 ? cr : parseInt(cr);
// Parent ActorSheet update steps
return super._updateObject(event, formData);
}
/* -------------------------------------------- */
/* Event Listeners and Handlers */
/* -------------------------------------------- */
/** @override */
activateListeners(html) {
super.activateListeners(html);
html.find(".health .rollable").click(this._onRollHPFormula.bind(this));
}
/* -------------------------------------------- */
/**
* Handle rolling NPC health values using the provided formula
* @param {Event} event The original click event
* @private
*/
_onRollHPFormula(event) {
event.preventDefault();
const formula = this.actor.data.data.attributes.hp.formula;
if (!formula) return;
const hp = new Roll(formula).roll().total;
AudioHelper.play({src: CONFIG.sounds.dice});
this.actor.update({"data.attributes.hp.value": hp, "data.attributes.hp.max": hp});
}
}

View file

@ -6,380 +6,427 @@ import ActorSheet5e from "./base.js";
* @type {ActorSheet5e} * @type {ActorSheet5e}
*/ */
export default class ActorSheet5eVehicle extends ActorSheet5e { export default class ActorSheet5eVehicle extends ActorSheet5e {
/** /**
* Define default rendering options for the Vehicle sheet. * Define default rendering options for the Vehicle sheet.
* @returns {Object} * @returns {Object}
*/ */
static get defaultOptions() { static get defaultOptions() {
return mergeObject(super.defaultOptions, { return mergeObject(super.defaultOptions, {
classes: ["sw5e", "sheet", "actor", "vehicle"], classes: ["sw5e", "sheet", "actor", "vehicle"],
width: 605, width: 605,
height: 680 height: 680
}); });
}
/* -------------------------------------------- */
/**
* Creates a new cargo entry for a vehicle Actor.
*/
static get newCargo() {
return {
name: '',
quantity: 1
};
}
/* -------------------------------------------- */
/**
* Compute the total weight of the vehicle's cargo.
* @param {Number} totalWeight The cumulative item weight from inventory items
* @param {Object} actorData The data object for the Actor being rendered
* @returns {{max: number, value: number, pct: number}}
* @private
*/
_computeEncumbrance(totalWeight, actorData) {
// Compute currency weight
const totalCoins = Object.values(actorData.data.currency).reduce((acc, denom) => acc + denom, 0);
totalWeight += totalCoins / CONFIG.SW5E.encumbrance.currencyPerWeight;
// Vehicle weights are an order of magnitude greater.
totalWeight /= CONFIG.SW5E.encumbrance.vehicleWeightMultiplier;
// Compute overall encumbrance
const max = actorData.data.attributes.capacity.cargo;
const pct = Math.clamped((totalWeight * 100) / max, 0, 100);
return {value: totalWeight.toNearest(0.1), max, pct};
}
/* -------------------------------------------- */
/** @override */
_getMovementSpeed(actorData, largestPrimary=true) {
return super._getMovementSpeed(actorData, largestPrimary);
}
/* -------------------------------------------- */
/**
* Prepare items that are mounted to a vehicle and require one or more crew
* to operate.
* @private
*/
_prepareCrewedItem(item) {
// Determine crewed status
const isCrewed = item.data.crewed;
item.toggleClass = isCrewed ? 'active' : '';
item.toggleTitle = game.i18n.localize(`SW5E.${isCrewed ? 'Crewed' : 'Uncrewed'}`);
// Handle crew actions
if (item.type === 'feat' && item.data.activation.type === 'crew') {
item.crew = item.data.activation.cost;
item.cover = game.i18n.localize(`SW5E.${item.data.cover ? 'CoverTotal' : 'None'}`);
if (item.data.cover === .5) item.cover = '½';
else if (item.data.cover === .75) item.cover = '¾';
else if (item.data.cover === null) item.cover = '—';
if (item.crew < 1 || item.crew === null) item.crew = '—';
} }
// Prepare vehicle weapons /* -------------------------------------------- */
if (item.type === 'equipment' || item.type === 'weapon') {
item.threshold = item.data.hp.dt ? item.data.hp.dt : '—'; /** @override */
static unsupportedItemTypes = new Set(["class"]);
/* -------------------------------------------- */
/**
* Creates a new cargo entry for a vehicle Actor.
*/
static get newCargo() {
return {
name: "",
quantity: 1
};
} }
}
/* -------------------------------------------- */ /* -------------------------------------------- */
/** /**
* Organize Owned Items for rendering the Vehicle sheet. * Compute the total weight of the vehicle's cargo.
* @private * @param {Number} totalWeight The cumulative item weight from inventory items
*/ * @param {Object} actorData The data object for the Actor being rendered
_prepareItems(data) { * @returns {{max: number, value: number, pct: number}}
const cargoColumns = [{ * @private
label: game.i18n.localize('SW5E.Quantity'), */
css: 'item-qty', _computeEncumbrance(totalWeight, actorData) {
property: 'quantity', // Compute currency weight
editable: 'Number' const totalCoins = Object.values(actorData.data.currency).reduce((acc, denom) => acc + denom, 0);
}]; totalWeight += totalCoins / CONFIG.SW5E.encumbrance.currencyPerWeight;
const equipmentColumns = [{ // Vehicle weights are an order of magnitude greater.
label: game.i18n.localize('SW5E.Quantity'), totalWeight /= CONFIG.SW5E.encumbrance.vehicleWeightMultiplier;
css: 'item-qty',
property: 'data.quantity'
}, {
label: game.i18n.localize('SW5E.AC'),
css: 'item-ac',
property: 'data.armor.value'
}, {
label: game.i18n.localize('SW5E.HP'),
css: 'item-hp',
property: 'data.hp.value',
editable: 'Number'
}, {
label: game.i18n.localize('SW5E.Threshold'),
css: 'item-threshold',
property: 'threshold'
}];
const features = { // Compute overall encumbrance
actions: { const max = actorData.data.attributes.capacity.cargo;
label: game.i18n.localize('SW5E.ActionPl'), const pct = Math.clamped((totalWeight * 100) / max, 0, 100);
items: [], return {value: totalWeight.toNearest(0.1), max, pct};
crewable: true, }
dataset: {type: 'feat', 'activation.type': 'crew'},
columns: [{
label: game.i18n.localize('SW5E.VehicleCrew'),
css: 'item-crew',
property: 'crew'
}, {
label: game.i18n.localize('SW5E.Cover'),
css: 'item-cover',
property: 'cover'
}]
},
equipment: {
label: game.i18n.localize('SW5E.ItemTypeEquipment'),
items: [],
crewable: true,
dataset: {type: 'equipment', 'armor.type': 'vehicle'},
columns: equipmentColumns
},
passive: {
label: game.i18n.localize('SW5E.Features'),
items: [],
dataset: {type: 'feat'}
},
reactions: {
label: game.i18n.localize('SW5E.ReactionPl'),
items: [],
dataset: {type: 'feat', 'activation.type': 'reaction'}
},
weapons: {
label: game.i18n.localize('SW5E.ItemTypeWeaponPl'),
items: [],
crewable: true,
dataset: {type: 'weapon', 'weapon-type': 'siege'},
columns: equipmentColumns
}
};
const cargo = { /* -------------------------------------------- */
crew: {
label: game.i18n.localize('SW5E.VehicleCrew'),
items: data.data.cargo.crew,
css: 'cargo-row crew',
editableName: true,
dataset: {type: 'crew'},
columns: cargoColumns
},
passengers: {
label: game.i18n.localize('SW5E.VehiclePassengers'),
items: data.data.cargo.passengers,
css: 'cargo-row passengers',
editableName: true,
dataset: {type: 'passengers'},
columns: cargoColumns
},
cargo: {
label: game.i18n.localize('SW5E.VehicleCargo'),
items: [],
dataset: {type: 'loot'},
columns: [{
label: game.i18n.localize('SW5E.Quantity'),
css: 'item-qty',
property: 'data.quantity',
editable: 'Number'
}, {
label: game.i18n.localize('SW5E.Price'),
css: 'item-price',
property: 'data.price',
editable: 'Number'
}, {
label: game.i18n.localize('SW5E.Weight'),
css: 'item-weight',
property: 'data.weight',
editable: 'Number'
}]
}
};
let totalWeight = 0; /** @override */
for (const item of data.items) { _getMovementSpeed(actorData, largestPrimary = true) {
this._prepareCrewedItem(item); return super._getMovementSpeed(actorData, largestPrimary);
if (item.type === 'weapon') features.weapons.items.push(item); }
else if (item.type === 'equipment') features.equipment.items.push(item);
else if (item.type === 'loot') { /* -------------------------------------------- */
totalWeight += (item.data.weight || 0) * item.data.quantity;
cargo.cargo.items.push(item); /**
} * Prepare items that are mounted to a vehicle and require one or more crew
else if (item.type === 'feat') { * to operate.
if (!item.data.activation.type || item.data.activation.type === 'none') { * @private
features.passive.items.push(item); */
_prepareCrewedItem(item) {
// Determine crewed status
const isCrewed = item.data.crewed;
item.toggleClass = isCrewed ? "active" : "";
item.toggleTitle = game.i18n.localize(`SW5E.${isCrewed ? "Crewed" : "Uncrewed"}`);
// Handle crew actions
if (item.type === "feat" && item.data.activation.type === "crew") {
item.crew = item.data.activation.cost;
item.cover = game.i18n.localize(`SW5E.${item.data.cover ? "CoverTotal" : "None"}`);
if (item.data.cover === 0.5) item.cover = "½";
else if (item.data.cover === 0.75) item.cover = "¾";
else if (item.data.cover === null) item.cover = "—";
if (item.crew < 1 || item.crew === null) item.crew = "—";
}
// Prepare vehicle weapons
if (item.type === "equipment" || item.type === "weapon") {
item.threshold = item.data.hp.dt ? item.data.hp.dt : "—";
} }
else if (item.data.activation.type === 'reaction') features.reactions.items.push(item);
else features.actions.items.push(item);
}
} }
data.features = Object.values(features); /* -------------------------------------------- */
data.cargo = Object.values(cargo);
data.data.attributes.encumbrance = this._computeEncumbrance(totalWeight, data);
}
/* -------------------------------------------- */ /**
/* Event Listeners and Handlers */ * Organize Owned Items for rendering the Vehicle sheet.
/* -------------------------------------------- */ * @private
*/
_prepareItems(data) {
const cargoColumns = [
{
label: game.i18n.localize("SW5E.Quantity"),
css: "item-qty",
property: "quantity",
editable: "Number"
}
];
/** @override */ const equipmentColumns = [
activateListeners(html) { {
super.activateListeners(html); label: game.i18n.localize("SW5E.Quantity"),
if (!this.options.editable) return; css: "item-qty",
property: "data.quantity"
},
{
label: game.i18n.localize("SW5E.AC"),
css: "item-ac",
property: "data.armor.value"
},
{
label: game.i18n.localize("SW5E.HP"),
css: "item-hp",
property: "data.hp.value",
editable: "Number"
},
{
label: game.i18n.localize("SW5E.Threshold"),
css: "item-threshold",
property: "threshold"
}
];
html.find('.item-toggle').click(this._onToggleItem.bind(this)); const features = {
html.find('.item-hp input') actions: {
.click(evt => evt.target.select()) label: game.i18n.localize("SW5E.ActionPl"),
.change(this._onHPChange.bind(this)); items: [],
crewable: true,
dataset: {"type": "feat", "activation.type": "crew"},
columns: [
{
label: game.i18n.localize("SW5E.VehicleCrew"),
css: "item-crew",
property: "crew"
},
{
label: game.i18n.localize("SW5E.Cover"),
css: "item-cover",
property: "cover"
}
]
},
equipment: {
label: game.i18n.localize("SW5E.ItemTypeEquipment"),
items: [],
crewable: true,
dataset: {"type": "equipment", "armor.type": "vehicle"},
columns: equipmentColumns
},
passive: {
label: game.i18n.localize("SW5E.Features"),
items: [],
dataset: {type: "feat"}
},
reactions: {
label: game.i18n.localize("SW5E.ReactionPl"),
items: [],
dataset: {"type": "feat", "activation.type": "reaction"}
},
weapons: {
label: game.i18n.localize("SW5E.ItemTypeWeaponPl"),
items: [],
crewable: true,
dataset: {"type": "weapon", "weapon-type": "siege"},
columns: equipmentColumns
}
};
html.find('.item:not(.cargo-row) input[data-property]') const cargo = {
.click(evt => evt.target.select()) crew: {
.change(this._onEditInSheet.bind(this)); label: game.i18n.localize("SW5E.VehicleCrew"),
items: data.data.cargo.crew,
css: "cargo-row crew",
editableName: true,
dataset: {type: "crew"},
columns: cargoColumns
},
passengers: {
label: game.i18n.localize("SW5E.VehiclePassengers"),
items: data.data.cargo.passengers,
css: "cargo-row passengers",
editableName: true,
dataset: {type: "passengers"},
columns: cargoColumns
},
cargo: {
label: game.i18n.localize("SW5E.VehicleCargo"),
items: [],
dataset: {type: "loot"},
columns: [
{
label: game.i18n.localize("SW5E.Quantity"),
css: "item-qty",
property: "data.quantity",
editable: "Number"
},
{
label: game.i18n.localize("SW5E.Price"),
css: "item-price",
property: "data.price",
editable: "Number"
},
{
label: game.i18n.localize("SW5E.Weight"),
css: "item-weight",
property: "data.weight",
editable: "Number"
}
]
}
};
html.find('.cargo-row input') // Classify items owned by the vehicle and compute total cargo weight
.click(evt => evt.target.select()) let totalWeight = 0;
.change(this._onCargoRowChange.bind(this)); for (const item of data.items) {
this._prepareCrewedItem(item);
if (this.actor.data.data.attributes.actions.stations) { // Handle cargo explicitly
html.find('.counter.actions, .counter.action-thresholds').hide(); const isCargo = item.flags.sw5e?.vehicleCargo === true;
} if (isCargo) {
} totalWeight += (item.data.weight || 0) * item.data.quantity;
cargo.cargo.items.push(item);
continue;
}
/* -------------------------------------------- */ // Handle non-cargo item types
switch (item.type) {
case "weapon":
features.weapons.items.push(item);
break;
case "equipment":
features.equipment.items.push(item);
break;
case "feat":
if (!item.data.activation.type || item.data.activation.type === "none")
features.passive.items.push(item);
else if (item.data.activation.type === "reaction") features.reactions.items.push(item);
else features.actions.items.push(item);
break;
default:
totalWeight += (item.data.weight || 0) * item.data.quantity;
cargo.cargo.items.push(item);
}
}
/** // Update the rendering context data
* Handle saving a cargo row (i.e. crew or passenger) in-sheet. data.features = Object.values(features);
* @param event {Event} data.cargo = Object.values(cargo);
* @returns {Promise<Actor>|null} data.data.attributes.encumbrance = this._computeEncumbrance(totalWeight, data);
* @private
*/
_onCargoRowChange(event) {
event.preventDefault();
const target = event.currentTarget;
const row = target.closest('.item');
const idx = Number(row.dataset.itemId);
const property = row.classList.contains('crew') ? 'crew' : 'passengers';
// Get the cargo entry
const cargo = duplicate(this.actor.data.data.cargo[property]);
const entry = cargo[idx];
if (!entry) return null;
// Update the cargo value
const key = target.dataset.property || 'name';
const type = target.dataset.dtype;
let value = target.value;
if (type === 'Number') value = Number(value);
entry[key] = value;
// Perform the Actor update
return this.actor.update({[`data.cargo.${property}`]: cargo});
}
/* -------------------------------------------- */
/**
* Handle editing certain values like quantity, price, and weight in-sheet.
* @param event {Event}
* @returns {Promise<Item>}
* @private
*/
_onEditInSheet(event) {
event.preventDefault();
const itemID = event.currentTarget.closest('.item').dataset.itemId;
const item = this.actor.items.get(itemID);
const property = event.currentTarget.dataset.property;
const type = event.currentTarget.dataset.dtype;
let value = event.currentTarget.value;
switch (type) {
case 'Number': value = parseInt(value); break;
case 'Boolean': value = value === 'true'; break;
}
return item.update({[`${property}`]: value});
}
/* -------------------------------------------- */
/**
* Handle creating a new crew or passenger row.
* @param event {Event}
* @returns {Promise<Actor|Item>}
* @private
*/
_onItemCreate(event) {
event.preventDefault();
const target = event.currentTarget;
const type = target.dataset.type;
if (type === 'crew' || type === 'passengers') {
const cargo = duplicate(this.actor.data.data.cargo[type]);
cargo.push(this.constructor.newCargo);
return this.actor.update({[`data.cargo.${type}`]: cargo});
}
return super._onItemCreate(event);
}
/* -------------------------------------------- */
/**
* Handle deleting a crew or passenger row.
* @param event {Event}
* @returns {Promise<Actor|Item>}
* @private
*/
_onItemDelete(event) {
event.preventDefault();
const row = event.currentTarget.closest('.item');
if (row.classList.contains('cargo-row')) {
const idx = Number(row.dataset.itemId);
const type = row.classList.contains('crew') ? 'crew' : 'passengers';
const cargo = duplicate(this.actor.data.data.cargo[type]).filter((_, i) => i !== idx);
return this.actor.update({[`data.cargo.${type}`]: cargo});
} }
return super._onItemDelete(event); /* -------------------------------------------- */
} /* Event Listeners and Handlers */
/* -------------------------------------------- */
/* -------------------------------------------- */ /** @override */
activateListeners(html) {
super.activateListeners(html);
if (!this.isEditable) return;
/** html.find(".item-toggle").click(this._onToggleItem.bind(this));
* Special handling for editing HP to clamp it within appropriate range. html.find(".item-hp input")
* @param event {Event} .click((evt) => evt.target.select())
* @returns {Promise<Item>} .change(this._onHPChange.bind(this));
* @private
*/
_onHPChange(event) {
event.preventDefault();
const itemID = event.currentTarget.closest('.item').dataset.itemId;
const item = this.actor.items.get(itemID);
const hp = Math.clamped(0, parseInt(event.currentTarget.value), item.data.data.hp.max);
event.currentTarget.value = hp;
return item.update({'data.hp.value': hp});
}
/* -------------------------------------------- */ html.find(".item:not(.cargo-row) input[data-property]")
.click((evt) => evt.target.select())
.change(this._onEditInSheet.bind(this));
/** html.find(".cargo-row input")
* Handle toggling an item's crewed status. .click((evt) => evt.target.select())
* @param event {Event} .change(this._onCargoRowChange.bind(this));
* @returns {Promise<Item>}
* @private if (this.actor.data.data.attributes.actions.stations) {
*/ html.find(".counter.actions, .counter.action-thresholds").hide();
_onToggleItem(event) { }
event.preventDefault(); }
const itemID = event.currentTarget.closest('.item').dataset.itemId;
const item = this.actor.items.get(itemID); /* -------------------------------------------- */
const crewed = !!item.data.data.crewed;
return item.update({'data.crewed': !crewed}); /**
} * Handle saving a cargo row (i.e. crew or passenger) in-sheet.
}; * @param event {Event}
* @returns {Promise<Actor>|null}
* @private
*/
_onCargoRowChange(event) {
event.preventDefault();
const target = event.currentTarget;
const row = target.closest(".item");
const idx = Number(row.dataset.itemId);
const property = row.classList.contains("crew") ? "crew" : "passengers";
// Get the cargo entry
const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[property]);
const entry = cargo[idx];
if (!entry) return null;
// Update the cargo value
const key = target.dataset.property || "name";
const type = target.dataset.dtype;
let value = target.value;
if (type === "Number") value = Number(value);
entry[key] = value;
// Perform the Actor update
return this.actor.update({[`data.cargo.${property}`]: cargo});
}
/* -------------------------------------------- */
/**
* Handle editing certain values like quantity, price, and weight in-sheet.
* @param event {Event}
* @returns {Promise<Item>}
* @private
*/
_onEditInSheet(event) {
event.preventDefault();
const itemID = event.currentTarget.closest(".item").dataset.itemId;
const item = this.actor.items.get(itemID);
const property = event.currentTarget.dataset.property;
const type = event.currentTarget.dataset.dtype;
let value = event.currentTarget.value;
switch (type) {
case "Number":
value = parseInt(value);
break;
case "Boolean":
value = value === "true";
break;
}
return item.update({[`${property}`]: value});
}
/* -------------------------------------------- */
/**
* Handle creating a new crew or passenger row.
* @param event {Event}
* @returns {Promise<Actor|Item>}
* @private
*/
_onItemCreate(event) {
event.preventDefault();
const target = event.currentTarget;
const type = target.dataset.type;
if (type === "crew" || type === "passengers") {
const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[type]);
cargo.push(this.constructor.newCargo);
return this.actor.update({[`data.cargo.${type}`]: cargo});
}
return super._onItemCreate(event);
}
/* -------------------------------------------- */
/**
* Handle deleting a crew or passenger row.
* @param event {Event}
* @returns {Promise<Actor|Item>}
* @private
*/
_onItemDelete(event) {
event.preventDefault();
const row = event.currentTarget.closest(".item");
if (row.classList.contains("cargo-row")) {
const idx = Number(row.dataset.itemId);
const type = row.classList.contains("crew") ? "crew" : "passengers";
const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[type]).filter((_, i) => i !== idx);
return this.actor.update({[`data.cargo.${type}`]: cargo});
}
return super._onItemDelete(event);
}
/* -------------------------------------------- */
/** @override */
async _onDropItemCreate(itemData) {
const cargoTypes = ["weapon", "equipment", "consumable", "tool", "loot", "backpack"];
const isCargo = cargoTypes.includes(itemData.type) && this._tabs[0].active === "cargo";
foundry.utils.setProperty(itemData, "flags.sw5e.vehicleCargo", isCargo);
return super._onDropItemCreate(itemData);
}
/* -------------------------------------------- */
/**
* Special handling for editing HP to clamp it within appropriate range.
* @param event {Event}
* @returns {Promise<Item>}
* @private
*/
_onHPChange(event) {
event.preventDefault();
const itemID = event.currentTarget.closest(".item").dataset.itemId;
const item = this.actor.items.get(itemID);
const hp = Math.clamped(0, parseInt(event.currentTarget.value), item.data.data.hp.max);
event.currentTarget.value = hp;
return item.update({"data.hp.value": hp});
}
/* -------------------------------------------- */
/**
* Handle toggling an item's crewed status.
* @param event {Event}
* @returns {Promise<Item>}
* @private
*/
_onToggleItem(event) {
event.preventDefault();
const itemID = event.currentTarget.closest(".item").dataset.itemId;
const item = this.actor.items.get(itemID);
const crewed = !!item.data.data.crewed;
return item.update({"data.crewed": !crewed});
}
}

File diff suppressed because it is too large Load diff

View file

@ -7,292 +7,362 @@ import Actor5e from "../../entity.js";
* @type {ActorSheet5e} * @type {ActorSheet5e}
*/ */
export default class ActorSheet5eCharacter extends ActorSheet5e { export default class ActorSheet5eCharacter extends ActorSheet5e {
/**
* Define default rendering options for the NPC sheet
* @return {Object}
*/
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
classes: ["sw5e", "sheet", "actor", "character"],
width: 720,
height: 736
});
}
/** /* -------------------------------------------- */
* Define default rendering options for the NPC sheet
* @return {Object}
*/
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
classes: ["sw5e", "sheet", "actor", "character"],
width: 720,
height: 736
});
}
/* -------------------------------------------- */ /**
* Add some extra data when rendering the sheet to reduce the amount of logic required within the template.
*/
getData() {
const sheetData = super.getData();
/** // Temporary HP
* Add some extra data when rendering the sheet to reduce the amount of logic required within the template. let hp = sheetData.data.attributes.hp;
*/ if (hp.temp === 0) delete hp.temp;
getData() { if (hp.tempmax === 0) delete hp.tempmax;
const sheetData = super.getData();
// Temporary HP // Resources
let hp = sheetData.data.attributes.hp; sheetData["resources"] = ["primary", "secondary", "tertiary"].reduce((arr, r) => {
if (hp.temp === 0) delete hp.temp; const res = sheetData.data.resources[r] || {};
if (hp.tempmax === 0) delete hp.tempmax; res.name = r;
res.placeholder = game.i18n.localize("SW5E.Resource" + r.titleCase());
if (res && res.value === 0) delete res.value;
if (res && res.max === 0) delete res.max;
return arr.concat([res]);
}, []);
// Resources // Experience Tracking
sheetData["resources"] = ["primary", "secondary", "tertiary"].reduce((arr, r) => { sheetData["disableExperience"] = game.settings.get("sw5e", "disableExperienceTracking");
const res = sheetData.data.resources[r] || {}; sheetData["classLabels"] = this.actor.itemTypes.class.map((c) => c.name).join(", ");
res.name = r; sheetData["multiclassLabels"] = this.actor.itemTypes.class
res.placeholder = game.i18n.localize("SW5E.Resource"+r.titleCase()); .map((c) => {
if (res && res.value === 0) delete res.value; return [c.data.data.archetype, c.name, c.data.data.levels].filterJoin(" ");
if (res && res.max === 0) delete res.max; })
return arr.concat([res]); .join(", ");
}, []);
// Experience Tracking // Return data for rendering
sheetData["disableExperience"] = game.settings.get("sw5e", "disableExperienceTracking"); return sheetData;
sheetData["classLabels"] = this.actor.itemTypes.class.map(c => c.name).join(", "); }
sheetData["multiclassLabels"] = this.actor.itemTypes.class.map(c => {
return [c.data.data.archetype, c.name, c.data.data.levels].filterJoin(' ')
}).join(', ');
// Return data for rendering /* -------------------------------------------- */
return sheetData;
}
/* -------------------------------------------- */ /**
* Organize and classify Owned Items for Character sheets
* @private
*/
_prepareItems(data) {
// Categorize items as inventory, powerbook, features, and classes
const inventory = {
weapon: {label: "SW5E.ItemTypeWeaponPl", items: [], dataset: {type: "weapon"}},
equipment: {label: "SW5E.ItemTypeEquipmentPl", items: [], dataset: {type: "equipment"}},
consumable: {label: "SW5E.ItemTypeConsumablePl", items: [], dataset: {type: "consumable"}},
tool: {label: "SW5E.ItemTypeToolPl", items: [], dataset: {type: "tool"}},
backpack: {label: "SW5E.ItemTypeContainerPl", items: [], dataset: {type: "backpack"}},
loot: {label: "SW5E.ItemTypeLootPl", items: [], dataset: {type: "loot"}}
};
/** // Partition items by category
* Organize and classify Owned Items for Character sheets let [
* @private items,
*/ powers,
_prepareItems(data) { feats,
classes,
species,
archetypes,
classfeatures,
backgrounds,
fightingstyles,
fightingmasteries,
lightsaberforms
] = data.items.reduce(
(arr, item) => {
// Item details
item.img = item.img || CONST.DEFAULT_TOKEN;
item.isStack = Number.isNumeric(item.data.quantity) && item.data.quantity !== 1;
item.attunement = {
[CONFIG.SW5E.attunementTypes.REQUIRED]: {
icon: "fa-sun",
cls: "not-attuned",
title: "SW5E.AttunementRequired"
},
[CONFIG.SW5E.attunementTypes.ATTUNED]: {
icon: "fa-sun",
cls: "attuned",
title: "SW5E.AttunementAttuned"
}
}[item.data.attunement];
// Categorize items as inventory, powerbook, features, and classes // Item usage
const inventory = { item.hasUses = item.data.uses && item.data.uses.max > 0;
weapon: { label: "SW5E.ItemTypeWeaponPl", items: [], dataset: {type: "weapon"} }, item.isOnCooldown =
equipment: { label: "SW5E.ItemTypeEquipmentPl", items: [], dataset: {type: "equipment"} }, item.data.recharge && !!item.data.recharge.value && item.data.recharge.charged === false;
consumable: { label: "SW5E.ItemTypeConsumablePl", items: [], dataset: {type: "consumable"} }, item.isDepleted = item.isOnCooldown && item.data.uses.per && item.data.uses.value > 0;
tool: { label: "SW5E.ItemTypeToolPl", items: [], dataset: {type: "tool"} }, item.hasTarget = !!item.data.target && !["none", ""].includes(item.data.target.type);
backpack: { label: "SW5E.ItemTypeContainerPl", items: [], dataset: {type: "backpack"} },
loot: { label: "SW5E.ItemTypeLootPl", items: [], dataset: {type: "loot"} }
};
// Partition items by category // Item toggle state
let [items, powers, feats, classes, species, archetypes, classfeatures, backgrounds, fightingstyles, fightingmasteries, lightsaberforms] = data.items.reduce((arr, item) => { this._prepareItemToggleState(item);
// Item details // Primary Class
item.img = item.img || DEFAULT_TOKEN; if (item.type === "class")
item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1); item.isOriginalClass = item._id === this.actor.data.data.details.originalClass;
item.attunement = {
[CONFIG.SW5E.attunementTypes.REQUIRED]: { // Classify items into types
icon: "fa-sun", if (item.type === "power") arr[1].push(item);
cls: "not-attuned", else if (item.type === "feat") arr[2].push(item);
title: "SW5E.AttunementRequired" else if (item.type === "class") arr[3].push(item);
}, else if (item.type === "species") arr[4].push(item);
[CONFIG.SW5E.attunementTypes.ATTUNED]: { else if (item.type === "archetype") arr[5].push(item);
icon: "fa-sun", else if (item.type === "classfeature") arr[6].push(item);
cls: "attuned", else if (item.type === "background") arr[7].push(item);
title: "SW5E.AttunementAttuned" else if (item.type === "fightingstyle") arr[8].push(item);
else if (item.type === "fightingmastery") arr[9].push(item);
else if (item.type === "lightsaberform") arr[10].push(item);
else if (Object.keys(inventory).includes(item.type)) arr[0].push(item);
return arr;
},
[[], [], [], [], [], [], [], [], [], [], []]
);
// Apply active item filters
items = this._filterItems(items, this._filters.inventory);
powers = this._filterItems(powers, this._filters.powerbook);
feats = this._filterItems(feats, this._filters.features);
// Organize items
for (let i of items) {
i.data.quantity = i.data.quantity || 0;
i.data.weight = i.data.weight || 0;
i.totalWeight = (i.data.quantity * i.data.weight).toNearest(0.1);
inventory[i.type].items.push(i);
} }
}[item.data.attunement];
// Item usage // Organize Powerbook and count the number of prepared powers (excluding always, at will, etc...)
item.hasUses = item.data.uses && (item.data.uses.max > 0); const powerbook = this._preparePowerbook(data, powers);
item.isOnCooldown = item.data.recharge && !!item.data.recharge.value && (item.data.recharge.charged === false); const nPrepared = powers.filter((s) => {
item.isDepleted = item.isOnCooldown && (item.data.uses.per && (item.data.uses.value > 0)); return s.data.level > 0 && s.data.preparation.mode === "prepared" && s.data.preparation.prepared;
item.hasTarget = !!item.data.target && !(["none",""].includes(item.data.target.type)); }).length;
// Item toggle state // Organize Features
this._prepareItemToggleState(item); const features = {
classes: {
// Classify items into types label: "SW5E.ItemTypeClassPl",
if ( item.type === "power" ) arr[1].push(item); items: [],
else if ( item.type === "feat" ) arr[2].push(item); hasActions: false,
else if ( item.type === "class" ) arr[3].push(item); dataset: {type: "class"},
else if ( item.type === "species" ) arr[4].push(item); isClass: true
else if ( item.type === "archetype" ) arr[5].push(item); },
else if ( item.type === "classfeature" ) arr[6].push(item); classfeatures: {
else if ( item.type === "background" ) arr[7].push(item); label: "SW5E.ItemTypeClassFeats",
else if ( item.type === "fightingstyle" ) arr[8].push(item); items: [],
else if ( item.type === "fightingmastery" ) arr[9].push(item); hasActions: true,
else if ( item.type === "lightsaberform" ) arr[10].push(item); dataset: {type: "classfeature"},
else if ( Object.keys(inventory).includes(item.type ) ) arr[0].push(item); isClassfeature: true
return arr; },
}, [[], [], [], [], [], [], [], [], [], [], []]); archetype: {
label: "SW5E.ItemTypeArchetype",
// Apply active item filters items: [],
items = this._filterItems(items, this._filters.inventory); hasActions: false,
powers = this._filterItems(powers, this._filters.powerbook); dataset: {type: "archetype"},
feats = this._filterItems(feats, this._filters.features); isArchetype: true
},
// Organize items species: {
for ( let i of items ) { label: "SW5E.ItemTypeSpecies",
i.data.quantity = i.data.quantity || 0; items: [],
i.data.weight = i.data.weight || 0; hasActions: false,
i.totalWeight = (i.data.quantity * i.data.weight).toNearest(0.1); dataset: {type: "species"},
inventory[i.type].items.push(i); isSpecies: true
} },
background: {
// Organize Powerbook and count the number of prepared powers (excluding always, at will, etc...) label: "SW5E.ItemTypeBackground",
const powerbook = this._preparePowerbook(data, powers); items: [],
const nPrepared = powers.filter(s => { hasActions: false,
return (s.data.level > 0) && (s.data.preparation.mode === "prepared") && s.data.preparation.prepared; dataset: {type: "background"},
}).length; isBackground: true
},
// Organize Features fightingstyles: {
const features = { label: "SW5E.ItemTypeFightingStylePl",
classes: { label: "SW5E.ItemTypeClassPl", items: [], hasActions: false, dataset: {type: "class"}, isClass: true }, items: [],
classfeatures: { label: "SW5E.ItemTypeClassFeats", items: [], hasActions: true, dataset: {type: "classfeature"}, isClassfeature: true }, hasActions: false,
archetype: { label: "SW5E.ItemTypeArchetype", items: [], hasActions: false, dataset: {type: "archetype"}, isArchetype: true }, dataset: {type: "fightingstyle"},
species: { label: "SW5E.ItemTypeSpecies", items: [], hasActions: false, dataset: {type: "species"}, isSpecies: true }, isFightingstyle: true
background: { label: "SW5E.ItemTypeBackground", items: [], hasActions: false, dataset: {type: "background"}, isBackground: true }, },
fightingstyles: { label: "SW5E.ItemTypeFightingStylePl", items: [], hasActions: false, dataset: {type: "fightingstyle"}, isFightingstyle: true }, fightingmasteries: {
fightingmasteries: { label: "SW5E.ItemTypeFightingMasteryPl", items: [], hasActions: false, dataset: {type: "fightingmastery"}, isFightingmastery: true }, label: "SW5E.ItemTypeFightingMasteryPl",
lightsaberforms: { label: "SW5E.ItemTypeLightsaberFormPl", items: [], hasActions: false, dataset: {type: "lightsaberform"}, isLightsaberform: true }, items: [],
active: { label: "SW5E.FeatureActive", items: [], hasActions: true, dataset: {type: "feat", "activation.type": "action"} }, hasActions: false,
passive: { label: "SW5E.FeaturePassive", items: [], hasActions: false, dataset: {type: "feat"} } dataset: {type: "fightingmastery"},
}; isFightingmastery: true
for ( let f of feats ) { },
if ( f.data.activation.type ) features.active.items.push(f); lightsaberforms: {
else features.passive.items.push(f); label: "SW5E.ItemTypeLightsaberFormPl",
} items: [],
classes.sort((a, b) => b.levels - a.levels); hasActions: false,
features.classes.items = classes; dataset: {type: "lightsaberform"},
features.classfeatures.items = classfeatures; isLightsaberform: true
features.archetype.items = archetypes; },
features.species.items = species; active: {
features.background.items = backgrounds; label: "SW5E.FeatureActive",
features.fightingstyles.items = fightingstyles; items: [],
features.fightingmasteries.items = fightingmasteries; hasActions: true,
features.lightsaberforms.items = lightsaberforms; dataset: {"type": "feat", "activation.type": "action"}
},
// Assign and return passive: {label: "SW5E.FeaturePassive", items: [], hasActions: false, dataset: {type: "feat"}}
data.inventory = Object.values(inventory); };
data.powerbook = powerbook; for (let f of feats) {
data.preparedPowers = nPrepared; if (f.data.activation.type) features.active.items.push(f);
data.features = Object.values(features); else features.passive.items.push(f);
}
/* -------------------------------------------- */
/**
* A helper method to establish the displayed preparation state for an item
* @param {Item} item
* @private
*/
_prepareItemToggleState(item) {
if (item.type === "power") {
const isAlways = getProperty(item.data, "preparation.mode") === "always";
const isPrepared = getProperty(item.data, "preparation.prepared");
item.toggleClass = isPrepared ? "active" : "";
if ( isAlways ) item.toggleClass = "fixed";
if ( isAlways ) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.always;
else if ( isPrepared ) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.prepared;
else item.toggleTitle = game.i18n.localize("SW5E.PowerUnprepared");
}
else {
const isActive = getProperty(item.data, "equipped");
item.toggleClass = isActive ? "active" : "";
item.toggleTitle = game.i18n.localize(isActive ? "SW5E.Equipped" : "SW5E.Unequipped");
}
}
/* -------------------------------------------- */
/* Event Listeners and Handlers
/* -------------------------------------------- */
/**
* Activate event listeners using the prepared sheet HTML
* @param html {HTML} The prepared HTML object ready to be rendered into the DOM
*/
activateListeners(html) {
super.activateListeners(html);
if ( !this.options.editable ) return;
// Item State Toggling
html.find('.item-toggle').click(this._onToggleItem.bind(this));
// Short and Long Rest
html.find('.short-rest').click(this._onShortRest.bind(this));
html.find('.long-rest').click(this._onLongRest.bind(this));
// Rollable sheet actions
html.find(".rollable[data-action]").click(this._onSheetAction.bind(this));
}
/* -------------------------------------------- */
/**
* Handle mouse click events for character sheet actions
* @param {MouseEvent} event The originating click event
* @private
*/
_onSheetAction(event) {
event.preventDefault();
const button = event.currentTarget;
switch( button.dataset.action ) {
case "rollDeathSave":
return this.actor.rollDeathSave({event: event});
case "rollInitiative":
return this.actor.rollInitiative({createCombatants: true});
}
}
/* -------------------------------------------- */
/**
* Handle toggling the state of an Owned Item within the Actor
* @param {Event} event The triggering click event
* @private
*/
_onToggleItem(event) {
event.preventDefault();
const itemId = event.currentTarget.closest(".item").dataset.itemId;
const item = this.actor.getOwnedItem(itemId);
const attr = item.data.type === "power" ? "data.preparation.prepared" : "data.equipped";
return item.update({[attr]: !getProperty(item.data, attr)});
}
/* -------------------------------------------- */
/**
* Take a short rest, calling the relevant function on the Actor instance
* @param {Event} event The triggering click event
* @private
*/
async _onShortRest(event) {
event.preventDefault();
await this._onSubmit(event);
return this.actor.shortRest();
}
/* -------------------------------------------- */
/**
* Take a long rest, calling the relevant function on the Actor instance
* @param {Event} event The triggering click event
* @private
*/
async _onLongRest(event) {
event.preventDefault();
await this._onSubmit(event);
return this.actor.longRest();
}
/* -------------------------------------------- */
/** @override */
async _onDropItemCreate(itemData) {
// Increment the number of class levels a character instead of creating a new item
if ( itemData.type === "class" ) {
const cls = this.actor.itemTypes.class.find(c => c.name === itemData.name);
let priorLevel = cls?.data.data.levels ?? 0;
if ( !!cls ) {
const next = Math.min(priorLevel + 1, 20 + priorLevel - this.actor.data.data.details.level);
if ( next > priorLevel ) {
itemData.levels = next;
return cls.update({"data.levels": next});
} }
} classes.sort((a, b) => b.data.levels - a.data.levels);
features.classes.items = classes;
features.classfeatures.items = classfeatures;
features.archetype.items = archetypes;
features.species.items = species;
features.background.items = backgrounds;
features.fightingstyles.items = fightingstyles;
features.fightingmasteries.items = fightingmasteries;
features.lightsaberforms.items = lightsaberforms;
// Assign and return
data.inventory = Object.values(inventory);
data.powerbook = powerbook;
data.preparedPowers = nPrepared;
data.features = Object.values(features);
} }
// Default drop handling if levels were not added /* -------------------------------------------- */
super._onDropItemCreate(itemData);
} /**
} * A helper method to establish the displayed preparation state for an item
* @param {Item} item
* @private
*/
_prepareItemToggleState(item) {
if (item.type === "power") {
const isAlways = getProperty(item.data, "preparation.mode") === "always";
const isPrepared = getProperty(item.data, "preparation.prepared");
item.toggleClass = isPrepared ? "active" : "";
if (isAlways) item.toggleClass = "fixed";
if (isAlways) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.always;
else if (isPrepared) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.prepared;
else item.toggleTitle = game.i18n.localize("SW5E.PowerUnprepared");
} else {
const isActive = getProperty(item.data, "equipped");
item.toggleClass = isActive ? "active" : "";
item.toggleTitle = game.i18n.localize(isActive ? "SW5E.Equipped" : "SW5E.Unequipped");
}
}
/* -------------------------------------------- */
/* Event Listeners and Handlers
/* -------------------------------------------- */
/**
* Activate event listeners using the prepared sheet HTML
* @param html {jQuery} The prepared HTML object ready to be rendered into the DOM
*/
activateListeners(html) {
super.activateListeners(html);
if (!this.isEditable) return;
// Item State Toggling
html.find(".item-toggle").click(this._onToggleItem.bind(this));
// Short and Long Rest
html.find(".short-rest").click(this._onShortRest.bind(this));
html.find(".long-rest").click(this._onLongRest.bind(this));
// Rollable sheet actions
html.find(".rollable[data-action]").click(this._onSheetAction.bind(this));
}
/* -------------------------------------------- */
/**
* Handle mouse click events for character sheet actions
* @param {MouseEvent} event The originating click event
* @private
*/
_onSheetAction(event) {
event.preventDefault();
const button = event.currentTarget;
switch (button.dataset.action) {
case "rollDeathSave":
return this.actor.rollDeathSave({event: event});
case "rollInitiative":
return this.actor.rollInitiative({createCombatants: true});
}
}
/* -------------------------------------------- */
/**
* Handle toggling the state of an Owned Item within the Actor
* @param {Event} event The triggering click event
* @private
*/
_onToggleItem(event) {
event.preventDefault();
const itemId = event.currentTarget.closest(".item").dataset.itemId;
const item = this.actor.items.get(itemId);
const attr = item.data.type === "power" ? "data.preparation.prepared" : "data.equipped";
return item.update({[attr]: !getProperty(item.data, attr)});
}
/* -------------------------------------------- */
/**
* Take a short rest, calling the relevant function on the Actor instance
* @param {Event} event The triggering click event
* @private
*/
async _onShortRest(event) {
event.preventDefault();
await this._onSubmit(event);
return this.actor.shortRest();
}
/* -------------------------------------------- */
/**
* Take a long rest, calling the relevant function on the Actor instance
* @param {Event} event The triggering click event
* @private
*/
async _onLongRest(event) {
event.preventDefault();
await this._onSubmit(event);
return this.actor.longRest();
}
/* -------------------------------------------- */
/** @override */
async _onDropItemCreate(itemData) {
// Increment the number of class levels a character instead of creating a new item
if (itemData.type === "class") {
const cls = this.actor.itemTypes.class.find((c) => c.name === itemData.name);
let priorLevel = cls?.data.data.levels ?? 0;
if (!!cls) {
const next = Math.min(priorLevel + 1, 20 + priorLevel - this.actor.data.data.details.level);
if (next > priorLevel) {
itemData.levels = next;
return cls.update({"data.levels": next});
}
}
}
// Default drop handling if levels were not added
return super._onDropItemCreate(itemData);
}
}

View file

@ -6,122 +6,139 @@ import ActorSheet5e from "./base.js";
* @extends {ActorSheet5e} * @extends {ActorSheet5e}
*/ */
export default class ActorSheet5eNPC extends ActorSheet5e { export default class ActorSheet5eNPC extends ActorSheet5e {
/** @override */
/** @override */ static get defaultOptions() {
static get defaultOptions() { return mergeObject(super.defaultOptions, {
return mergeObject(super.defaultOptions, { classes: ["sw5e", "sheet", "actor", "npc"],
classes: ["sw5e", "sheet", "actor", "npc"], width: 600,
width: 600, height: 680
height: 680 });
});
}
/* -------------------------------------------- */
/**
* Organize Owned Items for rendering the NPC sheet
* @private
*/
_prepareItems(data) {
// Categorize Items as Features and Powers
const features = {
weapons: { label: game.i18n.localize("SW5E.AttackPl"), items: [] , hasActions: true, dataset: {type: "weapon", "weapon-type": "natural"} },
actions: { label: game.i18n.localize("SW5E.ActionPl"), items: [] , hasActions: true, dataset: {type: "feat", "activation.type": "action"} },
passive: { label: game.i18n.localize("SW5E.Features"), items: [], dataset: {type: "feat"} },
equipment: { label: game.i18n.localize("SW5E.Inventory"), items: [], dataset: {type: "loot"}}
};
// Start by classifying items into groups for rendering
let [powers, other] = data.items.reduce((arr, item) => {
item.img = item.img || DEFAULT_TOKEN;
item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1);
item.hasUses = item.data.uses && (item.data.uses.max > 0);
item.isOnCooldown = item.data.recharge && !!item.data.recharge.value && (item.data.recharge.charged === false);
item.isDepleted = item.isOnCooldown && (item.data.uses.per && (item.data.uses.value > 0));
item.hasTarget = !!item.data.target && !(["none",""].includes(item.data.target.type));
if ( item.type === "power" ) arr[0].push(item);
else arr[1].push(item);
return arr;
}, [[], []]);
// Apply item filters
powers = this._filterItems(powers, this._filters.powerbook);
other = this._filterItems(other, this._filters.features);
// Organize Powerbook
const powerbook = this._preparePowerbook(data, powers);
// Organize Features
for ( let item of other ) {
if ( item.type === "weapon" ) features.weapons.items.push(item);
else if ( item.type === "feat" ) {
if ( item.data.activation.type ) features.actions.items.push(item);
else features.passive.items.push(item);
}
else features.equipment.items.push(item);
} }
// Assign and return /* -------------------------------------------- */
data.features = Object.values(features);
data.powerbook = powerbook;
}
/** @override */
static unsupportedItemTypes = new Set(["class"]);
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @override */ /**
getData() { * Organize Owned Items for rendering the NPC sheet
const data = super.getData(); * @private
*/
_prepareItems(data) {
// Categorize Items as Features and Powers
const features = {
weapons: {
label: game.i18n.localize("SW5E.AttackPl"),
items: [],
hasActions: true,
dataset: {"type": "weapon", "weapon-type": "natural"}
},
actions: {
label: game.i18n.localize("SW5E.ActionPl"),
items: [],
hasActions: true,
dataset: {"type": "feat", "activation.type": "action"}
},
passive: {label: game.i18n.localize("SW5E.Features"), items: [], dataset: {type: "feat"}},
equipment: {label: game.i18n.localize("SW5E.Inventory"), items: [], dataset: {type: "loot"}}
};
// Challenge Rating // Start by classifying items into groups for rendering
const cr = parseFloat(data.data.details.cr || 0); let [powers, other] = data.items.reduce(
const crLabels = {0: "0", 0.125: "1/8", 0.25: "1/4", 0.5: "1/2"}; (arr, item) => {
data.labels["cr"] = cr >= 1 ? String(cr) : crLabels[cr] || 1; item.img = item.img || CONST.DEFAULT_TOKEN;
return data; item.isStack = Number.isNumeric(item.data.quantity) && item.data.quantity !== 1;
} item.hasUses = item.data.uses && item.data.uses.max > 0;
item.isOnCooldown =
item.data.recharge && !!item.data.recharge.value && item.data.recharge.charged === false;
item.isDepleted = item.isOnCooldown && item.data.uses.per && item.data.uses.value > 0;
item.hasTarget = !!item.data.target && !["none", ""].includes(item.data.target.type);
if (item.type === "power") arr[0].push(item);
else arr[1].push(item);
return arr;
},
[[], []]
);
/* -------------------------------------------- */ // Apply item filters
/* Object Updates */ powers = this._filterItems(powers, this._filters.powerbook);
/* -------------------------------------------- */ other = this._filterItems(other, this._filters.features);
/** @override */ // Organize Powerbook
_updateObject(event, formData) { const powerbook = this._preparePowerbook(data, powers);
// Format NPC Challenge Rating // Organize Features
const crs = {"1/8": 0.125, "1/4": 0.25, "1/2": 0.5}; for (let item of other) {
let crv = "data.details.cr"; if (item.type === "weapon") features.weapons.items.push(item);
let cr = formData[crv]; else if (item.type === "feat") {
cr = crs[cr] || parseFloat(cr); if (item.data.activation.type) features.actions.items.push(item);
if ( cr ) formData[crv] = cr < 1 ? cr : parseInt(cr); else features.passive.items.push(item);
} else features.equipment.items.push(item);
}
// Parent ActorSheet update steps // Assign and return
super._updateObject(event, formData); data.features = Object.values(features);
} data.powerbook = powerbook;
}
/* -------------------------------------------- */ /* -------------------------------------------- */
/* Event Listeners and Handlers */
/* -------------------------------------------- */
/** @override */ /** @inheritdoc */
activateListeners(html) { getData(options) {
super.activateListeners(html); const data = super.getData(options);
html.find(".health .rollable").click(this._onRollHPFormula.bind(this));
}
/* -------------------------------------------- */ // Challenge Rating
const cr = parseFloat(data.data.details.cr || 0);
const crLabels = {0: "0", 0.125: "1/8", 0.25: "1/4", 0.5: "1/2"};
data.labels["cr"] = cr >= 1 ? String(cr) : crLabels[cr] || 1;
/** // Creature Type
* Handle rolling NPC health values using the provided formula data.labels["type"] = this.actor.labels.creatureType;
* @param {Event} event The original click event return data;
* @private }
*/
_onRollHPFormula(event) { /* -------------------------------------------- */
event.preventDefault(); /* Object Updates */
const formula = this.actor.data.data.attributes.hp.formula; /* -------------------------------------------- */
if ( !formula ) return;
const hp = new Roll(formula).roll().total; /** @override */
AudioHelper.play({src: CONFIG.sounds.dice}); async _updateObject(event, formData) {
this.actor.update({"data.attributes.hp.value": hp, "data.attributes.hp.max": hp}); // Format NPC Challenge Rating
} const crs = {"1/8": 0.125, "1/4": 0.25, "1/2": 0.5};
let crv = "data.details.cr";
let cr = formData[crv];
cr = crs[cr] || parseFloat(cr);
if (cr) formData[crv] = cr < 1 ? cr : parseInt(cr);
// Parent ActorSheet update steps
return super._updateObject(event, formData);
}
/* -------------------------------------------- */
/* Event Listeners and Handlers */
/* -------------------------------------------- */
/** @override */
activateListeners(html) {
super.activateListeners(html);
html.find(".health .rollable").click(this._onRollHPFormula.bind(this));
}
/* -------------------------------------------- */
/**
* Handle rolling NPC health values using the provided formula
* @param {Event} event The original click event
* @private
*/
_onRollHPFormula(event) {
event.preventDefault();
const formula = this.actor.data.data.attributes.hp.formula;
if (!formula) return;
const hp = new Roll(formula).roll().total;
AudioHelper.play({src: CONFIG.sounds.dice});
this.actor.update({"data.attributes.hp.value": hp, "data.attributes.hp.max": hp});
}
} }

View file

@ -6,380 +6,427 @@ import ActorSheet5e from "./base.js";
* @type {ActorSheet5e} * @type {ActorSheet5e}
*/ */
export default class ActorSheet5eVehicle extends ActorSheet5e { export default class ActorSheet5eVehicle extends ActorSheet5e {
/** /**
* Define default rendering options for the Vehicle sheet. * Define default rendering options for the Vehicle sheet.
* @returns {Object} * @returns {Object}
*/ */
static get defaultOptions() { static get defaultOptions() {
return mergeObject(super.defaultOptions, { return mergeObject(super.defaultOptions, {
classes: ["sw5e", "sheet", "actor", "vehicle"], classes: ["sw5e", "sheet", "actor", "vehicle"],
width: 605, width: 605,
height: 680 height: 680
}); });
}
/* -------------------------------------------- */
/**
* Creates a new cargo entry for a vehicle Actor.
*/
static get newCargo() {
return {
name: '',
quantity: 1
};
}
/* -------------------------------------------- */
/**
* Compute the total weight of the vehicle's cargo.
* @param {Number} totalWeight The cumulative item weight from inventory items
* @param {Object} actorData The data object for the Actor being rendered
* @returns {{max: number, value: number, pct: number}}
* @private
*/
_computeEncumbrance(totalWeight, actorData) {
// Compute currency weight
const totalCoins = Object.values(actorData.data.currency).reduce((acc, denom) => acc + denom, 0);
totalWeight += totalCoins / CONFIG.SW5E.encumbrance.currencyPerWeight;
// Vehicle weights are an order of magnitude greater.
totalWeight /= CONFIG.SW5E.encumbrance.vehicleWeightMultiplier;
// Compute overall encumbrance
const max = actorData.data.attributes.capacity.cargo;
const pct = Math.clamped((totalWeight * 100) / max, 0, 100);
return {value: totalWeight.toNearest(0.1), max, pct};
}
/* -------------------------------------------- */
/** @override */
_getMovementSpeed(actorData, largestPrimary=true) {
return super._getMovementSpeed(actorData, largestPrimary);
}
/* -------------------------------------------- */
/**
* Prepare items that are mounted to a vehicle and require one or more crew
* to operate.
* @private
*/
_prepareCrewedItem(item) {
// Determine crewed status
const isCrewed = item.data.crewed;
item.toggleClass = isCrewed ? 'active' : '';
item.toggleTitle = game.i18n.localize(`SW5E.${isCrewed ? 'Crewed' : 'Uncrewed'}`);
// Handle crew actions
if (item.type === 'feat' && item.data.activation.type === 'crew') {
item.crew = item.data.activation.cost;
item.cover = game.i18n.localize(`SW5E.${item.data.cover ? 'CoverTotal' : 'None'}`);
if (item.data.cover === .5) item.cover = '½';
else if (item.data.cover === .75) item.cover = '¾';
else if (item.data.cover === null) item.cover = '—';
if (item.crew < 1 || item.crew === null) item.crew = '—';
} }
// Prepare vehicle weapons /* -------------------------------------------- */
if (item.type === 'equipment' || item.type === 'weapon') {
item.threshold = item.data.hp.dt ? item.data.hp.dt : '—'; /** @override */
static unsupportedItemTypes = new Set(["class"]);
/* -------------------------------------------- */
/**
* Creates a new cargo entry for a vehicle Actor.
*/
static get newCargo() {
return {
name: "",
quantity: 1
};
} }
}
/* -------------------------------------------- */ /* -------------------------------------------- */
/** /**
* Organize Owned Items for rendering the Vehicle sheet. * Compute the total weight of the vehicle's cargo.
* @private * @param {Number} totalWeight The cumulative item weight from inventory items
*/ * @param {Object} actorData The data object for the Actor being rendered
_prepareItems(data) { * @returns {{max: number, value: number, pct: number}}
const cargoColumns = [{ * @private
label: game.i18n.localize('SW5E.Quantity'), */
css: 'item-qty', _computeEncumbrance(totalWeight, actorData) {
property: 'quantity', // Compute currency weight
editable: 'Number' const totalCoins = Object.values(actorData.data.currency).reduce((acc, denom) => acc + denom, 0);
}]; totalWeight += totalCoins / CONFIG.SW5E.encumbrance.currencyPerWeight;
const equipmentColumns = [{ // Vehicle weights are an order of magnitude greater.
label: game.i18n.localize('SW5E.Quantity'), totalWeight /= CONFIG.SW5E.encumbrance.vehicleWeightMultiplier;
css: 'item-qty',
property: 'data.quantity'
}, {
label: game.i18n.localize('SW5E.AC'),
css: 'item-ac',
property: 'data.armor.value'
}, {
label: game.i18n.localize('SW5E.HP'),
css: 'item-hp',
property: 'data.hp.value',
editable: 'Number'
}, {
label: game.i18n.localize('SW5E.Threshold'),
css: 'item-threshold',
property: 'threshold'
}];
const features = { // Compute overall encumbrance
actions: { const max = actorData.data.attributes.capacity.cargo;
label: game.i18n.localize('SW5E.ActionPl'), const pct = Math.clamped((totalWeight * 100) / max, 0, 100);
items: [], return {value: totalWeight.toNearest(0.1), max, pct};
crewable: true, }
dataset: {type: 'feat', 'activation.type': 'crew'},
columns: [{
label: game.i18n.localize('SW5E.VehicleCrew'),
css: 'item-crew',
property: 'crew'
}, {
label: game.i18n.localize('SW5E.Cover'),
css: 'item-cover',
property: 'cover'
}]
},
equipment: {
label: game.i18n.localize('SW5E.ItemTypeEquipment'),
items: [],
crewable: true,
dataset: {type: 'equipment', 'armor.type': 'vehicle'},
columns: equipmentColumns
},
passive: {
label: game.i18n.localize('SW5E.Features'),
items: [],
dataset: {type: 'feat'}
},
reactions: {
label: game.i18n.localize('SW5E.ReactionPl'),
items: [],
dataset: {type: 'feat', 'activation.type': 'reaction'}
},
weapons: {
label: game.i18n.localize('SW5E.ItemTypeWeaponPl'),
items: [],
crewable: true,
dataset: {type: 'weapon', 'weapon-type': 'siege'},
columns: equipmentColumns
}
};
const cargo = { /* -------------------------------------------- */
crew: {
label: game.i18n.localize('SW5E.VehicleCrew'),
items: data.data.cargo.crew,
css: 'cargo-row crew',
editableName: true,
dataset: {type: 'crew'},
columns: cargoColumns
},
passengers: {
label: game.i18n.localize('SW5E.VehiclePassengers'),
items: data.data.cargo.passengers,
css: 'cargo-row passengers',
editableName: true,
dataset: {type: 'passengers'},
columns: cargoColumns
},
cargo: {
label: game.i18n.localize('SW5E.VehicleCargo'),
items: [],
dataset: {type: 'loot'},
columns: [{
label: game.i18n.localize('SW5E.Quantity'),
css: 'item-qty',
property: 'data.quantity',
editable: 'Number'
}, {
label: game.i18n.localize('SW5E.Price'),
css: 'item-price',
property: 'data.price',
editable: 'Number'
}, {
label: game.i18n.localize('SW5E.Weight'),
css: 'item-weight',
property: 'data.weight',
editable: 'Number'
}]
}
};
let totalWeight = 0; /** @override */
for (const item of data.items) { _getMovementSpeed(actorData, largestPrimary = true) {
this._prepareCrewedItem(item); return super._getMovementSpeed(actorData, largestPrimary);
if (item.type === 'weapon') features.weapons.items.push(item); }
else if (item.type === 'equipment') features.equipment.items.push(item);
else if (item.type === 'loot') { /* -------------------------------------------- */
totalWeight += (item.data.weight || 0) * item.data.quantity;
cargo.cargo.items.push(item); /**
} * Prepare items that are mounted to a vehicle and require one or more crew
else if (item.type === 'feat') { * to operate.
if (!item.data.activation.type || item.data.activation.type === 'none') { * @private
features.passive.items.push(item); */
_prepareCrewedItem(item) {
// Determine crewed status
const isCrewed = item.data.crewed;
item.toggleClass = isCrewed ? "active" : "";
item.toggleTitle = game.i18n.localize(`SW5E.${isCrewed ? "Crewed" : "Uncrewed"}`);
// Handle crew actions
if (item.type === "feat" && item.data.activation.type === "crew") {
item.crew = item.data.activation.cost;
item.cover = game.i18n.localize(`SW5E.${item.data.cover ? "CoverTotal" : "None"}`);
if (item.data.cover === 0.5) item.cover = "½";
else if (item.data.cover === 0.75) item.cover = "¾";
else if (item.data.cover === null) item.cover = "—";
if (item.crew < 1 || item.crew === null) item.crew = "—";
}
// Prepare vehicle weapons
if (item.type === "equipment" || item.type === "weapon") {
item.threshold = item.data.hp.dt ? item.data.hp.dt : "—";
} }
else if (item.data.activation.type === 'reaction') features.reactions.items.push(item);
else features.actions.items.push(item);
}
} }
data.features = Object.values(features); /* -------------------------------------------- */
data.cargo = Object.values(cargo);
data.data.attributes.encumbrance = this._computeEncumbrance(totalWeight, data);
}
/* -------------------------------------------- */ /**
/* Event Listeners and Handlers */ * Organize Owned Items for rendering the Vehicle sheet.
/* -------------------------------------------- */ * @private
*/
_prepareItems(data) {
const cargoColumns = [
{
label: game.i18n.localize("SW5E.Quantity"),
css: "item-qty",
property: "quantity",
editable: "Number"
}
];
/** @override */ const equipmentColumns = [
activateListeners(html) { {
super.activateListeners(html); label: game.i18n.localize("SW5E.Quantity"),
if (!this.options.editable) return; css: "item-qty",
property: "data.quantity"
},
{
label: game.i18n.localize("SW5E.AC"),
css: "item-ac",
property: "data.armor.value"
},
{
label: game.i18n.localize("SW5E.HP"),
css: "item-hp",
property: "data.hp.value",
editable: "Number"
},
{
label: game.i18n.localize("SW5E.Threshold"),
css: "item-threshold",
property: "threshold"
}
];
html.find('.item-toggle').click(this._onToggleItem.bind(this)); const features = {
html.find('.item-hp input') actions: {
.click(evt => evt.target.select()) label: game.i18n.localize("SW5E.ActionPl"),
.change(this._onHPChange.bind(this)); items: [],
crewable: true,
dataset: {"type": "feat", "activation.type": "crew"},
columns: [
{
label: game.i18n.localize("SW5E.VehicleCrew"),
css: "item-crew",
property: "crew"
},
{
label: game.i18n.localize("SW5E.Cover"),
css: "item-cover",
property: "cover"
}
]
},
equipment: {
label: game.i18n.localize("SW5E.ItemTypeEquipment"),
items: [],
crewable: true,
dataset: {"type": "equipment", "armor.type": "vehicle"},
columns: equipmentColumns
},
passive: {
label: game.i18n.localize("SW5E.Features"),
items: [],
dataset: {type: "feat"}
},
reactions: {
label: game.i18n.localize("SW5E.ReactionPl"),
items: [],
dataset: {"type": "feat", "activation.type": "reaction"}
},
weapons: {
label: game.i18n.localize("SW5E.ItemTypeWeaponPl"),
items: [],
crewable: true,
dataset: {"type": "weapon", "weapon-type": "siege"},
columns: equipmentColumns
}
};
html.find('.item:not(.cargo-row) input[data-property]') const cargo = {
.click(evt => evt.target.select()) crew: {
.change(this._onEditInSheet.bind(this)); label: game.i18n.localize("SW5E.VehicleCrew"),
items: data.data.cargo.crew,
css: "cargo-row crew",
editableName: true,
dataset: {type: "crew"},
columns: cargoColumns
},
passengers: {
label: game.i18n.localize("SW5E.VehiclePassengers"),
items: data.data.cargo.passengers,
css: "cargo-row passengers",
editableName: true,
dataset: {type: "passengers"},
columns: cargoColumns
},
cargo: {
label: game.i18n.localize("SW5E.VehicleCargo"),
items: [],
dataset: {type: "loot"},
columns: [
{
label: game.i18n.localize("SW5E.Quantity"),
css: "item-qty",
property: "data.quantity",
editable: "Number"
},
{
label: game.i18n.localize("SW5E.Price"),
css: "item-price",
property: "data.price",
editable: "Number"
},
{
label: game.i18n.localize("SW5E.Weight"),
css: "item-weight",
property: "data.weight",
editable: "Number"
}
]
}
};
html.find('.cargo-row input') // Classify items owned by the vehicle and compute total cargo weight
.click(evt => evt.target.select()) let totalWeight = 0;
.change(this._onCargoRowChange.bind(this)); for (const item of data.items) {
this._prepareCrewedItem(item);
if (this.actor.data.data.attributes.actions.stations) { // Handle cargo explicitly
html.find('.counter.actions, .counter.action-thresholds').hide(); const isCargo = item.flags.sw5e?.vehicleCargo === true;
} if (isCargo) {
} totalWeight += (item.data.weight || 0) * item.data.quantity;
cargo.cargo.items.push(item);
continue;
}
/* -------------------------------------------- */ // Handle non-cargo item types
switch (item.type) {
case "weapon":
features.weapons.items.push(item);
break;
case "equipment":
features.equipment.items.push(item);
break;
case "feat":
if (!item.data.activation.type || item.data.activation.type === "none")
features.passive.items.push(item);
else if (item.data.activation.type === "reaction") features.reactions.items.push(item);
else features.actions.items.push(item);
break;
default:
totalWeight += (item.data.weight || 0) * item.data.quantity;
cargo.cargo.items.push(item);
}
}
/** // Update the rendering context data
* Handle saving a cargo row (i.e. crew or passenger) in-sheet. data.features = Object.values(features);
* @param event {Event} data.cargo = Object.values(cargo);
* @returns {Promise<Actor>|null} data.data.attributes.encumbrance = this._computeEncumbrance(totalWeight, data);
* @private
*/
_onCargoRowChange(event) {
event.preventDefault();
const target = event.currentTarget;
const row = target.closest('.item');
const idx = Number(row.dataset.itemId);
const property = row.classList.contains('crew') ? 'crew' : 'passengers';
// Get the cargo entry
const cargo = duplicate(this.actor.data.data.cargo[property]);
const entry = cargo[idx];
if (!entry) return null;
// Update the cargo value
const key = target.dataset.property || 'name';
const type = target.dataset.dtype;
let value = target.value;
if (type === 'Number') value = Number(value);
entry[key] = value;
// Perform the Actor update
return this.actor.update({[`data.cargo.${property}`]: cargo});
}
/* -------------------------------------------- */
/**
* Handle editing certain values like quantity, price, and weight in-sheet.
* @param event {Event}
* @returns {Promise<Item>}
* @private
*/
_onEditInSheet(event) {
event.preventDefault();
const itemID = event.currentTarget.closest('.item').dataset.itemId;
const item = this.actor.items.get(itemID);
const property = event.currentTarget.dataset.property;
const type = event.currentTarget.dataset.dtype;
let value = event.currentTarget.value;
switch (type) {
case 'Number': value = parseInt(value); break;
case 'Boolean': value = value === 'true'; break;
}
return item.update({[`${property}`]: value});
}
/* -------------------------------------------- */
/**
* Handle creating a new crew or passenger row.
* @param event {Event}
* @returns {Promise<Actor|Item>}
* @private
*/
_onItemCreate(event) {
event.preventDefault();
const target = event.currentTarget;
const type = target.dataset.type;
if (type === 'crew' || type === 'passengers') {
const cargo = duplicate(this.actor.data.data.cargo[type]);
cargo.push(this.constructor.newCargo);
return this.actor.update({[`data.cargo.${type}`]: cargo});
}
return super._onItemCreate(event);
}
/* -------------------------------------------- */
/**
* Handle deleting a crew or passenger row.
* @param event {Event}
* @returns {Promise<Actor|Item>}
* @private
*/
_onItemDelete(event) {
event.preventDefault();
const row = event.currentTarget.closest('.item');
if (row.classList.contains('cargo-row')) {
const idx = Number(row.dataset.itemId);
const type = row.classList.contains('crew') ? 'crew' : 'passengers';
const cargo = duplicate(this.actor.data.data.cargo[type]).filter((_, i) => i !== idx);
return this.actor.update({[`data.cargo.${type}`]: cargo});
} }
return super._onItemDelete(event); /* -------------------------------------------- */
} /* Event Listeners and Handlers */
/* -------------------------------------------- */
/* -------------------------------------------- */ /** @override */
activateListeners(html) {
super.activateListeners(html);
if (!this.isEditable) return;
/** html.find(".item-toggle").click(this._onToggleItem.bind(this));
* Special handling for editing HP to clamp it within appropriate range. html.find(".item-hp input")
* @param event {Event} .click((evt) => evt.target.select())
* @returns {Promise<Item>} .change(this._onHPChange.bind(this));
* @private
*/
_onHPChange(event) {
event.preventDefault();
const itemID = event.currentTarget.closest('.item').dataset.itemId;
const item = this.actor.items.get(itemID);
const hp = Math.clamped(0, parseInt(event.currentTarget.value), item.data.data.hp.max);
event.currentTarget.value = hp;
return item.update({'data.hp.value': hp});
}
/* -------------------------------------------- */ html.find(".item:not(.cargo-row) input[data-property]")
.click((evt) => evt.target.select())
.change(this._onEditInSheet.bind(this));
/** html.find(".cargo-row input")
* Handle toggling an item's crewed status. .click((evt) => evt.target.select())
* @param event {Event} .change(this._onCargoRowChange.bind(this));
* @returns {Promise<Item>}
* @private if (this.actor.data.data.attributes.actions.stations) {
*/ html.find(".counter.actions, .counter.action-thresholds").hide();
_onToggleItem(event) { }
event.preventDefault(); }
const itemID = event.currentTarget.closest('.item').dataset.itemId;
const item = this.actor.items.get(itemID); /* -------------------------------------------- */
const crewed = !!item.data.data.crewed;
return item.update({'data.crewed': !crewed}); /**
} * Handle saving a cargo row (i.e. crew or passenger) in-sheet.
}; * @param event {Event}
* @returns {Promise<Actor>|null}
* @private
*/
_onCargoRowChange(event) {
event.preventDefault();
const target = event.currentTarget;
const row = target.closest(".item");
const idx = Number(row.dataset.itemId);
const property = row.classList.contains("crew") ? "crew" : "passengers";
// Get the cargo entry
const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[property]);
const entry = cargo[idx];
if (!entry) return null;
// Update the cargo value
const key = target.dataset.property || "name";
const type = target.dataset.dtype;
let value = target.value;
if (type === "Number") value = Number(value);
entry[key] = value;
// Perform the Actor update
return this.actor.update({[`data.cargo.${property}`]: cargo});
}
/* -------------------------------------------- */
/**
* Handle editing certain values like quantity, price, and weight in-sheet.
* @param event {Event}
* @returns {Promise<Item>}
* @private
*/
_onEditInSheet(event) {
event.preventDefault();
const itemID = event.currentTarget.closest(".item").dataset.itemId;
const item = this.actor.items.get(itemID);
const property = event.currentTarget.dataset.property;
const type = event.currentTarget.dataset.dtype;
let value = event.currentTarget.value;
switch (type) {
case "Number":
value = parseInt(value);
break;
case "Boolean":
value = value === "true";
break;
}
return item.update({[`${property}`]: value});
}
/* -------------------------------------------- */
/**
* Handle creating a new crew or passenger row.
* @param event {Event}
* @returns {Promise<Actor|Item>}
* @private
*/
_onItemCreate(event) {
event.preventDefault();
const target = event.currentTarget;
const type = target.dataset.type;
if (type === "crew" || type === "passengers") {
const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[type]);
cargo.push(this.constructor.newCargo);
return this.actor.update({[`data.cargo.${type}`]: cargo});
}
return super._onItemCreate(event);
}
/* -------------------------------------------- */
/**
* Handle deleting a crew or passenger row.
* @param event {Event}
* @returns {Promise<Actor|Item>}
* @private
*/
_onItemDelete(event) {
event.preventDefault();
const row = event.currentTarget.closest(".item");
if (row.classList.contains("cargo-row")) {
const idx = Number(row.dataset.itemId);
const type = row.classList.contains("crew") ? "crew" : "passengers";
const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[type]).filter((_, i) => i !== idx);
return this.actor.update({[`data.cargo.${type}`]: cargo});
}
return super._onItemDelete(event);
}
/* -------------------------------------------- */
/** @override */
async _onDropItemCreate(itemData) {
const cargoTypes = ["weapon", "equipment", "consumable", "tool", "loot", "backpack"];
const isCargo = cargoTypes.includes(itemData.type) && this._tabs[0].active === "cargo";
foundry.utils.setProperty(itemData, "flags.sw5e.vehicleCargo", isCargo);
return super._onDropItemCreate(itemData);
}
/* -------------------------------------------- */
/**
* Special handling for editing HP to clamp it within appropriate range.
* @param event {Event}
* @returns {Promise<Item>}
* @private
*/
_onHPChange(event) {
event.preventDefault();
const itemID = event.currentTarget.closest(".item").dataset.itemId;
const item = this.actor.items.get(itemID);
const hp = Math.clamped(0, parseInt(event.currentTarget.value), item.data.data.hp.max);
event.currentTarget.value = hp;
return item.update({"data.hp.value": hp});
}
/* -------------------------------------------- */
/**
* Handle toggling an item's crewed status.
* @param event {Event}
* @returns {Promise<Item>}
* @private
*/
_onToggleItem(event) {
event.preventDefault();
const itemID = event.currentTarget.closest(".item").dataset.itemId;
const item = this.actor.items.get(itemID);
const crewed = !!item.data.data.crewed;
return item.update({"data.crewed": !crewed});
}
}

View file

@ -3,226 +3,225 @@
* @type {Dialog} * @type {Dialog}
*/ */
export default class AbilityUseDialog extends Dialog { export default class AbilityUseDialog extends Dialog {
constructor(item, dialogData={}, options={}) { constructor(item, dialogData = {}, options = {}) {
super(dialogData, options); super(dialogData, options);
this.options.classes = ["sw5e", "dialog"]; this.options.classes = ["sw5e", "dialog"];
/**
* Store a reference to the Item entity being used
* @type {Item5e}
*/
this.item = item;
}
/* -------------------------------------------- */
/* Rendering */
/* -------------------------------------------- */
/** /**
* Store a reference to the Item entity being used * A constructor function which displays the Power Cast Dialog app for a given Actor and Item.
* @type {Item5e} * Returns a Promise which resolves to the dialog FormData once the workflow has been completed.
* @param {Item5e} item
* @return {Promise}
*/ */
this.item = item; static async create(item) {
} if (!item.isOwned) throw new Error("You cannot display an ability usage dialog for an unowned item");
/* -------------------------------------------- */ // Prepare data
/* Rendering */ const actorData = item.actor.data.data;
/* -------------------------------------------- */ const itemData = item.data.data;
const uses = itemData.uses || {};
const quantity = itemData.quantity || 0;
const recharge = itemData.recharge || {};
const recharges = !!recharge.value;
const sufficientUses = (quantity > 0 && !uses.value) || uses.value > 0;
/** // Prepare dialog form data
* A constructor function which displays the Power Cast Dialog app for a given Actor and Item. const data = {
* Returns a Promise which resolves to the dialog FormData once the workflow has been completed. item: item.data,
* @param {Item5e} item title: game.i18n.format("SW5E.AbilityUseHint", {
* @return {Promise} type: game.i18n.localize(`SW5E.ItemType${item.type.capitalize()}`),
*/ name: item.name
static async create(item) { }),
if ( !item.isOwned ) throw new Error("You cannot display an ability usage dialog for an unowned item"); note: this._getAbilityUseNote(item.data, uses, recharge),
consumePowerSlot: false,
consumeRecharge: recharges,
consumeResource: !!itemData.consume.target,
consumeUses: uses.per && uses.max > 0,
canUse: recharges ? recharge.charged : sufficientUses,
createTemplate: game.user.can("TEMPLATE_CREATE") && item.hasAreaTarget,
errors: []
};
if (item.data.type === "power") this._getPowerData(actorData, itemData, data);
// Prepare data // Render the ability usage template
const actorData = item.actor.data.data; const html = await renderTemplate("systems/sw5e/templates/apps/ability-use.html", data);
const itemData = item.data.data;
const uses = itemData.uses || {};
const quantity = itemData.quantity || 0;
const recharge = itemData.recharge || {};
const recharges = !!recharge.value;
const sufficientUses = (quantity > 0 && !uses.value) || uses.value > 0;
// Prepare dialog form data // Create the Dialog and return data as a Promise
const data = { const icon = data.isPower ? "fa-magic" : "fa-fist-raised";
item: item.data, const label = game.i18n.localize("SW5E.AbilityUse" + (data.isPower ? "Cast" : "Use"));
title: game.i18n.format("SW5E.AbilityUseHint", item.data), return new Promise((resolve) => {
note: this._getAbilityUseNote(item.data, uses, recharge), const dlg = new this(item, {
consumePowerSlot: false, title: `${item.name}: ${game.i18n.localize("SW5E.AbilityUseConfig")}`,
consumeRecharge: recharges, content: html,
consumeResource: !!itemData.consume.target, buttons: {
consumeUses: uses.max, use: {
canUse: recharges ? recharge.charged : sufficientUses, icon: `<i class="fas ${icon}"></i>`,
createTemplate: game.user.can("TEMPLATE_CREATE") && item.hasAreaTarget, label: label,
errors: [] callback: (html) => {
}; const fd = new FormDataExtended(html[0].querySelector("form"));
if ( item.data.type === "power" ) this._getPowerData(actorData, itemData, data); resolve(fd.toObject());
}
}
},
default: "use",
close: () => resolve(null)
});
dlg.render(true);
});
}
// Render the ability usage template /* -------------------------------------------- */
const html = await renderTemplate("systems/sw5e/templates/apps/ability-use.html", data); /* Helpers */
/* -------------------------------------------- */
// Create the Dialog and return data as a Promise /**
const icon = data.isPower ? "fa-magic" : "fa-fist-raised"; * Get dialog data related to limited power slots
const label = game.i18n.localize("SW5E.AbilityUse" + (data.isPower ? "Cast" : "Use")); * @private
return new Promise((resolve) => { */
const dlg = new this(item, { static _getPowerData(actorData, itemData, data) {
title: `${item.name}: Usage Configuration`, // Determine whether the power may be up-cast
content: html, const lvl = itemData.level;
buttons: { const consumePowerSlot = lvl > 0 && CONFIG.SW5E.powerUpcastModes.includes(itemData.preparation.mode);
use: {
icon: `<i class="fas ${icon}"></i>`, // If can't upcast, return early and don't bother calculating available power slots
label: label, if (!consumePowerSlot) {
callback: html => { mergeObject(data, {isPower: true, consumePowerSlot});
const fd = new FormDataExtended(html[0].querySelector("form")); return;
resolve(fd.toObject()); }
// Determine the levels which are feasible
let lmax = 0;
let points;
let powerType;
switch (itemData.school) {
case "lgt":
case "uni":
case "drk": {
powerType = "force";
points = actorData.attributes.force.points.value + actorData.attributes.force.points.temp;
break;
}
case "tec": {
powerType = "tech";
points = actorData.attributes.tech.points.value + actorData.attributes.tech.points.temp;
break;
} }
}
},
default: "use",
close: () => resolve(null)
});
dlg.render(true);
});
}
/* -------------------------------------------- */
/* Helpers */
/* -------------------------------------------- */
/**
* Get dialog data related to limited power slots
* @private
*/
static _getPowerData(actorData, itemData, data) {
// Determine whether the power may be up-cast
const lvl = itemData.level;
const consumePowerSlot = (lvl > 0) && CONFIG.SW5E.powerUpcastModes.includes(itemData.preparation.mode);
// If can't upcast, return early and don't bother calculating available power slots
if (!consumePowerSlot) {
mergeObject(data, { isPower: true, consumePowerSlot });
return;
}
// Determine the levels which are feasible
let lmax = 0;
let points;
let powerType;
switch (itemData.school){
case "lgt":
case "uni":
case "drk": {
powerType = "force"
points = actorData.attributes.force.points.value + actorData.attributes.force.points.temp;
break;
}
case "tec": {
powerType = "tech"
points = actorData.attributes.tech.points.value + actorData.attributes.tech.points.temp;
break;
}
}
// eliminate point usage for innate casters
if (actorData.attributes.powercasting === 'innate') points = 999;
let powerLevels
if (powerType === "force"){
powerLevels = Array.fromRange(10).reduce((arr, i) => {
if ( i < lvl ) return arr;
const label = CONFIG.SW5E.powerLevels[i];
const l = actorData.powers["power"+i] || {fmax: 0, foverride: null};
let max = parseInt(l.foverride || l.fmax || 0);
let slots = Math.clamped(parseInt(l.fvalue || 0), 0, max);
if ( max > 0 ) lmax = i;
if ((max > 0) && (slots > 0) && (points > i)){
arr.push({
level: i,
label: i > 0 ? game.i18n.format('SW5E.PowerLevelSlot', {level: label, n: slots}) : label,
canCast: max > 0,
hasSlots: slots > 0
});
} }
return arr;
}, []).filter(sl => sl.level <= lmax); // eliminate point usage for innate casters
}else if (powerType === "tech"){ if (actorData.attributes.powercasting === "innate") points = 999;
powerLevels = Array.fromRange(10).reduce((arr, i) => {
if ( i < lvl ) return arr; let powerLevels;
const label = CONFIG.SW5E.powerLevels[i]; if (powerType === "force") {
const l = actorData.powers["power"+i] || {tmax: 0, toverride: null}; powerLevels = Array.fromRange(10)
let max = parseInt(l.override || l.tmax || 0); .reduce((arr, i) => {
let slots = Math.clamped(parseInt(l.tvalue || 0), 0, max); if (i < lvl) return arr;
if ( max > 0 ) lmax = i; const label = CONFIG.SW5E.powerLevels[i];
if ((max > 0) && (slots > 0) && (points > i)){ const l = actorData.powers["power" + i] || {fmax: 0, foverride: null};
arr.push({ let max = parseInt(l.foverride || l.fmax || 0);
level: i, let slots = Math.clamped(parseInt(l.fvalue || 0), 0, max);
label: i > 0 ? game.i18n.format('SW5E.PowerLevelSlot', {level: label, n: slots}) : label, if (max > 0) lmax = i;
canCast: max > 0, if (max > 0 && slots > 0 && points > i) {
hasSlots: slots > 0 arr.push({
}); level: i,
label: i > 0 ? game.i18n.format("SW5E.PowerLevelSlot", {level: label, n: slots}) : label,
canCast: max > 0,
hasSlots: slots > 0
});
}
return arr;
}, [])
.filter((sl) => sl.level <= lmax);
} else if (powerType === "tech") {
powerLevels = Array.fromRange(10)
.reduce((arr, i) => {
if (i < lvl) return arr;
const label = CONFIG.SW5E.powerLevels[i];
const l = actorData.powers["power" + i] || {tmax: 0, toverride: null};
let max = parseInt(l.override || l.tmax || 0);
let slots = Math.clamped(parseInt(l.tvalue || 0), 0, max);
if (max > 0) lmax = i;
if (max > 0 && slots > 0 && points > i) {
arr.push({
level: i,
label: i > 0 ? game.i18n.format("SW5E.PowerLevelSlot", {level: label, n: slots}) : label,
canCast: max > 0,
hasSlots: slots > 0
});
}
return arr;
}, [])
.filter((sl) => sl.level <= lmax);
} }
return arr;
}, []).filter(sl => sl.level <= lmax);
}
const canCast = powerLevels.some(l => l.hasSlots); const canCast = powerLevels.some((l) => l.hasSlots);
if ( !canCast ) data.errors.push(game.i18n.format("SW5E.PowerCastNoSlots", { if (!canCast)
level: CONFIG.SW5E.powerLevels[lvl], data.errors.push(
name: data.item.name game.i18n.format("SW5E.PowerCastNoSlots", {
})); level: CONFIG.SW5E.powerLevels[lvl],
name: data.item.name
})
);
// Merge power casting data // Merge power casting data
return mergeObject(data, { isPower: true, consumePowerSlot, powerLevels }); return foundry.utils.mergeObject(data, {isPower: true, consumePowerSlot, powerLevels});
}
/* -------------------------------------------- */
/**
* Get the ability usage note that is displayed
* @private
*/
static _getAbilityUseNote(item, uses, recharge) {
// Zero quantity
const quantity = item.data.quantity;
if ( quantity <= 0 ) return game.i18n.localize("SW5E.AbilityUseUnavailableHint");
// Abilities which use Recharge
if ( !!recharge.value ) {
return game.i18n.format(recharge.charged ? "SW5E.AbilityUseChargedHint" : "SW5E.AbilityUseRechargeHint", {
type: item.type,
})
} }
// Does not use any resource /* -------------------------------------------- */
if ( !uses.per || !uses.max ) return "";
// Consumables /**
if ( item.type === "consumable" ) { * Get the ability usage note that is displayed
let str = "SW5E.AbilityUseNormalHint"; * @private
if ( uses.value > 1 ) str = "SW5E.AbilityUseConsumableChargeHint"; */
else if ( item.data.quantity === 1 && uses.autoDestroy ) str = "SW5E.AbilityUseConsumableDestroyHint"; static _getAbilityUseNote(item, uses, recharge) {
else if ( item.data.quantity > 1 ) str = "SW5E.AbilityUseConsumableQuantityHint"; // Zero quantity
return game.i18n.format(str, { const quantity = item.data.quantity;
type: item.data.consumableType, if (quantity <= 0) return game.i18n.localize("SW5E.AbilityUseUnavailableHint");
value: uses.value,
quantity: item.data.quantity, // Abilities which use Recharge
max: uses.max, if (!!recharge.value) {
per: CONFIG.SW5E.limitedUsePeriods[uses.per] return game.i18n.format(recharge.charged ? "SW5E.AbilityUseChargedHint" : "SW5E.AbilityUseRechargeHint", {
}); type: game.i18n.localize(`SW5E.ItemType${item.type.capitalize()}`)
});
}
// Does not use any resource
if (!uses.per || !uses.max) return "";
// Consumables
if (item.type === "consumable") {
let str = "SW5E.AbilityUseNormalHint";
if (uses.value > 1) str = "SW5E.AbilityUseConsumableChargeHint";
else if (item.data.quantity === 1 && uses.autoDestroy) str = "SW5E.AbilityUseConsumableDestroyHint";
else if (item.data.quantity > 1) str = "SW5E.AbilityUseConsumableQuantityHint";
return game.i18n.format(str, {
type: game.i18n.localize(`SW5E.Consumable${item.data.consumableType.capitalize()}`),
value: uses.value,
quantity: item.data.quantity,
max: uses.max,
per: CONFIG.SW5E.limitedUsePeriods[uses.per]
});
}
// Other Items
else {
return game.i18n.format("SW5E.AbilityUseNormalHint", {
type: game.i18n.localize(`SW5E.ItemType${item.type.capitalize()}`),
value: uses.value,
max: uses.max,
per: CONFIG.SW5E.limitedUsePeriods[uses.per]
});
}
} }
// Other Items
else {
return game.i18n.format("SW5E.AbilityUseNormalHint", {
type: item.type,
value: uses.value,
max: uses.max,
per: CONFIG.SW5E.limitedUsePeriods[uses.per]
});
}
}
/* -------------------------------------------- */
static _handleSubmit(formData, item) {
}
} }

View file

@ -1,121 +1,139 @@
/** /**
* An application class which provides advanced configuration for special character flags which modify an Actor * An application class which provides advanced configuration for special character flags which modify an Actor
* @implements {BaseEntitySheet} * @implements {DocumentSheet}
*/ */
export default class ActorSheetFlags extends BaseEntitySheet { export default class ActorSheetFlags extends DocumentSheet {
static get defaultOptions() { static get defaultOptions() {
const options = super.defaultOptions; return foundry.utils.mergeObject(super.defaultOptions, {
return mergeObject(options, { id: "actor-flags",
id: "actor-flags", classes: ["sw5e"],
classes: ["sw5e"], template: "systems/sw5e/templates/apps/actor-flags.html",
template: "systems/sw5e/templates/apps/actor-flags.html", width: 500,
width: 500, closeOnSubmit: true
closeOnSubmit: true });
});
}
/* -------------------------------------------- */
/** @override */
get title() {
return `${game.i18n.localize('SW5E.FlagsTitle')}: ${this.object.name}`;
}
/* -------------------------------------------- */
/** @override */
getData() {
const data = {};
data.actor = this.object;
data.flags = this._getFlags();
data.bonuses = this._getBonuses();
return data;
}
/* -------------------------------------------- */
/**
* Prepare an object of flags data which groups flags by section
* Add some additional data for rendering
* @return {object}
*/
_getFlags() {
const flags = {};
const baseData = this.entity._data;
for ( let [k, v] of Object.entries(CONFIG.SW5E.characterFlags) ) {
if ( !flags.hasOwnProperty(v.section) ) flags[v.section] = {};
let flag = duplicate(v);
flag.type = v.type.name;
flag.isCheckbox = v.type === Boolean;
flag.isSelect = v.hasOwnProperty('choices');
flag.value = getProperty(baseData.flags, `sw5e.${k}`);
flags[v.section][`flags.sw5e.${k}`] = flag;
} }
return flags;
}
/* -------------------------------------------- */ /* -------------------------------------------- */
/** /** @override */
* Get the bonuses fields and their localization strings get title() {
* @return {Array<object>} return `${game.i18n.localize("SW5E.FlagsTitle")}: ${this.object.name}`;
* @private
*/
_getBonuses() {
const bonuses = [
{name: "data.bonuses.mwak.attack", label: "SW5E.BonusMWAttack"},
{name: "data.bonuses.mwak.damage", label: "SW5E.BonusMWDamage"},
{name: "data.bonuses.rwak.attack", label: "SW5E.BonusRWAttack"},
{name: "data.bonuses.rwak.damage", label: "SW5E.BonusRWDamage"},
{name: "data.bonuses.mpak.attack", label: "SW5E.BonusMPAttack"},
{name: "data.bonuses.mpak.damage", label: "SW5E.BonusMPDamage"},
{name: "data.bonuses.rpak.attack", label: "SW5E.BonusRPAttack"},
{name: "data.bonuses.rpak.damage", label: "SW5E.BonusRPDamage"},
{name: "data.bonuses.abilities.check", label: "SW5E.BonusAbilityCheck"},
{name: "data.bonuses.abilities.save", label: "SW5E.BonusAbilitySave"},
{name: "data.bonuses.abilities.skill", label: "SW5E.BonusAbilitySkill"},
{name: "data.bonuses.power.dc", label: "SW5E.BonusPowerDC"},
{name: "data.bonuses.power.forceLightDC", label: "SW5E.BonusForceLightPowerDC"},
{name: "data.bonuses.power.forceDarkDC", label: "SW5E.BonusForceDarkPowerDC"},
{name: "data.bonuses.power.forceUnivDC", label: "SW5E.BonusForceUnivPowerDC"},
{name: "data.bonuses.power.techDC", label: "SW5E.BonusTechPowerDC"}
];
for ( let b of bonuses ) {
b.value = getProperty(this.object._data, b.name) || "";
} }
return bonuses;
}
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @override */ /** @override */
async _updateObject(event, formData) { getData() {
const actor = this.object; const data = {};
let updateData = expandObject(formData); data.actor = this.object;
data.classes = this._getClasses();
data.flags = this._getFlags();
data.bonuses = this._getBonuses();
return data;
}
// Unset any flags which are "false" /* -------------------------------------------- */
let unset = false;
const flags = updateData.flags.sw5e; /**
//clone flags to dnd5e for module compatability * Prepare an object of sorted classes.
updateData.flags.dnd5e = updateData.flags.sw5e * @return {object}
for ( let [k, v] of Object.entries(flags) ) { * @private
if ( [undefined, null, "", false, 0].includes(v) ) { */
delete flags[k]; _getClasses() {
if ( hasProperty(actor._data.flags, `sw5e.${k}`) ) { const classes = this.object.items.filter((i) => i.type === "class");
unset = true; return classes
flags[`-=${k}`] = null; .sort((a, b) => a.name.localeCompare(b.name))
.reduce((obj, i) => {
obj[i.id] = i.name;
return obj;
}, {});
}
/* -------------------------------------------- */
/**
* Prepare an object of flags data which groups flags by section
* Add some additional data for rendering
* @return {object}
* @private
*/
_getFlags() {
const flags = {};
const baseData = this.document.toJSON();
for (let [k, v] of Object.entries(CONFIG.SW5E.characterFlags)) {
if (!flags.hasOwnProperty(v.section)) flags[v.section] = {};
let flag = foundry.utils.deepClone(v);
flag.type = v.type.name;
flag.isCheckbox = v.type === Boolean;
flag.isSelect = v.hasOwnProperty("choices");
flag.value = getProperty(baseData.flags, `sw5e.${k}`);
flags[v.section][`flags.sw5e.${k}`] = flag;
} }
} return flags;
} }
// Clear any bonuses which are whitespace only /* -------------------------------------------- */
for ( let b of Object.values(updateData.data.bonuses ) ) {
for ( let [k, v] of Object.entries(b) ) { /**
b[k] = v.trim(); * Get the bonuses fields and their localization strings
} * @return {Array<object>}
* @private
*/
_getBonuses() {
const bonuses = [
{name: "data.bonuses.mwak.attack", label: "SW5E.BonusMWAttack"},
{name: "data.bonuses.mwak.damage", label: "SW5E.BonusMWDamage"},
{name: "data.bonuses.rwak.attack", label: "SW5E.BonusRWAttack"},
{name: "data.bonuses.rwak.damage", label: "SW5E.BonusRWDamage"},
{name: "data.bonuses.mpak.attack", label: "SW5E.BonusMPAttack"},
{name: "data.bonuses.mpak.damage", label: "SW5E.BonusMPDamage"},
{name: "data.bonuses.rpak.attack", label: "SW5E.BonusRPAttack"},
{name: "data.bonuses.rpak.damage", label: "SW5E.BonusRPDamage"},
{name: "data.bonuses.abilities.check", label: "SW5E.BonusAbilityCheck"},
{name: "data.bonuses.abilities.save", label: "SW5E.BonusAbilitySave"},
{name: "data.bonuses.abilities.skill", label: "SW5E.BonusAbilitySkill"},
{name: "data.bonuses.power.dc", label: "SW5E.BonusPowerDC"},
{name: "data.bonuses.power.forceLightDC", label: "SW5E.BonusForceLightPowerDC"},
{name: "data.bonuses.power.forceDarkDC", label: "SW5E.BonusForceDarkPowerDC"},
{name: "data.bonuses.power.forceUnivDC", label: "SW5E.BonusForceUnivPowerDC"},
{name: "data.bonuses.power.techDC", label: "SW5E.BonusTechPowerDC"}
];
for (let b of bonuses) {
b.value = getProperty(this.object._data, b.name) || "";
}
return bonuses;
} }
// Diff the data against any applied overrides and apply /* -------------------------------------------- */
await actor.update(updateData, {diff: false});
} /** @override */
async _updateObject(event, formData) {
const actor = this.object;
let updateData = expandObject(formData);
// Unset any flags which are "false"
let unset = false;
const flags = updateData.flags.sw5e;
//clone flags to dnd5e for module compatability
updateData.flags.dnd5e = updateData.flags.sw5e;
for (let [k, v] of Object.entries(flags)) {
if ([undefined, null, "", false, 0].includes(v)) {
delete flags[k];
if (hasProperty(actor._data.flags, `sw5e.${k}`)) {
unset = true;
flags[`-=${k}`] = null;
}
}
}
// Clear any bonuses which are whitespace only
for (let b of Object.values(updateData.data.bonuses)) {
for (let [k, v] of Object.entries(b)) {
b[k] = v.trim();
}
}
// Diff the data against any applied overrides and apply
await actor.update(updateData, {diff: false});
}
} }

111
module/apps/actor-type.js Normal file
View file

@ -0,0 +1,111 @@
import Actor5e from "../actor/entity.js";
/**
* A specialized form used to select from a checklist of attributes, traits, or properties
* @extends {FormApplication}
*/
export default class ActorTypeConfig extends FormApplication {
/** @inheritdoc */
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
classes: ["sw5e", "actor-type", "trait-selector"],
template: "systems/sw5e/templates/apps/actor-type.html",
title: "SW5E.CreatureTypeTitle",
width: 280,
height: "auto",
choices: {},
allowCustom: true,
minimum: 0,
maximum: null
});
}
/* -------------------------------------------- */
/** @override */
get id() {
return `actor-type-${this.object.id}`;
}
/* -------------------------------------------- */
/** @override */
getData(options) {
// Get current value or new default
let attr = foundry.utils.getProperty(this.object.data.data, "details.type");
if (foundry.utils.getType(attr) !== "Object")
attr = {
value: attr in CONFIG.SW5E.creatureTypes ? attr : "humanoid",
subtype: "",
swarm: "",
custom: ""
};
// Populate choices
const types = {};
for (let [k, v] of Object.entries(CONFIG.SW5E.creatureTypes)) {
types[k] = {
label: game.i18n.localize(v),
chosen: attr.value === k
};
}
// Return data for rendering
return {
types: types,
custom: {
value: attr.custom,
label: game.i18n.localize("SW5E.CreatureTypeSelectorCustom"),
chosen: attr.value === "custom"
},
subtype: attr.subtype,
swarm: attr.swarm,
sizes: Array.from(Object.entries(CONFIG.SW5E.actorSizes))
.reverse()
.reduce((obj, e) => {
obj[e[0]] = e[1];
return obj;
}, {}),
preview: Actor5e.formatCreatureType(attr) || ""
};
}
/* -------------------------------------------- */
/** @override */
async _updateObject(event, formData) {
const typeObject = foundry.utils.expandObject(formData);
return this.object.update({"data.details.type": typeObject});
}
/* -------------------------------------------- */
/* Event Listeners and Handlers */
/* -------------------------------------------- */
/** @inheritdoc */
activateListeners(html) {
super.activateListeners(html);
html.find("input[name='custom']").focusin(this._onCustomFieldFocused.bind(this));
}
/* -------------------------------------------- */
/** @inheritdoc */
_onChangeInput(event) {
super._onChangeInput(event);
const typeObject = foundry.utils.expandObject(this._getSubmitData());
this.form["preview"].value = Actor5e.formatCreatureType(typeObject) || "—";
}
/* -------------------------------------------- */
/**
* Select the custom radio button when the custom text field is focused.
* @param {FocusEvent} event The original focusin event
* @private
*/
_onCustomFieldFocused(event) {
this.form.querySelector("input[name='value'][value='custom']").checked = true;
this._onChangeInput(event);
}
}

View file

@ -0,0 +1,92 @@
/**
* A simple form to set actor hit dice amounts
* @implements {DocumentSheet}
*/
export default class ActorHitDiceConfig extends DocumentSheet {
/** @override */
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
classes: ["sw5e", "hd-config", "dialog"],
template: "systems/sw5e/templates/apps/hit-dice-config.html",
width: 360,
height: "auto"
});
}
/* -------------------------------------------- */
/** @override */
get title() {
return `${game.i18n.localize("SW5E.HitDiceConfig")}: ${this.object.name}`;
}
/* -------------------------------------------- */
/** @override */
getData(options) {
return {
classes: this.object.items
.reduce((classes, item) => {
if (item.data.type === "class") {
// Add the appropriate data only if this item is a "class"
classes.push({
classItemId: item.data._id,
name: item.data.name,
diceDenom: item.data.data.hitDice,
currentHitDice: item.data.data.levels - item.data.data.hitDiceUsed,
maxHitDice: item.data.data.levels,
canRoll: item.data.data.levels - item.data.data.hitDiceUsed > 0
});
}
return classes;
}, [])
.sort((a, b) => parseInt(b.diceDenom.slice(1)) - parseInt(a.diceDenom.slice(1)))
};
}
/* -------------------------------------------- */
/** @override */
activateListeners(html) {
super.activateListeners(html);
// Hook up -/+ buttons to adjust the current value in the form
html.find("button.increment,button.decrement").click((event) => {
const button = event.currentTarget;
const current = button.parentElement.querySelector(".current");
const max = button.parentElement.querySelector(".max");
const direction = button.classList.contains("increment") ? 1 : -1;
current.value = Math.clamped(parseInt(current.value) + direction, 0, parseInt(max.value));
});
html.find("button.roll-hd").click(this._onRollHitDie.bind(this));
}
/* -------------------------------------------- */
/** @override */
async _updateObject(event, formData) {
const actorItems = this.object.items;
const classUpdates = Object.entries(formData).map(([id, hd]) => ({
"_id": id,
"data.hitDiceUsed": actorItems.get(id).data.data.levels - hd
}));
return this.object.updateEmbeddedDocuments("Item", classUpdates);
}
/* -------------------------------------------- */
/**
* Rolls the hit die corresponding with the class row containing the event's target button.
* @param {MouseEvent} event
* @private
*/
async _onRollHitDie(event) {
event.preventDefault();
const button = event.currentTarget;
await this.object.rollHitDie(button.dataset.hdDenom);
// Re-render dialog to reflect changed hit dice quantities
this.render();
}
}

View file

@ -3,67 +3,65 @@
* @extends {Dialog} * @extends {Dialog}
*/ */
export default class LongRestDialog extends Dialog { export default class LongRestDialog extends Dialog {
constructor(actor, dialogData = {}, options = {}) { constructor(actor, dialogData = {}, options = {}) {
super(dialogData, options); super(dialogData, options);
this.actor = actor; this.actor = actor;
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @override */ /** @override */
static get defaultOptions() { static get defaultOptions() {
return mergeObject(super.defaultOptions, { return mergeObject(super.defaultOptions, {
template: "systems/sw5e/templates/apps/long-rest.html", template: "systems/sw5e/templates/apps/long-rest.html",
classes: ["sw5e", "dialog"] classes: ["sw5e", "dialog"]
}); });
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @override */ /** @override */
getData() { getData() {
const data = super.getData(); const data = super.getData();
const variant = game.settings.get("sw5e", "restVariant"); const variant = game.settings.get("sw5e", "restVariant");
data.promptNewDay = variant !== "gritty"; // It's always a new day when resting 1 week data.promptNewDay = variant !== "gritty"; // It's always a new day when resting 1 week
data.newDay = variant === "normal"; // It's probably a new day when resting normally (8 hours) data.newDay = variant === "normal"; // It's probably a new day when resting normally (8 hours)
return data; return data;
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
/** /**
* A helper constructor function which displays the Long Rest confirmation dialog and returns a Promise once it's * A helper constructor function which displays the Long Rest confirmation dialog and returns a Promise once it's
* workflow has been resolved. * workflow has been resolved.
* @param {Actor5e} actor * @param {Actor5e} actor
* @return {Promise} * @return {Promise}
*/ */
static async longRestDialog({ actor } = {}) { static async longRestDialog({actor} = {}) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const dlg = new this(actor, { const dlg = new this(actor, {
title: "Long Rest", title: game.i18n.localize("SW5E.LongRest"),
buttons: { buttons: {
rest: { rest: {
icon: '<i class="fas fa-bed"></i>', icon: '<i class="fas fa-bed"></i>',
label: "Rest", label: game.i18n.localize("SW5E.Rest"),
callback: html => { callback: (html) => {
let newDay = false; let newDay = true;
if (game.settings.get("sw5e", "restVariant") === "normal") if (game.settings.get("sw5e", "restVariant") !== "gritty")
newDay = html.find('input[name="newDay"]')[0].checked; newDay = html.find('input[name="newDay"]')[0].checked;
else if(game.settings.get("sw5e", "restVariant") === "gritty") resolve(newDay);
newDay = true; }
resolve(newDay); },
} cancel: {
}, icon: '<i class="fas fa-times"></i>',
cancel: { label: game.i18n.localize("Cancel"),
icon: '<i class="fas fa-times"></i>', callback: reject
label: "Cancel", }
callback: reject },
} default: "rest",
}, close: reject
default: 'rest', });
close: reject dlg.render(true);
}); });
dlg.render(true); }
}); }
}
}

View file

@ -1,38 +1,38 @@
/** /**
* A simple form to set actor movement speeds * A simple form to set actor movement speeds
* @implements {BaseEntitySheet} * @extends {DocumentSheet}
*/ */
export default class ActorMovementConfig extends BaseEntitySheet { export default class ActorMovementConfig extends DocumentSheet {
/** @override */
/** @override */ static get defaultOptions() {
static get defaultOptions() { return foundry.utils.mergeObject(super.defaultOptions, {
return mergeObject(super.defaultOptions, { classes: ["sw5e"],
classes: ["sw5e"], template: "systems/sw5e/templates/apps/movement-config.html",
template: "systems/sw5e/templates/apps/movement-config.html", width: 300,
width: 300, height: "auto"
height: "auto" });
});
}
/* -------------------------------------------- */
/** @override */
get title() {
return `${game.i18n.localize("SW5E.MovementConfig")}: ${this.entity.name}`;
}
/* -------------------------------------------- */
/** @override */
getData(options) {
const data = {
movement: duplicate(this.entity._data.data.attributes.movement),
units: CONFIG.SW5E.movementUnits
} }
for ( let [k, v] of Object.entries(data.movement) ) {
if ( ["units", "hover"].includes(k) ) continue; /* -------------------------------------------- */
data.movement[k] = Number.isNumeric(v) ? v.toNearest(0.1) : 0;
/** @override */
get title() {
return `${game.i18n.localize("SW5E.MovementConfig")}: ${this.document.name}`;
}
/* -------------------------------------------- */
/** @override */
getData(options) {
const sourceMovement = foundry.utils.getProperty(this.document.data._source, "data.attributes.movement") || {};
const data = {
movement: foundry.utils.deepClone(sourceMovement),
units: CONFIG.SW5E.movementUnits
};
for (let [k, v] of Object.entries(data.movement)) {
if (["units", "hover"].includes(k)) continue;
data.movement[k] = Number.isNumeric(v) ? v.toNearest(0.1) : 0;
}
return data;
} }
return data;
}
} }

View file

@ -0,0 +1,66 @@
/**
* A Dialog to prompt the user to select from a list of items.
* @type {Dialog}
*/
export default class SelectItemsPrompt extends Dialog {
constructor(items, dialogData = {}, options = {}) {
super(dialogData, options);
this.options.classes = ["sw5e", "dialog", "select-items-prompt", "sheet"];
/**
* Store a reference to the Item entities being used
* @type {Array<Item5e>}
*/
this.items = items;
}
activateListeners(html) {
super.activateListeners(html);
// render the item's sheet if its image is clicked
html.on("click", ".item-image", (event) => {
const item = this.items.find((feature) => feature.id === event.currentTarget.dataset?.itemId);
item?.sheet.render(true);
});
}
/**
* A constructor function which displays the AddItemPrompt app for a given Actor and Item set.
* Returns a Promise which resolves to the dialog FormData once the workflow has been completed.
* @param {Array<Item5e>} items
* @param {Object} options
* @param {string} options.hint - Localized hint to display at the top of the prompt
* @return {Promise<string[]>} - list of item ids which the user has selected
*/
static async create(items, {hint}) {
// Render the ability usage template
const html = await renderTemplate("systems/sw5e/templates/apps/select-items-prompt.html", {items, hint});
return new Promise((resolve) => {
const dlg = new this(items, {
title: game.i18n.localize("SW5E.SelectItemsPromptTitle"),
content: html,
buttons: {
apply: {
icon: `<i class="fas fa-user-plus"></i>`,
label: game.i18n.localize("SW5E.Apply"),
callback: (html) => {
const fd = new FormDataExtended(html[0].querySelector("form")).toObject();
const selectedIds = Object.keys(fd).filter((itemId) => fd[itemId]);
resolve(selectedIds);
}
},
cancel: {
icon: '<i class="fas fa-forward"></i>',
label: game.i18n.localize("SW5E.Skip"),
callback: () => resolve([])
}
},
default: "apply",
close: () => resolve([])
});
dlg.render(true);
});
}
}

View file

@ -1,43 +1,43 @@
/** /**
* A simple form to set actor movement speeds * A simple form to set Actor movement speeds.
* @implements {BaseEntitySheet} * @extends {DocumentSheet}
*/ */
export default class ActorSensesConfig extends BaseEntitySheet { export default class ActorSensesConfig extends DocumentSheet {
/** @inheritdoc */
/** @override */ static get defaultOptions() {
static get defaultOptions() { return foundry.utils.mergeObject(super.defaultOptions, {
return mergeObject(super.defaultOptions, { classes: ["sw5e"],
classes: ["sw5e"], template: "systems/sw5e/templates/apps/senses-config.html",
template: "systems/sw5e/templates/apps/senses-config.html", width: 300,
width: 300, height: "auto"
height: "auto" });
}); }
}
/* -------------------------------------------- */
/* -------------------------------------------- */
/** @inheritdoc */
/** @override */ get title() {
get title() { return `${game.i18n.localize("SW5E.SensesConfig")}: ${this.document.name}`;
return `${game.i18n.localize("SW5E.SensesConfig")}: ${this.entity.name}`; }
}
/* -------------------------------------------- */
/* -------------------------------------------- */
/** @inheritdoc */
/** @override */ getData(options) {
getData(options) { const senses = foundry.utils.getProperty(this.document.data._source, "data.attributes.senses") || {};
const senses = this.entity._data.data.attributes?.senses ?? {}; const data = {
const data = { senses: {},
senses: {}, special: senses.special ?? "",
special: senses.special ?? "", units: senses.units,
units: senses.units, movementUnits: CONFIG.SW5E.movementUnits movementUnits: CONFIG.SW5E.movementUnits
}; };
for ( let [name, label] of Object.entries(CONFIG.SW5E.senses) ) { for (let [name, label] of Object.entries(CONFIG.SW5E.senses)) {
const v = senses[name]; const v = senses[name];
data.senses[name] = { data.senses[name] = {
label: game.i18n.localize(label), label: game.i18n.localize(label),
value: Number.isNumeric(v) ? v.toNearest(0.1) : 0 value: Number.isNumeric(v) ? v.toNearest(0.1) : 0
} };
}
return data;
} }
return data;
}
} }

View file

@ -5,129 +5,130 @@ import LongRestDialog from "./long-rest.js";
* @extends {Dialog} * @extends {Dialog}
*/ */
export default class ShortRestDialog extends Dialog { export default class ShortRestDialog extends Dialog {
constructor(actor, dialogData={}, options={}) { constructor(actor, dialogData = {}, options = {}) {
super(dialogData, options); super(dialogData, options);
/** /**
* Store a reference to the Actor entity which is resting * Store a reference to the Actor entity which is resting
* @type {Actor} * @type {Actor}
*/ */
this.actor = actor; this.actor = actor;
/** /**
* Track the most recently used HD denomination for re-rendering the form * Track the most recently used HD denomination for re-rendering the form
* @type {string} * @type {string}
*/ */
this._denom = null; this._denom = null;
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @override */ /** @override */
static get defaultOptions() { static get defaultOptions() {
return mergeObject(super.defaultOptions, { return mergeObject(super.defaultOptions, {
template: "systems/sw5e/templates/apps/short-rest.html", template: "systems/sw5e/templates/apps/short-rest.html",
classes: ["sw5e", "dialog"] classes: ["sw5e", "dialog"]
}); });
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @override */ /** @override */
getData() { getData() {
const data = super.getData(); const data = super.getData();
// Determine Hit Dice // Determine Hit Dice
data.availableHD = this.actor.data.items.reduce((hd, item) => { data.availableHD = this.actor.data.items.reduce((hd, item) => {
if ( item.type === "class" ) { if (item.type === "class") {
const d = item.data; const d = item.data.data;
const denom = d.hitDice || "d6"; const denom = d.hitDice || "d6";
const available = parseInt(d.levels || 1) - parseInt(d.hitDiceUsed || 0); const available = parseInt(d.levels || 1) - parseInt(d.hitDiceUsed || 0);
hd[denom] = denom in hd ? hd[denom] + available : available; hd[denom] = denom in hd ? hd[denom] + available : available;
}
return hd;
}, {});
data.canRoll = this.actor.data.data.attributes.hd > 0;
data.denomination = this._denom;
// Determine rest type
const variant = game.settings.get("sw5e", "restVariant");
data.promptNewDay = variant !== "epic"; // It's never a new day when only resting 1 minute
data.newDay = false; // It may be a new day, but not by default
return data;
}
/* -------------------------------------------- */
/** @override */
activateListeners(html) {
super.activateListeners(html);
let btn = html.find("#roll-hd");
btn.click(this._onRollHitDie.bind(this));
}
/* -------------------------------------------- */
/**
* Handle rolling a Hit Die as part of a Short Rest action
* @param {Event} event The triggering click event
* @private
*/
async _onRollHitDie(event) {
event.preventDefault();
const btn = event.currentTarget;
this._denom = btn.form.hd.value;
await this.actor.rollHitDie(this._denom);
this.render();
}
/* -------------------------------------------- */
/**
* A helper constructor function which displays the Short Rest dialog and returns a Promise once it's workflow has
* been resolved.
* @param {Actor5e} actor
* @return {Promise}
*/
static async shortRestDialog({actor}={}) {
return new Promise((resolve, reject) => {
const dlg = new this(actor, {
title: "Short Rest",
buttons: {
rest: {
icon: '<i class="fas fa-bed"></i>',
label: "Rest",
callback: html => {
let newDay = false;
if (game.settings.get("sw5e", "restVariant") === "gritty")
newDay = html.find('input[name="newDay"]')[0].checked;
resolve(newDay);
} }
}, return hd;
cancel: { }, {});
icon: '<i class="fas fa-times"></i>', data.canRoll = this.actor.data.data.attributes.hd > 0;
label: "Cancel", data.denomination = this._denom;
callback: reject
}
},
close: reject
});
dlg.render(true);
});
}
/* -------------------------------------------- */ // Determine rest type
const variant = game.settings.get("sw5e", "restVariant");
data.promptNewDay = variant !== "epic"; // It's never a new day when only resting 1 minute
data.newDay = false; // It may be a new day, but not by default
return data;
}
/** /* -------------------------------------------- */
* A helper constructor function which displays the Long Rest confirmation dialog and returns a Promise once it's
* workflow has been resolved. /** @override */
* @deprecated activateListeners(html) {
* @param {Actor5e} actor super.activateListeners(html);
* @return {Promise} let btn = html.find("#roll-hd");
*/ btn.click(this._onRollHitDie.bind(this));
static async longRestDialog({actor}={}) { }
console.warn("WARNING! ShortRestDialog.longRestDialog has been deprecated, use LongRestDialog.longRestDialog instead.");
return LongRestDialog.longRestDialog(...arguments); /* -------------------------------------------- */
}
/**
* Handle rolling a Hit Die as part of a Short Rest action
* @param {Event} event The triggering click event
* @private
*/
async _onRollHitDie(event) {
event.preventDefault();
const btn = event.currentTarget;
this._denom = btn.form.hd.value;
await this.actor.rollHitDie(this._denom);
this.render();
}
/* -------------------------------------------- */
/**
* A helper constructor function which displays the Short Rest dialog and returns a Promise once it's workflow has
* been resolved.
* @param {Actor5e} actor
* @return {Promise}
*/
static async shortRestDialog({actor} = {}) {
return new Promise((resolve, reject) => {
const dlg = new this(actor, {
title: game.i18n.localize("SW5E.ShortRest"),
buttons: {
rest: {
icon: '<i class="fas fa-bed"></i>',
label: game.i18n.localize("SW5E.Rest"),
callback: (html) => {
let newDay = false;
if (game.settings.get("sw5e", "restVariant") === "gritty")
newDay = html.find('input[name="newDay"]')[0].checked;
resolve(newDay);
}
},
cancel: {
icon: '<i class="fas fa-times"></i>',
label: game.i18n.localize("Cancel"),
callback: reject
}
},
close: reject
});
dlg.render(true);
});
}
/* -------------------------------------------- */
/**
* A helper constructor function which displays the Long Rest confirmation dialog and returns a Promise once it's
* workflow has been resolved.
* @deprecated
* @param {Actor5e} actor
* @return {Promise}
*/
static async longRestDialog({actor} = {}) {
console.warn(
"WARNING! ShortRestDialog.longRestDialog has been deprecated, use LongRestDialog.longRestDialog instead."
);
return LongRestDialog.longRestDialog(...arguments);
}
} }

View file

@ -1,88 +1,87 @@
/** /**
* A specialized form used to select from a checklist of attributes, traits, or properties * A specialized form used to select from a checklist of attributes, traits, or properties
* @implements {FormApplication} * @extends {DocumentSheet}
*/ */
export default class TraitSelector extends FormApplication { export default class TraitSelector extends DocumentSheet {
/** @inheritdoc */
/** @override */ static get defaultOptions() {
static get defaultOptions() { return foundry.utils.mergeObject(super.defaultOptions, {
return mergeObject(super.defaultOptions, { id: "trait-selector",
id: "trait-selector", classes: ["sw5e", "trait-selector", "subconfig"],
classes: ["sw5e"], title: "Actor Trait Selection",
title: "Actor Trait Selection", template: "systems/sw5e/templates/apps/trait-selector.html",
template: "systems/sw5e/templates/apps/trait-selector.html", width: 320,
width: 320, height: "auto",
height: "auto", choices: {},
choices: {}, allowCustom: true,
allowCustom: true, minimum: 0,
minimum: 0, maximum: null,
maximum: null valueKey: "value",
}); customKey: "custom"
} });
/* -------------------------------------------- */
/**
* Return a reference to the target attribute
* @type {String}
*/
get attribute() {
return this.options.name;
}
/* -------------------------------------------- */
/** @override */
getData() {
// Get current values
let attr = getProperty(this.object._data, this.attribute);
if ( getType(attr) !== "Object" ) attr = {value: [], custom: ""};
// Populate choices
const choices = duplicate(this.options.choices);
for ( let [k, v] of Object.entries(choices) ) {
choices[k] = {
label: v,
chosen: attr ? attr.value.includes(k) : false
}
} }
// Return data /* -------------------------------------------- */
return {
allowCustom: this.options.allowCustom,
choices: choices,
custom: attr ? attr.custom : ""
}
}
/* -------------------------------------------- */ /**
* Return a reference to the target attribute
/** @override */ * @type {string}
_updateObject(event, formData) { */
const updateData = {}; get attribute() {
return this.options.name;
// Obtain choices
const chosen = [];
for ( let [k, v] of Object.entries(formData) ) {
if ( (k !== "custom") && v ) chosen.push(k);
}
updateData[`${this.attribute}.value`] = chosen;
// Validate the number chosen
if ( this.options.minimum && (chosen.length < this.options.minimum) ) {
return ui.notifications.error(`You must choose at least ${this.options.minimum} options`);
}
if ( this.options.maximum && (chosen.length > this.options.maximum) ) {
return ui.notifications.error(`You may choose no more than ${this.options.maximum} options`);
} }
// Include custom /* -------------------------------------------- */
if ( this.options.allowCustom ) {
updateData[`${this.attribute}.custom`] = formData.custom; /** @override */
getData() {
const attr = foundry.utils.getProperty(this.object.data, this.attribute);
const o = this.options;
const value = o.valueKey ? attr[o.valueKey] ?? [] : attr;
const custom = o.customKey ? attr[o.customKey] ?? "" : "";
// Populate choices
const choices = Object.entries(o.choices).reduce((obj, e) => {
let [k, v] = e;
obj[k] = {label: v, chosen: attr ? value.includes(k) : false};
return obj;
}, {});
// Return data
return {
allowCustom: o.allowCustom,
choices: choices,
custom: custom
};
} }
// Update the object /* -------------------------------------------- */
this.object.update(updateData);
} /** @override */
async _updateObject(event, formData) {
const o = this.options;
// Obtain choices
const chosen = [];
for (let [k, v] of Object.entries(formData)) {
if (k !== "custom" && v) chosen.push(k);
}
// Object including custom data
const updateData = {};
if (o.valueKey) updateData[`${this.attribute}.${o.valueKey}`] = chosen;
else updateData[this.attribute] = chosen;
if (o.allowCustom) updateData[`${this.attribute}.${o.customKey}`] = formData.custom;
// Validate the number chosen
if (o.minimum && chosen.length < o.minimum) {
return ui.notifications.error(`You must choose at least ${o.minimum} options`);
}
if (o.maximum && chosen.length > o.maximum) {
return ui.notifications.error(`You may choose no more than ${o.maximum} options`);
}
// Update the object
this.object.update(updateData);
}
} }

View file

@ -1,54 +1,38 @@
/** @override */ /** @override */
export const measureDistances = function(segments, options={}) { export const measureDistances = function (segments, options = {}) {
if ( !options.gridSpaces ) return BaseGrid.prototype.measureDistances.call(this, segments, options); if (!options.gridSpaces) return BaseGrid.prototype.measureDistances.call(this, segments, options);
// Track the total number of diagonals // Track the total number of diagonals
let nDiagonal = 0; let nDiagonal = 0;
const rule = this.parent.diagonalRule; const rule = this.parent.diagonalRule;
const d = canvas.dimensions; const d = canvas.dimensions;
// Iterate over measured segments // Iterate over measured segments
return segments.map(s => { return segments.map((s) => {
let r = s.ray; let r = s.ray;
// Determine the total distance traveled // Determine the total distance traveled
let nx = Math.abs(Math.ceil(r.dx / d.size)); let nx = Math.abs(Math.ceil(r.dx / d.size));
let ny = Math.abs(Math.ceil(r.dy / d.size)); let ny = Math.abs(Math.ceil(r.dy / d.size));
// Determine the number of straight and diagonal moves // Determine the number of straight and diagonal moves
let nd = Math.min(nx, ny); let nd = Math.min(nx, ny);
let ns = Math.abs(ny - nx); let ns = Math.abs(ny - nx);
nDiagonal += nd; nDiagonal += nd;
// Alternative DMG Movement // Alternative DMG Movement
if (rule === "5105") { if (rule === "5105") {
let nd10 = Math.floor(nDiagonal / 2) - Math.floor((nDiagonal - nd) / 2); let nd10 = Math.floor(nDiagonal / 2) - Math.floor((nDiagonal - nd) / 2);
let spaces = (nd10 * 2) + (nd - nd10) + ns; let spaces = nd10 * 2 + (nd - nd10) + ns;
return spaces * canvas.dimensions.distance; return spaces * canvas.dimensions.distance;
} }
// Euclidean Measurement // Euclidean Measurement
else if (rule === "EUCL") { else if (rule === "EUCL") {
return Math.round(Math.hypot(nx, ny) * canvas.scene.data.gridDistance); return Math.round(Math.hypot(nx, ny) * canvas.scene.data.gridDistance);
} }
// Standard PHB Movement // Standard PHB Movement
else return (ns + nd) * canvas.scene.data.gridDistance; else return (ns + nd) * canvas.scene.data.gridDistance;
}); });
};
/* -------------------------------------------- */
/**
* Hijack Token health bar rendering to include temporary and temp-max health in the bar display
* TODO: This should probably be replaced with a formal Token class extension
*/
const _TokenGetBarAttribute = Token.prototype.getBarAttribute;
export const getBarAttribute = function(...args) {
const data = _TokenGetBarAttribute.bind(this)(...args);
if ( data && (data.attribute === "attributes.hp") ) {
data.value += parseInt(getProperty(this.actor.data, "data.attributes.hp.temp") || 0);
data.max += parseInt(getProperty(this.actor.data, "data.attributes.hp.tempmax") || 0);
}
return data;
}; };

View file

@ -1,121 +1,326 @@
export default class CharacterImporter { export default class CharacterImporter {
// transform JSON from sw5e.com to Foundry friendly format
// and insert new actor
static async transform(rawCharacter) {
const sourceCharacter = JSON.parse(rawCharacter); //source character
// transform JSON from sw5e.com to Foundry friendly format const details = {
// and insert new actor species: sourceCharacter.attribs.find((e) => e.name == "race").current,
static async transform(rawCharacter){ background: sourceCharacter.attribs.find((e) => e.name == "background").current,
const sourceCharacter = JSON.parse(rawCharacter); //source character alignment: sourceCharacter.attribs.find((e) => e.name == "alignment").current
};
const details = {
species: sourceCharacter.attribs.find(e => e.name == "race").current, const hp = {
background: sourceCharacter.attribs.find(e => e.name == "background").current, value: sourceCharacter.attribs.find((e) => e.name == "hp").current,
alignment: sourceCharacter.attribs.find(e => e.name == "alignment").current min: 0,
max: sourceCharacter.attribs.find((e) => e.name == "hp").current,
temp: sourceCharacter.attribs.find((e) => e.name == "hp_temp").current
};
const abilities = {
str: {
value: sourceCharacter.attribs.find((e) => e.name == "strength").current,
proficient: sourceCharacter.attribs.find((e) => e.name == "strength_save_prof").current ? 1 : 0
},
dex: {
value: sourceCharacter.attribs.find((e) => e.name == "dexterity").current,
proficient: sourceCharacter.attribs.find((e) => e.name == "dexterity_save_prof").current ? 1 : 0
},
con: {
value: sourceCharacter.attribs.find((e) => e.name == "constitution").current,
proficient: sourceCharacter.attribs.find((e) => e.name == "constitution_save_prof").current ? 1 : 0
},
int: {
value: sourceCharacter.attribs.find((e) => e.name == "intelligence").current,
proficient: sourceCharacter.attribs.find((e) => e.name == "intelligence_save_prof").current ? 1 : 0
},
wis: {
value: sourceCharacter.attribs.find((e) => e.name == "wisdom").current,
proficient: sourceCharacter.attribs.find((e) => e.name == "wisdom_save_prof").current ? 1 : 0
},
cha: {
value: sourceCharacter.attribs.find((e) => e.name == "charisma").current,
proficient: sourceCharacter.attribs.find((e) => e.name == "charisma_save_prof").current ? 1 : 0
}
};
/* ----------------------------------------------------------------- */
/* character.data.skills.<skill_name>.value is all that matters
/* values can be 0, 0.5, 1 or 2
/* 0 = regular
/* 0.5 = half-proficient
/* 1 = proficient
/* 2 = expertise
/* foundry takes care of calculating the rest
/* ----------------------------------------------------------------- */
const skills = {
acr: {
value: sourceCharacter.attribs.find((e) => e.name == "acrobatics_type").current
},
ani: {
value: sourceCharacter.attribs.find((e) => e.name == "animal_handling_type").current
},
ath: {
value: sourceCharacter.attribs.find((e) => e.name == "athletics_type").current
},
dec: {
value: sourceCharacter.attribs.find((e) => e.name == "deception_type").current
},
ins: {
value: sourceCharacter.attribs.find((e) => e.name == "insight_type").current
},
inv: {
value: sourceCharacter.attribs.find((e) => e.name == "investigation_type").current
},
itm: {
value: sourceCharacter.attribs.find((e) => e.name == "intimidation_type").current
},
lor: {
value: sourceCharacter.attribs.find((e) => e.name == "lore_type").current
},
med: {
value: sourceCharacter.attribs.find((e) => e.name == "medicine_type").current
},
nat: {
value: sourceCharacter.attribs.find((e) => e.name == "nature_type").current
},
per: {
value: sourceCharacter.attribs.find((e) => e.name == "persuasion_type").current
},
pil: {
value: sourceCharacter.attribs.find((e) => e.name == "piloting_type").current
},
prc: {
value: sourceCharacter.attribs.find((e) => e.name == "perception_type").current
},
prf: {
value: sourceCharacter.attribs.find((e) => e.name == "performance_type").current
},
slt: {
value: sourceCharacter.attribs.find((e) => e.name == "sleight_of_hand_type").current
},
ste: {
value: sourceCharacter.attribs.find((e) => e.name == "stealth_type").current
},
sur: {
value: sourceCharacter.attribs.find((e) => e.name == "survival_type").current
},
tec: {
value: sourceCharacter.attribs.find((e) => e.name == "technology_type").current
}
};
const targetCharacter = {
name: sourceCharacter.name,
type: "character",
data: {
abilities: abilities,
details: details,
skills: skills,
attributes: {
hp: hp
}
}
};
let actor = await Actor.create(targetCharacter);
CharacterImporter.addProfessions(sourceCharacter, actor);
} }
const hp = {
value: sourceCharacter.attribs.find(e => e.name == "hp").current,
min: 0,
max: sourceCharacter.attribs.find(e => e.name == "hp").current,
temp: sourceCharacter.attribs.find(e => e.name == "hp_temp").current
};
const ac = { // Parse all classes and add them to already created actor.
value: sourceCharacter.attribs.find(e => e.name == "ac").current // "class" is a reserved word, therefore I use profession where I can.
}; static async addProfessions(sourceCharacter, actor) {
let result = [];
const abilities = { // parse all class and multiclassX items
str: { // couldn't get Array.filter to work here for some reason
value: sourceCharacter.attribs.find(e => e.name == "strength").current, // result = array of objects. each object is a separate class
proficient: sourceCharacter.attribs.find(e => e.name == 'strength_save_prof').current ? 1 : 0 sourceCharacter.attribs.forEach((e) => {
}, if (CharacterImporter.classOrMulticlass(e.name)) {
dex: { var t = {
value: sourceCharacter.attribs.find(e => e.name == "dexterity").current, profession: CharacterImporter.capitalize(e.current),
proficient: sourceCharacter.attribs.find(e => e.name == 'dexterity_save_prof').current ? 1 : 0 type: CharacterImporter.baseOrMulti(e.name),
}, level: CharacterImporter.getLevel(e, sourceCharacter)
con: { };
value: sourceCharacter.attribs.find(e => e.name == "constitution").current, result.push(t);
proficient: sourceCharacter.attribs.find(e => e.name == 'constitution_save_prof').current ? 1 : 0 }
}, });
int: {
value: sourceCharacter.attribs.find(e => e.name == "intelligence").current,
proficient: sourceCharacter.attribs.find(e => e.name == 'intelligence_save_prof').current ? 1 : 0
},
wis: {
value: sourceCharacter.attribs.find(e => e.name == "wisdom").current,
proficient: sourceCharacter.attribs.find(e => e.name == 'wisdom_save_prof').current ? 1 : 0
},
cha: {
value: sourceCharacter.attribs.find(e => e.name == "charisma").current,
proficient: sourceCharacter.attribs.find(e => e.name == 'charisma_save_prof').current ? 1 : 0
},
};
const targetCharacter = { // pull classes directly from system compendium and add them to current actor
name: sourceCharacter.name, const professionsPack = await game.packs.get("sw5e.classes").getDocuments();
type: "character", result.forEach((prof) => {
data: { let assignedProfession = professionsPack.find((o) => o.name === prof.profession);
abilities: abilities, assignedProfession.data.data.levels = prof.level;
details: details, actor.createEmbeddedDocuments("Item", [assignedProfession.data], {displaySheet: false});
attributes: { });
ac: ac,
hp: hp this.addSpecies(sourceCharacter.attribs.find((e) => e.name == "race").current, actor);
this.addPowers(
sourceCharacter.attribs
.filter((e) => e.name.search(/repeating_power.+_powername/g) != -1)
.map((e) => e.current),
actor
);
const discoveredItems = sourceCharacter.attribs.filter(
(e) => e.name.search(/repeating_inventory.+_itemname/g) != -1
);
const items = discoveredItems.map((item) => {
const id = item.name.match(/-\w{19}/g);
return {
name: item.current,
quantity: sourceCharacter.attribs.find((e) => e.name === `repeating_inventory_${id}_itemcount`).current
};
});
this.addItems(items, actor);
}
static async addClasses(profession, level, actor) {
let classes = await game.packs.get("sw5e.classes").getDocuments();
let assignedClass = classes.find((c) => c.name === profession);
assignedClass.data.data.levels = level;
await actor.createEmbeddedDocuments("Item", [assignedClass.data], {displaySheet: false});
}
static classOrMulticlass(name) {
return name === "class" || (name.includes("multiclass") && name.length <= 12);
}
static baseOrMulti(name) {
if (name === "class") {
return "base_class";
} else {
return "multi_class";
} }
} }
};
let actor = await Actor.create(targetCharacter); static getLevel(item, sourceCharacter) {
if (item.name === "class") {
const profession = sourceCharacter.attribs.find(e => e.name == "class").current; let result = sourceCharacter.attribs.find((e) => e.name === "base_level").current;
let professionLevel = sourceCharacter.attribs.find(e => e.name == "class_display").current; return parseInt(result);
professionLevel = parseInt( professionLevel.replace(/[^0-9]/g,'') ); //remove a-z, leaving only integers } else {
CharacterImporter.addClasses(profession, professionLevel, actor); let result = sourceCharacter.attribs.find((e) => e.name === `${item.name}_lvl`).current;
} return parseInt(result);
static async addClasses(profession, level, actor){
let classes = await game.packs.get('sw5e.classes').getContent();
let assignedClass = classes.find( c => c.name === profession );
assignedClass.data.data.levels = level;
await actor.createEmbeddedEntity("OwnedItem", assignedClass.data, { displaySheet: false });
}
static addImportButton(html){
const header = $("#actors").find("header.directory-header");
const search = $("#actors").children().find("div.header-search");
const newImportButtonDiv = $("#actors").children().find("div.header-actions").clone();
const newSearch = search.clone();
search.remove();
newImportButtonDiv.attr('id', 'character-sheet-import');
header.append(newImportButtonDiv);
newImportButtonDiv.children("button").remove();
newImportButtonDiv.append("<button class='create-entity' id='cs-import-button'><i class='fas fa-upload'></i> Import Character</button>");
newSearch.appendTo(header);
let characterImportButton = $("#cs-import-button");
characterImportButton.click(ev => {
let content = '<h1>Saved Character JSON Import</h1> '
+ '<label for="character-json">Paste character JSON here:</label> '
+ '</br>'
+ '<textarea id="character-json" name="character-json" rows="10" cols="50"></textarea>';
let importDialog = new Dialog({
title: "Import Character from SW5e.com",
content: content,
buttons: {
"Import": {
icon: '<i class="fas fa-file-import"></i>',
label: "Import Character",
callback: (e) => {
let characterData = $('#character-json').val();
console.log('Parsing Character JSON');
CharacterImporter.transform(characterData);
}
},
"Cancel": {
icon: '<i class="fas fa-times-circle"></i>',
label: "Cancel",
callback: () => {},
}
} }
}) }
importDialog.render(true);
}); static capitalize(str) {
} return str.charAt(0).toUpperCase() + str.slice(1);
} }
static async addSpecies(race, actor) {
const species = await game.packs.get("sw5e.species").getDocuments();
const assignedSpecies = species.find((c) => c.name === race);
const activeEffects = [...assignedSpecies.data.effects][0].data.changes;
const actorData = {data: {abilities: {...actor.data.data.abilities}}};
activeEffects.map((effect) => {
switch (effect.key) {
case "data.abilities.str.value":
actorData.data.abilities.str.value -= effect.value;
break;
case "data.abilities.dex.value":
actorData.data.abilities.dex.value -= effect.value;
break;
case "data.abilities.con.value":
actorData.data.abilities.con.value -= effect.value;
break;
case "data.abilities.int.value":
actorData.data.abilities.int.value -= effect.value;
break;
case "data.abilities.wis.value":
actorData.data.abilities.wis.value -= effect.value;
break;
case "data.abilities.cha.value":
actorData.data.abilities.cha.value -= effect.value;
break;
default:
break;
}
});
actor.update(actorData);
await actor.createEmbeddedDocuments("Item", [assignedSpecies.data], {displaySheet: false});
}
static async addPowers(powers, actor) {
const forcePowers = await game.packs.get("sw5e.forcepowers").getDocuments();
const techPowers = await game.packs.get("sw5e.techpowers").getDocuments();
for (const power of powers) {
const createdPower = forcePowers.find((c) => c.name === power) || techPowers.find((c) => c.name === power);
if (createdPower) {
await actor.createEmbeddedDocuments("Item", [createdPower.data], {displaySheet: false});
}
}
}
static async addItems(items, actor) {
const weapons = await game.packs.get("sw5e.weapons").getDocuments();
const armors = await game.packs.get("sw5e.armor").getDocuments();
const adventuringGear = await game.packs.get("sw5e.adventuringgear").getDocuments();
for (const item of items) {
const createdItem =
weapons.find((c) => c.name.toLowerCase() === item.name.toLowerCase()) ||
armors.find((c) => c.name.toLowerCase() === item.name.toLowerCase()) ||
adventuringGear.find((c) => c.name.toLowerCase() === item.name.toLowerCase());
if (createdItem) {
if (item.quantity != 1) {
createdItem.data.data.quantity = item.quantity;
}
await actor.createEmbeddedDocuments("Item", [createdItem.data], {displaySheet: false});
}
}
}
static addImportButton(html) {
const actionButtons = html.find(".header-actions");
actionButtons[0].insertAdjacentHTML(
"afterend",
`<div class="header-actions action-buttons flexrow"><button class="create-entity cs-import-button"><i class="fas fa-upload"></i> Import Character</button></div>`
);
let characterImportButton = $(".cs-import-button");
characterImportButton.click(() => {
let content = `<h1>Saved Character JSON Import</h1>
<label for="character-json">Paste character JSON here:</label>
</br>
<textarea id="character-json" name="character-json" rows="10" cols="50"></textarea>`;
let importDialog = new Dialog({
title: "Import Character from SW5e.com",
content: content,
buttons: {
Import: {
icon: `<i class="fas fa-file-import"></i>`,
label: "Import Character",
callback: () => {
let characterData = $("#character-json").val();
console.log("Parsing Character JSON");
CharacterImporter.transform(characterData);
}
},
Cancel: {
icon: `<i class="fas fa-times-circle"></i>`,
label: "Cancel",
callback: () => {}
}
}
});
importDialog.render(true);
});
}
}

View file

@ -1,30 +1,29 @@
/** /**
* Highlight critical success or failure on d20 rolls * Highlight critical success or failure on d20 rolls
*/ */
export const highlightCriticalSuccessFailure = function(message, html, data) { export const highlightCriticalSuccessFailure = function (message, html, data) {
if ( !message.isRoll || !message.isContentVisible ) return; if (!message.isRoll || !message.isContentVisible) return;
// Highlight rolls where the first part is a d20 roll // Highlight rolls where the first part is a d20 roll
const roll = message.roll; const roll = message.roll;
if ( !roll.dice.length ) return; if (!roll.dice.length) return;
const d = roll.dice[0]; const d = roll.dice[0];
// Ensure it is an un-modified d20 roll // Ensure it is an un-modified d20 roll
const isD20 = (d.faces === 20) && ( d.values.length === 1 ); const isD20 = d.faces === 20 && d.values.length === 1;
if ( !isD20 ) return; if (!isD20) return;
const isModifiedRoll = ("success" in d.results[0]) || d.options.marginSuccess || d.options.marginFailure; const isModifiedRoll = "success" in d.results[0] || d.options.marginSuccess || d.options.marginFailure;
if ( isModifiedRoll ) return; if (isModifiedRoll) return;
// Highlight successes and failures // Highlight successes and failures
const critical = d.options.critical || 20; const critical = d.options.critical || 20;
const fumble = d.options.fumble || 1; const fumble = d.options.fumble || 1;
if ( d.total >= critical ) html.find(".dice-total").addClass("critical"); if (d.total >= critical) html.find(".dice-total").addClass("critical");
else if ( d.total <= fumble ) html.find(".dice-total").addClass("fumble"); else if (d.total <= fumble) html.find(".dice-total").addClass("fumble");
else if ( d.options.target ) { else if (d.options.target) {
if ( roll.total >= d.options.target ) html.find(".dice-total").addClass("success"); if (roll.total >= d.options.target) html.find(".dice-total").addClass("success");
else html.find(".dice-total").addClass("failure"); else html.find(".dice-total").addClass("failure");
} }
}; };
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -32,24 +31,24 @@ export const highlightCriticalSuccessFailure = function(message, html, data) {
/** /**
* Optionally hide the display of chat card action buttons which cannot be performed by the user * Optionally hide the display of chat card action buttons which cannot be performed by the user
*/ */
export const displayChatActionButtons = function(message, html, data) { export const displayChatActionButtons = function (message, html, data) {
const chatCard = html.find(".sw5e.chat-card"); const chatCard = html.find(".sw5e.chat-card");
if ( chatCard.length > 0 ) { if (chatCard.length > 0) {
const flavor = html.find(".flavor-text"); const flavor = html.find(".flavor-text");
if ( flavor.text() === html.find(".item-name").text() ) flavor.remove(); if (flavor.text() === html.find(".item-name").text()) flavor.remove();
// If the user is the message author or the actor owner, proceed // If the user is the message author or the actor owner, proceed
let actor = game.actors.get(data.message.speaker.actor); let actor = game.actors.get(data.message.speaker.actor);
if ( actor && actor.owner ) return; if (actor && actor.isOwner) return;
else if ( game.user.isGM || (data.author.id === game.user.id)) return; else if (game.user.isGM || data.author.id === game.user.id) return;
// Otherwise conceal action buttons except for saving throw // Otherwise conceal action buttons except for saving throw
const buttons = chatCard.find("button[data-action]"); const buttons = chatCard.find("button[data-action]");
buttons.each((i, btn) => { buttons.each((i, btn) => {
if ( btn.dataset.action === "save" ) return; if (btn.dataset.action === "save") return;
btn.style.display = "none" btn.style.display = "none";
}); });
} }
}; };
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -63,38 +62,38 @@ export const displayChatActionButtons = function(message, html, data) {
* *
* @return {Array} The extended options Array including new context choices * @return {Array} The extended options Array including new context choices
*/ */
export const addChatMessageContextOptions = function(html, options) { export const addChatMessageContextOptions = function (html, options) {
let canApply = li => { let canApply = (li) => {
const message = game.messages.get(li.data("messageId")); const message = game.messages.get(li.data("messageId"));
return message?.isRoll && message?.isContentVisible && canvas?.tokens.controlled.length; return message?.isRoll && message?.isContentVisible && canvas.tokens?.controlled.length;
}; };
options.push( options.push(
{ {
name: game.i18n.localize("SW5E.ChatContextDamage"), name: game.i18n.localize("SW5E.ChatContextDamage"),
icon: '<i class="fas fa-user-minus"></i>', icon: '<i class="fas fa-user-minus"></i>',
condition: canApply, condition: canApply,
callback: li => applyChatCardDamage(li, 1) callback: (li) => applyChatCardDamage(li, 1)
}, },
{ {
name: game.i18n.localize("SW5E.ChatContextHealing"), name: game.i18n.localize("SW5E.ChatContextHealing"),
icon: '<i class="fas fa-user-plus"></i>', icon: '<i class="fas fa-user-plus"></i>',
condition: canApply, condition: canApply,
callback: li => applyChatCardDamage(li, -1) callback: (li) => applyChatCardDamage(li, -1)
}, },
{ {
name: game.i18n.localize("SW5E.ChatContextDoubleDamage"), name: game.i18n.localize("SW5E.ChatContextDoubleDamage"),
icon: '<i class="fas fa-user-injured"></i>', icon: '<i class="fas fa-user-injured"></i>',
condition: canApply, condition: canApply,
callback: li => applyChatCardDamage(li, 2) callback: (li) => applyChatCardDamage(li, 2)
}, },
{ {
name: game.i18n.localize("SW5E.ChatContextHalfDamage"), name: game.i18n.localize("SW5E.ChatContextHalfDamage"),
icon: '<i class="fas fa-user-shield"></i>', icon: '<i class="fas fa-user-shield"></i>',
condition: canApply, condition: canApply,
callback: li => applyChatCardDamage(li, 0.5) callback: (li) => applyChatCardDamage(li, 0.5)
} }
); );
return options; return options;
}; };
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -108,12 +107,14 @@ export const addChatMessageContextOptions = function(html, options) {
* @return {Promise} * @return {Promise}
*/ */
function applyChatCardDamage(li, multiplier) { function applyChatCardDamage(li, multiplier) {
const message = game.messages.get(li.data("messageId")); const message = game.messages.get(li.data("messageId"));
const roll = message.roll; const roll = message.roll;
return Promise.all(canvas.tokens.controlled.map(t => { return Promise.all(
const a = t.actor; canvas.tokens.controlled.map((t) => {
return a.applyDamage(roll.total, multiplier); const a = t.actor;
})); return a.applyDamage(roll.total, multiplier);
})
);
} }
/* -------------------------------------------- */ /* -------------------------------------------- */

View file

@ -1,4 +1 @@
export const ClassFeatures = { export const ClassFeatures = {};
};

View file

@ -1,28 +1,31 @@
/** /**
* Override the default Initiative formula to customize special behaviors of the SW5e system. * Override the default Initiative formula to customize special behaviors of the SW5e system.
* Apply advantage, proficiency, or bonuses where appropriate * Apply advantage, proficiency, or bonuses where appropriate
* Apply the dexterity score as a decimal tiebreaker if requested * Apply the dexterity score as a decimal tiebreaker if requested
* See Combat._getInitiativeFormula for more detail. * See Combat._getInitiativeFormula for more detail.
*/ */
export const _getInitiativeFormula = function(combatant) { export const _getInitiativeFormula = function () {
const actor = combatant.actor; const actor = this.actor;
if ( !actor ) return "1d20"; if (!actor) return "1d20";
const init = actor.data.data.attributes.init; const init = actor.data.data.attributes.init;
let nd = 1; // Construct initiative formula parts
let mods = ""; let nd = 1;
let mods = "";
if (actor.getFlag("sw5e", "halflingLucky")) mods += "r1=1";
if (actor.getFlag("sw5e", "initiativeAdv")) {
nd = 2;
mods += "kh";
}
const parts = [
`${nd}d20${mods}`,
init.mod,
init.prof !== 0 ? init.prof : null,
init.bonus !== 0 ? init.bonus : null
];
if (actor.getFlag("sw5e", "halflingLucky")) mods += "r1=1"; // Optionally apply Dexterity tiebreaker
if (actor.getFlag("sw5e", "initiativeAdv")) { const tiebreaker = game.settings.get("sw5e", "initiativeDexTiebreaker");
nd = 2; if (tiebreaker) parts.push(actor.data.data.abilities.dex.value / 100);
mods += "kh"; return parts.filter((p) => p !== null).join(" + ");
}
const parts = [`${nd}d20${mods}`, init.mod, (init.prof !== 0) ? init.prof : null, (init.bonus !== 0) ? init.bonus : null];
// Optionally apply Dexterity tiebreaker
const tiebreaker = game.settings.get("sw5e", "initiativeDexTiebreaker");
if ( tiebreaker ) parts.push(actor.data.data.abilities.dex.value / 100);
return parts.filter(p => p !== null).join(" + ");
}; };

File diff suppressed because it is too large Load diff

View file

@ -1,3 +1,6 @@
export {default as D20Roll} from "./dice/d20-roll.js";
export {default as DamageRoll} from "./dice/damage-roll.js";
/** /**
* A standardized helper function for simplifying the constant parts of a multipart roll formula * A standardized helper function for simplifying the constant parts of a multipart roll formula
* *
@ -9,42 +12,55 @@
* @return {string} The resulting simplified formula * @return {string} The resulting simplified formula
*/ */
export function simplifyRollFormula(formula, data, {constantFirst = false} = {}) { export function simplifyRollFormula(formula, data, {constantFirst = false} = {}) {
const roll = new Roll(formula, data); // Parses the formula and replaces any @properties const roll = new Roll(formula, data); // Parses the formula and replaces any @properties
const terms = roll.terms; const terms = roll.terms;
// Some terms are "too complicated" for this algorithm to simplify // Some terms are "too complicated" for this algorithm to simplify
// In this case, the original formula is returned. // In this case, the original formula is returned.
if (terms.some(_isUnsupportedTerm)) return roll.formula; if (terms.some(_isUnsupportedTerm)) return roll.formula;
const rollableTerms = []; // Terms that are non-constant, and their associated operators const rollableTerms = []; // Terms that are non-constant, and their associated operators
const constantTerms = []; // Terms that are constant, and their associated operators const constantTerms = []; // Terms that are constant, and their associated operators
let operators = []; // Temporary storage for operators before they are moved to one of the above let operators = []; // Temporary storage for operators before they are moved to one of the above
for (let term of terms) { // For each term for (let term of terms) {
if (["+", "-"].includes(term)) operators.push(term); // If the term is an addition/subtraction operator, push the term into the operators array // For each term
else { // Otherwise the term is not an operator if (term instanceof OperatorTerm) operators.push(term);
if (term instanceof DiceTerm) { // If the term is something rollable // If the term is an addition/subtraction operator, push the term into the operators array
rollableTerms.push(...operators); // Place all the operators into the rollableTerms array else {
rollableTerms.push(term); // Then place this rollable term into it as well // Otherwise the term is not an operator
} // if (term instanceof DiceTerm) {
else { // Otherwise, this must be a constant // If the term is something rollable
constantTerms.push(...operators); // Place the operators into the constantTerms array rollableTerms.push(...operators); // Place all the operators into the rollableTerms array
constantTerms.push(term); // Then also add this constant term to that array. rollableTerms.push(term); // Then place this rollable term into it as well
} // } //
operators = []; // Finally, the operators have now all been assigend to one of the arrays, so empty this before the next iteration. else {
// Otherwise, this must be a constant
constantTerms.push(...operators); // Place the operators into the constantTerms array
constantTerms.push(term); // Then also add this constant term to that array.
} //
operators = []; // Finally, the operators have now all been assigend to one of the arrays, so empty this before the next iteration.
}
} }
}
const constantFormula = Roll.cleanFormula(constantTerms); // Cleans up the constant terms and produces a new formula string const constantFormula = Roll.getFormula(constantTerms); // Cleans up the constant terms and produces a new formula string
const rollableFormula = Roll.cleanFormula(rollableTerms); // Cleans up the non-constant terms and produces a new formula string const rollableFormula = Roll.getFormula(rollableTerms); // Cleans up the non-constant terms and produces a new formula string
const constantPart = roll._safeEval(constantFormula); // Mathematically evaluate the constant formula to produce a single constant term // Mathematically evaluate the constant formula to produce a single constant term
let constantPart = undefined;
if (constantFormula) {
try {
constantPart = Roll.safeEval(constantFormula);
} catch (err) {
console.warn(`Unable to evaluate constant term ${constantFormula} in simplifyRollFormula`);
}
}
const parts = constantFirst ? // Order the rollable and constant terms, either constant first or second depending on the optional argumen // Order the rollable and constant terms, either constant first or second depending on the optional argument
[constantPart, rollableFormula] : [rollableFormula, constantPart]; const parts = constantFirst ? [constantPart, rollableFormula] : [rollableFormula, constantPart];
// Join the parts with a + sign, pass them to `Roll` once again to clean up the formula // Join the parts with a + sign, pass them to `Roll` once again to clean up the formula
return new Roll(parts.filterJoin(" + ")).formula; return new Roll(parts.filterJoin(" + ")).formula;
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -55,316 +71,243 @@ export function simplifyRollFormula(formula, data, {constantFirst = false} = {})
* @return {Boolean} True when unsupported, false if supported * @return {Boolean} True when unsupported, false if supported
*/ */
function _isUnsupportedTerm(term) { function _isUnsupportedTerm(term) {
const diceTerm = term instanceof DiceTerm; const diceTerm = term instanceof DiceTerm;
const operator = ["+", "-"].includes(term); const operator = term instanceof OperatorTerm && ["+", "-"].includes(term.operator);
const number = !isNaN(Number(term)); const number = term instanceof NumericTerm;
return !(diceTerm || operator || number); return !(diceTerm || operator || number);
} }
/* -------------------------------------------- */
/* D20 Roll */
/* -------------------------------------------- */ /* -------------------------------------------- */
/** /**
* A standardized helper function for managing core 5e "d20 rolls" * A standardized helper function for managing core 5e d20 rolls.
*
* Holding SHIFT, ALT, or CTRL when the attack is rolled will "fast-forward". * Holding SHIFT, ALT, or CTRL when the attack is rolled will "fast-forward".
* This chooses the default options of a normal attack with no bonus, Advantage, or Disadvantage respectively * This chooses the default options of a normal attack with no bonus, Advantage, or Disadvantage respectively
* *
* @param {Array} parts The dice roll component parts, excluding the initial d20 * @param {string[]} parts The dice roll component parts, excluding the initial d20
* @param {Object} data Actor or item data against which to parse the roll * @param {object} data Actor or item data against which to parse the roll
* @param {Event|object} event The triggering event which initiated the roll
* @param {string} rollMode A specific roll mode to apply as the default for the resulting roll
* @param {string|null} template The HTML template used to render the roll dialog
* @param {string|null} title The dice roll UI window title
* @param {Object} speaker The ChatMessage speaker to pass when creating the chat
* @param {string|null} flavor Flavor text to use in the posted chat message
* @param {Boolean} fastForward Allow fast-forward advantage selection
* @param {Function} onClose Callback for actions to take when the dialog form is closed
* @param {Object} dialogOptions Modal dialog options
* @param {boolean} advantage Apply advantage to the roll (unless otherwise specified)
* @param {boolean} disadvantage Apply disadvantage to the roll (unless otherwise specified)
* @param {number} critical The value of d20 result which represents a critical success
* @param {number} fumble The value of d20 result which represents a critical failure
* @param {number} targetValue Assign a target value against which the result of this roll should be compared
* @param {boolean} elvenAccuracy Allow Elven Accuracy to modify this roll?
* @param {boolean} halflingLucky Allow Halfling Luck to modify this roll?
* @param {boolean} reliableTalent Allow Reliable Talent to modify this roll?
* @param {boolean} chatMessage Automatically create a Chat Message for the result of this roll
* @param {object} messageData Additional data which is applied to the created Chat Message, if any
* *
* @return {Promise} A Promise which resolves once the roll workflow has completed * @param {boolean} [advantage] Apply advantage to the roll (unless otherwise specified)
* @param {boolean} [disadvantage] Apply disadvantage to the roll (unless otherwise specified)
* @param {number} [critical] The value of d20 result which represents a critical success
* @param {number} [fumble] The value of d20 result which represents a critical failure
* @param {number} [targetValue] Assign a target value against which the result of this roll should be compared
* @param {boolean} [elvenAccuracy] Allow Elven Accuracy to modify this roll?
* @param {boolean} [halflingLucky] Allow Halfling Luck to modify this roll?
* @param {boolean} [reliableTalent] Allow Reliable Talent to modify this roll?
* @param {boolean} [chooseModifier=false] Choose the ability modifier that should be used when the roll is made
* @param {boolean} [fastForward=false] Allow fast-forward advantage selection
* @param {Event} [event] The triggering event which initiated the roll
* @param {string} [rollMode] A specific roll mode to apply as the default for the resulting roll
* @param {string} [template] The HTML template used to render the roll dialog
* @param {string} [title] The dialog window title
* @param {Object} [dialogOptions] Modal dialog options
*
* @param {boolean} [chatMessage=true] Automatically create a Chat Message for the result of this roll
* @param {object} [messageData={}] Additional data which is applied to the created Chat Message, if any
* @param {string} [rollMode] A specific roll mode to apply as the default for the resulting roll
* @param {object} [speaker] The ChatMessage speaker to pass when creating the chat
* @param {string} [flavor] Flavor text to use in the posted chat message
*
* @return {Promise<D20Roll|null>} The evaluated D20Roll, or null if the workflow was cancelled
*/ */
export async function d20Roll({parts=[], data={}, event={}, rollMode=null, template=null, title=null, speaker=null, export async function d20Roll({
flavor=null, fastForward=null, dialogOptions, parts = [],
advantage=null, disadvantage=null, critical=20, fumble=1, targetValue=null, data = {}, // Roll creation
elvenAccuracy=false, halflingLucky=false, reliableTalent=false, advantage,
chatMessage=true, messageData={}}={}) { disadvantage,
fumble = 1,
critical = 20,
targetValue,
elvenAccuracy,
halflingLucky,
reliableTalent, // Roll customization
chooseModifier = false,
fastForward = false,
event,
template,
title,
dialogOptions, // Dialog configuration
chatMessage = true,
messageData = {},
rollMode,
speaker,
flavor // Chat Message customization
} = {}) {
// Handle input arguments
const formula = ["1d20"].concat(parts).join(" + ");
const {advantageMode, isFF} = _determineAdvantageMode({advantage, disadvantage, fastForward, event});
const defaultRollMode = rollMode || game.settings.get("core", "rollMode");
if (chooseModifier && !isFF) data["mod"] = "@mod";
// Prepare Message Data // Construct the D20Roll instance
messageData.flavor = flavor || title; const roll = new CONFIG.Dice.D20Roll(formula, data, {
messageData.speaker = speaker || ChatMessage.getSpeaker(); flavor: flavor || title,
const messageOptions = {rollMode: rollMode || game.settings.get("core", "rollMode")}; advantageMode,
parts = parts.concat(["@bonus"]); defaultRollMode,
critical,
fumble,
targetValue,
elvenAccuracy,
halflingLucky,
reliableTalent
});
// Handle fast-forward events // Prompt a Dialog to further configure the D20Roll
let adv = 0; if (!isFF) {
fastForward = fastForward ?? (event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey)); const configured = await roll.configureDialog(
if (fastForward) { {
if ( advantage ?? event.altKey ) adv = 1; title,
else if ( disadvantage ?? (event.ctrlKey || event.metaKey) ) adv = -1; chooseModifier,
} defaultRollMode: defaultRollMode,
defaultAction: advantageMode,
// Define the inner roll function defaultAbility: data?.item?.ability,
const _roll = (parts, adv, form) => { template
},
// Determine the d20 roll and modifiers dialogOptions
let nd = 1; );
let mods = halflingLucky ? "r1=1" : ""; if (configured === null) return null;
// Handle advantage
if (adv === 1) {
nd = elvenAccuracy ? 3 : 2;
messageData.flavor += ` (${game.i18n.localize("SW5E.Advantage")})`;
if ( "flags.sw5e.roll" in messageData ) messageData["flags.sw5e.roll"].advantage = true;
mods += "kh";
} }
// Handle disadvantage // Evaluate the configured roll
else if (adv === -1) { await roll.evaluate({async: true});
nd = 2;
messageData.flavor += ` (${game.i18n.localize("SW5E.Disadvantage")})`;
if ( "flags.sw5e.roll" in messageData ) messageData["flags.sw5e.roll"].disadvantage = true;
mods += "kl";
}
// Prepend the d20 roll // Create a Chat Message
let formula = `${nd}d20${mods}`; if (speaker) {
if (reliableTalent) formula = `{${nd}d20${mods},10}kh`; console.warn(
parts.unshift(formula); `You are passing the speaker argument to the d20Roll function directly which should instead be passed as an internal key of messageData`
);
// Optionally include a situational bonus messageData.speaker = speaker;
if ( form ) {
data['bonus'] = form.bonus.value;
messageOptions.rollMode = form.rollMode.value;
}
if (!data["bonus"]) parts.pop();
// Optionally include an ability score selection (used for tool checks)
const ability = form ? form.ability : null;
if (ability && ability.value) {
data.ability = ability.value;
const abl = data.abilities[data.ability];
if (abl) {
data.mod = abl.mod;
messageData.flavor += ` (${CONFIG.SW5E.abilities[data.ability]})`;
}
}
// Execute the roll
let roll = new Roll(parts.join(" + "), data);
try {
roll.roll();
} catch (err) {
console.error(err);
ui.notifications.error(`Dice roll evaluation failed: ${err.message}`);
return null;
}
// Flag d20 options for any 20-sided dice in the roll
for (let d of roll.dice) {
if (d.faces === 20) {
d.options.critical = critical;
d.options.fumble = fumble;
if ( adv === 1 ) d.options.advantage = true;
else if ( adv === -1 ) d.options.disadvantage = true;
if (targetValue) d.options.target = targetValue;
}
}
// If reliable talent was applied, add it to the flavor text
if (reliableTalent && roll.dice[0].total < 10) {
messageData.flavor += ` (${game.i18n.localize("SW5E.FlagsReliableTalent")})`;
} }
if (roll && chatMessage) await roll.toMessage(messageData);
return roll; return roll;
};
// Create the Roll instance
const roll = fastForward ? _roll(parts, adv) :
await _d20RollDialog({template, title, parts, data, rollMode: messageOptions.rollMode, dialogOptions, roll: _roll});
// Create a Chat Message
if ( roll && chatMessage ) roll.toMessage(messageData, messageOptions);
return roll;
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
/** /**
* Present a Dialog form which creates a d20 roll once submitted * Determines whether this d20 roll should be fast-forwarded, and whether advantage or disadvantage should be applied
* @return {Promise<Roll>} * @returns {{isFF: boolean, advantageMode: number}} Whether the roll is fast-forward, and its advantage mode
* @private
*/ */
async function _d20RollDialog({template, title, parts, data, rollMode, dialogOptions, roll}={}) { function _determineAdvantageMode({event, advantage = false, disadvantage = false, fastForward = false} = {}) {
const isFF = fastForward || (event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey));
// Render modal dialog let advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.NORMAL;
template = template || "systems/sw5e/templates/chat/roll-dialog.html"; if (advantage || event?.altKey) advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.ADVANTAGE;
let dialogData = { else if (disadvantage || event?.ctrlKey || event?.metaKey)
formula: parts.join(" + "), advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.DISADVANTAGE;
data: data, return {isFF, advantageMode};
rollMode: rollMode,
rollModes: CONFIG.Dice.rollModes,
config: CONFIG.SW5E
};
const html = await renderTemplate(template, dialogData);
// Create the Dialog window
return new Promise(resolve => {
new Dialog({
title: title,
content: html,
buttons: {
advantage: {
label: game.i18n.localize("SW5E.Advantage"),
callback: html => resolve(roll(parts, 1, html[0].querySelector("form")))
},
normal: {
label: game.i18n.localize("SW5E.Normal"),
callback: html => resolve(roll(parts, 0, html[0].querySelector("form")))
},
disadvantage: {
label: game.i18n.localize("SW5E.Disadvantage"),
callback: html => resolve(roll(parts, -1, html[0].querySelector("form")))
}
},
default: "normal",
close: () => resolve(null)
}, dialogOptions).render(true);
});
} }
/* -------------------------------------------- */
/* Damage Roll */
/* -------------------------------------------- */ /* -------------------------------------------- */
/** /**
* A standardized helper function for managing core 5e "d20 rolls" * A standardized helper function for managing core 5e damage rolls.
* *
* Holding SHIFT, ALT, or CTRL when the attack is rolled will "fast-forward". * Holding SHIFT, ALT, or CTRL when the attack is rolled will "fast-forward".
* This chooses the default options of a normal attack with no bonus, Critical, or no bonus respectively * This chooses the default options of a normal attack with no bonus, Critical, or no bonus respectively
* *
* @param {Array} parts The dice roll component parts, excluding the initial d20 * @param {string[]} parts The dice roll component parts, excluding the initial d20
* @param {Actor} actor The Actor making the damage roll * @param {object} [data] Actor or item data against which to parse the roll
* @param {Object} data Actor or item data against which to parse the roll
* @param {Event|object}[event The triggering event which initiated the roll
* @param {string} rollMode A specific roll mode to apply as the default for the resulting roll
* @param {String} template The HTML template used to render the roll dialog
* @param {String} title The dice roll UI window title
* @param {Object} speaker The ChatMessage speaker to pass when creating the chat
* @param {string} flavor Flavor text to use in the posted chat message
* @param {boolean} allowCritical Allow the opportunity for a critical hit to be rolled
* @param {Boolean} critical Flag this roll as a critical hit for the purposes of fast-forward rolls
* @param {number} criticalBonusDice A number of bonus damage dice that are added for critical hits
* @param {number} criticalMultiplier A critical hit multiplier which is applied to critical hits
* @param {Boolean} fastForward Allow fast-forward advantage selection
* @param {Function} onClose Callback for actions to take when the dialog form is closed
* @param {Object} dialogOptions Modal dialog options
* @param {boolean} chatMessage Automatically create a Chat Message for the result of this roll
* @param {object} messageData Additional data which is applied to the created Chat Message, if any
* *
* @return {Promise} A Promise which resolves once the roll workflow has completed * @param {boolean} [critical=false] Flag this roll as a critical hit for the purposes of fast-forward or default dialog action
* @param {number} [criticalBonusDice=0] A number of bonus damage dice that are added for critical hits
* @param {number} [criticalMultiplier=2] A critical hit multiplier which is applied to critical hits
* @param {boolean} [multiplyNumeric=false] Multiply numeric terms by the critical multiplier
* @param {boolean} [powerfulCritical=false] Apply the "powerful criticals" house rule to critical hits
* @param {boolean} [fastForward=false] Allow fast-forward advantage selection
* @param {Event}[event] The triggering event which initiated the roll
* @param {boolean} [allowCritical=true] Allow the opportunity for a critical hit to be rolled
* @param {string} [template] The HTML template used to render the roll dialog
* @param {string} [title] The dice roll UI window title
* @param {object} [dialogOptions] Configuration dialog options
*
* @param {boolean} [chatMessage=true] Automatically create a Chat Message for the result of this roll
* @param {object} [messageData={}] Additional data which is applied to the created Chat Message, if any
* @param {string} [rollMode] A specific roll mode to apply as the default for the resulting roll
* @param {object} [speaker] The ChatMessage speaker to pass when creating the chat
* @param {string} [flavor] Flavor text to use in the posted chat message
*
* @return {Promise<DamageRoll|null>} The evaluated DamageRoll, or null if the workflow was canceled
*/ */
export async function damageRoll({parts, actor, data, event={}, rollMode=null, template, title, speaker, flavor, export async function damageRoll({
allowCritical=true, critical=false, criticalBonusDice=0, criticalMultiplier=2, fastForward=null, parts = [],
dialogOptions={}, chatMessage=true, messageData={}}={}) { data, // Roll creation
critical = false,
criticalBonusDice,
criticalMultiplier,
multiplyNumeric,
powerfulCritical, // Damage customization
fastForward = false,
event,
allowCritical = true,
template,
title,
dialogOptions, // Dialog configuration
chatMessage = true,
messageData = {},
rollMode,
speaker,
flavor // Chat Message customization
} = {}) {
// Handle input arguments
const defaultRollMode = rollMode || game.settings.get("core", "rollMode");
// Prepare Message Data // Construct the DamageRoll instance
messageData.flavor = flavor || title; const formula = parts.join(" + ");
messageData.speaker = speaker || ChatMessage.getSpeaker(); const {isCritical, isFF} = _determineCriticalMode({critical, fastForward, event});
const messageOptions = {rollMode: rollMode || game.settings.get("core", "rollMode")}; const roll = new CONFIG.Dice.DamageRoll(formula, data, {
parts = parts.concat(["@bonus"]); flavor: flavor || title,
critical: isCritical,
criticalBonusDice,
criticalMultiplier,
multiplyNumeric,
powerfulCritical
});
// Define inner roll function // Prompt a Dialog to further configure the DamageRoll
const _roll = function(parts, crit, form) { if (!isFF) {
const configured = await roll.configureDialog(
// Optionally include a situational bonus {
if ( form ) { title,
data['bonus'] = form.bonus.value; defaultRollMode: defaultRollMode,
messageOptions.rollMode = form.rollMode.value; defaultCritical: isCritical,
} template,
if (!data["bonus"]) parts.pop(); allowCritical
},
// Create the damage roll dialogOptions
let roll = new Roll(parts.join("+"), data); );
if (configured === null) return null;
// Modify the damage formula for critical hits
if ( crit === true ) {
roll.alter(criticalMultiplier, 0); // Multiply all dice
if ( roll.terms[0] instanceof Die ) { // Add bonus dice for only the main dice term
roll.terms[0].alter(1, criticalBonusDice);
roll._formula = roll.formula;
}
messageData.flavor += ` (${game.i18n.localize("SW5E.Critical")})`;
if ( "flags.sw5e.roll" in messageData ) messageData["flags.sw5e.roll"].critical = true;
} }
// Execute the roll // Evaluate the configured roll
try { await roll.evaluate({async: true});
roll.evaluate()
if ( crit ) roll.dice.forEach(d => d.options.critical = true); // TODO workaround core bug which wipes Roll#options on roll // Create a Chat Message
return roll; if (speaker) {
} catch(err) { console.warn(
console.error(err); `You are passing the speaker argument to the damageRoll function directly which should instead be passed as an internal key of messageData`
ui.notifications.error(`Dice roll evaluation failed: ${err.message}`); );
return null; messageData.speaker = speaker;
} }
}; if (roll && chatMessage) await roll.toMessage(messageData);
return roll;
// Create the Roll instance
const roll = fastForward ? _roll(parts, critical) : await _damageRollDialog({
template, title, parts, data, allowCritical, rollMode: messageOptions.rollMode, dialogOptions, roll: _roll
});
// Create a Chat Message
if ( roll && chatMessage ) roll.toMessage(messageData, messageOptions);
return roll;
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
/** /**
* Present a Dialog form which creates a damage roll once submitted * Determines whether this d20 roll should be fast-forwarded, and whether advantage or disadvantage should be applied
* @return {Promise<Roll>} * @returns {{isFF: boolean, isCritical: boolean}} Whether the roll is fast-forward, and whether it is a critical hit
* @private
*/ */
async function _damageRollDialog({template, title, parts, data, allowCritical, rollMode, dialogOptions, roll}={}) { function _determineCriticalMode({event, critical = false, fastForward = false} = {}) {
const isFF = fastForward || (event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey));
// Render modal dialog if (event?.altKey) critical = true;
template = template || "systems/sw5e/templates/chat/roll-dialog.html"; return {isFF, isCritical: critical};
let dialogData = {
formula: parts.join(" + "),
data: data,
rollMode: rollMode,
rollModes: CONFIG.Dice.rollModes
};
const html = await renderTemplate(template, dialogData);
// Create the Dialog window
return new Promise(resolve => {
new Dialog({
title: title,
content: html,
buttons: {
critical: {
condition: allowCritical,
label: game.i18n.localize("SW5E.CriticalHit"),
callback: html => resolve(roll(parts, true, html[0].querySelector("form")))
},
normal: {
label: game.i18n.localize(allowCritical ? "SW5E.Normal" : "SW5E.Roll"),
callback: html => resolve(roll(parts, false, html[0].querySelector("form")))
},
},
default: "normal",
close: () => resolve(null)
}, dialogOptions).render(true);
});
} }

230
module/dice/d20-roll.js Normal file
View file

@ -0,0 +1,230 @@
/**
* A type of Roll specific to a d20-based check, save, or attack roll in the 5e system.
* @param {string} formula The string formula to parse
* @param {object} data The data object against which to parse attributes within the formula
* @param {object} [options={}] Extra optional arguments which describe or modify the D20Roll
* @param {number} [options.advantageMode] What advantage modifier to apply to the roll (none, advantage, disadvantage)
* @param {number} [options.critical] The value of d20 result which represents a critical success
* @param {number} [options.fumble] The value of d20 result which represents a critical failure
* @param {(number)} [options.targetValue] Assign a target value against which the result of this roll should be compared
* @param {boolean} [options.elvenAccuracy=false] Allow Elven Accuracy to modify this roll?
* @param {boolean} [options.halflingLucky=false] Allow Halfling Luck to modify this roll?
* @param {boolean} [options.reliableTalent=false] Allow Reliable Talent to modify this roll?
*/
// TODO: Check elven accuracy, halfling lucky, and reliable talent are required
// Elven Accuracy is Supreme accuracy feat, Reliable Talent is operative's Reliable Talent Class Feat
export default class D20Roll extends Roll {
constructor(formula, data, options) {
super(formula, data, options);
if (!(this.terms[0] instanceof Die && this.terms[0].faces === 20)) {
throw new Error(`Invalid D20Roll formula provided ${this._formula}`);
}
this.configureModifiers();
}
/* -------------------------------------------- */
/**
* Advantage mode of a 5e d20 roll
* @enum {number}
*/
static ADV_MODE = {
NORMAL: 0,
ADVANTAGE: 1,
DISADVANTAGE: -1
};
/**
* The HTML template path used to configure evaluation of this Roll
* @type {string}
*/
static EVALUATION_TEMPLATE = "systems/sw5e/templates/chat/roll-dialog.html";
/* -------------------------------------------- */
/**
* A convenience reference for whether this D20Roll has advantage
* @type {boolean}
*/
get hasAdvantage() {
return this.options.advantageMode === D20Roll.ADV_MODE.ADVANTAGE;
}
/**
* A convenience reference for whether this D20Roll has disadvantage
* @type {boolean}
*/
get hasDisadvantage() {
return this.options.advantageMode === D20Roll.ADV_MODE.DISADVANTAGE;
}
/* -------------------------------------------- */
/* D20 Roll Methods */
/* -------------------------------------------- */
/**
* Apply optional modifiers which customize the behavior of the d20term
* @private
*/
configureModifiers() {
const d20 = this.terms[0];
d20.modifiers = [];
// Halfling Lucky
if (this.options.halflingLucky) d20.modifiers.push("r1=1");
// Reliable Talent
if (this.options.reliableTalent) d20.modifiers.push("min10");
// Handle Advantage or Disadvantage
if (this.hasAdvantage) {
d20.number = this.options.elvenAccuracy ? 3 : 2;
d20.modifiers.push("kh");
d20.options.advantage = true;
} else if (this.hasDisadvantage) {
d20.number = 2;
d20.modifiers.push("kl");
d20.options.disadvantage = true;
} else d20.number = 1;
// Assign critical and fumble thresholds
if (this.options.critical) d20.options.critical = this.options.critical;
if (this.options.fumble) d20.options.fumble = this.options.fumble;
if (this.options.targetValue) d20.options.target = this.options.targetValue;
// Re-compile the underlying formula
this._formula = this.constructor.getFormula(this.terms);
}
/* -------------------------------------------- */
/** @inheritdoc */
async toMessage(messageData = {}, options = {}) {
// Evaluate the roll now so we have the results available to determine whether reliable talent came into play
if (!this._evaluated) await this.evaluate({async: true});
// Add appropriate advantage mode message flavor and sw5e roll flags
messageData.flavor = messageData.flavor || this.options.flavor;
if (this.hasAdvantage) messageData.flavor += ` (${game.i18n.localize("SW5E.Advantage")})`;
else if (this.hasDisadvantage) messageData.flavor += ` (${game.i18n.localize("SW5E.Disadvantage")})`;
// Add reliable talent to the d20-term flavor text if it applied
if (this.options.reliableTalent) {
const d20 = this.dice[0];
const isRT = d20.results.every((r) => !r.active || r.result < 10);
const label = `(${game.i18n.localize("SW5E.FlagsReliableTalent")})`;
if (isRT) d20.options.flavor = d20.options.flavor ? `${d20.options.flavor} (${label})` : label;
}
// Record the preferred rollMode
options.rollMode = options.rollMode ?? this.options.rollMode;
return super.toMessage(messageData, options);
}
/* -------------------------------------------- */
/* Configuration Dialog */
/* -------------------------------------------- */
/**
* Create a Dialog prompt used to configure evaluation of an existing D20Roll instance.
* @param {object} data Dialog configuration data
* @param {string} [data.title] The title of the shown dialog window
* @param {number} [data.defaultRollMode] The roll mode that the roll mode select element should default to
* @param {number} [data.defaultAction] The button marked as default
* @param {boolean} [data.chooseModifier] Choose which ability modifier should be applied to the roll?
* @param {string} [data.defaultAbility] For tool rolls, the default ability modifier applied to the roll
* @param {string} [data.template] A custom path to an HTML template to use instead of the default
* @param {object} options Additional Dialog customization options
* @returns {Promise<D20Roll|null>} A resulting D20Roll object constructed with the dialog, or null if the dialog was closed
*/
async configureDialog(
{
title,
defaultRollMode,
defaultAction = D20Roll.ADV_MODE.NORMAL,
chooseModifier = false,
defaultAbility,
template
} = {},
options = {}
) {
// Render the Dialog inner HTML
const content = await renderTemplate(template ?? this.constructor.EVALUATION_TEMPLATE, {
formula: `${this.formula} + @bonus`,
defaultRollMode,
rollModes: CONFIG.Dice.rollModes,
chooseModifier,
defaultAbility,
abilities: CONFIG.SW5E.abilities
});
let defaultButton = "normal";
switch (defaultAction) {
case D20Roll.ADV_MODE.ADVANTAGE:
defaultButton = "advantage";
break;
case D20Roll.ADV_MODE.DISADVANTAGE:
defaultButton = "disadvantage";
break;
}
// Create the Dialog window and await submission of the form
return new Promise((resolve) => {
new Dialog(
{
title,
content,
buttons: {
advantage: {
label: game.i18n.localize("SW5E.Advantage"),
callback: (html) => resolve(this._onDialogSubmit(html, D20Roll.ADV_MODE.ADVANTAGE))
},
normal: {
label: game.i18n.localize("SW5E.Normal"),
callback: (html) => resolve(this._onDialogSubmit(html, D20Roll.ADV_MODE.NORMAL))
},
disadvantage: {
label: game.i18n.localize("SW5E.Disadvantage"),
callback: (html) => resolve(this._onDialogSubmit(html, D20Roll.ADV_MODE.DISADVANTAGE))
}
},
default: defaultButton,
close: () => resolve(null)
},
options
).render(true);
});
}
/* -------------------------------------------- */
/**
* Handle submission of the Roll evaluation configuration Dialog
* @param {jQuery} html The submitted dialog content
* @param {number} advantageMode The chosen advantage mode
* @private
*/
_onDialogSubmit(html, advantageMode) {
const form = html[0].querySelector("form");
// Append a situational bonus term
if (form.bonus.value) {
const bonus = new Roll(form.bonus.value, this.data);
if (!(bonus.terms[0] instanceof OperatorTerm)) this.terms.push(new OperatorTerm({operator: "+"}));
this.terms = this.terms.concat(bonus.terms);
}
// Customize the modifier
if (form.ability?.value) {
const abl = this.data.abilities[form.ability.value];
this.terms.findSplice((t) => t.term === "@mod", new NumericTerm({number: abl.mod}));
this.options.flavor += ` (${CONFIG.SW5E.abilities[form.ability.value]})`;
}
// Apply advantage or disadvantage
this.options.advantageMode = advantageMode;
this.options.rollMode = form.rollMode.value;
this.configureModifiers();
return this;
}
}

186
module/dice/damage-roll.js Normal file
View file

@ -0,0 +1,186 @@
/**
* A type of Roll specific to a damage (or healing) roll in the 5e system.
* @param {string} formula The string formula to parse
* @param {object} data The data object against which to parse attributes within the formula
* @param {object} [options={}] Extra optional arguments which describe or modify the DamageRoll
* @param {number} [options.criticalBonusDice=0] A number of bonus damage dice that are added for critical hits
* @param {number} [options.criticalMultiplier=2] A critical hit multiplier which is applied to critical hits
* @param {boolean} [options.multiplyNumeric=false] Multiply numeric terms by the critical multiplier
* @param {boolean} [options.powerfulCritical=false] Apply the "powerful criticals" house rule to critical hits
*
*/
export default class DamageRoll extends Roll {
constructor(formula, data, options) {
super(formula, data, options);
// For backwards compatibility, skip rolls which do not have the "critical" option defined
if (this.options.critical !== undefined) this.configureDamage();
}
/**
* The HTML template path used to configure evaluation of this Roll
* @type {string}
*/
static EVALUATION_TEMPLATE = "systems/sw5e/templates/chat/roll-dialog.html";
/* -------------------------------------------- */
/**
* A convenience reference for whether this DamageRoll is a critical hit
* @type {boolean}
*/
get isCritical() {
return this.options.critical;
}
/* -------------------------------------------- */
/* Damage Roll Methods */
/* -------------------------------------------- */
/**
* Apply optional modifiers which customize the behavior of the d20term
* @private
*/
configureDamage() {
let flatBonus = 0;
for (let [i, term] of this.terms.entries()) {
// Multiply dice terms
if (term instanceof DiceTerm) {
term.options.baseNumber = term.options.baseNumber ?? term.number; // Reset back
term.number = term.options.baseNumber;
if (this.isCritical) {
let cm = this.options.criticalMultiplier ?? 2;
// Powerful critical - maximize damage and reduce the multiplier by 1
if (this.options.powerfulCritical) {
flatBonus += term.number * term.faces;
cm = Math.max(1, cm - 1);
}
// Alter the damage term
let cb = this.options.criticalBonusDice && i === 0 ? this.options.criticalBonusDice : 0;
term.alter(cm, cb);
term.options.critical = true;
}
}
// Multiply numeric terms
else if (this.options.multiplyNumeric && term instanceof NumericTerm) {
term.options.baseNumber = term.options.baseNumber ?? term.number; // Reset back
term.number = term.options.baseNumber;
if (this.isCritical) {
term.number *= this.options.criticalMultiplier ?? 2;
term.options.critical = true;
}
}
}
// Add powerful critical bonus
if (this.options.powerfulCritical && flatBonus > 0) {
this.terms.push(new OperatorTerm({operator: "+"}));
this.terms.push(
new NumericTerm({number: flatBonus}, {flavor: game.i18n.localize("SW5E.PowerfulCritical")})
);
}
// Re-compile the underlying formula
this._formula = this.constructor.getFormula(this.terms);
}
/* -------------------------------------------- */
/** @inheritdoc */
toMessage(messageData = {}, options = {}) {
messageData.flavor = messageData.flavor || this.options.flavor;
if (this.isCritical) {
const label = game.i18n.localize("SW5E.CriticalHit");
messageData.flavor = messageData.flavor ? `${messageData.flavor} (${label})` : label;
}
options.rollMode = options.rollMode ?? this.options.rollMode;
return super.toMessage(messageData, options);
}
/* -------------------------------------------- */
/* Configuration Dialog */
/* -------------------------------------------- */
/**
* Create a Dialog prompt used to configure evaluation of an existing D20Roll instance.
* @param {object} data Dialog configuration data
* @param {string} [data.title] The title of the shown dialog window
* @param {number} [data.defaultRollMode] The roll mode that the roll mode select element should default to
* @param {string} [data.defaultCritical] Should critical be selected as default
* @param {string} [data.template] A custom path to an HTML template to use instead of the default
* @param {boolean} [data.allowCritical=true] Allow critical hit to be chosen as a possible damage mode
* @param {object} options Additional Dialog customization options
* @returns {Promise<D20Roll|null>} A resulting D20Roll object constructed with the dialog, or null if the dialog was closed
*/
async configureDialog(
{title, defaultRollMode, defaultCritical = false, template, allowCritical = true} = {},
options = {}
) {
// Render the Dialog inner HTML
const content = await renderTemplate(template ?? this.constructor.EVALUATION_TEMPLATE, {
formula: `${this.formula} + @bonus`,
defaultRollMode,
rollModes: CONFIG.Dice.rollModes
});
// Create the Dialog window and await submission of the form
return new Promise((resolve) => {
new Dialog(
{
title,
content,
buttons: {
critical: {
condition: allowCritical,
label: game.i18n.localize("SW5E.CriticalHit"),
callback: (html) => resolve(this._onDialogSubmit(html, true))
},
normal: {
label: game.i18n.localize(allowCritical ? "SW5E.Normal" : "SW5E.Roll"),
callback: (html) => resolve(this._onDialogSubmit(html, false))
}
},
default: defaultCritical ? "critical" : "normal",
close: () => resolve(null)
},
options
).render(true);
});
}
/* -------------------------------------------- */
/**
* Handle submission of the Roll evaluation configuration Dialog
* @param {jQuery} html The submitted dialog content
* @param {boolean} isCritical Is the damage a critical hit?
* @private
*/
_onDialogSubmit(html, isCritical) {
const form = html[0].querySelector("form");
// Append a situational bonus term
if (form.bonus.value) {
const bonus = new Roll(form.bonus.value, this.data);
if (!(bonus.terms[0] instanceof OperatorTerm)) this.terms.push(new OperatorTerm({operator: "+"}));
this.terms = this.terms.concat(bonus.terms);
}
// Apply advantage or disadvantage
this.options.critical = isCritical;
this.options.rollMode = form.rollMode.value;
this.configureDamage();
return this;
}
/* -------------------------------------------- */
/** @inheritdoc */
static fromData(data) {
const roll = super.fromData(data);
roll._formula = this.getFormula(roll.terms);
return roll;
}
}

View file

@ -0,0 +1,15 @@
/**
* @deprecated since 1.3.0
* @ignore
*/
async function d20Dialog(data, options) {
throw new Error(`The d20Dialog helper method is deprecated in favor of D20Roll#configureDialog`);
}
/**
* @deprecated since 1.3.0
* @ignore
*/
async function damageDialog(data, options) {
throw new Error(`The damageDialog helper method is deprecated in favor of DamageRoll#configureDialog`);
}

85
module/effects.js vendored
View file

@ -4,26 +4,28 @@
* @param {Actor|Item} owner The owning entity which manages this effect * @param {Actor|Item} owner The owning entity which manages this effect
*/ */
export function onManageActiveEffect(event, owner) { export function onManageActiveEffect(event, owner) {
event.preventDefault(); event.preventDefault();
const a = event.currentTarget; const a = event.currentTarget;
const li = a.closest("li"); const li = a.closest("li");
const effect = li.dataset.effectId ? owner.effects.get(li.dataset.effectId) : null; const effect = li.dataset.effectId ? owner.effects.get(li.dataset.effectId) : null;
switch ( a.dataset.action ) { switch (a.dataset.action) {
case "create": case "create":
return ActiveEffect.create({ return owner.createEmbeddedDocuments("ActiveEffect", [
label: "New Effect", {
icon: "icons/svg/aura.svg", "label": game.i18n.localize("SW5E.EffectNew"),
origin: owner.uuid, "icon": "icons/svg/aura.svg",
"duration.rounds": li.dataset.effectType === "temporary" ? 1 : undefined, "origin": owner.uuid,
disabled: li.dataset.effectType === "inactive" "duration.rounds": li.dataset.effectType === "temporary" ? 1 : undefined,
}, owner).create(); "disabled": li.dataset.effectType === "inactive"
case "edit": }
return effect.sheet.render(true); ]);
case "delete": case "edit":
return effect.delete(); return effect.sheet.render(true);
case "toggle": case "delete":
return effect.update({disabled: !effect.data.disabled}); return effect.delete();
} case "toggle":
return effect.update({disabled: !effect.data.disabled});
}
} }
/** /**
@ -32,32 +34,31 @@ export function onManageActiveEffect(event, owner) {
* @return {object} Data for rendering * @return {object} Data for rendering
*/ */
export function prepareActiveEffectCategories(effects) { export function prepareActiveEffectCategories(effects) {
// Define effect header categories // Define effect header categories
const categories = { const categories = {
temporary: { temporary: {
type: "temporary", type: "temporary",
label: "SW5E.EffectsCategoryTemporary", label: game.i18n.localize("SW5E.EffectTemporary"),
effects: [] effects: []
}, },
passive: { passive: {
type: "passive", type: "passive",
label: "SW5E.EffectsCategoryPassive", label: game.i18n.localize("SW5E.EffectPassive"),
effects: [] effects: []
}, },
inactive: { inactive: {
type: "inactive", type: "inactive",
label: "SW5E.EffectsCategoryInactive", label: game.i18n.localize("SW5E.EffectInactive"),
effects: [] effects: []
} }
}; };
// Iterate over active effects, classifying them into categories // Iterate over active effects, classifying them into categories
for ( let e of effects ) { for (let e of effects) {
e._getSourceName(); // Trigger a lookup for the source name e._getSourceName(); // Trigger a lookup for the source name
if ( e.data.disabled ) categories.inactive.effects.push(e); if (e.data.disabled) categories.inactive.effects.push(e);
else if ( e.isTemporary ) categories.temporary.effects.push(e); else if (e.isTemporary) categories.temporary.effects.push(e);
else categories.passive.effects.push(e); else categories.passive.effects.push(e);
} }
return categories; return categories;
} }

File diff suppressed because it is too large Load diff

View file

@ -1,346 +1,370 @@
import TraitSelector from "../apps/trait-selector.js"; import TraitSelector from "../apps/trait-selector.js";
import { onManageActiveEffect, prepareActiveEffectCategories } from "../effects.js"; import {onManageActiveEffect, prepareActiveEffectCategories} from "../effects.js";
/** /**
* Override and extend the core ItemSheet implementation to handle specific item types * Override and extend the core ItemSheet implementation to handle specific item types
* @extends {ItemSheet} * @extends {ItemSheet}
*/ */
export default class ItemSheet5e extends ItemSheet { export default class ItemSheet5e extends ItemSheet {
constructor(...args) { constructor(...args) {
super(...args); super(...args);
// Expand the default size of the class sheet // Expand the default size of the class sheet
if (this.object.data.type === "class") { if (this.object.data.type === "class") {
this.options.width = this.position.width = 600; this.options.width = this.position.width = 600;
this.options.height = this.position.height = 680; this.options.height = this.position.height = 680;
}
}
/* -------------------------------------------- */
/** @override */
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
width: 560,
height: 400,
classes: ["sw5e", "sheet", "item"],
resizable: true,
scrollY: [".tab.details"],
tabs: [{ navSelector: ".tabs", contentSelector: ".sheet-body", initial: "description" }]
});
}
/* -------------------------------------------- */
/** @override */
get template() {
const path = "systems/sw5e/templates/items/";
return `${path}/${this.item.data.type}.html`;
}
/* -------------------------------------------- */
/** @override */
async getData(options) {
const data = super.getData(options);
data.labels = this.item.labels;
data.config = CONFIG.SW5E;
// Item Type, Status, and Details
data.itemType = game.i18n.localize(`ITEM.Type${data.item.type.titleCase()}`);
data.itemStatus = this._getItemStatus(data.item);
data.itemProperties = this._getItemProperties(data.item);
data.isPhysical = data.item.data.hasOwnProperty("quantity");
// Potential consumption targets
data.abilityConsumptionTargets = this._getItemConsumptionTargets(data.item);
// Action Detail
data.hasAttackRoll = this.item.hasAttack;
data.isHealing = data.item.data.actionType === "heal";
data.isFlatDC = getProperty(data.item.data, "save.scaling") === "flat";
data.isLine = ["line", "wall"].includes(data.item.data.target?.type);
// Original maximum uses formula
if (this.item._data.data?.uses?.max) data.data.uses.max = this.item._data.data.uses.max;
// Vehicles
data.isCrewed = data.item.data.activation?.type === "crew";
data.isMountable = this._isItemMountable(data.item);
// Prepare Active Effects
data.effects = prepareActiveEffectCategories(this.entity.effects);
return data;
}
/* -------------------------------------------- */
/**
* Get the valid item consumption targets which exist on the actor
* @param {Object} item Item data for the item being displayed
* @return {{string: string}} An object of potential consumption targets
* @private
*/
_getItemConsumptionTargets(item) {
const consume = item.data.consume || {};
if (!consume.type) return [];
const actor = this.item.actor;
if (!actor) return {};
// Ammunition
if (consume.type === "ammo") {
return actor.itemTypes.consumable.reduce(
(ammo, i) => {
if (i.data.data.consumableType === "ammo") {
ammo[i.id] = `${i.name} (${i.data.data.quantity})`;
}
return ammo;
},
{ [item._id]: `${item.name} (${item.data.quantity})` }
);
}
// Attributes
else if (consume.type === "attribute") {
const attributes = Object.values(CombatTrackerConfig.prototype.getAttributeChoices())[0]; // Bit of a hack
return attributes.reduce((obj, a) => {
obj[a] = a;
return obj;
}, {});
}
// Materials
else if (consume.type === "material") {
return actor.items.reduce((obj, i) => {
if (["consumable", "loot"].includes(i.data.type) && !i.data.data.activation) {
obj[i.id] = `${i.name} (${i.data.data.quantity})`;
} }
return obj;
}, {});
} }
// Charges /* -------------------------------------------- */
else if (consume.type === "charges") {
return actor.items.reduce((obj, i) => { /** @inheritdoc */
// Limited-use items static get defaultOptions() {
const uses = i.data.data.uses || {}; return foundry.utils.mergeObject(super.defaultOptions, {
if (uses.per && uses.max) { width: 560,
const label = height: 400,
uses.per === "charges" classes: ["sw5e", "sheet", "item"],
? ` (${game.i18n.format("SW5E.AbilityUseChargesLabel", { value: uses.value })})` resizable: true,
: ` (${game.i18n.format("SW5E.AbilityUseConsumableLabel", { max: uses.max, per: uses.per })})`; scrollY: [".tab.details"],
obj[i.id] = i.name + label; tabs: [{navSelector: ".tabs", contentSelector: ".sheet-body", initial: "description"}]
});
}
/* -------------------------------------------- */
/** @inheritdoc */
get template() {
const path = "systems/sw5e/templates/items/";
return `${path}/${this.item.data.type}.html`;
}
/* -------------------------------------------- */
/** @override */
async getData(options) {
const data = super.getData(options);
const itemData = data.data;
data.labels = this.item.labels;
data.config = CONFIG.SW5E;
// Item Type, Status, and Details
data.itemType = game.i18n.localize(`ITEM.Type${data.item.type.titleCase()}`);
data.itemStatus = this._getItemStatus(itemData);
data.itemProperties = this._getItemProperties(itemData);
data.isPhysical = itemData.data.hasOwnProperty("quantity");
// Potential consumption targets
data.abilityConsumptionTargets = this._getItemConsumptionTargets(itemData);
// Action Details
data.hasAttackRoll = this.item.hasAttack;
data.isHealing = itemData.data.actionType === "heal";
data.isFlatDC = getProperty(itemData, "data.save.scaling") === "flat";
data.isLine = ["line", "wall"].includes(itemData.data.target?.type);
// Original maximum uses formula
const sourceMax = foundry.utils.getProperty(this.item.data._source, "data.uses.max");
if (sourceMax) itemData.data.uses.max = sourceMax;
// Vehicles
data.isCrewed = itemData.data.activation?.type === "crew";
data.isMountable = this._isItemMountable(itemData);
// Prepare Active Effects
data.effects = prepareActiveEffectCategories(this.item.effects);
// Re-define the template data references (backwards compatible)
data.item = itemData;
data.data = itemData.data;
return data;
}
/* -------------------------------------------- */
/**
* Get the valid item consumption targets which exist on the actor
* @param {Object} item Item data for the item being displayed
* @return {{string: string}} An object of potential consumption targets
* @private
*/
_getItemConsumptionTargets(item) {
const consume = item.data.consume || {};
if (!consume.type) return [];
const actor = this.item.actor;
if (!actor) return {};
// Ammunition
if (consume.type === "ammo") {
return actor.itemTypes.consumable.reduce(
(ammo, i) => {
if (i.data.data.consumableType === "ammo") {
ammo[i.id] = `${i.name} (${i.data.data.quantity})`;
}
return ammo;
},
{[item._id]: `${item.name} (${item.data.quantity})`}
);
} }
// Recharging items // Attributes
const recharge = i.data.data.recharge || {}; else if (consume.type === "attribute") {
if (recharge.value) obj[i.id] = `${i.name} (${game.i18n.format("SW5E.Recharge")})`; const attributes = TokenDocument.getTrackedAttributes(actor.data.data);
return obj; attributes.bar.forEach((a) => a.push("value"));
}, {}); return attributes.bar.concat(attributes.value).reduce((obj, a) => {
} else return {}; let k = a.join(".");
} obj[k] = k;
return obj;
}, {});
}
/* -------------------------------------------- */ // Materials
else if (consume.type === "material") {
return actor.items.reduce((obj, i) => {
if (["consumable", "loot"].includes(i.data.type) && !i.data.data.activation) {
obj[i.id] = `${i.name} (${i.data.data.quantity})`;
}
return obj;
}, {});
}
/** // Charges
* Get the text item status which is shown beneath the Item type in the top-right corner of the sheet else if (consume.type === "charges") {
* @return {string} return actor.items.reduce((obj, i) => {
* @private // Limited-use items
*/ const uses = i.data.data.uses || {};
_getItemStatus(item) { if (uses.per && uses.max) {
if (item.type === "power") { const label =
return CONFIG.SW5E.powerPreparationModes[item.data.preparation]; uses.per === "charges"
} else if (["weapon", "equipment"].includes(item.type)) { ? ` (${game.i18n.format("SW5E.AbilityUseChargesLabel", {value: uses.value})})`
return game.i18n.localize(item.data.equipped ? "SW5E.Equipped" : "SW5E.Unequipped"); : ` (${game.i18n.format("SW5E.AbilityUseConsumableLabel", {
} else if (item.type === "tool") { max: uses.max,
return game.i18n.localize(item.data.proficient ? "SW5E.Proficient" : "SW5E.NotProficient"); per: uses.per
} })})`;
} obj[i.id] = i.name + label;
}
/* -------------------------------------------- */ // Recharging items
const recharge = i.data.data.recharge || {};
/** if (recharge.value) obj[i.id] = `${i.name} (${game.i18n.format("SW5E.Recharge")})`;
* Get the Array of item properties which are used in the small sidebar of the description tab return obj;
* @return {Array} }, {});
* @private } else return {};
*/
_getItemProperties(item) {
const props = [];
const labels = this.item.labels;
if (item.type === "weapon") {
props.push(
...Object.entries(item.data.properties)
.filter((e) => e[1] === true)
.map((e) => CONFIG.SW5E.weaponProperties[e[0]])
);
} else if (item.type === "power") {
props.push(
labels.components,
labels.materials,
item.data.components.concentration ? game.i18n.localize("SW5E.Concentration") : null,
item.data.components.ritual ? game.i18n.localize("SW5E.Ritual") : null
);
} else if (item.type === "equipment") {
props.push(CONFIG.SW5E.equipmentTypes[item.data.armor.type]);
props.push(labels.armor);
} else if (item.type === "feat") {
props.push(labels.featType);
} else if (item.type === "species") {
//props.push(labels.species);
} else if (item.type === "archetype") {
//props.push(labels.archetype);
} else if (item.type === "background") {
//props.push(labels.background);
} else if (item.type === "classfeature") {
//props.push(labels.classfeature);
} else if (item.type === "fightingmastery") {
//props.push(labels.fightingmastery);
} else if (item.type === "fightingstyle") {
//props.push(labels.fightingstyle);
} else if (item.type === "lightsaberform") {
//props.push(labels.lightsaberform);
} }
// Action type /* -------------------------------------------- */
if (item.data.actionType) {
props.push(CONFIG.SW5E.itemActionTypes[item.data.actionType]); /**
* Get the text item status which is shown beneath the Item type in the top-right corner of the sheet
* @return {string}
* @private
*/
_getItemStatus(item) {
if (item.type === "power") {
return CONFIG.SW5E.powerPreparationModes[item.data.preparation];
} else if (["weapon", "equipment"].includes(item.type)) {
return game.i18n.localize(item.data.equipped ? "SW5E.Equipped" : "SW5E.Unequipped");
} else if (item.type === "tool") {
return game.i18n.localize(item.data.proficient ? "SW5E.Proficient" : "SW5E.NotProficient");
}
} }
// Action usage /* -------------------------------------------- */
if (item.type !== "weapon" && item.data.activation && !isObjectEmpty(item.data.activation)) {
props.push(labels.activation, labels.range, labels.target, labels.duration);
}
return props.filter((p) => !!p);
}
/* -------------------------------------------- */ /**
* Get the Array of item properties which are used in the small sidebar of the description tab
* @return {Array}
* @private
*/
_getItemProperties(item) {
const props = [];
const labels = this.item.labels;
/** if (item.type === "weapon") {
* Is this item a separate large object like a siege engine or vehicle props.push(
* component that is usually mounted on fixtures rather than equipped, and ...Object.entries(item.data.properties)
* has its own AC and HP. .filter((e) => e[1] === true)
* @param item .map((e) => CONFIG.SW5E.weaponProperties[e[0]])
* @returns {boolean} );
* @private } else if (item.type === "power") {
*/ props.push(
_isItemMountable(item) { labels.materials,
const data = item.data; item.data.components.concentration ? game.i18n.localize("SW5E.Concentration") : null,
return ( item.data.components.ritual ? game.i18n.localize("SW5E.Ritual") : null
(item.type === "weapon" && data.weaponType === "siege") || );
(item.type === "equipment" && data.armor.type === "vehicle") } else if (item.type === "equipment") {
); props.push(CONFIG.SW5E.equipmentTypes[item.data.armor.type]);
} props.push(labels.armor);
} else if (item.type === "feat") {
props.push(labels.featType);
//TODO: Work out these
} else if (item.type === "species") {
//props.push(labels.species);
} else if (item.type === "archetype") {
//props.push(labels.archetype);
} else if (item.type === "background") {
//props.push(labels.background);
} else if (item.type === "classfeature") {
//props.push(labels.classfeature);
} else if (item.type === "deployment") {
//props.push(labels.deployment);
} else if (item.type === "venture") {
//props.push(labels.venture);
} else if (item.type === "fightingmastery") {
//props.push(labels.fightingmastery);
} else if (item.type === "fightingstyle") {
//props.push(labels.fightingstyle);
} else if (item.type === "lightsaberform") {
//props.push(labels.lightsaberform);
}
/* -------------------------------------------- */ // Action type
if (item.data.actionType) {
props.push(CONFIG.SW5E.itemActionTypes[item.data.actionType]);
}
/** @override */ // Action usage
setPosition(position = {}) { if (item.type !== "weapon" && item.data.activation && !isObjectEmpty(item.data.activation)) {
if (!(this._minimized || position.height)) { props.push(labels.activation, labels.range, labels.target, labels.duration);
position.height = this._tabs[0].active === "details" ? "auto" : this.options.height; }
} return props.filter((p) => !!p);
return super.setPosition(position);
}
/* -------------------------------------------- */
/* Form Submission */
/* -------------------------------------------- */
/** @override */
_getSubmitData(updateData = {}) {
// Create the expanded update data object
const fd = new FormDataExtended(this.form, { editors: this.editors });
let data = fd.toObject();
if (updateData) data = mergeObject(data, updateData);
else data = expandObject(data);
// Handle Damage array
const damage = data.data?.damage;
if (damage) damage.parts = Object.values(damage?.parts || {}).map((d) => [d[0] || "", d[1] || ""]);
// Return the flattened submission data
return flattenObject(data);
}
/* -------------------------------------------- */
/** @override */
activateListeners(html) {
super.activateListeners(html);
if (this.isEditable) {
html.find(".damage-control").click(this._onDamageControl.bind(this));
html.find(".trait-selector.class-skills").click(this._onConfigureClassSkills.bind(this));
html.find(".effect-control").click((ev) => {
if (this.item.isOwned)
return ui.notifications.warn(
"Managing Active Effects within an Owned Item is not currently supported and will be added in a subsequent update."
);
onManageActiveEffect(ev, this.item);
});
}
}
/* -------------------------------------------- */
/**
* Add or remove a damage part from the damage formula
* @param {Event} event The original click event
* @return {Promise}
* @private
*/
async _onDamageControl(event) {
event.preventDefault();
const a = event.currentTarget;
// Add new damage component
if (a.classList.contains("add-damage")) {
await this._onSubmit(event); // Submit any unsaved changes
const damage = this.item.data.data.damage;
return this.item.update({ "data.damage.parts": damage.parts.concat([["", ""]]) });
} }
// Remove a damage component /* -------------------------------------------- */
if (a.classList.contains("delete-damage")) {
await this._onSubmit(event); // Submit any unsaved changes /**
const li = a.closest(".damage-part"); * Is this item a separate large object like a siege engine or vehicle
const damage = duplicate(this.item.data.data.damage); * component that is usually mounted on fixtures rather than equipped, and
damage.parts.splice(Number(li.dataset.damagePart), 1); * has its own AC and HP.
return this.item.update({ "data.damage.parts": damage.parts }); * @param item
* @returns {boolean}
* @private
*/
_isItemMountable(item) {
const data = item.data;
return (
(item.type === "weapon" && data.weaponType === "siege") ||
(item.type === "equipment" && data.armor.type === "vehicle")
);
} }
}
/* -------------------------------------------- */ /* -------------------------------------------- */
/** /** @inheritdoc */
* Handle spawning the TraitSelector application which allows a checkbox of multiple trait options setPosition(position = {}) {
* @param {Event} event The click event which originated the selection if (!(this._minimized || position.height)) {
* @private position.height = this._tabs[0].active === "details" ? "auto" : this.options.height;
*/ }
_onConfigureClassSkills(event) { return super.setPosition(position);
event.preventDefault(); }
const skills = this.item.data.data.skills;
const choices = skills.choices && skills.choices.length ? skills.choices : Object.keys(CONFIG.SW5E.skills);
const a = event.currentTarget;
const label = a.parentElement;
// Render the Trait Selector dialog /* -------------------------------------------- */
new TraitSelector(this.item, { /* Form Submission */
name: a.dataset.target, /* -------------------------------------------- */
title: label.innerText,
choices: Object.entries(CONFIG.SW5E.skills).reduce((obj, e) => {
if (choices.includes(e[0])) obj[e[0]] = e[1];
return obj;
}, {}),
minimum: skills.number,
maximum: skills.number
}).render(true);
}
/* -------------------------------------------- */ /** @inheritdoc */
_getSubmitData(updateData = {}) {
// Create the expanded update data object
const fd = new FormDataExtended(this.form, {editors: this.editors});
let data = fd.toObject();
if (updateData) data = mergeObject(data, updateData);
else data = expandObject(data);
/** @override */ // Handle Damage array
async _onSubmit(...args) { const damage = data.data?.damage;
if (this._tabs[0].active === "details") this.position.height = "auto"; if (damage) damage.parts = Object.values(damage?.parts || {}).map((d) => [d[0] || "", d[1] || ""]);
await super._onSubmit(...args);
} // Return the flattened submission data
return flattenObject(data);
}
/* -------------------------------------------- */
/** @inheritdoc */
activateListeners(html) {
super.activateListeners(html);
if (this.isEditable) {
html.find(".damage-control").click(this._onDamageControl.bind(this));
html.find(".trait-selector.class-skills").click(this._onConfigureTraits.bind(this));
html.find(".effect-control").click((ev) => {
if (this.item.isOwned)
return ui.notifications.warn(
"Managing Active Effects within an Owned Item is not currently supported and will be added in a subsequent update."
);
onManageActiveEffect(ev, this.item);
});
}
}
/* -------------------------------------------- */
/**
* Add or remove a damage part from the damage formula
* @param {Event} event The original click event
* @return {Promise}
* @private
*/
async _onDamageControl(event) {
event.preventDefault();
const a = event.currentTarget;
// Add new damage component
if (a.classList.contains("add-damage")) {
await this._onSubmit(event); // Submit any unsaved changes
const damage = this.item.data.data.damage;
return this.item.update({"data.damage.parts": damage.parts.concat([["", ""]])});
}
// Remove a damage component
if (a.classList.contains("delete-damage")) {
await this._onSubmit(event); // Submit any unsaved changes
const li = a.closest(".damage-part");
const damage = foundry.utils.deepClone(this.item.data.data.damage);
damage.parts.splice(Number(li.dataset.damagePart), 1);
return this.item.update({"data.damage.parts": damage.parts});
}
}
/* -------------------------------------------- */
/**
* Handle spawning the TraitSelector application for selection various options.
* @param {Event} event The click event which originated the selection
* @private
*/
_onConfigureTraits(event) {
event.preventDefault();
const a = event.currentTarget;
const options = {
name: a.dataset.target,
title: a.parentElement.innerText,
choices: [],
allowCustom: false
};
switch (a.dataset.options) {
case "saves":
options.choices = CONFIG.SW5E.abilities;
options.valueKey = null;
break;
case "skills":
const skills = this.item.data.data.skills;
const choiceSet =
skills.choices && skills.choices.length ? skills.choices : Object.keys(CONFIG.SW5E.skills);
options.choices = Object.fromEntries(
Object.entries(CONFIG.SW5E.skills).filter((skill) => choiceSet.includes(skill[0]))
);
options.maximum = skills.number;
break;
}
new TraitSelector(this.item, options).render(true);
}
/* -------------------------------------------- */
/** @inheritdoc */
async _onSubmit(...args) {
if (this._tabs[0].active === "details") this.position.height = "auto";
await super._onSubmit(...args);
}
} }

View file

@ -1,4 +1,3 @@
/* -------------------------------------------- */ /* -------------------------------------------- */
/* Hotbar Macros */ /* Hotbar Macros */
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -11,24 +10,24 @@
* @returns {Promise} * @returns {Promise}
*/ */
export async function create5eMacro(data, slot) { export async function create5eMacro(data, slot) {
if ( data.type !== "Item" ) return; if (data.type !== "Item") return;
if (!( "data" in data ) ) return ui.notifications.warn("You can only create macro buttons for owned Items"); if (!("data" in data)) return ui.notifications.warn("You can only create macro buttons for owned Items");
const item = data.data; const item = data.data;
// Create the macro command // Create the macro command
const command = `game.sw5e.rollItemMacro("${item.name}");`; const command = `game.sw5e.rollItemMacro("${item.name}");`;
let macro = game.macros.entities.find(m => (m.name === item.name) && (m.command === command)); let macro = game.macros.entities.find((m) => m.name === item.name && m.command === command);
if ( !macro ) { if (!macro) {
macro = await Macro.create({ macro = await Macro.create({
name: item.name, name: item.name,
type: "script", type: "script",
img: item.img, img: item.img,
command: command, command: command,
flags: {"sw5e.itemMacro": true} flags: {"sw5e.itemMacro": true}
}); });
} }
game.user.assignHotbarMacro(macro, slot); game.user.assignHotbarMacro(macro, slot);
return false; return false;
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -40,20 +39,22 @@ export async function create5eMacro(data, slot) {
* @return {Promise} * @return {Promise}
*/ */
export function rollItemMacro(itemName) { export function rollItemMacro(itemName) {
const speaker = ChatMessage.getSpeaker(); const speaker = ChatMessage.getSpeaker();
let actor; let actor;
if ( speaker.token ) actor = game.actors.tokens[speaker.token]; if (speaker.token) actor = game.actors.tokens[speaker.token];
if ( !actor ) actor = game.actors.get(speaker.actor); if (!actor) actor = game.actors.get(speaker.actor);
// Get matching items // Get matching items
const items = actor ? actor.items.filter(i => i.name === itemName) : []; const items = actor ? actor.items.filter((i) => i.name === itemName) : [];
if ( items.length > 1 ) { if (items.length > 1) {
ui.notifications.warn(`Your controlled Actor ${actor.name} has more than one Item with name ${itemName}. The first matched item will be chosen.`); ui.notifications.warn(
} else if ( items.length === 0 ) { `Your controlled Actor ${actor.name} has more than one Item with name ${itemName}. The first matched item will be chosen.`
return ui.notifications.warn(`Your controlled Actor does not have an item named ${itemName}`); );
} } else if (items.length === 0) {
const item = items[0]; return ui.notifications.warn(`Your controlled Actor does not have an item named ${itemName}`);
}
const item = items[0];
// Trigger the item roll // Trigger the item roll
return item.roll(); return item.roll();
} }

File diff suppressed because it is too large Load diff

View file

@ -1,137 +1,132 @@
import { SW5E } from "../config.js"; import {SW5E} from "../config.js";
/** /**
* A helper class for building MeasuredTemplates for 5e powers and abilities * A helper class for building MeasuredTemplates for 5e powers and abilities
* @extends {MeasuredTemplate} * @extends {MeasuredTemplate}
*/ */
export default class AbilityTemplate extends MeasuredTemplate { export default class AbilityTemplate extends MeasuredTemplate {
/**
* A factory method to create an AbilityTemplate instance using provided data from an Item5e instance
* @param {Item5e} item The Item object for which to construct the template
* @return {AbilityTemplate|null} The template object, or null if the item does not produce a template
*/
static fromItem(item) {
const target = getProperty(item.data, "data.target") || {};
const templateShape = SW5E.areaTargetTypes[target.type];
if (!templateShape) return null;
/** // Prepare template data
* A factory method to create an AbilityTemplate instance using provided data from an Item5e instance const templateData = {
* @param {Item5e} item The Item object for which to construct the template t: templateShape,
* @return {AbilityTemplate|null} The template object, or null if the item does not produce a template user: game.user.data._id,
*/ distance: target.value,
static fromItem(item) { direction: 0,
const target = getProperty(item.data, "data.target") || {}; x: 0,
const templateShape = SW5E.areaTargetTypes[target.type]; y: 0,
if ( !templateShape ) return null; fillColor: game.user.color
};
// Prepare template data // Additional type-specific data
const templateData = { switch (templateShape) {
t: templateShape, case "cone":
user: game.user._id, templateData.angle = CONFIG.MeasuredTemplate.defaults.angle;
distance: target.value, break;
direction: 0, case "rect": // 5e rectangular AoEs are always cubes
x: 0, templateData.distance = Math.hypot(target.value, target.value);
y: 0, templateData.width = target.value;
fillColor: game.user.color templateData.direction = 45;
}; break;
case "ray": // 5e rays are most commonly 1 square (5 ft) in width
templateData.width = target.width ?? canvas.dimensions.distance;
break;
default:
break;
}
// Additional type-specific data // Return the template constructed from the item data
switch ( templateShape ) { const cls = CONFIG.MeasuredTemplate.documentClass;
case "cone": const template = new cls(templateData, {parent: canvas.scene});
templateData.angle = CONFIG.MeasuredTemplate.defaults.angle; const object = new this(template);
break; object.item = item;
case "rect": // 5e rectangular AoEs are always cubes object.actorSheet = item.actor?.sheet || null;
templateData.distance = Math.hypot(target.value, target.value); return object;
templateData.width = target.value;
templateData.direction = 45;
break;
case "ray": // 5e rays are most commonly 1 square (5 ft) in width
templateData.width = target.width ?? canvas.dimensions.distance;
break;
default:
break;
} }
// Return the template constructed from the item data /* -------------------------------------------- */
const template = new this(templateData);
template.item = item;
template.actorSheet = item.actor?.sheet || null;
return template;
}
/* -------------------------------------------- */ /**
* Creates a preview of the power template
*/
drawPreview() {
const initialLayer = canvas.activeLayer;
/** // Draw the template and switch to the template layer
* Creates a preview of the power template this.draw();
*/ this.layer.activate();
drawPreview() { this.layer.preview.addChild(this);
const initialLayer = canvas.activeLayer;
// Draw the template and switch to the template layer // Hide the sheet that originated the preview
this.draw(); if (this.actorSheet) this.actorSheet.minimize();
this.layer.activate();
this.layer.preview.addChild(this);
// Hide the sheet that originated the preview // Activate interactivity
if ( this.actorSheet ) this.actorSheet.minimize(); this.activatePreviewListeners(initialLayer);
}
// Activate interactivity /* -------------------------------------------- */
this.activatePreviewListeners(initialLayer);
}
/* -------------------------------------------- */ /**
* Activate listeners for the template preview
* @param {CanvasLayer} initialLayer The initially active CanvasLayer to re-activate after the workflow is complete
*/
activatePreviewListeners(initialLayer) {
const handlers = {};
let moveTime = 0;
/** // Update placement (mouse-move)
* Activate listeners for the template preview handlers.mm = (event) => {
* @param {CanvasLayer} initialLayer The initially active CanvasLayer to re-activate after the workflow is complete event.stopPropagation();
*/ let now = Date.now(); // Apply a 20ms throttle
activatePreviewListeners(initialLayer) { if (now - moveTime <= 20) return;
const handlers = {}; const center = event.data.getLocalPosition(this.layer);
let moveTime = 0; const snapped = canvas.grid.getSnappedPosition(center.x, center.y, 2);
this.data.update({x: snapped.x, y: snapped.y});
this.refresh();
moveTime = now;
};
// Update placement (mouse-move) // Cancel the workflow (right-click)
handlers.mm = event => { handlers.rc = (event) => {
event.stopPropagation(); this.layer.preview.removeChildren();
let now = Date.now(); // Apply a 20ms throttle canvas.stage.off("mousemove", handlers.mm);
if ( now - moveTime <= 20 ) return; canvas.stage.off("mousedown", handlers.lc);
const center = event.data.getLocalPosition(this.layer); canvas.app.view.oncontextmenu = null;
const snapped = canvas.grid.getSnappedPosition(center.x, center.y, 2); canvas.app.view.onwheel = null;
this.data.x = snapped.x; initialLayer.activate();
this.data.y = snapped.y; this.actorSheet.maximize();
this.refresh(); };
moveTime = now;
};
// Cancel the workflow (right-click) // Confirm the workflow (left-click)
handlers.rc = event => { handlers.lc = (event) => {
this.layer.preview.removeChildren(); handlers.rc(event);
canvas.stage.off("mousemove", handlers.mm); const destination = canvas.grid.getSnappedPosition(this.data.x, this.data.y, 2);
canvas.stage.off("mousedown", handlers.lc); this.data.update(destination);
canvas.app.view.oncontextmenu = null; canvas.scene.createEmbeddedDocuments("MeasuredTemplate", [this.data]);
canvas.app.view.onwheel = null; };
initialLayer.activate();
this.actorSheet.maximize();
};
// Confirm the workflow (left-click) // Rotate the template by 3 degree increments (mouse-wheel)
handlers.lc = event => { handlers.mw = (event) => {
handlers.rc(event); if (event.ctrlKey) event.preventDefault(); // Avoid zooming the browser window
event.stopPropagation();
let delta = canvas.grid.type > CONST.GRID_TYPES.SQUARE ? 30 : 15;
let snap = event.shiftKey ? delta : 5;
this.data.update({direction: this.data.direction + snap * Math.sign(event.deltaY)});
this.refresh();
};
// Confirm final snapped position // Activate listeners
const destination = canvas.grid.getSnappedPosition(this.x, this.y, 2); canvas.stage.on("mousemove", handlers.mm);
this.data.x = destination.x; canvas.stage.on("mousedown", handlers.lc);
this.data.y = destination.y; canvas.app.view.oncontextmenu = handlers.rc;
canvas.app.view.onwheel = handlers.mw;
// Create the template }
canvas.scene.createEmbeddedEntity("MeasuredTemplate", this.data);
};
// Rotate the template by 3 degree increments (mouse-wheel)
handlers.mw = event => {
if ( event.ctrlKey ) event.preventDefault(); // Avoid zooming the browser window
event.stopPropagation();
let delta = canvas.grid.type > CONST.GRID_TYPES.SQUARE ? 30 : 15;
let snap = event.shiftKey ? delta : 5;
this.data.direction += (snap * Math.sign(event.deltaY));
this.refresh();
};
// Activate listeners
canvas.stage.on("mousemove", handlers.mm);
canvas.stage.on("mousedown", handlers.lc);
canvas.app.view.oncontextmenu = handlers.rc;
canvas.app.view.onwheel = handlers.mw;
}
} }

View file

@ -1,145 +1,144 @@
export const registerSystemSettings = function() { export const registerSystemSettings = function () {
/**
* Track the system version upon which point a migration was last applied
*/
game.settings.register("sw5e", "systemMigrationVersion", {
name: "System Migration Version",
scope: "world",
config: false,
type: String,
default: game.system.data.version
});
/** /**
* Track the system version upon which point a migration was last applied * Register resting variants
*/ */
game.settings.register("sw5e", "systemMigrationVersion", { game.settings.register("sw5e", "restVariant", {
name: "System Migration Version", name: "SETTINGS.5eRestN",
scope: "world", hint: "SETTINGS.5eRestL",
config: false, scope: "world",
type: String, config: true,
default: "" default: "normal",
}); type: String,
choices: {
normal: "SETTINGS.5eRestPHB",
gritty: "SETTINGS.5eRestGritty",
epic: "SETTINGS.5eRestEpic"
}
});
/** /**
* Register resting variants * Register diagonal movement rule setting
*/ */
game.settings.register("sw5e", "restVariant", { game.settings.register("sw5e", "diagonalMovement", {
name: "SETTINGS.5eRestN", name: "SETTINGS.5eDiagN",
hint: "SETTINGS.5eRestL", hint: "SETTINGS.5eDiagL",
scope: "world", scope: "world",
config: true, config: true,
default: "normal", default: "555",
type: String, type: String,
choices: { choices: {
"normal": "SETTINGS.5eRestPHB", 555: "SETTINGS.5eDiagPHB",
"gritty": "SETTINGS.5eRestGritty", 5105: "SETTINGS.5eDiagDMG",
"epic": "SETTINGS.5eRestEpic", EUCL: "SETTINGS.5eDiagEuclidean"
} },
}); onChange: (rule) => (canvas.grid.diagonalRule = rule)
});
/** /**
* Register diagonal movement rule setting * Register Initiative formula setting
*/ */
game.settings.register("sw5e", "diagonalMovement", { game.settings.register("sw5e", "initiativeDexTiebreaker", {
name: "SETTINGS.5eDiagN", name: "SETTINGS.5eInitTBN",
hint: "SETTINGS.5eDiagL", hint: "SETTINGS.5eInitTBL",
scope: "world", scope: "world",
config: true, config: true,
default: "555", default: false,
type: String, type: Boolean
choices: { });
"555": "SETTINGS.5eDiagPHB",
"5105": "SETTINGS.5eDiagDMG",
"EUCL": "SETTINGS.5eDiagEuclidean",
},
onChange: rule => canvas.grid.diagonalRule = rule
});
/** /**
* Register Initiative formula setting * Require Currency Carrying Weight
*/ */
game.settings.register("sw5e", "initiativeDexTiebreaker", { game.settings.register("sw5e", "currencyWeight", {
name: "SETTINGS.5eInitTBN", name: "SETTINGS.5eCurWtN",
hint: "SETTINGS.5eInitTBL", hint: "SETTINGS.5eCurWtL",
scope: "world", scope: "world",
config: true, config: true,
default: false, default: true,
type: Boolean type: Boolean
}); });
/** /**
* Require Currency Carrying Weight * Option to disable XP bar for session-based or story-based advancement.
*/ */
game.settings.register("sw5e", "currencyWeight", { game.settings.register("sw5e", "disableExperienceTracking", {
name: "SETTINGS.5eCurWtN", name: "SETTINGS.5eNoExpN",
hint: "SETTINGS.5eCurWtL", hint: "SETTINGS.5eNoExpL",
scope: "world", scope: "world",
config: true, config: true,
default: true, default: false,
type: Boolean type: Boolean
}); });
/** /**
* Option to disable XP bar for session-based or story-based advancement. * Option to automatically collapse Item Card descriptions
*/ */
game.settings.register("sw5e", "disableExperienceTracking", { game.settings.register("sw5e", "autoCollapseItemCards", {
name: "SETTINGS.5eNoExpN", name: "SETTINGS.5eAutoCollapseCardN",
hint: "SETTINGS.5eNoExpL", hint: "SETTINGS.5eAutoCollapseCardL",
scope: "world", scope: "client",
config: true, config: true,
default: false, default: false,
type: Boolean, type: Boolean,
}); onChange: (s) => {
ui.chat.render();
}
});
/** /**
* Option to automatically collapse Item Card descriptions * Option to allow GMs to restrict polymorphing to GMs only.
*/ */
game.settings.register("sw5e", "autoCollapseItemCards", { game.settings.register("sw5e", "allowPolymorphing", {
name: "SETTINGS.5eAutoCollapseCardN", name: "SETTINGS.5eAllowPolymorphingN",
hint: "SETTINGS.5eAutoCollapseCardL", hint: "SETTINGS.5eAllowPolymorphingL",
scope: "client", scope: "world",
config: true, config: true,
default: false, default: false,
type: Boolean, type: Boolean
onChange: s => { });
ui.chat.render();
}
});
/** /**
* Option to allow GMs to restrict polymorphing to GMs only. * Remember last-used polymorph settings.
*/ */
game.settings.register('sw5e', 'allowPolymorphing', { game.settings.register("sw5e", "polymorphSettings", {
name: 'SETTINGS.5eAllowPolymorphingN', scope: "client",
hint: 'SETTINGS.5eAllowPolymorphingL', default: {
scope: 'world', keepPhysical: false,
config: true, keepMental: false,
default: false, keepSaves: false,
type: Boolean keepSkills: false,
}); mergeSaves: false,
mergeSkills: false,
/** keepClass: false,
* Remember last-used polymorph settings. keepFeats: false,
*/ keepPowers: false,
game.settings.register('sw5e', 'polymorphSettings', { keepItems: false,
scope: 'client', keepBio: false,
default: { keepVision: true,
keepPhysical: false, transformTokens: true
keepMental: false, }
keepSaves: false, });
keepSkills: false, game.settings.register("sw5e", "colorTheme", {
mergeSaves: false, name: "SETTINGS.SWColorN",
mergeSkills: false, hint: "SETTINGS.SWColorL",
keepClass: false, scope: "world",
keepFeats: false, config: true,
keepPowers: false, default: "light",
keepItems: false, type: String,
keepBio: false, choices: {
keepVision: true, light: "SETTINGS.SWColorLight",
transformTokens: true dark: "SETTINGS.SWColorDark"
} }
}); });
game.settings.register("sw5e", "colorTheme", {
name: "SETTINGS.SWColorN",
hint: "SETTINGS.SWColorL",
scope: "world",
config: true,
default: "light",
type: String,
choices: {
"light": "SETTINGS.SWColorLight",
"dark": "SETTINGS.SWColorDark"
}
});
}; };

View file

@ -3,33 +3,33 @@
* Pre-loaded templates are compiled and cached for fast access when rendering * Pre-loaded templates are compiled and cached for fast access when rendering
* @return {Promise} * @return {Promise}
*/ */
export const preloadHandlebarsTemplates = async function() { export const preloadHandlebarsTemplates = async function () {
return loadTemplates([ return loadTemplates([
// Shared Partials
"systems/sw5e/templates/actors/parts/active-effects.html",
// Shared Partials // Actor Sheet Partials
"systems/sw5e/templates/actors/parts/active-effects.html", "systems/sw5e/templates/actors/oldActor/parts/actor-traits.html",
"systems/sw5e/templates/actors/oldActor/parts/actor-inventory.html",
"systems/sw5e/templates/actors/oldActor/parts/actor-features.html",
"systems/sw5e/templates/actors/oldActor/parts/actor-powerbook.html",
"systems/sw5e/templates/actors/oldActor/parts/actor-notes.html",
// Actor Sheet Partials "systems/sw5e/templates/actors/newActor/parts/swalt-biography.html",
"systems/sw5e/templates/actors/oldActor/parts/actor-traits.html", "systems/sw5e/templates/actors/newActor/parts/swalt-core.html",
"systems/sw5e/templates/actors/oldActor/parts/actor-inventory.html", "systems/sw5e/templates/actors/newActor/parts/swalt-crew.html",
"systems/sw5e/templates/actors/oldActor/parts/actor-features.html", "systems/sw5e/templates/actors/newActor/parts/swalt-active-effects.html",
"systems/sw5e/templates/actors/oldActor/parts/actor-powerbook.html", "systems/sw5e/templates/actors/newActor/parts/swalt-features.html",
"systems/sw5e/templates/actors/oldActor/parts/actor-notes.html", "systems/sw5e/templates/actors/newActor/parts/swalt-inventory.html",
"systems/sw5e/templates/actors/newActor/parts/swalt-force-powerbook.html",
"systems/sw5e/templates/actors/newActor/parts/swalt-tech-powerbook.html",
"systems/sw5e/templates/actors/newActor/parts/swalt-resources.html",
"systems/sw5e/templates/actors/newActor/parts/swalt-traits.html",
"systems/sw5e/templates/actors/newActor/parts/swalt-biography.html", // Item Sheet Partials
"systems/sw5e/templates/actors/newActor/parts/swalt-core.html", "systems/sw5e/templates/items/parts/item-action.html",
"systems/sw5e/templates/actors/newActor/parts/swalt-active-effects.html", "systems/sw5e/templates/items/parts/item-activation.html",
"systems/sw5e/templates/actors/newActor/parts/swalt-features.html", "systems/sw5e/templates/items/parts/item-description.html",
"systems/sw5e/templates/actors/newActor/parts/swalt-inventory.html", "systems/sw5e/templates/items/parts/item-mountable.html"
"systems/sw5e/templates/actors/newActor/parts/swalt-force-powerbook.html", ]);
"systems/sw5e/templates/actors/newActor/parts/swalt-tech-powerbook.html",
"systems/sw5e/templates/actors/newActor/parts/swalt-resources.html",
"systems/sw5e/templates/actors/newActor/parts/swalt-traits.html",
// Item Sheet Partials
"systems/sw5e/templates/items/parts/item-action.html",
"systems/sw5e/templates/items/parts/item-activation.html",
"systems/sw5e/templates/items/parts/item-description.html",
"systems/sw5e/templates/items/parts/item-mountable.html"
]);
}; };

102
module/token.js Normal file
View file

@ -0,0 +1,102 @@
/**
* Extend the base TokenDocument class to implement system-specific HP bar logic.
* @extends {TokenDocument}
*/
export class TokenDocument5e extends TokenDocument {
/** @inheritdoc */
getBarAttribute(...args) {
const data = super.getBarAttribute(...args);
if (data && data.attribute === "attributes.hp") {
data.value += parseInt(getProperty(this.actor.data, "data.attributes.hp.temp") || 0);
data.max += parseInt(getProperty(this.actor.data, "data.attributes.hp.tempmax") || 0);
}
return data;
}
}
/* -------------------------------------------- */
/**
* Extend the base Token class to implement additional system-specific logic.
* @extends {Token}
*/
export class Token5e extends Token {
/** @inheritdoc */
_drawBar(number, bar, data) {
if (data.attribute === "attributes.hp") return this._drawHPBar(number, bar, data);
return super._drawBar(number, bar, data);
}
/* -------------------------------------------- */
/**
* Specialized drawing function for HP bars.
* @param {number} number The Bar number
* @param {PIXI.Graphics} bar The Bar container
* @param {object} data Resource data for this bar
* @private
*/
_drawHPBar(number, bar, data) {
// Extract health data
let {value, max, temp, tempmax} = this.document.actor.data.data.attributes.hp;
temp = Number(temp || 0);
tempmax = Number(tempmax || 0);
// Differentiate between effective maximum and displayed maximum
const effectiveMax = Math.max(0, max + tempmax);
let displayMax = max + (tempmax > 0 ? tempmax : 0);
// Allocate percentages of the total
const tempPct = Math.clamped(temp, 0, displayMax) / displayMax;
const valuePct = Math.clamped(value, 0, effectiveMax) / displayMax;
const colorPct = Math.clamped(value, 0, effectiveMax) / displayMax;
// Determine colors to use
const blk = 0x000000;
const hpColor = PIXI.utils.rgb2hex([1 - colorPct / 2, colorPct, 0]);
const c = CONFIG.SW5E.tokenHPColors;
// Determine the container size (logic borrowed from core)
const w = this.w;
let h = Math.max(canvas.dimensions.size / 12, 8);
if (this.data.height >= 2) h *= 1.6;
const bs = Math.clamped(h / 8, 1, 2);
const bs1 = bs + 1;
// Overall bar container
bar.clear();
bar.beginFill(blk, 0.5).lineStyle(bs, blk, 1.0).drawRoundedRect(0, 0, w, h, 3);
// Temporary maximum HP
if (tempmax > 0) {
const pct = max / effectiveMax;
bar.beginFill(c.tempmax, 1.0)
.lineStyle(1, blk, 1.0)
.drawRoundedRect(pct * w, 0, (1 - pct) * w, h, 2);
}
// Maximum HP penalty
else if (tempmax < 0) {
const pct = (max + tempmax) / max;
bar.beginFill(c.negmax, 1.0)
.lineStyle(1, blk, 1.0)
.drawRoundedRect(pct * w, 0, (1 - pct) * w, h, 2);
}
// Health bar
bar.beginFill(hpColor, 1.0)
.lineStyle(bs, blk, 1.0)
.drawRoundedRect(0, 0, valuePct * w, h, 2);
// Temporary hit points
if (temp > 0) {
bar.beginFill(c.temp, 1.0)
.lineStyle(0)
.drawRoundedRect(bs1, bs1, tempPct * w - 2 * bs1, h - 2 * bs1, 1);
}
// Set position
let posY = number === 0 ? this.h - h : 0;
bar.position.set(0, posY);
}
}

12
package-lock.json generated
View file

@ -1266,9 +1266,9 @@
} }
}, },
"hosted-git-info": { "hosted-git-info": {
"version": "2.8.8", "version": "2.8.9",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
"integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==" "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw=="
}, },
"image-size": { "image-size": {
"version": "0.5.5", "version": "0.5.5",
@ -3068,9 +3068,9 @@
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="
}, },
"y18n": { "y18n": {
"version": "3.2.1", "version": "3.2.2",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.2.tgz",
"integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=" "integrity": "sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ=="
}, },
"yargs": { "yargs": {
"version": "7.1.1", "version": "7.1.1",

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

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