Compare commits

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

542 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
CK
e8d4153333
Merge pull request #179 from unrealkakeman89/professorbunbury-sw5e
Updates to Force and Tech Powers Compendia (3/22/2021)
2021-03-22 15:51:27 -04:00
CK
14e9e996e5
Merge branch 'Develop' into professorbunbury-sw5e 2021-03-22 15:51:17 -04:00
Professor Bunbury
8c5de7d74d Updates to Force and Tech Powers Compendia (3/22/2021)
^ Updates Force Powers compendium with recent "My Dudes" releases.
^ Updates Tech Powers compendium with recent "My Dudes" releases.
+ Associated artwork to new powers from recent "My Dudes" releases.
2021-03-22 14:26:51 -04:00
supervj
56e6640f38
Merge pull request #174 from unrealkakeman89/Develop
update develop-VJ with Develop branch
2021-03-17 06:16:09 -04:00
supervj
4d67bef903 Update default values of temp data
Update defaults to null from 0 to allow descriptor of fields to show

Noted by Bacalla:
8206774843

I followed suit from the HP model, need to test to make sure this doesn't break anything.  All the code appears to default to 0 if it is null.
2021-03-16 12:24:29 -04:00
CK
fee77e2172
Merge pull request #172 from unrealkakeman89/Develop
Develop
2021-03-16 10:15:16 -04:00
supervj
a9b261d397 Update techpowers.db
removed @prof attack bonus and added scaling where necessary
2021-03-16 00:20:24 -04:00
CK
c586ae8c80
Merge pull request #170 from unrealkakeman89/Develop-VJ
Power point upgrades are finally done
2021-03-13 13:26:11 -05:00
supervj
90fe3126cb Update monsters.db
Added current version flag and updated the force powers to have accurate concentration and prerequisites as perf the Prof's updates
2021-03-12 21:33:35 -05:00
supervj
9b63457ae1 Fine tune migration concurrency
Fix migration concurrency so that it goes in an orderly process.  This ensures that big migrations will not crash.
2021-03-12 16:47:24 -05:00
Kakeman89
6e9f94a893 Merge branch 'Develop' of https://github.com/ExileofBrokenSky/sw5e into Develop 2021-03-11 15:50:52 -05:00
Kakeman89
dca918efb5 Update classfeatures.db 2021-03-11 15:49:23 -05:00
CK
87d615babc
Merge pull request #155 from ExileofBrokenSky/ExileOfBrokenSky
updated operative
2021-03-11 15:00:27 -05:00
CK
b309743425
Merge pull request #165 from unrealkakeman89/professorbunbury-sw5e
"My Dudes" Update - 3/11/2021
2021-03-11 15:00:07 -05:00
Professor Bunbury
649f2a8da7 Merge branch 'professorbunbury-sw5e' of https://github.com/unrealkakeman89/sw5e into professorbunbury-sw5e 2021-03-11 14:56:31 -05:00
Professor Bunbury
ba30ffbdcf "My Dudes" Update - 3/11/2021
^ Updates Half-Human traits list.
+ Adds Nothoiin to species compendium.
2021-03-11 14:56:27 -05:00
supervj
a99faad77c Code Cleanup and fix multiple migrations
see above
2021-03-11 01:30:03 -05:00
TJ
893c7b9d5e Async all the way down 2021-03-10 13:50:17 -06:00
supervj
2160508076 Update migration.js
more attempts at banging my head against the wall to no avail...  shortcut out if dataVersion matches though...
2021-03-09 23:37:20 -05:00
supervj
79e3286308 Update migration.js
Trying to add async to the entire migration function chain for owned actor entities s owe can use getUuid since it returns a promise.
2021-03-06 06:51:50 -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
CK
3b3f80db3c
Merge pull request #158 from baccalla/Develop
i18n:FR + fix localize not set
2021-03-05 18:21:38 -05:00
Michael Burgess
f37213ccbe Update classfeatures.db
Now aiming at develop branch, hopefully
has upto scholar and Aqinos form class features.
2021-03-05 13:26:50 -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
CK
8ec4b75ab9
Merge pull request #159 from unrealkakeman89/professorbunbury-sw5e
"My Dudes" Update 3/4/2021
2021-03-05 11:12:31 -05:00
CK
35460dd17a
Merge branch 'Develop' into professorbunbury-sw5e 2021-03-05 11:10:12 -05:00
supervj
b9cf140e33 Update migration.js
Try to get items to work, just need to get the flag to work
2021-03-04 22:07:44 -05:00
Professor Bunbury
e6bba96be0 "My Dudes" Update 3/4/2021
+ Adds Aquinos Form and associated artwork to Archetypes compendium.
^ Updates Archetypes links in Classes compendium
2021-03-04 12:58:39 -05:00
supervj
d48a25bf2f Revert 000 Series Droid and update migration
- Reverted UUID for 000 Series Protocol Droid.
- Updated NPC migration to check for dataVersion flag and shortcut out
- Attempted to add Item migration like NPC migration to fix power concentration and prerequisites, but it fails on compendium pull
2021-03-03 00:58:56 -05:00
baccalla
5619a32d84 fix a lot of localize not set 2021-02-28 08:57:21 +01:00
baccalla
4594cbea06 init i18n FR + Fix some localise not set 2021-02-27 21:05:42 +01:00
CK
b273125379 Artwork add
- background artwork added
- weapons artwork updated
2021-02-25 16:21:33 -05:00
CK
79274d5e54 Revert "Backgrounds art"
This reverts commit b5cd8d2fe8.
2021-02-25 16:18:33 -05:00
CK
b5cd8d2fe8 Backgrounds art
- add Backgrounds art
- add weapons art
2021-02-25 16:18:18 -05:00
CK
390489cef5
Update adventuringgear.db
- power cells updated to 240 charges
2021-02-25 15:31:32 -05:00
CK
7ee7c0b77d Compendium updates
- Weapons added
- backgrounds art added
2021-02-25 15:23:23 -05:00
CK
195c3150a5
Merge pull request #156 from unrealkakeman89/Develop-VJ
Develop vj
2021-02-25 15:12:43 -05:00
CK
94b0863de5 NPC Powers added
- Powers added to monsters.db
2021-02-25 14:52:10 -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
supervj
14a5639f40 Update Force Powers database
from Prof:
Added Telekinetic Shield and associated artwork (previously missing from compendium).
^ Updated force powers that were missing concentration settings.
Added prerequisites to all appropriate force powers.
2021-02-24 17:01:33 -05:00
supervj
eedef19778
Merge pull request #154 from unrealkakeman89/cyr-power-point-ui
Merge main into branch
2021-02-24 05:07:49 -05:00
TJ
2b723d051b Merge branch 'Develop-VJ' into cyr-power-point-ui 2021-02-23 22:43:23 -06:00
TJ
6ca9b0c7b4 Merge branch 'master' into cyr-power-point-ui 2021-02-23 22:42:20 -06:00
Professor Bunbury
cd9e3d15aa Updates to Force Powers Compendium
+ Added Telekinetic Shield and associated artwork (previously missed from compendium)
^ Updated force powers that were missing concentration settings.
+ Added prerequisites to all appropriate force powers.
2021-02-23 19:43:23 -05:00
TJ
1c58a1b813 Update migration.js 2021-02-23 17:49:07 -06:00
supervj
311854408c Update migration.js
thought this would work, but didn't?  Getting a reserved word error
2021-02-23 07:08:21 -05:00
supervj
6a7c65eefb Update migration.js
Changed to use newer createEmbeddedEntity (createOwnedItem will be deprecated in 0.8.x) and push the entire newPowers array at the same time.  Still saying createEmbeddedEntity doesn't exist on actor.
2021-02-23 07:00:43 -05:00
supervj
6f2e9cdab2 Update Migration
Everything but powers and flags are working
2021-02-23 01:17:30 -05:00
supervj
e43444bc48 Adding flag to config.js for dataVersion control
Adding flag for dataVersion control

** added shell of migration.js for updating NPC Data.  This is only being pushed for visibility by people other than me and is by no means done or tested and probably very broken.

Line 159 commented out to keep from running broken function.  Uncomment for testing.

Broken _updateNPCData from lines 242 to 289
2021-02-22 16:43:41 -05:00
CK
26390d5a4a
Merge pull request #150 from baccalla/Develop
Add Conditions Compendium with an entry for each
2021-02-21 10:16:15 -05:00
baccalla
0063b7dbca Add Conditions Compendium with an entry for each 2021-02-20 19:20:29 +01:00
supervj
c829f2b7b1
Merge pull request #149 from unrealkakeman89/cyr-power-point-ui
Fix typo
2021-02-19 21:07:06 -05:00
TJ
2127eaebc2 Merge branch 'Develop-VJ' into cyr-power-point-ui 2021-02-19 18:02:55 -06:00
TJ
1fbbf68f94 Fix typo 2021-02-19 18:02:39 -06:00
CK
59e4d5b0be
Update adventuringgear.db
re-add power cell
2021-02-19 11:14:26 -05:00
CK
eec45f8017
Merge pull request #147 from unrealkakeman89/professorbunbury-sw5e
2/18/2021 "My Dudes" Update to Species Compendium
2021-02-19 09:18:14 -05:00
Professor Bunbury
f2b8f32234 2/18/2021 "My Dudes" Update to Species Compendium
^ Updates Species compendium to include Baragwin with associated artwork.
2021-02-18 15:20:14 -05:00
supervj
2855185091 Merge branch 'Develop-VJ' of https://github.com/unrealkakeman89/sw5e into Develop-VJ 2021-02-18 14:33:07 -05:00
supervj
6e37fd8306 Update migration.js
Found a case where power migration could be skipped if the actor had no items
2021-02-18 14:32:54 -05:00
supervj
55517eec2f Update monsters.db
Updated **000 Protocol Droid as proof of concept
2021-02-18 14:11:39 -05:00
supervj
5113f3d58c Merge branch 'Develop-VJ' of https://github.com/unrealkakeman89/sw5e into Develop-VJ 2021-02-18 14:10:01 -05:00
supervj
66f51f8055 Revert "Update monsters.db"
This reverts commit cef1377926.
2021-02-18 14:09:45 -05:00
supervj
cef1377926 Update monsters.db
updated 000 Protocol Droid
2021-02-18 13:45:40 -05:00
CK
e5c197ce03
Merge pull request #146 from ExileofBrokenSky/ExileOfBrokenSky
Updated Monk
2021-02-18 13:24:23 -05:00
Kakeman89
d17515a59b Update monsters.db 2021-02-18 13:21:15 -05:00
Michael Burgess
1081675f56 Updated Monk
Updated Monk base class features with Icons
Separated Monk Vows into individual class features.
added DAE effects to most class features
added midi-qol effect to Evasion: superSaver.dex
2021-02-18 13:16:45 -05:00
CK
b984b074e9
Merge pull request #143 from unrealkakeman89/css-hotfix
Hotfixes for some minor css issues
2021-02-18 12:41:55 -05:00
supervj
1a1f27da49 Create monsters_backup.db
added as a temporary backup to correct changed _id numbers
2021-02-18 11:19:05 -05:00
supervj
7b71009fd7 Fix NPC power points
Ignore power point calculation for NPCs which are hardcoded in.
2021-02-18 10:14:06 -05:00
supervj
cbd9e7f94b Update Monsters and more casting styles
Finished adding monster values

Added innate and dual powercasting modes

Innate powercasters do not use force points or slots when casting powers
2021-02-18 00:24:02 -05:00
supervj
4fd32de1cd
Merge pull request #142 from unrealkakeman89/cyr-power-point-ui
Adding powers fix and powercasting dropdown
2021-02-17 20:53:18 -05:00
TJ
a676fecbfe Hotfixes for some minor css issues 2021-02-17 19:22:42 -06:00
Kakeman89
ec7dd517d1 Update monsters.db
Up to Nightsister Shaman
2021-02-17 15:40:05 -05:00
Kakeman89
81ec9d3ef4 Update monsters.db
Started with Sith Anathema
Ended with Trooper, Sapper
2021-02-16 16:52:22 -05:00
TJ
08700670f9 Add powercasting dropdown 2021-02-16 15:35:03 -06:00
TJ
d716d514df Fix add buttons on powers sheet 2021-02-16 14:40:06 -06:00
CK
f52dd99b7a
Update system.json 2021-02-16 12:28:34 -05:00
CK
f049a24c3e
Merge pull request #141 from unrealkakeman89/Develop
Develop
2021-02-16 11:01:05 -05:00
CK
abec2231ab
Merge pull request #140 from unrealkakeman89/professorbunbury-sw5e
Updates to Archetypes and Force Powers (artwork and databases)
2021-02-16 11:00:30 -05:00
Professor Bunbury
0fb0b5f3e7 Updates to Archetypes and Force Powers (artwork and databases)
^ Updates Archetypes compendium to include Occultist Pursuit and associated artwork.
^ Updates Force Powers compendium to include additional artwork.
2021-02-16 10:58:26 -05:00
Kakeman89
6f25cabf41 Merge branch 'Develop' 2021-02-16 09:34:59 -05:00
Kakeman89
f9d4ff4934 Merge branch 'master' into Develop 2021-02-16 09:34:38 -05:00
supervj
9758bfb8fc Update monsters.db
Added structure for force/tech points.
2021-02-16 07:34:15 -05:00
supervj
7fc983b042 Update to help NPCs work better
This requires all the monsters to be updated with force powers, powerForce/TechLevels, and powercasting "classes"
2021-02-16 02:15:39 -05:00
supervj
f9bc405787
Merge pull request #138 from unrealkakeman89/cyr-power-point-ui
More UI changes
2021-02-15 21:48:08 -05:00
Kakeman89
17cf6e836b Merge branch 'ExileOfBrokenSky' of https://github.com/ExileofBrokenSky/sw5e 2021-02-15 17:50:41 -05:00
Kakeman89
71cfd67c8b Merge branch 'ExileOfBrokenSky' of https://github.com/ExileofBrokenSky/sw5e into Develop 2021-02-15 17:48:53 -05:00
TJ
6ad94a7f82 Use localizations 2021-02-15 14:37:44 -06:00
TJ
86268ddabb Set correct power level 2021-02-15 14:28:07 -06:00
TJ
58a4033bf1 Merge branch 'Develop-VJ' into cyr-power-point-ui 2021-02-15 14:01:39 -06:00
TJ
74414e2257 Fixes for NPC display 2021-02-15 14:01:08 -06:00
supervj
e73c04b9f5 Update entity.js
whoops again :)
2021-02-15 14:05:45 -05:00
supervj
a893212b22 Change to Fix migration power lockdown
I think this will migrate every value over to have 1000 casts, which will then get reset to the lower of the value or character max once the actor is loaded.  Essentially pushing a long rest for power casting limits upon migration.
2021-02-15 09:15:36 -05:00
TJ
55f084592c Smaller font size 2021-02-14 21:59:51 -06:00
Michael Burgess
7509f9e1f8 Added Occultist Archetype and some minor updates.
Fixed typoed Manuevers into Maneuvers
Added Occultist Archetype for Scholar with icons
Updated Guardian Class Features text and icons
Expanded guardian options into individual class features for channel the force and guardian auras
2021-02-14 22:52:06 -05:00
supervj
95fc015a87
Merge pull request #136 from unrealkakeman89/cyr-power-point-ui
Power Point UI
2021-02-11 21:45:19 -05:00
Kakeman89
607fe25d8b Exile update 2021-02-11 17:45:21 -05:00
Kakeman89
75d82c2634 Merge branch 'ExileOfBrokenSky' of https://github.com/ExileofBrokenSky/sw5e 2021-02-11 17:38:44 -05:00
TJ
b56e652192 Merge branch 'Develop-VJ' into cyr-power-point-ui 2021-02-11 16:01:45 -06:00
supervj
4599639237 Fix Tech point consumption
whoops again...
2021-02-11 15:50:38 -05:00
supervj
c82a4331a5 Calculate Powers Known
added ability to calculate how many force/tech powers are known
2021-02-11 10:29:35 -05:00
supervj
aff4dda77f Fix Point usage
it seems to work now.  It really helps having a live data tree to look at.
2021-02-11 10:01:43 -05:00
supervj
1b0a70becb Update migration.js
whoops
2021-02-11 09:47:46 -05:00
supervj
54d04f0642 Fix Long Rest to split force and tech powers
Updated  long rest functionality to handle the force tech split
2021-02-11 07:04:13 -05:00
TJ
f0d94ba98b Add temp points and powers known 2021-02-10 21:32:00 -06:00
TJ
1ce2e92ccd Fully split force and tech powers into their own sheets 2021-02-10 18:39:36 -06:00
TJ
70d399e8f6 Merge branch 'Develop-VJ' into cyr-power-point-ui 2021-02-10 16:17:14 -06:00
TJ
8c6723a035 Start splitting force/tech sheets 2021-02-10 16:16:47 -06:00
supervj
fb73cdfc08 Split Force and Tech Power Limits
Split force and tech power limits.  Didn't test, may be broken.

Needs separate tab for force vs tech.  Still need to tally number of powers known
2021-02-10 15:41:02 -05:00
supervj
fa5dc07869 Have Powers consume Force and Tech Points
Add the ability for powers to automatically deduct force and tech points
2021-02-10 02:32:08 -05:00
supervj
ad50d1549f Powercasting works based on school of power
Updated power items to automatically change mod used when set to powercasting
2021-02-09 23:47:32 -05:00
supervj
8134ee4f09 Removed Duplicate Senses
Removed duplicate SW5E.Senses from en.json
2021-02-09 02:19:48 -05:00
supervj
b5ecde7f0c Added short and long rest and Temp points
Updated Force and Tech Points on Short and Long Rests along with Temporary points.

Also added class powercasting to migration
2021-02-09 02:14:10 -05:00
Michael Burgess
071ea6a67f added Operative: Pugnacity archetype, with icons
Linked icons to exisiting class features for:
base berserker ,
base consular,
base engineer,
base fighter.

Added individual class features for:
Consular's Force-Empowered Casting options,
Consular's Force Affinity options,
Fighter's Manuevers. (added experimental DAE effects, some may rely on Midi automation, if testing goes will may add similar to other options in the future)

Updated descriptions of:
base class features for berserker, consular, engineer and fighter, as the sw5e webstie has slightly tweaked the wording style for class features. Typically clarifying the class and level when originally it had just the level. Some have had mechanical changes, though very small. (Consular gaining their 3rd FEC at lvl 9 instead of lvl 10)

Replaced redundant class features and replaced with specific version when they were different class features sharing the same name. (Projected Barrier & One With the Force)
2021-02-05 14:49:14 -05:00
CK
a400d50817
Update weapons.db
- Add proper Item consumption to ranged weapons
- Changed consumption type to 'Item Use' instead of 'Ammunition'
2021-02-05 11:04:43 -05:00
CK
70f1e387fd
Update weapons.db 2021-02-05 11:00:08 -05:00
Kakeman89
c424033356 Update adventuringgear.db 2021-02-05 10:32:28 -05:00
Kakeman89
7c4807ddc0 Update entity.js 2021-02-05 10:01:35 -05:00
CK
917d55e706
Merge pull request #134 from unrealkakeman89/professorbunbury-sw5e
Updates to CSS/LESS (Chat text area)
2021-02-05 09:51:29 -05:00
professorbunbury
bec1a93446 Regenerate css 2021-02-05 13:02:03 +00:00
Professor Bunbury
bbbf765e99 Updates to CSS/LESS (Chat text area)
^ Updates CSS/LESS affecting background and text colors in Chat text area to make text more legible.
2021-02-04 21:03:19 -05:00
unrealkakeman89
d03828137c Regenerate css 2021-02-04 20:35:43 +00:00
unrealkakeman89
57a2ebee2b Regenerate css 2021-02-04 20:35:37 +00:00
CK
68f19ecab6
Merge pull request #133 from unrealkakeman89/professorbunbury-sw5e
Updates to Archetypes, CSS/LESS, and Species
2021-02-04 15:34:22 -05:00
CK
08a5c0be33
Merge branch 'Develop' into professorbunbury-sw5e 2021-02-04 15:34:12 -05:00
Professor Bunbury
61cfc83afd Updates to Archetypes, CSS/LESS, and Species
+ Adds Pugnacity Practice to Archetypes compendium
+ Adds Ho'Din to Species compendium
^ Updates senses DAE in Species compendium
^ Updates search bar background color for Light Theme
2021-02-04 15:25:37 -05:00
CK
745963dd51
Create CONTRIBUTIONS.md 2021-02-04 09:55:53 -05:00
CK
10a9be66f9
Rename CONTIBUTIONS.md to CONTRIBUTIONS.md 2021-02-04 09:38:26 -05:00
CK
9eaf8676de
Update CONTIBUTIONS.md 2021-02-04 09:37:52 -05:00
CK
c9b624c5d3
Update CONTIBUTIONS.md 2021-02-04 09:37:31 -05:00
CK
e192147a7b
Rename CONTIBUTIONS.MD to CONTIBUTIONS.md 2021-02-04 09:36:44 -05:00
CK
ddc5628d5b
Update CONTIBUTIONS.MD 2021-02-04 09:36:24 -05:00
CK
5d83ce3583
Create CONTIBUTIONS.MD 2021-02-04 09:35:32 -05:00
CK
cee8dc2c56
Update README.md 2021-02-03 17:44:18 -05:00
CK
d50279cd3a
Update README.md 2021-02-03 17:43:56 -05:00
CK
254fab365f
Update README.md 2021-02-03 17:43:15 -05:00
supervj
932c96ba4a Update Actor Entity
minor bug fixes and add points and powers known
2021-02-02 23:46:11 -05:00
CK
0934be8054
Update system.json 2021-02-02 22:09:02 -05:00
CK
5ccacb79a0
Merge pull request #130 from ExileofBrokenSky/ExileOfBrokenSky
added icons for Classs and Archetype features
2021-02-02 22:07:46 -05:00
supervj
6169add4a3
Merge pull request #131 from Cyr-/cyr-vj-changes
Changes to display force/tech points
2021-02-02 21:56:59 -05:00
supervj
1fda73a36e Update migration.js
add in new power structures
2021-02-02 21:42:15 -05:00
TJ
5898fb6f5b Changes to display force/tech points 2021-02-02 20:40:28 -06:00
supervj
ac198745f1 Fix for Power DC's
beginning attempt at storing power DC's
2021-02-01 12:33:51 -05:00
supervj
18c626bd42 Update classes.db
Add progression to the classes
2021-02-01 10:56:57 -05:00
supervj
e5df96b9b6 Power overhaul part 2
I think this will calculate the level limits for single and multiclassed characters in _computePowercastingProgression of module\actor\entity.js
2021-01-30 18:05:01 -05:00
Michael Burgess
63ce6b2d21 added icons for Classs and Archetype features
Notes on icon symbols:
Square outline = feature that is an Action
Diagonaly cut in half Half Square outline = feature that is a Bonus Action
Corner of a square = feature is a reaction.
Small square in bottom right corner = feature is passive or otherwise special

Class images are shown for each one.
Colors represent Base class or archetype as follows:
All base classes: Grey
Archetype monocolor for:
	Berserker - red
	Consular - green
	Engineer - brown
	Fighter - orange
	Monk - white
	Operative - black
	Scholar - violet
	Scout - spring green (i dub it Spriggeen)
	Guardian - azure
	Sentinel - yellow
Finally the symbols in the bottom left of each icon are Aurebesh letters, and are simply the first letter of either Action, Bonus, Reaction or Passive respectivly.
2021-01-30 13:56:46 -05:00
supervj
6b239d5d6b Partial update to entity.js
pushing a partial code section for feedback so far
2021-01-27 01:35:50 -05:00
supervj
4b7aac7fd5 Structure for Points overhaul 2021-01-27 01:25:46 -05:00
supervj
1994902fec subclass 2021-01-26 21:20:36 -05:00
supervj
3762d819eb
Merge pull request #129 from unrealkakeman89/master
Update my dev branch to current version
2021-01-26 20:32:09 -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
unrealkakeman89
b4cd5b1aec Regenerate css 2021-01-26 14:31:21 +00:00
CK
91fa36e19b
Merge pull request #128 from ellimist25/character-sheet-importer
sw5e.com character importer v1
2021-01-26 09:30:01 -05:00
CK
159680aa55
Merge branch 'Develop' into character-sheet-importer 2021-01-26 09:28:50 -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
ellimist25
3d5024b0c5 refactored current import 2021-01-25 21:59:05 -05:00
Mike Magarino
90dfc64b0f alpha 2021-01-25 19:22:30 -05:00
Mike Magarino
b38e51b20f skills still not working 2021-01-25 15:32:47 -05:00
Mike Magarino
6384c701e1 add class and level v1 2021-01-25 14:30:31 -05:00
CK
c537895682
Merge pull request #124 from unrealkakeman89/npc-biography
Fix to display NPC biography tab
2021-01-25 10:34:33 -05:00
Cyr-
fbd46f6dae Regenerate css 2021-01-25 00:42:02 +00:00
TJ
4d8a81b397
Update actor-global.less 2021-01-24 18:40:29 -06:00
Mike Magarino
cc96d58fa7 skills still broken 2021-01-24 17:19:30 -05:00
Mike Magarino
9742983db0 prior to revert 2021-01-24 17:01:49 -05:00
Mike Magarino
6d02f68608 Removed examples 2021-01-24 16:55:26 -05:00
Mike Magarino
71a99e97a9 Updated formatting 2021-01-24 16:53:56 -05:00
Mike Magarino
ea7a6e063a Merge branch 'master' into character-sheet-importer 2021-01-24 16:34:34 -05:00
Mike Magarino
78d5c0e26b messing around 2021-01-24 16:20:51 -05:00
CK
db93481e03
Merge pull request #123 from Cyr-/death-save-fix
Fix to let death saves be rollable
2021-01-24 14:52:16 -05:00
CK
4d9bd7fb36
Merge pull request #122 from Cyr-/ammo-fixes
Two ammo display fixes
2021-01-24 14:52:02 -05:00
TJ
5d91304592 Fix to let death saves be rollable 2021-01-24 13:49:56 -06:00
TJ
1b058f0540 Two ammo display fixes
1. Allow ammunition items to show up in the dropdown picker
2. Don't prompt to consume resources if the consuming object is a blaster
2021-01-22 16:17:49 -06:00
CK
dd04c983fe
Merge pull request #120 from unrealkakeman89/Develop-VJ
Hotfix o 1.2.4 version
2021-01-21 21:52:58 -05:00
CK
3399c9c279
Merge pull request #119 from unrealkakeman89/Develop-VJ
Hotfix o 1.2.4 version
2021-01-21 21:52:44 -05:00
supervj
911d1ce547 Hotfix o 1.2.4 version
Hotfix for 1.2.4 version
2021-01-21 21:46:42 -05:00
unrealkakeman89
f8d404d32d Regenerate css 2021-01-21 20:17:49 +00:00
CK
67710349d9
Merge pull request #118 from unrealkakeman89/professorbunbury-sw5e
CSS/LESS and New Char Sheet Hotfixes
2021-01-21 15:16:29 -05:00
unrealkakeman89
d6222a351d Regenerate css 2021-01-21 20:10:01 +00:00
CK
342df7f073
Merge pull request #116 from unrealkakeman89/professorbunbury-sw5e
CSS/LESS and New Char Sheet Hotfixes
2021-01-21 15:08:49 -05:00
Professor Bunbury
1812158f67 CSS/LESS and New Char Sheet Hotfixes
Fixes gear inoperability on character sheet traits and improvies legibility of HUD and chat input/textarea.
2021-01-21 15:07:01 -05:00
CK
13234e1c28
Merge pull request #115 from unrealkakeman89/Develop
Develop
2021-01-21 10:51:37 -05:00
CK
bab98a5024
Update system.json
Update R1-A2 and Foundry ver.
2021-01-21 10:42:13 -05:00
CK
01dbc82a15
Merge pull request #113 from unrealkakeman89/Develop-VJ
Core Update 1.2.3 merged with recent dev changes
2021-01-20 06:38:01 -05:00
supervj
fc26dce51e Merge branch 'Develop-VJ' of https://github.com/unrealkakeman89/sw5e into Develop-VJ 2021-01-19 23:10:27 -05:00
supervj
cec9bbd1b4 Add EngliBesh
Add to be inline with Dev Changes
2021-01-19 23:08:40 -05:00
supervj
8f9fbbefe5 Regenerate css 2021-01-20 03:55:06 +00:00
supervj
7f75ffc195 Core Update 1.2.3
Core update with 1.2.3 and Recent Dev changes integrated... I hope :)
2021-01-19 22:52:02 -05:00
supervj
184e916811 Reset 2021-01-19 21:47:38 -05:00
CK
38c8941bef
Merge pull request #112 from Cyr-/hotfix-losing-scroll-position
Fix for sheet losing scroll position
2021-01-19 17:41:59 -05:00
TJ
f42fae640b Fix for sheet losing scroll position 2021-01-19 16:34:47 -06:00
CK
e7fe448447
Merge pull request #111 from unrealkakeman89/professorbunbury-sw5e
Updates to Archetypes, Classes, Feats, and Species
2021-01-19 17:26:11 -05:00
CK
90c88b92bb
Update swalt-traits.html 2021-01-19 17:24:50 -05:00
CK
26eb2bd358
Update character-sheet.html 2021-01-19 17:23:56 -05:00
Professor Bunbury
a0c95f0725 Merge branch 'professorbunbury-sw5e' of https://github.com/unrealkakeman89/sw5e into professorbunbury-sw5e 2021-01-19 16:50:03 -05:00
Professor Bunbury
9710794095 Updates to Archetypes, Classes, Feats, and Species
Major updates to Feats (adding DAE features) and minor updates to Archetypes, Classes (added new Archetype links), and Species (corrected physical characteristic of Abyssin and added Patrolian).
2021-01-19 16:49:59 -05:00
CK
a66fc9e5ac Merge pull request #109 from unrealkakeman89/professorbunbury-sw5e
Updates to CSS/LESS and New Character Sheet
2021-01-19 12:32:44 -05:00
CK
fb7a12b244 Merge branch 'Develop' into professorbunbury-sw5e 2021-01-19 12:32:32 -05:00
Professor Bunbury
c1d247f72a Updated template.js to fix Character Sheet Notes problem
Removes notes HTML from new character sheet template to fix text field commas problem.
2021-01-19 12:14:38 -05:00
CK
a1130056b3
Merge pull request #106 from unrealkakeman89/Develop-VJ
Update to 1.2.2 core
2021-01-19 12:13:00 -05:00
Professor Bunbury
e61d4fc1a2 Corrected Character Sheet Notes (text field error)
Corrects error with text fields in the Notes section of the character sheet.
2021-01-19 11:45:54 -05:00
Professor Bunbury
1c6202bede New Character Sheet Fine Tuning (Resources & Traits Tab)
Rearranged Resources/Counters and Traits on Resources & Traits tab to avoid pushing vital information down the sheet.
2021-01-19 10:08:49 -05:00
Professor Bunbury
6947d65330 Updates to CSS/LESS and New Character Sheet
Various updates to CSS/LESS and adjustments to New Character Sheet to rearrange traits, resources, counters, favorites, and notes.
2021-01-19 09:27:48 -05:00
supervj
9e781ddcaa Regenerate css 2021-01-19 11:49:55 +00:00
supervj
96fdd0bec9 Fix New NPC Rollable Heath
Update new actor npc to have rollable health when clicked.
2021-01-19 06:42:45 -05:00
supervj
4237c8fa6a 1.2.2 template fix to open character sheets
Like I said, something simple...
2021-01-19 00:07:01 -05:00
supervj
9c6bd3873e Update Core 1.2.2
Update core to 1.2.2.  Sheets are broken
2021-01-18 23:49:04 -05:00
CK
8f2b0488a4
Merge pull request #105 from Cyr-/hotfix-feat-rolling
Added new handling for resources/rolling
2021-01-18 23:16:18 -05:00
TJ
f070d2725c Added new handling for resources/rolling 2021-01-18 16:13:32 -06:00
Kakeman89
45989ff9a4 Update classfeatures.db 2021-01-18 10:05:31 -05:00
CK
cf9b51cb99
Merge pull request #102 from ExileofBrokenSky/ExileOfBrokenSky
Added features for Engineer: Construction
2021-01-17 19:00:26 -05:00
CK
a037520e27
Merge pull request #101 from Cyr-/auto-build-less
Auto build less
2021-01-17 18:59:35 -05:00
Michael Burgess
9925a225d6 Added features for Engineer: Construction
Added features for Engineer: Construction
2021-01-17 17:55:48 -05:00
Cyr-
72ddf93442 Regenerate css 2021-01-16 05:04:48 +00:00
TJ
6bbf3304ec Add code to handle github action 2021-01-15 22:58:54 -06:00
CK
a2dc561593
Update README.md 2021-01-15 11:57:15 -05:00
CK
12464379b7
Update README.md 2021-01-15 11:56:53 -05:00
CK
ec63981bfa
Merge pull request #86 from unrealkakeman89/Develop
Develop
2021-01-15 10:50:16 -05:00
CK
bb7c763bc0
Merge branch 'master' into Develop 2021-01-15 09:38:34 -05:00
Kakeman89
1047d71e60 Merge branch 'Develop' 2021-01-15 09:37:59 -05:00
Kakeman89
86d8ff4dfc Merge branch 'master' into Develop 2021-01-15 09:37:23 -05:00
CK
685768baaf
Update system.json
Version R2-A1 = 1.2.0 D&D
2021-01-15 09:27:30 -05:00
CK
7052219642
Merge pull request #83 from Cyr-/base-update-fixes
Add in speed config button for PCs
2021-01-12 17:21:38 -05:00
TJ
81d68341ea Add in speed config button 2021-01-12 16:14:39 -06:00
Kakeman89
890afd530c Update classfeatures.db 2021-01-11 11:59:39 -05:00
Kakeman89
571eab2122 Merge branch 'ExileofBrokenSky-ExileOfBrokenSky' 2021-01-11 11:51:41 -05:00
Kakeman89
3a20f189f7 Merge branch 'ExileOfBrokenSky' of https://github.com/ExileofBrokenSky/sw5e into ExileofBrokenSky-ExileOfBrokenSky 2021-01-11 11:50:58 -05:00
Michael Burgess
5fd325e294 Added the new monk archtype features. 2021-01-11 11:04:02 -05:00
supervj
ac60692006 Update monsters.db
Added new Movement to Monsters

3 Known Issues:
Sniper Commando Crawl 30 put in Burrowing
Destroyer Droid Roll 60 put in Burrowing
Melee Destroyer Droid Roll 60 put in Burrowing
2021-01-10 23:50:38 -05:00
CK
4ef943c87b
Merge pull request #78 from Cyr-/base-update-fixes
Enable display of character sheet
2021-01-05 13:04:52 -05:00
TJ
e4e755fd68 More fixes to allow further testing 2021-01-04 22:16:56 -06:00
TJ
a597964bc4 Enable display of character sheet 2021-01-04 17:56:28 -06:00
CK
db5c5f4810
Update npc.js
added ..base.js
2021-01-04 17:42:45 -05:00
CK
4cf9ece35c
Merge pull request #77 from unrealkakeman89/Develop-VJ
Update Core to 1.2
2021-01-04 16:16:36 -05:00
CK
fb99bfeff6
Merge branch 'Develop' into Develop-VJ 2021-01-04 16:16:01 -05:00
supervj
e6bff40e1b Update Core to 1.2
Update Core to 1.2, pulled from dev 12/10/2020
2021-01-04 15:23:30 -05:00
Kakeman89
3a9dd3b465 Merge branch 'ExileofBrokenSky-master' 2020-12-17 16:01:43 -05:00
Kakeman89
0259c8b4e1 Merge branch 'master' of https://github.com/ExileofBrokenSky/sw5e into ExileofBrokenSky-master 2020-12-17 16:00:52 -05:00
Michael Burgess
2b80b19d70
Updated recent PHB revisions
everything is now in classfeatures.db instead of my holdover .db
2020-12-17 13:59:37 -05:00
Michael Burgess
29a6ac9495 Updated class features with recent PHB revision
Removed redundant db and merged it into classfeatures.db
2020-12-17 13:46:56 -05:00
Michael Burgess
a64f4e6d97 Revert "Updated PHB revisions"
This reverts commit eaa9082bb1.
2020-12-17 13:44:50 -05:00
Michael Burgess
eaa9082bb1 Updated PHB revisions 2020-12-17 13:04:02 -05:00
CK
44f7e3fb07
Update system.json
1.1.2
2020-12-15 21:27:51 -05:00
CK
1df1054cdf
Merge pull request #73 from unrealkakeman89/professorbunbury-sw5e
Lost Lightsaber Form
2020-12-15 21:25:37 -05:00
Professor Bunbury
7dbc48b718 Revert "Revert "Merge branch 'professorbunbury-sw5e' of https://github.com/unrealkakeman89/sw5e into professorbunbury-sw5e""
This reverts commit ad5fc6f6a2.
2020-12-15 18:29:54 -05:00
Professor Bunbury
ad5fc6f6a2 Revert "Merge branch 'professorbunbury-sw5e' of https://github.com/unrealkakeman89/sw5e into professorbunbury-sw5e"
This reverts commit ebd22fb58b, reversing
changes made to 5314eb6637.
2020-12-15 18:28:28 -05:00
Professor Bunbury
ebd22fb58b Merge branch 'professorbunbury-sw5e' of https://github.com/unrealkakeman89/sw5e into professorbunbury-sw5e 2020-12-15 18:23:01 -05:00
Professor Bunbury
5314eb6637 Forgotten LIghtsaber Form
Accidentally omitted from the recent update.
2020-12-15 18:19:41 -05:00
CK
1c24a74004
Merge pull request #72 from unrealkakeman89/Develop
Develop
2020-12-14 17:42:16 -05:00
CK
10d8ac429e
Merge pull request #71 from Cyr-/force-power-fix
Add preparation key to 2 force powers
2020-12-14 17:41:59 -05:00
TJ
9408903c45 Add preparation key to 2 force powers 2020-12-14 16:23:58 -06:00
CK
96f68e119d
Merge pull request #70 from unrealkakeman89/Develop
Develop
2020-12-14 13:05:33 -05:00
CK
07933f0483
Delete class-features-exiles.db 2020-12-14 13:05:19 -05:00
Mike Magarino
8df3c99e20 More testing 2020-12-13 12:25:22 -05:00
CK
e9436b42e4
Merge pull request #69 from Cyr-/armor-fixes
Fixes to align armor.db file with API
2020-12-12 11:13:25 -05:00
CK
b8c0e33102
Merge pull request #68 from unrealkakeman89/professorbunbury-sw5e
Updates to newSheet character.js
2020-12-12 11:12:59 -05:00
TJ
8a2aa82217 Fixes to align armor.db file with API 2020-12-12 10:07:55 -06:00
Mike Magarino
750aa1875f More testing 2020-12-11 18:10:50 -05:00
Professor Bunbury
707a765906
Updates to newSheet character.js 2020-12-11 14:12:53 -05:00
Professor Bunbury
7d1f305cc5 Updates to newSheet character.js 2020-12-11 14:09:38 -05:00
CK
01b4f0db58
Merge pull request #67 from unrealkakeman89/professorbunbury-sw5e
Updates to DAE, Archetypes, Backgrounds, Classes, Species, CSS/LESS, …
2020-12-11 13:40:58 -05:00
CK
b383a5a382
Merge branch 'Develop' into professorbunbury-sw5e 2020-12-11 13:40:18 -05:00
Professor Bunbury
14e9b0c341 Updates to DAE, Archetypes, Backgrounds, Classes, Species, CSS/LESS, etc.
What the subject line said.
2020-12-11 13:23:21 -05:00
ellimist25
ad038b8b03 Temporary fix on leveling 2020-12-11 08:51:20 -05:00
Mike Magarino
3d7c24ca17 Temp - everything is broken 2020-12-10 18:02:05 -05:00
CK
d679d79e99
Delete class-features-exiles.db 2020-12-10 13:07:24 -05:00
Kakeman89
dfc6beac9f Update classfeatures.db 2020-12-10 13:06:47 -05:00
CK
e5baac4f19
Merge pull request #66 from ExileofBrokenSky/ExileOfBrokenSky
Update class-features-exiles.db
2020-12-10 13:03:05 -05:00
Michael Burgess
90b045b575 Update class-features-exiles.db
Added Consular: Way of Tutelage, and Guardian: Vonil/Ishu
2020-12-10 12:26:46 -05:00
Mike Magarino
a291d0c909 Skills and proficiencies done. Class & lvl bump broken. 2020-12-09 21:39:51 -05:00
Mike Magarino
9315047183 Fixed typo 2020-12-09 09:32:09 -05:00
Mike Magarino
58c1548b81 Merging with upsteam/master 2020-12-09 09:31:30 -05:00
Mike Magarino
852309c973 Merging with upsteam/master 2020-12-09 09:30:54 -05:00
Mike Magarino
f9a59bb7b9 v1 of import is working. 2020-12-09 09:17:05 -05:00
Mike Magarino
892be9986e Added Dialog.
Added the Dialog. However, been unable to get json upload to work. Will change my method to use a textarea input instead.
2020-12-09 00:52:36 -05:00
CK
2c545a2ea1
Merge pull request #65 from Cyr-/midi-qol-stretched-image
Midi QOL stretched image in chat message fix
2020-12-08 17:39:20 -05:00
TJ
d0adca6f93 Merge remote-tracking branch 'upstream/Develop' into midi-qol-stretched-image 2020-12-07 21:13:05 -06:00
TJ
de69028910 Added a css fix to correctly size Midi QOL chat images 2020-12-07 21:11:02 -06:00
CK
22a9504522
Merge pull request #64 from Cyr-/add-unarmed-strike
Add unarmed strike
2020-12-07 14:14:28 -05:00
TJ
0e21018c0a Add unarmed strike 2020-12-07 12:41:41 -06:00
TJ
b45adbd67c
Merge pull request #1 from unrealkakeman89/Develop
Develop
2020-12-07 12:40:00 -06:00
CK
dd4de90bc0
Merge pull request #63 from Cyr-/character-sheet-fighting-styles
Added section for fighting styles to new character sheet
2020-12-05 20:02:05 -05:00
CK
cda0516b97
Merge pull request #62 from Cyr-/Develop
Fix for melee weapon ranges
2020-12-05 20:01:50 -05:00
TJ
24f904d9ce Minor whitespace fix 2020-12-05 17:08:43 -06:00
TJ
aa38f26d2e Added section for fighting styles to new character sheet 2020-12-05 17:05:36 -06:00
TJ
5dfc3dae7d Revert "Added section for fighting styles to new character sheet"
This reverts commit d54597d3be.
2020-12-05 16:53:51 -06:00
TJ
d54597d3be Added section for fighting styles to new character sheet 2020-12-05 16:52:48 -06:00
TJ
0c980346bc Fix for melee weapon ranges 2020-12-05 15:37:37 -06:00
CK
6155d602ef
Merge pull request #61 from Cyr-/Develop
Added new weapons from player's handbook
2020-12-04 18:23:03 -05:00
TJ
f9e1bf3ab2 Set attack bonus to 0 2020-12-03 23:47:08 -06:00
TJ
ff0f26dc45 Use ints for ranges 2020-12-03 23:41:26 -06:00
TJ
2d2b63b352 Added new weapons from player's handbook 2020-12-03 23:23:55 -06:00
CK
e74de93825
Merge pull request #60 from ExileofBrokenSky/ExileOfBrokenSky
99% of archetype class features done
2020-12-03 17:28:06 -05:00
Michael Burgess
1a85c3df60 Update class-features-exiles.db
Added all scout and sentinel archtypes, but not the as of yet brand new guardian and consular archtypes with human companions.
2020-12-03 15:26:17 -05:00
CK
770f1650d3
Merge pull request #59 from unrealkakeman89/Develop
Develop
2020-12-03 09:36:58 -05:00
Kakeman89
3d1eb17edc Merge branch 'master' into Develop 2020-12-03 09:36:44 -05:00
CK
e2520833e1
Update classFeatures.js 2020-12-03 09:27:39 -05:00
CK
a5cb069075
Update system.json 2020-12-02 09:53:47 -05:00
CK
325e6c9802
Merge pull request #58 from unrealkakeman89/cbnathanael
fixed inventory and favorites dragging to macro bar
2020-12-02 09:36:04 -05:00
Nathanael Phillips
601cbd1a69 fixed inventory and favorites dragging to macro bar 2020-12-02 07:18:08 -07:00
CK
456c360a68
Merge pull request #57 from Cyr-/master
Set correct weapon types
2020-12-01 23:11:06 -05:00
TJ
a5e00b36c9 Set correct weapon types 2020-12-01 21:28:51 -06:00
CK
52ba3fcf17
Update classFeatures.js 2020-12-01 13:11:42 -05:00
CK
756865a10c
Merge pull request #56 from unrealkakeman89/cbnathanael
fixed character sheet image overflow
2020-12-01 09:27:48 -05:00
Nathanael Phillips
e481098c98 fixed character sheet image overflow 2020-12-01 06:24:59 -07:00
CK
0d90ff5ad6
Update classfeatures.db 2020-11-30 16:07:26 -05:00
CK
e0ce94e2b5
Update classfeatures.db 2020-11-30 16:06:59 -05:00
CK
7d50c95cc8
Merge pull request #54 from unrealkakeman89/cbnathanael
New NPC Sheet Updated
2020-11-30 16:04:40 -05:00
CK
e03afb2498
Merge pull request #53 from ExileofBrokenSky/ExileOfBrokenSky
added all operative and scholor archtype class features,
2020-11-30 16:04:30 -05:00
Nathanael Phillips
b621ba27d3 fixed npc size text in sheet header 2020-11-30 09:08:24 -07:00
Nathanael Phillips
27a53a9361 completed NPC sheet 2020-11-30 09:06:27 -07:00
Nathanael Phillips
88b4e6dfc9 Merge branch 'Develop' into cbnathanael 2020-11-30 06:18:34 -07:00
Nathanael Phillips
3d93ec8e90 npc-sheet work continued 2020-11-30 06:17:59 -07:00
Michael Burgess
5426b9f0ec added all operative and scholor archtype class features, including archaeologist 2020-11-27 13:58:01 -05:00
CK
3c4a77fc6e
Update classfeatures.db 2020-11-25 18:04:51 -05:00
CK
4f5298dbf6
Merge pull request #52 from unrealkakeman89/Develop-VJ
Change subclass references to archetype references
2020-11-25 14:44:15 -05:00
supervj
df456997eb Fix find-replace error
Fix typo in find and replace
2020-11-25 14:24:36 -05:00
supervj
e6a0cc8066 Change subclass references to archetype references
converting DND subclass to SW archetype
2020-11-25 14:09:47 -05:00
CK
7bf8d10df6
Merge pull request #51 from unrealkakeman89/Develop-VJ
Fix deprecated use of isPC
2020-11-25 13:59:35 -05:00
CK
539b9399b2
Merge pull request #50 from ExileofBrokenSky/ExileOfBrokenSky
Added more archtype features to class-features-exiles.db
2020-11-25 13:59:18 -05:00
supervj
3fae34cc43 Fix deprecated use of isPC
This is a patch to the DND5E system pulled from the DND5E gitlab site
2020-11-25 13:52:30 -05:00
Michael Burgess
af9a24a443 Added more archtype features to class-features-exiles.db
This adds archetype class features for:
Consular: Way of Technology;
Engineer: Biotech;
All Guardian Archetypes;
All Monk Archetypes.
For some reason class feature's requirement info wouldn't carry from the compendium to the world items, so for this update, I have left all requirements blank, to be readded when they won't get erased.
2020-11-20 15:37:53 -05:00
CK
7176bcebc1
Merge pull request #49 from unrealkakeman89/Develop
Develop
2020-11-19 21:15:27 +00:00
CK
719709d516
Merge branch 'master' into Develop 2020-11-19 21:15:06 +00:00
CK
916b820a01
Merge pull request #48 from unrealkakeman89/professorbunbury-sw5e
Long Lost Icons Reunited, and it feels so gooooooood...
2020-11-19 20:35:09 +00:00
Professor Bunbury
3afef6c7f1 Merge branch 'professorbunbury-sw5e' of https://github.com/unrealkakeman89/sw5e into professorbunbury-sw5e 2020-11-19 15:27:00 -05:00
Professor Bunbury
041fdc616f Long Lost Icons Reunited, and it feels so goooooood... 2020-11-19 15:26:32 -05:00
CK
95868ee15f
Update en.json 2020-11-19 15:00:16 -05:00
CK
176ffec3c6
Update en.json 2020-11-19 14:59:16 -05:00
CK
0cfddd6d27
Update en.json 2020-11-19 14:54:54 -05:00
CK
da6b387cf5
Update en.json 2020-11-19 14:54:43 -05:00
CK
87bb57a4ba
Merge pull request #47 from unrealkakeman89/professorbunbury-sw5e
Major DAE Updates to Species plus New Archetype
2020-11-19 18:47:07 +00:00
CK
9fea221d6b
Merge branch 'Develop' into professorbunbury-sw5e 2020-11-19 18:47:00 +00:00
Professor Bunbury
25ec442f9f Major DAE Updates to Species plus New Archetype 2020-11-19 13:24:57 -05:00
CK
e82dd1406c
Update classfeatures.db 2020-11-18 09:24:29 -05:00
CK
b8e3511be8
Merge pull request #46 from ExileofBrokenSky/ExileOfBrokenSky
Update classfeatures.db with some archtype features
2020-11-18 03:04:28 +00:00
Nathanael Phillips
2d8cc8cda2 Merge branch 'Develop' into cbnathanael 2020-11-17 08:50:06 -07:00
Nathanael Phillips
acd0e151e7 beginning npc sheet 2020-11-17 06:19:57 -07:00
Mike Magarino
af2eb1e97e Temp 2020-11-15 15:15:25 -05:00
ellimist25
c068ebff9a Finished and tested adding import button 2020-11-14 01:11:43 -05:00
ellimist25
435c9e295f More updates 2020-11-14 00:56:23 -05:00
ellimist25
3d7a13941d Updated characterImport with main logic 2020-11-13 23:34:28 -05:00
ellimist25
ab23a67a72 Added examples 2020-11-13 21:46:01 -05:00
Michael Burgess
409dc867c8 Update classfeatures.db 2020-11-13 17:44:42 -05:00
CK
f6e875ecb5
Update weapon.html 2020-11-13 09:45:47 -05:00
ellimist25
5c6e39e633 Enabled hooks debug and created class. 2020-11-13 08:14:14 -05:00
supervj
68a1b6a9f0 System 1.1.1 ** Requires Foundry 0.7.6
System main update to be inline with dnd5e 1.1.1

Added active effects to as many sheets as I thought applicable. Please check loot, I made an attempt but it may be broken

All .less .css and actor .html updates were made to the old actors.  New actors may be broken with this update

removed templates\actors\oldActor\parts\actor-effects.html for newer templates\actors\parts\active-effects.html

removed module\apps\cast-dialog, templates\apps\cast-cast.html, and templates\items\cast.html.  I do not think they are used, I think they were deprecated when powers were treated as items, if not we can add them back in.

**NOTE** REQUIRES Foundry 0.7.6
2020-11-12 17:30:07 -05:00
CK
e25986900c
Update weapon.html 2020-11-10 17:15:51 -05:00
CK
53e02ca3be
Update system.json 2020-11-10 17:15:14 -05:00
Kakeman89
1c3cc3dd37 Merge branch 'master' of https://github.com/unrealkakeman89/sw5e 2020-11-09 09:59:38 -05:00
Kakeman89
c2bf72a57e Update classes.db
updated icon path
2020-11-09 09:59:26 -05:00
CK
b88572c344
Delete items.db 2020-11-09 09:58:37 -05:00
CK
27f5fa3670
Merge pull request #40 from unrealkakeman89/cbnathanael
small style updates
2020-11-09 14:52:50 +00:00
Kakeman89
50b70163c6 Delete journal.db 2020-11-09 09:51:56 -05:00
Kakeman89
494f1b45fc Update sw5e.css 2020-11-09 09:42:14 -05:00
Nathanael Phillips
0dba80c85a small style updates 2020-11-09 07:27:16 -07:00
Nathanael Phillips
105ebc16b7 Merge branch 'pr/39' into Develop 2020-11-06 13:35:18 -07:00
Nathanael Phillips
d217b00916 character sheet added, light theme only 2020-11-05 20:37:19 -07:00
Kakeman89
36719a08c6 Updated 2020-11-05 16:57:54 -05:00
Kakeman89
22564df6a0 Merge branch 'professorbunbury-sw5e' into Develop 2020-11-05 16:55:14 -05:00
Professor Bunbury
1130fdc30d Compendia for Fighting Masteries and Styles and Other Minor Updates
What the title says.
2020-11-05 15:37:53 -05:00
Nathanael Phillips
e7ec90c944 more ui elements updated. 2020-11-05 09:56:31 -07:00
CK
0fa0f8b10c
Update forcepowers.db 2020-11-05 10:16:00 -05:00
Nathanael Phillips
b75aafd3ce split styles to accomodate themes; added light/dark theme 2020-11-04 13:12:41 -07:00
Nathanael Phillips
f7d7580b3a Beginning of global styling; Full Sidebar restyle 2020-11-02 12:23:55 -07:00
CK
f3c834e661
Merge pull request #37 from unrealkakeman89/Kakeman89
Kakeman89
2020-11-02 18:42:46 +00:00
Kakeman89
e8d748f9b4 Compendia Update
Paths updated. Journal Entries cleaned out.
2020-11-02 13:38:19 -05:00
CK
2bd79a7603
Update system.json 2020-11-02 07:45:59 -05:00
CK
470a5d52d5
Update system.json 2020-11-01 21:18:09 -05:00
CK
752c35be94
Merge pull request #36 from unrealkakeman89/professorbunbury-sw5e
Fixes to armor.db shield AC bonuses
2020-11-02 01:10:50 +00:00
Professor Bunbury
255e658768 Merge branch 'professorbunbury-sw5e' of https://github.com/unrealkakeman89/sw5e into professorbunbury-sw5e 2020-11-01 20:03:22 -05:00
Professor Bunbury
24639120f5 Fixes to shield AC bonuses 2020-11-01 20:03:17 -05:00
Professor Bunbury
dee0c0a01a
Update classes.db 2020-11-01 19:21:00 -05:00
Professor Bunbury
9386cece09
Update system.json 2020-11-01 11:10:35 -05:00
CK
53c963874f
Merge pull request #35 from unrealkakeman89/professorbunbury-sw5e
Fixes to .dbs' icon path problem
2020-10-30 19:39:48 +00:00
CK
9458928409
Merge branch 'master' into professorbunbury-sw5e 2020-10-30 19:38:52 +00:00
Professor Bunbury
13e09fbada Merge branch 'professorbunbury-sw5e' of https://github.com/unrealkakeman89/sw5e into professorbunbury-sw5e 2020-10-30 15:15:57 -04:00
Professor Bunbury
c6b4b54cf4 Fixes to .dbs for icon problem 2020-10-30 15:03:27 -04:00
Kakeman89
a8c2881a7b PNG to Webp 2020-10-30 11:57:25 -04:00
CK
63e123809c
Update classes.db
change png to webp
2020-10-30 11:54:20 -04:00
CK
39e951903f
Update system.json
Removed duplicate Classes db
2020-10-30 11:40:45 -04:00
CK
9c06b01c6d
Merge pull request #34 from unrealkakeman89/professorbunbury-sw5e
0.7.x Updates for Archetypes, Backgrounds, Classes, Lightsaber Forms, and Species
2020-10-30 15:38:44 +00:00
Kakeman89
dd0d207149 Merge branch 'master' into professorbunbury-sw5e 2020-10-30 11:36:29 -04:00
Professor Bunbury
18a28217c1 0.7.x Updates for Archetypes, Backgrounds, Classes, Lightsaber Forms, and Species
Tons of updates to relect recent changes to PHB and EC source materials.
AC minimum for "Hide" species trait currently does not working using DAE.
2020-10-29 20:30:50 -04:00
CK
d3801930df
Update system.json 2020-10-29 09:16:19 -04:00
Professor Bunbury
d392b568db Spot the link / entityClass error! 2020-10-23 17:45:27 -04:00
Professor Bunbury
5f5a145626 Spot the link / entityClass error! 2020-10-23 13:32:23 -04:00
Professor Bunbury
c9a9f75a5d Spot the link / entityClass error! 2020-10-23 13:05:18 -04:00
Professor Bunbury
de52576408 Spot the link / entityClass error! 2020-10-23 12:36:42 -04:00
CK
26e9e89106
Merge pull request #33 from cbnathanael/master
basic setup to use less for css compilation
2020-10-21 12:22:17 -04:00
Nathanael Phillips
f90b2f2605 basic setup to use less for css compilation 2020-10-21 07:15:47 -07:00
CK
088aa386ad
Merge pull request #32 from cbnathanael/master
removed duplicate data.isLine check
2020-10-20 10:26:43 -04:00
Nathanael Phillips
a7aa7a8502 removed duplicate data.isLine check 2020-10-20 07:16:30 -07:00
CK
48c93b542a
Merge pull request #31 from cbnathanael/techpowers
added tech powers compendium and icons
2020-10-19 17:14:37 -04:00
Nathanael Phillips
575768880e removed addition of proficiency bonus to damage 2020-10-19 13:51:15 -07:00
Nathanael Phillips
3069effd1e added tech powers compendium and icons 2020-10-19 13:45:51 -07:00
Kakeman89
b92edb4200 Update from 0.98 DND
Current 10-18-20
2020-10-19 15:52:56 -04:00
Kakeman89
90fab8d89f Iteration on effects tab of character sheet 2020-10-19 13:04:48 -04:00
CK
a40f7adb9c
Update forcepowers.db 2020-10-13 16:24:42 -04:00
990 changed files with 43808 additions and 14869 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

24
.github/workflows/main.yml vendored Normal file
View file

@ -0,0 +1,24 @@
name: Gulp build and commit updated stylesheets
on:
push:
branches: [master, Develop]
pull_request:
branches: [master, Develop]
jobs:
gulp-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
ref: ${{ github.event.pull_request.head.ref }}
- name: Compile with Gulp
uses: elstudio/actions-js-build/build@v2
- name: Commit changes
uses: elstudio/actions-js-build/commit@v3
with:
commitMessage: Regenerate css

4
.gitignore vendored
View file

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

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

3
.vscode/settings.json vendored Normal file
View file

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

8
CONTRIBUTIONS.md Normal file
View file

@ -0,0 +1,8 @@
Rick Fisto
- [Fisto's Codex](https://www.gmbinder.com/share/-M-qA_FYgTwJjU8yFjjx)
Heresy
- [Heritic's Guide to the Galaxy](https://www.gmbinder.com/share/-M815p5BfQ0wbdKY7zqN)
Erikstormtrooper
- [Englibesh Font](http://www.erikstormtrooper.com/englibesh.htm)

View file

@ -1,5 +1,7 @@
# Foundry Virtual Tabletop - SW5e Game System
This unofficial implementation of the SW5e system for Foundry VTT is made by fans for fans and is not associated with SW5e, Disney, Wizards of the Coast, or their partners in any way.
This game system for [Foundry Virtual Tabletop](http://foundryvtt.com) provides character sheet and game system
support for the SW5E roleplaying game.
@ -25,3 +27,10 @@ may do this by cloning the repository or downloading a zip archive from the
Code and content contributions are accepted. Please feel free to submit issues to the issue tracker or submit merge
requests for code changes. Approval for such requests involves code and (if necessary) design review by The Dev Team.
Please reach out on the SW5E Foundry Dev Discord with any questions.
## Compatible Modules and Optimum Settings
- DAE (Dynamic Active Effects) is needed for many automatic features.
- **Please enable: "Include active effects in special traits display" in "Configure Game Settings> Module Settings> Dynamic Active Effects".**
- Midi QoL is compatible with great features
- Token Action Hud has compatibility

BIN
fonts/Aurebesh.ttf Normal file

Binary file not shown.

BIN
fonts/EngliBesh-KG3W.ttf Normal file

Binary file not shown.

BIN
fonts/OpenSans-Bold.ttf Normal file

Binary file not shown.

Binary file not shown.

BIN
fonts/OpenSans-Italic.ttf Normal file

Binary file not shown.

View file

@ -1,32 +1,41 @@
const gulp = require('gulp');
const less = require('gulp-less');
/* ----------------------------------------- */
/* Compile LESS
/* ----------------------------------------- */
const SW5E_LESS = ["less/*.less"];
function compileLESS() {
return gulp.src("less/sw5e.less")
.pipe(less())
.pipe(gulp.dest("./"))
}
const css = gulp.series(compileLESS);
/* ----------------------------------------- */
/* Watch Updates
/* ----------------------------------------- */
function watchUpdates() {
gulp.watch(SW5E_LESS, css);
}
/* ----------------------------------------- */
/* Export Tasks
/* ----------------------------------------- */
exports.default = gulp.series(
gulp.parallel(css),
watchUpdates
);
exports.css = css;
const gulp = require("gulp");
const less = require("gulp-less");
/* ----------------------------------------- */
/* Compile LESS
/* ----------------------------------------- */
const SW5E_LESS = ["less/**/*.less"];
function compileLESS() {
return gulp.src("less/original/sw5e.less").pipe(less()).pipe(gulp.dest("./"));
}
function compileGlobalLess() {
return gulp.src("less/update/sw5e-global.less").pipe(less()).pipe(gulp.dest("./"));
}
function compileLightLess() {
return gulp.src("less/update/sw5e-light.less").pipe(less()).pipe(gulp.dest("./"));
}
function compileDarkLess() {
return gulp.src("less/update/sw5e-dark.less").pipe(less()).pipe(gulp.dest("./"));
}
const css = gulp.series(compileLESS, compileGlobalLess, compileLightLess, compileDarkLess);
/* ----------------------------------------- */
/* Watch Updates
/* ----------------------------------------- */
function watchUpdates() {
gulp.watch(SW5E_LESS, css);
}
/* ----------------------------------------- */
/* Export Tasks
/* ----------------------------------------- */
exports.default = css;
gulp.parallel(css), (exports.watch = gulp.series(gulp.parallel(css), watchUpdates));

File diff suppressed because it is too large Load diff

1027
lang/fr.json Normal file

File diff suppressed because it is too large Load diff

1027
lang/it.json Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,232 +0,0 @@
@import "./variables.less";
.sw5e.sheet.item {
min-height: 420px;
/* ----------------------------------------- */
/* Sheet Header */
/* ----------------------------------------- */
.sheet-header {
img.profile {
border: 2px solid #000;
}
.item-subtitle {
flex: 0 0 80px;
height: 60px;
margin: 0;
padding: 5px;
text-align: right;
color: @colorTan;
.item-type {
font-size: 24px;
line-height: 26px;
margin: 0;
}
.item-status {
font-size: 16px;
line-height: 24px;
}
}
}
.sheet-navigation {
margin-bottom: 5px;
.item {
font-size: 18px;
}
}
.sheet-body {
overflow: hidden;
.tab {
padding: 0 5px;
overflow: hidden auto;
}
.item-properties {
flex: 0 0 120px;
margin: 5px 5px 5px 0;
padding-right: 5px;
border-right: @borderGroove;
.form-group {
margin: 0;
label {
line-height: 20px;
}
input {
text-align: right;
}
}
.properties-list {
list-style: none;
margin: 0;
padding: 0;
li {
margin: 3px 0;
padding: 0 2px;
background: rgba(0, 0, 0, 0.05);
border: @borderGroove;
text-align: center;
font-size: 12px;
line-height: 18px;
}
}
}
}
/* ----------------------------------------- */
/* Item Details Form */
/* ----------------------------------------- */
.details {
// Item Sheet form fields
input[type="text"],
input[type="number"],
select {
height: 24px;
border: 1px solid @colorTan;
background: rgba(0, 0, 0, 0.05);
}
.form-group {
span {
text-align: center;
line-height: 24px;
}
}
.form-group.input-select {
select {
flex: 1.8;
}
}
.form-group.input-select-select {
select {
flex: 1.5;
}
}
.form-group.uses-per {
input {
flex: 1;
}
span {
flex: 0 0 16px;
}
select {
flex: 3;
}
}
span.sep {
flex: 0 0 8px;
}
.prepared {
flex: 1.3 !important;
text-align: right;
padding-right: 10px;
}
// Power Materials
.power-materials {
flex: 0 0 100%;
margin: 0.25em 0;
justify-content: flex-end;
label {
flex: 0 0 64px;
text-align: right;
margin-right: 5px;
font-size: 12px;
line-height: 24px;
}
input[type="text"] {
flex: 0 0 48px;
margin-right: 10px;
}
}
}
/* ----------------------------------------- */
/* Item Actions */
/* ----------------------------------------- */
h4.damage-header {
margin: 0;
padding: 0;
font-weight: bold;
line-height: 24px;
color: @colorOlive;
}
.damage-parts {
list-style: none;
margin: 0;
padding: 0;
.damage-part {
flex: 0 0 100%;
padding: 0;
input {
flex: 3;
}
select {
margin-left: 5px;
flex: 1;
}
}
}
.damage-control {
width: 18px;
flex: 0 0 18px;
line-height: 24px;
float: right;
text-align: right;
color: @colorTan;
}
.recharge.form-group {
span {
text-align: right;
padding-right: 3px;
}
input[type="text"] {
flex: 0 0 32px;
text-align: center;
}
label.checkbox {
flex: none;
input {
width: 16px;
height: 16px;
top: 4px;
}
}
}
/* ----------------------------------------- */
/* Item Actions */
/* ----------------------------------------- */
.weapon-properties label.checkbox {
flex: 0 0 98px;
}
/* ----------------------------------------- */
/* Loot Sheet (No Tabs) */
/* ----------------------------------------- */
.loot-header {
margin-bottom: 10px;
}
}

View file

@ -69,12 +69,29 @@
line-height: 30px;
}
}
}
}
.attributes {
input.temphp {
width: 48%;
// Movement Configuration
.movement, .hit-dice {
h4.attribute-name {
position: relative;
}
.config-button {
position: absolute;
display: none;
right: 0;
top: 1px;
font-size: 12px;
font-weight: normal;
}
&:hover .config-button {
display: block;
}
}
// Temporary HP
input.temphp {
width: 48%;
}
}
}
@ -87,10 +104,10 @@
height: 18px;
line-height: 16px;
margin: 4px 8px 2px;
.modesto();
font-size: 18px;
.russoOne(14px);
color: @colorOlive;
border-bottom: 1px solid @colorFaint;
white-space: nowrap;
}
/* ----------------------------------------- */
@ -124,7 +141,7 @@
align-items: center;
height: 28px;
line-height: 28px;
.modesto();
.russoOne();
> * {
font-weight: 400;
@ -143,6 +160,7 @@
font-family: "Signika", sans-serif;
font-size: 12px;
font-weight: 400;
white-space: nowrap;
}
}
}
@ -157,7 +175,7 @@
list-style: none;
margin: 0;
padding: 0;
.modesto();
.russoOne();
border: @borderGroove;
border-radius: 3px;
@ -237,6 +255,7 @@
li.skill {
height: 24px;
width: 225px;
padding: 3px 2px;
&:nth-child(even) {
@ -339,7 +358,7 @@
margin: 0 0 3px 0;
justify-content: space-between;
}
.configure-flags {
.config-button {
flex: 1;
}
@ -381,7 +400,8 @@
.tab.features,
.tab.inventory,
.tab.powerbook {
.tab.force-powerbook,
.tab.tech-powerbook {
overflow-y: hidden;
}
@ -414,49 +434,20 @@
}
}
// Inventory item lists
.inventory-list {
list-style: none;
margin: 0;
padding: 0 5px;
overflow-y: auto;
scrollbar-width: thin;
color: @colorTan;
// Inventory Item
.item {
line-height: 30px;
padding: 0 2px; // to align with the header border
border-bottom: 1px solid @colorFaint;
&:last-child { border-bottom: none; }
// Item Header Name
.item-name {
cursor: pointer;
max-height: 30px;
overflow: hidden;
.item-image {
flex: 0 0 30px;
background-size: 30px;
margin-right: 5px;
}
h4 {
margin: 0;
white-space: nowrap;
overflow-x: hidden;
}
&.rollable:hover .item-image {
background-image: url("../../icons/svg/d20-grey.svg") !important;
}
&.rollable .item-image:hover {
background-image: url("../../icons/svg/d20-black.svg") !important;
}
i.attuned {
color: @colorTan;
}
i.attuned { color: @colorTan; }
i.not-attuned { color: @colorCrimson; }
}
// Item uses
@ -475,77 +466,39 @@
flex: 0 0 80px;
text-align: right;
font-size: 11px;
color: @colorTan;
white-space: nowrap;
}
}
// Inventory Header
.inventory-header {
margin: 2px 0;
padding: 0;
align-items: center;
background: rgba(0, 0, 0, 0.05);
border: @borderGroove;
font-weight: bold;
line-height: 24px;
h3 {
margin: 0 -5px 0 0;
padding-left: 5px;
.modesto();
font-size: 16px;
}
.item-controls a.item-create {
flex: 0 0 100%;
}
}
// Item names
.item-name {
color: @colorDark;
}
// Item Detail Sections
.item-detail {
flex: 0 0 70px;
font-size: 12px;
color: @colorTan;
text-align: center;
border-right: 1px solid @colorFaint;
word-break: break-word;
white-space: nowrap;
overflow: hidden;
&:last-child { border-right: none; }
&.item-action {flex: 0 0 100px}
&.attunement {flex: 0 0 24px}
}
.item-weight {
flex: 0 0 60px;
border-left: 1px solid @colorFaint;
border-right: 1px solid @colorFaint;
}
.item-list {
list-style: none;
margin: 0;
padding: 0;
}
// Item Control Buttons
.item-controls {
flex: 0 0 44px;
.flexrow();
justify-content: flex-end;
a {
flex: 0 0 22px;
font-size: 12px;
text-align: center;
color: @colorTan;
}
}
// Item Dropdown Summary
@ -554,7 +507,17 @@
font-size: 12px;
line-height: 16px;
padding: 0.25em 0.5em;
color: @colorDark;
border-top: 1px solid @colorFaint;
h2 {
font-family: 'Russo One';
font-size: 20px;
font-weight: 400;
text-transform: uppercase;
letter-spacing: 0.5px;
border-bottom: 2px solid rgb(13, 153, 204);
color: #c40f0f;
}
}
}
@ -628,26 +591,23 @@
.powercasting-ability {
flex: 0 0 240px;
margin: 0;
input, span {
flex: 0 0 32px;
label, span {
flex: none;
}
input {
flex: 0 0 28px;
text-align: center;
}
select {
margin: 0 5px;
flex: 0 0 150px;
}
h3.power-dc {
flex: 1;
text-align: right;
flex: 0 0 120px;
}
}
.power-slots,
.power-comps {
flex: 0 0 75px;
padding-right: 5px;
text-align: right;
flex: none;
padding: 0 5px;
font-size: 12px;
color: @colorTan;
border-right: 1px solid @colorFaint;
@ -664,9 +624,10 @@
}
}
.power-uses {
padding-right: 8px;
text-align: right !important;
.powerbook .power-uses {
padding-right: 5px;
text-align: right;
color: @colorTan;
}
.power-school, .power-action, .power-target {
@ -695,41 +656,12 @@
.powerbook-empty .item-controls { flex: 1; }
/* ----------------------------------------- */
/* Active Effects */
/* Features Tab */
/* ----------------------------------------- */
.effects {
.effect-name{
flex: 2;
align-items: center;
color: @colorDark;
h4 { margin: 0; }
}
.effect-icon {
flex: 0 0 30px;
height: 30px;
margin-right: 5px;
border: none;
}
.effect-source,
.effect-duration {
text-align: center;
border-left: 1px solid @colorFaint;
border-right: 1px solid @colorFaint;
}
.effect-controls {
flex: 0 0 60px;
text-align: right;
}
.effect {
align-items: center;
border-bottom: 1px solid @colorFaint;
&:last-child { border-bottom: none; }
}
// Original class icon
.features i.original-class {
color: #4b4a44
}
/* ----------------------------------------- */
@ -740,3 +672,19 @@
padding: 0 8px;
}
}
#actor-flags {
.window-content {
overflow-y: hidden;
}
form {
height: 100%;
}
.form-body {
height: calc(100% - 40px);
padding-right: 8px;
margin-bottom: 4px;
overflow-y: auto;
scrollbar-width: thin;
}
}

View file

@ -10,9 +10,7 @@
.sw5e {
.window-content {
background: @sheetBackground;
font-size: 13px;
color: @colorDark;
}
/* ----------------------------------------- */
@ -44,6 +42,8 @@
select:disabled,
textarea:disabled {
color: @colorOlive;
border: 1px solid transparent !important;
outline: none !important;
&:hover,
&:focus {
box-shadow: none !important;
@ -58,28 +58,6 @@
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-group {
label {
@ -98,11 +76,12 @@
// Stacked Groups
.form-group.stacked {
label {
> label {
flex: 0 0 100%;
margin: 0;
}
label.checkbox {
label.checkbox,
label.radio {
flex: auto;
text-align: left;
}
@ -114,7 +93,7 @@
padding: 2px 0;
border-top: @borderGroove;
border-bottom: @borderGroove;
.modesto();
.russoOne();
}
/* Tags */
@ -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 */
/* ----------------------------------------- */
@ -178,11 +185,14 @@
background: transparent;
}
// Rollable Links
// Rollable Titles
.editable .rollable:hover {
cursor: pointer;
}
.editable h4.rollable:hover,
.editable .rollable:hover > h4 {
color: #000;
text-shadow: 0 0 10px red;
cursor: pointer;
}
// Separators
@ -216,7 +226,7 @@
border-bottom: @borderGroove;
.header-details {
.modesto();
.russoOne();
}
/* Character Name */
@ -278,7 +288,7 @@
.sheet-navigation {
flex: 0 0 @navHeight;
margin-bottom: 5px;
.modesto();
.russoOne(14px);
.item {
height: 30px;
@ -306,6 +316,7 @@
/* ----------------------------------------- */
.filter-list {
align-items: center;
list-style: none;
margin: 0;
padding: 0;
@ -362,6 +373,108 @@
padding: 0;
}
}
/* ----------------------------------------- */
/* Items Lists */
/* ----------------------------------------- */
.items-list {
list-style: none;
margin: 0;
padding: 0;
overflow-y: auto;
scrollbar-width: thin;
color: @colorTan;
// Child lists
.item-list {
list-style: none;
margin: 0;
padding: 0;
}
// Item Name
.item-name {
flex: 2;
margin: 0;
overflow: hidden;
font-size: 13px;
text-align: left;
align-items: center;
h3, h4 {
margin: 0;
white-space: nowrap;
overflow-x: hidden;
}
}
// Control Buttons
.item-controls {
flex: 0 0 60px;
justify-content: space-between;
a {
font-size: 12px;
text-align: center;
}
}
// Individual Item
.item {
align-items: center;
padding: 0 2px; // to align with the header border
border-bottom: 1px solid @colorFaint;
&:last-child { border-bottom: none; }
.item-name {
color: @colorDark;
.item-image {
flex: 0 0 30px;
height: 30px;
background-size: 30px;
border: none;
margin-right: 5px;
}
}
}
// Section Header
.items-header {
height: 28px;
margin: 2px 0;
padding: 0;
align-items: center;
background: rgba(0, 0, 0, 0.05);
border: @borderGroove;
font-weight: bold;
> * {
font-size: 12px;
text-align: center;
}
h3 {
padding-left: 5px;
//.modesto();
text-align: left;
font-size: 16px;
}
}
}
/* ----------------------------------------- */
/* Active Effects */
/* ----------------------------------------- */
.effects .item {
.effect-source,
.effect-duration,
.effect-controls {
text-align: center;
border-left: 1px solid @colorFaint;
border-right: 1px solid @colorFaint;
font-size: 12px;
}
.effect-controls {
border: none;
}
}
}
@ -369,7 +482,7 @@
/* Trait Selector
/* ----------------------------------------- */
#trait-selector {
.trait-selector {
.trait-list {
list-style: none;
margin: 0;
@ -380,4 +493,94 @@
height: 24px;
margin: 2px;
}
}
/* ----------------------------------------- */
/* 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
/* ----------------------------------------- */
.placeable-hud .control-icon {
box-sizing: content-box;
width: 40px;
flex: 0 0 40px;
margin: 8px 0;
font-size: 28px;
line-height: 40px;
text-align: center;
color: #FBF4F4;
background: rgba(0, 0, 0, 0.6);
box-shadow: 0 0 15px #000;
border: 1px solid #333;
border-radius: 8px;
pointer-events: all;
}
#token-hud .status-effects {
visibility: hidden;
position: absolute;
left: 50px;
top: 0;
display: grid;
padding: 3px;
box-sizing: content-box;
width: 100px;
color: #FBF4F4;
grid-template-columns: 25px 25px 25px 25px;
background: rgba(0, 0, 0, 0.6);
box-shadow: 0 0 15px #000;
border: 1px solid #333;
border-radius: 4px;
pointer-events: all;
}

View file

@ -4,7 +4,7 @@
/* Basic Structure */
/* ----------------------------------------- */
.sw5e.sheet.actor.character {
min-width: 720px;
min-width: 800px;
min-height: 680px;
/* ----------------------------------------- */
@ -89,7 +89,7 @@
// Custom Resources
.resource .attribute-value {
input {
> input {
flex: 0 0 25%;
}
label.recharge {
@ -99,6 +99,7 @@
font-size: 11px;
text-align: center;
color: @colorOlive;
align-items: center;
input[type="checkbox"] {
height: 14px;
width: 14px;
@ -160,7 +161,7 @@
padding: 0 3px 3px;
label {
flex: 0 0 20px;
.bungeeInline();
.russoOne();
font-size: 16px;
font-weight: normal;
line-height: 20px;
@ -172,4 +173,8 @@
resize: none;
}
}
.biography {
max-width: calc(100% - 180px);
}
}

View file

@ -4,7 +4,8 @@
/* Chat Cards
/* ----------------------------------------- */
.sw5e.chat-card {
.sw5e.chat-card,
.midi-qol-item-card {
font-style: normal;
font-size: 12px;
@ -22,7 +23,7 @@
flex: 1;
margin: 0;
line-height: 36px;
.bungeeInline();
.engli-Besh();
color: @colorOlive;
&:hover {
color: #111;
@ -72,7 +73,7 @@
span {
border-right: 2px groove #FFF;
padding: 0 5px 0 0;
padding: 0 3px 0 0;
font-size: 10px;
&:last-child {

524
less/original/items.less Normal file
View file

@ -0,0 +1,524 @@
@import "./variables.less";
.sw5e.sheet.item {
min-height: 660px;
min-width: 680px;
/* ----------------------------------------- */
/* Sheet Header */
/* ----------------------------------------- */
.sheet-header {
img.profile {
border: 2px solid #000;
}
h1 {
input {
font-size: 26px;
}
}
.header-details.flexrow {
h1 {
font-size: 26px;
}
.charname {
font-size: 26px;
}
}
.item-subtitle {
flex: 0 0 80px;
height: 60px;
margin: 0;
padding: 5px;
text-align: right;
color: @colorTan;
.item-type {
font-size: 20px;
line-height: 24px;
margin: 0;
}
.item-status {
font-size: 16px;
line-height: 24px;
}
.summary {
li {
font-size: 16px;
}
}
}
}
.sheet-navigation {
margin-bottom: 5px;
.item {
font-size: 16px;
}
}
.sheet-body {
overflow: hidden;
h1 {
font-family: 'Russo One';
font-size: 20px;
font-weight: 400;
text-transform: uppercase;
letter-spacing: 0.5px;
border-bottom: none;
color: #c40f0f;
}
h2 {
font-family: 'Russo One';
font-size: 18px;
font-weight: 400;
text-transform: uppercase;
letter-spacing: 0.5px;
border-bottom: 2px solid rgb(13, 153, 204);
color: #c40f0f;
}
h3 {
font-family: 'Russo One';
font-size: 16px;
font-weight: 400;
text-transform: uppercase;
letter-spacing: 0.5px;
border-bottom: none;
color: #c40f0f;
}
.smalltable {
table {
border-top: none;
border-bottom: none;
width: 200px;
}
td {
&:nth-child(odd) {
width: 50px;
margin: 0.5em 0.5em;
text-align: center;
}
&:nth-child(even) {
width: 150px;
margin: 0.5em 0.5em;
padding: 0 10px 0 10px;
text-align: left;
}
}
thead {
border-bottom: 0;
}
th {
color: #000000;
text-shadow: none;
border-bottom: 0;
background-color: #bdc8cc;
text-transform: none;
font-weight: bold;
font-family: 'Open Sans';
&:nth-child(odd) {
width: 50px;
margin: 0.5em 0.5em;
text-align: center;
}
&:nth-child(even) {
width: 150px;
margin: 0.5em 0.5em;
padding: 0 10px 0 10px;
text-align: left;
}
}
}
.medtable {
table {
width: 500px;
border: 0;
margin: 0.5em 0.5em;
}
td {
&:nth-child(odd) {
width: 50px;
margin: 0.5em 0.5em;
text-align: center;
}
&:nth-child(even) {
width: 450px;
margin: 0.5em 0.5em;
padding: 0 10px 0 0;
text-align: left;
}
}
thead {
border-bottom: 0;
}
th {
color: #000000;
text-shadow: none;
border-bottom: 0;
background-color: #bdc8cc;
text-transform: none;
font-weight: bold;
font-family: 'Open Sans';
&:nth-child(odd) {
text-align: center;
}
&:nth-child(even) {
text-align: left;
}
}
}
.classtable {
blockquote {
border-left: 0;
border-right: 0;
background-color: #bdc8cc;
width: 600px;
h3 {
color: #000000;
text-transform: uppercase;
font-family: 'Russo One';
font-size: 16px;
}
}
table {
width: 100%;
border-collapse: collapse;
background: rgba(0, 0, 0, 0.05);
border-left: 0;
border-right: 0;
border-top: 0;
border-bottom: 0;
margin: 0.5em 0;
font-style: normal;
text-shadow: none;
}
thead {
color: #000000;
text-shadow: none;
border-bottom: 0;
background-color: #bdc8cc;
text-transform: none;
font-style: normal;
font-family: 'Open Sans';
}
th {
color: #000000;
text-shadow: none;
border-bottom: 0;
background-color: #bdc8cc;
text-transform: none;
font-style: normal;
font-weight: bold;
font-family: 'Open Sans';
}
tbody {
text-align: center;
}
}
.speciestable {
blockquote {
width: 620px;
padding: 15px 10px;
margin: 15px;
line-height: 20px;
background-color: #bdc8cc;
border-top: 2px solid #0d99cc !important;
border-bottom: 2px solid #0d99cc !important;
border-left: 0 !important;
border-right: 0 !important;
-webkit-box-shadow: 0 0 1.5em rgba(13, 153, 204, .5) !important;
box-shadow: 0 0 1.5em rgba(13, 153, 204, .5) !important;
overflow-x: auto;
h3 {
color: #000000;
font-size: 22px;
border-bottom: none;
}
}
table {
background-color: #bdc8cc;
border-collapse: collapse;
width: 100%;
line-height: 18px;
margin-bottom: 15px;
border: 0;
border-bottom: none;
overflow-x: auto;
tbody {
tr {
&:nth-child(odd) {
background-color: #c9d6db;
}
&:nth-child(even) {
background-color: #bdc8cc;
}
}
}
td {
&:nth-child(1) {
padding-right: 5px;
width: 100px;
font-style: italic;
font-weight: 800;
}
}
h3 {
font-family: 'Russo One';
color: #000000;
font-size: 15px;
text-transform: uppercase;
}
thead {
font-style: normal;
font-size: 18px;
background-color: #bdc8cc;
text-shadow: none;
text-align: left;
line-height: 20px;
border-top: 5px solid #0d99cc;
border-bottom: 0;
}
}
}
.icon {
&:before {
display: inline-block;
font-style: normal;
font-variant: normal;
text-rendering: auto;
-webkit-font-smoothing: antialiased;
}
}
a.entity-link {
background: #DDD;
padding: 1px 4px;
border: 1px solid #4b4a44;
border-radius: 2px;
white-space: nowrap;
word-break: break-all;
i {
&::before {
content: url("ui/jedi-order.svg") !important;
display: inline-block;
position: relative;
top: 2px;
height: 15px;
width: 15px;
}
}
}
#species-description {
h2 {
font-family: 'Russo One';
font-size: 20px;
font-weight: 400;
letter-spacing: 0.5px;
border-bottom: 2px solid rgb(13, 153, 204);
color: #c40f0f;
}
}
#Traits {
h2 {
font-family: 'Russo One';
font-size: 20px;
font-weight: 400;
text-transform: uppercase;
letter-spacing: 0.5px;
border-bottom: 2px solid rgb(13, 153, 204);
color: #c40f0f;
}
}
.tab {
padding: 0 5px;
overflow: hidden auto;
}
.item-properties {
flex: 0 0 120px;
margin: 5px 5px 5px 0;
padding-right: 5px;
border-right: @borderGroove;
.form-group {
margin: 0;
label {
line-height: 20px;
}
input {
text-align: right;
}
}
.properties-list {
list-style: none;
margin: 0;
padding: 0;
li {
margin: 3px 0;
padding: 0 2px;
background: rgba(0, 0, 0, 0.05);
border: @borderGroove;
text-align: center;
font-size: 12px;
line-height: 18px;
}
}
}
}
/* ----------------------------------------- */
/* Item Details Form */
/* ----------------------------------------- */
.details {
// Item Sheet form fields
input[type="text"],
input[type="number"],
select {
height: 24px;
border: 1px solid @colorTan;
background: rgba(0, 0, 0, 0.05);
}
.form-group {
span {
text-align: center;
line-height: 24px;
}
}
.form-group.input-select {
select {
flex: 1.8;
}
}
.form-group.input-select-select {
select {
flex: 1.5;
}
}
.form-group.uses-per {
.form-fields {
flex-wrap: nowrap;
}
input {
flex: 0 0 32px;
}
span {
flex: 0 0 16px;
margin: 0 4px 0 0;
}
}
span.sep {
flex: 0 0 8px;
}
.prepared {
flex: 1.3 !important;
text-align: right;
padding-right: 10px;
}
// Power Materials
.power-materials {
flex: 0 0 100%;
margin: 0.25em 0;
justify-content: flex-end;
label {
flex: 0 0 64px;
text-align: right;
margin-right: 5px;
font-size: 12px;
line-height: 24px;
}
input[type="text"] {
flex: 0 0 48px;
margin-right: 10px;
}
}
}
/* ----------------------------------------- */
/* Item Actions */
/* ----------------------------------------- */
h4.damage-header {
margin: 0;
padding: 0;
font-weight: bold;
line-height: 24px;
color: @colorOlive;
}
.damage-parts {
list-style: none;
margin: 0;
padding: 0;
.damage-part {
flex: 0 0 100%;
padding: 0;
input {
flex: 3;
}
select {
margin-left: 5px;
flex: 1;
}
}
}
.damage-control {
width: 18px;
flex: 0 0 18px;
line-height: 24px;
float: right;
text-align: right;
color: @colorTan;
}
.recharge.form-group {
span {
text-align: right;
padding-right: 3px;
}
input[type="text"] {
flex: 0 0 32px;
text-align: center;
}
label.checkbox {
flex: none;
input {
width: 16px;
height: 16px;
top: 4px;
}
}
}
/* ----------------------------------------- */
/* Item Actions */
/* ----------------------------------------- */
.weapon-properties label.checkbox {
flex: 0 0 98px;
}
/* ----------------------------------------- */
/* Loot Sheet (No Tabs) */
/* ----------------------------------------- */
.loot-header {
margin-bottom: 10px;
}
}

View file

@ -4,7 +4,7 @@
/* Basic Structure */
/* ----------------------------------------- */
.sw5e.sheet.actor.npc {
min-width: 600px;
min-width: 872px;
min-height: 680px;
.header-exp {
@ -30,12 +30,28 @@
.summary {
font-size: 18px;
}
.powercasting-ability {
label {
flex: none;
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;
}
}
}
}

8
less/original/sw5e.less Normal file
View file

@ -0,0 +1,8 @@
@import "variables.less";
@import "apps.less";
@import "actors.less";
@import "items.less";
@import "chat.less";
@import "character.less";
@import "npc.less";
@import "vehicle.less";

View file

@ -14,20 +14,20 @@
font-weight: 400;
src: url('./fonts/RussoOne.ttf');
}
.russoOne {
.russoOne(@font-size: 20px) {
font-family: 'Russo One';
font-size: 20px;
font-size: @font-size;
font-weight: 400;
}
/* bungee-inline-regular - latin */
/* engli-besh */
@font-face {
font-family: 'Bungee Inline';
font-family: 'Engli-Besh';
font-style: normal;
font-weight: 400;
src: url('./fonts/BungeeInline.ttf');
src: url('./fonts/EngliBesh-KG3W.ttf');
}
.bungeeInline {
font-family: 'Bungee Inline';
.engli-Besh {
font-family: 'Engli-Besh';
font-size: 20px;
font-weight: 400;
}
@ -55,7 +55,23 @@
@colorOlive: #4b4a44;
@colorCrimson: #44191A;
@borderGroove: 2px groove #eeede0;
@sheetBackground: url("ui/parchment.jpg") repeat;
//@sheetBackground: url("ui/parchment.jpg") repeat;
//SW5e Colors
@colorBlack: #1C1C1C;
@colorDarkGray: #363636;
@colorGray: #4f4f4f;
@colorLightGray: #828282;
@colorPaleGray: #D6D6D6;
@colorRed: #c40f0f;
@colorPaleRed: #FBF4F4;
@colorLightRed: #F6E1E1;
@colorBlue: #0d99cc;
@colorLightBlue: #7ed6f7;
@colorPaleBlue: #afc6d6;
@sheetBackground: linear-gradient(90deg, @colorPaleBlue 0%, @colorPaleGray 30%, @colorPaleGray 70%, @colorPaleBlue); //url("ui/parchment.webp") repeat;
/* ----------------------------------------- */
/* Flexbox */

View file

@ -1,38 +0,0 @@
@import "variables.less";
@import "apps.less";
@import "actors.less";
@import "items.less";
@import "chat.less";
@import "character.less";
@import "npc.less";
@import "vehicle.less";
// TODO: Remove number styling after 0.7.x
input[type="number"] {
width: calc(100% - 2px);
min-width: 20px;
height: 26px;
background: rgba(0, 0, 0, 0.05);
padding: 1px 3px;
margin: 0;
color: #191813;
font-family: inherit;
font-size: inherit;
text-align: inherit;
line-height: inherit;
border: 1px solid #7a7971;
border-radius: 3px;
-webkit-user-select: text;
-moz-user-select: text;
-ms-user-select: text;
user-select: text;
-moz-appearance: textfield;
&:focus {
box-shadow: 0 0 5px red;
}
}
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
-webkit-appearance: none;
}

View file

@ -0,0 +1,146 @@
//override Primary Red
@colorRed: #E81111;
@colorDarkBg: #2b2b2b;
//Background
@primaryBackground: linear-gradient(90deg,#626262 0,#4d4d4d 30%,#4d4d4d 70%,#626262);
//Typography
@headingColor: @colorRed;
@headerBorderColor: @colorBlue;
@bodyFontColor: white;
@linkColor: @colorRed;
@linkSecondaryColor: @colorPaleGray;
@blockquoteBackground: @colorPaleRed;
@blockquoteBorder: @colorRed;
@blockquoteShadow: 0 0 20px rgba(@colorRed, 0.8);
//forms
@inputBackgroundColor: @colorDarkGray;
@inputBorderNormal: @colorLightGray;
@inputBorderHover: @colorGray;
@inputBorderFocus: @colorRed;
@inputTextColor: white;
@buttonBackground: @colorRed;
@buttonTextColor: white;
@buttonHoverBackground: lighten(@colorRed, 5);
@buttonSecondaryBackground: @colorLightGray;
@buttonSecondaryTextColor: white;
@buttonSecondaryHoverBackground: lighten(@colorLightGray, 5);
//other bits
@hrColor: @colorBlue;
@tableTextColor: white;
@tableHeaderTextColor: @colorPaleGray;
@tableBackground: @colorGray;
@tableRowHoverBackground: lighten(@colorLightGray, 10);
@tableRowBorderColor: @colorLightGray;
//universalColors
@windowHeaderBackground: @colorDarkBg;
@windowHeaderLinkColor: @colorRed;
//Sidebar
@sidebarTabBackground: @windowHeaderBackground;
@sidebarTabLinkColor: @windowHeaderLinkColor;
@sidebarTabLinkUnderline: @colorRed;
@chatBackground: @colorDarkGray;
@chatHeaderColor: @colorRed;
@chatHeaderBottomBorderColor: @colorBlue;
@chatNotificationColor: @colorBlue;
@cardButtonBorder: @colorLightGray;
@cardFooterBorder: @colorLightBlue;
@cardFooterSeparator: @colorPaleGray;
@diceFormulaBackground: @colorGray;
@diceFormualColor: white;
@diceTotalBackground: @colorPaleRed;
@diceTotalBorder: @colorRed;
@diceTotalShadow: @colorRed;
@diceSuccessColor: @colorGreen;
@diceFailureColor: @colorRed;
@diceCriticalBackground: @colorPaleGreen;
@diceCriticalColor: @colorGreen;
@diceFumbleBackground: @colorPaleRed;
@diceFumbleColor: @colorRed;
@altRowBackground: @colorGray;
@combatRoundColor: @colorRed;
@combatRoundBorder: @colorBlue;
@combatCombatantControlColor: @colorPaleGray;
@combatCombatantControlColorActive: @colorRed;
@combatActiveCombatantColor: @colorBlue;
@combatTokenResourceColor: white;
@combatTokenResouceBorder: @colorLightGray;
@combatControlsBorder: @colorBlue;
@folderSearchIconColor: @colorBlue;
@folderSubdirectoryBackground: @colorDarkBg;
@folderSubdirectoryBorder: @colorLightGray;
@directoryListItemBorder: @colorBlue;
@folderHeaderBackground: @colorDarkBg;
@folderHeaderColor: white;
@folderIconColor: @colorBlue;
@entityBackgroundColor: @colorDarkBg;
@entityNameColor: @colorBlack;
@sceneBorderColor: @colorBlue;
@sceneBackgroundColor: @colorDarkBg;
@playlistBackgroundColor: @colorDarkBg;
@playlistHeaderBorder: @colorBlue;
@playlistSoundColor: @colorBlack;
@compendiumEntityBackground: @colorDarkBg;
@compendiumStatusIcon: @colorLightGray;
@foundryNavBgColor: rgba(@colorLightBlue, 0.4);
@foundryNavTextColor: white;
@foundryNavBorderColor: @colorBlue;
@foundryNavBgColorGM: @colorBlue;
@foundryNavBorderColorGM: @colorPaleBlue;
@foundryNavSceneLinkColor: white;
@foundryNavActiveBgColor: rgba(@colorRed, 0.6);
@foundryNavActiveBorderColor: lighten(@colorRed, 20);
@foundryNavActiveGlow: darken(@colorRed, 20);
@foundryNavContextShadow: darken(@colorBlue, 20);
@foundryNavContextBorderColor: @colorBlue;
@foundryPlayersArrowColor: @colorLightGray;
@actorPanelBgColor: white;
@actorNameColor: @colorRed;
@actorXPBarBorder: @colorGray;
@actorXPBarBackground: @colorPaleBlue;
@actorXPBarColor: @colorBlue;
@actorProficiencyTextColor: @colorGray;
@actorAttributeInputColor: @colorGray;
@actorSeparatorColor: @colorLightGray;
@actorAttributeButtonBorder: @colorPaleGray;
@actorAttributeButtonBorderHover: @colorRed;
@actorNavigationTabsColor: @colorGray;
@actorNavigationTabsActiveColor: @colorRed;
@actorNavigationTabsHoverBgColor: rgba(@colorGray, 0.1);
@actorNavigationTabsActiveHoverBgColor: rgba(@colorRed, 0.1);
@actorFilterBorderColor: @colorLightGray;
@actorFilterHoverColor: @colorRed;
@actorFilterActiveColor: @colorRed;
@actorGroupListHeaderBgColor: lighten(@colorPaleGray, 10);
@actorGroupListTitleBorderColor: @colorBlue;
@actorGroupListColumnBorderColor: @colorPaleGray;
@actorGroupListAltRowColor: lighten(@colorPaleGray, 10);
@actorItemRollableD20Color: @colorGray;
@actorItemRollableD20HoverColor: @colorRed;
@actorItemControlToggleColor: @colorLightGray;
@actorAbilityScoreColor: @colorGray;
@actorAbilityBorderColor: @colorPaleGray;
@actorSkillsAltRowColor: lighten(@colorPaleGray, 10);
@actorEncumbranceLabelBackground: @colorPaleGray;
@actorEncumbranceTextColor: @colorBlack;
@actorEncumbranceBorderColor: @colorBlack;
@actorEncumbranceBarBgColor: @colorPaleBlue;
@actorEncumbranceBarColor: @colorBlue;

View file

@ -0,0 +1,143 @@
//Background
@primaryBackground: linear-gradient(90deg,#afc6d6 0,#d6d6d6 30%,#d6d6d6 70%,#afc6d6);// linear-gradient(90deg, @colorPaleBlue 0%, @colorPaleGray 30%, @colorPaleGray 70%, @colorPaleBlue);
//Typography
@headingColor: @colorRed;
@headerBorderColor: @colorBlue;
@bodyFontColor: @colorBlack;
@linkColor: @colorRed;
@linkSecondaryColor: @colorGray;
@blockquoteBackground: @colorPaleBlue;
@blockquoteBorder: @colorBlue;
@blockquoteShadow: 0 0 20px rgba(@colorBlue, 0.8);
//forms
@inputBackgroundColor: white;
@inputBorderNormal: @colorLightGray;
@inputBorderHover: @colorGray;
@inputBorderFocus: @colorRed;
@inputTextColor: @colorBlack;
@buttonBackground: @colorRed;
@buttonTextColor: white;
@buttonHoverBackground: lighten(@colorRed, 5);
@buttonSecondaryBackground: @colorPaleGray;
@buttonSecondaryTextColor: @colorBlack;
@buttonSecondaryHoverBackground: lighten(@colorPaleGray, 5);
//other bits
@hrColor: @colorBlue;
@tableTextColor: @colorBlack;
@tableHeaderTextColor: @colorLightGray;
@tableBackground: white;
@tableRowHoverBackground: lighten(@colorPaleGray, 10);
@tableRowBorderColor: @colorPaleGray;
//universalColors
@windowHeaderBackground: white;
@windowHeaderLinkColor: @colorRed;
//Sidebar
@sidebarTabBackground: @windowHeaderBackground;
@sidebarTabLinkColor: @windowHeaderLinkColor;
@sidebarTabLinkUnderline: @colorRed;
@chatBackground: white;
@chatHeaderColor: @colorRed;
@chatHeaderBottomBorderColor: @colorBlue;
@chatNotificationColor: @colorBlue;
@cardButtonBorder: @colorLightGray;
@cardFooterBorder: @colorLightBlue;
@cardFooterSeparator: @colorPaleGray;
@diceFormulaBackground: @colorPaleGray;
@diceFormualColor: @colorBlack;
@diceTotalBackground: @colorPaleBlue;
@diceTotalBorder: @colorBlue;
@diceTotalShadow: @colorBlue;
@diceSuccessColor: @colorGreen;
@diceFailureColor: @colorRed;
@diceCriticalBackground: @colorPaleGreen;
@diceCriticalColor: @colorGreen;
@diceFumbleBackground: @colorPaleRed;
@diceFumbleColor: @colorRed;
@altRowBackground: @colorPaleBlue;
@combatRoundColor: @colorRed;
@combatRoundBorder: @colorBlue;
@combatCombatantControlColor: @colorLightGray;
@combatCombatantControlColorActive: @colorDarkGray;
@combatActiveCombatantColor: @colorBlue;
@combatTokenResourceColor: @colorGray;
@combatTokenResouceBorder: @colorLightGray;
@combatControlsBorder: @colorBlue;
@folderSearchIconColor: @colorBlue;
@folderSubdirectoryBackground: white;
@folderSubdirectoryBorder: @colorBlack;
@directoryListItemBorder: @colorBlue;
@folderHeaderBackground: white;
@folderHeaderColor: @colorBlack;
@folderIconColor: @colorBlue;
@entityBackgroundColor: white;
@entityNameColor: @colorBlack;
@sceneBorderColor: @colorBlue;
@sceneBackgroundColor: white;
@playlistBackgroundColor: white;
@playlistHeaderBorder: @colorBlue;
@playlistSoundColor: @colorBlack;
@compendiumEntityBackground: white;
@compendiumStatusIcon: @colorLightGray;
@foundryNavBgColor: rgba(@colorLightBlue, 0.4);
@foundryNavTextColor: white;
@foundryNavBorderColor: @colorBlue;
@foundryNavBgColorGM: @colorBlue;
@foundryNavBorderColorGM: @colorPaleBlue;
@foundryNavSceneLinkColor: white;
@foundryNavActiveBgColor: rgba(@colorRed, 0.6);
@foundryNavActiveBorderColor: lighten(@colorRed, 20);
@foundryNavActiveGlow: darken(@colorRed, 20);
@foundryNavContextShadow: darken(@colorBlue, 20);
@foundryNavContextBorderColor: @colorBlue;
@foundryPlayersArrowColor: @colorLightGray;
@actorPanelBgColor: white;
@actorNameColor: @colorRed;
@actorXPBarBorder: @colorGray;
@actorXPBarBackground: @colorPaleBlue;
@actorXPBarColor: @colorBlue;
@actorProficiencyTextColor: @colorGray;
@actorAttributeInputColor: @colorGray;
@actorSeparatorColor: @colorLightGray;
@actorAttributeButtonBorder: @colorPaleGray;
@actorAttributeButtonBorderHover: @colorRed;
@actorNavigationTabsColor: @colorGray;
@actorNavigationTabsActiveColor: @colorRed;
@actorNavigationTabsHoverBgColor: rgba(@colorGray, 0.1);
@actorNavigationTabsActiveHoverBgColor: rgba(@colorRed, 0.1);
@actorFilterBorderColor: @colorLightGray;
@actorFilterHoverColor: @colorRed;
@actorFilterActiveColor: @colorRed;
@actorGroupListHeaderBgColor: lighten(@colorPaleGray, 10);
@actorGroupListTitleBorderColor: @colorBlue;
@actorGroupListColumnBorderColor: @colorPaleGray;
@actorGroupListAltRowColor: lighten(@colorPaleGray, 10);
@actorItemRollableD20Color: @colorGray;
@actorItemRollableD20HoverColor: @colorRed;
@actorItemControlToggleColor: @colorLightGray;
@actorAbilityScoreColor: @colorGray;
@actorAbilityBorderColor: @colorPaleGray;
@actorSkillsAltRowColor: lighten(@colorPaleGray, 10);
@actorEncumbranceLabelBackground: @colorPaleGray;
@actorEncumbranceTextColor: @colorBlack;
@actorEncumbranceBorderColor: @colorBlack;
@actorEncumbranceBarBgColor: @colorPaleBlue;
@actorEncumbranceBarColor: @colorBlue;

View file

@ -0,0 +1,67 @@
/* ----------------------------------------- */
/* Fonts */
/* ----------------------------------------- */
.russoOne(@size: 20px) {
font-family: 'Russo One';
font-size: @size;
font-weight: 400;
letter-spacing: 0.5px;
}
.openSans(@size: 13px, @weight: 400) {
font-family: 'Open Sans';
font-size: @size;
font-weight: @weight;
}
.fontAwesome() {
font-family: "Font Awesome 5 Free";
-webkit-font-smoothing: antialiased;
display: inline-block;
font-style: normal;
font-variant: normal;
text-rendering: auto;
font-weight: 900;
}
/* ----------------------------------------- */
/* Sheet Styles */
/* ----------------------------------------- */
@colorDark: #191813;
@colorFaint: #c9c7b8;
@colorBeige: #b5b3a4;
@colorTan: #7a7971;
@colorOlive: #4b4a44;
@colorCrimson: #44191A;
@borderGroove: 2px groove #eeede0;
//@sheetBackground: url("ui/parchment.jpg") repeat;
//SW5e Colors
@colorBlack: #1C1C1C;
@colorDarkGray: #363636;
@colorGray: #4f4f4f;
@colorLightGray: #828282;
@colorPaleGray: #D6D6D6;
@colorRed: #c40f0f;
@colorPaleRed: #FBF4F4;
@colorLightRed: #F6E1E1;
@colorBlue: #0d99cc;
@colorLightBlue: #7ed6f7;
@colorPaleBlue: #afc6d6;
@colorGreen: #0dce0d;
@colorPaleGreen: #bcdcbe;
@sheetBackground: linear-gradient(90deg, @colorPaleBlue 0%, @colorPaleGray 30%, @colorPaleGray 70%, @colorPaleBlue);
.dropShadow1(){
box-shadow: 0 2px 2px 0 rgba(0,0,0,0.14), 0 3px 1px -2px rgba(0,0,0,0.12), 0 1px 5px 0 rgba(0,0,0,0.2);
}
.dropShadow2() {
box-shadow: 0 4px 5px 0 rgba(0,0,0,0.14), 0 1px 10px 0 rgba(0,0,0,0.12), 0 2px 4px -1px rgba(0,0,0,0.3);
}
.dropShadow3() {
box-shadow: 0 8px 17px 2px rgba(0,0,0,0.14), 0 3px 14px 2px rgba(0,0,0,0.12), 0 5px 5px -3px rgba(0,0,0,0.2);
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,420 @@
.panel {
background: @actorPanelBgColor;
}
.sw5e.sheet .window-content {
color: @colorBlack;
background: linear-gradient(90deg,#afc6d6 0,#d6d6d6 30%,#d6d6d6 70%,#afc6d6);
input,
select {
color: @colorBlack;
&:hover {
border-color: @inputBorderHover;
}
&:focus {
border-color: @inputBorderFocus;
}
}
button {
&:hover,
&:focus {
border-color: @inputBorderFocus;
}
}
}
.sw5e.sheet.actor {
color: @colorBlack;
input, select, textarea {
&:hover {
border-color: @inputBorderFocus;
}
&:focus {
border-color: @inputBorderFocus;
}
}
.swalt-sheet {
section>h1 {
border-bottom: 2px solid @colorBlue;
}
header {
h1.character-name {
color: @actorNameColor;
input[type="text"] {
color: @actorNameColor;
}
}
.level-experience {
.xpbar {
border: 1px solid @actorXPBarBorder;
background-color: @actorXPBarBackground;
.bar {
background-color: @actorXPBarColor;
}
}
}
.summary {
input,
.proficiency {
color: @actorProficiencyTextColor;
}
}
.attributes {
.attribute-value,
.attribute-value input {
color: @actorAttributeInputColor;
}
.attribute-value {
.value-separator {
color: @actorSeparatorColor;
}
}
footer {
button {
border: 1px solid @actorAttributeButtonBorder;
&:hover {
color: @actorAttributeButtonBorderHover;
}
}
&.hit-points,
&.hit-dice,
&.initiative {
button {
border: 1px solid @actorAttributeButtonBorder;
color: @colorRed;
&:hover {
border-color: @actorAttributeButtonBorderHover;
}
}
}
}
}
}
nav.sheet-navigation {
.item {
color: @actorNavigationTabsColor;
&.active {
color: @actorNavigationTabsActiveColor;
border-bottom-color: @actorNavigationTabsActiveColor;
&:hover {
background: @actorNavigationTabsHoverBgColor;
}
}
&:hover {
background: @actorNavigationTabsHoverBgColor;
}
}
}
.tab {
.filter-list {
.filter-item {
border-bottom: 2px solid @actorFilterBorderColor;
&:hover {
color: @actorFilterHoverColor;
}
&.active {
color: @actorFilterActiveColor;
border-bottom-color: @actorFilterActiveColor;
}
}
}
.group-list-header {
background: @actorGroupListHeaderBgColor;
}
.group-list-title {
border-bottom: 1px solid @actorGroupListTitleBorderColor;
}
.group-list-header,
.group-list {
.item-detail {
border-left: 1px solid @actorGroupListColumnBorderColor;
}
}
.group-list,
.group-list ol {
li.item {
&:nth-child(even) {
background-color: @actorGroupListAltRowColor;
}
h4 {
color: @colorBlack;
}
.item-name {
.item-image {
&::before {
color: @actorItemRollableD20Color;
}
}
&.rollable:hover {
.item-image {
&:hover {
&::before {
color: @actorItemRollableD20HoverColor;
}
}
}
}
}
.item-control {
&:hover {
color: @linkColor !important;
}
&.item-toggle {
color: @actorItemControlToggleColor;
&.active {
color: @colorBlack;
}
}
}
}
}
}
.tab.attributes {
.abilities {
.scores {
li {
border: 1px solid @actorAbilityBorderColor;
h2 {
&:hover {
color: @linkColor;
}
}
.ability-score {
color: @actorAbilityScoreColor;
}
.ability-modifiers {
.ability-mod,
.ability-save {
border-color: @actorAbilityBorderColor;
}
}
}
}
.skills {
li {
&:nth-child(even) {
background-color: @actorSkillsAltRowColor;
}
.proficiency-toggle {
color: @colorBlack;
}
.skill-name {
&:hover {
color: @linkColor;
}
}
}
}
}
.traits-resources {
nav {
button {
color: @actorNavigationTabsColor;
&.active {
color: @actorNavigationTabsActiveColor;
border-bottom-color: @actorNavigationTabsActiveColor;
&:hover {
background: @actorNavigationTabsActiveHoverBgColor;
}
}
&:hover {
background: @actorNavigationTabsHoverBgColor;
}
}
}
section.traits {
.trait-selector {
i.fas {
color: @linkColor;
}
}
.languages {
label {
&:hover {
color: @linkColor;
}
}
}
}
section.resources {
.resource-items {
.resource {
h1 {
input {
color: @headingColor;
border-bottom: 2px solid @headerBorderColor;
}
}
.attribute-value,
.attribute-value input {
color: @actorAttributeInputColor;
}
.attribute-value {
.value-separator {
color: @actorSeparatorColor;
}
}
}
}
.counters {
.counter {
h4 {
&.rollable {
&:hover {
color: @linkColor;
}
}
}
.death-success {
i {
color: @colorGreen;
}
}
.death-fail {
i {
color: @colorRed;
}
}
}
}
}
}
}
.tab.inventory {
.currency {
color: @headingColor;
}
.encumbrance-wrapper {
.encumbrance-label {
background: @actorEncumbranceLabelBackground;
color: @actorEncumbranceTextColor;
border: 1px solid @actorEncumbranceBorderColor;
}
.encumbrance {
background: @actorEncumbranceBarBgColor;
.encumbrance-bar {
background: @actorEncumbranceBarColor;
}
}
}
}
.tab.force-powerbook,
.tab.tech-powerbook {
.powercasting-ability {
label,
h3 {
color: @headingColor;
span {
color: @colorBlack;
}
}
}
}
.tab.notes {
section {
&>input {
color: @headingColor;
border-bottom: 2px solid @headerBorderColor;
}
}
}
}
&.npc {
.swalt-sheet {
header {
div.creature-type:hover {
border-color: @inputBorderFocus;
}
.experience {
color: @actorProficiencyTextColor;
}
}
}
}
}

View file

@ -0,0 +1,105 @@
input[type="text"], input[type="number"], input[type="password"], input[type="date"], input[type="time"], select, textarea, .roundTransition {
border-radius: 4px;
transition: all 0.3s;
&:hover {
box-shadow: none;
}
&:focus {
box-shadow: none;
}
}
input[type=range] {
-webkit-appearance: none; /* Hides the slider so that custom slider can be made */
width: 100%; /* Specific width is required for Firefox. */
background: transparent; /* Otherwise white in Chrome */
}
input[type=range]::-webkit-slider-thumb{
-webkit-appearance: none;
background: @colorRed;
width: 12px;
height: 12px;
border-radius: 32px;
cursor: pointer;
box-shadow: none;
}
input[type=range]::-moz-range-thumb{
-webkit-appearance: none;
background: @colorRed;
width: 12px;
height: 12px;
border-radius: 32px;
cursor: pointer;
box-shadow: none;
}
input[type=range]::-ms-thumb {
-webkit-appearance: none;
background: @colorRed;
width: 12px;
height: 12px;
border-radius: 32px;
cursor: pointer;
box-shadow: none;
}
input[type=range]::-webkit-slider-runnable-track {
width: 100%;
height: 6px;
cursor: pointer;
background: @colorLightBlue;
border-radius: 4px;
border: 1px solid @colorBlue;
box-shadow: none;
}
input[type=range]:focus::-webkit-slider-runnable-track {
background: @colorBlue;
}
input[type=range]::-moz-range-track {
width: 100%;
height: 6px;
cursor: pointer;
background: @colorLightBlue;
border-radius: 4px;
border: 1px solid @colorBlue;
box-shadow: none;
}
input[type=range]::-ms-track {
width: 100%;
height: 6px;
cursor: pointer;
background: @colorLightBlue;
border-radius: 4px;
border: 1px solid @colorBlue;
box-shadow: none;
}
input[type=range]:focus {
outline: none; /* Removes the blue border. You should probably do some kind of focus styling for accessibility reasons though. */
}
input[type=range]::-ms-track {
width: 100%;
cursor: pointer;
/* Hides the slider so custom styles can be added */
background: transparent;
border-color: transparent;
color: transparent;
}
button, input[type="button"], input[type="submit"], input[type="reset"] {
.openSans(13px, 700);
text-align: center;
border: none;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s;
&:hover, &:focus {
box-shadow: none;
}
&:disabled {
opacity: 0.6;
cursor: default;
}
}

View file

@ -0,0 +1,52 @@
input[type="text"], input[type="number"], input[type="password"], input[type="date"], input[type="time"], select, textarea {
border: 1px solid @inputBorderNormal;
&:hover {
border-color: @inputBorderHover;
}
&:focus {
border-color: @inputBorderFocus;
}
&::placeholder {
color: @inputTextColor;
opacity: 0.5;
}
::-ms-input-placeholder { /* Microsoft Edge */
color: @inputTextColor;
opacity: 0.5;
}
}
button, input[type="button"], input[type="submit"], input[type="reset"] {
background: @buttonBackground;
color: @buttonTextColor;
&:hover, &:focus {
background: @buttonHoverBackground;
}
&:disabled {
&:hover, &:focus {
background: @buttonBackground;
}
}
}
input[type="reset"], button.secondary, button[type="reset"], input[type="button"].secondary, input[type="submit"].secondary {
background: @buttonSecondaryBackground;
color: @buttonSecondaryTextColor;
&:hover {
background: @buttonSecondaryHoverBackground;
}
&:disabled {
&:hover, &:focus {
background: @buttonSecondaryBackground;
}
}
}
form {
button {
border: none;
}
.notes, .hint {
color: rgba(@bodyFontColor, 0.8);
}
}

View file

@ -0,0 +1,76 @@
.window-app {
border-radius: 4px;
border: none;
.dropShadow2();
& > header {
background: @windowHeaderBackground;
border-radius: 4px 4px 0 0;
border: none;
.dropShadow1();
margin-bottom: 4px;
}
.window-content {
background: @primaryBackground;
color: @bodyFontColor;
footer {
margin-top: 8px;
}
}
&.minimized {
& > header, & > .window-header {
border: none;
border-radius: 4px;
margin: 0;
}
}
}
#client-settings {
nav.tabs {
border: none;
font-size: 17px;
line-height: 1.6;
a.item {
border-bottom: 3px solid transparent;
color: @bodyFontColor;
&:hover {
text-decoration: none;
}
&.active {
text-shadow: none;
border-bottom-color: @sidebarTabLinkUnderline;
}
}
}
section.content {
border: none;
margin-top: 4px;
}
}
.dialog-buttons {
margin-top: 8px;
button:last-child {
margin-right: 0;
}
button:not(.default) {
border: 1px solid @buttonBackground;
margin-right: 4px;
background: @buttonSecondaryBackground;
color: @buttonSecondaryTextColor;
&:hover {
background: @buttonSecondaryHoverBackground;
}
}
button.normal.default {
border: none;
background: @buttonBackground;
color: @buttonTextColor;
&:hover {
background: @buttonHoverBackground;
}
}
}

View file

@ -0,0 +1,84 @@
#navigation {
#nav-toggle {
background: @foundryNavBgColor;
color: @foundryNavTextColor;
transform: rotate(-90deg);
}
.nav-item {
border: 1px solid @foundryNavBorderColor;
}
#scene-list {
.scene {
border: 1px solid @foundryNavBorderColor;
background: rgba(@foundryNavBgColor, 0.4);
a {
color: @foundryNavSceneLinkColor;
}
&.gm {
border: 1px solid @foundryNavBorderColorGM;
background: rgba(@foundryNavBgColorGM, 0.4);
}
&.view, &.context {
box-shadow: 0 0 8px @foundryNavContextShadow;
border-color: @foundryNavContextBorderColor;
}
&.active {
border-color: @foundryNavActiveBorderColor;
background: @foundryNavActiveBgColor;
box-shadow: 0 0 8px @foundryNavActiveGlow;
}
}
}
}
#controls {
.scene-control, .control-tool {
background: @foundryNavBgColor;
color: @foundryNavTextColor;
border: 1px solid @foundryNavBorderColor;
box-shadow: none;
&:hover {
background: @foundryNavBgColor;
box-shadow: 0 0 8px @foundryNavContextShadow;
}
&.active {
border-color: @foundryNavActiveBorderColor;
background: @foundryNavActiveBgColor;
box-shadow: 0 0 8px @foundryNavActiveGlow;
}
}
}
#players {
border: none;
border-radius: 4px;
h3 {
background: @sidebarTabBackground;
border: none;
margin: 0;
padding: 0 8px;
font-size: 17px;
line-height: 30px;
.dropShadow1();
border-radius: 4px 4px 0 0;
.players-mode {
color: @foundryPlayersArrowColor;
}
}
ol {
margin: 4px 0;
.player-name.self {
color: inherit;
font-weight: 700;
}
.player {
color: @bodyFontColor;
}
.player-active {
margin-top: 7px;
&.active {
box-shadow: none;
}
}
}
}

View file

@ -0,0 +1,388 @@
#sidebar {
border: none; //1px solid @colorBlue;
&.collapsed {
#sidebar-tabs {
min-height: 370px;
justify-content: center;
& > .item.active {
border: none;
}
}
}
}
#sidebar-tabs {
border: none;
box-shadow: none;
justify-content: space-between;
.dropShadow1();
.item {
font-size: 16px;
}
.item.active {
border: none;
box-shadow: none;
background: none;
text-shadow: none;
}
}
/*-----------
** Chat Tab
-----------*/
#chat-log {
.chat-message {
border: none;
border-radius: 4px;
margin-bottom: 8px;
.dropShadow1();
& > header {
color: @colorRed;
border-bottom: 2px solid @colorBlue;
margin-bottom: 4px;
span {
color: @colorBlack;
}
}
}
}
.notification-pip {
color: @colorBlue;
}
.sw5e.chat-card,
.midi-qol-item-card {
.card-header {
padding: 0;
border: none;
img {
flex: 0 0 36px;
margin-right: 4px;
}
h3 {
flex: 1;
margin: 0;
line-height: 36px;
.russoOne(17px);
border-bottom: none;
&:hover {
text-shadow: none;
}
}
}
.card-content {
margin: 4px 0;
h3 {
font-size: 12px;
margin: 0;
font-weight: bold;
}
> * {
-webkit-user-select: text;
-moz-user-select: text;
-ms-user-select: text;
user-select: text;
}
}
.card-buttons {
margin: 4px 0;
span {
display: block;
line-height: 28px;
text-align: center;
}
button {
.openSans(13px, 700);
padding: 4px 0;
height: auto;
line-height: 1.6;
margin: 4px 0;
border: none;
border-radius: 4px;
&:hover, &:focus {
box-shadow: none;
}
}
}
.card-footer {
padding: 4px 0 0;
span {
padding: 0 4px 0 0;
font-size: 10px;
&:last-child {
border-right: none;
padding-right: 0;
}
}
}
}
.dice-roll {
.dice-formula {
border: none;
box-shadow: none;
border-radius: 4px;
}
.dice-total {
border-radius: 0;
padding: 4px 0;
}
}
#chat-controls {
padding-top: 4px;
}
#chat-form textarea {
&:focus {
box-shadow: none;
outline: none;
}
}
/*-----------
** Combat Tab
-----------*/
#combat {
h3 {
border: none;
}
#combat-tracker {
li.combatant {
padding: 4px 0;
background: none;
.token-name {
text-shadow: none;
}
.ce-image-wrapper {
.token-image {
width: auto;
height: auto;
}
}
h4 {
color: @colorBlack;
}
.roll {
background: none;
&::before {
content: "\f6cf";
.fontAwesome();
font-size: 28px;
}
}
.initiative {
text-shadow: none;
}
&.hidden {
opacity: 0.5;
}
}
}
#combat-controls {
padding-top: 0;
}
}
/*
** Folders
*/
.sidebar-tab {
.directory-header {
margin-bottom: 4px;
.header-search {
position: relative;
i.fa-search {
position: absolute;
left: 8px;
}
input {
text-align: left;
padding-left: 22px;
&:focus {
box-shadow: none;
}
}
}
}
.subdirectory {
border: none;
margin-left: 8px;
min-height: 8px;
}
.directory-list {
padding-bottom: 4px;
.folder {
& > .folder-header {
line-height: initial;
padding: 0 0 0 8px;
position: relative;
border: none;
h3 {
padding: 8px 4px;
.openSans(13px, 700);
line-height: 1.6;
& > i {
margin-right: 4px;
}
}
a {
position: absolute;
top: 0;
right: 4px;
height: 100%;
padding: 0 4px;
i {
margin-top: 12px;
}
&.create-folder {
right: 28px;
}
}
}
}
.directory-item img {
flex: 0 0 32px;
height: 32px;
width: 32px;
align-self: center;
}
.actor, .item, .journal, .table {
border: none;
.entity-name {
.openSans(13px, 700);
}
}
}
}
#scenes {
.subdirectory {
border-left: none;
}
.scene {
border: none;
box-shadow: none;
position: relative;
height: 128px;
& + .scene {
margin-top: 4px;
}
&::after {
content: '';
display: block;
width: 100%;
height: 99px;
position: absolute;
top: 28px;
left: 0;
}
h3 {
.openSans(13px, 700);
text-align: left;
text-shadow: none;
padding: 4px 4px 4px 12px;
line-height: 1.6;
position: absolute;
top: 0;
left: 0;
width: 100%;
}
}
}
#playlists {
.directory-list {
padding: 0 8px;
li.playlist {
padding: 8px;
border-radius: 4px;
margin-bottom: 8px;
border-top: inherit;
.dropShadow1();
.playlist-header {
text-decoration: none;
}
li.sound {
border: none;
h4 {
.openSans(13px, 400);
}
}
}
}
}
#compendium {
.compendium-entity {
margin: 0 4px;
padding: 8px;
.dropShadow1();
border-radius: 4px;
border: none;
&+ .compendium-entity {
margin-top: 4px;
}
h3 {
background: none;
border: none;
.russoOne(17px);
padding: 0;
margin-bottom: 4px;
}
ol.compendium-list {
li.compendium-pack {
margin: 0;
padding: 4px;
border: none;
.pack-title {
margin: 0;
position: relative;
a {
.openSans(13px, 700);
i {
display: none;
}
}
.status-icons {
top: 4px;
font-size: 13px;
}
}
}
}
}
}
#settings {
h2 {
border: none;
margin: 0 8px;
padding: 0;
background: none;
}
#game-details, #settings-game, #settings-documentation, #settings-access {
padding: 0 8px;
margin: 0 0 8px;
}
}

View file

@ -0,0 +1,298 @@
#sidebar-tabs {
background: @sidebarTabBackground;
& > .collapse {
color: @sidebarTabLinkColor;
}
.item.active {
color: @sidebarTabLinkColor;
border-bottom: 3px solid @sidebarTabLinkUnderline;
}
}
/*-----------
** Chat Tab
-----------*/
#chat-log {
.chat-message {
background: @chatBackground;
color: @bodyFontColor;
& > header {
color: @chatHeaderColor;
border-bottom: 2px solid @chatHeaderBottomBorderColor;
span {
color: @bodyFontColor;
}
}
}
}
.notification-pip {
color: @chatNotificationColor;
}
.sw5e.chat-card,
.midi-qol-item-card {
.card-header {
h3 {
color: @bodyFontColor;
&:hover {
color: @bodyFontColor;
}
}
}
.card-buttons {
span {
border: 1px solid @cardButtonBorder;
}
}
.card-footer {
border-top: 1px solid @cardFooterBorder;
span {
border-right: 1px solid @cardFooterSeparator;
&:last-child {
border-right: none;
}
}
}
}
.dice-roll {
.dice-formula {
background: @diceFormulaBackground;
color: @diceFormualColor;
box-shadow: none;
border-radius: 4px;
}
.dice-total {
background: @diceTotalBackground;
border: 1px solid @diceTotalBorder;
box-shadow: 0 0 12px rgba(@diceTotalShadow,.8);
&.success {
color: @diceSuccessColor;
}
&.failure {
color: @diceFailureColor;
}
&.critical {
color: @diceCriticalColor;
background: @diceCriticalBackground;
box-shadow: 0 0 12px rgba(@diceCriticalColor,.5);
}
&.fumble {
color: @diceFumbleColor;
background: @diceFumbleBackground;
box-shadow: 0 0 12px rgba(@diceFumbleColor,.5);
}
}
}
#chat-controls {
.roll-type-select {
background: #a9a9a9;
color: #1C1C1C;
}
label {
color: @bodyFontColor;
}
}
#chat-form textarea {
background: #a9a9a9;
color: #1C1C1C;
}
/*-----------
** Combat Tab
-----------*/
#combat {
#combat-round {
color: @combatRoundColor;
border-bottom: 2px solid @combatRoundColor;
.encounters {
h4 {
color: @combatRoundColor;
}
a {
color: @linkSecondaryColor;
&:hover {
color: @linkColor;
}
}
}
}
#combat-tracker {
//padding-top: 4px;
li.combatant {
color: @bodyFontColor;
&:nth-child(even) {
background: rgba(@altRowBackground, 0.5);
}
h4 {
color: @bodyFontColor
}
.roll {
color: @linkSecondaryColor;
&:hover {
color: @linkColor;
}
}
.combatant-control {
color: @combatCombatantControlColor;
&.active {
color: @combatCombatantControlColorActive;
}
}
.token-resource {
color: @combatTokenResourceColor;
border-right: 1px solid @combatTokenResouceBorder;
}
&.active {
color: @combatActiveCombatantColor;
.initiative, h4 {
color: @combatActiveCombatantColor;
}
}
&.hidden {
color: @bodyFontColor;
}
}
}
#combat-controls {
border-top: 1px solid @combatControlsBorder;
}
}
/*
** Folders
*/
.sidebar-tab {
.directory-header {
.header-search {
i.fa-search {
color: @folderSearchIconColor;
}
input {
background: @inputBackgroundColor;
}
}
}
.subdirectory {
background: @folderSubdirectoryBackground;
.folder {
border-left: 2px solid rgba(@folderSubdirectoryBorder, 0.4);
}
}
.directory-list {
li + li {
border-top: 1px solid @directoryListItemBorder;
}
.folder {
& > .folder-header {
background: @folderHeaderBackground;
h3 {
background: @folderHeaderBackground;
color: @folderHeaderColor;
& > i {
color: @folderIconColor;
}
}
a {
color: @linkSecondaryColor;
&:hover {
color: @linkColor;
}
}
}
&.collapsed > .folder-header {
background: @folderHeaderBackground;
}
& + .entity {
border-top: 1px solid @directoryListItemBorder;
}
}
.actor, .item, .journal, .table {
background: @entityBackgroundColor;
.entity-name {
color: @entityNameColor;
}
&:nth-child(even) {
background: rgba(@altRowBackground, 0.3);
}
}
}
}
#scenes {
.scene {
border-top: 1px solid @sceneBorderColor;
border-left: 4px solid @sceneBorderColor;
&::after {
box-shadow: 0 0 20px @sceneBorderColor inset;
}
h3 {
background: @sceneBackgroundColor;
}
}
}
#playlists {
.directory-list {
li.playlist {
background: @playlistBackgroundColor;
.playlist-header {
background: @playlistBackgroundColor;
color: @colorRed;
border-bottom: 2px solid @playlistHeaderBorder;
}
li.sound {
color: @playlistSoundColor;
}
a.sound-control {
color: @linkColor;
}
}
}
}
#compendium {
.compendium-entity {
background: @compendiumEntityBackground !important;
h3 {
border-bottom: 2px solid @headerBorderColor;
}
ol.compendium-list {
li.compendium-pack {
&:nth-child(even) {
background: rgba(@altRowBackground, 0.3);
}
.pack-title {
.status-icons {
color: @compendiumStatusIcon;
}
}
footer.compendium-footer {
color: @bodyFontColor;
}
}
}
}
}
#settings {
h2 {
color: @headingColor;
border-bottom: 2px solid @headerBorderColor;
}
#game-details, #settings-game, #settings-documentation, #settings-access {
color: @bodyFontColor;
}
}

View file

@ -0,0 +1,500 @@
#sidebar {
border: none; //1px solid @colorBlue;
}
#sidebar-tabs {
border: none;
box-shadow: none;
background: white;
.dropShadow1();
& > .collapse {
color: @colorRed;
}
.item {
font-size: 16px;
}
.item.active {
color: @colorRed;
border: none;
border-bottom: 3px solid @colorRed;
box-shadow: none;
background: none;
text-shadow: none;
}
}
/*-----------
** Chat Tab
-----------*/
#chat-log {
.chat-message {
background: white;
border: none;
border-radius: 4px;
margin-bottom: 8px;
.dropShadow1();
& > header {
color: @colorRed;
border-bottom: 2px solid @colorBlue;
margin-bottom: 4px;
span {
color: @colorBlack;
}
}
}
}
.notification-pip {
color: @colorBlue;
text-shadow: none;
}
.sw5e.chat-card,
.midi-qol-item-card {
font-size: 13px;
.card-header {
padding: 0;
border: none;
img {
flex: 0 0 36px;
margin-right: 4px;
}
h3 {
flex: 1;
margin: 0;
line-height: 36px;
.russoOne(17px);
color: @colorBlack;
&:hover {
color: @colorBlack;
text-shadow: none;
}
}
}
.card-content {
margin: 4px 0;
h3 {
font-size: 12px;
margin: 0;
font-weight: bold;
}
> * {
-webkit-user-select: text;
-moz-user-select: text;
-ms-user-select: text;
user-select: text;
}
}
.card-buttons {
margin: 4px 0;
span {
display: block;
line-height: 28px;
text-align: center;
border: 1px solid @colorLightGray;
}
button {
.openSans(13px, 700);
padding: 4px 0;
height: auto;
line-height: 1.6;
margin: 4px 0;
background: @colorRed;
border: none;
border-radius: 4px;
&:hover, &:focus {
background-color: lighten(@colorRed, 5);
box-shadow: none;
}
}
}
.card-footer {
padding: 4px 0 0;
border-top: 1px solid @colorLightBlue;
span {
border-right: 2px groove #FFF;
padding: 0 4px 0 0;
font-size: 10px;
&:last-child {
border-right: none;
padding-right: 0;
}
}
}
}
.dice-roll {
.dice-formula {
background: none;
border: none;
}
.dice-total {
background: @colorPaleBlue;
border: 1px solid @colorBlue;
border-radius: 0;
padding: 4px 0;
box-shadow: 0 0 12px rgba(@colorBlue,.5);
&.success {
color: inherit;
background: #c7d0c0;
border: 1px solid #006c00;
}
&.failure {
color: inherit;
background: #ffdddd;
border: 1px solid #6e0000;
}
&.critical {
color: @colorGreen;
background: @colorPaleGreen;
box-shadow: 0 0 12px rgba(@colorGreen,.5);
}
&.fumble {
color: red;
}
}
}
#chat-controls {
&.roll-type-select {
background: #4f4f4f;
color: #FFFFFF;
}
padding-top: 4px;
label {
color: @colorBlack;
}
}
#chat-form textarea {
background: #4f4f4f;
&:focus {
box-shadow: none;
outline: none;
}
}
/*-----------
** Combat Tab
-----------*/
#combat {
#combat-round {
color: @colorRed;
border-bottom: 2px solid @colorBlue;
.encounters {
h4 {
color: @colorRed;
}
a {
color: @colorGray;
&:hover {
color: @colorRed;
}
}
}
}
#combat-tracker {
//padding-top: 4px;
li.combatant {
padding: 4px 0;
color: @colorBlack;
background: none;
&:nth-child(even) {
background: rgba(@colorPaleBlue, 0.5);
}
h4 {
color: @colorBlack;
text-shadow: none;
}
.roll {
background: none;
color: @colorGray;
&::before {
content: "\f6cf";
.fontAwesome();
font-size: 28px;
}
&:hover {
color: @colorRed;
}
}
.combatant-control {
color: @colorLightGray;
text-shadow: none;
&.active {
color: @colorDarkGray;
}
}
.token-resource {
color: @colorGray;
border-right: 1px solid @colorLightGray;
}
.initiative {
text-shadow: none;
}
&.active {
color: @colorBlue;
.initiative, h4 {
color: @colorBlue;
}
}
&.hidden {
opacity: 0.5;
color: @colorBlack;
}
}
}
#combat-controls {
padding-top: 0;
border-top: 1px solid @colorBlue;
}
}
/*
** Folders
*/
.sidebar-tab {
.directory-header {
margin-bottom: 4px;
.header-search {
position: relative;
i.fa-search {
position: absolute;
left: 8px;
color: @colorBlue;
}
input {
text-align: left;
padding-left: 22px;
background: white;
&:focus {
box-shadow: none;
}
}
}
}
.subdirectory {
border: none;
margin-left: 8px;
background: white;
min-height: 8px;
.folder {
border-left: 2px solid rgba(@colorBlack, 0.4);
}
}
.directory-list {
padding-bottom: 4px;
li + li {
border-top: 1px solid @colorBlue;
}
.folder {
& > .folder-header {
line-height: initial;
padding: 0 0 0 8px;
position: relative;
border: none;
background: white;
h3 {
padding: 8px 4px;
background: white;
color: @colorBlack;
.openSans(13px, 700);
line-height: 1.6;
& > i {
margin-right: 4px;
color: @colorBlue;
}
}
a {
position: absolute;
top: 0;
right: 4px;
height: 100%;
padding: 0 4px;
color: @colorLightGray;
&:hover {
color: @colorRed;
}
i {
margin-top: 12px;
}
&.create-folder {
right: 28px;
}
}
}
&.collapsed > .folder-header {
background: white;
}
& + .entity {
border-top: 1px solid @colorBlue;
}
}
.directory-item img {
flex: 0 0 32px;
height: 32px;
width: 32px;
align-self: center;
}
.actor, .item, .journal, .table {
background: white;
border: none;
.entity-name {
.openSans(13px, 700);
color: @colorBlack;
}
&:nth-child(even) {
background: rgba(@colorPaleBlue, 0.3);
}
}
}
}
#scenes {
.subdirectory {
border-left: none;
}
.scene {
border: none;
border-top: 1px solid @colorBlue;
border-left: 4px solid @colorBlue;
box-shadow: none;
position: relative;
height: 128px;
//margin-bottom: 4px;
& + .scene {
margin-top: 4px;
}
&::after {
content: '';
display: block;
width: 100%;
height: 99px;
position: absolute;
top: 28px;
left: 0;
box-shadow: 0 0 20px @colorBlue inset;
}
h3 {
.openSans(13px, 700);
text-align: left;
text-shadow: none;
padding: 4px 4px 4px 12px;
background: white;
line-height: 1.6;
position: absolute;
top: 0;
left: 0;
width: 100%;
}
}
}
#playlists {
.directory-list {
padding: 0 8px;
li.playlist {
padding: 8px;
border-radius: 4px;
background: white;
margin-bottom: 8px;
border-top: inherit;
.dropShadow1();
.playlist-header {
background: white;
color: @colorRed;
text-decoration: none;
border-bottom: 2px solid @colorBlue;
}
li.sound {
border: none;
color: @colorBlack;
h4 {
.openSans(13px, 400);
}
}
a.sound-control {
color: @colorRed;
}
}
}
}
#compendium {
.compendium-entity {
margin: 0 4px;
padding: 8px;
background: white !important;
.dropShadow1();
border-radius: 4px;
border: none;
&+ .compendium-entity {
margin-top: 4px;
}
h3 {
border: none;
color: @colorRed;
border-bottom: 2px solid @colorBlue;
.russoOne(17px);
padding: 0;
margin-bottom: 4px;
}
ol.compendium-list {
li.compendium-pack {
margin: 0;
padding: 4px;
border: none;
&:nth-child(even) {
background: rgba(@colorPaleBlue, 0.3);
}
.pack-title {
margin: 0;
position: relative;
a {
.openSans(13px, 700);
i {
display: none;
}
}
.status-icons {
top: 4px;
color: @colorLightGray;
font-size: 13px;
}
}
footer.compendium-footer {
color: @colorBlack;
}
}
}
}
}
#settings {
h2 {
color: @colorRed;
border: none;
border-bottom: 2px solid @colorBlue;
margin: 0 8px;
padding: 0;
}
#game-details, #settings-game, #settings-documentation, #settings-access {
padding: 0 8px;
margin: 0 0 8px;
color: @colorBlack;
}
}

View file

@ -0,0 +1,57 @@
@import "_variables.less";
@import "_variables-dark.less";
body.dark-theme {
.app {
background: @primaryBackground;
}
h1,
h2,
h3,
h4,
h5,
h6 {
color: @headingColor;
}
h3 {
border-bottom: 2px solid @headerBorderColor;
}
a {
color: @linkColor;
text-decoration: none;
&:hover,
&:active {
text-shadow: none;
text-decoration: underline;
}
}
blockquote {
padding: 4px 8px;
background-color: @blockquoteBackground;
border: 1px solid @blockquoteBorder;
box-shadow: @blockquoteShadow;
}
hr {
border-width: 0 0 1px 0;
border-bottom: 1px solid @hrColor;
}
select {
color: white;
background-color: rgba(0, 0, 0, 0.5);
}
input[type="text"], input[type="number"], input[type="password"], input[type="date"], input[type="time"], select, textarea {
color: @inputTextColor;
}
@import "components/forms-themes.less";
@import "components/sidebar-themes.less";
@import "components/foundry-nav-themes.less";
@import "components/foundry-app-window-themes.less";
@import "components/actor-themes.less";
}

View file

@ -0,0 +1,194 @@
/* open-sans-regular - latin */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
src: url('./fonts/OpenSans-Regular.ttf');
}
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400;
src: url('./fonts/OpenSans-Italic.ttf');
}
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 700;
src: url('./fonts/OpenSans-Bold.ttf');
}
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 700;
src: url('./fonts/OpenSans-BoldItalic.ttf');
}
/* russo-one-regular - latin */
@font-face {
font-family: 'Russo One';
font-style: normal;
font-weight: 400;
src: url('./fonts/RussoOne.ttf');
}
@font-face {
font-family: 'Russo One';
font-style: italic;
font-weight: 400;
src: url('./fonts/RussoOne.ttf');
}
@font-face {
font-family: 'Russo One';
font-style: normal;
font-weight: 700;
src: url('./fonts/RussoOne.ttf');
}
@font-face {
font-family: 'Aurebesh';
font-style: normal;
font-weight: 400;
src: url('./fonts/Aurebesh.ttf');
}
@font-face {
font-family: 'Engli-Besh';
font-style: normal;
font-weight: 400;
src: url('./fonts/EngliBesh-KG3W.ttf');
}
@import "_variables.less";
html {
box-sizing: border-box;
}
*, *:before, *:after {
box-sizing: inherit;
}
// ::-webkit-scrollbar {
// width: 6px;
// height: 6px;
// }
::-webkit-scrollbar-track {
border: 1px solid @colorBlue;
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
outline: none;
border-radius: 4px;
background: @colorBlue;
border: none;
}
:root {
scrollbar-width: thin;
scrollbar-color: @colorBlue @colorPaleBlue;
}
body {
.openSans(13px, 400);
background-image: url('./ui/SW5e-logo.svg');
background-repeat: no-repeat;
background-size: cover;
}
h1 {
.russoOne(34px);
}
h2 {
.russoOne(27px);
}
h3 {
.russoOne(21px);
}
h4 {
.russoOne(17px);
}
h5, h6 {
.russoOne(13px);
}
a {
text-decoration: none;
&:hover, &:active {
text-shadow: none;
text-decoration: underline;
}
}
.app {
border: none;// 1px solid @colorBlue;
.dropShadow1();
}
#pause {
img {display: none;}
background: none;
height: 128px;
&::before {
content: '';
position: absolute;
top: 0;
margin-left: -64px;
left: 50%;
width: 128px;
height: 128px;
background: url("ui/pause-inner.svg") no-repeat 50% 50%;
animation-name: pause-spin;
animation-duration: 10000ms;
animation-iteration-count: infinite;
animation-timing-function: linear;
}
&::after {
content: '';
position: absolute;
top: 0;
margin-left: -64px;
left: 50%;
width: 128px;
height: 128px;
background: url("ui/pause-outer.svg") no-repeat 50% 50%;
animation-name: pause-spin;
animation-duration: 5000ms;
animation-iteration-count: infinite;
animation-timing-function: linear;
animation-direction: reverse;
}
h3 {
border-bottom: 0;
line-height: 1;
position: absolute;
top: 50%;
left: 50%;
width: 256px;
margin-left: -128px;
margin-top: -13px;
text-shadow: 0 0 24px @colorBlue;
&::before, &::after {
position: absolute;
font-family: "Aurebesh", sans-serif;
font-size: 13px;
color: @colorGray;
animation: none;
opacity: 0.8;
text-shadow: 0 0 8px @colorBlue;
}
&::before {
content: "GAME";
top: -13px;
left: 42px;
}
&::after {
content: "PAUSED";
bottom: -13px;
right: 42px;
}
}
}
@import "components/forms-global.less";
@import "components/sidebar-global.less";
@import "components/actor-global.less";
@keyframes pause-spin {
from {
transform:rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

View file

@ -0,0 +1,47 @@
@import "_variables.less";
@import "_variables-light.less";
body.light-theme {
.app {
background: @primaryBackground;
}
h1,
h2,
h3,
h4,
h5,
h6 {
color: @headingColor;
}
h3 {
border-bottom: 2px solid @headerBorderColor;
}
a {
color: @linkColor;
text-decoration: none;
&:hover,
&:active {
text-shadow: none;
text-decoration: underline;
}
}
blockquote {
padding: 4px 8px;
background-color: @blockquoteBackground;
border: 1px solid @blockquoteBorder;
box-shadow: @blockquoteShadow;
}
hr {
border-width: 0 0 1px 0;
border-bottom: 1px solid @hrColor;
}
@import "components/forms-themes.less";
@import "components/sidebar-themes.less";
@import "components/foundry-nav-themes.less";
@import "components/foundry-app-window-themes.less";
@import "components/actor-themes.less";
}

View file

@ -0,0 +1,61 @@
@import "variables.less";
a {
color: @colorRed;
text-decoration: none;
&:hover, &:active {
text-shadow: none;
text-decoration: underline;
}
}
.app {
background: @sheetBackground;
border: none;// 1px solid @colorBlue;
.dropShadow1();
}
#context-menu {
background: none;
border: none;
border-radius: 0;
color: @colorBlack;
padding: 0 8px;
ol.context-items {
background: white;
border-radius: 4px;
border: 1px solid @colorLightGray;
.dropShadow2();
li.context-item {
&:first-child {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
&:last-child {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
}
i {
color: @colorBlue;
}
&:hover {
background: @colorRed;
color: white;
text-shadow: none;
cursor: pointer;
i {
color: white;
}
}
& + li {
border-top: 1px solid @colorPaleGray;
}
}
}
}
@import "components/forms.less";
@import "components/sidebar.less";

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

View file

@ -1,929 +0,0 @@
import Item5e from "../../item/entity.js";
import TraitSelector from "../../apps/trait-selector.js";
import ActorSheetFlags from "../../apps/actor-flags.js";
import {SW5E} from '../../config.js';
/**
* Extend the basic ActorSheet class to do all the SW5e things!
* This sheet is an Abstract layer which is not used.
* @extends {ActorSheet}
*/
export default class ActorSheet5e extends ActorSheet {
constructor(...args) {
super(...args);
/**
* Track the set of item filters which are applied
* @type {Set}
*/
this._filters = {
inventory: new Set(),
powerbook: new Set(),
features: new Set(),
effects: new Set()
};
}
/* -------------------------------------------- */
/** @override */
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
scrollY: [
".inventory .inventory-list",
".features .inventory-list",
".powerbook .inventory-list",
".effects .inventory-list"
],
tabs: [{navSelector: ".tabs", contentSelector: ".sheet-body", initial: "description"}]
});
}
/* -------------------------------------------- */
/** @override */
get template() {
if ( !game.user.isGM && this.actor.limited ) return "systems/sw5e/templates/actors/limited-sheet.html";
return `systems/sw5e/templates/actors/${this.actor.data.type}-sheet.html`;
}
/* -------------------------------------------- */
/** @override */
getData() {
// Basic data
let isOwner = this.entity.owner;
const data = {
owner: isOwner,
limited: this.entity.limited,
options: this.options,
editable: this.isEditable,
cssClass: isOwner ? "editable" : "locked",
isCharacter: this.entity.data.type === "character",
isNPC: this.entity.data.type === "npc",
isVehicle: this.entity.data.type === 'vehicle',
config: CONFIG.SW5E,
};
// The Actor and its Items
data.actor = duplicate(this.actor.data);
data.items = this.actor.items.map(i => {
i.data.labels = i.labels;
return i.data;
});
data.items.sort((a, b) => (a.sort || 0) - (b.sort || 0));
data.data = data.actor.data;
data.labels = this.actor.labels || {};
data.filters = this._filters;
// Ability Scores
for ( let [a, abl] of Object.entries(data.actor.data.abilities)) {
abl.icon = this._getProficiencyIcon(abl.proficient);
abl.hover = CONFIG.SW5E.proficiencyLevels[abl.proficient];
abl.label = CONFIG.SW5E.abilities[a];
}
// Skills
if (data.actor.data.skills) {
for ( let [s, skl] of Object.entries(data.actor.data.skills)) {
skl.ability = CONFIG.SW5E.abilityAbbreviations[skl.ability];
skl.icon = this._getProficiencyIcon(skl.value);
skl.hover = CONFIG.SW5E.proficiencyLevels[skl.value];
skl.label = CONFIG.SW5E.skills[s];
}
}
// Update traits
this._prepareTraits(data.actor.data.traits);
// Prepare owned items
this._prepareItems(data);
// Prepare active effects
// TODO Disabled until 0.7.5 release
// this._prepareEffects(data);
// Return data to the sheet
return data
}
/* -------------------------------------------- */
/**
* Prepare the data structure for traits data like languages, resistances & vulnerabilities, and proficiencies
* @param {object} traits The raw traits data object from the actor data
* @private
*/
_prepareTraits(traits) {
const map = {
"dr": CONFIG.SW5E.damageResistanceTypes,
"di": CONFIG.SW5E.damageResistanceTypes,
"dv": CONFIG.SW5E.damageResistanceTypes,
"ci": CONFIG.SW5E.conditionTypes,
"languages": CONFIG.SW5E.languages,
"armorProf": CONFIG.SW5E.armorProficiencies,
"weaponProf": CONFIG.SW5E.weaponProficiencies,
"toolProf": CONFIG.SW5E.toolProficiencies
};
for ( let [t, choices] of Object.entries(map) ) {
const trait = traits[t];
if ( !trait ) continue;
let values = [];
if ( trait.value ) {
values = trait.value instanceof Array ? trait.value : [trait.value];
}
trait.selected = values.reduce((obj, t) => {
obj[t] = choices[t];
return obj;
}, {});
// Add custom entry
if ( trait.custom ) {
trait.custom.split(";").forEach((c, i) => trait.selected[`custom${i+1}`] = c.trim());
}
trait.cssClass = !isObjectEmpty(trait.selected) ? "" : "inactive";
}
}
/* -------------------------------------------- */
/**
* Prepare the data structure for Active Effects which are currently applied to the Actor.
* @param {object} data The object of rendering data which is being prepared
* @private
*/
_prepareEffects(data) {
// Define effect header categories
const categories = {
temporary: {
label: "Temporary Effects",
effects: []
},
passive: {
label: "Passive Effects",
effects: []
},
inactive: {
label: "Inactive Effects",
effects: []
}
};
// Iterate over active effects, classifying them into categories
for ( let e of this.actor.effects ) {
e._getSourceName(); // Trigger a lookup for the source name
if ( e.data.disabled ) categories.inactive.effects.push(e);
else if ( e.isTemporary ) categories.temporary.effects.push(e);
else categories.inactive.push(e);
}
// Add the prepared categories of effects to the rendering data
return data.effects = categories;
}
/* -------------------------------------------- */
/**
* Insert a power into the powerbook object when rendering the character sheet
* @param {Object} data The Actor data being prepared
* @param {Array} powers The power data being prepared
* @private
*/
_preparePowerbook(data, powers) {
const owner = this.actor.owner;
const levels = data.data.powers;
const powerbook = {};
// Define some mappings
const sections = {
"atwill": -20,
"innate": -10,
"pact": 0.5
};
// Label power slot uses headers
const useLabels = {
"-20": "-",
"-10": "-",
"0": "&infin;"
};
// Format a powerbook entry for a certain indexed level
const registerSection = (sl, i, label, {prepMode="prepared", value, max, override}={}) => {
powerbook[i] = {
order: i,
label: label,
usesSlots: i > 0,
canCreate: owner,
canPrepare: (data.actor.type === "character") && (i >= 1),
powers: [],
uses: useLabels[i] || value || 0,
slots: useLabels[i] || max || 0,
override: override || 0,
dataset: {"type": "power", "level": prepMode in sections ? 1 : i, "preparation.mode": prepMode},
prop: sl
};
};
// Determine the maximum power level which has a slot
const maxLevel = Array.fromRange(10).reduce((max, i) => {
if ( i === 0 ) return max;
const level = levels[`power${i}`];
if ( (level.max || level.override ) && ( i > max ) ) max = i;
return max;
}, 0);
// Level-based powercasters have cantrips and leveled slots
if ( maxLevel > 0 ) {
registerSection("power0", 0, CONFIG.SW5E.powerLevels[0]);
for (let lvl = 1; lvl <= maxLevel; lvl++) {
const sl = `power${lvl}`;
registerSection(sl, lvl, CONFIG.SW5E.powerLevels[lvl], levels[sl]);
}
}
// Pact magic users have cantrips and a pact magic section
if ( levels.pact && levels.pact.max ) {
if ( !powerbook["0"] ) registerSection("power0", 0, CONFIG.SW5E.powerLevels[0]);
const l = levels.pact;
const config = CONFIG.SW5E.powerPreparationModes.pact;
registerSection("pact", sections.pact, config, {
prepMode: "pact",
value: l.value,
max: l.max,
override: l.override
});
}
// Iterate over every power item, adding powers to the powerbook by section
powers.forEach(power => {
const mode = power.data.preparation.mode || "prepared";
let s = power.data.level || 0;
const sl = `power${s}`;
// Specialized powercasting modes (if they exist)
if ( mode in sections ) {
s = sections[mode];
if ( !powerbook[s] ){
const l = levels[mode] || {};
const config = CONFIG.SW5E.powerPreparationModes[mode];
registerSection(mode, s, config, {
prepMode: mode,
value: l.value,
max: l.max,
override: l.override
});
}
}
// Sections for higher-level powers which the caster "should not" have, but power items exist for
else if ( !powerbook[s] ) {
registerSection(sl, s, CONFIG.SW5E.powerLevels[s], {levels: levels[sl]});
}
// Add the power to the relevant heading
powerbook[s].powers.push(power);
});
// Sort the powerbook by section level
const sorted = Object.values(powerbook);
sorted.sort((a, b) => a.order - b.order);
return sorted;
}
/* -------------------------------------------- */
/**
* Determine whether an Owned Item will be shown based on the current set of filters
* @return {boolean}
* @private
*/
_filterItems(items, filters) {
return items.filter(item => {
const data = item.data;
// Action usage
for ( let f of ["action", "bonus", "reaction"] ) {
if ( filters.has(f) ) {
if ((data.activation && (data.activation.type !== f))) return false;
}
}
// Power-specific filters
if ( filters.has("ritual") ) {
if (data.components.ritual !== true) return false;
}
if ( filters.has("concentration") ) {
if (data.components.concentration !== true) return false;
}
if ( filters.has("prepared") ) {
if ( data.level === 0 || ["innate", "always"].includes(data.preparation.mode) ) return true;
if ( this.actor.data.type === "npc" ) return true;
return data.preparation.prepared;
}
// Equipment-specific filters
if ( filters.has("equipped") ) {
if ( data.equipped !== true ) return false;
}
return true;
});
}
/* -------------------------------------------- */
/**
* Get the font-awesome icon used to display a certain level of skill proficiency
* @private
*/
_getProficiencyIcon(level) {
const icons = {
0: '<i class="far fa-circle"></i>',
0.5: '<i class="fas fa-adjust"></i>',
1: '<i class="fas fa-check"></i>',
2: '<i class="fas fa-check-double"></i>'
};
return icons[level];
}
/* -------------------------------------------- */
/* 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) {
// Activate Item Filters
const filterLists = html.find(".filter-list");
filterLists.each(this._initializeFilterItemList.bind(this));
filterLists.on("click", ".filter-item", this._onToggleFilter.bind(this));
// Item summaries
html.find('.item .item-name h4').click(event => this._onItemSummary(event));
// Editable Only Listeners
if ( this.isEditable ) {
// Input focus and update
const inputs = html.find("input");
inputs.focus(ev => ev.currentTarget.select());
inputs.addBack().find('[data-dtype="Number"]').change(this._onChangeInputDelta.bind(this));
// Ability Proficiency
html.find('.ability-proficiency').click(this._onToggleAbilityProficiency.bind(this));
// Toggle Skill Proficiency
html.find('.skill-proficiency').on("click contextmenu", this._onCycleSkillProficiency.bind(this));
// Trait Selector
html.find('.trait-selector').click(this._onTraitSelector.bind(this));
// Configure Special Flags
html.find('.configure-flags').click(this._onConfigureFlags.bind(this));
// Owned Item management
html.find('.item-create').click(this._onItemCreate.bind(this));
html.find('.item-edit').click(this._onItemEdit.bind(this));
html.find('.item-delete').click(this._onItemDelete.bind(this));
html.find('.item-uses input').click(ev => ev.target.select()).change(this._onUsesChange.bind(this));
html.find('.slot-max-override').click(this._onPowerSlotOverride.bind(this));
// Active Effect management
html.find(".effect-control").click(this._onManageActiveEffect.bind(this));
}
// Owner Only Listeners
if ( this.actor.owner ) {
// Ability Checks
html.find('.ability-name').click(this._onRollAbilityTest.bind(this));
// Roll Skill Checks
html.find('.skill-name').click(this._onRollSkillCheck.bind(this));
// Item Rolling
html.find('.item .item-image').click(event => this._onItemRoll(event));
html.find('.item .item-recharge').click(event => this._onItemRecharge(event));
}
// Otherwise remove rollable classes
else {
html.find(".rollable").each((i, el) => el.classList.remove("rollable"));
}
// Handle default listeners last so system listeners are triggered first
super.activateListeners(html);
}
/* -------------------------------------------- */
/**
* Iinitialize Item list filters by activating the set of filters which are currently applied
* @private
*/
_initializeFilterItemList(i, ul) {
const set = this._filters[ul.dataset.filter];
const filters = ul.querySelectorAll(".filter-item");
for ( let li of filters ) {
if ( set.has(li.dataset.filter) ) li.classList.add("active");
}
}
/* -------------------------------------------- */
/* Event Listeners and Handlers */
/* -------------------------------------------- */
/**
* Handle input changes to numeric form fields, allowing them to accept delta-typed inputs
* @param event
* @private
*/
_onChangeInputDelta(event) {
const input = event.target;
const value = input.value;
if ( ["+", "-"].includes(value[0]) ) {
let delta = parseFloat(value);
input.value = getProperty(this.actor.data, input.name) + delta;
} else if ( value[0] === "=" ) {
input.value = value.slice(1);
}
}
/* -------------------------------------------- */
/**
* Handle click events for the Traits tab button to configure special Character Flags
*/
_onConfigureFlags(event) {
event.preventDefault();
new ActorSheetFlags(this.actor).render(true);
}
/* -------------------------------------------- */
/**
* Handle cycling proficiency in a Skill
* @param {Event} event A click or contextmenu event which triggered the handler
* @private
*/
_onCycleSkillProficiency(event) {
event.preventDefault();
const field = $(event.currentTarget).siblings('input[type="hidden"]');
// Get the current level and the array of levels
const level = parseFloat(field.val());
const levels = [0, 1, 0.5, 2];
let idx = levels.indexOf(level);
// Toggle next level - forward on click, backwards on right
if ( event.type === "click" ) {
field.val(levels[(idx === levels.length - 1) ? 0 : idx + 1]);
} else if ( event.type === "contextmenu" ) {
field.val(levels[(idx === 0) ? levels.length - 1 : idx - 1]);
}
// Update the field value and save the form
this._onSubmit(event);
}
/* -------------------------------------------- */
/** @override */
async _onDropActor(event, data) {
const canPolymorph = game.user.isGM || (this.actor.owner && game.settings.get('sw5e', 'allowPolymorphing'));
if ( !canPolymorph ) return false;
// Get the target actor
let sourceActor = null;
if (data.pack) {
const pack = game.packs.find(p => p.collection === data.pack);
sourceActor = await pack.getEntity(data.id);
} else {
sourceActor = game.actors.get(data.id);
}
if ( !sourceActor ) return;
// Define a function to record polymorph settings for future use
const rememberOptions = html => {
const options = {};
html.find('input').each((i, el) => {
options[el.name] = el.checked;
});
const settings = mergeObject(game.settings.get('sw5e', 'polymorphSettings') || {}, options);
game.settings.set('sw5e', 'polymorphSettings', settings);
return settings;
};
// Create and render the Dialog
return new Dialog({
title: game.i18n.localize('SW5E.PolymorphPromptTitle'),
content: {
options: game.settings.get('sw5e', 'polymorphSettings'),
i18n: SW5E.polymorphSettings,
isToken: this.actor.isToken
},
default: 'accept',
buttons: {
accept: {
icon: '<i class="fas fa-check"></i>',
label: game.i18n.localize('SW5E.PolymorphAcceptSettings'),
callback: html => this.actor.transformInto(sourceActor, rememberOptions(html))
},
wildshape: {
icon: '<i class="fas fa-paw"></i>',
label: game.i18n.localize('SW5E.PolymorphWildShape'),
callback: html => this.actor.transformInto(sourceActor, {
keepMental: true,
mergeSaves: true,
mergeSkills: true,
transformTokens: rememberOptions(html).transformTokens
})
},
polymorph: {
icon: '<i class="fas fa-pastafarianism"></i>',
label: game.i18n.localize('SW5E.Polymorph'),
callback: html => this.actor.transformInto(sourceActor, {
transformTokens: rememberOptions(html).transformTokens
})
},
cancel: {
icon: '<i class="fas fa-times"></i>',
label: game.i18n.localize('Cancel')
}
}
}, {
classes: ['dialog', 'sw5e'],
width: 600,
template: 'systems/sw5e/templates/apps/polymorph-prompt.html'
}).render(true);
}
/* -------------------------------------------- */
/** @override */
async _onDropItemCreate(itemData) {
// Create a Consumable power scroll on the Inventory tab
if ( (itemData.type === "power") && (this._tabs[0].active === "inventory") ) {
const scroll = await Item5e.createScrollFromPower(itemData);
itemData = scroll.data;
}
// Create the owned item as normal
// TODO remove conditional logic in 0.7.x
if (isNewerVersion(game.data.version, "0.6.9")) return super._onDropItemCreate(itemData);
else return this.actor.createEmbeddedEntity("OwnedItem", itemData);
}
/* -------------------------------------------- */
/**
* Handle enabling editing for a power slot override value
* @param {MouseEvent} event The originating click event
* @private
*/
async _onPowerSlotOverride (event) {
const span = event.currentTarget.parentElement;
const level = span.dataset.level;
const override = this.actor.data.data.powers[level].override || span.dataset.slots;
const input = document.createElement("INPUT");
input.type = "text";
input.name = `data.powers.${level}.override`;
input.value = override;
input.placeholder = span.dataset.slots;
input.dataset.dtype = "Number";
// Replace the HTML
const parent = span.parentElement;
parent.removeChild(span);
parent.appendChild(input);
}
/* -------------------------------------------- */
/**
* Change the uses amount of an Owned Item within the Actor
* @param {Event} event The triggering click event
* @private
*/
async _onUsesChange(event) {
event.preventDefault();
const itemId = event.currentTarget.closest(".item").dataset.itemId;
const item = this.actor.getOwnedItem(itemId);
const uses = Math.clamped(0, parseInt(event.target.value), item.data.data.uses.max);
event.target.value = uses;
return item.update({ 'data.uses.value': uses });
}
/* -------------------------------------------- */
/**
* Handle rolling of an item from the Actor sheet, obtaining the Item instance and dispatching to it's roll method
* @private
*/
_onItemRoll(event) {
event.preventDefault();
const itemId = event.currentTarget.closest(".item").dataset.itemId;
const item = this.actor.getOwnedItem(itemId);
// Roll powers through the actor
if ( item.data.type === "power" ) {
return this.actor.usePower(item, {configureDialog: !event.shiftKey});
}
// Otherwise roll the Item directly
else return item.roll();
}
/* -------------------------------------------- */
/**
* Handle attempting to recharge an item usage by rolling a recharge check
* @param {Event} event The originating click event
* @private
*/
_onItemRecharge(event) {
event.preventDefault();
const itemId = event.currentTarget.closest(".item").dataset.itemId;
const item = this.actor.getOwnedItem(itemId);
return item.rollRecharge();
};
/* -------------------------------------------- */
/**
* Handle rolling of an item from the Actor sheet, obtaining the Item instance and dispatching to it's roll method
* @private
*/
_onItemSummary(event) {
event.preventDefault();
let li = $(event.currentTarget).parents(".item"),
item = this.actor.getOwnedItem(li.data("item-id")),
chatData = item.getChatData({secrets: this.actor.owner});
// Toggle summary
if ( li.hasClass("expanded") ) {
let summary = li.children(".item-summary");
summary.slideUp(200, () => summary.remove());
} else {
let div = $(`<div class="item-summary">${chatData.description.value}</div>`);
let props = $(`<div class="item-properties"></div>`);
chatData.properties.forEach(p => props.append(`<span class="tag">${p}</span>`));
div.append(props);
li.append(div.hide());
div.slideDown(200);
}
li.toggleClass("expanded");
}
/* -------------------------------------------- */
/**
* Handle creating a new Owned Item for the actor using initial data defined in the HTML dataset
* @param {Event} event The originating click event
* @private
*/
_onItemCreate(event) {
event.preventDefault();
const header = event.currentTarget;
const type = header.dataset.type;
const itemData = {
name: game.i18n.format("SW5E.ItemNew", {type: type.capitalize()}),
type: type,
data: duplicate(header.dataset)
};
delete itemData.data["type"];
return this.actor.createOwnedItem(itemData);
}
/* -------------------------------------------- */
/**
* Handle editing an existing Owned Item for the Actor
* @param {Event} event The originating click event
* @private
*/
_onItemEdit(event) {
event.preventDefault();
const li = event.currentTarget.closest(".item");
const item = this.actor.getOwnedItem(li.dataset.itemId);
item.sheet.render(true);
}
/* -------------------------------------------- */
/**
* Handle deleting an existing Owned Item for the Actor
* @param {Event} event The originating click event
* @private
*/
_onItemDelete(event) {
event.preventDefault();
const li = event.currentTarget.closest(".item");
this.actor.deleteOwnedItem(li.dataset.itemId);
}
/* -------------------------------------------- */
/**
* Manage Active Effect instances through the Actor Sheet via effect control buttons.
* @param {MouseEvent} event The left-click event on the effect control
* @private
*/
_onManageActiveEffect(event) {
event.preventDefault();
const a = event.currentTarget;
const li = a.closest(".effect");
const effect = this.actor.effects.get(li.dataset.effectId);
switch ( a.dataset.action ) {
case "edit":
return new ActiveEffectConfig(effect).render(true);
case "delete":
return effect.delete();
case "toggle":
return effect.update({disabled: !effect.data.disabled});
}
}
/* -------------------------------------------- */
/**
* Handle rolling an Ability check, either a test or a saving throw
* @param {Event} event The originating click event
* @private
*/
_onRollAbilityTest(event) {
event.preventDefault();
let ability = event.currentTarget.parentElement.dataset.ability;
this.actor.rollAbility(ability, {event: event});
}
/* -------------------------------------------- */
/**
* Handle rolling a Skill check
* @param {Event} event The originating click event
* @private
*/
_onRollSkillCheck(event) {
event.preventDefault();
const skill = event.currentTarget.parentElement.dataset.skill;
this.actor.rollSkill(skill, {event: event});
}
/* -------------------------------------------- */
/**
* Handle toggling Ability score proficiency level
* @param {Event} event The originating click event
* @private
*/
_onToggleAbilityProficiency(event) {
event.preventDefault();
const field = event.currentTarget.previousElementSibling;
this.actor.update({[field.name]: 1 - parseInt(field.value)});
}
/* -------------------------------------------- */
/**
* Handle toggling of filters to display a different set of owned items
* @param {Event} event The click event which triggered the toggle
* @private
*/
_onToggleFilter(event) {
event.preventDefault();
const li = event.currentTarget;
const set = this._filters[li.parentElement.dataset.filter];
const filter = li.dataset.filter;
if ( set.has(filter) ) set.delete(filter);
else set.add(filter);
this.render();
}
/* -------------------------------------------- */
/**
* Handle spawning the TraitSelector application which allows a checkbox of multiple trait options
* @param {Event} event The click event which originated the selection
* @private
*/
_onTraitSelector(event) {
event.preventDefault();
const a = event.currentTarget;
const label = a.parentElement.querySelector("label");
const choices = CONFIG.SW5E[a.dataset.options];
const options = { name: a.dataset.target, title: label.innerText, choices };
new TraitSelector(this.actor, options).render(true)
}
/* -------------------------------------------- */
/** @override */
_getHeaderButtons() {
let buttons = super._getHeaderButtons();
// Add button to revert polymorph
if ( !this.actor.isPolymorphed || this.actor.isToken ) return buttons;
buttons.unshift({
label: 'SW5E.PolymorphRestoreTransformation',
class: "restore-transformation",
icon: "fas fa-backward",
onclick: ev => this.actor.revertOriginalForm()
});
return buttons;
}
/* -------------------------------------------- */
/* DEPRECATED */
/* -------------------------------------------- */
/**
* TODO: Remove once 0.7.x is release
* @deprecated since 0.7.0
*/
async _onDrop (event) {
event.preventDefault();
// Get dropped data
let data;
try {
data = JSON.parse(event.dataTransfer.getData('text/plain'));
} catch (err) {
return false;
}
if ( !data ) return false;
// Handle the drop with a Hooked function
const allowed = Hooks.call("dropActorSheetData", this.actor, this, data);
if ( allowed === false ) return;
// Case 1 - Dropped Item
if ( data.type === "Item" ) {
return this._onDropItem(event, data);
}
// Case 2 - Dropped Actor
if ( data.type === "Actor" ) {
return this._onDropActor(event, data);
}
}
/* -------------------------------------------- */
/**
* TODO: Remove once 0.7.x is release
* @deprecated since 0.7.0
*/
async _onDropItem(event, data) {
if ( !this.actor.owner ) return false;
let itemData = await this._getItemDropData(event, data);
// Handle item sorting within the same Actor
const actor = this.actor;
let sameActor = (data.actorId === actor._id) || (actor.isToken && (data.tokenId === actor.token.id));
if (sameActor) return this._onSortItem(event, itemData);
// Create a new item
this._onDropItemCreate(itemData);
}
/* -------------------------------------------- */
/**
* TODO: Remove once 0.7.x is release
* @deprecated since 0.7.0
*/
async _getItemDropData(event, data) {
let itemData = null;
// Case 1 - Import from a Compendium pack
if (data.pack) {
const pack = game.packs.get(data.pack);
if (pack.metadata.entity !== "Item") return;
itemData = await pack.getEntry(data.id);
}
// Case 2 - Data explicitly provided
else if (data.data) {
itemData = data.data;
}
// Case 3 - Import from World entity
else {
let item = game.items.get(data.id);
if (!item) return;
itemData = item.data;
}
// Return a copy of the extracted data
return duplicate(itemData);
}
}

View file

@ -1,300 +0,0 @@
import ActorSheet5e from "./base.js";
import Actor5e from "../entity.js";
/**
* An Actor sheet for player character type actors in the SW5E system.
* Extends the base ActorSheet5e class.
* @type {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
});
}
/* -------------------------------------------- */
/**
* 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
let hp = sheetData.data.attributes.hp;
if (hp.temp === 0) delete hp.temp;
if (hp.tempmax === 0) delete hp.tempmax;
// Resources
sheetData["resources"] = ["primary", "secondary", "tertiary"].reduce((arr, r) => {
const res = sheetData.data.resources[r] || {};
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]);
}, []);
// Experience Tracking
sheetData["disableExperience"] = game.settings.get("sw5e", "disableExperienceTracking");
sheetData["classLabels"] = this.actor.itemTypes.class.map(c => c.name).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
let [items, powers, feats, classes, species, archetypes, classfeatures] = data.items.reduce((arr, item) => {
// Item details
item.img = item.img || DEFAULT_TOKEN;
item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1);
// Item usage
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));
// Item toggle state
this._prepareItemToggleState(item);
// Classify items into types
if ( item.type === "power" ) arr[1].push(item);
else if ( item.type === "feat" ) arr[2].push(item);
else if ( item.type === "class" ) arr[3].push(item);
else if ( item.type === "species" ) arr[4].push(item);
else if ( item.type === "archetype" ) arr[5].push(item);
else if ( item.type === "classfeature" ) arr[6].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 = Math.round(i.data.quantity * i.data.weight * 10) / 10;
inventory[i.type].items.push(i);
}
// Organize Powerbook and count the number of prepared powers (excluding always, at will, etc...)
const powerbook = this._preparePowerbook(data, powers);
const nPrepared = powers.filter(s => {
return (s.data.level > 0) && (s.data.preparation.mode === "prepared") && s.data.preparation.prepared;
}).length;
// Organize Features
const features = {
classes: { label: "SW5E.ItemTypeClassPl", items: [], hasActions: false, dataset: {type: "class"}, isClass: true },
classfeatures: { label: "SW5E.ItemTypeClassFeats", items: [], hasActions: false, dataset: {type: "classfeature"}, isClassfeature: true},
archetype: { label: "SW5E.ItemTypeArchetype", items: [], hasActions: false, dataset: {type: "archetype"}, isArchetype: true },
species: { label: "SW5E.ItemTypeSpecies", items: [], hasActions: false, dataset: {type: "species"}, isSpecies: true},
active: { label: "SW5E.FeatureActive", items: [], hasActions: true, dataset: {type: "feat", "activation.type": "action"} },
passive: { label: "SW5E.FeaturePassive", items: [], hasActions: false, dataset: {type: "feat"} }
};
for ( let f of feats ) {
if ( f.data.activation.type ) features.active.items.push(f);
else features.passive.items.push(f);
}
classes.sort((a, b) => b.levels - a.levels);
features.classes.items = classes;
features.classfeatures.items = classfeatures;
features.archetype.items = archetypes;
features.species.items = species;
// Assign and return
data.inventory = Object.values(inventory);
data.powerbook = powerbook;
data.preparedPowers = nPrepared;
data.features = Object.values(features);
}
/* -------------------------------------------- */
/**
* 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;
// Inventory Functions
html.find(".currency-convert").click(this._onConvertCurrency.bind(this));
// 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));
// Death saving throws
html.find('.death-save').click(this._onDeathSave.bind(this));
}
/* -------------------------------------------- */
/**
* Handle rolling a death saving throw for the Character
* @param {MouseEvent} event The originating click event
* @private
*/
_onDeathSave(event) {
event.preventDefault();
return this.actor.rollDeathSave({event: event});
}
/* -------------------------------------------- */
/**
* 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();
}
/* -------------------------------------------- */
/**
* Handle mouse click events to convert currency to the highest possible denomination
* @param {MouseEvent} event The originating click event
* @private
*/
async _onConvertCurrency(event) {
event.preventDefault();
return Dialog.confirm({
title: `${game.i18n.localize("SW5E.CurrencyConvert")}`,
content: `<p>${game.i18n.localize("SW5E.CurrencyConvertHint")}</p>`,
yes: () => this.actor.convertCurrency()
});
}
/* -------------------------------------------- */
/** @override */
async _onDropItemCreate(itemData) {
// Upgrade the number of class levels a character has
// and add features
if ( itemData.type === "class" ) {
const cls = this.actor.itemTypes.class.find(c => c.name === itemData.name);
const classWasAlreadyPresent = !!cls;
// Add new features for class level
if ( !classWasAlreadyPresent ) {
Actor5e.getClassFeatures(itemData).then(features => {
this.actor.createEmbeddedEntity("OwnedItem", features);
});
}
// If the actor already has the class, increment the level instead of creating a new item
// then add new features as long as level increases
if ( classWasAlreadyPresent ) {
const lvl = cls.data.data.levels;
const newLvl = Math.min(lvl + 1, 20 + lvl - this.actor.data.data.details.level);
if ( !(lvl === newLvl) ) {
cls.update({"data.levels": newLvl});
itemData.data.levels = newLvl;
Actor5e.getClassFeatures(itemData).then(features => {
this.actor.createEmbeddedEntity("OwnedItem", features);
});
}
return
}
}
super._onDropItemCreate(itemData);
}
}

View file

@ -0,0 +1,989 @@
import Item5e from "../../../item/entity.js";
import TraitSelector from "../../../apps/trait-selector.js";
import ActorSheetFlags from "../../../apps/actor-flags.js";
import ActorHitDiceConfig from "../../../apps/hit-dice-config.js";
import ActorMovementConfig from "../../../apps/movement-config.js";
import ActorSensesConfig from "../../../apps/senses-config.js";
import ActorTypeConfig from "../../../apps/actor-type.js";
import {SW5E} from "../../../config.js";
import {onManageActiveEffect, prepareActiveEffectCategories} from "../../../effects.js";
/**
* Extend the basic ActorSheet class to suppose SW5e-specific logic and functionality.
* This sheet is an Abstract layer which is not used.
* @extends {ActorSheet}
*/
export default class ActorSheet5e extends ActorSheet {
constructor(...args) {
super(...args);
/**
* Track the set of item filters which are applied
* @type {Set}
*/
this._filters = {
inventory: new Set(),
forcePowerbook: new Set(),
techPowerbook: new Set(),
features: new Set(),
effects: new Set()
};
}
/* -------------------------------------------- */
/** @override */
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
scrollY: [
".inventory .group-list",
".features .group-list",
".force-powerbook .group-list",
".tech-powerbook .group-list",
".effects .effects-list"
],
tabs: [{navSelector: ".tabs", contentSelector: ".sheet-body", initial: "description"}]
});
}
/* -------------------------------------------- */
/**
* A set of item types that should be prevented from being dropped on this type of actor sheet.
* @type {Set<string>}
*/
static unsupportedItemTypes = new Set();
/* -------------------------------------------- */
/** @override */
get template() {
if (!game.user.isGM && this.actor.limited)
return "systems/sw5e/templates/actors/newActor/expanded-limited-sheet.html";
return `systems/sw5e/templates/actors/newActor/${this.actor.data.type}-sheet.html`;
}
/* -------------------------------------------- */
/** @override */
getData(options) {
// Basic data
let isOwner = this.actor.isOwner;
const data = {
owner: isOwner,
limited: this.actor.limited,
options: this.options,
editable: this.isEditable,
cssClass: isOwner ? "editable" : "locked",
isCharacter: this.actor.type === "character",
isNPC: this.actor.type === "npc",
isStarship: this.actor.type === "starship",
isVehicle: this.actor.type === "vehicle",
config: CONFIG.SW5E,
rollData: this.actor.getRollData.bind(this.actor)
};
// The Actor's data
const actorData = this.actor.data.toObject(false);
data.actor = actorData;
data.data = actorData.data;
// Owned Items
data.items = actorData.items;
for (let i of data.items) {
const item = this.actor.items.get(i._id);
i.labels = item.labels;
}
data.items.sort((a, b) => (a.sort || 0) - (b.sort || 0));
// Labels and filters
data.labels = this.actor.labels || {};
data.filters = this._filters;
// Ability Scores
for (let [a, abl] of Object.entries(actorData.data.abilities)) {
abl.icon = this._getProficiencyIcon(abl.proficient);
abl.hover = CONFIG.SW5E.proficiencyLevels[abl.proficient];
abl.label = CONFIG.SW5E.abilities[a];
}
// Skills
if (actorData.data.skills) {
for (let [s, skl] of Object.entries(actorData.data.skills)) {
skl.ability = CONFIG.SW5E.abilityAbbreviations[skl.ability];
skl.icon = this._getProficiencyIcon(skl.value);
skl.hover = CONFIG.SW5E.proficiencyLevels[skl.value];
if (data.actor.type === "starship") {
skl.label = CONFIG.SW5E.starshipSkills[s];
} else {
skl.label = CONFIG.SW5E.skills[s];
}
}
}
// Movement speeds
data.movement = this._getMovementSpeed(actorData);
// Senses
data.senses = this._getSenses(actorData);
// Update traits
this._prepareTraits(actorData.data.traits);
// Prepare owned items
this._prepareItems(data);
// Prepare active effects
data.effects = prepareActiveEffectCategories(this.actor.effects);
// Return data to the sheet
return data;
}
/* -------------------------------------------- */
/**
* Prepare the display of movement speed data for the Actor*
* @param {object} actorData The Actor data being prepared.
* @param {boolean} [largestPrimary=false] Show the largest movement speed as "primary", otherwise show "walk"
* @returns {{primary: string, special: string}}
* @private
*/
_getMovementSpeed(actorData, largestPrimary = false) {
const movement = actorData.data.attributes.movement || {};
// Prepare an array of available movement speeds
let speeds = [
[movement.burrow, `${game.i18n.localize("SW5E.MovementBurrow")} ${movement.burrow}`],
[movement.climb, `${game.i18n.localize("SW5E.MovementClimb")} ${movement.climb}`],
[
movement.fly,
`${game.i18n.localize("SW5E.MovementFly")} ${movement.fly}` +
(movement.hover ? ` (${game.i18n.localize("SW5E.MovementHover")})` : "")
],
[movement.swim, `${game.i18n.localize("SW5E.MovementSwim")} ${movement.swim}`]
];
if (largestPrimary) {
speeds.push([movement.walk, `${game.i18n.localize("SW5E.MovementWalk")} ${movement.walk}`]);
}
// Filter and sort speeds on their values
speeds = speeds.filter((s) => !!s[0]).sort((a, b) => b[0] - a[0]);
// Case 1: Largest as primary
if (largestPrimary) {
let primary = speeds.shift();
return {
primary: `${primary ? primary[1] : "0"} ${movement.units}`,
special: speeds.map((s) => s[1]).join(", ")
};
}
// Case 2: Walk as primary
else {
return {
primary: `${movement.walk || 0} ${movement.units}`,
special: speeds.length ? speeds.map((s) => s[1]).join(", ") : ""
};
}
}
/* -------------------------------------------- */
_getSenses(actorData) {
const senses = actorData.data.attributes.senses || {};
const tags = {};
for (let [k, label] of Object.entries(CONFIG.SW5E.senses)) {
const v = senses[k] ?? 0;
if (v === 0) continue;
tags[k] = `${game.i18n.localize(label)} ${v} ${senses.units}`;
}
if (!!senses.special) tags["special"] = senses.special;
return tags;
}
/* -------------------------------------------- */
/**
* Prepare the data structure for traits data like languages, resistances & vulnerabilities, and proficiencies
* @param {object} traits The raw traits data object from the actor data
* @private
*/
_prepareTraits(traits) {
const map = {
dr: CONFIG.SW5E.damageResistanceTypes,
di: CONFIG.SW5E.damageResistanceTypes,
dv: CONFIG.SW5E.damageResistanceTypes,
ci: CONFIG.SW5E.conditionTypes,
languages: CONFIG.SW5E.languages,
armorProf: CONFIG.SW5E.armorProficiencies,
weaponProf: CONFIG.SW5E.weaponProficiencies,
toolProf: CONFIG.SW5E.toolProficiencies
};
for (let [t, choices] of Object.entries(map)) {
const trait = traits[t];
if (!trait) continue;
let values = [];
if (trait.value) {
values = trait.value instanceof Array ? trait.value : [trait.value];
}
trait.selected = values.reduce((obj, t) => {
obj[t] = choices[t];
return obj;
}, {});
// Add custom entry
if (trait.custom) {
trait.custom.split(";").forEach((c, i) => (trait.selected[`custom${i + 1}`] = c.trim()));
}
trait.cssClass = !isObjectEmpty(trait.selected) ? "" : "inactive";
}
}
/* -------------------------------------------- */
/**
* Insert a power into the powerbook object when rendering the character sheet
* @param {Object} data The Actor data being prepared
* @param {Array} powers The power data being prepared
* @param {string} school The school of the powerbook being prepared
* @private
*/
_preparePowerbook(data, powers, school) {
const owner = this.actor.isOwner;
const levels = data.data.powers;
const powerbook = {};
// Define some mappings
const sections = {
atwill: -20,
innate: -10
};
// Label power slot uses headers
const useLabels = {
"-20": "-",
"-10": "-",
"0": "&infin;"
};
// Format a powerbook entry for a certain indexed level
const registerSection = (sl, i, label, {prepMode = "prepared", value, max, override} = {}) => {
powerbook[i] = {
order: i,
label: label,
usesSlots: i > 0,
canCreate: owner,
canPrepare: data.actor.type === "character" && i >= 1,
powers: [],
uses: useLabels[i] || value || 0,
slots: useLabels[i] || max || 0,
override: override || 0,
dataset: {
"type": "power",
"level": prepMode in sections ? 1 : i,
"preparation.mode": prepMode,
"school": school
},
prop: sl
};
};
// Determine the maximum power level which has a slot
const maxLevel = Array.fromRange(10).reduce((max, i) => {
if (i === 0) return max;
const level = levels[`power${i}`];
if ((level.max || level.override) && i > max) max = i;
return max;
}, 0);
// Level-based powercasters have cantrips and leveled slots
if (maxLevel > 0) {
registerSection("power0", 0, CONFIG.SW5E.powerLevels[0]);
for (let lvl = 1; lvl <= maxLevel; lvl++) {
const sl = `power${lvl}`;
registerSection(sl, lvl, CONFIG.SW5E.powerLevels[lvl], levels[sl]);
}
}
// Iterate over every power item, adding powers to the powerbook by section
powers.forEach((power) => {
const mode = power.data.preparation.mode || "prepared";
let s = power.data.level || 0;
const sl = `power${s}`;
// Specialized powercasting modes (if they exist)
if (mode in sections) {
s = sections[mode];
if (!powerbook[s]) {
const l = levels[mode] || {};
const config = CONFIG.SW5E.powerPreparationModes[mode];
registerSection(mode, s, config, {
prepMode: mode,
value: l.value,
max: l.max,
override: l.override
});
}
}
// Sections for higher-level powers which the caster "should not" have, but power items exist for
else if (!powerbook[s]) {
registerSection(sl, s, CONFIG.SW5E.powerLevels[s], {levels: levels[sl]});
}
// Add the power to the relevant heading
powerbook[s].powers.push(power);
});
// Sort the powerbook by section level
const sorted = Object.values(powerbook);
sorted.sort((a, b) => a.order - b.order);
return sorted;
}
/* -------------------------------------------- */
/**
* Determine whether an Owned Item will be shown based on the current set of filters
* @return {boolean}
* @private
*/
_filterItems(items, filters) {
return items.filter((item) => {
const data = item.data;
// Action usage
for (let f of ["action", "bonus", "reaction"]) {
if (filters.has(f)) {
if (data.activation && data.activation.type !== f) return false;
}
}
// Power-specific filters
if (filters.has("ritual")) {
if (data.components.ritual !== true) return false;
}
if (filters.has("concentration")) {
if (data.components.concentration !== true) return false;
}
if (filters.has("prepared")) {
if (data.level === 0 || ["innate", "always"].includes(data.preparation.mode)) return true;
if (this.actor.data.type === "npc") return true;
if (this.actor.data.type === "starship") return true;
return data.preparation.prepared;
}
// Equipment-specific filters
if (filters.has("equipped")) {
if (data.equipped !== true) return false;
}
return true;
});
}
/* -------------------------------------------- */
/**
* Get the font-awesome icon used to display a certain level of skill proficiency
* @private
*/
_getProficiencyIcon(level) {
const icons = {
0: '<i class="far fa-circle"></i>',
0.5: '<i class="fas fa-adjust"></i>',
1: '<i class="fas fa-check"></i>',
2: '<i class="fas fa-check-double"></i>'
};
return icons[level] || icons[0];
}
/* -------------------------------------------- */
/* Event Listeners and Handlers
/* -------------------------------------------- */
/** @inheritdoc */
activateListeners(html) {
// Activate Item Filters
const filterLists = html.find(".filter-list");
filterLists.each(this._initializeFilterItemList.bind(this));
filterLists.on("click", ".filter-item", this._onToggleFilter.bind(this));
// Item summaries
html.find(".item .item-name.rollable h4").click((event) => this._onItemSummary(event));
// View Item Sheets
html.find(".item-edit").click(this._onItemEdit.bind(this));
// Editable Only Listeners
if (this.isEditable) {
// Input focus and update
const inputs = html.find("input");
inputs.focus((ev) => ev.currentTarget.select());
inputs.addBack().find('[data-dtype="Number"]').change(this._onChangeInputDelta.bind(this));
// Ability Proficiency
html.find(".ability-proficiency").click(this._onToggleAbilityProficiency.bind(this));
// Toggle Skill Proficiency
html.find(".skill-proficiency").on("click contextmenu", this._onCycleSkillProficiency.bind(this));
// Trait Selector
html.find(".trait-selector").click(this._onTraitSelector.bind(this));
// Configure Special Flags
html.find(".config-button").click(this._onConfigMenu.bind(this));
// Owned Item management
html.find(".item-create").click(this._onItemCreate.bind(this));
html.find(".item-delete").click(this._onItemDelete.bind(this));
html.find(".item-collapse").click(this._onItemCollapse.bind(this));
html.find(".item-uses input")
.click((ev) => ev.target.select())
.change(this._onUsesChange.bind(this));
html.find(".slot-max-override").click(this._onPowerSlotOverride.bind(this));
html.find(".increment-class-level").click(this._onIncrementClassLevel.bind(this));
html.find(".decrement-class-level").click(this._onDecrementClassLevel.bind(this));
// Active Effect management
html.find(".effect-control").click((ev) => onManageActiveEffect(ev, this.actor));
}
// Owner Only Listeners
if (this.actor.isOwner) {
// Ability Checks
html.find(".ability-name").click(this._onRollAbilityTest.bind(this));
// Roll Skill Checks
html.find(".skill-name").click(this._onRollSkillCheck.bind(this));
// Item Rolling
html.find(".item .item-image").click((event) => this._onItemRoll(event));
html.find(".item .item-recharge").click((event) => this._onItemRecharge(event));
}
// Otherwise remove rollable classes
else {
html.find(".rollable").each((i, el) => el.classList.remove("rollable"));
}
// Handle default listeners last so system listeners are triggered first
super.activateListeners(html);
}
/* -------------------------------------------- */
/**
* Iinitialize Item list filters by activating the set of filters which are currently applied
* @private
*/
_initializeFilterItemList(i, ul) {
const set = this._filters[ul.dataset.filter];
const filters = ul.querySelectorAll(".filter-item");
for (let li of filters) {
if (set.has(li.dataset.filter)) li.classList.add("active");
}
}
/* -------------------------------------------- */
/* Event Listeners and Handlers */
/* -------------------------------------------- */
/**
* Handle input changes to numeric form fields, allowing them to accept delta-typed inputs
* @param event
* @private
*/
_onChangeInputDelta(event) {
const input = event.target;
const value = input.value;
if (["+", "-"].includes(value[0])) {
let delta = parseFloat(value);
input.value = getProperty(this.actor.data, input.name) + delta;
} else if (value[0] === "=") {
input.value = value.slice(1);
}
}
/* -------------------------------------------- */
/**
* Handle spawning the TraitSelector application which allows a checkbox of multiple trait options
* @param {Event} event The click event which originated the selection
* @private
*/
_onConfigMenu(event) {
event.preventDefault();
const button = event.currentTarget;
let app;
switch (button.dataset.action) {
case "hit-dice":
app = new ActorHitDiceConfig(this.object);
break;
case "movement":
app = new ActorMovementConfig(this.object);
break;
case "flags":
app = new ActorSheetFlags(this.object);
break;
case "senses":
app = new ActorSensesConfig(this.object);
break;
case "type":
new ActorTypeConfig(this.object).render(true);
break;
}
app?.render(true);
}
/* -------------------------------------------- */
/**
* Handle cycling proficiency in a Skill
* @param {Event} event A click or contextmenu event which triggered the handler
* @private
*/
_onCycleSkillProficiency(event) {
event.preventDefault();
const field = $(event.currentTarget).siblings('input[type="hidden"]');
// Get the current level and the array of levels
const level = parseFloat(field.val());
const levels = [0, 1, 0.5, 2];
let idx = levels.indexOf(level);
// Toggle next level - forward on click, backwards on right
if (event.type === "click") {
field.val(levels[idx === levels.length - 1 ? 0 : idx + 1]);
} else if (event.type === "contextmenu") {
field.val(levels[idx === 0 ? levels.length - 1 : idx - 1]);
}
// Update the field value and save the form
this._onSubmit(event);
}
/* -------------------------------------------- */
/** @override */
async _onDropActor(event, data) {
const canPolymorph = game.user.isGM || (this.actor.isOwner && game.settings.get("sw5e", "allowPolymorphing"));
if (!canPolymorph) return false;
// Get the target actor
let sourceActor = null;
if (data.pack) {
const pack = game.packs.find((p) => p.collection === data.pack);
sourceActor = await pack.getEntity(data.id);
} else {
sourceActor = game.actors.get(data.id);
}
if (!sourceActor) return;
// Define a function to record polymorph settings for future use
const rememberOptions = (html) => {
const options = {};
html.find("input").each((i, el) => {
options[el.name] = el.checked;
});
const settings = mergeObject(game.settings.get("sw5e", "polymorphSettings") || {}, options);
game.settings.set("sw5e", "polymorphSettings", settings);
return settings;
};
// Create and render the Dialog
return new Dialog(
{
title: game.i18n.localize("SW5E.PolymorphPromptTitle"),
content: {
options: game.settings.get("sw5e", "polymorphSettings"),
i18n: SW5E.polymorphSettings,
isToken: this.actor.isToken
},
default: "accept",
buttons: {
accept: {
icon: '<i class="fas fa-check"></i>',
label: game.i18n.localize("SW5E.PolymorphAcceptSettings"),
callback: (html) => this.actor.transformInto(sourceActor, rememberOptions(html))
},
wildshape: {
icon: '<i class="fas fa-paw"></i>',
label: game.i18n.localize("SW5E.PolymorphWildShape"),
callback: (html) =>
this.actor.transformInto(sourceActor, {
keepBio: true,
keepClass: true,
keepMental: true,
mergeSaves: true,
mergeSkills: true,
transformTokens: rememberOptions(html).transformTokens
})
},
polymorph: {
icon: '<i class="fas fa-pastafarianism"></i>',
label: game.i18n.localize("SW5E.Polymorph"),
callback: (html) =>
this.actor.transformInto(sourceActor, {
transformTokens: rememberOptions(html).transformTokens
})
},
cancel: {
icon: '<i class="fas fa-times"></i>',
label: game.i18n.localize("Cancel")
}
}
},
{
classes: ["dialog", "sw5e"],
width: 600,
template: "systems/sw5e/templates/apps/polymorph-prompt.html"
}
).render(true);
}
/* -------------------------------------------- */
/** @override */
async _onDropItemCreate(itemData) {
// Check to make sure items of this type are allowed on this actor
if (this.constructor.unsupportedItemTypes.has(itemData.type)) {
return ui.notifications.warn(
game.i18n.format("SW5E.ActorWarningInvalidItem", {
itemType: game.i18n.localize(CONFIG.Item.typeLabels[itemData.type]),
actorType: game.i18n.localize(CONFIG.Actor.typeLabels[this.actor.type])
})
);
}
// Create a Consumable power scroll on the Inventory tab
if (itemData.type === "power" && this._tabs[0].active === "inventory") {
const scroll = await Item5e.createScrollFromPower(itemData);
itemData = scroll.data;
}
if (itemData.data) {
// Ignore certain statuses
["equipped", "proficient", "prepared"].forEach((k) => delete itemData.data[k]);
// Downgrade ATTUNED to REQUIRED
itemData.data.attunement = Math.min(itemData.data.attunement, CONFIG.SW5E.attunementTypes.REQUIRED);
}
// Stack identical consumables
if (itemData.type === "consumable" && itemData.flags.core?.sourceId) {
const similarItem = this.actor.items.find((i) => {
const sourceId = i.getFlag("core", "sourceId");
return sourceId && sourceId === itemData.flags.core?.sourceId && i.type === "consumable";
});
if (similarItem && itemData.name !== "Power Cell") {
// Always create a new powercell instead of increasing quantity
return similarItem.update({
"data.quantity": similarItem.data.data.quantity + Math.max(itemData.data.quantity, 1)
});
}
}
// Create the owned item as normal
return super._onDropItemCreate(itemData);
}
/* -------------------------------------------- */
/**
* Handle enabling editing for a power slot override value
* @param {MouseEvent} event The originating click event
* @private
*/
async _onPowerSlotOverride(event) {
const span = event.currentTarget.parentElement;
const level = span.dataset.level;
const override = this.actor.data.data.powers[level].override || span.dataset.slots;
const input = document.createElement("INPUT");
input.type = "text";
input.name = `data.powers.${level}.override`;
input.value = override;
input.placeholder = span.dataset.slots;
input.dataset.dtype = "Number";
// Replace the HTML
const parent = span.parentElement;
parent.removeChild(span);
parent.appendChild(input);
}
/* -------------------------------------------- */
/**
* Change the uses amount of an Owned Item within the Actor
* @param {Event} event The triggering click event
* @private
*/
async _onUsesChange(event) {
event.preventDefault();
const itemId = event.currentTarget.closest(".item").dataset.itemId;
const item = this.actor.items.get(itemId);
const uses = Math.clamped(0, parseInt(event.target.value), item.data.data.uses.max);
event.target.value = uses;
return item.update({"data.uses.value": uses});
}
/* -------------------------------------------- */
/**
* Handle rolling of an item from the Actor sheet, obtaining the Item instance and dispatching to it's roll method
* @private
*/
_onItemRoll(event) {
event.preventDefault();
const itemId = event.currentTarget.closest(".item").dataset.itemId;
const item = this.actor.items.get(itemId);
return item.roll();
}
/* -------------------------------------------- */
/**
* Handle attempting to recharge an item usage by rolling a recharge check
* @param {Event} event The originating click event
* @private
*/
_onItemRecharge(event) {
event.preventDefault();
const itemId = event.currentTarget.closest(".item").dataset.itemId;
const item = this.actor.items.get(itemId);
return item.rollRecharge();
}
/* -------------------------------------------- */
/**
* Handle rolling of an item from the Actor sheet, obtaining the Item instance and dispatching to it's roll method
* @private
*/
_onItemSummary(event) {
event.preventDefault();
let li = $(event.currentTarget).parents(".item"),
item = this.actor.items.get(li.data("item-id")),
chatData = item.getChatData({secrets: this.actor.isOwner});
// Toggle summary
if (li.hasClass("expanded")) {
let summary = li.children(".item-summary");
summary.slideUp(200, () => summary.remove());
} else {
let div = $(`<div class="item-summary">${chatData.description.value}</div>`);
let props = $(`<div class="item-properties"></div>`);
chatData.properties.forEach((p) => props.append(`<span class="tag">${p}</span>`));
div.append(props);
li.append(div.hide());
div.slideDown(200);
}
li.toggleClass("expanded");
}
/* -------------------------------------------- */
/**
* Handle creating a new Owned Item for the actor using initial data defined in the HTML dataset
* @param {Event} event The originating click event
* @private
*/
_onItemCreate(event) {
event.preventDefault();
const header = event.currentTarget;
const type = header.dataset.type;
const itemData = {
name: game.i18n.format("SW5E.ItemNew", {type: game.i18n.localize(`SW5E.ItemType${type.capitalize()}`)}),
type: type,
data: foundry.utils.deepClone(header.dataset)
};
delete itemData.data["type"];
return this.actor.createEmbeddedDocuments("Item", [itemData]);
}
/* -------------------------------------------- */
/**
* Handle editing an existing Owned Item for the Actor
* @param {Event} event The originating click event
* @private
*/
_onItemEdit(event) {
event.preventDefault();
const li = event.currentTarget.closest(".item");
const item = this.actor.items.get(li.dataset.itemId);
return item.sheet.render(true);
}
/* -------------------------------------------- */
/**
* Handle deleting an existing Owned Item for the Actor
* @param {Event} event The originating click event
* @private
*/
_onItemDelete(event) {
event.preventDefault();
const li = event.currentTarget.closest(".item");
const item = this.actor.items.get(li.dataset.itemId);
if (item) return item.delete();
}
/**
* Handle collapsing a Feature row on the actor sheet
* @param {Event} event The originating click event
* @private
*/
_onItemCollapse(event) {
event.preventDefault();
event.currentTarget.classList.toggle("active");
const li = event.currentTarget.closest("li");
const content = li.querySelector(".content");
if (content.style.display === "none") {
content.style.display = "block";
} else {
content.style.display = "none";
}
}
/**
* Handle incrementing class level on the actor sheet
* @param {Event} event The originating click event
* @private
*/
_onIncrementClassLevel(event) {
event.preventDefault();
const div = event.currentTarget.closest(".character");
const li = event.currentTarget.closest("li");
const actorId = div.id.split("-")[1];
const itemId = li.dataset.itemId;
const actor = game.actors.get(actorId);
const item = actor.items.get(itemId);
let levels = item.data.data.levels;
const update = {_id: item.data._id, data: {levels: levels + 1}};
actor.updateEmbeddedDocuments("Item", [update]);
}
/**
* Handle decrementing class level on the actor sheet
* @param {Event} event The originating click event
* @private
*/
_onDecrementClassLevel(event) {
event.preventDefault();
const div = event.currentTarget.closest(".character");
const li = event.currentTarget.closest("li");
const actorId = div.id.split("-")[1];
const itemId = li.dataset.itemId;
const actor = game.actors.get(actorId);
const item = actor.items.get(itemId);
let levels = item.data.data.levels;
const update = {_id: item.data._id, data: {levels: levels - 1}};
actor.updateEmbeddedDocuments("Item", [update]);
}
/* -------------------------------------------- */
/**
* Handle rolling an Ability check, either a test or a saving throw
* @param {Event} event The originating click event
* @private
*/
_onRollAbilityTest(event) {
event.preventDefault();
let ability = event.currentTarget.parentElement.dataset.ability;
return this.actor.rollAbility(ability, {event: event});
}
/* -------------------------------------------- */
/**
* Handle rolling a Skill check
* @param {Event} event The originating click event
* @private
*/
_onRollSkillCheck(event) {
event.preventDefault();
const skill = event.currentTarget.parentElement.dataset.skill;
return this.actor.rollSkill(skill, {event: event});
}
/* -------------------------------------------- */
/**
* Handle toggling Ability score proficiency level
* @param {Event} event The originating click event
* @private
*/
_onToggleAbilityProficiency(event) {
event.preventDefault();
const field = event.currentTarget.previousElementSibling;
return this.actor.update({[field.name]: 1 - parseInt(field.value)});
}
/* -------------------------------------------- */
/**
* Handle toggling of filters to display a different set of owned items
* @param {Event} event The click event which triggered the toggle
* @private
*/
_onToggleFilter(event) {
event.preventDefault();
const li = event.currentTarget;
const set = this._filters[li.parentElement.dataset.filter];
const filter = li.dataset.filter;
if (set.has(filter)) set.delete(filter);
else set.add(filter);
return this.render();
}
/* -------------------------------------------- */
/**
* Handle spawning the TraitSelector application which allows a checkbox of multiple trait options
* @param {Event} event The click event which originated the selection
* @private
*/
_onTraitSelector(event) {
event.preventDefault();
const a = event.currentTarget;
const label = a.parentElement.querySelector("label");
const choices = CONFIG.SW5E[a.dataset.options];
const options = {name: a.dataset.target, title: label.innerText, choices};
return new TraitSelector(this.actor, options).render(true);
}
/* -------------------------------------------- */
/** @override */
_getHeaderButtons() {
let buttons = super._getHeaderButtons();
if (this.actor.isPolymorphed) {
buttons.unshift({
label: "SW5E.PolymorphRestoreTransformation",
class: "restore-transformation",
icon: "fas fa-backward",
onclick: () => this.actor.revertOriginalForm()
});
}
return buttons;
}
}

View file

@ -0,0 +1,752 @@
import ActorSheet5e from "./base.js";
import Actor5e from "../../entity.js";
/**
* An Actor sheet for player character type actors in the SW5E system.
* Extends the base ActorSheet5e class.
* @type {ActorSheet5e}
*/
export default class ActorSheet5eCharacterNew extends ActorSheet5e {
get template() {
if (!game.user.isGM && this.actor.limited) return "systems/sw5e/templates/actors/newActor/limited-sheet.html";
return "systems/sw5e/templates/actors/newActor/character-sheet.html";
}
/**
* Define default rendering options for the NPC sheet
* @return {Object}
*/
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
classes: ["swalt", "sw5e", "sheet", "actor", "character"],
blockFavTab: true,
subTabs: null,
width: 800,
tabs: [
{
navSelector: ".root-tabs",
contentSelector: ".sheet-body",
initial: "attributes"
}
]
});
}
/* -------------------------------------------- */
/**
* 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
let hp = sheetData.data.attributes.hp;
if (hp.temp === 0) delete hp.temp;
if (hp.tempmax === 0) delete hp.tempmax;
// Resources
sheetData["resources"] = ["primary", "secondary", "tertiary"].reduce((arr, r) => {
const res = sheetData.data.resources[r] || {};
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]);
}, []);
// Experience Tracking
sheetData["disableExperience"] = game.settings.get("sw5e", "disableExperienceTracking");
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
let [
items,
forcepowers,
techpowers,
feats,
classes,
deployments,
deploymentfeatures,
ventures,
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];
// Item usage
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);
// Item toggle state
this._prepareItemToggleState(item);
// Primary Class
if (item.type === "class")
item.isOriginalClass = item._id === this.actor.data.data.details.originalClass;
// Classify items into types
if (item.type === "power" && ["lgt", "drk", "uni"].includes(item.data.school)) arr[1].push(item);
else if (item.type === "power" && ["tec"].includes(item.data.school)) arr[2].push(item);
else if (item.type === "feat") arr[3].push(item);
else if (item.type === "class") arr[4].push(item);
else if (item.type === "deployment") arr[5].push(item);
else if (item.type === "deploymentfeature") arr[6].push(item);
else if (item.type === "venture") arr[7].push(item);
else if (item.type === "species") arr[8].push(item);
else if (item.type === "archetype") arr[9].push(item);
else if (item.type === "classfeature") arr[10].push(item);
else if (item.type === "background") arr[11].push(item);
else if (item.type === "fightingstyle") arr[12].push(item);
else if (item.type === "fightingmastery") arr[13].push(item);
else if (item.type === "lightsaberform") arr[14].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);
forcepowers = this._filterItems(forcepowers, this._filters.forcePowerbook);
techpowers = this._filterItems(techpowers, this._filters.techPowerbook);
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);
}
// Organize Powerbook and count the number of prepared powers (excluding always, at will, etc...)
const forcePowerbook = this._preparePowerbook(data, forcepowers, "uni");
const techPowerbook = this._preparePowerbook(data, techpowers, "tec");
// Organize Features
const features = {
classes: {
label: "SW5E.ItemTypeClassPl",
items: [],
hasActions: false,
dataset: {type: "class"},
isClass: true
},
classfeatures: {
label: "SW5E.ItemTypeClassFeats",
items: [],
hasActions: true,
dataset: {type: "classfeature"},
isClassfeature: true
},
archetype: {
label: "SW5E.ItemTypeArchetype",
items: [],
hasActions: false,
dataset: {type: "archetype"},
isArchetype: true
},
deployments: {
label: "SW5E.ItemTypeDeploymentPl",
items: [],
hasActions: false,
dataset: {type: "deployment"},
isDeployment: true
},
deploymentfeatures: {
label: "SW5E.ItemTypeDeploymentFeaturePl",
items: [],
hasActions: true,
dataset: {type: "deploymentfeature"},
isDeploymentfeature: true
},
ventures: {
label: "SW5E.ItemTypeVenturePl",
items: [],
hasActions: false,
dataset: {type: "venture"},
isVenture: true
},
species: {
label: "SW5E.ItemTypeSpecies",
items: [],
hasActions: false,
dataset: {type: "species"},
isSpecies: 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: {
label: "SW5E.ItemTypeFightingMasteryPl",
items: [],
hasActions: false,
dataset: {type: "fightingmastery"},
isFightingmastery: true
},
lightsaberforms: {
label: "SW5E.ItemTypeLightsaberFormPl",
items: [],
hasActions: false,
dataset: {type: "lightsaberform"},
isLightsaberform: true
},
active: {
label: "SW5E.FeatureActive",
items: [],
hasActions: true,
dataset: {"type": "feat", "activation.type": "action"}
},
passive: {label: "SW5E.FeaturePassive", items: [], hasActions: false, dataset: {type: "feat"}}
};
for (let f of feats) {
if (f.data.activation.type) features.active.items.push(f);
else features.passive.items.push(f);
}
classes.sort((a, b) => b.data.levels - a.data.levels);
features.classes.items = classes;
features.classfeatures.items = classfeatures;
features.archetype.items = archetypes;
features.deployments.items = deployments;
features.deploymentfeatures.items = deploymentfeatures;
features.ventures.items = ventures;
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.forcePowerbook = forcePowerbook;
data.techPowerbook = techPowerbook;
data.features = Object.values(features);
}
/* -------------------------------------------- */
/**
* 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;
// Inventory Functions
// html.find(".currency-convert").click(this._onConvertCurrency.bind(this));
// 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));
// Send Languages to Chat onClick
html.find('[data-options="share-languages"]').click((event) => {
event.preventDefault();
let langs = this.actor.data.data.traits.languages.value
.map((l) => CONFIG.SW5E.languages[l] || l)
.join(", ");
let custom = this.actor.data.data.traits.languages.custom;
if (custom) langs += ", " + custom.replace(/;/g, ",");
let content = `
<div class="sw5e chat-card item-card" data-acor-id="${this.actor.data._id}">
<header class="card-header flexrow">
<img src="${this.actor.data.token.img}" title="" width="36" height="36" style="border: none;"/>
<h3>Known Languages</h3>
</header>
<div class="card-content">${langs}</div>
</div>
`;
// Send to Chat
let rollBlind = false;
let rollMode = game.settings.get("core", "rollMode");
if (rollMode === "blindroll") rollBlind = true;
let data = {
user: game.user.data._id,
content: content,
blind: rollBlind,
speaker: {
actor: this.actor.data._id,
token: this.actor.token,
alias: this.actor.name
},
type: CONST.CHAT_MESSAGE_TYPES.OTHER
};
if (["gmroll", "blindroll"].includes(rollMode)) data["whisper"] = ChatMessage.getWhisperRecipients("GM");
else if (rollMode === "selfroll") data["whisper"] = [game.users.get(game.user.data._id)];
ChatMessage.create(data);
});
// Item Delete Confirmation
html.find(".item-delete").off("click");
html.find(".item-delete").click((event) => {
let li = $(event.currentTarget).parents(".item");
let itemId = li.attr("data-item-id");
let item = this.actor.items.get(itemId);
new Dialog({
title: `Deleting ${item.data.name}`,
content: `<p>Are you sure you want to delete ${item.data.name}?</p>`,
buttons: {
Yes: {
icon: '<i class="fa fa-check"></i>',
label: "Yes",
callback: (dlg) => {
this.actor.deleteOwnedItem(itemId);
}
},
cancel: {
icon: '<i class="fas fa-times"></i>',
label: "No"
}
},
default: "cancel"
}).render(true);
});
}
/* -------------------------------------------- */
/**
* 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 of 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});
}
}
}
// Increment the number of deployment ranks of a character instead of creating a new item
// else if ( itemData.type === "deployment" ) {
// const rnk = this.actor.itemTypes.deployment.find(c => c.name === itemData.name);
// let priorRank = rnk?.data.data.ranks ?? 0;
// if ( !!rnk ) {
// const next = Math.min(priorLevel + 1, 5 + priorRank - this.actor.data.data.details.rank);
// if ( next > priorRank ) {
// itemData.ranks = next;
// return rnk.update({"data.ranks": next});
// }
// }
// }
// Default drop handling if levels were not added
return super._onDropItemCreate(itemData);
}
}
async function addFavorites(app, html, data) {
// Thisfunction is adapted for the SwaltSheet from the Favorites Item
// Tab Module created for Foundry VTT - by Felix Müller (Felix#6196 on Discord).
// It is licensed under a Creative Commons Attribution 4.0 International License
// and can be found at https://github.com/syl3r86/favtab.
let favItems = [];
let favFeats = [];
let favPowers = {
0: {
isCantrip: true,
powers: []
},
1: {
powers: [],
value: data.actor.data.powers.power1.value,
max: data.actor.data.powers.power1.max
},
2: {
powers: [],
value: data.actor.data.powers.power2.value,
max: data.actor.data.powers.power2.max
},
3: {
powers: [],
value: data.actor.data.powers.power3.value,
max: data.actor.data.powers.power3.max
},
4: {
powers: [],
value: data.actor.data.powers.power4.value,
max: data.actor.data.powers.power4.max
},
5: {
powers: [],
value: data.actor.data.powers.power5.value,
max: data.actor.data.powers.power5.max
},
6: {
powers: [],
value: data.actor.data.powers.power6.value,
max: data.actor.data.powers.power6.max
},
7: {
powers: [],
value: data.actor.data.powers.power7.value,
max: data.actor.data.powers.power7.max
},
8: {
powers: [],
value: data.actor.data.powers.power8.value,
max: data.actor.data.powers.power8.max
},
9: {
powers: [],
value: data.actor.data.powers.power9.value,
max: data.actor.data.powers.power9.max
}
};
let powerCount = 0;
let items = data.actor.items;
for (let item of items) {
if (item.type == "class") continue;
if (item.flags.favtab === undefined || item.flags.favtab.isFavourite === undefined) {
item.flags.favtab = {
isFavourite: false
};
}
let isFav = item.flags.favtab.isFavourite;
if (app.options.editable) {
let favBtn = $(
`<a class="item-control item-toggle item-fav ${isFav ? "active" : ""}" data-fav="${isFav}" title="${
isFav ? "Remove from Favourites" : "Add to Favourites"
}"><i class="fas fa-star"></i></a>`
);
favBtn.click((ev) => {
app.actor.items.get(item.data._id).update({
"flags.favtab.isFavourite": !item.flags.favtab.isFavourite
});
});
html.find(`.item[data-item-id="${item.data._id}"]`).find(".item-controls").prepend(favBtn);
}
if (isFav) {
item.powerComps = "";
if (item.data.components) {
let comps = item.data.components;
let v = comps.vocal ? "V" : "";
let s = comps.somatic ? "S" : "";
let m = comps.material ? "M" : "";
let c = !!comps.concentration;
let r = !!comps.ritual;
item.powerComps = `${v}${s}${m}`;
item.powerCon = c;
item.powerRit = r;
}
item.editable = app.options.editable;
switch (item.type) {
case "feat":
if (item.flags.favtab.sort === undefined) {
item.flags.favtab.sort = (favFeats.count + 1) * 100000; // initial sort key if not present
}
favFeats.push(item);
break;
case "power":
if (item.data.preparation.mode) {
item.powerPrepMode = ` (${CONFIG.SW5E.powerPreparationModes[item.data.preparation.mode]})`;
}
if (item.data.level) {
favPowers[item.data.level].powers.push(item);
} else {
favPowers[0].powers.push(item);
}
powerCount++;
break;
default:
if (item.flags.favtab.sort === undefined) {
item.flags.favtab.sort = (favItems.count + 1) * 100000; // initial sort key if not present
}
favItems.push(item);
break;
}
}
}
// Alter core CSS to fit new button
// if (app.options.editable) {
// html.find('.powerbook .item-controls').css('flex', '0 0 88px');
// html.find('.inventory .item-controls, .features .item-controls').css('flex', '0 0 90px');
// html.find('.favourite .item-controls').css('flex', '0 0 22px');
// }
let tabContainer = html.find(".favtabtarget");
data.favItems = favItems.length > 0 ? favItems.sort((a, b) => a.flags.favtab.sort - b.flags.favtab.sort) : false;
data.favFeats = favFeats.length > 0 ? favFeats.sort((a, b) => a.flags.favtab.sort - b.flags.favtab.sort) : false;
data.favPowers = powerCount > 0 ? favPowers : false;
data.editable = app.options.editable;
await loadTemplates(["systems/sw5e/templates/actors/newActor/item.hbs"]);
let favtabHtml = $(await renderTemplate("systems/sw5e/templates/actors/newActor/template.hbs", data));
favtabHtml.find(".item-name h4").click((event) => app._onItemSummary(event));
if (app.options.editable) {
favtabHtml.find(".item-image").click((ev) => app._onItemRoll(ev));
let handler = (ev) => app._onDragStart(ev);
favtabHtml.find(".item").each((i, li) => {
if (li.classList.contains("inventory-header")) return;
li.setAttribute("draggable", true);
li.addEventListener("dragstart", handler, false);
});
//favtabHtml.find('.item-toggle').click(event => app._onToggleItem(event));
favtabHtml.find(".item-edit").click((ev) => {
let itemId = $(ev.target).parents(".item")[0].dataset.itemId;
app.actor.items.get(itemId).sheet.render(true);
});
favtabHtml.find(".item-fav").click((ev) => {
let itemId = $(ev.target).parents(".item")[0].dataset.itemId;
let val = !app.actor.items.get(itemId).data.flags.favtab.isFavourite;
app.actor.items.get(itemId).update({
"flags.favtab.isFavourite": val
});
});
// Sorting
favtabHtml.find(".item").on("drop", (ev) => {
ev.preventDefault();
ev.stopPropagation();
let dropData = JSON.parse(ev.originalEvent.dataTransfer.getData("text/plain"));
// if (dropData.actorId !== app.actor.id || dropData.data.type === 'power') return;
if (dropData.actorId !== app.actor.id) return;
let list = null;
if (dropData.data.type === "feat") list = favFeats;
else list = favItems;
let dragSource = list.find((i) => i.data._id === dropData.data._id);
let siblings = list.filter((i) => i.data._id !== dropData.data._id);
let targetId = ev.target.closest(".item").dataset.itemId;
let dragTarget = siblings.find((s) => s.data._id === targetId);
if (dragTarget === undefined) return;
const sortUpdates = SortingHelpers.performIntegerSort(dragSource, {
target: dragTarget,
siblings: siblings,
sortKey: "flags.favtab.sort"
});
const updateData = sortUpdates.map((u) => {
const update = u.update;
update._id = u.target.data._id;
return update;
});
app.actor.updateEmbeddedEntity("OwnedItem", updateData);
});
}
tabContainer.append(favtabHtml);
// if(app.options.editable) {
// let handler = ev => app._onDragItemStart(ev);
// tabContainer.find('.item').each((i, li) => {
// if (li.classList.contains("inventory-header")) return;
// li.setAttribute("draggable", true);
// li.addEventListener("dragstart", handler, false);
// });
//}
// try {
// if (game.modules.get("betterrolls5e") && game.modules.get("betterrolls5e").active) BetterRolls.addItemContent(app.object, favtabHtml, ".item .item-name h4", ".item-properties", ".item > .rollable div");
// }
// catch (err) {
// // Better Rolls not found!
// }
Hooks.callAll("renderedSwaltSheet", app, html, data);
}
async function addSubTabs(app, html, data) {
if (data.options.subTabs == null) {
//let subTabs = []; //{subgroup: '', target: '', active: false}
data.options.subTabs = {};
html.find("[data-subgroup-selection] [data-subgroup]").each((idx, el) => {
let subgroup = el.getAttribute("data-subgroup");
let target = el.getAttribute("data-target");
let targetObj = {target: target, active: el.classList.contains("active")};
if (data.options.subTabs.hasOwnProperty(subgroup)) {
data.options.subTabs[subgroup].push(targetObj);
} else {
data.options.subTabs[subgroup] = [];
data.options.subTabs[subgroup].push(targetObj);
}
});
}
for (const group in data.options.subTabs) {
data.options.subTabs[group].forEach((tab) => {
if (tab.active) {
html.find(`[data-subgroup=${group}][data-target=${tab.target}]`).addClass("active");
} else {
html.find(`[data-subgroup=${group}][data-target=${tab.target}]`).removeClass("active");
}
});
}
html.find("[data-subgroup-selection]")
.children()
.on("click", (event) => {
let subgroup = event.target.closest("[data-subgroup]").getAttribute("data-subgroup");
let target = event.target.closest("[data-target]").getAttribute("data-target");
html.find(`[data-subgroup=${subgroup}]`).removeClass("active");
html.find(`[data-subgroup=${subgroup}][data-target=${target}]`).addClass("active");
let tabId = data.options.subTabs[subgroup].find((tab) => {
return tab.target == target;
});
data.options.subTabs[subgroup].map((el) => {
el.active = el.target == target;
return el;
});
});
}
Hooks.on("renderActorSheet5eCharacterNew", (app, html, data) => {
addFavorites(app, html, data);
addSubTabs(app, html, data);
});

View file

@ -0,0 +1,159 @@
import ActorSheet5e from "./base.js";
/**
* An Actor sheet for NPC type characters in the SW5E system.
* Extends the base ActorSheet5e class.
* @extends {ActorSheet5e}
*/
export default class ActorSheet5eNPCNew 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/npc-sheet.html`;
}
/** @override */
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
classes: ["sw5e", "sheet", "actor", "npc"],
width: 800,
tabs: [
{
navSelector: ".root-tabs",
contentSelector: ".sheet-body",
initial: "attributes"
}
]
});
}
/* -------------------------------------------- */
/** @override */
static unsupportedItemTypes = new Set(["class"]);
/* -------------------------------------------- */
/**
* 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 || 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 features.equipment.items.push(item);
}
// Assign and return
data.features = Object.values(features);
data.forcePowerbook = forcePowerbook;
data.techPowerbook = techPowerbook;
}
/* -------------------------------------------- */
/** @inheritdoc */
getData(options) {
const data = super.getData(options);
// 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
data.labels["type"] = this.actor.labels.creatureType;
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

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

@ -0,0 +1,432 @@
import ActorSheet5e from "./base.js";
/**
* An Actor sheet for Vehicle type actors.
* Extends the base ActorSheet5e class.
* @type {ActorSheet5e}
*/
export default class ActorSheet5eVehicle extends ActorSheet5e {
/**
* Define default rendering options for the Vehicle sheet.
* @returns {Object}
*/
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
classes: ["sw5e", "sheet", "actor", "vehicle"],
width: 605,
height: 680
});
}
/* -------------------------------------------- */
/** @override */
static unsupportedItemTypes = new Set(["class"]);
/* -------------------------------------------- */
/**
* 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 === 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 : "—";
}
}
/* -------------------------------------------- */
/**
* 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"
}
];
const equipmentColumns = [
{
label: game.i18n.localize("SW5E.Quantity"),
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 = {
actions: {
label: game.i18n.localize("SW5E.ActionPl"),
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
}
};
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"
}
]
}
};
// Classify items owned by the vehicle and compute total cargo weight
let totalWeight = 0;
for (const item of data.items) {
this._prepareCrewedItem(item);
// Handle cargo explicitly
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
data.features = Object.values(features);
data.cargo = Object.values(cargo);
data.data.attributes.encumbrance = this._computeEncumbrance(totalWeight, data);
}
/* -------------------------------------------- */
/* Event Listeners and Handlers */
/* -------------------------------------------- */
/** @override */
activateListeners(html) {
super.activateListeners(html);
if (!this.isEditable) return;
html.find(".item-toggle").click(this._onToggleItem.bind(this));
html.find(".item-hp input")
.click((evt) => evt.target.select())
.change(this._onHPChange.bind(this));
html.find(".item:not(.cargo-row) input[data-property]")
.click((evt) => evt.target.select())
.change(this._onEditInSheet.bind(this));
html.find(".cargo-row input")
.click((evt) => evt.target.select())
.change(this._onCargoRowChange.bind(this));
if (this.actor.data.data.attributes.actions.stations) {
html.find(".counter.actions, .counter.action-thresholds").hide();
}
}
/* -------------------------------------------- */
/**
* 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

@ -1,127 +0,0 @@
import ActorSheet5e from "../sheets/base.js";
/**
* An Actor sheet for NPC type characters in the SW5E system.
* Extends the base ActorSheet5e class.
* @extends {ActorSheet5e}
*/
export default class ActorSheet5eNPC extends ActorSheet5e {
/** @override */
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
classes: ["sw5e", "sheet", "actor", "npc"],
width: 600,
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 */
getData() {
const data = super.getData();
// 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 */
_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
super._updateObject(event, formData);
}
/* -------------------------------------------- */
/* Event Listeners and Handlers */
/* -------------------------------------------- */
/** @override */
activateListeners(html) {
super.activateListeners(html);
html.find(".health .rollable").click(this._onRollHealthFormula.bind(this));
}
/* -------------------------------------------- */
/**
* Handle rolling NPC health values using the provided formula
* @param {Event} event The original click event
* @private
*/
_onRollHealthFormula(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,920 @@
import Item5e from "../../../item/entity.js";
import TraitSelector from "../../../apps/trait-selector.js";
import ActorSheetFlags from "../../../apps/actor-flags.js";
import ActorHitDiceConfig from "../../../apps/hit-dice-config.js";
import ActorMovementConfig from "../../../apps/movement-config.js";
import ActorSensesConfig from "../../../apps/senses-config.js";
import ActorTypeConfig from "../../../apps/actor-type.js";
import {SW5E} from "../../../config.js";
import {onManageActiveEffect, prepareActiveEffectCategories} from "../../../effects.js";
/**
* Extend the basic ActorSheet class to suppose SW5e-specific logic and functionality.
* This sheet is an Abstract layer which is not used.
* @extends {ActorSheet}
*/
export default class ActorSheet5e extends ActorSheet {
constructor(...args) {
super(...args);
/**
* Track the set of item filters which are applied
* @type {Set}
*/
this._filters = {
inventory: new Set(),
powerbook: new Set(),
features: new Set(),
effects: new Set()
};
}
/* -------------------------------------------- */
/** @override */
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
scrollY: [
".inventory .inventory-list",
".features .inventory-list",
".powerbook .inventory-list",
".effects .inventory-list"
],
tabs: [{navSelector: ".tabs", contentSelector: ".sheet-body", initial: "description"}]
});
}
/* -------------------------------------------- */
/**
* A set of item types that should be prevented from being dropped on this type of actor sheet.
* @type {Set<string>}
*/
static unsupportedItemTypes = new Set();
/* -------------------------------------------- */
/** @override */
get template() {
if (!game.user.isGM && this.actor.limited) return "systems/sw5e/templates/actors/oldActor/limited-sheet.html";
return `systems/sw5e/templates/actors/oldActor/${this.actor.data.type}-sheet.html`;
}
/* -------------------------------------------- */
/** @override */
getData(options) {
// Basic data
let isOwner = this.actor.isOwner;
const data = {
owner: isOwner,
limited: this.actor.limited,
options: this.options,
editable: this.isEditable,
cssClass: isOwner ? "editable" : "locked",
isCharacter: this.actor.type === "character",
isNPC: this.actor.type === "npc",
isStarship: this.actor.type === "starship",
isVehicle: this.actor.type === "vehicle",
config: CONFIG.SW5E,
rollData: this.actor.getRollData.bind(this.actor)
};
// The Actor's data
const actorData = this.actor.data.toObject(false);
data.actor = actorData;
data.data = actorData.data;
// Owned Items
data.items = actorData.items;
for (let i of data.items) {
const item = this.actor.items.get(i._id);
i.labels = item.labels;
}
data.items.sort((a, b) => (a.sort || 0) - (b.sort || 0));
// Labels and filters
data.labels = this.actor.labels || {};
data.filters = this._filters;
// Ability Scores
for (let [a, abl] of Object.entries(actorData.data.abilities)) {
abl.icon = this._getProficiencyIcon(abl.proficient);
abl.hover = CONFIG.SW5E.proficiencyLevels[abl.proficient];
abl.label = CONFIG.SW5E.abilities[a];
}
// Skills
if (actorData.data.skills) {
for (let [s, skl] of Object.entries(actorData.data.skills)) {
skl.ability = CONFIG.SW5E.abilityAbbreviations[skl.ability];
skl.icon = this._getProficiencyIcon(skl.value);
skl.hover = CONFIG.SW5E.proficiencyLevels[skl.value];
skl.label = CONFIG.SW5E.skills[s];
}
}
// Movement speeds
data.movement = this._getMovementSpeed(actorData);
// Senses
data.senses = this._getSenses(actorData);
// Update traits
this._prepareTraits(actorData.data.traits);
// Prepare owned items
this._prepareItems(data);
// Prepare active effects
data.effects = prepareActiveEffectCategories(this.actor.effects);
// Return data to the sheet
return data;
}
/* -------------------------------------------- */
/**
* Prepare the display of movement speed data for the Actor*
* @param {object} actorData The Actor data being prepared.
* @param {boolean} [largestPrimary=false] Show the largest movement speed as "primary", otherwise show "walk"
* @returns {{primary: string, special: string}}
* @private
*/
_getMovementSpeed(actorData, largestPrimary = false) {
const movement = actorData.data.attributes.movement || {};
// Prepare an array of available movement speeds
let speeds = [
[movement.burrow, `${game.i18n.localize("SW5E.MovementBurrow")} ${movement.burrow}`],
[movement.climb, `${game.i18n.localize("SW5E.MovementClimb")} ${movement.climb}`],
[
movement.fly,
`${game.i18n.localize("SW5E.MovementFly")} ${movement.fly}` +
(movement.hover ? ` (${game.i18n.localize("SW5E.MovementHover")})` : "")
],
[movement.swim, `${game.i18n.localize("SW5E.MovementSwim")} ${movement.swim}`]
];
if (largestPrimary) {
speeds.push([movement.walk, `${game.i18n.localize("SW5E.MovementWalk")} ${movement.walk}`]);
}
// Filter and sort speeds on their values
speeds = speeds.filter((s) => !!s[0]).sort((a, b) => b[0] - a[0]);
// Case 1: Largest as primary
if (largestPrimary) {
let primary = speeds.shift();
return {
primary: `${primary ? primary[1] : "0"} ${movement.units}`,
special: speeds.map((s) => s[1]).join(", ")
};
}
// Case 2: Walk as primary
else {
return {
primary: `${movement.walk || 0} ${movement.units}`,
special: speeds.length ? speeds.map((s) => s[1]).join(", ") : ""
};
}
}
/* -------------------------------------------- */
_getSenses(actorData) {
const senses = actorData.data.attributes.senses || {};
const tags = {};
for (let [k, label] of Object.entries(CONFIG.SW5E.senses)) {
const v = senses[k] ?? 0;
if (v === 0) continue;
tags[k] = `${game.i18n.localize(label)} ${v} ${senses.units}`;
}
if (!!senses.special) tags["special"] = senses.special;
return tags;
}
/* -------------------------------------------- */
/**
* Prepare the data structure for traits data like languages, resistances & vulnerabilities, and proficiencies
* @param {object} traits The raw traits data object from the actor data
* @private
*/
_prepareTraits(traits) {
const map = {
dr: CONFIG.SW5E.damageResistanceTypes,
di: CONFIG.SW5E.damageResistanceTypes,
dv: CONFIG.SW5E.damageResistanceTypes,
ci: CONFIG.SW5E.conditionTypes,
languages: CONFIG.SW5E.languages,
armorProf: CONFIG.SW5E.armorProficiencies,
weaponProf: CONFIG.SW5E.weaponProficiencies,
toolProf: CONFIG.SW5E.toolProficiencies
};
for (let [t, choices] of Object.entries(map)) {
const trait = traits[t];
if (!trait) continue;
let values = [];
if (trait.value) {
values = trait.value instanceof Array ? trait.value : [trait.value];
}
trait.selected = values.reduce((obj, t) => {
obj[t] = choices[t];
return obj;
}, {});
// Add custom entry
if (trait.custom) {
trait.custom.split(";").forEach((c, i) => (trait.selected[`custom${i + 1}`] = c.trim()));
}
trait.cssClass = !isObjectEmpty(trait.selected) ? "" : "inactive";
}
}
/* -------------------------------------------- */
/**
* Insert a power into the powerbook object when rendering the character sheet
* @param {Object} data The Actor data being prepared
* @param {Array} powers The power data being prepared
* @private
*/
_preparePowerbook(data, powers) {
const owner = this.actor.isOwner;
const levels = data.data.powers;
const powerbook = {};
// Define some mappings
const sections = {
atwill: -20,
innate: -10,
pact: 0.5
};
// Label power slot uses headers
const useLabels = {
"-20": "-",
"-10": "-",
"0": "&infin;"
};
// Format a powerbook entry for a certain indexed level
const registerSection = (sl, i, label, {prepMode = "prepared", value, max, override} = {}) => {
powerbook[i] = {
order: i,
label: label,
usesSlots: i > 0,
canCreate: owner,
canPrepare: data.actor.type === "character" && i >= 1,
powers: [],
uses: useLabels[i] || value || 0,
slots: useLabels[i] || max || 0,
override: override || 0,
dataset: {"type": "power", "level": prepMode in sections ? 1 : i, "preparation.mode": prepMode},
prop: sl
};
};
// Determine the maximum power level which has a slot
const maxLevel = Array.fromRange(10).reduce((max, i) => {
if (i === 0) return max;
const level = levels[`power${i}`];
if ((level.max || level.override) && i > max) max = i;
return max;
}, 0);
// Level-based powercasters have cantrips and leveled slots
if (maxLevel > 0) {
registerSection("power0", 0, CONFIG.SW5E.powerLevels[0]);
for (let lvl = 1; lvl <= maxLevel; lvl++) {
const sl = `power${lvl}`;
registerSection(sl, lvl, CONFIG.SW5E.powerLevels[lvl], levels[sl]);
}
}
// Pact magic users have cantrips and a pact magic section
// TODO: Check if this is needed, we've removed pacts everywhere else
if (levels.pact && levels.pact.max) {
if (!powerbook["0"]) registerSection("power0", 0, CONFIG.SW5E.powerLevels[0]);
const l = levels.pact;
const config = CONFIG.SW5E.powerPreparationModes.pact;
const level = game.i18n.localize(`SW5E.PowerLevel${levels.pact.level}`);
const label = `${config}${level}`;
registerSection("pact", sections.pact, label, {
prepMode: "pact",
value: l.value,
max: l.max,
override: l.override
});
}
// Iterate over every power item, adding powers to the powerbook by section
powers.forEach((power) => {
const mode = power.data.preparation.mode || "prepared";
let s = power.data.level || 0;
const sl = `power${s}`;
// Specialized powercasting modes (if they exist)
if (mode in sections) {
s = sections[mode];
if (!powerbook[s]) {
const l = levels[mode] || {};
const config = CONFIG.SW5E.powerPreparationModes[mode];
registerSection(mode, s, config, {
prepMode: mode,
value: l.value,
max: l.max,
override: l.override
});
}
}
// Sections for higher-level powers which the caster "should not" have, but power items exist for
else if (!powerbook[s]) {
registerSection(sl, s, CONFIG.SW5E.powerLevels[s], {levels: levels[sl]});
}
// Add the power to the relevant heading
powerbook[s].powers.push(power);
});
// Sort the powerbook by section level
const sorted = Object.values(powerbook);
sorted.sort((a, b) => a.order - b.order);
return sorted;
}
/* -------------------------------------------- */
/**
* Determine whether an Owned Item will be shown based on the current set of filters
* @return {boolean}
* @private
*/
_filterItems(items, filters) {
return items.filter((item) => {
const data = item.data;
// Action usage
for (let f of ["action", "bonus", "reaction"]) {
if (filters.has(f)) {
if (data.activation && data.activation.type !== f) return false;
}
}
// Power-specific filters
if (filters.has("ritual")) {
if (data.components.ritual !== true) return false;
}
if (filters.has("concentration")) {
if (data.components.concentration !== true) return false;
}
if (filters.has("prepared")) {
if (data.level === 0 || ["innate", "always"].includes(data.preparation.mode)) return true;
if (this.actor.data.type === "npc") return true;
return data.preparation.prepared;
}
// Equipment-specific filters
if (filters.has("equipped")) {
if (data.equipped !== true) return false;
}
return true;
});
}
/* -------------------------------------------- */
/**
* Get the font-awesome icon used to display a certain level of skill proficiency
* @private
*/
_getProficiencyIcon(level) {
const icons = {
0: '<i class="far fa-circle"></i>',
0.5: '<i class="fas fa-adjust"></i>',
1: '<i class="fas fa-check"></i>',
2: '<i class="fas fa-check-double"></i>'
};
return icons[level] || icons[0];
}
/* -------------------------------------------- */
/* Event Listeners and Handlers
/* -------------------------------------------- */
/** @inheritdoc */
activateListeners(html) {
// Activate Item Filters
const filterLists = html.find(".filter-list");
filterLists.each(this._initializeFilterItemList.bind(this));
filterLists.on("click", ".filter-item", this._onToggleFilter.bind(this));
// Item summaries
html.find(".item .item-name.rollable h4").click((event) => this._onItemSummary(event));
// View Item Sheets
html.find(".item-edit").click(this._onItemEdit.bind(this));
// Editable Only Listeners
if (this.isEditable) {
// Input focus and update
const inputs = html.find("input");
inputs.focus((ev) => ev.currentTarget.select());
inputs.addBack().find('[data-dtype="Number"]').change(this._onChangeInputDelta.bind(this));
// Ability Proficiency
html.find(".ability-proficiency").click(this._onToggleAbilityProficiency.bind(this));
// Toggle Skill Proficiency
html.find(".skill-proficiency").on("click contextmenu", this._onCycleSkillProficiency.bind(this));
// Trait Selector
html.find(".trait-selector").click(this._onTraitSelector.bind(this));
// Configure Special Flags
html.find(".config-button").click(this._onConfigMenu.bind(this));
// Owned Item management
html.find(".item-create").click(this._onItemCreate.bind(this));
html.find(".item-delete").click(this._onItemDelete.bind(this));
html.find(".item-uses input")
.click((ev) => ev.target.select())
.change(this._onUsesChange.bind(this));
html.find(".slot-max-override").click(this._onPowerSlotOverride.bind(this));
// Active Effect management
html.find(".effect-control").click((ev) => onManageActiveEffect(ev, this.actor));
}
// Owner Only Listeners
if (this.actor.isOwner) {
// Ability Checks
html.find(".ability-name").click(this._onRollAbilityTest.bind(this));
// Roll Skill Checks
html.find(".skill-name").click(this._onRollSkillCheck.bind(this));
// Item Rolling
html.find(".item .item-image").click((event) => this._onItemRoll(event));
html.find(".item .item-recharge").click((event) => this._onItemRecharge(event));
}
// Otherwise remove rollable classes
else {
html.find(".rollable").each((i, el) => el.classList.remove("rollable"));
}
// Handle default listeners last so system listeners are triggered first
super.activateListeners(html);
}
/* -------------------------------------------- */
/**
* Iinitialize Item list filters by activating the set of filters which are currently applied
* @private
*/
_initializeFilterItemList(i, ul) {
const set = this._filters[ul.dataset.filter];
const filters = ul.querySelectorAll(".filter-item");
for (let li of filters) {
if (set.has(li.dataset.filter)) li.classList.add("active");
}
}
/* -------------------------------------------- */
/* Event Listeners and Handlers */
/* -------------------------------------------- */
/**
* Handle input changes to numeric form fields, allowing them to accept delta-typed inputs
* @param event
* @private
*/
_onChangeInputDelta(event) {
const input = event.target;
const value = input.value;
if (["+", "-"].includes(value[0])) {
let delta = parseFloat(value);
input.value = getProperty(this.actor.data, input.name) + delta;
} else if (value[0] === "=") {
input.value = value.slice(1);
}
}
/* -------------------------------------------- */
/**
* Handle spawning the TraitSelector application which allows a checkbox of multiple trait options
* @param {Event} event The click event which originated the selection
* @private
*/
_onConfigMenu(event) {
event.preventDefault();
const button = event.currentTarget;
let app;
switch (button.dataset.action) {
case "hit-dice":
app = new ActorHitDiceConfig(this.object);
break;
case "movement":
app = new ActorMovementConfig(this.object);
break;
case "flags":
app = new ActorSheetFlags(this.object);
break;
case "senses":
app = new ActorSensesConfig(this.object);
break;
case "type":
new ActorTypeConfig(this.object).render(true);
break;
}
app?.render(true);
}
/* -------------------------------------------- */
/**
* Handle cycling proficiency in a Skill
* @param {Event} event A click or contextmenu event which triggered the handler
* @private
*/
_onCycleSkillProficiency(event) {
event.preventDefault();
const field = $(event.currentTarget).siblings('input[type="hidden"]');
// Get the current level and the array of levels
const level = parseFloat(field.val());
const levels = [0, 1, 0.5, 2];
let idx = levels.indexOf(level);
// Toggle next level - forward on click, backwards on right
if (event.type === "click") {
field.val(levels[idx === levels.length - 1 ? 0 : idx + 1]);
} else if (event.type === "contextmenu") {
field.val(levels[idx === 0 ? levels.length - 1 : idx - 1]);
}
// Update the field value and save the form
this._onSubmit(event);
}
/* -------------------------------------------- */
/** @override */
async _onDropActor(event, data) {
const canPolymorph = game.user.isGM || (this.actor.isOwner && game.settings.get("sw5e", "allowPolymorphing"));
if (!canPolymorph) return false;
// Get the target actor
let sourceActor = null;
if (data.pack) {
const pack = game.packs.find((p) => p.collection === data.pack);
sourceActor = await pack.getEntity(data.id);
} else {
sourceActor = game.actors.get(data.id);
}
if (!sourceActor) return;
// Define a function to record polymorph settings for future use
const rememberOptions = (html) => {
const options = {};
html.find("input").each((i, el) => {
options[el.name] = el.checked;
});
const settings = mergeObject(game.settings.get("sw5e", "polymorphSettings") || {}, options);
game.settings.set("sw5e", "polymorphSettings", settings);
return settings;
};
// Create and render the Dialog
return new Dialog(
{
title: game.i18n.localize("SW5E.PolymorphPromptTitle"),
content: {
options: game.settings.get("sw5e", "polymorphSettings"),
i18n: SW5E.polymorphSettings,
isToken: this.actor.isToken
},
default: "accept",
buttons: {
accept: {
icon: '<i class="fas fa-check"></i>',
label: game.i18n.localize("SW5E.PolymorphAcceptSettings"),
callback: (html) => this.actor.transformInto(sourceActor, rememberOptions(html))
},
wildshape: {
icon: '<i class="fas fa-paw"></i>',
label: game.i18n.localize("SW5E.PolymorphWildShape"),
callback: (html) =>
this.actor.transformInto(sourceActor, {
keepBio: true,
keepClass: true,
keepMental: true,
mergeSaves: true,
mergeSkills: true,
transformTokens: rememberOptions(html).transformTokens
})
},
polymorph: {
icon: '<i class="fas fa-pastafarianism"></i>',
label: game.i18n.localize("SW5E.Polymorph"),
callback: (html) =>
this.actor.transformInto(sourceActor, {
transformTokens: rememberOptions(html).transformTokens
})
},
cancel: {
icon: '<i class="fas fa-times"></i>',
label: game.i18n.localize("Cancel")
}
}
},
{
classes: ["dialog", "sw5e"],
width: 600,
template: "systems/sw5e/templates/apps/polymorph-prompt.html"
}
).render(true);
}
/* -------------------------------------------- */
/** @override */
async _onDropItemCreate(itemData) {
// Check to make sure items of this type are allowed on this actor
if (this.constructor.unsupportedItemTypes.has(itemData.type)) {
return ui.notifications.warn(
game.i18n.format("SW5E.ActorWarningInvalidItem", {
itemType: game.i18n.localize(CONFIG.Item.typeLabels[itemData.type]),
actorType: game.i18n.localize(CONFIG.Actor.typeLabels[this.actor.type])
})
);
}
// Create a Consumable power scroll on the Inventory tab
// TODO: This is pretty non functional as the base items for the scrolls, and the powers, are not defined, maybe consider using holocrons
if (itemData.type === "power" && this._tabs[0].active === "inventory") {
const scroll = await Item5e.createScrollFromPower(itemData);
itemData = scroll.data;
}
if (itemData.data) {
// Ignore certain statuses
["equipped", "proficient", "prepared"].forEach((k) => delete itemData.data[k]);
// Downgrade ATTUNED to REQUIRED
itemData.data.attunement = Math.min(itemData.data.attunement, CONFIG.SW5E.attunementTypes.REQUIRED);
}
// Stack identical consumables
if (itemData.type === "consumable" && itemData.flags.core?.sourceId) {
const similarItem = this.actor.items.find((i) => {
const sourceId = i.getFlag("core", "sourceId");
return sourceId && sourceId === itemData.flags.core?.sourceId && i.type === "consumable";
});
if (similarItem) {
return similarItem.update({
"data.quantity": similarItem.data.data.quantity + Math.max(itemData.data.quantity, 1)
});
}
}
// Create the owned item as normal
return super._onDropItemCreate(itemData);
}
/* -------------------------------------------- */
/**
* Handle enabling editing for a power slot override value
* @param {MouseEvent} event The originating click event
* @private
*/
async _onPowerSlotOverride(event) {
const span = event.currentTarget.parentElement;
const level = span.dataset.level;
const override = this.actor.data.data.powers[level].override || span.dataset.slots;
const input = document.createElement("INPUT");
input.type = "text";
input.name = `data.powers.${level}.override`;
input.value = override;
input.placeholder = span.dataset.slots;
input.dataset.dtype = "Number";
// Replace the HTML
const parent = span.parentElement;
parent.removeChild(span);
parent.appendChild(input);
}
/* -------------------------------------------- */
/**
* Change the uses amount of an Owned Item within the Actor
* @param {Event} event The triggering click event
* @private
*/
async _onUsesChange(event) {
event.preventDefault();
const itemId = event.currentTarget.closest(".item").dataset.itemId;
const item = this.actor.items.get(itemId);
const uses = Math.clamped(0, parseInt(event.target.value), item.data.data.uses.max);
event.target.value = uses;
return item.update({"data.uses.value": uses});
}
/* -------------------------------------------- */
/**
* Handle rolling of an item from the Actor sheet, obtaining the Item instance and dispatching to it's roll method
* @private
*/
_onItemRoll(event) {
event.preventDefault();
const itemId = event.currentTarget.closest(".item").dataset.itemId;
const item = this.actor.items.get(itemId);
return item.roll();
}
/* -------------------------------------------- */
/**
* Handle attempting to recharge an item usage by rolling a recharge check
* @param {Event} event The originating click event
* @private
*/
_onItemRecharge(event) {
event.preventDefault();
const itemId = event.currentTarget.closest(".item").dataset.itemId;
const item = this.actor.items.get(itemId);
return item.rollRecharge();
}
/* -------------------------------------------- */
/**
* Handle rolling of an item from the Actor sheet, obtaining the Item instance and dispatching to it's roll method
* @private
*/
_onItemSummary(event) {
event.preventDefault();
let li = $(event.currentTarget).parents(".item"),
item = this.actor.items.get(li.data("item-id")),
chatData = item.getChatData({secrets: this.actor.isOwner});
// Toggle summary
if (li.hasClass("expanded")) {
let summary = li.children(".item-summary");
summary.slideUp(200, () => summary.remove());
} else {
let div = $(`<div class="item-summary">${chatData.description.value}</div>`);
let props = $(`<div class="item-properties"></div>`);
chatData.properties.forEach((p) => props.append(`<span class="tag">${p}</span>`));
div.append(props);
li.append(div.hide());
div.slideDown(200);
}
li.toggleClass("expanded");
}
/* -------------------------------------------- */
/**
* Handle creating a new Owned Item for the actor using initial data defined in the HTML dataset
* @param {Event} event The originating click event
* @private
*/
_onItemCreate(event) {
event.preventDefault();
const header = event.currentTarget;
const type = header.dataset.type;
const itemData = {
name: game.i18n.format("SW5E.ItemNew", {type: game.i18n.localize(`SW5E.ItemType${type.capitalize()}`)}),
type: type,
data: foundry.utils.deepClone(header.dataset)
};
delete itemData.data["type"];
return this.actor.createEmbeddedDocuments("Item", [itemData]);
}
/* -------------------------------------------- */
/**
* Handle editing an existing Owned Item for the Actor
* @param {Event} event The originating click event
* @private
*/
_onItemEdit(event) {
event.preventDefault();
const li = event.currentTarget.closest(".item");
const item = this.actor.items.get(li.dataset.itemId);
return item.sheet.render(true);
}
/* -------------------------------------------- */
/**
* Handle deleting an existing Owned Item for the Actor
* @param {Event} event The originating click event
* @private
*/
_onItemDelete(event) {
event.preventDefault();
const li = event.currentTarget.closest(".item");
const item = this.actor.items.get(li.dataset.itemId);
if (item) return item.delete();
}
/* -------------------------------------------- */
/**
* Handle rolling an Ability check, either a test or a saving throw
* @param {Event} event The originating click event
* @private
*/
_onRollAbilityTest(event) {
event.preventDefault();
let ability = event.currentTarget.parentElement.dataset.ability;
return this.actor.rollAbility(ability, {event: event});
}
/* -------------------------------------------- */
/**
* Handle rolling a Skill check
* @param {Event} event The originating click event
* @private
*/
_onRollSkillCheck(event) {
event.preventDefault();
const skill = event.currentTarget.parentElement.dataset.skill;
return this.actor.rollSkill(skill, {event: event});
}
/* -------------------------------------------- */
/**
* Handle toggling Ability score proficiency level
* @param {Event} event The originating click event
* @private
*/
_onToggleAbilityProficiency(event) {
event.preventDefault();
const field = event.currentTarget.previousElementSibling;
return this.actor.update({[field.name]: 1 - parseInt(field.value)});
}
/* -------------------------------------------- */
/**
* Handle toggling of filters to display a different set of owned items
* @param {Event} event The click event which triggered the toggle
* @private
*/
_onToggleFilter(event) {
event.preventDefault();
const li = event.currentTarget;
const set = this._filters[li.parentElement.dataset.filter];
const filter = li.dataset.filter;
if (set.has(filter)) set.delete(filter);
else set.add(filter);
return this.render();
}
/* -------------------------------------------- */
/**
* Handle spawning the TraitSelector application which allows a checkbox of multiple trait options
* @param {Event} event The click event which originated the selection
* @private
*/
_onTraitSelector(event) {
event.preventDefault();
const a = event.currentTarget;
const label = a.parentElement.querySelector("label");
const choices = CONFIG.SW5E[a.dataset.options];
const options = {name: a.dataset.target, title: label.innerText, choices};
return new TraitSelector(this.actor, options).render(true);
}
/* -------------------------------------------- */
/** @override */
_getHeaderButtons() {
let buttons = super._getHeaderButtons();
if (this.actor.isPolymorphed) {
buttons.unshift({
label: "SW5E.PolymorphRestoreTransformation",
class: "restore-transformation",
icon: "fas fa-backward",
onclick: () => this.actor.revertOriginalForm()
});
}
return buttons;
}
}

View file

@ -0,0 +1,368 @@
import ActorSheet5e from "./base.js";
import Actor5e from "../../entity.js";
/**
* An Actor sheet for player character type actors in the SW5E system.
* Extends the base ActorSheet5e class.
* @type {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
});
}
/* -------------------------------------------- */
/**
* 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
let hp = sheetData.data.attributes.hp;
if (hp.temp === 0) delete hp.temp;
if (hp.tempmax === 0) delete hp.tempmax;
// Resources
sheetData["resources"] = ["primary", "secondary", "tertiary"].reduce((arr, r) => {
const res = sheetData.data.resources[r] || {};
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]);
}, []);
// Experience Tracking
sheetData["disableExperience"] = game.settings.get("sw5e", "disableExperienceTracking");
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
let [
items,
powers,
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];
// Item usage
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);
// Item toggle state
this._prepareItemToggleState(item);
// Primary Class
if (item.type === "class")
item.isOriginalClass = item._id === this.actor.data.data.details.originalClass;
// Classify items into types
if (item.type === "power") arr[1].push(item);
else if (item.type === "feat") arr[2].push(item);
else if (item.type === "class") arr[3].push(item);
else if (item.type === "species") arr[4].push(item);
else if (item.type === "archetype") arr[5].push(item);
else if (item.type === "classfeature") arr[6].push(item);
else if (item.type === "background") arr[7].push(item);
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);
}
// Organize Powerbook and count the number of prepared powers (excluding always, at will, etc...)
const powerbook = this._preparePowerbook(data, powers);
const nPrepared = powers.filter((s) => {
return s.data.level > 0 && s.data.preparation.mode === "prepared" && s.data.preparation.prepared;
}).length;
// Organize Features
const features = {
classes: {
label: "SW5E.ItemTypeClassPl",
items: [],
hasActions: false,
dataset: {type: "class"},
isClass: true
},
classfeatures: {
label: "SW5E.ItemTypeClassFeats",
items: [],
hasActions: true,
dataset: {type: "classfeature"},
isClassfeature: true
},
archetype: {
label: "SW5E.ItemTypeArchetype",
items: [],
hasActions: false,
dataset: {type: "archetype"},
isArchetype: true
},
species: {
label: "SW5E.ItemTypeSpecies",
items: [],
hasActions: false,
dataset: {type: "species"},
isSpecies: 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: {
label: "SW5E.ItemTypeFightingMasteryPl",
items: [],
hasActions: false,
dataset: {type: "fightingmastery"},
isFightingmastery: true
},
lightsaberforms: {
label: "SW5E.ItemTypeLightsaberFormPl",
items: [],
hasActions: false,
dataset: {type: "lightsaberform"},
isLightsaberform: true
},
active: {
label: "SW5E.FeatureActive",
items: [],
hasActions: true,
dataset: {"type": "feat", "activation.type": "action"}
},
passive: {label: "SW5E.FeaturePassive", items: [], hasActions: false, dataset: {type: "feat"}}
};
for (let f of feats) {
if (f.data.activation.type) features.active.items.push(f);
else features.passive.items.push(f);
}
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);
}
/* -------------------------------------------- */
/**
* 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

@ -0,0 +1,144 @@
import ActorSheet5e from "./base.js";
/**
* An Actor sheet for NPC type characters in the SW5E system.
* Extends the base ActorSheet5e class.
* @extends {ActorSheet5e}
*/
export default class ActorSheet5eNPC extends ActorSheet5e {
/** @override */
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
classes: ["sw5e", "sheet", "actor", "npc"],
width: 600,
height: 680
});
}
/* -------------------------------------------- */
/** @override */
static unsupportedItemTypes = new Set(["class"]);
/* -------------------------------------------- */
/**
* 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 || 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") 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;
}
/* -------------------------------------------- */
/** @inheritdoc */
getData(options) {
const data = super.getData(options);
// 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
data.labels["type"] = this.actor.labels.creatureType;
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

@ -0,0 +1,432 @@
import ActorSheet5e from "./base.js";
/**
* An Actor sheet for Vehicle type actors.
* Extends the base ActorSheet5e class.
* @type {ActorSheet5e}
*/
export default class ActorSheet5eVehicle extends ActorSheet5e {
/**
* Define default rendering options for the Vehicle sheet.
* @returns {Object}
*/
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
classes: ["sw5e", "sheet", "actor", "vehicle"],
width: 605,
height: 680
});
}
/* -------------------------------------------- */
/** @override */
static unsupportedItemTypes = new Set(["class"]);
/* -------------------------------------------- */
/**
* 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 === 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 : "—";
}
}
/* -------------------------------------------- */
/**
* 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"
}
];
const equipmentColumns = [
{
label: game.i18n.localize("SW5E.Quantity"),
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 = {
actions: {
label: game.i18n.localize("SW5E.ActionPl"),
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
}
};
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"
}
]
}
};
// Classify items owned by the vehicle and compute total cargo weight
let totalWeight = 0;
for (const item of data.items) {
this._prepareCrewedItem(item);
// Handle cargo explicitly
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
data.features = Object.values(features);
data.cargo = Object.values(cargo);
data.data.attributes.encumbrance = this._computeEncumbrance(totalWeight, data);
}
/* -------------------------------------------- */
/* Event Listeners and Handlers */
/* -------------------------------------------- */
/** @override */
activateListeners(html) {
super.activateListeners(html);
if (!this.isEditable) return;
html.find(".item-toggle").click(this._onToggleItem.bind(this));
html.find(".item-hp input")
.click((evt) => evt.target.select())
.change(this._onHPChange.bind(this));
html.find(".item:not(.cargo-row) input[data-property]")
.click((evt) => evt.target.select())
.change(this._onEditInSheet.bind(this));
html.find(".cargo-row input")
.click((evt) => evt.target.select())
.change(this._onCargoRowChange.bind(this));
if (this.actor.data.data.attributes.actions.stations) {
html.find(".counter.actions, .counter.action-thresholds").hide();
}
}
/* -------------------------------------------- */
/**
* 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

@ -1,381 +0,0 @@
import ActorSheet5e from "./base.js";
/**
* An Actor sheet for Vehicle type actors.
* Extends the base ActorSheet5e class.
* @type {ActorSheet5e}
*/
export default class ActorSheet5eVehicle extends ActorSheet5e {
/**
* Define default rendering options for the Vehicle sheet.
* @returns {Object}
*/
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
classes: ["sw5e", "sheet", "actor", "vehicle"],
width: 605,
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 enc = {
max: actorData.data.attributes.capacity.cargo,
value: Math.round(totalWeight * 10) / 10
};
enc.pct = Math.min(enc.value * 100 / enc.max, 99);
return enc;
}
/* -------------------------------------------- */
/**
* 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 : '—';
}
}
/* -------------------------------------------- */
/**
* 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'
}];
const equipmentColumns = [{
label: game.i18n.localize('SW5E.Quantity'),
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 = {
actions: {
label: game.i18n.localize('SW5E.ActionPl'),
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
}
};
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;
for (const item of data.items) {
this._prepareCrewedItem(item);
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);
}
else if (item.type === '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);
}
}
data.features = Object.values(features);
data.cargo = Object.values(cargo);
data.data.attributes.encumbrance = this._computeEncumbrance(totalWeight, data);
}
/* -------------------------------------------- */
/* Event Listeners and Handlers */
/* -------------------------------------------- */
/** @override */
activateListeners(html) {
super.activateListeners(html);
if (!this.options.editable) return;
html.find('.item-toggle').click(this._onToggleItem.bind(this));
html.find('.item-hp input')
.click(evt => evt.target.select())
.change(this._onHPChange.bind(this));
html.find('.item:not(.cargo-row) input[data-property]')
.click(evt => evt.target.select())
.change(this._onEditInSheet.bind(this));
html.find('.cargo-row input')
.click(evt => evt.target.select())
.change(this._onCargoRowChange.bind(this));
if (this.actor.data.data.attributes.actions.stations) {
html.find('.counter.actions, .counter.action-thresholds').hide();
}
}
/* -------------------------------------------- */
/**
* 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 = 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);
}
/* -------------------------------------------- */
/**
* 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,178 +3,225 @@
* @type {Dialog}
*/
export default class AbilityUseDialog extends Dialog {
constructor(item, dialogData={}, options={}) {
super(dialogData, options);
this.options.classes = ["sw5e", "dialog"];
constructor(item, dialogData = {}, options = {}) {
super(dialogData, options);
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
* @type {Item5e}
* A constructor function which displays the Power Cast Dialog app for a given Actor and Item.
* 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");
/* -------------------------------------------- */
/* Rendering */
/* -------------------------------------------- */
// Prepare data
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;
/**
* A constructor function which displays the Power Cast Dialog app for a given Actor and Item.
* Returns a Promise which resolves to the dialog FormData once the workflow has been completed.
* @param {Item5e} item
* @return {Promise}
*/
static async create(item) {
if ( !item.isOwned ) throw new Error("You cannot display an ability usage dialog for an unowned item");
// Prepare dialog form data
const data = {
item: item.data,
title: game.i18n.format("SW5E.AbilityUseHint", {
type: game.i18n.localize(`SW5E.ItemType${item.type.capitalize()}`),
name: item.name
}),
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
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;
// Render the ability usage template
const html = await renderTemplate("systems/sw5e/templates/apps/ability-use.html", data);
// Prepare dialog form data
const data = {
item: item.data,
title: game.i18n.format("SW5E.AbilityUseHint", item.data),
note: this._getAbilityUseNote(item.data, uses, recharge),
hasLimitedUses: uses.max || recharges,
canUse: recharges ? recharge.charged : (quantity > 0 && !uses.value) || uses.value > 0,
hasPlaceableTemplate: game.user.can("TEMPLATE_CREATE") && item.hasAreaTarget,
errors: []
};
if ( item.data.type === "power" ) this._getPowerData(actorData, itemData, data);
// Render the ability usage template
const html = await renderTemplate("systems/sw5e/templates/apps/ability-use.html", data);
// Create the Dialog and return as a Promise
const icon = data.isPower ? "fa-magic" : "fa-fist-raised";
const label = game.i18n.localize("SW5E.AbilityUse" + (data.isPower ? "Cast" : "Use"));
return new Promise((resolve) => {
const dlg = new this(item, {
title: `${item.name}: Usage Configuration`,
content: html,
buttons: {
use: {
icon: `<i class="fas ${icon}"></i>`,
label: label,
callback: html => resolve(new FormData(html[0].querySelector("form")))
}
},
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 canUpcast = (lvl > 0) && CONFIG.SW5E.powerUpcastModes.includes(itemData.preparation.mode);
// If can't upcast, return early and don't bother calculating available power slots
if (!canUpcast) {
data = mergeObject(data, { isPower: true, canUpcast });
return;
// Create the Dialog and return data as a Promise
const icon = data.isPower ? "fa-magic" : "fa-fist-raised";
const label = game.i18n.localize("SW5E.AbilityUse" + (data.isPower ? "Cast" : "Use"));
return new Promise((resolve) => {
const dlg = new this(item, {
title: `${item.name}: ${game.i18n.localize("SW5E.AbilityUseConfig")}`,
content: html,
buttons: {
use: {
icon: `<i class="fas ${icon}"></i>`,
label: label,
callback: (html) => {
const fd = new FormDataExtended(html[0].querySelector("form"));
resolve(fd.toObject());
}
}
},
default: "use",
close: () => resolve(null)
});
dlg.render(true);
});
}
// Determine the levels which are feasible
let lmax = 0;
const powerLevels = Array.fromRange(10).reduce((arr, i) => {
if ( i < lvl ) return arr;
const label = CONFIG.SW5E.powerLevels[i];
const l = actorData.powers["power"+i] || {max: 0, override: null};
let max = parseInt(l.override || l.max || 0);
let slots = Math.clamped(parseInt(l.value || 0), 0, max);
if ( max > 0 ) lmax = 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);
/* -------------------------------------------- */
/* Helpers */
/* -------------------------------------------- */
// If this character has pact slots, present them as an option for casting the power.
const pact = actorData.powers.pact;
if (pact.level >= lvl) {
powerLevels.push({
level: 'pact',
label: `${game.i18n.format('SW5E.PowerLevelPact', {level: pact.level, n: pact.value})}`,
canCast: true,
hasSlots: pact.value > 0
});
}
const canCast = powerLevels.some(l => l.hasSlots);
/**
* 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);
// Return merged data
data = mergeObject(data, { isPower: true, canUpcast, powerLevels });
if ( !canCast ) data.errors.push("SW5E.PowerCastNoSlots");
}
// 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;
}
}
/**
* Get the ability usage note that is displayed
* @private
*/
static _getAbilityUseNote(item, uses, recharge) {
// eliminate point usage for innate casters
if (actorData.attributes.powercasting === "innate") points = 999;
// Zero quantity
const quantity = item.data.quantity;
if ( quantity <= 0 ) return game.i18n.localize("SW5E.AbilityUseUnavailableHint");
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);
} 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);
}
// Abilities which use Recharge
if ( !!recharge.value ) {
return game.i18n.format(recharge.charged ? "SW5E.AbilityUseChargedHint" : "SW5E.AbilityUseRechargeHint", {
type: item.type,
})
const canCast = powerLevels.some((l) => l.hasSlots);
if (!canCast)
data.errors.push(
game.i18n.format("SW5E.PowerCastNoSlots", {
level: CONFIG.SW5E.powerLevels[lvl],
name: data.item.name
})
);
// Merge power casting data
return foundry.utils.mergeObject(data, {isPower: true, consumePowerSlot, powerLevels});
}
// 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: item.data.consumableType,
value: uses.value,
quantity: item.data.quantity,
});
/**
* 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: 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,127 +1,139 @@
/**
* An application class which provides advanced configuration for special character flags which modify an Actor
* @extends {BaseEntitySheet}
* @implements {DocumentSheet}
*/
export default class ActorSheetFlags extends BaseEntitySheet {
export default class ActorSheetFlags extends DocumentSheet {
static get defaultOptions() {
const options = super.defaultOptions;
return mergeObject(options, {
id: "actor-flags",
classes: ["sw5e"],
template: "systems/sw5e/templates/apps/actor-flags.html",
width: 500,
closeOnSubmit: true
});
}
/* -------------------------------------------- */
/**
* Configure the title of the special traits selection window to include the Actor name
* @type {String}
*/
get title() {
return `${game.i18n.localize('SW5E.FlagsTitle')}: ${this.object.name}`;
}
/* -------------------------------------------- */
/**
* Prepare data used to render the special Actor traits selection UI
* @return {Object}
*/
getData() {
const data = super.getData();
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 = {};
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 = this.entity.getFlag("sw5e", k);
flags[v.section][`flags.sw5e.${k}`] = flag;
return foundry.utils.mergeObject(super.defaultOptions, {
id: "actor-flags",
classes: ["sw5e"],
template: "systems/sw5e/templates/apps/actor-flags.html",
width: 500,
closeOnSubmit: true
});
}
return flags;
}
/* -------------------------------------------- */
/* -------------------------------------------- */
/**
* Get the bonuses fields and their localization strings
* @return {Array}
* @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"}
];
for ( let b of bonuses ) {
b.value = getProperty(this.object.data, b.name) || "";
/** @override */
get title() {
return `${game.i18n.localize("SW5E.FlagsTitle")}: ${this.object.name}`;
}
return bonuses;
}
/* -------------------------------------------- */
/* -------------------------------------------- */
/**
* Update the Actor using the configured flags
* Remove/unset any flags which are no longer configured
*/
async _updateObject(event, formData) {
const actor = this.object;
let updateData = expandObject(formData);
/** @override */
getData() {
const data = {};
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;
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;
/* -------------------------------------------- */
/**
* Prepare an object of sorted classes.
* @return {object}
* @private
*/
_getClasses() {
const classes = this.object.items.filter((i) => i.type === "class");
return classes
.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
// TODO: Remove this logical gate once 0.7.x is release channel
if ( !isNewerVersion("0.7.1", game.data.version) ){
updateData = diffObject(this.object.data, updateData);
/* -------------------------------------------- */
/** @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});
}
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}
*/
export default class LongRestDialog extends Dialog {
constructor(actor, dialogData = {}, options = {}) {
super(dialogData, options);
this.actor = actor;
}
constructor(actor, dialogData = {}, options = {}) {
super(dialogData, options);
this.actor = actor;
}
/* -------------------------------------------- */
/* -------------------------------------------- */
/** @override */
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
template: "systems/sw5e/templates/apps/long-rest.html",
classes: ["sw5e", "dialog"]
});
}
/** @override */
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
template: "systems/sw5e/templates/apps/long-rest.html",
classes: ["sw5e", "dialog"]
});
}
/* -------------------------------------------- */
/* -------------------------------------------- */
/** @override */
getData() {
const data = super.getData();
const variant = game.settings.get("sw5e", "restVariant");
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)
return data;
}
/** @override */
getData() {
const data = super.getData();
const variant = game.settings.get("sw5e", "restVariant");
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)
return data;
}
/* -------------------------------------------- */
/* -------------------------------------------- */
/**
* A helper constructor function which displays the Long Rest confirmation dialog and returns a Promise once it's
* workflow has been resolved.
* @param {Actor5e} actor
* @return {Promise}
*/
static async longRestDialog({ actor } = {}) {
return new Promise((resolve, reject) => {
const dlg = new this(actor, {
title: "Long Rest",
buttons: {
rest: {
icon: '<i class="fas fa-bed"></i>',
label: "Rest",
callback: html => {
let newDay = false;
if (game.settings.get("sw5e", "restVariant") === "normal")
newDay = html.find('input[name="newDay"]')[0].checked;
else if(game.settings.get("sw5e", "restVariant") === "gritty")
newDay = true;
resolve(newDay);
}
},
cancel: {
icon: '<i class="fas fa-times"></i>',
label: "Cancel",
callback: reject
}
},
default: 'rest',
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.
* @param {Actor5e} actor
* @return {Promise}
*/
static async longRestDialog({actor} = {}) {
return new Promise((resolve, reject) => {
const dlg = new this(actor, {
title: game.i18n.localize("SW5E.LongRest"),
buttons: {
rest: {
icon: '<i class="fas fa-bed"></i>',
label: game.i18n.localize("SW5E.Rest"),
callback: (html) => {
let newDay = true;
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
}
},
default: "rest",
close: reject
});
dlg.render(true);
});
}
}

View file

@ -0,0 +1,38 @@
/**
* A simple form to set actor movement speeds
* @extends {DocumentSheet}
*/
export default class ActorMovementConfig extends DocumentSheet {
/** @override */
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
classes: ["sw5e"],
template: "systems/sw5e/templates/apps/movement-config.html",
width: 300,
height: "auto"
});
}
/* -------------------------------------------- */
/** @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;
}
}

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

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

View file

@ -5,129 +5,130 @@ import LongRestDialog from "./long-rest.js";
* @extends {Dialog}
*/
export default class ShortRestDialog extends Dialog {
constructor(actor, dialogData={}, options={}) {
super(dialogData, options);
constructor(actor, dialogData = {}, options = {}) {
super(dialogData, options);
/**
* Store a reference to the Actor entity which is resting
* @type {Actor}
*/
this.actor = actor;
/**
* Store a reference to the Actor entity which is resting
* @type {Actor}
*/
this.actor = actor;
/**
* Track the most recently used HD denomination for re-rendering the form
* @type {string}
*/
this._denom = null;
}
/**
* Track the most recently used HD denomination for re-rendering the form
* @type {string}
*/
this._denom = null;
}
/* -------------------------------------------- */
/* -------------------------------------------- */
/** @override */
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
template: "systems/sw5e/templates/apps/short-rest.html",
classes: ["sw5e", "dialog"]
});
}
/** @override */
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
template: "systems/sw5e/templates/apps/short-rest.html",
classes: ["sw5e", "dialog"]
});
}
/* -------------------------------------------- */
/* -------------------------------------------- */
/** @override */
getData() {
const data = super.getData();
// Determine Hit Dice
data.availableHD = this.actor.data.items.reduce((hd, item) => {
if ( item.type === "class" ) {
const d = item.data;
const denom = d.hitDice || "d6";
const available = parseInt(d.levels || 1) - parseInt(d.hitDiceUsed || 0);
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 */
getData() {
const data = super.getData();
/* -------------------------------------------- */
/** @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);
// Determine Hit Dice
data.availableHD = this.actor.data.items.reduce((hd, item) => {
if (item.type === "class") {
const d = item.data.data;
const denom = d.hitDice || "d6";
const available = parseInt(d.levels || 1) - parseInt(d.hitDiceUsed || 0);
hd[denom] = denom in hd ? hd[denom] + available : available;
}
},
cancel: {
icon: '<i class="fas fa-times"></i>',
label: "Cancel",
callback: reject
}
},
close: reject
});
dlg.render(true);
});
}
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;
}
/**
* 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);
}
/* -------------------------------------------- */
/** @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: 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
* @extends {FormApplication}
* @extends {DocumentSheet}
*/
export default class TraitSelector extends FormApplication {
/** @override */
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
id: "trait-selector",
classes: ["sw5e"],
title: "Actor Trait Selection",
template: "systems/sw5e/templates/apps/trait-selector.html",
width: 320,
height: "auto",
choices: {},
allowCustom: true,
minimum: 0,
maximum: null
});
}
/* -------------------------------------------- */
/**
* 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) || {};
attr.value = attr.value || [];
// 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
}
export default class TraitSelector extends DocumentSheet {
/** @inheritdoc */
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
id: "trait-selector",
classes: ["sw5e", "trait-selector", "subconfig"],
title: "Actor Trait Selection",
template: "systems/sw5e/templates/apps/trait-selector.html",
width: 320,
height: "auto",
choices: {},
allowCustom: true,
minimum: 0,
maximum: null,
valueKey: "value",
customKey: "custom"
});
}
// Return data
return {
allowCustom: this.options.allowCustom,
choices: choices,
custom: attr ? attr.custom : ""
}
}
/* -------------------------------------------- */
/* -------------------------------------------- */
/** @override */
_updateObject(event, formData) {
const updateData = {};
// 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`);
/**
* Return a reference to the target attribute
* @type {string}
*/
get attribute() {
return this.options.name;
}
// 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 */
export const measureDistances = function(segments, options={}) {
if ( !options.gridSpaces ) return BaseGrid.prototype.measureDistances.call(this, segments, options);
export const measureDistances = function (segments, options = {}) {
if (!options.gridSpaces) return BaseGrid.prototype.measureDistances.call(this, segments, options);
// Track the total number of diagonals
let nDiagonal = 0;
const rule = this.parent.diagonalRule;
const d = canvas.dimensions;
// Track the total number of diagonals
let nDiagonal = 0;
const rule = this.parent.diagonalRule;
const d = canvas.dimensions;
// Iterate over measured segments
return segments.map(s => {
let r = s.ray;
// Iterate over measured segments
return segments.map((s) => {
let r = s.ray;
// Determine the total distance traveled
let nx = Math.abs(Math.ceil(r.dx / d.size));
let ny = Math.abs(Math.ceil(r.dy / d.size));
// Determine the total distance traveled
let nx = Math.abs(Math.ceil(r.dx / d.size));
let ny = Math.abs(Math.ceil(r.dy / d.size));
// Determine the number of straight and diagonal moves
let nd = Math.min(nx, ny);
let ns = Math.abs(ny - nx);
nDiagonal += nd;
// Determine the number of straight and diagonal moves
let nd = Math.min(nx, ny);
let ns = Math.abs(ny - nx);
nDiagonal += nd;
// Alternative DMG Movement
if (rule === "5105") {
let nd10 = Math.floor(nDiagonal / 2) - Math.floor((nDiagonal - nd) / 2);
let spaces = (nd10 * 2) + (nd - nd10) + ns;
return spaces * canvas.dimensions.distance;
}
// Alternative DMG Movement
if (rule === "5105") {
let nd10 = Math.floor(nDiagonal / 2) - Math.floor((nDiagonal - nd) / 2);
let spaces = nd10 * 2 + (nd - nd10) + ns;
return spaces * canvas.dimensions.distance;
}
// Euclidean Measurement
else if (rule === "EUCL") {
return Math.round(Math.hypot(nx, ny) * canvas.scene.data.gridDistance);
}
// Euclidean Measurement
else if (rule === "EUCL") {
return Math.round(Math.hypot(nx, ny) * canvas.scene.data.gridDistance);
}
// Standard PHB Movement
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;
// Standard PHB Movement
else return (ns + nd) * canvas.scene.data.gridDistance;
});
};

326
module/characterImporter.js Normal file
View file

@ -0,0 +1,326 @@
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
const details = {
species: sourceCharacter.attribs.find((e) => e.name == "race").current,
background: sourceCharacter.attribs.find((e) => e.name == "background").current,
alignment: sourceCharacter.attribs.find((e) => e.name == "alignment").current
};
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 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);
}
// Parse all classes and add them to already created actor.
// "class" is a reserved word, therefore I use profession where I can.
static async addProfessions(sourceCharacter, actor) {
let result = [];
// parse all class and multiclassX items
// couldn't get Array.filter to work here for some reason
// result = array of objects. each object is a separate class
sourceCharacter.attribs.forEach((e) => {
if (CharacterImporter.classOrMulticlass(e.name)) {
var t = {
profession: CharacterImporter.capitalize(e.current),
type: CharacterImporter.baseOrMulti(e.name),
level: CharacterImporter.getLevel(e, sourceCharacter)
};
result.push(t);
}
});
// pull classes directly from system compendium and add them to current actor
const professionsPack = await game.packs.get("sw5e.classes").getDocuments();
result.forEach((prof) => {
let assignedProfession = professionsPack.find((o) => o.name === prof.profession);
assignedProfession.data.data.levels = prof.level;
actor.createEmbeddedDocuments("Item", [assignedProfession.data], {displaySheet: false});
});
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";
}
}
static getLevel(item, sourceCharacter) {
if (item.name === "class") {
let result = sourceCharacter.attribs.find((e) => e.name === "base_level").current;
return parseInt(result);
} else {
let result = sourceCharacter.attribs.find((e) => e.name === `${item.name}_lvl`).current;
return parseInt(result);
}
}
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,28 +1,29 @@
/**
* Highlight critical success or failure on d20 rolls
*/
export const highlightCriticalSuccessFailure = function(message, html, data) {
if ( !message.isRoll || !message.isContentVisible ) return;
export const highlightCriticalSuccessFailure = function (message, html, data) {
if (!message.isRoll || !message.isContentVisible) return;
// Highlight rolls where the first part is a d20 roll
const roll = message.roll;
if ( !roll.dice.length ) return;
const d = roll.dice[0];
// Highlight rolls where the first part is a d20 roll
const roll = message.roll;
if (!roll.dice.length) return;
const d = roll.dice[0];
// Ensure it is an un-modified d20 roll
const isD20 = (d.faces === 20) && ( d.results.length === 1 );
if ( !isD20 ) return;
const isModifiedRoll = ("success" in d.rolls[0]) || d.options.marginSuccess || d.options.marginFailure;
if ( isModifiedRoll ) return;
// Ensure it is an un-modified d20 roll
const isD20 = d.faces === 20 && d.values.length === 1;
if (!isD20) return;
const isModifiedRoll = "success" in d.results[0] || d.options.marginSuccess || d.options.marginFailure;
if (isModifiedRoll) return;
// Highlight successes and failures
if ( d.options.critical && (d.total >= d.options.critical) ) html.find(".dice-total").addClass("critical");
else if ( d.options.fumble && (d.total <= d.options.fumble) ) html.find(".dice-total").addClass("fumble");
else if ( d.options.target ) {
if ( roll.total >= d.options.target ) html.find(".dice-total").addClass("success");
else html.find(".dice-total").addClass("failure");
}
// Highlight successes and failures
const critical = d.options.critical || 20;
const fumble = d.options.fumble || 1;
if (d.total >= critical) html.find(".dice-total").addClass("critical");
else if (d.total <= fumble) html.find(".dice-total").addClass("fumble");
else if (d.options.target) {
if (roll.total >= d.options.target) html.find(".dice-total").addClass("success");
else html.find(".dice-total").addClass("failure");
}
};
/* -------------------------------------------- */
@ -30,23 +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
*/
export const displayChatActionButtons = function(message, html, data) {
const chatCard = html.find(".sw5e.chat-card");
if ( chatCard.length > 0 ) {
html.find(".flavor-text").remove();
export const displayChatActionButtons = function (message, html, data) {
const chatCard = html.find(".sw5e.chat-card");
if (chatCard.length > 0) {
const flavor = html.find(".flavor-text");
if (flavor.text() === html.find(".item-name").text()) flavor.remove();
// If the user is the message author or the actor owner, proceed
let actor = game.actors.get(data.message.speaker.actor);
if ( actor && actor.owner ) return;
else if ( game.user.isGM || (data.author.id === game.user.id)) return;
// If the user is the message author or the actor owner, proceed
let actor = game.actors.get(data.message.speaker.actor);
if (actor && actor.isOwner) return;
else if (game.user.isGM || data.author.id === game.user.id) return;
// Otherwise conceal action buttons except for saving throw
const buttons = chatCard.find("button[data-action]");
buttons.each((i, btn) => {
if ( btn.dataset.action === "save" ) return;
btn.style.display = "none"
});
}
// Otherwise conceal action buttons except for saving throw
const buttons = chatCard.find("button[data-action]");
buttons.each((i, btn) => {
if (btn.dataset.action === "save") return;
btn.style.display = "none";
});
}
};
/* -------------------------------------------- */
@ -60,38 +62,38 @@ export const displayChatActionButtons = function(message, html, data) {
*
* @return {Array} The extended options Array including new context choices
*/
export const addChatMessageContextOptions = function(html, options) {
let canApply = li => {
const message = game.messages.get(li.data("messageId"));
return message.isRoll && message.isContentVisible && canvas.tokens.controlled.length;
};
options.push(
{
name: game.i18n.localize("SW5E.ChatContextDamage"),
icon: '<i class="fas fa-user-minus"></i>',
condition: canApply,
callback: li => applyChatCardDamage(li, 1)
},
{
name: game.i18n.localize("SW5E.ChatContextHealing"),
icon: '<i class="fas fa-user-plus"></i>',
condition: canApply,
callback: li => applyChatCardDamage(li, -1)
},
{
name: game.i18n.localize("SW5E.ChatContextDoubleDamage"),
icon: '<i class="fas fa-user-injured"></i>',
condition: canApply,
callback: li => applyChatCardDamage(li, 2)
},
{
name: game.i18n.localize("SW5E.ChatContextHalfDamage"),
icon: '<i class="fas fa-user-shield"></i>',
condition: canApply,
callback: li => applyChatCardDamage(li, 0.5)
}
);
return options;
export const addChatMessageContextOptions = function (html, options) {
let canApply = (li) => {
const message = game.messages.get(li.data("messageId"));
return message?.isRoll && message?.isContentVisible && canvas.tokens?.controlled.length;
};
options.push(
{
name: game.i18n.localize("SW5E.ChatContextDamage"),
icon: '<i class="fas fa-user-minus"></i>',
condition: canApply,
callback: (li) => applyChatCardDamage(li, 1)
},
{
name: game.i18n.localize("SW5E.ChatContextHealing"),
icon: '<i class="fas fa-user-plus"></i>',
condition: canApply,
callback: (li) => applyChatCardDamage(li, -1)
},
{
name: game.i18n.localize("SW5E.ChatContextDoubleDamage"),
icon: '<i class="fas fa-user-injured"></i>',
condition: canApply,
callback: (li) => applyChatCardDamage(li, 2)
},
{
name: game.i18n.localize("SW5E.ChatContextHalfDamage"),
icon: '<i class="fas fa-user-shield"></i>',
condition: canApply,
callback: (li) => applyChatCardDamage(li, 0.5)
}
);
return options;
};
/* -------------------------------------------- */
@ -100,16 +102,19 @@ export const addChatMessageContextOptions = function(html, options) {
* Apply rolled dice damage to the token or tokens which are currently controlled.
* This allows for damage to be scaled by a multiplier to account for healing, critical hits, or resistance
*
* @param {HTMLElement} roll The chat entry which contains the roll data
* @param {HTMLElement} li The chat entry which contains the roll data
* @param {Number} multiplier A damage multiplier to apply to the rolled damage.
* @return {Promise}
*/
function applyChatCardDamage(roll, multiplier) {
const amount = roll.find('.dice-total').text();
return Promise.all(canvas.tokens.controlled.map(t => {
const a = t.actor;
return a.applyDamage(amount, multiplier);
}));
function applyChatCardDamage(li, multiplier) {
const message = game.messages.get(li.data("messageId"));
const roll = message.roll;
return Promise.all(
canvas.tokens.controlled.map((t) => {
const a = t.actor;
return a.applyDamage(roll.total, multiplier);
})
);
}
/* -------------------------------------------- */

View file

@ -1,35 +1 @@
export const ClassFeatures = {
"berserker": {
"archetypes": {
"addicted-approach": {
"label": "Addicted Approach",
"source": "PHB",
"features": {
"3": ["Compendium.sw5e.archetypes.PCwepUZqHYlxr4T3", "Compendium.sw5e.classfeatures.efOA0nrvUqKJOOeP", "Compendium.sw5e.classfeatures.nT6AfpQXSZ4IeChO"],
"6": ["Compendium.sw5e.classfeatures.GbJDWzoTKWL7sEpR"],
"10": ["Compendium.sw5e.classfeatures.3jqPPd5qJBBnonPw"],
"14": ["Compendium.sw5e.classfeatures.xzRNHB2M2HdOZzr7"]
}
}
},
"features": {
"1": ["Compendium.sw5e.classfeatures.IDt6duVrBzL8euRc", "Compendium.sw5e.classfeatures.rPOLy96fW96N2UPg"],
"2": ["Compendium.sw5e.classfeatures.DlYiCiG39R0goG9u", "Compendium.sw5e.classfeatures.FbSpxpXm1xONn0na", "Compendium.sw5e.classfeatures.KDiQ8O2evV2Z1YTo", "Compendium.sw5e.classfeatures.Q1JyHnVs9iIEBs91", "Compendium.sw5e.classfeatures.ROdICoWR82v6A2Rf", "Compendium.sw5e.classfeatures.cdCx5Hvq2rYRMzRj", "Compendium.sw5e.classfeatures.dTdbL8dypa6BAdnP", "Compendium.sw5e.classfeatures.h1uDhP1tEOuvjRw6", "Compendium.sw5e.classfeatures.hMiA075EKBBOL2cv", "Compendium.sw5e.classfeatures.sgJdISZMtwv08WPJ", "Compendium.sw5e.classfeatures.v4CZJ8LBMl5PYZCO"],
"3": ["Compendium.sw5e.classfeatures.kzwSN9SabKgWZZvU"],
"4": ["Compendium.sw5e.classfeatures.9oyy0MMqEws2qoil"],
"5": ["Compendium.sw5e.classfeatures.dPWmHiWmpnhHTsgd"],
"7": ["Compendium.sw5e.classfeatures.Cid5ujSdnooH0vMm", "Compendium.sw5e.classfeatures.WTBhKJgkArQI3Tgv", "Compendium.sw5e.classfeatures.oiT3TJxzRWPKAX9E", "Compendium.sw5e.classfeatures.pMEmIt3NWThbee8k", "Compendium.sw5e.classfeatures.qWV5YogZcpZ3Y3xj"],
"9": ["Compendium.sw5e.classfeatures.bi8G8H5Ur9B3BAyM"],
"11": ["Compendium.sw5e.classfeatures.eWbTifdXJvvXT4CV"],
"13": ["Compendium.sw5e.classfeatures.Hg8zYh1iXL0DGUVq", "Compendium.sw5e.classfeatures.QRnYiJmRk18ekE9v", "Compendium.sw5e.classfeatures.sfEr8ZBFVddlfLeF", "Compendium.sw5e.classfeatures.yGC9VzT840qQWxca"],
"15": ["Compendium.sw5e.classfeatures.YHPUv9lN3nCapAgP"],
"18": ["Compendium.sw5e.classfeatures.fFKNqUAWh0ZOhvRc"],
"20": ["Compendium.sw5e.classfeatures.IWTDawTUf79eWbEV"]
}
},
"consular": {
"features": {
"20": ["Compendium.sw5e.classfeatures.gSGeitc98ItAwhfF"]
}
}
};
export const ClassFeatures = {};

View file

@ -1,67 +1,31 @@
/**
* Override the default Initiative formula to customize special behaviors of the SW5e system.
* Apply advantage, proficiency, or bonuses where appropriate
* Apply the dexterity score as a decimal tiebreaker if requested
* See Combat._getInitiativeFormula for more detail.
*/
export const _getInitiativeFormula = function(combatant) {
const actor = combatant.actor;
if ( !actor ) return "1d20";
const init = actor.data.data.attributes.init;
export const _getInitiativeFormula = function () {
const actor = this.actor;
if (!actor) return "1d20";
const init = actor.data.data.attributes.init;
let nd = 1;
let mods = "";
if (actor.getFlag("sw5e", "halflingLucky")) mods += "r=1";
if (actor.getFlag("sw5e", "initiativeAdv")) {
nd = 2;
mods += "kh";
}
// Construct initiative formula parts
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
];
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(" + ");
// 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(" + ");
};
/* -------------------------------------------- */
/**
* TODO: A temporary shim until 0.7.x becomes stable
* @override
*/
TokenConfig.getTrackedAttributes = function(data, _path=[]) {
// Track the path and record found attributes
const attributes = {
"bar": [],
"value": []
};
// Recursively explore the object
for ( let [k, v] of Object.entries(data) ) {
let p = _path.concat([k]);
// Check objects for both a "value" and a "max"
if ( v instanceof Object ) {
const isBar = ("value" in v) && ("max" in v);
if ( isBar ) attributes.bar.push(p);
else {
const inner = this.getTrackedAttributes(data[k], p);
attributes.bar.push(...inner.bar);
attributes.value.push(...inner.value);
}
}
// Otherwise identify values which are numeric or null
else if ( Number.isNumeric(v) || (v === null) ) {
attributes.value.push(p);
}
}
return attributes;
};

File diff suppressed because it is too large Load diff

View file

@ -1,300 +1,313 @@
/**
* A standardized helper function for managing core 5e "d20 rolls"
*
* 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
*
* @param {Array} parts The dice roll component parts, excluding the initial d20
* @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
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
*
* @return {Promise} A Promise which resolves once the roll workflow has completed
*/
export async function d20Roll({parts=[], data={}, event={}, rollMode=null, template=null, title=null, speaker=null,
flavor=null, fastForward=null, dialogOptions,
advantage=null, disadvantage=null, critical=20, fumble=1, targetValue=null,
elvenAccuracy=false, halflingLucky=false, reliableTalent=false,
chatMessage=true, messageData={}}={}) {
// Prepare Message Data
messageData.flavor = flavor || title;
messageData.speaker = speaker || ChatMessage.getSpeaker();
const messageOptions = {rollMode: rollMode || game.settings.get("core", "rollMode")};
parts = parts.concat(["@bonus"]);
// Handle fast-forward events
let adv = 0;
fastForward = fastForward ?? (event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey));
if (fastForward) {
if ( advantage || event.altKey ) adv = 1;
else if ( disadvantage || event.ctrlKey || event.metaKey ) adv = -1;
}
// Define the inner roll function
const _roll = (parts, adv, form) => {
// Determine the d20 roll and modifiers
let nd = 1;
let mods = halflingLucky ? "r=1" : "";
* @param {string} formula The original Roll formula
* @param {Object} data Actor or item data against which to parse the roll
* @param {Object} options Formatting options
* @param {boolean} options.constantFirst Puts the constants before the dice terms in the resulting formula
*
* @return {string} The resulting simplified formula
*/
export function simplifyRollFormula(formula, data, {constantFirst = false} = {}) {
const roll = new Roll(formula, data); // Parses the formula and replaces any @properties
const terms = roll.terms;
// 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";
}
// Some terms are "too complicated" for this algorithm to simplify
// In this case, the original formula is returned.
if (terms.some(_isUnsupportedTerm)) return roll.formula;
// Handle disadvantage
else if (adv === -1) {
nd = 2;
messageData.flavor += ` (${game.i18n.localize("SW5E.Disadvantage")})`;
if ( "flags.sw5e.roll" in messageData ) messageData["flags.sw5e.roll"].disadvantage = true;
mods += "kl";
}
const rollableTerms = []; // Terms that are non-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
// Prepend the d20 roll
let formula = `${nd}d20${mods}`;
if (reliableTalent) formula = `{${nd}d20${mods},10}kh`;
parts.unshift(formula);
// Optionally include a situational bonus
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;
for (let term of terms) {
// For each term
if (term instanceof OperatorTerm) operators.push(term);
// If the term is an addition/subtraction operator, push the term into the operators array
else {
// Otherwise the term is not an operator
if (term instanceof DiceTerm) {
// If the term is something rollable
rollableTerms.push(...operators); // Place all the operators into the rollableTerms array
rollableTerms.push(term); // Then place this rollable term into it as well
} //
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.
}
}
// 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 (targetValue) d.options.target = targetValue;
}
const constantFormula = Roll.getFormula(constantTerms); // Cleans up the 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
// 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`);
}
}
// 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")})`;
}
return roll;
};
// Order the rollable and constant terms, either constant first or second depending on the optional argument
const parts = constantFirst ? [constantPart, rollableFormula] : [rollableFormula, constantPart];
// 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;
// Join the parts with a + sign, pass them to `Roll` once again to clean up the formula
return new Roll(parts.filterJoin(" + ")).formula;
}
/* -------------------------------------------- */
/**
* Only some terms are supported by simplifyRollFormula, this method returns true when the term is not supported.
* @param {*} term - A single Dice term to check support on
* @return {Boolean} True when unsupported, false if supported
*/
function _isUnsupportedTerm(term) {
const diceTerm = term instanceof DiceTerm;
const operator = term instanceof OperatorTerm && ["+", "-"].includes(term.operator);
const number = term instanceof NumericTerm;
return !(diceTerm || operator || number);
}
/* -------------------------------------------- */
/* D20 Roll */
/* -------------------------------------------- */
/**
* Present a Dialog form which creates a d20 roll once submitted
* @return {Promise<Roll>}
* @private
* A standardized helper function for managing core 5e d20 rolls.
* 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
*
* @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 {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
*/
async function _d20RollDialog({template, title, parts, data, rollMode, dialogOptions, roll}={}) {
export async function d20Roll({
parts = [],
data = {}, // Roll creation
advantage,
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";
// Render modal dialog
template = template || "systems/sw5e/templates/chat/roll-dialog.html";
let dialogData = {
formula: parts.join(" + "),
data: data,
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);
// Construct the D20Roll instance
const roll = new CONFIG.Dice.D20Roll(formula, data, {
flavor: flavor || title,
advantageMode,
defaultRollMode,
critical,
fumble,
targetValue,
elvenAccuracy,
halflingLucky,
reliableTalent
});
}
/* -------------------------------------------- */
/**
* A standardized helper function for managing core 5e "d20 rolls"
*
* 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
*
* @param {Array} 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 {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 {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
*/
export async function damageRoll({parts, actor, data, event={}, rollMode=null, template, title, speaker, flavor,
allowCritical=true, critical=false, fastForward=null, dialogOptions, chatMessage=true, messageData={}}={}) {
// Prepare Message Data
messageData.flavor = flavor || title;
messageData.speaker = speaker || ChatMessage.getSpeaker();
const messageOptions = {rollMode: rollMode || game.settings.get("core", "rollMode")};
parts = parts.concat(["@bonus"]);
fastForward = fastForward ?? (event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey));
// Define inner roll function
const _roll = function(parts, crit, form) {
// Optionally include a situational bonus
if ( form ) {
data['bonus'] = form.bonus.value;
messageOptions.rollMode = form.rollMode.value;
}
if (!data["bonus"]) parts.pop();
// Create the damage roll
let roll = new Roll(parts.join("+"), data);
// Modify the damage formula for critical hits
if ( crit === true ) {
let add = (actor && actor.getFlag("sw5e", "savageAttacks")) ? 1 : 0;
let mult = 2;
// TODO Backwards compatibility - REMOVE LATER
if (isNewerVersion(game.data.version, "0.6.9")) roll.alter(mult, add);
else roll.alter(add, mult);
messageData.flavor += ` (${game.i18n.localize("SW5E.Critical")})`;
if ( "flags.sw5e.roll" in messageData ) messageData["flags.sw5e.roll"].critical = true;
// Prompt a Dialog to further configure the D20Roll
if (!isFF) {
const configured = await roll.configureDialog(
{
title,
chooseModifier,
defaultRollMode: defaultRollMode,
defaultAction: advantageMode,
defaultAbility: data?.item?.ability,
template
},
dialogOptions
);
if (configured === null) return null;
}
// Execute the roll
try {
return roll.roll();
} catch(err) {
console.error(err);
ui.notifications.error(`Dice roll evaluation failed: ${err.message}`);
return null;
// Evaluate the configured roll
await roll.evaluate({async: true});
// Create a Chat Message
if (speaker) {
console.warn(
`You are passing the speaker argument to the d20Roll function directly which should instead be passed as an internal key of messageData`
);
messageData.speaker = speaker;
}
};
// Create the Roll instance
const roll = fastForward ? _roll(parts, critical || event.altKey) : 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;
if (roll && chatMessage) await roll.toMessage(messageData);
return roll;
}
/* -------------------------------------------- */
/**
* Present a Dialog form which creates a damage roll once submitted
* @return {Promise<Roll>}
* @private
* Determines whether this d20 roll should be fast-forwarded, and whether advantage or disadvantage should be applied
* @returns {{isFF: boolean, advantageMode: number}} Whether the roll is fast-forward, and its advantage mode
*/
async function _damageRollDialog({template, title, parts, data, allowCritical, rollMode, dialogOptions, roll}={}) {
// Render modal dialog
template = template || "systems/sw5e/templates/chat/roll-dialog.html";
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);
});
function _determineAdvantageMode({event, advantage = false, disadvantage = false, fastForward = false} = {}) {
const isFF = fastForward || (event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey));
let advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.NORMAL;
if (advantage || event?.altKey) advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.ADVANTAGE;
else if (disadvantage || event?.ctrlKey || event?.metaKey)
advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.DISADVANTAGE;
return {isFF, advantageMode};
}
/* -------------------------------------------- */
/* Damage Roll */
/* -------------------------------------------- */
/**
* A standardized helper function for managing core 5e damage rolls.
*
* 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
*
* @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 {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 = [],
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");
// Construct the DamageRoll instance
const formula = parts.join(" + ");
const {isCritical, isFF} = _determineCriticalMode({critical, fastForward, event});
const roll = new CONFIG.Dice.DamageRoll(formula, data, {
flavor: flavor || title,
critical: isCritical,
criticalBonusDice,
criticalMultiplier,
multiplyNumeric,
powerfulCritical
});
// Prompt a Dialog to further configure the DamageRoll
if (!isFF) {
const configured = await roll.configureDialog(
{
title,
defaultRollMode: defaultRollMode,
defaultCritical: isCritical,
template,
allowCritical
},
dialogOptions
);
if (configured === null) return null;
}
// Evaluate the configured roll
await roll.evaluate({async: true});
// Create a Chat Message
if (speaker) {
console.warn(
`You are passing the speaker argument to the damageRoll function directly which should instead be passed as an internal key of messageData`
);
messageData.speaker = speaker;
}
if (roll && chatMessage) await roll.toMessage(messageData);
return roll;
}
/* -------------------------------------------- */
/**
* Determines whether this d20 roll should be fast-forwarded, and whether advantage or disadvantage should be applied
* @returns {{isFF: boolean, isCritical: boolean}} Whether the roll is fast-forward, and whether it is a critical hit
*/
function _determineCriticalMode({event, critical = false, fastForward = false} = {}) {
const isFF = fastForward || (event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey));
if (event?.altKey) critical = true;
return {isFF, isCritical: critical};
}

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

64
module/effects.js vendored Normal file
View file

@ -0,0 +1,64 @@
/**
* Manage Active Effect instances through the Actor Sheet via effect control buttons.
* @param {MouseEvent} event The left-click event on the effect control
* @param {Actor|Item} owner The owning entity which manages this effect
*/
export function onManageActiveEffect(event, owner) {
event.preventDefault();
const a = event.currentTarget;
const li = a.closest("li");
const effect = li.dataset.effectId ? owner.effects.get(li.dataset.effectId) : null;
switch (a.dataset.action) {
case "create":
return owner.createEmbeddedDocuments("ActiveEffect", [
{
"label": game.i18n.localize("SW5E.EffectNew"),
"icon": "icons/svg/aura.svg",
"origin": owner.uuid,
"duration.rounds": li.dataset.effectType === "temporary" ? 1 : undefined,
"disabled": li.dataset.effectType === "inactive"
}
]);
case "edit":
return effect.sheet.render(true);
case "delete":
return effect.delete();
case "toggle":
return effect.update({disabled: !effect.data.disabled});
}
}
/**
* Prepare the data structure for Active Effects which are currently applied to an Actor or Item.
* @param {ActiveEffect[]} effects The array of Active Effect instances to prepare sheet data for
* @return {object} Data for rendering
*/
export function prepareActiveEffectCategories(effects) {
// Define effect header categories
const categories = {
temporary: {
type: "temporary",
label: game.i18n.localize("SW5E.EffectTemporary"),
effects: []
},
passive: {
type: "passive",
label: game.i18n.localize("SW5E.EffectPassive"),
effects: []
},
inactive: {
type: "inactive",
label: game.i18n.localize("SW5E.EffectInactive"),
effects: []
}
};
// Iterate over active effects, classifying them into categories
for (let e of effects) {
e._getSourceName(); // Trigger a lookup for the source name
if (e.data.disabled) categories.inactive.effects.push(e);
else if (e.isTemporary) categories.temporary.effects.push(e);
else categories.passive.effects.push(e);
}
return categories;
}

File diff suppressed because it is too large Load diff

View file

@ -1,316 +1,370 @@
import TraitSelector from "../apps/trait-selector.js";
import {onManageActiveEffect, prepareActiveEffectCategories} from "../effects.js";
/**
* Override and extend the core ItemSheet implementation to handle specific item types
* @extends {ItemSheet}
*/
export default class ItemSheet5e extends ItemSheet {
constructor(...args) {
super(...args);
if ( this.object.data.type === "class" ) {
this.options.width = 600;
constructor(...args) {
super(...args);
// Expand the default size of the class sheet
if (this.object.data.type === "class") {
this.options.width = this.position.width = 600;
this.options.height = this.position.height = 680;
}
}
}
/* -------------------------------------------- */
/* -------------------------------------------- */
/** @override */
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
width: 560,
height: "auto",
classes: ["sw5e", "sheet", "item"],
resizable: true,
scrollY: [".tab.details"],
tabs: [{navSelector: ".tabs", contentSelector: ".sheet-body", initial: "description"}]
});
}
/** @inheritdoc */
static get defaultOptions() {
return foundry.utils.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`;
}
/** @inheritdoc */
get template() {
const path = "systems/sw5e/templates/items/";
return `${path}/${this.item.data.type}.html`;
}
/* -------------------------------------------- */
/* -------------------------------------------- */
/** @override */
getData() {
const data = super.getData();
data.labels = this.item.labels;
/** @override */
async getData(options) {
const data = super.getData(options);
const itemData = data.data;
data.labels = this.item.labels;
data.config = CONFIG.SW5E;
// Include CONFIG values
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");
// Item Type, Status, and Details
data.itemType = 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(itemData);
// Potential consumption targets
data.abilityConsumptionTargets = this._getItemConsumptionTargets(data.item);
// 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);
// Action Details
data.hasAttackRoll = this.item.hasAttack;
data.isHealing = data.item.data.actionType === "heal";
data.isFlatDC = getProperty(data.item.data, "save.scaling") === "flat";
// 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 = data.item.data.activation?.type === 'crew';
data.isMountable = this._isItemMountable(data.item);
return data;
}
// 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 {};
* 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})`;
// 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})`}
);
}
return ammo;
}, {});
}
// 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})`;
// Attributes
else if (consume.type === "attribute") {
const attributes = TokenDocument.getTrackedAttributes(actor.data.data);
attributes.bar.forEach((a) => a.push("value"));
return attributes.bar.concat(attributes.value).reduce((obj, a) => {
let k = a.join(".");
obj[k] = k;
return obj;
}, {});
}
return obj;
}, {});
}
// Charges
else if ( consume.type === "charges" ) {
return actor.items.reduce((obj, i) => {
const uses = i.data.data.uses || {};
if ( uses.per && uses.max ) {
const label = uses.per === "charges" ?
` (${game.i18n.format("SW5E.AbilityUseChargesLabel", {value: uses.value})})` :
` (${game.i18n.format("SW5E.AbilityUseConsumableLabel", {max: uses.max, per: uses.per})})`;
obj[i.id] = i.name + label;
// 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;
}, {});
}
return obj;
}, {})
}
else return {};
}
/* -------------------------------------------- */
// Charges
else if (consume.type === "charges") {
return actor.items.reduce((obj, i) => {
// Limited-use items
const uses = i.data.data.uses || {};
if (uses.per && uses.max) {
const label =
uses.per === "charges"
? ` (${game.i18n.format("SW5E.AbilityUseChargesLabel", {value: uses.value})})`
: ` (${game.i18n.format("SW5E.AbilityUseConsumableLabel", {
max: uses.max,
per: uses.per
})})`;
obj[i.id] = i.name + label;
}
/**
* 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");
}
}
/* -------------------------------------------- */
/**
* 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" ) {
props.push(...Object.entries(item.data.properties)
.filter(e => e[1] === true)
.map(e => CONFIG.SW5E.weaponProperties[e[0]]));
// Recharging items
const recharge = i.data.data.recharge || {};
if (recharge.value) obj[i.id] = `${i.name} (${game.i18n.format("SW5E.Recharge")})`;
return obj;
}, {});
} else return {};
}
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
)
/* -------------------------------------------- */
/**
* 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");
}
}
else if ( item.type === "equipment" ) {
props.push(CONFIG.SW5E.equipmentTypes[item.data.armor.type]);
props.push(labels.armor);
/* -------------------------------------------- */
/**
* 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") {
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.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);
//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]);
}
// 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);
}
else if ( item.type === "feat" ) {
props.push(labels.featType);
}
else if ( item.type === "species" ) {
/* -------------------------------------------- */
}
else if ( item.type === "archetype" ) {
}
else if ( item.type === "classfeature" ) {
}
// Action type
if ( item.data.actionType ) {
props.push(CONFIG.SW5E.itemActionTypes[item.data.actionType]);
/**
* Is this item a separate large object like a siege engine or vehicle
* component that is usually mounted on fixtures rather than equipped, and
* has its own AC and HP.
* @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")
);
}
// 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);
}
/* -------------------------------------------- */
/* -------------------------------------------- */
/**
* Is this item a separate large object like a siege engine or vehicle
* component that is usually mounted on fixtures rather than equipped, and
* has its own AC and HP.
* @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');
}
/* -------------------------------------------- */
/** @override */
setPosition(position={}) {
if ( !this._minimized ) {
position.height = this._tabs[0].active === "details" ? "auto" : this.options.height;
}
return super.setPosition(position);
}
/* -------------------------------------------- */
/* Form Submission */
/* -------------------------------------------- */
/** @override */
_updateObject(event, formData) {
// TODO: This can be removed once 0.7.x is release channel
if ( !formData.data ) formData = expandObject(formData);
// Handle Damage Array
const damage = formData.data?.damage;
if ( damage ) damage.parts = Object.values(damage?.parts || {}).map(d => [d[0] || "", d[1] || ""]);
// Update the Item
super._updateObject(event, formData);
}
/* -------------------------------------------- */
/** @override */
activateListeners(html) {
super.activateListeners(html);
html.find(".damage-control").click(this._onDamageControl.bind(this));
html.find('.trait-selector.class-skills').click(this._onConfigureClassSkills.bind(this));
}
/* -------------------------------------------- */
/**
* 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([["", ""]])});
/** @inheritdoc */
setPosition(position = {}) {
if (!(this._minimized || position.height)) {
position.height = this._tabs[0].active === "details" ? "auto" : this.options.height;
}
return super.setPosition(position);
}
// 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 = duplicate(this.item.data.data.damage);
damage.parts.splice(Number(li.dataset.damagePart), 1);
return this.item.update({"data.damage.parts": damage.parts});
/* -------------------------------------------- */
/* Form Submission */
/* -------------------------------------------- */
/** @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);
// 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);
}
}
/* -------------------------------------------- */
/* -------------------------------------------- */
/**
* Handle spawning the TraitSelector application which allows a checkbox of multiple trait options
* @param {Event} event The click event which originated the selection
* @private
*/
_onConfigureClassSkills(event) {
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;
/** @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);
});
}
}
// Render the Trait Selector dialog
new TraitSelector(this.item, {
name: a.dataset.edit,
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)
}
/* -------------------------------------------- */
/**
* 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 */
/* -------------------------------------------- */
@ -11,24 +10,24 @@
* @returns {Promise}
*/
export async function create5eMacro(data, slot) {
if ( data.type !== "Item" ) return;
if (!( "data" in data ) ) return ui.notifications.warn("You can only create macro buttons for owned Items");
const item = data.data;
if (data.type !== "Item") return;
if (!("data" in data)) return ui.notifications.warn("You can only create macro buttons for owned Items");
const item = data.data;
// Create the macro command
const command = `game.sw5e.rollItemMacro("${item.name}");`;
let macro = game.macros.entities.find(m => (m.name === item.name) && (m.command === command));
if ( !macro ) {
macro = await Macro.create({
name: item.name,
type: "script",
img: item.img,
command: command,
flags: {"sw5e.itemMacro": true}
});
}
game.user.assignHotbarMacro(macro, slot);
return false;
// Create the macro command
const command = `game.sw5e.rollItemMacro("${item.name}");`;
let macro = game.macros.entities.find((m) => m.name === item.name && m.command === command);
if (!macro) {
macro = await Macro.create({
name: item.name,
type: "script",
img: item.img,
command: command,
flags: {"sw5e.itemMacro": true}
});
}
game.user.assignHotbarMacro(macro, slot);
return false;
}
/* -------------------------------------------- */
@ -40,21 +39,22 @@ export async function create5eMacro(data, slot) {
* @return {Promise}
*/
export function rollItemMacro(itemName) {
const speaker = ChatMessage.getSpeaker();
let actor;
if ( speaker.token ) actor = game.actors.tokens[speaker.token];
if ( !actor ) actor = game.actors.get(speaker.actor);
const speaker = ChatMessage.getSpeaker();
let actor;
if (speaker.token) actor = game.actors.tokens[speaker.token];
if (!actor) actor = game.actors.get(speaker.actor);
// Get matching items
const items = actor ? actor.items.filter(i => i.name === itemName) : [];
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.`);
} else if ( items.length === 0 ) {
return ui.notifications.warn(`Your controlled Actor does not have an item named ${itemName}`);
}
const item = items[0];
// Get matching items
const items = actor ? actor.items.filter((i) => i.name === itemName) : [];
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.`
);
} else if (items.length === 0) {
return ui.notifications.warn(`Your controlled Actor does not have an item named ${itemName}`);
}
const item = items[0];
// Trigger the item roll
if ( item.data.type === "power" ) return actor.usePower(item);
return item.roll();
// Trigger the item roll
return item.roll();
}

View file

@ -2,59 +2,68 @@
* Perform a system migration for the entire World, applying migrations for Actors, Items, and Compendium packs
* @return {Promise} A Promise which resolves once the migration is completed
*/
export const migrateWorld = async function() {
ui.notifications.info(`Applying SW5E System Migration for version ${game.system.data.version}. Please be patient and do not close your game or shut down your server.`, {permanent: true});
export const migrateWorld = async function () {
ui.notifications.info(
`Applying SW5e System Migration for version ${game.system.data.version}. Please be patient and do not close your game or shut down your server.`,
{permanent: true}
);
// Migrate World Actors
for ( let a of game.actors.entities ) {
try {
const updateData = migrateActorData(a.data);
if ( !isObjectEmpty(updateData) ) {
console.log(`Migrating Actor entity ${a.name}`);
await a.update(updateData, {enforceTypes: false});
}
} catch(err) {
console.error(err);
// Migrate World Actors
for await (let a of game.actors.contents) {
try {
console.log(`Checking Actor entity ${a.name} for migration needs`);
const updateData = await migrateActorData(a.data);
if (!foundry.utils.isObjectEmpty(updateData)) {
console.log(`Migrating Actor entity ${a.name}`);
await a.update(updateData, {enforceTypes: false});
}
} catch (err) {
err.message = `Failed sw5e system migration for Actor ${a.name}: ${err.message}`;
console.error(err);
}
}
}
// Migrate World Items
for ( let i of game.items.entities ) {
try {
const updateData = migrateItemData(i.data);
if ( !isObjectEmpty(updateData) ) {
console.log(`Migrating Item entity ${i.name}`);
await i.update(updateData, {enforceTypes: false});
}
} catch(err) {
console.error(err);
// Migrate World Items
for (let i of game.items.contents) {
try {
const updateData = migrateItemData(i.toObject());
if (!foundry.utils.isObjectEmpty(updateData)) {
console.log(`Migrating Item entity ${i.name}`);
await i.update(updateData, {enforceTypes: false});
}
} catch (err) {
err.message = `Failed sw5e system migration for Item ${i.name}: ${err.message}`;
console.error(err);
}
}
}
// Migrate Actor Override Tokens
for ( let s of game.scenes.entities ) {
try {
const updateData = migrateSceneData(s.data);
if ( !isObjectEmpty(updateData) ) {
console.log(`Migrating Scene entity ${s.name}`);
await s.update(updateData, {enforceTypes: false});
}
} catch(err) {
console.error(err);
// Migrate Actor Override Tokens
for (let s of game.scenes.contents) {
try {
const updateData = await migrateSceneData(s.data);
if (!foundry.utils.isObjectEmpty(updateData)) {
console.log(`Migrating Scene entity ${s.name}`);
await s.update(updateData, {enforceTypes: false});
// If we do not do this, then synthetic token actors remain in cache
// with the un-updated actorData.
s.tokens.contents.forEach((t) => (t._actor = null));
}
} catch (err) {
err.message = `Failed sw5e system migration for Scene ${s.name}: ${err.message}`;
console.error(err);
}
}
}
// Migrate World Compendium Packs
const packs = game.packs.filter(p => {
return (p.metadata.package === "world") && ["Actor", "Item", "Scene"].includes(p.metadata.entity)
});
for ( let p of packs ) {
await migrateCompendium(p);
}
// Migrate World Compendium Packs
for (let p of game.packs) {
if (p.metadata.package !== "world") continue;
if (!["Actor", "Item", "Scene"].includes(p.metadata.entity)) continue;
await migrateCompendium(p);
}
// Set the migration as complete
game.settings.set("sw5e", "systemMigrationVersion", game.system.data.version);
ui.notifications.info(`SW5E System Migration to version ${game.system.data.version} completed!`, {permanent: true});
// Set the migration as complete
game.settings.set("sw5e", "systemMigrationVersion", game.system.data.version);
ui.notifications.info(`SW5e System Migration to version ${game.system.data.version} completed!`, {permanent: true});
};
/* -------------------------------------------- */
@ -64,32 +73,48 @@ export const migrateWorld = async function() {
* @param pack
* @return {Promise}
*/
export const migrateCompendium = async function(pack) {
const entity = pack.metadata.entity;
if ( !["Actor", "Item", "Scene"].includes(entity) ) return;
export const migrateCompendium = async function (pack) {
const entity = pack.metadata.entity;
if (!["Actor", "Item", "Scene"].includes(entity)) return;
// Begin by requesting server-side data model migration and get the migrated content
await pack.migrate();
const content = await pack.getContent();
// Unlock the pack for editing
const wasLocked = pack.locked;
await pack.configure({locked: false});
// Iterate over compendium entries - applying fine-tuned migration functions
for ( let ent of content ) {
try {
let updateData = null;
if (entity === "Item") updateData = migrateItemData(ent.data);
else if (entity === "Actor") updateData = migrateActorData(ent.data);
else if ( entity === "Scene" ) updateData = migrateSceneData(ent.data);
if (!isObjectEmpty(updateData)) {
expandObject(updateData);
updateData["_id"] = ent._id;
await pack.updateEntity(updateData);
console.log(`Migrated ${entity} entity ${ent.name} in Compendium ${pack.collection}`);
}
} catch(err) {
console.error(err);
// Begin by requesting server-side data model migration and get the migrated content
await pack.migrate();
const documents = await pack.getDocuments();
// Iterate over compendium entries - applying fine-tuned migration functions
for await (let doc of documents) {
let updateData = {};
try {
switch (entity) {
case "Actor":
updateData = await migrateActorData(doc.data);
break;
case "Item":
updateData = migrateItemData(doc.toObject());
break;
case "Scene":
updateData = await migrateSceneData(doc.data);
break;
}
if (foundry.utils.isObjectEmpty(updateData)) continue;
// Save the entry, if data was changed
await doc.update(updateData);
console.log(`Migrated ${entity} entity ${doc.name} in Compendium ${pack.collection}`);
} catch (err) {
// Handle migration failures
err.message = `Failed sw5e system migration for entity ${doc.name} in pack ${pack.collection}: ${err.message}`;
console.error(err);
}
}
}
console.log(`Migrated all ${entity} entities from Compendium ${pack.collection}`);
// Apply the original locked status for the pack
await pack.configure({locked: wasLocked});
console.log(`Migrated all ${entity} entities from Compendium ${pack.collection}`);
};
/* -------------------------------------------- */
@ -99,85 +124,113 @@ export const migrateCompendium = async function(pack) {
/**
* Migrate a single Actor entity to incorporate latest data model changes
* Return an Object of updateData to be applied
* @param {Actor} actor The actor to Update
* @return {Object} The updateData to apply
* @param {object} actor The actor data object to update
* @return {Object} The updateData to apply
*/
export const migrateActorData = function(actor) {
const updateData = {};
export const migrateActorData = async function (actor) {
const updateData = {};
// Actor Data Updates
_migrateActorBonuses(actor, updateData);
// Remove deprecated fields
_migrateRemoveDeprecated(actor, updateData);
// Migrate Owned Items
if ( !actor.items ) return updateData;
let hasItemUpdates = false;
const items = actor.items.map(i => {
// Migrate the Owned Item
let itemUpdate = migrateItemData(i);
// Prepared, Equipped, and Proficient for NPC actors
if ( actor.type === "npc" ) {
if (getProperty(i.data, "preparation.prepared") === false) itemUpdate["data.preparation.prepared"] = true;
if (getProperty(i.data, "equipped") === false) itemUpdate["data.equipped"] = true;
if (getProperty(i.data, "proficient") === false) itemUpdate["data.proficient"] = true;
// Actor Data Updates
if (actor.data) {
_migrateActorMovement(actor, updateData);
_migrateActorSenses(actor, updateData);
_migrateActorType(actor, updateData);
}
// Update the Owned Item
if ( !isObjectEmpty(itemUpdate) ) {
hasItemUpdates = true;
return mergeObject(i, itemUpdate, {enforceTypes: false, inplace: false});
} else return i;
});
if ( hasItemUpdates ) updateData.items = items;
return updateData;
// Migrate Owned Items
if (!!actor.items) {
const items = await actor.items.reduce(async (memo, i) => {
const results = await memo;
// Migrate the Owned Item
const itemData = i instanceof CONFIG.Item.documentClass ? i.toObject() : i;
let itemUpdate = await migrateActorItemData(itemData, actor);
// Prepared, Equipped, and Proficient for NPC actors
if (actor.type === "npc") {
if (getProperty(itemData.data, "preparation.prepared") === false)
itemUpdate["data.preparation.prepared"] = true;
if (getProperty(itemData.data, "equipped") === false) itemUpdate["data.equipped"] = true;
if (getProperty(itemData.data, "proficient") === false) itemUpdate["data.proficient"] = true;
}
// Update the Owned Item
if (!isObjectEmpty(itemUpdate)) {
itemUpdate._id = itemData._id;
console.log(`Migrating Actor ${actor.name}'s ${i.name}`);
results.push(expandObject(itemUpdate));
}
return results;
}, []);
if (items.length > 0) updateData.items = items;
}
// Update NPC data with new datamodel information
if (actor.type === "npc") {
_updateNPCData(actor);
}
// migrate powers last since it relies on item classes being migrated first.
_migrateActorPowers(actor, updateData);
return updateData;
};
/* -------------------------------------------- */
/**
* Scrub an Actor's system data, removing all keys which are not explicitly defined in the system template
* @param {Object} actorData The data object for an Actor
* @return {Object} The scrubbed Actor data
*/
function cleanActorData(actorData) {
// Scrub system data
const model = game.system.model.Actor[actorData.type];
actorData.data = filterObject(actorData.data, model);
// Scrub system data
const model = game.system.model.Actor[actorData.type];
actorData.data = filterObject(actorData.data, model);
// Scrub system flags
const allowedFlags = CONFIG.SW5E.allowedActorFlags.reduce((obj, f) => {
obj[f] = null;
return obj;
}, {});
if (actorData.flags.sw5e) {
actorData.flags.sw5e = filterObject(actorData.flags.sw5e, allowedFlags);
}
// Scrub system flags
const allowedFlags = CONFIG.SW5E.allowedActorFlags.reduce((obj, f) => {
obj[f] = null;
return obj;
}, {});
if ( actorData.flags.sw5e ) {
actorData.flags.sw5e = filterObject(actorData.flags.sw5e, allowedFlags);
}
// Return the scrubbed data
return actorData;
// Return the scrubbed data
return actorData;
}
/* -------------------------------------------- */
/**
* Migrate a single Item entity to incorporate latest data model changes
* @param item
*
* @param {object} item Item data to migrate
* @return {object} The updateData to apply
*/
export const migrateItemData = function(item) {
const updateData = {};
export const migrateItemData = function (item) {
const updateData = {};
_migrateItemClassPowerCasting(item, updateData);
_migrateItemAttunement(item, updateData);
return updateData;
};
// Remove deprecated fields
_migrateRemoveDeprecated(item, updateData);
/* -------------------------------------------- */
// Return the migrated update data
return updateData;
/**
* Migrate a single owned actor Item entity to incorporate latest data model changes
* @param item
* @param actor
*/
export const migrateActorItemData = async function (item, actor) {
const updateData = {};
_migrateItemClassPowerCasting(item, updateData);
_migrateItemAttunement(item, updateData);
await _migrateItemPower(item, actor, updateData);
return updateData;
};
/* -------------------------------------------- */
@ -188,72 +241,428 @@ export const migrateItemData = function(item) {
* @param {Object} scene The Scene data to Update
* @return {Object} The updateData to apply
*/
export const migrateSceneData = function(scene) {
const tokens = duplicate(scene.tokens);
return {
tokens: tokens.map(t => {
if (!t.actorId || t.actorLink || !t.actorData.data) {
t.actorData = {};
return t;
}
const token = new Token(t);
if ( !token.actor ) {
t.actorId = null;
t.actorData = {};
} else if ( !t.actorLink ) {
const updateData = migrateActorData(token.data.actorData);
t.actorData = mergeObject(token.data.actorData, updateData);
}
return t;
})
};
export const migrateSceneData = async function (scene) {
const tokens = await Promise.all(
scene.tokens.map(async (token) => {
const t = token.toJSON();
if (!t.actorId || t.actorLink) {
t.actorData = {};
} else if (!game.actors.has(t.actorId)) {
t.actorId = null;
t.actorData = {};
} else if (!t.actorLink) {
const actorData = duplicate(t.actorData);
actorData.type = token.actor?.type;
const update = migrateActorData(actorData);
["items", "effects"].forEach((embeddedName) => {
if (!update[embeddedName]?.length) return;
const updates = new Map(update[embeddedName].map((u) => [u._id, u]));
t.actorData[embeddedName].forEach((original) => {
const update = updates.get(original._id);
if (update) mergeObject(original, update);
});
delete update[embeddedName];
});
mergeObject(t.actorData, update);
}
return t;
})
);
return {tokens};
};
/* -------------------------------------------- */
/* Low level migration utilities
/* -------------------------------------------- */
/* -------------------------------------------- */
/**
* Migrate the actor bonuses object
* @private
* Update an NPC Actor's data based on compendium
* @param {Object} actor The data object for an Actor
* @return {Object} The updated Actor
*/
function _migrateActorBonuses(actor, updateData) {
const b = game.system.model.Actor.character.bonuses;
for ( let k of Object.keys(actor.data.bonuses || {}) ) {
if ( k in b ) updateData[`data.bonuses.${k}`] = b[k];
else updateData[`data.bonuses.-=${k}`] = null;
}
function _updateNPCData(actor) {
let actorData = actor.data;
const updateData = {};
// check for flag.core, if not there is no compendium monster so exit
const hasSource = actor?.flags?.core?.sourceId !== undefined;
if (!hasSource) return actor;
// shortcut out if dataVersion flag is set to 1.2.4 or higher
const hasDataVersion = actor?.flags?.sw5e?.dataVersion !== undefined;
if (
hasDataVersion &&
(actor.flags.sw5e.dataVersion === "1.2.4" || isNewerVersion("1.2.4", actor.flags.sw5e.dataVersion))
)
return actor;
// Check to see what the source of NPC is
const sourceId = actor.flags.core.sourceId;
const coreSource = sourceId.substr(0, sourceId.length - 17);
const core_id = sourceId.substr(sourceId.length - 16, 16);
if (coreSource === "Compendium.sw5e.monsters") {
game.packs
.get("sw5e.monsters")
.getEntity(core_id)
.then((monster) => {
const monsterData = monster.data.data;
// copy movement[], senses[], powercasting, force[], tech[], powerForceLevel, powerTechLevel
updateData["data.attributes.movement"] = monsterData.attributes.movement;
updateData["data.attributes.senses"] = monsterData.attributes.senses;
updateData["data.attributes.powercasting"] = monsterData.attributes.powercasting;
updateData["data.attributes.force"] = monsterData.attributes.force;
updateData["data.attributes.tech"] = monsterData.attributes.tech;
updateData["data.details.powerForceLevel"] = monsterData.details.powerForceLevel;
updateData["data.details.powerTechLevel"] = monsterData.details.powerTechLevel;
// push missing powers onto actor
let newPowers = [];
for (let i of monster.items) {
const itemData = i.data;
if (itemData.type === "power") {
const itemCompendium_id = itemData.flags?.core?.sourceId.split(".").slice(-1)[0];
let hasPower = !!actor.items.find(
(item) => item.flags?.core?.sourceId.split(".").slice(-1)[0] === itemCompendium_id
);
if (!hasPower) {
// Clone power to new object. Don't know if it is technically needed, but seems to prevent some weirdness.
const newPower = JSON.parse(JSON.stringify(itemData));
newPowers.push(newPower);
}
}
}
// get actor to create new powers
const liveActor = game.actors.get(actor._id);
// create the powers on the actor
liveActor.createEmbeddedEntity("OwnedItem", newPowers);
// set flag to check to see if migration has been done so we don't do it again.
liveActor.setFlag("sw5e", "dataVersion", "1.2.4");
});
}
//merge object
actorData = mergeObject(actorData, updateData);
// Return the scrubbed data
return actor;
}
/* -------------------------------------------- */
/**
* A general migration to remove all fields from the data model which are flagged with a _deprecated tag
* Migrate the actor speed string to movement object
* @private
*/
const _migrateRemoveDeprecated = function(ent, updateData) {
const flat = flattenObject(ent.data);
function _migrateActorMovement(actorData, updateData) {
const ad = actorData.data;
// Identify objects to deprecate
const toDeprecate = Object.entries(flat).filter(e => e[0].endsWith("_deprecated") && (e[1] === true)).map(e => {
let parent = e[0].split(".");
parent.pop();
return parent.join(".");
});
// Remove them
for ( let k of toDeprecate ) {
let parts = k.split(".");
parts[parts.length-1] = "-=" + parts[parts.length-1];
updateData[`data.${parts.join(".")}`] = null;
}
};
// Work is needed if old data is present
const old = actorData.type === "vehicle" ? ad?.attributes?.speed : ad?.attributes?.speed?.value;
const hasOld = old !== undefined;
if (hasOld) {
// If new data is not present, migrate the old data
const hasNew = ad?.attributes?.movement?.walk !== undefined;
if (!hasNew && typeof old === "string") {
const s = (old || "").split(" ");
if (s.length > 0)
updateData["data.attributes.movement.walk"] = Number.isNumeric(s[0]) ? parseInt(s[0]) : null;
}
// Remove the old attribute
updateData["data.attributes.-=speed"] = null;
}
return updateData;
}
/* -------------------------------------------- */
/**
* Migrate the actor speed string to movement object
* @private
*/
function _migrateActorPowers(actorData, updateData) {
const ad = actorData.data;
// If new Force & Tech data is not present, create it
let hasNewAttrib = ad?.attributes?.force?.level !== undefined;
if (!hasNewAttrib) {
updateData["data.attributes.force.known.value"] = 0;
updateData["data.attributes.force.known.max"] = 0;
updateData["data.attributes.force.points.value"] = 0;
updateData["data.attributes.force.points.min"] = 0;
updateData["data.attributes.force.points.max"] = 0;
updateData["data.attributes.force.points.temp"] = null;
updateData["data.attributes.force.points.tempmax"] = null;
updateData["data.attributes.force.level"] = 0;
updateData["data.attributes.tech.known.value"] = 0;
updateData["data.attributes.tech.known.max"] = 0;
updateData["data.attributes.tech.points.value"] = 0;
updateData["data.attributes.tech.points.min"] = 0;
updateData["data.attributes.tech.points.max"] = 0;
updateData["data.attributes.tech.points.temp"] = null;
updateData["data.attributes.tech.points.tempmax"] = null;
updateData["data.attributes.tech.level"] = 0;
}
// If new Power F/T split data is not present, create it
const hasNewLimit = ad?.powers?.power1?.foverride !== undefined;
if (!hasNewLimit) {
for (let i = 1; i <= 9; i++) {
// add new
updateData["data.powers.power" + i + ".fvalue"] = getProperty(ad.powers, "power" + i + ".value");
updateData["data.powers.power" + i + ".fmax"] = getProperty(ad.powers, "power" + i + ".max");
updateData["data.powers.power" + i + ".foverride"] = null;
updateData["data.powers.power" + i + ".tvalue"] = getProperty(ad.powers, "power" + i + ".value");
updateData["data.powers.power" + i + ".tmax"] = getProperty(ad.powers, "power" + i + ".max");
updateData["data.powers.power" + i + ".toverride"] = null;
//remove old
updateData["data.powers.power" + i + ".-=value"] = null;
updateData["data.powers.power" + i + ".-=override"] = null;
}
}
// If new Bonus Power DC data is not present, create it
const hasNewBonus = ad?.bonuses?.power?.forceLightDC !== undefined;
if (!hasNewBonus) {
updateData["data.bonuses.power.forceLightDC"] = "";
updateData["data.bonuses.power.forceDarkDC"] = "";
updateData["data.bonuses.power.forceUnivDC"] = "";
updateData["data.bonuses.power.techDC"] = "";
}
// Remove the Power DC Bonus
updateData["data.bonuses.power.-=dc"] = null;
return updateData;
}
/* -------------------------------------------- */
/**
* Migrate the actor traits.senses string to attributes.senses object
* @private
*/
function _migrateActorSenses(actor, updateData) {
const ad = actor.data;
if (ad?.traits?.senses === undefined) return;
const original = ad.traits.senses || "";
if (typeof original !== "string") return;
// Try to match old senses with the format like "Darkvision 60 ft, Blindsight 30 ft"
const pattern = /([A-z]+)\s?([0-9]+)\s?([A-z]+)?/;
let wasMatched = false;
// Match each comma-separated term
for (let s of original.split(",")) {
s = s.trim();
const match = s.match(pattern);
if (!match) continue;
const type = match[1].toLowerCase();
if (type in CONFIG.SW5E.senses) {
updateData[`data.attributes.senses.${type}`] = Number(match[2]).toNearest(0.5);
wasMatched = true;
}
}
// If nothing was matched, but there was an old string - put the whole thing in "special"
if (!wasMatched && !!original) {
updateData["data.attributes.senses.special"] = original;
}
// Remove the old traits.senses string once the migration is complete
updateData["data.traits.-=senses"] = null;
return updateData;
}
/* -------------------------------------------- */
/**
* Migrate the actor details.type string to object
* @private
*/
function _migrateActorType(actor, updateData) {
const ad = actor.data;
const original = ad.details?.type;
if (typeof original !== "string") return;
// New default data structure
let data = {
value: "",
subtype: "",
swarm: "",
custom: ""
};
// Specifics
// (Some of these have weird names, these need to be addressed individually)
if (original === "force entity") {
data.value = "force";
data.subtype = "storm";
} else if (original === "human") {
data.value = "humanoid";
data.subtype = "human";
} else if (["humanoid (any)", "humanoid (Villainous"].includes(original)) {
data.value = "humanoid";
} else if (original === "tree") {
data.value = "plant";
data.subtype = "tree";
} else if (original === "(humanoid) or Large (beast) force entity") {
data.value = "force";
} else if (original === "droid (appears human)") {
data.value = "droid";
} else {
// Match the existing string
const pattern = /^(?:swarm of (?<size>[\w\-]+) )?(?<type>[^(]+?)(?:\((?<subtype>[^)]+)\))?$/i;
const match = original.trim().match(pattern);
if (match) {
// Match a known creature type
const typeLc = match.groups.type.trim().toLowerCase();
const typeMatch = Object.entries(CONFIG.SW5E.creatureTypes).find(([k, v]) => {
return (
typeLc === k ||
typeLc === game.i18n.localize(v).toLowerCase() ||
typeLc === game.i18n.localize(`${v}Pl`).toLowerCase()
);
});
if (typeMatch) data.value = typeMatch[0];
else {
data.value = "custom";
data.custom = match.groups.type.trim().titleCase();
}
data.subtype = match.groups.subtype?.trim().titleCase() || "";
// Match a swarm
const isNamedSwarm = actor.name.startsWith(game.i18n.localize("SW5E.CreatureSwarm"));
if (match.groups.size || isNamedSwarm) {
const sizeLc = match.groups.size ? match.groups.size.trim().toLowerCase() : "tiny";
const sizeMatch = Object.entries(CONFIG.SW5E.actorSizes).find(([k, v]) => {
return sizeLc === k || sizeLc === game.i18n.localize(v).toLowerCase();
});
data.swarm = sizeMatch ? sizeMatch[0] : "tiny";
} else data.swarm = "";
}
// No match found
else {
data.value = "custom";
data.custom = original;
}
}
// Update the actor data
updateData["data.details.type"] = data;
return updateData;
}
/* -------------------------------------------- */
/**
* @private
*/
function _migrateItemClassPowerCasting(item, updateData) {
if (item.type === "class") {
switch (item.name) {
case "Consular":
updateData["data.powercasting"] = {
progression: "consular",
ability: ""
};
break;
case "Engineer":
updateData["data.powercasting"] = {
progression: "engineer",
ability: ""
};
break;
case "Guardian":
updateData["data.powercasting"] = {
progression: "guardian",
ability: ""
};
break;
case "Scout":
updateData["data.powercasting"] = {
progression: "scout",
ability: ""
};
break;
case "Sentinel":
updateData["data.powercasting"] = {
progression: "sentinel",
ability: ""
};
break;
}
}
return updateData;
}
/* -------------------------------------------- */
/**
* Update an Power Item's data based on compendium
* @param {Object} item The data object for an item
* @param {Object} actor The data object for the actor owning the item
* @private
*/
async function _migrateItemPower(item, actor, updateData) {
// if item is not a power shortcut out
if (item.type !== "power") return updateData;
console.log(`Checking Actor ${actor.name}'s ${item.name} for migration needs`);
// check for flag.core, if not there is no compendium power so exit
const hasSource = item?.flags?.core?.sourceId !== undefined;
if (!hasSource) return updateData;
// shortcut out if dataVersion flag is set to 1.2.4 or higher
const hasDataVersion = item?.flags?.sw5e?.dataVersion !== undefined;
if (
hasDataVersion &&
(item.flags.sw5e.dataVersion === "1.2.4" || isNewerVersion("1.2.4", item.flags.sw5e.dataVersion))
)
return updateData;
// Check to see what the source of Power is
const sourceId = item.flags.core.sourceId;
const coreSource = sourceId.substr(0, sourceId.length - 17);
const core_id = sourceId.substr(sourceId.length - 16, 16);
//if power type is not force or tech exit out
let powerType = "none";
if (coreSource === "Compendium.sw5e.forcepowers") powerType = "sw5e.forcepowers";
if (coreSource === "Compendium.sw5e.techpowers") powerType = "sw5e.techpowers";
if (powerType === "none") return updateData;
const corePower = duplicate(await game.packs.get(powerType).getEntity(core_id));
console.log(`Updating Actor ${actor.name}'s ${item.name} from compendium`);
const corePowerData = corePower.data;
// copy Core Power Data over original Power
updateData["data"] = corePowerData;
updateData["flags"] = {sw5e: {dataVersion: "1.2.4"}};
return updateData;
//game.packs.get(powerType).getEntity(core_id).then(corePower => {
//})
}
/* -------------------------------------------- */
/**
* Delete the old data.attuned boolean
*
* @param {object} item Item data to migrate
* @param {object} updateData Existing update to expand upon
* @return {object} The updateData to apply
* @private
*/
function _migrateItemAttunement(item, updateData) {
if (item.data?.attuned === undefined) return updateData;
updateData["data.attunement"] = CONFIG.SW5E.attunementTypes.NONE;
updateData["data.-=attuned"] = null;
return updateData;
}
/* -------------------------------------------- */
/**
* A general tool to purge flags from all entities in a Compendium pack.
@ -261,22 +670,41 @@ const _migrateRemoveDeprecated = function(ent, updateData) {
* @private
*/
export async function purgeFlags(pack) {
const cleanFlags = (flags) => {
const flags5e = flags.sw5e || null;
return flags5e ? {sw5e: flags5e} : {};
};
await pack.configure({locked: false});
const content = await pack.getContent();
for ( let entity of content ) {
const update = {_id: entity.id, flags: cleanFlags(entity.data.flags)};
if ( pack.entity === "Actor" ) {
update.items = entity.data.items.map(i => {
i.flags = cleanFlags(i.flags);
return i;
})
const cleanFlags = (flags) => {
const flags5e = flags.sw5e || null;
return flags5e ? {sw5e: flags5e} : {};
};
await pack.configure({locked: false});
const content = await pack.getContent();
for (let entity of content) {
const update = {_id: entity.id, flags: cleanFlags(entity.data.flags)};
if (pack.entity === "Actor") {
update.items = entity.data.items.map((i) => {
i.flags = cleanFlags(i.flags);
return i;
});
}
await pack.updateEntity(update, {recursive: false});
console.log(`Purged flags from ${entity.name}`);
}
await pack.updateEntity(update, {recursive: false});
console.log(`Purged flags from ${entity.name}`);
}
await pack.configure({locked: true});
await pack.configure({locked: true});
}
/* -------------------------------------------- */
/**
* Purge the data model of any inner objects which have been flagged as _deprecated.
* @param {object} data The data to clean
* @private
*/
export function removeDeprecatedObjects(data) {
for (let [k, v] of Object.entries(data)) {
if (getType(v) === "Object") {
if (v._deprecated === true) {
console.log(`Deleting deprecated object key ${k}`);
delete data[k];
} else removeDeprecatedObjects(v);
}
}
return data;
}

View file

@ -1,126 +1,132 @@
import { SW5E } from "../config.js";
import {SW5E} from "../config.js";
/**
* A helper class for building MeasuredTemplates for 5e powers and abilities
* @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;
/**
* 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
const templateData = {
t: templateShape,
user: game.user.data._id,
distance: target.value,
direction: 0,
x: 0,
y: 0,
fillColor: game.user.color
};
// Prepare template data
const templateData = {
t: templateShape,
user: game.user._id,
distance: target.value,
direction: 0,
x: 0,
y: 0,
fillColor: game.user.color
};
// Additional type-specific data
switch (templateShape) {
case "cone":
templateData.angle = CONFIG.MeasuredTemplate.defaults.angle;
break;
case "rect": // 5e rectangular AoEs are always cubes
templateData.distance = Math.hypot(target.value, target.value);
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;
}
// Additional type-specific data
switch ( templateShape ) {
case "cone": // 5e cone RAW should be 53.13 degrees
templateData.angle = 53.13;
break;
case "rect": // 5e rectangular AoEs are always cubes
templateData.distance = Math.hypot(target.value, target.value);
templateData.width = target.value;
templateData.direction = 45;
break;
case "ray": // 5e rays are most commonly 1 square (5 ft) in width
templateData.width = canvas.dimensions.distance;
break;
default:
break;
// Return the template constructed from the item data
const cls = CONFIG.MeasuredTemplate.documentClass;
const template = new cls(templateData, {parent: canvas.scene});
const object = new this(template);
object.item = item;
object.actorSheet = item.actor?.sheet || null;
return object;
}
// Return the template constructed from the item data
return new this(templateData);
}
/* -------------------------------------------- */
/* -------------------------------------------- */
/**
* Creates a preview of the power template
*/
drawPreview() {
const initialLayer = canvas.activeLayer;
/**
* Creates a preview of the power template
*/
drawPreview() {
const initialLayer = canvas.activeLayer;
this.draw();
this.layer.activate();
this.layer.preview.addChild(this);
this.activatePreviewListeners(initialLayer);
}
// Draw the template and switch to the template layer
this.draw();
this.layer.activate();
this.layer.preview.addChild(this);
/* -------------------------------------------- */
// Hide the sheet that originated the preview
if (this.actorSheet) this.actorSheet.minimize();
/**
* 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;
// Activate interactivity
this.activatePreviewListeners(initialLayer);
}
// Update placement (mouse-move)
handlers.mm = event => {
event.stopPropagation();
let now = Date.now(); // Apply a 20ms throttle
if ( now - moveTime <= 20 ) return;
const center = event.data.getLocalPosition(this.layer);
const snapped = canvas.grid.getSnappedPosition(center.x, center.y, 2);
this.data.x = snapped.x;
this.data.y = snapped.y;
this.refresh();
moveTime = now;
};
/* -------------------------------------------- */
// Cancel the workflow (right-click)
handlers.rc = event => {
this.layer.preview.removeChildren();
canvas.stage.off("mousemove", handlers.mm);
canvas.stage.off("mousedown", handlers.lc);
canvas.app.view.oncontextmenu = null;
canvas.app.view.onwheel = null;
initialLayer.activate();
};
/**
* 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;
// Confirm the workflow (left-click)
handlers.lc = event => {
handlers.rc(event);
// Update placement (mouse-move)
handlers.mm = (event) => {
event.stopPropagation();
let now = Date.now(); // Apply a 20ms throttle
if (now - moveTime <= 20) return;
const center = event.data.getLocalPosition(this.layer);
const snapped = canvas.grid.getSnappedPosition(center.x, center.y, 2);
this.data.update({x: snapped.x, y: snapped.y});
this.refresh();
moveTime = now;
};
// Confirm final snapped position
const destination = canvas.grid.getSnappedPosition(this.x, this.y, 2);
this.data.x = destination.x;
this.data.y = destination.y;
// Cancel the workflow (right-click)
handlers.rc = (event) => {
this.layer.preview.removeChildren();
canvas.stage.off("mousemove", handlers.mm);
canvas.stage.off("mousedown", handlers.lc);
canvas.app.view.oncontextmenu = null;
canvas.app.view.onwheel = null;
initialLayer.activate();
this.actorSheet.maximize();
};
// Create the template
canvas.scene.createEmbeddedEntity("MeasuredTemplate", this.data);
};
// Confirm the workflow (left-click)
handlers.lc = (event) => {
handlers.rc(event);
const destination = canvas.grid.getSnappedPosition(this.data.x, this.data.y, 2);
this.data.update(destination);
canvas.scene.createEmbeddedDocuments("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();
};
// 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.update({direction: 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;
}
// 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() {
/**
* 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: Number,
default: 0
});
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
});
/**
* Register resting variants
*/
game.settings.register("sw5e", "restVariant", {
name: "SETTINGS.5eRestN",
hint: "SETTINGS.5eRestL",
scope: "world",
config: true,
default: "normal",
type: String,
choices: {
"normal": "SETTINGS.5eRestPHB",
"gritty": "SETTINGS.5eRestGritty",
"epic": "SETTINGS.5eRestEpic",
}
});
* Register resting variants
*/
game.settings.register("sw5e", "restVariant", {
name: "SETTINGS.5eRestN",
hint: "SETTINGS.5eRestL",
scope: "world",
config: true,
default: "normal",
type: String,
choices: {
normal: "SETTINGS.5eRestPHB",
gritty: "SETTINGS.5eRestGritty",
epic: "SETTINGS.5eRestEpic"
}
});
/**
* Register diagonal movement rule setting
*/
game.settings.register("sw5e", "diagonalMovement", {
name: "SETTINGS.5eDiagN",
hint: "SETTINGS.5eDiagL",
scope: "world",
config: true,
default: "555",
type: String,
choices: {
"555": "SETTINGS.5eDiagPHB",
"5105": "SETTINGS.5eDiagDMG",
"EUCL": "SETTINGS.5eDiagEuclidean",
},
onChange: rule => canvas.grid.diagonalRule = rule
});
/**
* Register diagonal movement rule setting
*/
game.settings.register("sw5e", "diagonalMovement", {
name: "SETTINGS.5eDiagN",
hint: "SETTINGS.5eDiagL",
scope: "world",
config: true,
default: "555",
type: String,
choices: {
555: "SETTINGS.5eDiagPHB",
5105: "SETTINGS.5eDiagDMG",
EUCL: "SETTINGS.5eDiagEuclidean"
},
onChange: (rule) => (canvas.grid.diagonalRule = rule)
});
/**
* Register Initiative formula setting
*/
game.settings.register("sw5e", "initiativeDexTiebreaker", {
name: "SETTINGS.5eInitTBN",
hint: "SETTINGS.5eInitTBL",
scope: "world",
config: true,
default: false,
type: Boolean
});
/**
* Register Initiative formula setting
*/
game.settings.register("sw5e", "initiativeDexTiebreaker", {
name: "SETTINGS.5eInitTBN",
hint: "SETTINGS.5eInitTBL",
scope: "world",
config: true,
default: false,
type: Boolean
});
/**
* Require Currency Carrying Weight
*/
game.settings.register("sw5e", "currencyWeight", {
name: "SETTINGS.5eCurWtN",
hint: "SETTINGS.5eCurWtL",
scope: "world",
config: true,
default: true,
type: Boolean
});
/**
* Require Currency Carrying Weight
*/
game.settings.register("sw5e", "currencyWeight", {
name: "SETTINGS.5eCurWtN",
hint: "SETTINGS.5eCurWtL",
scope: "world",
config: true,
default: true,
type: Boolean
});
/**
* Option to disable XP bar for session-based or story-based advancement.
*/
game.settings.register("sw5e", "disableExperienceTracking", {
name: "SETTINGS.5eNoExpN",
hint: "SETTINGS.5eNoExpL",
scope: "world",
config: true,
default: false,
type: Boolean,
});
/**
* Option to disable XP bar for session-based or story-based advancement.
*/
game.settings.register("sw5e", "disableExperienceTracking", {
name: "SETTINGS.5eNoExpN",
hint: "SETTINGS.5eNoExpL",
scope: "world",
config: true,
default: false,
type: Boolean
});
/**
* Option to automatically create Power Measured Template on roll
*/
game.settings.register("sw5e", "alwaysPlacePowerTemplate", {
name: "SETTINGS.5eAutoPowerTemplateN",
hint: "SETTINGS.5eAutoPowerTemplateL",
scope: "client",
config: true,
default: false,
type: Boolean
});
/**
* Option to automatically collapse Item Card descriptions
*/
game.settings.register("sw5e", "autoCollapseItemCards", {
name: "SETTINGS.5eAutoCollapseCardN",
hint: "SETTINGS.5eAutoCollapseCardL",
scope: "client",
config: true,
default: false,
type: Boolean,
onChange: (s) => {
ui.chat.render();
}
});
/**
* Option to automatically collapse Item Card descriptions
*/
game.settings.register("sw5e", "autoCollapseItemCards", {
name: "SETTINGS.5eAutoCollapseCardN",
hint: "SETTINGS.5eAutoCollapseCardL",
scope: "client",
config: true,
default: false,
type: Boolean,
onChange: s => {
ui.chat.render();
}
});
/**
* Option to allow GMs to restrict polymorphing to GMs only.
*/
game.settings.register("sw5e", "allowPolymorphing", {
name: "SETTINGS.5eAllowPolymorphingN",
hint: "SETTINGS.5eAllowPolymorphingL",
scope: "world",
config: true,
default: false,
type: Boolean
});
/**
* Option to allow GMs to restrict polymorphing to GMs only.
*/
game.settings.register('sw5e', 'allowPolymorphing', {
name: 'SETTINGS.5eAllowPolymorphingN',
hint: 'SETTINGS.5eAllowPolymorphingL',
scope: 'world',
config: true,
default: false,
type: Boolean
});
/**
* Remember last-used polymorph settings.
*/
game.settings.register('sw5e', 'polymorphSettings', {
scope: 'client',
default: {
keepPhysical: false,
keepMental: false,
keepSaves: false,
keepSkills: false,
mergeSaves: false,
mergeSkills: false,
keepClass: false,
keepFeats: false,
keepPowers: false,
keepItems: false,
keepBio: false,
keepVision: true,
transformTokens: true
}
});
/**
* Remember last-used polymorph settings.
*/
game.settings.register("sw5e", "polymorphSettings", {
scope: "client",
default: {
keepPhysical: false,
keepMental: false,
keepSaves: false,
keepSkills: false,
mergeSaves: false,
mergeSkills: false,
keepClass: false,
keepFeats: false,
keepPowers: false,
keepItems: false,
keepBio: false,
keepVision: true,
transformTokens: true
}
});
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,25 +3,33 @@
* Pre-loaded templates are compiled and cached for fast access when rendering
* @return {Promise}
*/
export const preloadHandlebarsTemplates = async function() {
export const preloadHandlebarsTemplates = async function () {
return loadTemplates([
// Shared Partials
"systems/sw5e/templates/actors/parts/active-effects.html",
// Define template paths to load
const templatePaths = [
// Actor Sheet Partials
"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/parts/actor-traits.html",
"systems/sw5e/templates/actors/parts/actor-inventory.html",
"systems/sw5e/templates/actors/parts/actor-features.html",
"systems/sw5e/templates/actors/parts/actor-powerbook.html",
"systems/sw5e/templates/actors/parts/actor-effects.html",
"systems/sw5e/templates/actors/newActor/parts/swalt-biography.html",
"systems/sw5e/templates/actors/newActor/parts/swalt-core.html",
"systems/sw5e/templates/actors/newActor/parts/swalt-crew.html",
"systems/sw5e/templates/actors/newActor/parts/swalt-active-effects.html",
"systems/sw5e/templates/actors/newActor/parts/swalt-features.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",
// 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"
];
// Load the template parts
return loadTemplates(templatePaths);
// 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);
}
}

3105
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

9
package.json Normal file
View file

@ -0,0 +1,9 @@
{
"name": "sw5e",
"description": "This game system for [Foundry Virtual Tabletop](http://foundryvtt.com) provides character sheet and game system \r support for the SW5E roleplaying game.",
"main": "sw5e.js",
"dependencies": {
"gulp": "^4.0.2",
"gulp-less": "^4.0.1"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

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