Compare commits
542 commits
dnd5-Core-
...
master
Author | SHA1 | Date | |
---|---|---|---|
![]() |
2212e3c7d8 | ||
![]() |
8c0ad582f7 | ||
![]() |
584767b352 | ||
![]() |
d1b123100e | ||
![]() |
0a9c9f8ef0 | ||
![]() |
ee7418f552 | ||
![]() |
c44ad926a5 | ||
![]() |
10ee20354b | ||
![]() |
7214e3d260 | ||
![]() |
97df236b54 | ||
![]() |
cac466462b | ||
![]() |
063d529f09 | ||
![]() |
b343a06ef6 | ||
![]() |
fc09308d11 | ||
![]() |
7a18055c18 | ||
![]() |
e271c41239 | ||
![]() |
5d879f99e2 | ||
![]() |
df44ad0635 | ||
![]() |
aff43d3e98 | ||
![]() |
211201caea | ||
![]() |
7a29dfe600 | ||
![]() |
c467738845 | ||
![]() |
8a0940ccce | ||
![]() |
3b4300a8eb | ||
![]() |
55bbb95cfb | ||
![]() |
62e31afff2 | ||
![]() |
da5223cab8 | ||
![]() |
db286f7883 | ||
![]() |
25684173fa | ||
![]() |
74d841e9e1 | ||
![]() |
53064c0e09 | ||
![]() |
9a21ce2b2a | ||
![]() |
29a639ff90 | ||
![]() |
f18e537561 | ||
![]() |
bac8e3d642 | ||
![]() |
65594f62a3 | ||
![]() |
e30d823225 | ||
![]() |
17de2a89c2 | ||
![]() |
95b2b1e39c | ||
![]() |
fe520f2c0d | ||
![]() |
9a86bf7857 | ||
![]() |
88f5c0cbed | ||
![]() |
c0e71fe0f3 | ||
![]() |
ffffe5da52 | ||
![]() |
cd9bdf61d2 | ||
![]() |
e2f002292b | ||
![]() |
6c2d89ee82 | ||
![]() |
76ef89b518 | ||
![]() |
b414abbb81 | ||
![]() |
53a845feb7 | ||
![]() |
7134c4ac07 | ||
![]() |
585de42a46 | ||
![]() |
2007d116a2 | ||
![]() |
104e49615d | ||
![]() |
6041564835 | ||
![]() |
64bae2140c | ||
![]() |
c3cbc96499 | ||
![]() |
37a3e83f3a | ||
![]() |
db5e90281c | ||
![]() |
92bf020cdf | ||
![]() |
d0e0dda2b3 | ||
![]() |
b0c928c691 | ||
![]() |
c454c035a3 | ||
![]() |
f839166082 | ||
![]() |
3cfee9dd81 | ||
![]() |
6295de9fd6 | ||
![]() |
2a7e1c419e | ||
![]() |
aa07380c57 | ||
![]() |
4f3f22f3bc | ||
![]() |
1b8b8204e5 | ||
![]() |
8c74aa67a1 | ||
![]() |
c7c9bc3b5d | ||
![]() |
709ad758dc | ||
![]() |
3d0f869356 | ||
![]() |
8c93b090b4 | ||
![]() |
60fca48e8c | ||
![]() |
3ccf80d442 | ||
![]() |
0f53fdde5f | ||
![]() |
f0c4f9c5d5 | ||
![]() |
c0cfcda102 | ||
![]() |
078ad2584a | ||
![]() |
7200a9e2f0 | ||
![]() |
7d589c7e2f | ||
![]() |
cf57bdbc9e | ||
![]() |
ab420f5400 | ||
![]() |
84ab6cf478 | ||
![]() |
d39fa6acf2 | ||
![]() |
0a4b6de0fa | ||
![]() |
b7b4fa0c94 | ||
![]() |
c33982f97c | ||
![]() |
59c733735c | ||
![]() |
1e251a27b1 | ||
![]() |
a78aa37f7c | ||
![]() |
bf2f09381e | ||
![]() |
0607152f51 | ||
![]() |
27c9dd4f3e | ||
![]() |
d0eae64241 | ||
![]() |
6ecf1e7b96 | ||
![]() |
4bbd3e1cbb | ||
![]() |
ce29cf57be | ||
![]() |
692538f0c2 | ||
![]() |
97afabb3e0 | ||
![]() |
1df6ccb1c9 | ||
![]() |
a04a66ab6d | ||
![]() |
47cfad4624 | ||
![]() |
d60e1fbcfa | ||
![]() |
90a45dab55 | ||
![]() |
14f4e06788 | ||
![]() |
9de6a8f5c0 | ||
![]() |
20f47110cd | ||
![]() |
4d562d07d0 | ||
![]() |
fa7b03109f | ||
![]() |
eaac412cb3 | ||
![]() |
3eec52f647 | ||
![]() |
a28cbc5a8a | ||
![]() |
c7ef91e9ed | ||
![]() |
a5b0faae77 | ||
![]() |
e711ff0f7c | ||
![]() |
fb0b489c82 | ||
![]() |
a7089373dd | ||
![]() |
f393670528 | ||
![]() |
3f8a314b94 | ||
![]() |
1ad1e6976f | ||
![]() |
c793949b37 | ||
![]() |
01d7de2e46 | ||
![]() |
4f0c6addcf | ||
![]() |
7c03cd4b04 | ||
![]() |
0558cdec49 | ||
![]() |
e25140b529 | ||
![]() |
e942a9b803 | ||
![]() |
4b1b3bbeed | ||
![]() |
9d6fabe8c2 | ||
![]() |
bd94d75086 | ||
![]() |
16d01207a7 | ||
![]() |
5477f9371d | ||
![]() |
b057026328 | ||
![]() |
28d9290959 | ||
![]() |
3297d9bd8c | ||
![]() |
08d62d1e85 | ||
![]() |
9bbc9af285 | ||
![]() |
e8d4153333 | ||
![]() |
14e9e996e5 | ||
![]() |
8c5de7d74d | ||
![]() |
56e6640f38 | ||
![]() |
4d67bef903 | ||
![]() |
fee77e2172 | ||
![]() |
a9b261d397 | ||
![]() |
c586ae8c80 | ||
![]() |
90fe3126cb | ||
![]() |
9b63457ae1 | ||
![]() |
6e9f94a893 | ||
![]() |
dca918efb5 | ||
![]() |
87d615babc | ||
![]() |
b309743425 | ||
![]() |
649f2a8da7 | ||
![]() |
ba30ffbdcf | ||
![]() |
a99faad77c | ||
![]() |
893c7b9d5e | ||
![]() |
2160508076 | ||
![]() |
79e3286308 | ||
![]() |
9dfa851016 | ||
![]() |
5138feed4e | ||
![]() |
3b3f80db3c | ||
![]() |
f37213ccbe | ||
![]() |
04dcaf332d | ||
![]() |
8ec4b75ab9 | ||
![]() |
35460dd17a | ||
![]() |
b9cf140e33 | ||
![]() |
e6bba96be0 | ||
![]() |
d48a25bf2f | ||
![]() |
5619a32d84 | ||
![]() |
4594cbea06 | ||
![]() |
b273125379 | ||
![]() |
79274d5e54 | ||
![]() |
b5cd8d2fe8 | ||
![]() |
390489cef5 | ||
![]() |
7ee7c0b77d | ||
![]() |
195c3150a5 | ||
![]() |
94b0863de5 | ||
![]() |
4ee235566d | ||
![]() |
14a5639f40 | ||
![]() |
eedef19778 | ||
![]() |
2b723d051b | ||
![]() |
6ca9b0c7b4 | ||
![]() |
cd9e3d15aa | ||
![]() |
1c58a1b813 | ||
![]() |
311854408c | ||
![]() |
6a7c65eefb | ||
![]() |
6f2e9cdab2 | ||
![]() |
e43444bc48 | ||
![]() |
26390d5a4a | ||
![]() |
0063b7dbca | ||
![]() |
c829f2b7b1 | ||
![]() |
2127eaebc2 | ||
![]() |
1fbbf68f94 | ||
![]() |
59e4d5b0be | ||
![]() |
eec45f8017 | ||
![]() |
f2b8f32234 | ||
![]() |
2855185091 | ||
![]() |
6e37fd8306 | ||
![]() |
55517eec2f | ||
![]() |
5113f3d58c | ||
![]() |
66f51f8055 | ||
![]() |
cef1377926 | ||
![]() |
e5c197ce03 | ||
![]() |
d17515a59b | ||
![]() |
1081675f56 | ||
![]() |
b984b074e9 | ||
![]() |
1a1f27da49 | ||
![]() |
7b71009fd7 | ||
![]() |
cbd9e7f94b | ||
![]() |
4fd32de1cd | ||
![]() |
a676fecbfe | ||
![]() |
ec7dd517d1 | ||
![]() |
81ec9d3ef4 | ||
![]() |
08700670f9 | ||
![]() |
d716d514df | ||
![]() |
f52dd99b7a | ||
![]() |
f049a24c3e | ||
![]() |
abec2231ab | ||
![]() |
0fb0b5f3e7 | ||
![]() |
6f25cabf41 | ||
![]() |
f9d4ff4934 | ||
![]() |
9758bfb8fc | ||
![]() |
7fc983b042 | ||
![]() |
f9bc405787 | ||
![]() |
17cf6e836b | ||
![]() |
71cfd67c8b | ||
![]() |
6ad94a7f82 | ||
![]() |
86268ddabb | ||
![]() |
58a4033bf1 | ||
![]() |
74414e2257 | ||
![]() |
e73c04b9f5 | ||
![]() |
a893212b22 | ||
![]() |
55f084592c | ||
![]() |
7509f9e1f8 | ||
![]() |
95fc015a87 | ||
![]() |
607fe25d8b | ||
![]() |
75d82c2634 | ||
![]() |
b56e652192 | ||
![]() |
4599639237 | ||
![]() |
c82a4331a5 | ||
![]() |
aff4dda77f | ||
![]() |
1b0a70becb | ||
![]() |
54d04f0642 | ||
![]() |
f0d94ba98b | ||
![]() |
1ce2e92ccd | ||
![]() |
70d399e8f6 | ||
![]() |
8c6723a035 | ||
![]() |
fb73cdfc08 | ||
![]() |
fa5dc07869 | ||
![]() |
ad50d1549f | ||
![]() |
8134ee4f09 | ||
![]() |
b5ecde7f0c | ||
![]() |
071ea6a67f | ||
![]() |
a400d50817 | ||
![]() |
70f1e387fd | ||
![]() |
c424033356 | ||
![]() |
7c4807ddc0 | ||
![]() |
917d55e706 | ||
![]() |
bec1a93446 | ||
![]() |
bbbf765e99 | ||
![]() |
d03828137c | ||
![]() |
57a2ebee2b | ||
![]() |
68f19ecab6 | ||
![]() |
08a5c0be33 | ||
![]() |
61cfc83afd | ||
![]() |
745963dd51 | ||
![]() |
10a9be66f9 | ||
![]() |
9eaf8676de | ||
![]() |
c9b624c5d3 | ||
![]() |
e192147a7b | ||
![]() |
ddc5628d5b | ||
![]() |
5d83ce3583 | ||
![]() |
cee8dc2c56 | ||
![]() |
d50279cd3a | ||
![]() |
254fab365f | ||
![]() |
932c96ba4a | ||
![]() |
0934be8054 | ||
![]() |
5ccacb79a0 | ||
![]() |
6169add4a3 | ||
![]() |
1fda73a36e | ||
![]() |
5898fb6f5b | ||
![]() |
ac198745f1 | ||
![]() |
18c626bd42 | ||
![]() |
e5df96b9b6 | ||
![]() |
63ce6b2d21 | ||
![]() |
6b239d5d6b | ||
![]() |
4b7aac7fd5 | ||
![]() |
1994902fec | ||
![]() |
3762d819eb | ||
![]() |
abc65220ec | ||
![]() |
826f042dbb | ||
![]() |
b4cd5b1aec | ||
![]() |
91fa36e19b | ||
![]() |
159680aa55 | ||
![]() |
6f90f19ad1 | ||
![]() |
52fd477d39 | ||
![]() |
f4af3aad45 | ||
![]() |
3d5024b0c5 | ||
![]() |
90dfc64b0f | ||
![]() |
b38e51b20f | ||
![]() |
6384c701e1 | ||
![]() |
c537895682 | ||
![]() |
fbd46f6dae | ||
![]() |
4d8a81b397 | ||
![]() |
cc96d58fa7 | ||
![]() |
9742983db0 | ||
![]() |
6d02f68608 | ||
![]() |
71a99e97a9 | ||
![]() |
ea7a6e063a | ||
![]() |
78d5c0e26b | ||
![]() |
db93481e03 | ||
![]() |
4d9bd7fb36 | ||
![]() |
5d91304592 | ||
![]() |
1b058f0540 | ||
![]() |
dd04c983fe | ||
![]() |
3399c9c279 | ||
![]() |
911d1ce547 | ||
![]() |
f8d404d32d | ||
![]() |
67710349d9 | ||
![]() |
d6222a351d | ||
![]() |
342df7f073 | ||
![]() |
1812158f67 | ||
![]() |
13234e1c28 | ||
![]() |
bab98a5024 | ||
![]() |
01dbc82a15 | ||
![]() |
fc26dce51e | ||
![]() |
cec9bbd1b4 | ||
![]() |
8f9fbbefe5 | ||
![]() |
7f75ffc195 | ||
![]() |
184e916811 | ||
![]() |
38c8941bef | ||
![]() |
f42fae640b | ||
![]() |
e7fe448447 | ||
![]() |
90c88b92bb | ||
![]() |
26eb2bd358 | ||
![]() |
a0c95f0725 | ||
![]() |
9710794095 | ||
![]() |
a66fc9e5ac | ||
![]() |
fb7a12b244 | ||
![]() |
c1d247f72a | ||
![]() |
a1130056b3 | ||
![]() |
e61d4fc1a2 | ||
![]() |
1c6202bede | ||
![]() |
6947d65330 | ||
![]() |
9e781ddcaa | ||
![]() |
96fdd0bec9 | ||
![]() |
4237c8fa6a | ||
![]() |
9c6bd3873e | ||
![]() |
8f2b0488a4 | ||
![]() |
f070d2725c | ||
![]() |
45989ff9a4 | ||
![]() |
cf9b51cb99 | ||
![]() |
a037520e27 | ||
![]() |
9925a225d6 | ||
![]() |
72ddf93442 | ||
![]() |
6bbf3304ec | ||
![]() |
a2dc561593 | ||
![]() |
12464379b7 | ||
![]() |
ec63981bfa | ||
![]() |
bb7c763bc0 | ||
![]() |
1047d71e60 | ||
![]() |
86d8ff4dfc | ||
![]() |
685768baaf | ||
![]() |
7052219642 | ||
![]() |
81d68341ea | ||
![]() |
890afd530c | ||
![]() |
571eab2122 | ||
![]() |
3a20f189f7 | ||
![]() |
5fd325e294 | ||
![]() |
ac60692006 | ||
![]() |
4ef943c87b | ||
![]() |
e4e755fd68 | ||
![]() |
a597964bc4 | ||
![]() |
db5c5f4810 | ||
![]() |
4cf9ece35c | ||
![]() |
fb99bfeff6 | ||
![]() |
e6bff40e1b | ||
![]() |
3a9dd3b465 | ||
![]() |
0259c8b4e1 | ||
![]() |
2b80b19d70 | ||
![]() |
29a6ac9495 | ||
![]() |
a64f4e6d97 | ||
![]() |
eaa9082bb1 | ||
![]() |
44f7e3fb07 | ||
![]() |
1df1054cdf | ||
![]() |
7dbc48b718 | ||
![]() |
ad5fc6f6a2 | ||
![]() |
ebd22fb58b | ||
![]() |
5314eb6637 | ||
![]() |
1c24a74004 | ||
![]() |
10d8ac429e | ||
![]() |
9408903c45 | ||
![]() |
96f68e119d | ||
![]() |
07933f0483 | ||
![]() |
8df3c99e20 | ||
![]() |
e9436b42e4 | ||
![]() |
b8c0e33102 | ||
![]() |
8a2aa82217 | ||
![]() |
750aa1875f | ||
![]() |
707a765906 | ||
![]() |
7d1f305cc5 | ||
![]() |
01b4f0db58 | ||
![]() |
b383a5a382 | ||
![]() |
14e9b0c341 | ||
![]() |
ad038b8b03 | ||
![]() |
3d7c24ca17 | ||
![]() |
d679d79e99 | ||
![]() |
dfc6beac9f | ||
![]() |
e5baac4f19 | ||
![]() |
90b045b575 | ||
![]() |
a291d0c909 | ||
![]() |
9315047183 | ||
![]() |
58c1548b81 | ||
![]() |
852309c973 | ||
![]() |
f9a59bb7b9 | ||
![]() |
892be9986e | ||
![]() |
2c545a2ea1 | ||
![]() |
d0adca6f93 | ||
![]() |
de69028910 | ||
![]() |
22a9504522 | ||
![]() |
0e21018c0a | ||
![]() |
b45adbd67c | ||
![]() |
dd4de90bc0 | ||
![]() |
cda0516b97 | ||
![]() |
24f904d9ce | ||
![]() |
aa38f26d2e | ||
![]() |
5dfc3dae7d | ||
![]() |
d54597d3be | ||
![]() |
0c980346bc | ||
![]() |
6155d602ef | ||
![]() |
f9e1bf3ab2 | ||
![]() |
ff0f26dc45 | ||
![]() |
2d2b63b352 | ||
![]() |
e74de93825 | ||
![]() |
1a85c3df60 | ||
![]() |
770f1650d3 | ||
![]() |
3d1eb17edc | ||
![]() |
e2520833e1 | ||
![]() |
a5cb069075 | ||
![]() |
325e6c9802 | ||
![]() |
601cbd1a69 | ||
![]() |
456c360a68 | ||
![]() |
a5e00b36c9 | ||
![]() |
52ba3fcf17 | ||
![]() |
756865a10c | ||
![]() |
e481098c98 | ||
![]() |
0d90ff5ad6 | ||
![]() |
e0ce94e2b5 | ||
![]() |
7d50c95cc8 | ||
![]() |
e03afb2498 | ||
![]() |
b621ba27d3 | ||
![]() |
27a53a9361 | ||
![]() |
88b4e6dfc9 | ||
![]() |
3d93ec8e90 | ||
![]() |
5426b9f0ec | ||
![]() |
3c4a77fc6e | ||
![]() |
4f5298dbf6 | ||
![]() |
df456997eb | ||
![]() |
e6a0cc8066 | ||
![]() |
7bf8d10df6 | ||
![]() |
539b9399b2 | ||
![]() |
3fae34cc43 | ||
![]() |
af9a24a443 | ||
![]() |
7176bcebc1 | ||
![]() |
719709d516 | ||
![]() |
916b820a01 | ||
![]() |
3afef6c7f1 | ||
![]() |
041fdc616f | ||
![]() |
95868ee15f | ||
![]() |
176ffec3c6 | ||
![]() |
0cfddd6d27 | ||
![]() |
da6b387cf5 | ||
![]() |
87bb57a4ba | ||
![]() |
9fea221d6b | ||
![]() |
25ec442f9f | ||
![]() |
e82dd1406c | ||
![]() |
b8e3511be8 | ||
![]() |
2d8cc8cda2 | ||
![]() |
acd0e151e7 | ||
![]() |
af2eb1e97e | ||
![]() |
c068ebff9a | ||
![]() |
435c9e295f | ||
![]() |
3d7a13941d | ||
![]() |
ab23a67a72 | ||
![]() |
409dc867c8 | ||
![]() |
f6e875ecb5 | ||
![]() |
5c6e39e633 | ||
![]() |
68a1b6a9f0 | ||
![]() |
e25986900c | ||
![]() |
53e02ca3be | ||
![]() |
1c3cc3dd37 | ||
![]() |
c2bf72a57e | ||
![]() |
b88572c344 | ||
![]() |
27f5fa3670 | ||
![]() |
50b70163c6 | ||
![]() |
494f1b45fc | ||
![]() |
0dba80c85a | ||
![]() |
105ebc16b7 | ||
![]() |
d217b00916 | ||
![]() |
36719a08c6 | ||
![]() |
22564df6a0 | ||
![]() |
1130fdc30d | ||
![]() |
e7ec90c944 | ||
![]() |
0fa0f8b10c | ||
![]() |
b75aafd3ce | ||
![]() |
f7d7580b3a | ||
![]() |
f3c834e661 | ||
![]() |
e8d748f9b4 | ||
![]() |
2bd79a7603 | ||
![]() |
470a5d52d5 | ||
![]() |
752c35be94 | ||
![]() |
255e658768 | ||
![]() |
24639120f5 | ||
![]() |
dee0c0a01a | ||
![]() |
9386cece09 | ||
![]() |
53c963874f | ||
![]() |
9458928409 | ||
![]() |
13e09fbada | ||
![]() |
c6b4b54cf4 | ||
![]() |
a8c2881a7b | ||
![]() |
63e123809c | ||
![]() |
39e951903f | ||
![]() |
9c06b01c6d | ||
![]() |
dd0d207149 | ||
![]() |
18a28217c1 | ||
![]() |
d3801930df | ||
![]() |
d392b568db | ||
![]() |
5f5a145626 | ||
![]() |
c9a9f75a5d | ||
![]() |
de52576408 | ||
![]() |
26e9e89106 | ||
![]() |
f90b2f2605 | ||
![]() |
088aa386ad | ||
![]() |
a7aa7a8502 | ||
![]() |
48c93b542a | ||
![]() |
575768880e | ||
![]() |
3069effd1e | ||
![]() |
b92edb4200 | ||
![]() |
90fab8d89f | ||
![]() |
a40f7adb9c |
BIN
.DS_Store
vendored
Normal file
24
.github/workflows/main.yml
vendored
Normal 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
|
@ -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
|
@ -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
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"editor.formatOnSave": true
|
||||
}
|
8
CONTRIBUTIONS.md
Normal 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)
|
|
@ -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
BIN
fonts/EngliBesh-KG3W.ttf
Normal file
BIN
fonts/OpenSans-Bold.ttf
Normal file
BIN
fonts/OpenSans-BoldItalic.ttf
Normal file
BIN
fonts/OpenSans-Italic.ttf
Normal file
73
gulpfile.js
|
@ -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));
|
||||
|
|
2069
lang/en.json
1027
lang/fr.json
Normal file
1027
lang/it.json
Normal file
232
less/items.less
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
@ -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";
|
|
@ -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 */
|
|
@ -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;
|
||||
}
|
||||
|
146
less/update/_variables-dark.less
Normal 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;
|
143
less/update/_variables-light.less
Normal 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;
|
67
less/update/_variables.less
Normal 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);
|
||||
}
|
1126
less/update/components/actor-global.less
Normal file
420
less/update/components/actor-themes.less
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
105
less/update/components/forms-global.less
Normal 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;
|
||||
|
||||
}
|
||||
|
||||
}
|
52
less/update/components/forms-themes.less
Normal 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);
|
||||
}
|
||||
}
|
76
less/update/components/foundry-app-window-themes.less
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
84
less/update/components/foundry-nav-themes.less
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
388
less/update/components/sidebar-global.less
Normal 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;
|
||||
}
|
||||
}
|
298
less/update/components/sidebar-themes.less
Normal 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;
|
||||
}
|
||||
}
|
500
less/update/components/sidebar.less
Normal 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;
|
||||
}
|
||||
}
|
57
less/update/sw5e-dark.less
Normal 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";
|
||||
}
|
194
less/update/sw5e-global.less
Normal 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);
|
||||
}
|
||||
}
|
47
less/update/sw5e-light.less
Normal 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";
|
||||
}
|
61
less/update/sw5e-update.less
Normal 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";
|
2126
module/actor/old_entity.js
Normal 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": "∞"
|
||||
};
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
989
module/actor/sheets/newSheet/base.js
Normal 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": "∞"
|
||||
};
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
752
module/actor/sheets/newSheet/character.js
Normal 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);
|
||||
});
|
159
module/actor/sheets/newSheet/npc.js
Normal 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});
|
||||
}
|
||||
}
|
169
module/actor/sheets/newSheet/starship.js
Normal 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});
|
||||
}
|
||||
}
|
432
module/actor/sheets/newSheet/vehicle.js
Normal 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});
|
||||
}
|
||||
}
|
|
@ -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});
|
||||
}
|
||||
}
|
920
module/actor/sheets/oldSheets/base.js
Normal 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": "∞"
|
||||
};
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
368
module/actor/sheets/oldSheets/character.js
Normal 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);
|
||||
}
|
||||
}
|
144
module/actor/sheets/oldSheets/npc.js
Normal 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});
|
||||
}
|
||||
}
|
432
module/actor/sheets/oldSheets/vehicle.js
Normal 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});
|
||||
}
|
||||
}
|
|
@ -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});
|
||||
}
|
||||
};
|
|
@ -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) {
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
@ -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);
|
||||
}
|
||||
}
|
92
module/apps/hit-dice-config.js
Normal 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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
38
module/apps/movement-config.js
Normal 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;
|
||||
}
|
||||
}
|
66
module/apps/select-items-prompt.js
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
43
module/apps/senses-config.js
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
151
module/chat.js
|
@ -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);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
|
|
@ -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 = {};
|
||||
|
|
|
@ -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;
|
||||
};
|
2094
module/config.js
555
module/dice.js
|
@ -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
|
@ -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
|
@ -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;
|
||||
}
|
||||
}
|
15
module/dice/roll-dialog.js
Normal 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
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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
|
@ -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
9
package.json
Normal 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"
|
||||
}
|
||||
}
|
BIN
packs/Icons/Archetypes/Aqinos Form.webp
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
packs/Icons/Archetypes/Archaeologist Pursuit.webp
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
packs/Icons/Archetypes/Biotech Engineering.webp
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
packs/Icons/Archetypes/Bolstering Practice.webp
Normal file
After Width: | Height: | Size: 8.8 KiB |
BIN
packs/Icons/Archetypes/Construction Engineering.webp
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
packs/Icons/Archetypes/Exhibition Specialist.webp
Normal file
After Width: | Height: | Size: 9.4 KiB |
BIN
packs/Icons/Archetypes/Kro Var Order.webp
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
packs/Icons/Archetypes/Mechanist Technique.webp
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
packs/Icons/Archetypes/Occultist Pursuit.webp
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
packs/Icons/Archetypes/Path of Meditation.webp
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
packs/Icons/Archetypes/Pugnacity Practice.webp
Normal file
After Width: | Height: | Size: 8.3 KiB |