Compare commits
256 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9504b4299b | ||
|
|
6a381a5e09 | ||
|
|
d7abb591e2 | ||
|
|
83537f1c24 | ||
|
|
8abd629fb6 | ||
|
|
3843fcff97 | ||
|
|
d9ef848c4e | ||
|
|
c1544213e7 | ||
|
|
4f4779c3e5 | ||
|
|
7457372e13 | ||
|
|
fcdb7ca287 | ||
|
|
bc1c57de85 | ||
|
|
b42a873a5f | ||
|
|
77ae2d9de8 | ||
|
|
cbb8d3f184 | ||
|
|
beb4d94676 | ||
|
|
3af4528788 | ||
|
|
4b9802d6a0 | ||
|
|
fad59f8674 | ||
|
|
c24247cf38 | ||
|
|
04de76d20e | ||
|
|
442aea2bbe | ||
|
|
1fe7ed8972 | ||
|
|
957aabcb82 | ||
|
|
c580970d6d | ||
|
|
8954668f5a | ||
|
|
c314c0ab05 | ||
|
|
9ae8d63fc4 | ||
|
|
40b04e519f | ||
|
|
308f3e735b | ||
|
|
f66190463a | ||
|
|
3950db8672 | ||
|
|
af421eef9c | ||
|
|
26affda339 | ||
|
|
60172f8a31 | ||
|
|
ea02416353 | ||
|
|
f7461f40d6 | ||
|
|
e49dea469f | ||
|
|
85d97abbee | ||
|
|
c00e618f85 | ||
|
|
6e47395327 | ||
|
|
3acf42394d | ||
|
|
6bc737f850 | ||
|
|
4d6c6b6094 | ||
|
|
15ff16bb8c | ||
|
|
d4e5a2a529 | ||
|
|
6291071e0d | ||
|
|
35ebed81dd | ||
|
|
54e54ef5a8 | ||
|
|
99b5ad4e82 | ||
|
|
e068cf27b3 | ||
|
|
56ca4b1680 | ||
|
|
a25ab2040c | ||
|
|
1096c53f49 | ||
|
|
513c0f7148 | ||
|
|
2b4ab6258d | ||
|
|
376d3bc522 | ||
|
|
b402fdf517 | ||
|
|
16d3ea9d53 | ||
|
|
c6a3619178 | ||
|
|
1795316664 | ||
|
|
862e25eb0c | ||
|
|
0a3ea7672f | ||
|
|
4c34986fb7 | ||
|
|
bf6fb358e6 | ||
|
|
9f01b85df3 | ||
|
|
ce07766fb4 | ||
|
|
6133f25416 | ||
|
|
f120ddb75a | ||
|
|
c8a53a0235 | ||
|
|
b65b4b4497 | ||
|
|
8a4bfa8475 | ||
|
|
ea2416aaea | ||
|
|
44703a5aa5 | ||
|
|
53958fde92 | ||
|
|
0dbd5903b3 | ||
|
|
57ca3ecb01 | ||
|
|
1bc48330e0 | ||
|
|
0b8d824b2d | ||
|
|
ff2c5f5427 | ||
|
|
2c6cd7d243 | ||
|
|
baf99c65b3 | ||
|
|
b82e3d6f4c | ||
|
|
35f6037236 | ||
|
|
8289e9bd11 | ||
|
|
35c48ccd33 | ||
|
|
b7be15ad70 | ||
|
|
b82061b8d4 | ||
|
|
142072d810 | ||
|
|
4550661a59 | ||
|
|
9fb85b8c50 | ||
|
|
30a0c4d2a9 | ||
|
|
16de798916 | ||
|
|
656a079c58 | ||
|
|
93b0fe1885 | ||
|
|
0bf5954472 | ||
|
|
9e4bbe0d1b | ||
|
|
a58ccc0e0e | ||
|
|
ad7166f576 | ||
|
|
cf09abaa57 | ||
|
|
d643886a7f | ||
|
|
90235a5bc6 | ||
|
|
775e1fa842 | ||
|
|
288a086ffe | ||
|
|
e961fd2b98 | ||
|
|
32e5b0a9f6 | ||
|
|
b914415ef0 | ||
|
|
2ee3fe1e12 | ||
|
|
9992da711a | ||
|
|
c3c05a0727 | ||
|
|
53e88af93a | ||
|
|
b9588c83b1 | ||
|
|
8e610c2cd8 | ||
|
|
25e053c473 | ||
|
|
76a0918a78 | ||
|
|
61e72ad874 | ||
|
|
50cb6185ce | ||
|
|
2b7851ab32 | ||
|
|
deca9786b9 | ||
|
|
67da641244 | ||
|
|
16f5fe91ea | ||
|
|
cbbbcaf56a | ||
|
|
72d932538b | ||
|
|
265e3bf970 | ||
|
|
e0a397af78 | ||
|
|
3532898be9 | ||
|
|
055ddefae6 | ||
|
|
4c617332f2 | ||
|
|
03b623d898 | ||
|
|
a0744e5af3 | ||
|
|
b92d2ecf05 | ||
|
|
aabcdac242 | ||
|
|
9fbeb0c06f | ||
|
|
c058f3eab4 | ||
|
|
0a2d4cf97b | ||
|
|
7151e1bb4e | ||
|
|
b088a2d433 | ||
|
|
8aa5ee81d5 | ||
|
|
ef26153bb2 | ||
|
|
77597e8056 | ||
|
|
ee1b876259 | ||
|
|
12fbca5c78 | ||
|
|
da6fb55ca0 | ||
|
|
8551e318c2 | ||
|
|
f175cffab8 | ||
|
|
2bca582af6 | ||
|
|
5815c7ca34 | ||
|
|
c237162475 | ||
|
|
e87772c2a3 | ||
|
|
704314a7eb | ||
|
|
7ffd0bf61d | ||
|
|
69b3ba781d | ||
|
|
bf8eb52a96 | ||
|
|
684d672028 | ||
|
|
fb98544ae1 | ||
|
|
ec8b9c209c | ||
|
|
bee90a7a80 | ||
|
|
5ad0de9eb7 | ||
|
|
0b377fcb71 | ||
|
|
1f26fbf00e | ||
|
|
bb1e9624ad | ||
|
|
bda446858e | ||
|
|
04c9c4cfc2 | ||
|
|
cc915410da | ||
|
|
a5b4b20324 | ||
|
|
c48cc20fb9 | ||
|
|
c0b031f2b5 | ||
|
|
c6b633613c | ||
|
|
03b7f1037e | ||
|
|
d99c44fdeb | ||
|
|
1e8549323b | ||
|
|
49a83b487a | ||
|
|
a072d23097 | ||
|
|
ffb78c59b3 | ||
|
|
1f663bf4b4 | ||
|
|
f58a035209 | ||
|
|
3fdb9f79bd | ||
|
|
311bbfa58c | ||
|
|
d1126596c4 | ||
|
|
bb6125f84c | ||
|
|
2d489f119d | ||
|
|
3ab73f62bf | ||
|
|
8b30c7b6d0 | ||
|
|
4a6fa304b3 | ||
|
|
fabb884831 | ||
|
|
2a703900ee | ||
|
|
0789e4d759 | ||
|
|
39c91f58e4 | ||
|
|
c84342b21a | ||
|
|
0373feb2ea | ||
|
|
0b11595657 | ||
|
|
e7f3f669dd | ||
|
|
8d969bd447 | ||
|
|
b3aeaf06ea | ||
|
|
85e3b0724a | ||
|
|
81a3ede86e | ||
|
|
d4864dda5f | ||
|
|
5ce1b6aff8 | ||
|
|
41731212ef | ||
|
|
ef9867d409 | ||
|
|
721300700e | ||
|
|
bc6dfbe498 | ||
|
|
0a22073d67 | ||
|
|
857213f157 | ||
|
|
b3371fca53 | ||
|
|
3fbb006783 | ||
|
|
2253672f43 | ||
|
|
ed6d557f8a | ||
|
|
4d642b56bb | ||
|
|
436c5bb785 | ||
|
|
8489ef5ec0 | ||
|
|
c9710bdb09 | ||
|
|
26784f11b6 | ||
|
|
23d43f7d43 | ||
|
|
1ebb0d2527 | ||
|
|
9d86cb8bee | ||
|
|
3343f8a813 | ||
|
|
0260824c2f | ||
|
|
66ee3ff808 | ||
|
|
cb71f6d380 | ||
|
|
2f04d9ec1c | ||
|
|
40c54524a7 | ||
|
|
b890a3b11e | ||
|
|
c9242a95f3 | ||
|
|
fedda62c7c | ||
|
|
612575d0e6 | ||
|
|
d1d22c0d89 | ||
|
|
b94f5ebb4b | ||
|
|
3f32535666 | ||
|
|
4ea02c4fbb | ||
|
|
b052e8dd19 | ||
|
|
e2822b9f22 | ||
|
|
c46b836985 | ||
|
|
65d1bac0dc | ||
|
|
fcae3056de | ||
|
|
7d364c80c0 | ||
|
|
0ff6c08abd | ||
|
|
1c95336843 | ||
|
|
b36720511b | ||
|
|
261220fdd5 | ||
|
|
64edc52cca | ||
|
|
56f1bd2829 | ||
|
|
3b669fd2f9 | ||
|
|
933878e158 | ||
|
|
0e6ca56316 | ||
|
|
6599fe1ef8 | ||
|
|
f39baf43a1 | ||
|
|
96f4e35e25 | ||
|
|
e17dbf6601 | ||
|
|
3f81d419f7 | ||
|
|
1c00f5aa04 | ||
|
|
f5a32cb50a | ||
|
|
f4d3368fb4 | ||
|
|
1de9fb558a | ||
|
|
06ffc94b4c | ||
|
|
74c6a423ee |
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "app/packages/redis-oplog"]
|
||||||
|
path = app/packages/redis-oplog
|
||||||
|
url = https://github.com/ramezrafla/redis-oplog.git
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
DiceCloud
|
DiceCloud
|
||||||
========
|
========
|
||||||
|
|
||||||
This is the repo for [DiceCloud](dicecloud.com).
|
This is the repo for [DiceCloud](https://dicecloud.com).
|
||||||
|
|
||||||
DiceCloud is a free, auditable, real-time character sheet for D&D 5e.
|
DiceCloud is a free, auditable, real-time character sheet for D&D 5e.
|
||||||
|
|
||||||
|
|||||||
@@ -3,29 +3,30 @@
|
|||||||
# 'meteor add' and 'meteor remove' will edit this file for you,
|
# 'meteor add' and 'meteor remove' will edit this file for you,
|
||||||
# but you can also edit it by hand.
|
# but you can also edit it by hand.
|
||||||
|
|
||||||
accounts-password@2.3.1
|
zegenie:redis-oplog
|
||||||
|
accounts-password@2.3.4
|
||||||
random@1.2.1
|
random@1.2.1
|
||||||
underscore@1.0.11
|
underscore@1.0.13
|
||||||
dburles:mongo-collection-instances
|
dburles:mongo-collection-instances
|
||||||
accounts-google@1.4.0
|
accounts-google@1.4.0
|
||||||
email@2.2.2
|
email@2.2.5
|
||||||
meteor-base@1.5.1
|
meteor-base@1.5.1
|
||||||
mobile-experience@1.1.0
|
mobile-experience@1.1.0
|
||||||
mongo@1.16.1
|
mongo@1.16.6
|
||||||
session@1.2.1
|
session@1.2.1
|
||||||
tracker@1.2.1
|
tracker@1.3.2
|
||||||
logging@1.3.1
|
logging@1.3.2
|
||||||
reload@1.3.1
|
reload@1.3.1
|
||||||
ejson@1.1.3
|
ejson@1.1.3
|
||||||
check@1.3.2
|
check@1.3.2
|
||||||
standard-minifier-js@2.8.1
|
standard-minifier-js@2.8.1
|
||||||
shell-server@0.5.0
|
shell-server@0.5.0
|
||||||
ecmascript@0.16.3
|
ecmascript@0.16.7
|
||||||
es5-shim@4.8.0
|
es5-shim@4.8.0
|
||||||
service-configuration@1.3.1
|
service-configuration@1.3.1
|
||||||
dynamic-import@0.7.2
|
dynamic-import@0.7.3
|
||||||
ddp-rate-limiter@1.1.1
|
ddp-rate-limiter@1.2.0
|
||||||
rate-limit@1.0.9
|
rate-limit@1.1.1
|
||||||
mdg:validated-method
|
mdg:validated-method
|
||||||
static-html@1.3.2
|
static-html@1.3.2
|
||||||
aldeed:collection2
|
aldeed:collection2
|
||||||
@@ -38,7 +39,7 @@ simple:rest-method-mixin
|
|||||||
mikowals:batch-insert
|
mikowals:batch-insert
|
||||||
peerlibrary:subscription-data
|
peerlibrary:subscription-data
|
||||||
zer0th:meteor-vuetify-loader
|
zer0th:meteor-vuetify-loader
|
||||||
akryum:vue-component
|
akryum:vue-component@0.15.2
|
||||||
akryum:vue-router2
|
akryum:vue-router2
|
||||||
percolate:migrations
|
percolate:migrations
|
||||||
meteortesting:mocha
|
meteortesting:mocha
|
||||||
@@ -46,6 +47,7 @@ ostrio:files
|
|||||||
simple:rest-bearer-token-parser
|
simple:rest-bearer-token-parser
|
||||||
simple:rest-json-error-handler
|
simple:rest-json-error-handler
|
||||||
littledata:synced-cron
|
littledata:synced-cron
|
||||||
mdg:meteor-apm-agent
|
#mdg:meteor-apm-agent
|
||||||
typescript@4.5.4
|
typescript@4.9.4
|
||||||
seba:minifiers-autoprefixer
|
seba:minifiers-autoprefixer
|
||||||
|
mixmax:smart-disconnect
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
METEOR@2.8.1
|
METEOR@2.12
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
accounts-base@2.2.5
|
accounts-base@2.2.8
|
||||||
accounts-google@1.4.0
|
accounts-google@1.4.0
|
||||||
accounts-oauth@1.4.1
|
accounts-oauth@1.4.2
|
||||||
accounts-password@2.3.1
|
accounts-password@2.3.4
|
||||||
accounts-patreon@0.1.0
|
accounts-patreon@0.1.0
|
||||||
akryum:npm-check@0.1.2
|
akryum:npm-check@0.1.2
|
||||||
akryum:vue-component@0.15.2
|
akryum:vue-component@0.15.2
|
||||||
@@ -12,7 +12,7 @@ aldeed:collection2@3.5.0
|
|||||||
aldeed:schema-index@3.0.0
|
aldeed:schema-index@3.0.0
|
||||||
allow-deny@1.1.1
|
allow-deny@1.1.1
|
||||||
autoupdate@1.8.0
|
autoupdate@1.8.0
|
||||||
babel-compiler@7.9.2
|
babel-compiler@7.10.4
|
||||||
babel-runtime@1.5.1
|
babel-runtime@1.5.1
|
||||||
base64@1.0.12
|
base64@1.0.12
|
||||||
binary-heap@1.0.11
|
binary-heap@1.0.11
|
||||||
@@ -21,7 +21,7 @@ boilerplate-generator@1.7.1
|
|||||||
bozhao:link-accounts@2.6.1
|
bozhao:link-accounts@2.6.1
|
||||||
caching-compiler@1.2.2
|
caching-compiler@1.2.2
|
||||||
caching-html-compiler@1.2.1
|
caching-html-compiler@1.2.1
|
||||||
callback-hook@1.4.0
|
callback-hook@1.5.1
|
||||||
check@1.3.2
|
check@1.3.2
|
||||||
coffeescript@2.4.1
|
coffeescript@2.4.1
|
||||||
coffeescript-compiler@2.4.1
|
coffeescript-compiler@2.4.1
|
||||||
@@ -29,20 +29,20 @@ dburles:mongo-collection-instances@0.3.6
|
|||||||
ddp@1.4.1
|
ddp@1.4.1
|
||||||
ddp-client@2.6.1
|
ddp-client@2.6.1
|
||||||
ddp-common@1.4.0
|
ddp-common@1.4.0
|
||||||
ddp-rate-limiter@1.1.1
|
ddp-rate-limiter@1.2.0
|
||||||
ddp-server@2.6.0
|
ddp-server@2.6.1
|
||||||
diff-sequence@1.1.2
|
diff-sequence@1.1.2
|
||||||
dynamic-import@0.7.2
|
dynamic-import@0.7.3
|
||||||
ecmascript@0.16.3
|
ecmascript@0.16.7
|
||||||
ecmascript-runtime@0.8.0
|
ecmascript-runtime@0.8.1
|
||||||
ecmascript-runtime-client@0.12.1
|
ecmascript-runtime-client@0.12.1
|
||||||
ecmascript-runtime-server@0.11.0
|
ecmascript-runtime-server@0.11.0
|
||||||
ejson@1.1.3
|
ejson@1.1.3
|
||||||
email@2.2.2
|
email@2.2.5
|
||||||
es5-shim@4.8.0
|
es5-shim@4.8.0
|
||||||
fetch@0.1.2
|
fetch@0.1.3
|
||||||
geojson-utils@1.0.11
|
geojson-utils@1.0.11
|
||||||
google-oauth@1.4.2
|
google-oauth@1.4.3
|
||||||
hot-code-push@1.0.4
|
hot-code-push@1.0.4
|
||||||
html-tools@1.1.3
|
html-tools@1.1.3
|
||||||
htmljs@1.1.1
|
htmljs@1.1.1
|
||||||
@@ -52,36 +52,34 @@ inter-process-messaging@0.1.1
|
|||||||
lai:collection-extensions@0.3.0
|
lai:collection-extensions@0.3.0
|
||||||
launch-screen@1.3.0
|
launch-screen@1.3.0
|
||||||
littledata:synced-cron@1.5.1
|
littledata:synced-cron@1.5.1
|
||||||
livedata@1.0.18
|
|
||||||
localstorage@1.2.0
|
localstorage@1.2.0
|
||||||
logging@1.3.1
|
logging@1.3.2
|
||||||
mdg:meteor-apm-agent@3.5.1
|
mdg:validated-method@1.3.0
|
||||||
mdg:validated-method@1.2.0
|
meteor@1.11.2
|
||||||
meteor@1.10.2
|
|
||||||
meteor-base@1.5.1
|
meteor-base@1.5.1
|
||||||
meteortesting:browser-tests@1.3.5
|
meteortesting:browser-tests@1.4.2
|
||||||
meteortesting:mocha@2.0.3
|
meteortesting:mocha@2.1.0
|
||||||
meteortesting:mocha-core@8.1.2
|
meteortesting:mocha-core@8.1.2
|
||||||
mikowals:batch-insert@1.3.0
|
mikowals:batch-insert@1.3.0
|
||||||
minifier-css@1.6.1
|
minifier-css@1.6.4
|
||||||
minifier-js@2.7.5
|
minifier-js@2.7.5
|
||||||
minimongo@1.9.0
|
minimongo@1.9.3
|
||||||
|
mixmax:smart-disconnect@0.0.5
|
||||||
mobile-experience@1.1.0
|
mobile-experience@1.1.0
|
||||||
mobile-status-bar@1.1.0
|
mobile-status-bar@1.1.0
|
||||||
modern-browsers@0.1.9
|
modern-browsers@0.1.9
|
||||||
modules@0.19.0
|
modules@0.19.0
|
||||||
modules-runtime@0.13.1
|
modules-runtime@0.13.1
|
||||||
mongo@1.16.1
|
mongo@1.16.6
|
||||||
mongo-decimal@0.1.3
|
mongo-decimal@0.1.3
|
||||||
mongo-dev-server@1.1.0
|
mongo-dev-server@1.1.0
|
||||||
mongo-id@1.0.8
|
mongo-id@1.0.8
|
||||||
mongo-livedata@1.0.12
|
npm-mongo@4.16.0
|
||||||
npm-mongo@4.11.0
|
oauth@2.2.0
|
||||||
oauth@2.1.2
|
oauth2@1.3.2
|
||||||
oauth2@1.3.1
|
|
||||||
ordered-dict@1.1.0
|
ordered-dict@1.1.0
|
||||||
ostrio:cookies@2.7.2
|
ostrio:cookies@2.7.2
|
||||||
ostrio:files@2.3.2
|
ostrio:files@2.3.3
|
||||||
patreon-oauth@0.1.0
|
patreon-oauth@0.1.0
|
||||||
peerlibrary:assert@0.3.0
|
peerlibrary:assert@0.3.0
|
||||||
peerlibrary:check-extension@0.7.0
|
peerlibrary:check-extension@0.7.0
|
||||||
@@ -94,11 +92,11 @@ peerlibrary:reactive-publish@0.10.0
|
|||||||
peerlibrary:server-autorun@0.8.0
|
peerlibrary:server-autorun@0.8.0
|
||||||
peerlibrary:subscription-data@0.8.0
|
peerlibrary:subscription-data@0.8.0
|
||||||
percolate:migrations@1.1.0
|
percolate:migrations@1.1.0
|
||||||
promise@0.12.1
|
promise@0.12.2
|
||||||
raix:eventemitter@1.0.0
|
raix:eventemitter@1.0.0
|
||||||
random@1.2.1
|
random@1.2.1
|
||||||
rate-limit@1.0.9
|
rate-limit@1.1.1
|
||||||
react-fast-refresh@0.2.3
|
react-fast-refresh@0.2.7
|
||||||
reactive-dict@1.3.1
|
reactive-dict@1.3.1
|
||||||
reactive-var@1.0.12
|
reactive-var@1.0.12
|
||||||
reload@1.3.1
|
reload@1.3.1
|
||||||
@@ -114,16 +112,17 @@ simple:rest@1.2.1
|
|||||||
simple:rest-bearer-token-parser@1.1.1
|
simple:rest-bearer-token-parser@1.1.1
|
||||||
simple:rest-json-error-handler@1.1.1
|
simple:rest-json-error-handler@1.1.1
|
||||||
simple:rest-method-mixin@1.1.0
|
simple:rest-method-mixin@1.1.0
|
||||||
socket-stream-client@0.5.0
|
socket-stream-client@0.5.1
|
||||||
spacebars-compiler@1.3.1
|
spacebars-compiler@1.3.1
|
||||||
standard-minifier-js@2.8.1
|
standard-minifier-js@2.8.1
|
||||||
static-html@1.3.2
|
static-html@1.3.2
|
||||||
templating-tools@1.2.2
|
templating-tools@1.2.2
|
||||||
tmeasday:check-npm-versions@1.0.2
|
tmeasday:check-npm-versions@1.0.2
|
||||||
tracker@1.2.1
|
tracker@1.3.2
|
||||||
typescript@4.5.4
|
typescript@4.9.4
|
||||||
underscore@1.0.11
|
underscore@1.0.13
|
||||||
url@1.3.2
|
url@1.3.2
|
||||||
webapp@1.13.2
|
webapp@1.13.5
|
||||||
webapp-hashing@1.1.1
|
webapp-hashing@1.1.1
|
||||||
|
zegenie:redis-oplog@2.0.16
|
||||||
zer0th:meteor-vuetify-loader@0.1.41
|
zer0th:meteor-vuetify-loader@0.1.41
|
||||||
|
|||||||
27
app/imports/api/creature/actions/Actions.js
Normal file
27
app/imports/api/creature/actions/Actions.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import SimpleSchema from 'simpl-schema';
|
||||||
|
|
||||||
|
// Actions are creature actions that have been partially executed and not yet resolved
|
||||||
|
// They require some user input to progress
|
||||||
|
let Actions = new Mongo.Collection('actions');
|
||||||
|
|
||||||
|
let CreaturePropertySchema = new SimpleSchema({
|
||||||
|
_id: {
|
||||||
|
type: String,
|
||||||
|
regEx: SimpleSchema.RegEx.Id,
|
||||||
|
},
|
||||||
|
// Which creature is taking the action
|
||||||
|
_creatureId: {
|
||||||
|
type: String,
|
||||||
|
regEx: SimpleSchema.RegEx.Id,
|
||||||
|
},
|
||||||
|
// The user who began taking the action
|
||||||
|
user: {
|
||||||
|
type: String,
|
||||||
|
regEx: SimpleSchema.RegEx.Id,
|
||||||
|
},
|
||||||
|
// The property that is about to be applied
|
||||||
|
property: {
|
||||||
|
type: String,
|
||||||
|
regEx: SimpleSchema.RegEx.Id,
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -13,12 +13,12 @@ import { incrementFileStorageUsed } from '/imports/api/users/methods/updateFileS
|
|||||||
import verifyArchiveSafety from '/imports/api/creature/archive/methods/verifyArchiveSafety.js';
|
import verifyArchiveSafety from '/imports/api/creature/archive/methods/verifyArchiveSafety.js';
|
||||||
|
|
||||||
let migrateArchive;
|
let migrateArchive;
|
||||||
if (Meteor.isServer){
|
if (Meteor.isServer) {
|
||||||
migrateArchive = require('/imports/migrations/server/migrateArchive.js').default;
|
migrateArchive = require('/imports/migrations/archive/migrateArchive.js').default;
|
||||||
}
|
}
|
||||||
|
|
||||||
function restoreCreature(archive, userId){
|
function restoreCreature(archive, userId) {
|
||||||
if (SCHEMA_VERSION < archive.meta.schemaVersion){
|
if (SCHEMA_VERSION < archive.meta.schemaVersion) {
|
||||||
throw new Meteor.Error('Incompatible',
|
throw new Meteor.Error('Incompatible',
|
||||||
'The archive file is from a newer version. Update required to read.')
|
'The archive file is from a newer version. Update required to read.')
|
||||||
}
|
}
|
||||||
@@ -35,7 +35,7 @@ function restoreCreature(archive, userId){
|
|||||||
});
|
});
|
||||||
if (existingCreature) throw new Meteor.Error('Already exists',
|
if (existingCreature) throw new Meteor.Error('Already exists',
|
||||||
'The creature you are trying to restore already exists.')
|
'The creature you are trying to restore already exists.')
|
||||||
|
|
||||||
// Ensure the user owns the restored creature
|
// Ensure the user owns the restored creature
|
||||||
archive.creature.owner = userId;
|
archive.creature.owner = userId;
|
||||||
|
|
||||||
@@ -44,13 +44,13 @@ function restoreCreature(archive, userId){
|
|||||||
Creatures.insert(archive.creature);
|
Creatures.insert(archive.creature);
|
||||||
try {
|
try {
|
||||||
// Add all the properties
|
// Add all the properties
|
||||||
if (archive.properties && archive.properties.length){
|
if (archive.properties && archive.properties.length) {
|
||||||
CreatureProperties.batchInsert(archive.properties);
|
CreatureProperties.batchInsert(archive.properties);
|
||||||
}
|
}
|
||||||
if (archive.experiences && archive.experiences.length){
|
if (archive.experiences && archive.experiences.length) {
|
||||||
Experiences.batchInsert(archive.experiences);
|
Experiences.batchInsert(archive.experiences);
|
||||||
}
|
}
|
||||||
if (archive.logs && archive.logs.length){
|
if (archive.logs && archive.logs.length) {
|
||||||
CreatureLogs.batchInsert(archive.logs);
|
CreatureLogs.batchInsert(archive.logs);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -73,23 +73,23 @@ const restoreCreaturefromFile = new ValidatedMethod({
|
|||||||
numRequests: 10,
|
numRequests: 10,
|
||||||
timeInterval: 5000,
|
timeInterval: 5000,
|
||||||
},
|
},
|
||||||
async run({fileId}) {
|
async run({ fileId }) {
|
||||||
// fetch the file
|
// fetch the file
|
||||||
const file = ArchiveCreatureFiles.findOne({_id: fileId}).get();
|
const file = ArchiveCreatureFiles.findOne({ _id: fileId }).get();
|
||||||
if (!file){
|
if (!file) {
|
||||||
throw new Meteor.Error('File not found',
|
throw new Meteor.Error('File not found',
|
||||||
'The requested creature archive does not exist');
|
'The requested creature archive does not exist');
|
||||||
}
|
}
|
||||||
// Assert ownership
|
// Assert ownership
|
||||||
const userId = file?.userId;
|
const userId = file?.userId;
|
||||||
if (!userId || userId !== this.userId){
|
if (!userId || userId !== this.userId) {
|
||||||
throw new Meteor.Error('Permission denied',
|
throw new Meteor.Error('Permission denied',
|
||||||
'You can only restore creatures you own');
|
'You can only restore creatures you own');
|
||||||
}
|
}
|
||||||
|
|
||||||
assertHasCharactersSlots(this.userId);
|
assertHasCharactersSlots(this.userId);
|
||||||
|
|
||||||
if (Meteor.isServer){
|
if (Meteor.isServer) {
|
||||||
// Read the file data
|
// Read the file data
|
||||||
const archive = await ArchiveCreatureFiles.readJSONFile(file);
|
const archive = await ArchiveCreatureFiles.readJSONFile(file);
|
||||||
restoreCreature(archive, this.userId);
|
restoreCreature(archive, this.userId);
|
||||||
|
|||||||
@@ -46,6 +46,12 @@ let CreaturePropertySchema = new SimpleSchema({
|
|||||||
regEx: SimpleSchema.RegEx.Id,
|
regEx: SimpleSchema.RegEx.Id,
|
||||||
optional: true,
|
optional: true,
|
||||||
},
|
},
|
||||||
|
// Fill more than one quantity in a slot, like feats and ability score
|
||||||
|
// improvements, filtered out of UI if there isn't space in quantityExpected
|
||||||
|
slotQuantityFilled: {
|
||||||
|
type: SimpleSchema.Integer,
|
||||||
|
optional: true, // Undefined implies 1
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const DenormalisedOnlyCreaturePropertySchema = new SimpleSchema({
|
const DenormalisedOnlyCreaturePropertySchema = new SimpleSchema({
|
||||||
@@ -82,6 +88,12 @@ const DenormalisedOnlyCreaturePropertySchema = new SimpleSchema({
|
|||||||
index: 1,
|
index: 1,
|
||||||
removeBeforeCompute: true,
|
removeBeforeCompute: true,
|
||||||
},
|
},
|
||||||
|
deactivatingToggleId: {
|
||||||
|
type: String,
|
||||||
|
regEx: SimpleSchema.RegEx.Id,
|
||||||
|
optional: true,
|
||||||
|
removeBeforeCompute: true,
|
||||||
|
},
|
||||||
// When this is true on any property, the creature needs to be recomputed
|
// When this is true on any property, the creature needs to be recomputed
|
||||||
dirty: {
|
dirty: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
|
|||||||
@@ -0,0 +1,189 @@
|
|||||||
|
import SimpleSchema from 'simpl-schema';
|
||||||
|
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
||||||
|
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
||||||
|
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||||
|
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
|
||||||
|
import { RefSchema } from '/imports/api/parenting/ChildSchema.js';
|
||||||
|
import {
|
||||||
|
assertEditPermission,
|
||||||
|
assertDocEditPermission,
|
||||||
|
assertCopyPermission
|
||||||
|
} from '/imports/api/sharing/sharingPermissions.js';
|
||||||
|
import {
|
||||||
|
setLineageOfDocs,
|
||||||
|
getAncestry,
|
||||||
|
renewDocIds
|
||||||
|
} from '/imports/api/parenting/parenting.js';
|
||||||
|
import { reorderDocs } from '/imports/api/parenting/order.js';
|
||||||
|
import { setDocToLastOrder } from '/imports/api/parenting/order.js';
|
||||||
|
import Libraries from '/imports/api/library/Libraries.js';
|
||||||
|
const DUPLICATE_CHILDREN_LIMIT = 500;
|
||||||
|
|
||||||
|
const copyPropertyToLibrary = new ValidatedMethod({
|
||||||
|
name: 'creatureProperties.copyPropertyToLibrary',
|
||||||
|
validate: new SimpleSchema({
|
||||||
|
propId: {
|
||||||
|
type: String,
|
||||||
|
regEx: SimpleSchema.RegEx.Id,
|
||||||
|
},
|
||||||
|
parentRef: {
|
||||||
|
type: RefSchema,
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
type: Number,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
}).validator(),
|
||||||
|
mixins: [RateLimiterMixin],
|
||||||
|
rateLimit: {
|
||||||
|
numRequests: 1,
|
||||||
|
timeInterval: 5000,
|
||||||
|
},
|
||||||
|
run({ propId, parentRef, order }) {
|
||||||
|
// get the new ancestry for the properties
|
||||||
|
let { parentDoc, ancestors } = getAncestry({ parentRef });
|
||||||
|
|
||||||
|
// Check permission to edit the destination
|
||||||
|
let rootLibrary;
|
||||||
|
if (parentRef.collection === 'libraries') {
|
||||||
|
rootLibrary = parentDoc;
|
||||||
|
} else if (parentRef.collection === 'libraryNodes') {
|
||||||
|
rootLibrary = Libraries.findOne(parentDoc.ancestors[0].id)
|
||||||
|
} else {
|
||||||
|
throw `${parentRef.collection} is not a valid parent collection`
|
||||||
|
}
|
||||||
|
assertEditPermission(rootLibrary, this.userId);
|
||||||
|
|
||||||
|
const insertedRootNode = insertNodeFromProperty(propId, ancestors, order, this);
|
||||||
|
|
||||||
|
// Tree structure changed by inserts, reorder the tree
|
||||||
|
reorderDocs({
|
||||||
|
collection: LibraryNodes,
|
||||||
|
ancestorId: rootLibrary._id,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Return the docId of the inserted root property
|
||||||
|
return insertedRootNode?._id;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function insertNodeFromProperty(propId, ancestors, order, method) {
|
||||||
|
// Fetch the property and its descendants, provided they have not been
|
||||||
|
// removed
|
||||||
|
let prop = CreatureProperties.findOne({
|
||||||
|
_id: propId,
|
||||||
|
removed: { $ne: true },
|
||||||
|
});
|
||||||
|
if (!prop) {
|
||||||
|
if (Meteor.isClient) return;
|
||||||
|
else {
|
||||||
|
throw new Meteor.Error(
|
||||||
|
'Insert property from library failed',
|
||||||
|
`No property with id '${propId}' was found`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure we can edit this property
|
||||||
|
assertDocEditPermission(prop, method.userId);
|
||||||
|
|
||||||
|
let oldParent = prop.parent;
|
||||||
|
const propCursor = CreatureProperties.find({
|
||||||
|
'ancestors.id': propId,
|
||||||
|
removed: { $ne: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Make sure there aren't too many descendants
|
||||||
|
if (propCursor.count() > DUPLICATE_CHILDREN_LIMIT) {
|
||||||
|
throw new Meteor.Error('Copy children limit',
|
||||||
|
`The property has over ${DUPLICATE_CHILDREN_LIMIT} descendants and cannot be copied`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let props = propCursor.fetch();
|
||||||
|
|
||||||
|
// The root prop is first in the array of props
|
||||||
|
// It must get the first generated ID to prevent flickering
|
||||||
|
props = [prop, ...props];
|
||||||
|
|
||||||
|
// If the docs came from a library, that library must consent to this user copying their
|
||||||
|
// properties
|
||||||
|
assertSourceLibraryCopyPermission(props, method);
|
||||||
|
|
||||||
|
// re-map all the ancestors
|
||||||
|
setLineageOfDocs({
|
||||||
|
docArray: props,
|
||||||
|
newAncestry: ancestors,
|
||||||
|
oldParent,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Give the docs new IDs without breaking internal references
|
||||||
|
renewDocIds({
|
||||||
|
docArray: props,
|
||||||
|
collectionMap: { 'creatureProperties': 'libraryNodes' }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Order the root node
|
||||||
|
if (order === undefined) {
|
||||||
|
setDocToLastOrder({
|
||||||
|
collection: LibraryNodes,
|
||||||
|
doc: prop,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
prop.order = order;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert the props as library nodes
|
||||||
|
LibraryNodes.batchInsert(props);
|
||||||
|
return prop;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {[Property]} props The properties to check
|
||||||
|
* @param {String} userId The userId trying to copy these properties to a library
|
||||||
|
* Checks that every property can be copied out of the library that originated it by this user
|
||||||
|
*/
|
||||||
|
function assertSourceLibraryCopyPermission(props, method) {
|
||||||
|
// Skip on the client
|
||||||
|
if (method.isSimulation) return;
|
||||||
|
|
||||||
|
// Get all the library node ids that are sources for these properties
|
||||||
|
const libraryNodeIds = [];
|
||||||
|
props.forEach(prop => {
|
||||||
|
if (prop.libraryNodeId) libraryNodeIds.push(prop.libraryNodeId);
|
||||||
|
});
|
||||||
|
if (!libraryNodeIds.length) return;
|
||||||
|
|
||||||
|
// Get the actual library Ids that each of these source nodes came from
|
||||||
|
const sourceLibIds = new Set();
|
||||||
|
LibraryNodes.find({
|
||||||
|
_id: { $in: libraryNodeIds }
|
||||||
|
}, {
|
||||||
|
fields: { ancestors: 1 }
|
||||||
|
}).forEach(node => {
|
||||||
|
sourceLibIds.add(node.ancestors?.[0]?.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert copy permission on each of those libraries
|
||||||
|
Libraries.find({
|
||||||
|
_id: { $in: Array.from(sourceLibIds) }
|
||||||
|
}, {
|
||||||
|
fields: {
|
||||||
|
name: 1,
|
||||||
|
owner: 1,
|
||||||
|
readers: 1,
|
||||||
|
writers: 1,
|
||||||
|
public: 1,
|
||||||
|
readersCanCopy: 1,
|
||||||
|
}
|
||||||
|
}).forEach(lib => {
|
||||||
|
try {
|
||||||
|
assertCopyPermission(lib, method.userId);
|
||||||
|
} catch (e) {
|
||||||
|
throw new Meteor.Error('Copy permission denied',
|
||||||
|
`One of the properties you are copying comes from ${lib.name}, which you do not have permission to copy from`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default copyPropertyToLibrary;
|
||||||
@@ -64,12 +64,12 @@ export function damagePropertyWork({ prop, operation, value, actionContext, logF
|
|||||||
// Save the value to the scope before applying the before triggers
|
// Save the value to the scope before applying the before triggers
|
||||||
if (operation === 'increment') {
|
if (operation === 'increment') {
|
||||||
if (value >= 0) {
|
if (value >= 0) {
|
||||||
actionContext.scope['$damage'] = value;
|
actionContext.scope['~damage'] = { value };
|
||||||
} else {
|
} else {
|
||||||
actionContext.scope['$healing'] = -value;
|
actionContext.scope['~healing'] = { value: -value };
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
actionContext.scope['$set'] = value;
|
actionContext.scope['~set'] = { value };
|
||||||
}
|
}
|
||||||
|
|
||||||
applyTriggers(actionContext.triggers?.damageProperty?.before, prop, actionContext);
|
applyTriggers(actionContext.triggers?.damageProperty?.before, prop, actionContext);
|
||||||
@@ -77,12 +77,12 @@ export function damagePropertyWork({ prop, operation, value, actionContext, logF
|
|||||||
// fetch the value from the scope after the before triggers, in case they changed them
|
// fetch the value from the scope after the before triggers, in case they changed them
|
||||||
if (operation === 'increment') {
|
if (operation === 'increment') {
|
||||||
if (value >= 0) {
|
if (value >= 0) {
|
||||||
value = actionContext.scope['$damage'];
|
value = actionContext.scope['~damage']?.value;
|
||||||
} else {
|
} else {
|
||||||
value = -actionContext.scope['$healing'];
|
value = -actionContext.scope['~healing']?.value;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
value = actionContext.scope['$set'];
|
value = actionContext.scope['~set']?.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
let damage, newValue, increment;
|
let damage, newValue, increment;
|
||||||
|
|||||||
@@ -42,6 +42,11 @@ const duplicateProperty = new ValidatedMethod({
|
|||||||
let propertyId = randomSrc.id();
|
let propertyId = randomSrc.id();
|
||||||
property._id = propertyId;
|
property._id = propertyId;
|
||||||
|
|
||||||
|
// Change the variableName so it isn't immediately overridden
|
||||||
|
if (property.variableName) {
|
||||||
|
property.variableName += 'Copy'
|
||||||
|
}
|
||||||
|
|
||||||
// Get all the descendants
|
// Get all the descendants
|
||||||
let nodes = CreatureProperties.find({
|
let nodes = CreatureProperties.find({
|
||||||
'ancestors.id': _id,
|
'ancestors.id': _id,
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
export default function getSlotFillFilter({slot, libraryIds}){
|
export default function getSlotFillFilter({ slot, libraryIds }) {
|
||||||
|
|
||||||
|
if (!slot) throw 'Slot is required for getSlotFillFilter';
|
||||||
|
if (!libraryIds) throw 'LibraryIds is required for getSlotFillFilter';
|
||||||
|
|
||||||
let filter = {
|
let filter = {
|
||||||
removed: {$ne: true},
|
fillSlots: true,
|
||||||
|
removed: { $ne: true },
|
||||||
$and: []
|
$and: []
|
||||||
};
|
};
|
||||||
if (libraryIds){
|
filter['ancestors.id'] = { $in: libraryIds };
|
||||||
filter['ancestors.id'] = {$in: libraryIds};
|
if (slot.slotType) {
|
||||||
}
|
|
||||||
if (slot.slotType){
|
|
||||||
filter.$and.push({
|
filter.$and.push({
|
||||||
$or: [{
|
$or: [{
|
||||||
type: slot.slotType
|
type: slot.slotType
|
||||||
},{
|
}, {
|
||||||
type: 'slotFiller',
|
|
||||||
slotFillerType: slot.slotType,
|
slotFillerType: slot.slotType,
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
@@ -19,44 +21,43 @@ export default function getSlotFillFilter({slot, libraryIds}){
|
|||||||
filter.$and.push({
|
filter.$and.push({
|
||||||
$or: [{
|
$or: [{
|
||||||
type: 'classLevel',
|
type: 'classLevel',
|
||||||
},{
|
}, {
|
||||||
type: 'slotFiller',
|
|
||||||
slotFillerType: 'classLevel',
|
slotFillerType: 'classLevel',
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
if (slot.variableName) {
|
if (slot.variableName) {
|
||||||
filter.variableName = slot.variableName;
|
filter.variableName = slot.variableName;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only search for levels the class needs
|
// Only search for levels the class needs
|
||||||
if (slot.missingLevels && slot.missingLevels.length) {
|
if (slot.missingLevels && slot.missingLevels.length) {
|
||||||
filter.level = {$in: slot.missingLevels};
|
filter.level = { $in: slot.missingLevels };
|
||||||
} else {
|
} else {
|
||||||
filter.level = (slot.level || 0) + 1;
|
filter.level = { $gt: slot.level || 0 };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let tagsOr = [];
|
let tagsOr = [];
|
||||||
let tagsNin = [];
|
let tagsNin = [];
|
||||||
if (slot.slotTags && slot.slotTags.length){
|
if (slot.slotTags && slot.slotTags.length) {
|
||||||
tagsOr.push({tags: {$all: slot.slotTags}});
|
tagsOr.push({ libraryTags: { $all: slot.slotTags } });
|
||||||
}
|
}
|
||||||
if (slot.extraTags && slot.extraTags.length){
|
if (slot.extraTags && slot.extraTags.length) {
|
||||||
slot.extraTags.forEach(extra => {
|
slot.extraTags.forEach(extra => {
|
||||||
if (!extra.tags || !extra.tags.length) return;
|
if (!extra.tags || !extra.tags.length) return;
|
||||||
if (extra.operation === 'OR'){
|
if (extra.operation === 'OR') {
|
||||||
tagsOr.push({tags: {$all: extra.tags}});
|
tagsOr.push({ libraryTags: { $all: extra.tags } });
|
||||||
} else if (extra.operation === 'NOT'){
|
} else if (extra.operation === 'NOT') {
|
||||||
tagsNin.push(...extra.tags);
|
tagsNin.push(...extra.tags);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (tagsOr.length){
|
if (tagsOr.length) {
|
||||||
filter.$or = tagsOr;
|
filter.$or = tagsOr;
|
||||||
}
|
}
|
||||||
if (tagsNin.length){
|
if (tagsNin.length) {
|
||||||
filter.$and.push({tags: {$nin: tagsNin}});
|
filter.$and.push({ libraryTags: { $nin: tagsNin } });
|
||||||
}
|
}
|
||||||
if (!filter.$and.length){
|
if (!filter.$and.length) {
|
||||||
delete filter.$and;
|
delete filter.$and;
|
||||||
}
|
}
|
||||||
return filter;
|
return filter;
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import { assert } from 'chai';
|
||||||
|
import getSlotFillFilter from '/imports/api/creature/creatureProperties/methods/getSlotFillFilter.js';
|
||||||
|
|
||||||
|
describe('Slot fill filter', function () {
|
||||||
|
|
||||||
|
it('Gives error if arguments aren\'t provided', function () {
|
||||||
|
assert.throws(
|
||||||
|
() => getSlotFillFilter(undefined),
|
||||||
|
null, null, 'Passing undefined should give an error'
|
||||||
|
);
|
||||||
|
assert.throws(
|
||||||
|
() => getSlotFillFilter({
|
||||||
|
slot: { slotTags: ['tag1'] },
|
||||||
|
}),
|
||||||
|
null, null, 'Passing no libraryIds should give an error'
|
||||||
|
);
|
||||||
|
assert.throws(
|
||||||
|
() => getSlotFillFilter({
|
||||||
|
libraryIds: ['libraryId1'],
|
||||||
|
}),
|
||||||
|
null, null, 'Passing no slot should give an error'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('filters using basic slot tags', function () {
|
||||||
|
const filter = getSlotFillFilter({
|
||||||
|
slot: {
|
||||||
|
slotTags: ['tag1', 'tag2']
|
||||||
|
},
|
||||||
|
libraryIds: ['libraryId1', 'libraryId2'],
|
||||||
|
});
|
||||||
|
assert.deepStrictEqual(filter, {
|
||||||
|
$or: [{
|
||||||
|
libraryTags: { $all: ['tag1', 'tag2'] }
|
||||||
|
}],
|
||||||
|
'ancestors.id': { $in: ['libraryId1', 'libraryId2'] },
|
||||||
|
removed: { $ne: true },
|
||||||
|
fillSlots: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('filters using slot type', function () {
|
||||||
|
const filter = getSlotFillFilter({
|
||||||
|
slot: {
|
||||||
|
slotTags: ['tag1', 'tag2'],
|
||||||
|
slotType: 'feature',
|
||||||
|
},
|
||||||
|
libraryIds: ['libraryId1', 'libraryId2']
|
||||||
|
});
|
||||||
|
assert.deepStrictEqual(filter.$and, [{
|
||||||
|
$or: [{
|
||||||
|
type: 'feature'
|
||||||
|
}, {
|
||||||
|
slotFillerType: 'feature',
|
||||||
|
}],
|
||||||
|
}]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('filters using extra tags', function () {
|
||||||
|
const filter = getSlotFillFilter({
|
||||||
|
slot: {
|
||||||
|
slotTags: ['tag1', 'tag2'],
|
||||||
|
extraTags: [
|
||||||
|
{ operation: 'OR', tags: ['tag3', 'tag4'] },
|
||||||
|
{ operation: 'NOT', tags: ['tag5', 'tag6'] },
|
||||||
|
{ operation: 'NOT', tags: ['tag7', 'tag8'] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
libraryIds: ['libraryId1', 'libraryId2'],
|
||||||
|
});
|
||||||
|
assert.deepStrictEqual(filter, {
|
||||||
|
$or: [
|
||||||
|
{ libraryTags: { $all: ['tag1', 'tag2'] } },
|
||||||
|
{ libraryTags: { $all: ['tag3', 'tag4'] } },
|
||||||
|
],
|
||||||
|
$and: [
|
||||||
|
{ libraryTags: { $nin: ['tag5', 'tag6', 'tag7', 'tag8'] } },
|
||||||
|
],
|
||||||
|
'ancestors.id': { $in: ['libraryId1', 'libraryId2'] },
|
||||||
|
removed: { $ne: true },
|
||||||
|
fillSlots: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import '/imports/api/creature/creatureProperties/methods/adjustQuantity.js';
|
import '/imports/api/creature/creatureProperties/methods/adjustQuantity.js';
|
||||||
|
import '/imports/api/creature/creatureProperties/methods/copyPropertyToLibrary.js';
|
||||||
import '/imports/api/creature/creatureProperties/methods/damageProperty.js';
|
import '/imports/api/creature/creatureProperties/methods/damageProperty.js';
|
||||||
import '/imports/api/creature/creatureProperties/methods/duplicateProperty.js';
|
import '/imports/api/creature/creatureProperties/methods/duplicateProperty.js';
|
||||||
import '/imports/api/creature/creatureProperties/methods/equipItem.js';
|
import '/imports/api/creature/creatureProperties/methods/equipItem.js';
|
||||||
|
|||||||
@@ -98,13 +98,14 @@ function insertPropertyFromNode(nodeId, ancestors, order) {
|
|||||||
removed: { $ne: true },
|
removed: { $ne: true },
|
||||||
}).fetch();
|
}).fetch();
|
||||||
|
|
||||||
// Convert all references into actual nodes
|
|
||||||
nodes = reifyNodeReferences(nodes);
|
|
||||||
|
|
||||||
// The root node is first in the array of nodes
|
// The root node is first in the array of nodes
|
||||||
// It must get the first generated ID to prevent flickering
|
// It must get the first generated ID to prevent flickering
|
||||||
nodes = [node, ...nodes];
|
nodes = [node, ...nodes];
|
||||||
|
|
||||||
|
// Convert all references into actual nodes
|
||||||
|
nodes = reifyNodeReferences(nodes);
|
||||||
|
|
||||||
// set libraryNodeIds
|
// set libraryNodeIds
|
||||||
storeLibraryNodeReferences(nodes);
|
storeLibraryNodeReferences(nodes);
|
||||||
|
|
||||||
|
|||||||
@@ -83,13 +83,13 @@ export function resetProperties(creatureId, resetFilter, actionContext) {
|
|||||||
const attributeFilter = {
|
const attributeFilter = {
|
||||||
...filter,
|
...filter,
|
||||||
type: 'attribute',
|
type: 'attribute',
|
||||||
damage: { $ne: 0 },
|
damage: { $nin: [0, undefined] },
|
||||||
}
|
}
|
||||||
CreatureProperties.find(attributeFilter).forEach(prop => {
|
CreatureProperties.find(attributeFilter).forEach(prop => {
|
||||||
damagePropertyWork({
|
damagePropertyWork({
|
||||||
prop,
|
prop,
|
||||||
operation: 'increment',
|
operation: 'increment',
|
||||||
value: -prop.damage,
|
value: -prop.damage ?? 0,
|
||||||
actionContext,
|
actionContext,
|
||||||
logFunction(increment) {
|
logFunction(increment) {
|
||||||
actionContext.addLog({
|
actionContext.addLog({
|
||||||
@@ -105,7 +105,7 @@ export function resetProperties(creatureId, resetFilter, actionContext) {
|
|||||||
type: {
|
type: {
|
||||||
$in: ['action', 'spell']
|
$in: ['action', 'spell']
|
||||||
},
|
},
|
||||||
usesUsed: { $ne: 0 },
|
usesUsed: { $nin: [0, undefined] },
|
||||||
};
|
};
|
||||||
CreatureProperties.find(actionFilter, {
|
CreatureProperties.find(actionFilter, {
|
||||||
fields: { name: 1, usesUsed: 1 }
|
fields: { name: 1, usesUsed: 1 }
|
||||||
|
|||||||
@@ -7,9 +7,10 @@ import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
|||||||
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
|
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
|
||||||
import { parse, prettifyParseError } from '/imports/parser/parser.js';
|
import { parse, prettifyParseError } from '/imports/parser/parser.js';
|
||||||
import resolve, { toString } from '/imports/parser/resolve.js';
|
import resolve, { toString } from '/imports/parser/resolve.js';
|
||||||
const PER_CREATURE_LOG_LIMIT = 100;
|
|
||||||
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
||||||
|
|
||||||
|
const PER_CREATURE_LOG_LIMIT = 100;
|
||||||
|
|
||||||
if (Meteor.isServer) {
|
if (Meteor.isServer) {
|
||||||
var sendWebhookAsCreature = require('/imports/server/discord/sendWebhook.js').sendWebhookAsCreature;
|
var sendWebhookAsCreature = require('/imports/server/discord/sendWebhook.js').sendWebhookAsCreature;
|
||||||
}
|
}
|
||||||
@@ -70,10 +71,21 @@ function logToMessageData(log) {
|
|||||||
let embed = {
|
let embed = {
|
||||||
fields: [],
|
fields: [],
|
||||||
};
|
};
|
||||||
log.content.forEach(field => {
|
log.content.forEach((field, index) => {
|
||||||
|
// Empty character for blank names
|
||||||
if (!field.name) field.name = '\u200b';
|
if (!field.name) field.name = '\u200b';
|
||||||
if (!field.value) field.value = '\u200b';
|
if (!field.value) field.value = '\u200b';
|
||||||
embed.fields.push(field);
|
// Enforce Discord field character limits
|
||||||
|
if (field.name?.length > 256) {
|
||||||
|
field.name = field.name.substring(0, 255);
|
||||||
|
}
|
||||||
|
if (field.value?.length > 1024) {
|
||||||
|
field.value = field.value.substring(0, 1024 - 3) + '...';
|
||||||
|
}
|
||||||
|
// Enforce Discord 25 field limit
|
||||||
|
if (index < 25) {
|
||||||
|
embed.fields.push(field);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return { embeds: [embed] };
|
return { embeds: [embed] };
|
||||||
}
|
}
|
||||||
@@ -122,7 +134,15 @@ export function insertCreatureLogWork({ log, creature, method }) {
|
|||||||
log = { content: [{ value: log }] };
|
log = { content: [{ value: log }] };
|
||||||
}
|
}
|
||||||
if (!log.content?.length) return;
|
if (!log.content?.length) return;
|
||||||
|
|
||||||
|
// Truncate the string lengths to fit the log content schema
|
||||||
|
log.content.forEach((logItem) => {
|
||||||
|
if (logItem.value?.length > STORAGE_LIMITS.summary) {
|
||||||
|
logItem.value = logItem.value.substring(0, STORAGE_LIMITS.summary - 3) + '...';
|
||||||
|
}
|
||||||
|
});
|
||||||
log.date = new Date();
|
log.date = new Date();
|
||||||
|
|
||||||
// Insert it
|
// Insert it
|
||||||
let id = CreatureLogs.insert(log);
|
let id = CreatureLogs.insert(log);
|
||||||
if (Meteor.isServer) {
|
if (Meteor.isServer) {
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import { damagePropertyWork } from '/imports/api/creature/creatureProperties/met
|
|||||||
import numberToSignedString from '/imports/api/utility/numberToSignedString.js';
|
import numberToSignedString from '/imports/api/utility/numberToSignedString.js';
|
||||||
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
|
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
|
||||||
import { resetProperties } from '/imports/api/creature/creatures/methods/restCreature.js';
|
import { resetProperties } from '/imports/api/creature/creatures/methods/restCreature.js';
|
||||||
|
import { getPropertyDecendants } from '/imports/api/engine/loadCreatures.js';
|
||||||
|
import { nodeArrayToTree } from '/imports/api/parenting/nodesToTree.js';
|
||||||
|
|
||||||
export default function applyAction(node, actionContext) {
|
export default function applyAction(node, actionContext) {
|
||||||
applyNodeTriggers(node, 'before', actionContext);
|
applyNodeTriggers(node, 'before', actionContext);
|
||||||
@@ -51,11 +53,11 @@ export default function applyAction(node, actionContext) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function applyAttackWithoutTarget({ attack, actionContext }) {
|
function applyAttackWithoutTarget({ attack, actionContext }) {
|
||||||
delete actionContext.scope['$attackHit'];
|
delete actionContext.scope['~attackHit'];
|
||||||
delete actionContext.scope['$attackMiss'];
|
delete actionContext.scope['~attackMiss'];
|
||||||
delete actionContext.scope['$criticalHit'];
|
delete actionContext.scope['~criticalHit'];
|
||||||
delete actionContext.scope['$criticalMiss'];
|
delete actionContext.scope['~criticalMiss'];
|
||||||
delete actionContext.scope['$attackRoll'];
|
delete actionContext.scope['~attackRoll'];
|
||||||
|
|
||||||
recalculateCalculation(attack, actionContext);
|
recalculateCalculation(attack, actionContext);
|
||||||
const scope = actionContext.scope;
|
const scope = actionContext.scope;
|
||||||
@@ -66,16 +68,16 @@ function applyAttackWithoutTarget({ attack, actionContext }) {
|
|||||||
criticalMiss,
|
criticalMiss,
|
||||||
} = rollAttack(attack, scope);
|
} = rollAttack(attack, scope);
|
||||||
let name = criticalHit ? 'Critical Hit!' : criticalMiss ? 'Critical Miss!' : 'To Hit';
|
let name = criticalHit ? 'Critical Hit!' : criticalMiss ? 'Critical Miss!' : 'To Hit';
|
||||||
if (scope['$attackAdvantage'] === 1) {
|
if (scope['~attackAdvantage']?.value === 1) {
|
||||||
name += ' (Advantage)';
|
name += ' (Advantage)';
|
||||||
} else if (scope['$attackAdvantage'] === -1) {
|
} else if (scope['~attackAdvantage']?.value === -1) {
|
||||||
name += ' (Disadvantage)';
|
name += ' (Disadvantage)';
|
||||||
}
|
}
|
||||||
if (!criticalMiss) {
|
if (!criticalMiss) {
|
||||||
scope['$attackHit'] = { value: true }
|
scope['~attackHit'] = { value: true }
|
||||||
}
|
}
|
||||||
if (!criticalHit) {
|
if (!criticalHit) {
|
||||||
scope['$attackMiss'] = { value: true };
|
scope['~attackMiss'] = { value: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
actionContext.addLog({
|
actionContext.addLog({
|
||||||
@@ -87,12 +89,12 @@ function applyAttackWithoutTarget({ attack, actionContext }) {
|
|||||||
|
|
||||||
function applyAttackToTarget({ attack, target, actionContext }) {
|
function applyAttackToTarget({ attack, target, actionContext }) {
|
||||||
const scope = actionContext.scope;
|
const scope = actionContext.scope;
|
||||||
delete scope['$attackHit'];
|
delete scope['~attackHit'];
|
||||||
delete scope['$attackMiss'];
|
delete scope['~attackMiss'];
|
||||||
delete scope['$criticalHit'];
|
delete scope['~criticalHit'];
|
||||||
delete scope['$criticalMiss'];
|
delete scope['~criticalMiss'];
|
||||||
delete scope['$attackDiceRoll'];
|
delete scope['~attackDiceRoll'];
|
||||||
delete scope['$attackRoll'];
|
delete scope['~attackRoll'];
|
||||||
|
|
||||||
recalculateCalculation(attack, actionContext);
|
recalculateCalculation(attack, actionContext);
|
||||||
|
|
||||||
@@ -109,9 +111,9 @@ function applyAttackToTarget({ attack, target, actionContext }) {
|
|||||||
let name = criticalHit ? 'Critical Hit!' :
|
let name = criticalHit ? 'Critical Hit!' :
|
||||||
criticalMiss ? 'Critical Miss!' :
|
criticalMiss ? 'Critical Miss!' :
|
||||||
result > armor ? 'Hit!' : 'Miss!';
|
result > armor ? 'Hit!' : 'Miss!';
|
||||||
if (scope['$attackAdvantage'] === 1) {
|
if (scope['~attackAdvantage']?.value === 1) {
|
||||||
name += ' (Advantage)';
|
name += ' (Advantage)';
|
||||||
} else if (scope['$attackAdvantage'] === -1) {
|
} else if (scope['~attackAdvantage']?.value === -1) {
|
||||||
name += ' (Disadvantage)';
|
name += ' (Disadvantage)';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,9 +123,9 @@ function applyAttackToTarget({ attack, target, actionContext }) {
|
|||||||
inline: true,
|
inline: true,
|
||||||
});
|
});
|
||||||
if (criticalMiss || result < armor) {
|
if (criticalMiss || result < armor) {
|
||||||
scope['$attackMiss'] = { value: true };
|
scope['~attackMiss'] = { value: true };
|
||||||
} else {
|
} else {
|
||||||
scope['$attackHit'] = { value: true };
|
scope['~attackHit'] = { value: true };
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
actionContext.addLog({
|
actionContext.addLog({
|
||||||
@@ -141,7 +143,7 @@ function applyAttackToTarget({ attack, target, actionContext }) {
|
|||||||
function rollAttack(attack, scope) {
|
function rollAttack(attack, scope) {
|
||||||
const rollModifierText = numberToSignedString(attack.value, true);
|
const rollModifierText = numberToSignedString(attack.value, true);
|
||||||
let value, resultPrefix;
|
let value, resultPrefix;
|
||||||
if (scope['$attackAdvantage'] === 1) {
|
if (scope['~attackAdvantage']?.value === 1) {
|
||||||
const [a, b] = rollDice(2, 20);
|
const [a, b] = rollDice(2, 20);
|
||||||
if (a >= b) {
|
if (a >= b) {
|
||||||
value = a;
|
value = a;
|
||||||
@@ -150,7 +152,7 @@ function rollAttack(attack, scope) {
|
|||||||
value = b;
|
value = b;
|
||||||
resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`;
|
resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`;
|
||||||
}
|
}
|
||||||
} else if (scope['$attackAdvantage'] === -1) {
|
} else if (scope['~attackAdvantage']?.value === -1) {
|
||||||
const [a, b] = rollDice(2, 20);
|
const [a, b] = rollDice(2, 20);
|
||||||
if (a <= b) {
|
if (a <= b) {
|
||||||
value = a;
|
value = a;
|
||||||
@@ -163,23 +165,23 @@ function rollAttack(attack, scope) {
|
|||||||
value = rollDice(1, 20)[0];
|
value = rollDice(1, 20)[0];
|
||||||
resultPrefix = `1d20 [${value}] ${rollModifierText}`
|
resultPrefix = `1d20 [${value}] ${rollModifierText}`
|
||||||
}
|
}
|
||||||
scope['$attackDiceRoll'] = { value };
|
scope['~attackDiceRoll'] = { value };
|
||||||
const result = value + attack.value;
|
const result = value + attack.value;
|
||||||
scope['$attackRoll'] = { value: result };
|
scope['~attackRoll'] = { value: result };
|
||||||
const { criticalHit, criticalMiss } = applyCrits(value, scope);
|
const { criticalHit, criticalMiss } = applyCrits(value, scope);
|
||||||
return { resultPrefix, result, value, criticalHit, criticalMiss };
|
return { resultPrefix, result, value, criticalHit, criticalMiss };
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyCrits(value, scope) {
|
function applyCrits(value, scope) {
|
||||||
let criticalHitTarget = scope.criticalHitTarget?.value || 20;
|
const criticalHitTarget = scope['~criticalHitTarget']?.value || 20;
|
||||||
let criticalHit = value >= criticalHitTarget;
|
let criticalHit = value >= criticalHitTarget;
|
||||||
let criticalMiss;
|
let criticalMiss;
|
||||||
if (criticalHit) {
|
if (criticalHit) {
|
||||||
scope['$criticalHit'] = { value: true };
|
scope['~criticalHit'] = { value: true };
|
||||||
} else {
|
} else {
|
||||||
criticalMiss = value === 1;
|
criticalMiss = value === 1;
|
||||||
if (criticalMiss) {
|
if (criticalMiss) {
|
||||||
scope['$criticalMiss'] = { value: true };
|
scope['~criticalMiss'] = { value: true };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { criticalHit, criticalMiss };
|
return { criticalHit, criticalMiss };
|
||||||
@@ -211,6 +213,7 @@ function spendResources(prop, actionContext) {
|
|||||||
let itemQuantityAdjustments = [];
|
let itemQuantityAdjustments = [];
|
||||||
let spendLog = [];
|
let spendLog = [];
|
||||||
let gainLog = [];
|
let gainLog = [];
|
||||||
|
let ammoChildren = [];
|
||||||
try {
|
try {
|
||||||
prop.resources.itemsConsumed.forEach(itemConsumed => {
|
prop.resources.itemsConsumed.forEach(itemConsumed => {
|
||||||
recalculateCalculation(itemConsumed.quantity, actionContext);
|
recalculateCalculation(itemConsumed.quantity, actionContext);
|
||||||
@@ -221,9 +224,6 @@ function spendResources(prop, actionContext) {
|
|||||||
if (!item || item.ancestors[0].id !== prop.ancestors[0].id) {
|
if (!item || item.ancestors[0].id !== prop.ancestors[0].id) {
|
||||||
throw 'The prop\'s ammo was not found on the creature';
|
throw 'The prop\'s ammo was not found on the creature';
|
||||||
}
|
}
|
||||||
if (!item.equipped) {
|
|
||||||
throw 'The selected ammo is not equipped';
|
|
||||||
}
|
|
||||||
if (
|
if (
|
||||||
!itemConsumed.quantity.value ||
|
!itemConsumed.quantity.value ||
|
||||||
!isFinite(itemConsumed.quantity.value)
|
!isFinite(itemConsumed.quantity.value)
|
||||||
@@ -242,6 +242,7 @@ function spendResources(prop, actionContext) {
|
|||||||
} else if (itemConsumed.quantity.value < 0) {
|
} else if (itemConsumed.quantity.value < 0) {
|
||||||
gainLog.push(logName + ': ' + -itemConsumed.quantity.value);
|
gainLog.push(logName + ': ' + -itemConsumed.quantity.value);
|
||||||
}
|
}
|
||||||
|
ammoChildren.push(...getItemChildren(item, actionContext, prop));
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
actionContext.addLog({
|
actionContext.addLog({
|
||||||
@@ -303,4 +304,36 @@ function spendResources(prop, actionContext) {
|
|||||||
value: spendLog.join('\n'),
|
value: spendLog.join('\n'),
|
||||||
inline: true,
|
inline: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Apply the ammo children
|
||||||
|
ammoChildren.forEach(prop => {
|
||||||
|
applyProperty(prop, actionContext);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getItemChildren(item, actionContext, prop) {
|
||||||
|
// Skip if the prop or the item are ancestors of one another, otherwise infinite loop
|
||||||
|
if (hasAncestorRelationship(item, prop)) return [];
|
||||||
|
// Get the item children
|
||||||
|
const itemProperties = getPropertyDecendants(actionContext.creature._id, item._id);
|
||||||
|
// Tree them up
|
||||||
|
const propertyForest = nodeArrayToTree(itemProperties);
|
||||||
|
return propertyForest
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasAncestorRelationship(a, b) {
|
||||||
|
let top, bottom;
|
||||||
|
if (a.ancestors.length === b.ancestors.length) {
|
||||||
|
// Can't be ancestors of one another if they have the same number of ancestors
|
||||||
|
return false;
|
||||||
|
} else if (a.ancestors.length > b.ancestors.length) {
|
||||||
|
// longer ancestor list goes on the bottom
|
||||||
|
top = b;
|
||||||
|
bottom = a;
|
||||||
|
} else {
|
||||||
|
top = a;
|
||||||
|
bottom = b;
|
||||||
|
}
|
||||||
|
const expectedAncestorPosition = top.ancestors.length;
|
||||||
|
return bottom.ancestors[expectedAncestorPosition]?.id === top._id;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,22 +3,22 @@ import recalculateCalculation from './shared/recalculateCalculation.js';
|
|||||||
import rollDice from '/imports/parser/rollDice.js';
|
import rollDice from '/imports/parser/rollDice.js';
|
||||||
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
|
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
|
||||||
|
|
||||||
export default function applyBranch(node, actionContext){
|
export default function applyBranch(node, actionContext) {
|
||||||
applyNodeTriggers(node, 'before', actionContext);
|
applyNodeTriggers(node, 'before', actionContext);
|
||||||
const applyChildren = function(){
|
const applyChildren = function () {
|
||||||
applyNodeTriggers(node, 'after', actionContext);
|
applyNodeTriggers(node, 'after', actionContext);
|
||||||
node.children.forEach(child => applyProperty(child, actionContext));
|
node.children.forEach(child => applyProperty(child, actionContext));
|
||||||
};
|
};
|
||||||
const scope = actionContext.scope;
|
const scope = actionContext.scope;
|
||||||
const targets = actionContext.targets;
|
const targets = actionContext.targets;
|
||||||
const prop = node.node;
|
const prop = node.node;
|
||||||
switch(prop.branchType){
|
switch (prop.branchType) {
|
||||||
case 'if':
|
case 'if':
|
||||||
recalculateCalculation(prop.condition, actionContext);
|
recalculateCalculation(prop.condition, actionContext);
|
||||||
if (prop.condition?.value) applyChildren();
|
if (prop.condition?.value) applyChildren();
|
||||||
break;
|
break;
|
||||||
case 'index':
|
case 'index':
|
||||||
if (node.children.length){
|
if (node.children.length) {
|
||||||
recalculateCalculation(prop.condition, actionContext);
|
recalculateCalculation(prop.condition, actionContext);
|
||||||
if (!isFinite(prop.condition?.value)) {
|
if (!isFinite(prop.condition?.value)) {
|
||||||
actionContext.addLog({
|
actionContext.addLog({
|
||||||
@@ -35,31 +35,31 @@ export default function applyBranch(node, actionContext){
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'hit':
|
case 'hit':
|
||||||
if (scope['$attackHit']?.value){
|
if (scope['~attackHit']?.value) {
|
||||||
if (!targets.length && !prop.silent) actionContext.addLog({value: '**On hit**'});
|
if (!targets.length && !prop.silent) actionContext.addLog({ value: '**On hit**' });
|
||||||
applyChildren();
|
applyChildren();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'miss':
|
case 'miss':
|
||||||
if (scope['$attackMiss']?.value){
|
if (scope['~attackMiss']?.value) {
|
||||||
if (!targets.length && !prop.silent) actionContext.addLog({value: '**On miss**'});
|
if (!targets.length && !prop.silent) actionContext.addLog({ value: '**On miss**' });
|
||||||
applyChildren();
|
applyChildren();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'failedSave':
|
case 'failedSave':
|
||||||
if (scope['$saveFailed']?.value){
|
if (scope['~saveFailed']?.value) {
|
||||||
if (!targets.length && !prop.silent) actionContext.addLog({value: '**On failed save**'});
|
if (!targets.length && !prop.silent) actionContext.addLog({ value: '**On failed save**' });
|
||||||
applyChildren();
|
applyChildren();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'successfulSave':
|
case 'successfulSave':
|
||||||
if (scope['$saveSucceeded']?.value){
|
if (scope['~saveSucceeded']?.value) {
|
||||||
if (!targets.length && !prop.silent) actionContext.addLog({value: '**On save**',});
|
if (!targets.length && !prop.silent) actionContext.addLog({ value: '**On save**', });
|
||||||
applyChildren();
|
applyChildren();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'random':
|
case 'random':
|
||||||
if (node.children.length){
|
if (node.children.length) {
|
||||||
let index = rollDice(1, node.children.length)[0] - 1;
|
let index = rollDice(1, node.children.length)[0] - 1;
|
||||||
applyNodeTriggers(node, 'after', actionContext);
|
applyNodeTriggers(node, 'after', actionContext);
|
||||||
applyProperty(node.children[index], actionContext);
|
applyProperty(node.children[index], actionContext);
|
||||||
|
|||||||
@@ -21,7 +21,10 @@ export default function applyBuff(node, actionContext) {
|
|||||||
const prop = node.node;
|
const prop = node.node;
|
||||||
let buffTargets = prop.target === 'self' ? [actionContext.creature] : actionContext.targets;
|
let buffTargets = prop.target === 'self' ? [actionContext.creature] : actionContext.targets;
|
||||||
|
|
||||||
// Then copy the decendants of the buff to the targets
|
// Mark the buff as dirty for recalculation
|
||||||
|
prop.dirty = true;
|
||||||
|
|
||||||
|
// Then copy the descendants of the buff to the targets
|
||||||
let propList = [prop];
|
let propList = [prop];
|
||||||
function addChildrenToPropList(children, { skipCrystalize } = {}) {
|
function addChildrenToPropList(children, { skipCrystalize } = {}) {
|
||||||
children.forEach(child => {
|
children.forEach(child => {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export default function applyDamage(node, actionContext) {
|
|||||||
// Choose target
|
// Choose target
|
||||||
let damageTargets = prop.target === 'self' ? [actionContext.creature] : actionContext.targets;
|
let damageTargets = prop.target === 'self' ? [actionContext.creature] : actionContext.targets;
|
||||||
// Determine if the hit is critical
|
// Determine if the hit is critical
|
||||||
let criticalHit = scope['$criticalHit']?.value &&
|
let criticalHit = scope['~criticalHit']?.value &&
|
||||||
prop.damageType !== 'healing' // Can't critically heal
|
prop.damageType !== 'healing' // Can't critically heal
|
||||||
;
|
;
|
||||||
// Double the damage rolls if the hit is critical
|
// Double the damage rolls if the hit is critical
|
||||||
@@ -73,12 +73,12 @@ export default function applyDamage(node, actionContext) {
|
|||||||
damage = Math.floor(damage);
|
damage = Math.floor(damage);
|
||||||
|
|
||||||
// Convert extra damage into the stored type
|
// Convert extra damage into the stored type
|
||||||
if (prop.damageType === 'extra' && scope['$lastDamageType']) {
|
if (prop.damageType === 'extra' && scope['~lastDamageType']?.value) {
|
||||||
prop.damageType = scope['$lastDamageType'];
|
prop.damageType = scope['~lastDamageType']?.value;
|
||||||
}
|
}
|
||||||
// Store current damage type
|
// Store current damage type
|
||||||
if (prop.damageType !== 'healing') {
|
if (prop.damageType !== 'healing') {
|
||||||
scope['$lastDamageType'] = prop.damageType;
|
scope['~lastDamageType'] = { value: prop.damageType };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Memoise the damage suffix for the log
|
// Memoise the damage suffix for the log
|
||||||
@@ -194,14 +194,18 @@ function dealDamage({ target, damageType, amount, actionContext }) {
|
|||||||
let healthBars = getPropertiesOfType(target._id, 'attribute');
|
let healthBars = getPropertiesOfType(target._id, 'attribute');
|
||||||
|
|
||||||
// Keep only the healthbars that can take damage/healing
|
// Keep only the healthbars that can take damage/healing
|
||||||
remove(healthBars, (bar) =>
|
healthBars = healthBars.filter((bar) => {
|
||||||
bar.attributeType !== 'healthBar' ||
|
if (bar.attributeType !== 'healthBar' || bar.inactive || bar.removed || bar.overridden) {
|
||||||
bar.inactive ||
|
return false;
|
||||||
bar.removed ||
|
}
|
||||||
bar.overridden ||
|
if (damageType === 'healing' && bar.healthBarNoHealing) {
|
||||||
(amount >= 0 && bar.healthBarNoDamage) ||
|
return false;
|
||||||
(amount < 0 && bar.healthBarNoHealing)
|
}
|
||||||
);
|
if (damageType !== 'healing' && amount >= 0 && bar.healthBarNoDamage) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
// Sort healthbars by damage/healing order or tree order as a fallback
|
// Sort healthbars by damage/healing order or tree order as a fallback
|
||||||
healthBars.sort((a, b) => {
|
healthBars.sort((a, b) => {
|
||||||
|
|||||||
@@ -4,22 +4,22 @@ import applyEffectsToCalculationParseNode from '/imports/api/engine/actions/appl
|
|||||||
import resolve, { toString } from '/imports/parser/resolve.js';
|
import resolve, { toString } from '/imports/parser/resolve.js';
|
||||||
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
|
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
|
||||||
|
|
||||||
export default function applyRoll(node, actionContext){
|
export default function applyRoll(node, actionContext) {
|
||||||
applyNodeTriggers(node, 'before', actionContext);
|
applyNodeTriggers(node, 'before', actionContext);
|
||||||
const prop = node.node;
|
const prop = node.node;
|
||||||
|
|
||||||
const applyChildren = function(){
|
const applyChildren = function () {
|
||||||
applyNodeTriggers(node, 'after', actionContext);
|
applyNodeTriggers(node, 'after', actionContext);
|
||||||
node.children.forEach(child => applyProperty(child, actionContext));
|
node.children.forEach(child => applyProperty(child, actionContext));
|
||||||
};
|
};
|
||||||
|
|
||||||
if (prop.roll?.calculation){
|
if (prop.roll?.calculation) {
|
||||||
const logValue = [];
|
const logValue = [];
|
||||||
|
|
||||||
// roll the dice only and store that string
|
// roll the dice only and store that string
|
||||||
applyEffectsToCalculationParseNode(prop.roll, actionContext);
|
applyEffectsToCalculationParseNode(prop.roll, actionContext);
|
||||||
const {result: rolled, context} = resolve('roll', prop.roll.parseNode, actionContext.scope);
|
const { result: rolled, context } = resolve('roll', prop.roll.parseNode, actionContext.scope);
|
||||||
if (rolled.parseType !== 'constant'){
|
if (rolled.parseType !== 'constant') {
|
||||||
logValue.push(toString(rolled));
|
logValue.push(toString(rolled));
|
||||||
}
|
}
|
||||||
logErrors(context.errors, actionContext);
|
logErrors(context.errors, actionContext);
|
||||||
@@ -28,28 +28,28 @@ export default function applyRoll(node, actionContext){
|
|||||||
context.errors = [];
|
context.errors = [];
|
||||||
|
|
||||||
// Resolve the roll to a final value
|
// Resolve the roll to a final value
|
||||||
const {result: reduced} = resolve('reduce', rolled, actionContext.scope, context);
|
const { result: reduced } = resolve('reduce', rolled, actionContext.scope, context);
|
||||||
logErrors(context.errors, actionContext);
|
logErrors(context.errors, actionContext);
|
||||||
|
|
||||||
// Store the result
|
// Store the result
|
||||||
if (reduced.parseType === 'constant'){
|
if (reduced.parseType === 'constant') {
|
||||||
prop.roll.value = reduced.value;
|
prop.roll.value = reduced.value;
|
||||||
} else if (reduced.parseType === 'error'){
|
} else if (reduced.parseType === 'error') {
|
||||||
prop.roll.value = null;
|
prop.roll.value = null;
|
||||||
} else {
|
} else {
|
||||||
prop.roll.value = toString(reduced);
|
prop.roll.value = toString(reduced);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we didn't end up with a constant of finite amount, give up
|
// If we didn't end up with a constant or a number of finite value, give up
|
||||||
if (reduced?.parseType !== 'constant' || !isFinite(reduced.value)){
|
if (reduced?.parseType !== 'constant' || (reduced.valueType === 'number' && !isFinite(reduced.value))) {
|
||||||
return applyChildren();
|
return applyChildren();
|
||||||
}
|
}
|
||||||
const value = reduced.value;
|
const value = reduced.value;
|
||||||
|
|
||||||
actionContext.scope[prop.variableName] = value;
|
actionContext.scope[prop.variableName] = { value };
|
||||||
logValue.push(`**${value}**`);
|
logValue.push(`**${value}**`);
|
||||||
|
|
||||||
if (!prop.silent){
|
if (!prop.silent) {
|
||||||
actionContext.addLog({
|
actionContext.addLog({
|
||||||
name: prop.name,
|
name: prop.name,
|
||||||
value: logValue.join('\n'),
|
value: logValue.join('\n'),
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { applyUnresolvedEffects } from '/imports/api/engine/actions/doCheck.js';
|
|||||||
export default function applySavingThrow(node, actionContext) {
|
export default function applySavingThrow(node, actionContext) {
|
||||||
applyNodeTriggers(node, 'before', actionContext);
|
applyNodeTriggers(node, 'before', actionContext);
|
||||||
const prop = node.node;
|
const prop = node.node;
|
||||||
|
const originalTargets = actionContext.targets;
|
||||||
|
|
||||||
let saveTargets = prop.target === 'self' ? [actionContext.creature] : actionContext.targets;
|
let saveTargets = prop.target === 'self' ? [actionContext.creature] : actionContext.targets;
|
||||||
|
|
||||||
@@ -31,22 +32,22 @@ export default function applySavingThrow(node, actionContext) {
|
|||||||
// If there are no save targets, apply all children as if the save both
|
// If there are no save targets, apply all children as if the save both
|
||||||
// succeeeded and failed
|
// succeeeded and failed
|
||||||
if (!saveTargets?.length) {
|
if (!saveTargets?.length) {
|
||||||
scope['$saveFailed'] = { value: true };
|
scope['~saveFailed'] = { value: true };
|
||||||
scope['$saveSucceeded'] = { value: true };
|
scope['~saveSucceeded'] = { value: true };
|
||||||
applyNodeTriggers(node, 'after', actionContext);
|
applyNodeTriggers(node, 'after', actionContext);
|
||||||
return node.children.forEach(child => applyProperty(child, actionContext));
|
return node.children.forEach(child => applyProperty(child, actionContext));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Each target makes the saving throw
|
// Each target makes the saving throw
|
||||||
saveTargets.forEach(target => {
|
saveTargets.forEach(target => {
|
||||||
delete scope['$saveFailed'];
|
delete scope['~saveFailed'];
|
||||||
delete scope['$saveSucceeded'];
|
delete scope['~saveSucceeded'];
|
||||||
delete scope['$saveDiceRoll'];
|
delete scope['~saveDiceRoll'];
|
||||||
delete scope['$saveRoll'];
|
delete scope['~saveRoll'];
|
||||||
|
|
||||||
const applyChildren = function () {
|
const applyChildren = function () {
|
||||||
applyNodeTriggers(node, 'after', actionContext);
|
|
||||||
actionContext.targets = [target]
|
actionContext.targets = [target]
|
||||||
|
applyNodeTriggers(node, 'after', actionContext);
|
||||||
node.children.forEach(child => applyProperty(child, actionContext));
|
node.children.forEach(child => applyProperty(child, actionContext));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -90,14 +91,14 @@ export default function applySavingThrow(node, actionContext) {
|
|||||||
value = values[0];
|
value = values[0];
|
||||||
resultPrefix = `1d20 [ ${value} ] ${rollModifierText}`
|
resultPrefix = `1d20 [ ${value} ] ${rollModifierText}`
|
||||||
}
|
}
|
||||||
scope['$saveDiceRoll'] = { value };
|
scope['~saveDiceRoll'] = { value };
|
||||||
const result = value + rollModifier || 0;
|
const result = value + rollModifier || 0;
|
||||||
scope['$saveRoll'] = { value: result };
|
scope['~saveRoll'] = { value: result };
|
||||||
const saveSuccess = result >= dc;
|
const saveSuccess = result >= dc;
|
||||||
if (saveSuccess) {
|
if (saveSuccess) {
|
||||||
scope['$saveSucceeded'] = { value: true };
|
scope['~saveSucceeded'] = { value: true };
|
||||||
} else {
|
} else {
|
||||||
scope['$saveFailed'] = { value: true };
|
scope['~saveFailed'] = { value: true };
|
||||||
}
|
}
|
||||||
if (!prop.silent) actionContext.addLog({
|
if (!prop.silent) actionContext.addLog({
|
||||||
name: saveSuccess ? 'Successful save' : 'Failed save',
|
name: saveSuccess ? 'Successful save' : 'Failed save',
|
||||||
@@ -106,4 +107,6 @@ export default function applySavingThrow(node, actionContext) {
|
|||||||
});
|
});
|
||||||
return applyChildren();
|
return applyChildren();
|
||||||
});
|
});
|
||||||
|
// reset the targets after the save to each child
|
||||||
|
actionContext.targets = originalTargets;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export function applyTrigger(trigger, prop, actionContext) {
|
|||||||
if (trigger.inactive) {
|
if (trigger.inactive) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prevent triggers from firing if their condition is false
|
// Prevent triggers from firing if their condition is false
|
||||||
if (trigger.condition?.parseNode) {
|
if (trigger.condition?.parseNode) {
|
||||||
recalculateCalculation(trigger.condition, actionContext);
|
recalculateCalculation(trigger.condition, actionContext);
|
||||||
@@ -61,11 +61,11 @@ export function applyTrigger(trigger, prop, actionContext) {
|
|||||||
value: trigger.description,
|
value: trigger.description,
|
||||||
inline: false,
|
inline: false,
|
||||||
}
|
}
|
||||||
if (trigger.description?.text){
|
if (trigger.description?.text) {
|
||||||
recalculateInlineCalculations(trigger.description, actionContext);
|
recalculateInlineCalculations(trigger.description, actionContext);
|
||||||
content.value = trigger.description.value;
|
content.value = trigger.description.value;
|
||||||
}
|
}
|
||||||
if(!trigger.silent) actionContext.addLog(content);
|
if (!trigger.silent) actionContext.addLog(content);
|
||||||
|
|
||||||
// Get all the trigger's properties and apply them
|
// Get all the trigger's properties and apply them
|
||||||
const properties = getPropertyDecendants(actionContext.creature._id, trigger._id);
|
const properties = getPropertyDecendants(actionContext.creature._id, trigger._id);
|
||||||
@@ -78,7 +78,7 @@ export function applyTrigger(trigger, prop, actionContext) {
|
|||||||
trigger.firing = false;
|
trigger.firing = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function triggerMatchTags(trigger, prop) {
|
export function triggerMatchTags(trigger, prop) {
|
||||||
let matched = false;
|
let matched = false;
|
||||||
const propTags = getEffectivePropTags(prop);
|
const propTags = getEffectivePropTags(prop);
|
||||||
// Check the target tags
|
// Check the target tags
|
||||||
@@ -89,23 +89,26 @@ function triggerMatchTags(trigger, prop) {
|
|||||||
matched = true;
|
matched = true;
|
||||||
}
|
}
|
||||||
// Check the extra tags
|
// Check the extra tags
|
||||||
trigger.extraTags?.forEach(extra => {
|
if (trigger.extraTags) {
|
||||||
if (extra.operation === 'OR') {
|
for (const extra of trigger.extraTags) {
|
||||||
if (matched) return;
|
if (extra.operation === 'OR') {
|
||||||
if (
|
if (matched) break;
|
||||||
!extra.tags.length ||
|
if (
|
||||||
difference(extra.tags, propTags).length === 0
|
!extra.tags.length ||
|
||||||
) {
|
difference(extra.tags, propTags).length === 0
|
||||||
matched = true;
|
) {
|
||||||
}
|
matched = true;
|
||||||
} else if (extra.operation === 'NOT') {
|
}
|
||||||
if (
|
} else if (extra.operation === 'NOT') {
|
||||||
extra.tags.length &&
|
if (
|
||||||
intersection(extra.tags, propTags)
|
extra.tags.length &&
|
||||||
) {
|
intersection(extra.tags, propTags).length > 0
|
||||||
return false;
|
) {
|
||||||
|
matched = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
return matched;
|
return matched;
|
||||||
}
|
}
|
||||||
|
|||||||
67
app/imports/api/engine/actions/applyTriggers.testFn.js
Normal file
67
app/imports/api/engine/actions/applyTriggers.testFn.js
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import { triggerMatchTags } from '/imports/api/engine/actions/applyTriggers.js';
|
||||||
|
import clean from '/imports/api/engine/computation/utility/cleanProp.testFn.js';
|
||||||
|
import { assert } from 'chai';
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
const prop = clean({
|
||||||
|
id: 'propWithTags',
|
||||||
|
type: 'action',
|
||||||
|
tags: ['yes1', 'notUsed', 'no1', 'yes2', 'no2', 'or1', 'or2'],
|
||||||
|
});
|
||||||
|
const positiveProp = clean({
|
||||||
|
id: 'propWithTags',
|
||||||
|
type: 'action',
|
||||||
|
tags: ['yes1', 'notUsed', 'yes2', 'or1', 'or2'],
|
||||||
|
});
|
||||||
|
assert.isTrue(
|
||||||
|
triggerMatchTags(clean({
|
||||||
|
type: 'trigger',
|
||||||
|
targetTags: ['yes1'],
|
||||||
|
}), prop),
|
||||||
|
'Trigger matches on a single target tag'
|
||||||
|
);
|
||||||
|
assert.isTrue(
|
||||||
|
triggerMatchTags(clean({
|
||||||
|
type: 'trigger',
|
||||||
|
targetTags: ['yes1', 'yes2'],
|
||||||
|
}), prop),
|
||||||
|
'Trigger matches on a multiple target tags'
|
||||||
|
);
|
||||||
|
assert.isFalse(
|
||||||
|
triggerMatchTags(clean({
|
||||||
|
type: 'trigger',
|
||||||
|
targetTags: ['yes1'],
|
||||||
|
extraTags: [{ operation: 'NOT', tags: ['no1'] }]
|
||||||
|
}), prop),
|
||||||
|
'Trigger correctly fails to match when not tags are present'
|
||||||
|
);
|
||||||
|
assert.isFalse(
|
||||||
|
triggerMatchTags(clean({
|
||||||
|
type: 'trigger',
|
||||||
|
extraTags: [{ operation: 'NOT', tags: ['no1'] }]
|
||||||
|
}), prop),
|
||||||
|
'Trigger correctly fails to match when only not tags are present'
|
||||||
|
);
|
||||||
|
assert.isTrue(
|
||||||
|
triggerMatchTags(clean({
|
||||||
|
type: 'trigger',
|
||||||
|
extraTags: [{ operation: 'NOT', tags: ['no1'] }]
|
||||||
|
}), positiveProp),
|
||||||
|
'Trigger matches when only not tags are present'
|
||||||
|
);
|
||||||
|
assert.isTrue(
|
||||||
|
triggerMatchTags(clean({
|
||||||
|
type: 'trigger',
|
||||||
|
extraTags: [{ operation: 'OR', tags: ['or1'] }]
|
||||||
|
}), positiveProp),
|
||||||
|
'Trigger matches when OR tags are present'
|
||||||
|
);
|
||||||
|
assert.isTrue(
|
||||||
|
triggerMatchTags(clean({
|
||||||
|
type: 'trigger',
|
||||||
|
targetTags: ['missing1'],
|
||||||
|
extraTags: [{ operation: 'OR', tags: ['or1'] }]
|
||||||
|
}), positiveProp),
|
||||||
|
'Trigger matches when only OR tags are present'
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,16 +1,17 @@
|
|||||||
import '/imports/api/simpleSchemaConfig.js';
|
import '/imports/api/simpleSchemaConfig.js';
|
||||||
//import testTypes from './testTypes/index.js';
|
//import testTypes from './testTypes/index.js';
|
||||||
|
import applyTriggers from '/imports/api/engine/actions/applyTriggers.testFn.js';
|
||||||
import { doActionWork } from './doAction.js';
|
import { doActionWork } from './doAction.js';
|
||||||
import { CreatureLogSchema } from '/imports/api/creature/log/CreatureLogs.js';
|
import { CreatureLogSchema } from '/imports/api/creature/log/CreatureLogs.js';
|
||||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||||
|
|
||||||
function cleanProp(prop){
|
function cleanProp(prop) {
|
||||||
let schema = CreatureProperties.simpleSchema(prop);
|
let schema = CreatureProperties.simpleSchema(prop);
|
||||||
return schema.clean(prop);
|
return schema.clean(prop);
|
||||||
}
|
}
|
||||||
|
|
||||||
function cleanCreature(creature){
|
function cleanCreature(creature) {
|
||||||
let schema = Creatures.simpleSchema(creature);
|
let schema = Creatures.simpleSchema(creature);
|
||||||
return schema.clean(creature);
|
return schema.clean(creature);
|
||||||
}
|
}
|
||||||
@@ -28,7 +29,7 @@ const testActionContext = {
|
|||||||
}),
|
}),
|
||||||
scope: {},
|
scope: {},
|
||||||
addLog(content) {
|
addLog(content) {
|
||||||
if (content.name || content.value){
|
if (content.name || content.value) {
|
||||||
this.log.content.push(content);
|
this.log.content.push(content);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -40,8 +41,8 @@ const action = cleanProp({
|
|||||||
});
|
});
|
||||||
const actionAncestors = [];
|
const actionAncestors = [];
|
||||||
|
|
||||||
describe('Do Action', function(){
|
describe('Do Action', function () {
|
||||||
it('Does an empty action', function(){
|
it('Does an empty action', function () {
|
||||||
doActionWork({
|
doActionWork({
|
||||||
properties: [action],
|
properties: [action],
|
||||||
ancestors: actionAncestors,
|
ancestors: actionAncestors,
|
||||||
@@ -51,3 +52,7 @@ describe('Do Action', function(){
|
|||||||
});
|
});
|
||||||
//testTypes.forEach(test => it(test.text, test.fn));
|
//testTypes.forEach(test => it(test.text, test.fn));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Action utility functions', function () {
|
||||||
|
it('Triggers match tags', applyTriggers);
|
||||||
|
})
|
||||||
|
|||||||
@@ -117,7 +117,8 @@ const doAction = new ValidatedMethod({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
actionContext.scope['slotLevel'] = slotLevel;
|
actionContext.scope['slotLevel'] = { value: slotLevel };
|
||||||
|
actionContext.scope['~slotLevel'] = { value: slotLevel };
|
||||||
|
|
||||||
// Do the action
|
// Do the action
|
||||||
doActionWork({
|
doActionWork({
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ function rollCheck(prop, actionContext) {
|
|||||||
rollModifier += effectBonus;
|
rollModifier += effectBonus;
|
||||||
|
|
||||||
let value, values, resultPrefix;
|
let value, values, resultPrefix;
|
||||||
if (scope['$checkAdvantage'] === 1) {
|
if (scope['~checkAdvantage']?.value === 1) {
|
||||||
logName += ' (Advantage)';
|
logName += ' (Advantage)';
|
||||||
const [a, b] = rollDice(2, 20);
|
const [a, b] = rollDice(2, 20);
|
||||||
if (a >= b) {
|
if (a >= b) {
|
||||||
@@ -91,7 +91,7 @@ function rollCheck(prop, actionContext) {
|
|||||||
value = b;
|
value = b;
|
||||||
resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText} = `;
|
resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText} = `;
|
||||||
}
|
}
|
||||||
} else if (scope['$checkAdvantage'] === -1) {
|
} else if (scope['~checkAdvantage']?.value === -1) {
|
||||||
logName += ' (Disadvantage)';
|
logName += ' (Disadvantage)';
|
||||||
const [a, b] = rollDice(2, 20);
|
const [a, b] = rollDice(2, 20);
|
||||||
if (a <= b) {
|
if (a <= b) {
|
||||||
@@ -107,9 +107,9 @@ function rollCheck(prop, actionContext) {
|
|||||||
resultPrefix = `1d20 [ ${value} ] ${rollModifierText} = `
|
resultPrefix = `1d20 [ ${value} ] ${rollModifierText} = `
|
||||||
}
|
}
|
||||||
const result = (value + rollModifier) || 0;
|
const result = (value + rollModifier) || 0;
|
||||||
scope['$checkDiceRoll'] = value;
|
scope['~checkDiceRoll'] = { value };
|
||||||
scope['$checkRoll'] = result;
|
scope['~checkRoll'] = { value: result };
|
||||||
scope['$checkModifier'] = rollModifier;
|
scope['~checkModifier'] = { value: rollModifier };
|
||||||
actionContext.addLog({
|
actionContext.addLog({
|
||||||
name: logName,
|
name: logName,
|
||||||
value: `${resultPrefix} **${result}**`,
|
value: `${resultPrefix} **${result}**`,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* Only computes `totalFilled`, need to compute `quantityExpected.value`
|
* Only computes `totalFilled`, need to compute `quantityExpected.value`
|
||||||
* before `spacesLeft` can be computed
|
* before `spacesLeft` can be computed
|
||||||
*/
|
*/
|
||||||
export default function computeSlotQuantityFilled(node, dependencyGraph){
|
export default function computeSlotQuantityFilled(node, dependencyGraph) {
|
||||||
let slot = node.node;
|
let slot = node.node;
|
||||||
if (slot.type !== 'propertySlot') return;
|
if (slot.type !== 'propertySlot') return;
|
||||||
slot.totalFilled = 0;
|
slot.totalFilled = 0;
|
||||||
@@ -10,9 +10,8 @@ export default function computeSlotQuantityFilled(node, dependencyGraph){
|
|||||||
let childProp = child.node;
|
let childProp = child.node;
|
||||||
dependencyGraph.addLink(slot._id, childProp._id, 'slotFill');
|
dependencyGraph.addLink(slot._id, childProp._id, 'slotFill');
|
||||||
if (
|
if (
|
||||||
childProp.type === 'slotFiller' &&
|
|
||||||
Number.isFinite(childProp.slotQuantityFilled)
|
Number.isFinite(childProp.slotQuantityFilled)
|
||||||
){
|
) {
|
||||||
slot.totalFilled += childProp.slotQuantityFilled;
|
slot.totalFilled += childProp.slotQuantityFilled;
|
||||||
} else {
|
} else {
|
||||||
slot.totalFilled++;
|
slot.totalFilled++;
|
||||||
|
|||||||
@@ -1,16 +1,31 @@
|
|||||||
import walkDown from '/imports/api/engine/computation/utility/walkdown.js';
|
import walkDown from '/imports/api/engine/computation/utility/walkdown.js';
|
||||||
|
import { getEffectTagTargets } from '/imports/api/engine/computation/buildComputation/linkTypeDependencies.js';
|
||||||
|
|
||||||
export default function computeToggleDependencies(node, dependencyGraph){
|
export default function computeToggleDependencies(node, dependencyGraph, computation, forest) {
|
||||||
const prop = node.node;
|
const prop = node.node;
|
||||||
// Only for toggles that aren't inactive and aren't set to enabled or disabled
|
// Only for toggles
|
||||||
if (
|
if (prop.type !== 'toggle') return;
|
||||||
prop.inactive ||
|
|
||||||
prop.type !== 'toggle' ||
|
if (prop.targetByTags) {
|
||||||
prop.disabled ||
|
// Find all the props targeted by tags, and disable them and their children
|
||||||
prop.enabled
|
getEffectTagTargets(prop, computation).forEach(targetId => {
|
||||||
) return;
|
const target = forest.nodeIndex[targetId];
|
||||||
|
if (!target) return;
|
||||||
|
target.node._computationDetails.toggleAncestors.push(prop);
|
||||||
|
dependencyGraph.addLink(target.node._id, prop._id, 'toggle');
|
||||||
|
walkDown(target.children, child => {
|
||||||
|
// The child nodes depend on the toggle
|
||||||
|
child.node._computationDetails.toggleAncestors.push(prop);
|
||||||
|
dependencyGraph.addLink(child.node._id, prop._id, 'toggle');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't need to link direct children of static toggles, it's already done
|
||||||
|
if (prop.disabled || prop.enabled) return;
|
||||||
|
|
||||||
walkDown(node.children, child => {
|
walkDown(node.children, child => {
|
||||||
// The child nodes depend on the toggle condition compuation
|
// The child nodes depend on the toggle
|
||||||
child.node._computationDetails.toggleAncestors.push(prop);
|
child.node._computationDetails.toggleAncestors.push(prop);
|
||||||
dependencyGraph.addLink(child.node._id, prop._id, 'toggle');
|
dependencyGraph.addLink(child.node._id, prop._id, 'toggle');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ function linkEffects(dependencyGraph, prop, computation) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Returns an array of IDs of the properties the effect targets
|
// Returns an array of IDs of the properties the effect targets
|
||||||
function getEffectTagTargets(effect, computation) {
|
export function getEffectTagTargets(effect, computation) {
|
||||||
let targets = getTargetListFromTags(effect.targetTags, computation);
|
let targets = getTargetListFromTags(effect.targetTags, computation);
|
||||||
let notIds = [];
|
let notIds = [];
|
||||||
if (effect.extraTags) {
|
if (effect.extraTags) {
|
||||||
@@ -218,7 +218,6 @@ function getDefaultCalculationField(prop) {
|
|||||||
case 'roll': return 'roll';
|
case 'roll': return 'roll';
|
||||||
case 'savingThrow': return 'dc';
|
case 'savingThrow': return 'dc';
|
||||||
case 'skill': return 'baseValue';
|
case 'skill': return 'baseValue';
|
||||||
case 'slotFiller': return null;
|
|
||||||
case 'slot': return 'quantityExpected';
|
case 'slot': return 'quantityExpected';
|
||||||
case 'spellList': return 'attackRollBonus';
|
case 'spellList': return 'attackRollBonus';
|
||||||
case 'spell': return null;
|
case 'spell': return null;
|
||||||
@@ -268,20 +267,45 @@ function linkPointBuy(dependencyGraph, prop) {
|
|||||||
if (prop.inactive) return;
|
if (prop.inactive) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
function linkProficiencies(dependencyGraph, prop) {
|
function linkProficiencies(dependencyGraph, prop, computation) {
|
||||||
// The stats depend on the proficiency
|
// The stats depend on the proficiency
|
||||||
if (prop.inactive) return;
|
if (prop.inactive) return;
|
||||||
prop.stats.forEach(statName => {
|
if (prop.targetByTags) {
|
||||||
if (!statName) return;
|
// Tag targeted proficiencies depend on the creature's proficiencyBonus,
|
||||||
dependencyGraph.addLink(statName, prop._id, prop.type);
|
// since they add it directly to the targeted field
|
||||||
});
|
dependencyGraph.addLink(prop._id, 'proficiencyBonus', 'skillProficiencyBonus');
|
||||||
|
getEffectTagTargets(prop, computation).forEach(targetId => {
|
||||||
|
const targetProp = computation.propsById[targetId];
|
||||||
|
if (
|
||||||
|
(targetProp.type === 'attribute' || targetProp.type === 'skill')
|
||||||
|
&& targetProp.variableName
|
||||||
|
&& !prop.targetField
|
||||||
|
) {
|
||||||
|
// If the field wasn't specified and we're targeting an attribute or
|
||||||
|
// skill, just treat it like a normal proficiency on its variable name
|
||||||
|
dependencyGraph.addLink(targetProp.variableName, prop._id, 'proficiency');
|
||||||
|
} else {
|
||||||
|
// Otherwise target a field on that property
|
||||||
|
const key = prop.targetField || getDefaultCalculationField(targetProp);
|
||||||
|
const calcObj = get(targetProp, key);
|
||||||
|
if (calcObj && calcObj.calculation) {
|
||||||
|
dependencyGraph.addLink(`${targetProp._id}.${key}`, prop._id, 'proficiency');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
prop.stats.forEach(statName => {
|
||||||
|
if (!statName) return;
|
||||||
|
dependencyGraph.addLink(statName, prop._id, 'proficiency');
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function linkSavingThrow(dependencyGraph, prop) {
|
function linkSavingThrow(dependencyGraph, prop) {
|
||||||
dependOnCalc({ dependencyGraph, prop, key: 'dc' });
|
dependOnCalc({ dependencyGraph, prop, key: 'dc' });
|
||||||
}
|
}
|
||||||
|
|
||||||
function linkSkill(dependencyGraph, prop) {
|
function linkSkill(dependencyGraph, prop, computation) {
|
||||||
// Depends on base value
|
// Depends on base value
|
||||||
dependOnCalc({ dependencyGraph, prop, key: 'baseValue' });
|
dependOnCalc({ dependencyGraph, prop, key: 'baseValue' });
|
||||||
// Link dependents
|
// Link dependents
|
||||||
@@ -293,6 +317,20 @@ function linkSkill(dependencyGraph, prop) {
|
|||||||
}
|
}
|
||||||
// Skills depend on the creature's proficiencyBonus
|
// Skills depend on the creature's proficiencyBonus
|
||||||
dependencyGraph.addLink(prop._id, 'proficiencyBonus', 'skillProficiencyBonus');
|
dependencyGraph.addLink(prop._id, 'proficiencyBonus', 'skillProficiencyBonus');
|
||||||
|
|
||||||
|
// Skills can apply their value as a proficiency bonus to calculations based on tag
|
||||||
|
if (prop.targetByTags) {
|
||||||
|
getEffectTagTargets(prop, computation).forEach(targetId => {
|
||||||
|
const targetProp = computation.propsById[targetId];
|
||||||
|
// Always target a field on the target property, applying a skill to an attribute or
|
||||||
|
// other skill isn't supported
|
||||||
|
const key = prop.targetField || getDefaultCalculationField(targetProp);
|
||||||
|
const calcObj = get(targetProp, key);
|
||||||
|
if (calcObj && calcObj.calculation) {
|
||||||
|
dependencyGraph.addLink(`${targetProp._id}.${key}`, prop._id, 'proficiency');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function linkSlot(dependencyGraph, prop) {
|
function linkSlot(dependencyGraph, prop) {
|
||||||
|
|||||||
@@ -5,14 +5,14 @@ import { get, unset } from 'lodash';
|
|||||||
import errorNode from '/imports/parser/parseTree/error.js';
|
import errorNode from '/imports/parser/parseTree/error.js';
|
||||||
import cyrb53 from '/imports/api/engine/computation/utility/cyrb53.js';
|
import cyrb53 from '/imports/api/engine/computation/utility/cyrb53.js';
|
||||||
|
|
||||||
export default function parseCalculationFields(prop, schemas){
|
export default function parseCalculationFields(prop, schemas) {
|
||||||
discoverInlineCalculationFields(prop, schemas);
|
discoverInlineCalculationFields(prop, schemas);
|
||||||
parseAllCalculationFields(prop, schemas);
|
parseAllCalculationFields(prop, schemas);
|
||||||
}
|
}
|
||||||
|
|
||||||
function discoverInlineCalculationFields(prop, schemas){
|
function discoverInlineCalculationFields(prop, schemas) {
|
||||||
// For each key in the schema
|
// For each key in the schema
|
||||||
schemas[prop.type].inlineCalculationFields().forEach( calcKey => {
|
schemas[prop.type]?.inlineCalculationFields?.()?.forEach(calcKey => {
|
||||||
// That ends in .inlineCalculations
|
// That ends in .inlineCalculations
|
||||||
applyFnToKey(prop, calcKey, (prop, key) => {
|
applyFnToKey(prop, calcKey, (prop, key) => {
|
||||||
const inlineCalcObj = get(prop, key);
|
const inlineCalcObj = get(prop, key);
|
||||||
@@ -22,7 +22,7 @@ function discoverInlineCalculationFields(prop, schemas){
|
|||||||
// Extract the calculations and store them on the property
|
// Extract the calculations and store them on the property
|
||||||
let string = inlineCalcObj.text;
|
let string = inlineCalcObj.text;
|
||||||
// If there is no text, delete the whole field
|
// If there is no text, delete the whole field
|
||||||
if (!string){
|
if (!string) {
|
||||||
unset(prop, calcKey);
|
unset(prop, calcKey);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -32,7 +32,7 @@ function discoverInlineCalculationFields(prop, schemas){
|
|||||||
|
|
||||||
// Has the text, if it matches the existing hash, stop
|
// Has the text, if it matches the existing hash, stop
|
||||||
const inlineCalcHash = cyrb53(inlineCalcObj.text);
|
const inlineCalcHash = cyrb53(inlineCalcObj.text);
|
||||||
if (inlineCalcHash === inlineCalcObj.hash){
|
if (inlineCalcHash === inlineCalcObj.hash) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
inlineCalcObj.hash = inlineCalcHash;
|
inlineCalcObj.hash = inlineCalcHash;
|
||||||
@@ -41,7 +41,7 @@ function discoverInlineCalculationFields(prop, schemas){
|
|||||||
// It will be re set including the embedded calculation at the end of
|
// It will be re set including the embedded calculation at the end of
|
||||||
// the computation
|
// the computation
|
||||||
let matches = string.matchAll(INLINE_CALCULATION_REGEX);
|
let matches = string.matchAll(INLINE_CALCULATION_REGEX);
|
||||||
for (let match of matches){
|
for (let match of matches) {
|
||||||
let calculation = match[1];
|
let calculation = match[1];
|
||||||
inlineCalcObj.inlineCalculations.push({
|
inlineCalcObj.inlineCalculations.push({
|
||||||
calculation,
|
calculation,
|
||||||
@@ -51,9 +51,9 @@ function discoverInlineCalculationFields(prop, schemas){
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseAllCalculationFields(prop, schemas){
|
function parseAllCalculationFields(prop, schemas) {
|
||||||
// For each computed key in the schema
|
// For each computed key in the schema
|
||||||
schemas[prop.type].computedFields().forEach( calcKey => {
|
schemas[prop.type]?.computedFields?.()?.forEach(calcKey => {
|
||||||
// Determine the level the calculation should compute down to
|
// Determine the level the calculation should compute down to
|
||||||
let parseLevel = schemas[prop.type].getDefinition(calcKey).parseLevel || 'reduce';
|
let parseLevel = schemas[prop.type].getDefinition(calcKey).parseLevel || 'reduce';
|
||||||
|
|
||||||
@@ -66,7 +66,7 @@ function parseAllCalculationFields(prop, schemas){
|
|||||||
const calcObj = get(prop, key);
|
const calcObj = get(prop, key);
|
||||||
if (!calcObj) return;
|
if (!calcObj) return;
|
||||||
// Delete the whole calculation object if the calculation string isn't set
|
// Delete the whole calculation object if the calculation string isn't set
|
||||||
if (!calcObj.calculation){
|
if (!calcObj.calculation) {
|
||||||
unset(prop, calcKey);
|
unset(prop, calcKey);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -84,10 +84,10 @@ function parseAllCalculationFields(prop, schemas){
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseCalculation(calcObj){
|
function parseCalculation(calcObj) {
|
||||||
const calcHash = cyrb53(calcObj.calculation);
|
const calcHash = cyrb53(calcObj.calculation);
|
||||||
// If the cached parse calculation is equal to the calculation, skip
|
// If the cached parse calculation is equal to the calculation, skip
|
||||||
if (calcHash === calcObj.hash){
|
if (calcHash === calcObj.hash) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
calcObj.hash = calcHash;
|
calcObj.hash = calcHash;
|
||||||
@@ -100,6 +100,6 @@ function parseCalculation(calcObj){
|
|||||||
message: prettifyParseError(e),
|
message: prettifyParseError(e),
|
||||||
};
|
};
|
||||||
calcObj.parseError = error;
|
calcObj.parseError = error;
|
||||||
calcObj.parseNode = errorNode.create({error});
|
calcObj.parseNode = errorNode.create({ error });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import applyFnToKey from '../utility/applyFnToKey.js';
|
import applyFnToKey from '../utility/applyFnToKey.js';
|
||||||
import { unset } from 'lodash';
|
import { unset } from 'lodash';
|
||||||
|
|
||||||
export default function removeSchemaFields(schemas, prop){
|
export default function removeSchemaFields(schemas, prop) {
|
||||||
schemas.forEach(schema => {
|
schemas.forEach(schema => {
|
||||||
schema.removeBeforeComputeFields().forEach(
|
schema?.removeBeforeComputeFields?.().forEach(
|
||||||
key => applyFnToKey(prop, key, unset)
|
key => applyFnToKey(prop, key, unset)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { buildComputationFromProps } from '/imports/api/engine/computation/build
|
|||||||
import { assert } from 'chai';
|
import { assert } from 'chai';
|
||||||
import clean from '../../utility/cleanProp.testFn.js';
|
import clean from '../../utility/cleanProp.testFn.js';
|
||||||
|
|
||||||
export default function(){
|
export default function () {
|
||||||
const computation = buildComputationFromProps(testProperties);
|
const computation = buildComputationFromProps(testProperties);
|
||||||
const totalFilled = computation.propsById['slotId'].totalFilled;
|
const totalFilled = computation.propsById['slotId'].totalFilled;
|
||||||
assert.equal(totalFilled, 4);
|
assert.equal(totalFilled, 4);
|
||||||
@@ -13,24 +13,24 @@ var testProperties = [
|
|||||||
clean({
|
clean({
|
||||||
_id: 'slotId',
|
_id: 'slotId',
|
||||||
type: 'propertySlot',
|
type: 'propertySlot',
|
||||||
ancestors: [{id: 'charId'}],
|
ancestors: [{ id: 'charId' }],
|
||||||
}),
|
}),
|
||||||
// Children
|
// Children
|
||||||
clean({
|
clean({
|
||||||
_id: 'slotFillerId',
|
_id: 'slotFillerId',
|
||||||
type: 'slotFiller',
|
type: 'folder',
|
||||||
slotQuantityFilled: 3,
|
slotQuantityFilled: 3,
|
||||||
slotFillerType: 'item',
|
slotFillerType: 'item',
|
||||||
ancestors: [{id: 'charId'}, {id: 'slotId'}],
|
ancestors: [{ id: 'charId' }, { id: 'slotId' }],
|
||||||
}),
|
}),
|
||||||
clean({
|
clean({
|
||||||
_id: 'slotChildId',
|
_id: 'slotChildId',
|
||||||
type: 'item',
|
type: 'item',
|
||||||
ancestors: [{id: 'charId'}, {id: 'slotId'}],
|
ancestors: [{ id: 'charId' }, { id: 'slotId' }],
|
||||||
}),
|
}),
|
||||||
clean({
|
clean({
|
||||||
_id: 'slotGrandchildId',
|
_id: 'slotGrandchildId',
|
||||||
type: 'effect',
|
type: 'effect',
|
||||||
ancestors: [{id: 'charId'}, {id: 'slotId'}, {id: 'slotChildId'}],
|
ancestors: [{ id: 'charId' }, { id: 'slotId' }, { id: 'slotChildId' }],
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import removeSchemaFields from './buildComputation/removeSchemaFields.js';
|
|||||||
* computed toggles
|
* computed toggles
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export default function buildCreatureComputation(creatureId){
|
export default function buildCreatureComputation(creatureId) {
|
||||||
const creature = getCreature(creatureId);
|
const creature = getCreature(creatureId);
|
||||||
const variables = getVariables(creatureId);
|
const variables = getVariables(creatureId);
|
||||||
const properties = getProperties(creatureId);
|
const properties = getProperties(creatureId);
|
||||||
@@ -37,7 +37,7 @@ export default function buildCreatureComputation(creatureId){
|
|||||||
return computation;
|
return computation;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildComputationFromProps(properties, creature, variables){
|
export function buildComputationFromProps(properties, creature, variables) {
|
||||||
|
|
||||||
const computation = new CreatureComputation(properties, creature, variables);
|
const computation = new CreatureComputation(properties, creature, variables);
|
||||||
// Dependency graph where edge(a, b) means a depends on b
|
// Dependency graph where edge(a, b) means a depends on b
|
||||||
@@ -49,14 +49,14 @@ export function buildComputationFromProps(properties, creature, variables){
|
|||||||
const dependencyGraph = computation.dependencyGraph;
|
const dependencyGraph = computation.dependencyGraph;
|
||||||
|
|
||||||
// Link the denormalizedStats from the creature
|
// Link the denormalizedStats from the creature
|
||||||
if (creature && creature.denormalizedStats){
|
if (creature && creature.denormalizedStats) {
|
||||||
if (creature.denormalizedStats.xp){
|
if (creature.denormalizedStats.xp) {
|
||||||
dependencyGraph.addNode('xp', {
|
dependencyGraph.addNode('xp', {
|
||||||
baseValue: creature.denormalizedStats.xp,
|
baseValue: creature.denormalizedStats.xp,
|
||||||
type: '_variable'
|
type: '_variable'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (creature.denormalizedStats.milestoneLevels){
|
if (creature.denormalizedStats.milestoneLevels) {
|
||||||
dependencyGraph.addNode('milestoneLevels', {
|
dependencyGraph.addNode('milestoneLevels', {
|
||||||
baseValue: creature.denormalizedStats.milestoneLevels,
|
baseValue: creature.denormalizedStats.milestoneLevels,
|
||||||
type: '_variable'
|
type: '_variable'
|
||||||
@@ -93,7 +93,7 @@ export function buildComputationFromProps(properties, creature, variables){
|
|||||||
// Inactive status must be complete for the whole tree before toggle deps
|
// Inactive status must be complete for the whole tree before toggle deps
|
||||||
// are calculated
|
// are calculated
|
||||||
walkDown(forest, node => {
|
walkDown(forest, node => {
|
||||||
computeToggleDependencies(node, dependencyGraph);
|
computeToggleDependencies(node, dependencyGraph, computation, forest);
|
||||||
computeSlotQuantityFilled(node, dependencyGraph);
|
computeSlotQuantityFilled(node, dependencyGraph);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import pointBuy from './computeByType/computePointBuy.js';
|
|||||||
import propertySlot from './computeByType/computeSlot.js';
|
import propertySlot from './computeByType/computeSlot.js';
|
||||||
import container from './computeByType/computeContainer.js';
|
import container from './computeByType/computeContainer.js';
|
||||||
import spellList from './computeByType/computeSpellList.js';
|
import spellList from './computeByType/computeSpellList.js';
|
||||||
|
import toggle from './computeByType/computeToggle.js';
|
||||||
import _calculation from './computeByType/computeCalculation.js';
|
import _calculation from './computeByType/computeCalculation.js';
|
||||||
|
|
||||||
export default Object.freeze({
|
export default Object.freeze({
|
||||||
@@ -19,4 +20,5 @@ export default Object.freeze({
|
|||||||
propertySlot,
|
propertySlot,
|
||||||
spell: action,
|
spell: action,
|
||||||
spellList,
|
spellList,
|
||||||
|
toggle,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
import evaluateCalculation from '../../utility/evaluateCalculation.js';
|
import evaluateCalculation from '../../utility/evaluateCalculation.js';
|
||||||
|
import { getPropertyName } from '/imports/constants/PROPERTIES.js';
|
||||||
|
|
||||||
export default function computeCalculation(computation, node){
|
export default function computeCalculation(computation, node) {
|
||||||
const calcObj = node.data;
|
const calcObj = node.data;
|
||||||
evaluateCalculation(calcObj, computation.scope);
|
evaluateCalculation(calcObj, computation.scope);
|
||||||
|
if (calcObj.effects || calcObj.proficiencies) {
|
||||||
|
calcObj.baseValue = calcObj.value;
|
||||||
|
}
|
||||||
aggregateCalculationEffects(node, computation);
|
aggregateCalculationEffects(node, computation);
|
||||||
|
aggregateCalculationProficiencies(node, computation);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function aggregateCalculationEffects(node, computation){
|
function aggregateCalculationEffects(node, computation) {
|
||||||
const calcObj = node.data;
|
const calcObj = node.data;
|
||||||
delete calcObj.effects;
|
delete calcObj.effects;
|
||||||
computation.dependencyGraph.forEachLinkedNode(
|
computation.dependencyGraph.forEachLinkedNode(
|
||||||
@@ -34,15 +39,77 @@ export function aggregateCalculationEffects(node, computation){
|
|||||||
},
|
},
|
||||||
true // enumerate only outbound links
|
true // enumerate only outbound links
|
||||||
);
|
);
|
||||||
if (calcObj.effects && typeof calcObj.value === 'number'){
|
if (calcObj.effects && typeof calcObj.value === 'number') {
|
||||||
calcObj.baseValue = calcObj.value;
|
|
||||||
calcObj.effects.forEach(effect => {
|
calcObj.effects.forEach(effect => {
|
||||||
if (
|
if (
|
||||||
effect.operation === 'add' &&
|
effect.operation === 'add' &&
|
||||||
effect.amount && typeof effect.amount.value === 'number'
|
effect.amount && typeof effect.amount.value === 'number'
|
||||||
){
|
) {
|
||||||
calcObj.value += effect.amount.value
|
calcObj.value += effect.amount.value
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function aggregateCalculationProficiencies(node, computation) {
|
||||||
|
const calcObj = node.data;
|
||||||
|
delete calcObj.proficiencies;
|
||||||
|
delete calcObj.proficiency;
|
||||||
|
let profBonus = computation.scope['proficiencyBonus']?.value || 0;
|
||||||
|
|
||||||
|
// Go through all the links and collect them on the calculation
|
||||||
|
computation.dependencyGraph.forEachLinkedNode(
|
||||||
|
node.id,
|
||||||
|
(linkedNode, link) => {
|
||||||
|
// Only proficiency links
|
||||||
|
if (link.data !== 'proficiency') return;
|
||||||
|
// That have data
|
||||||
|
if (!linkedNode.data) return;
|
||||||
|
// Ignoring inactive props
|
||||||
|
if (linkedNode.data.inactive) return;
|
||||||
|
// Compute the proficiency and value
|
||||||
|
let proficiency, value;
|
||||||
|
if (linkedNode.data.type === 'proficiency') {
|
||||||
|
proficiency = linkedNode.data.value || 0;
|
||||||
|
// Multiply the proficiency bonus by the actual proficiency
|
||||||
|
if (proficiency === 0.49) {
|
||||||
|
// Round down proficiency bonus in the special case
|
||||||
|
value = Math.floor(profBonus * 0.5);
|
||||||
|
} else {
|
||||||
|
value = Math.ceil(profBonus * proficiency);
|
||||||
|
}
|
||||||
|
} else if (linkedNode.data.type === 'skill') {
|
||||||
|
value = linkedNode.data.value || 0;
|
||||||
|
proficiency = linkedNode.data.proficiency || 0;
|
||||||
|
}
|
||||||
|
// Collate proficiencies
|
||||||
|
calcObj.proficiencies = calcObj.proficiencies || [];
|
||||||
|
calcObj.proficiencies.push({
|
||||||
|
_id: linkedNode.data._id,
|
||||||
|
name: linkedNode.data.name,
|
||||||
|
type: linkedNode.data.type,
|
||||||
|
proficiency,
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
true // enumerate only outbound links
|
||||||
|
);
|
||||||
|
|
||||||
|
// Apply the highest proficiency, marking all others as overridden
|
||||||
|
if (calcObj.proficiencies && typeof calcObj.value === 'number') {
|
||||||
|
calcObj.proficiency = 0;
|
||||||
|
calcObj.proficiencyBonus = 0;
|
||||||
|
let currentProf;
|
||||||
|
calcObj.proficiencies.forEach(prof => {
|
||||||
|
if (prof.value > calcObj.proficiencyBonus) {
|
||||||
|
if (currentProf) currentProf.overridden = true;
|
||||||
|
calcObj.proficiencyBonus = prof.value;
|
||||||
|
calcObj.proficiency = prof.proficiency;
|
||||||
|
currentProf = prof;
|
||||||
|
} else {
|
||||||
|
prof.overridden = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
calcObj.value += calcObj.proficiencyBonus;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export default function computSlot(computation, node){
|
export default function computeSlot(computation, node) {
|
||||||
const prop = node.data;
|
const prop = node.data;
|
||||||
if (prop.quantityExpected && prop.quantityExpected.value){
|
if (prop.quantityExpected && prop.quantityExpected.value) {
|
||||||
prop.spaceLeft = prop.quantityExpected.value - prop.totalFilled;
|
prop.spaceLeft = prop.quantityExpected.value - prop.totalFilled;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export default function computeToggle(computation, node) {
|
||||||
|
const prop = node.data;
|
||||||
|
if (!prop.enabled && !prop.disabled && prop.condition && !prop.condition.value) {
|
||||||
|
prop.inactive = true;
|
||||||
|
prop.deactivatedBySelf = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +1,40 @@
|
|||||||
import getAggregatorResult from './getAggregatorResult.js';
|
import getAggregatorResult from './getAggregatorResult.js';
|
||||||
|
|
||||||
export default function computeVariableAsAttribute(computation, node, prop){
|
export default function computeVariableAsAttribute(computation, node, prop) {
|
||||||
let result = getAggregatorResult(node) || 0;
|
let result = getAggregatorResult(node) || 0;
|
||||||
|
|
||||||
prop.total = result;
|
prop.total = result;
|
||||||
|
|
||||||
|
// Apply damage in a way that respects the damage rules, modifying damage if need be
|
||||||
|
// Bound the damage
|
||||||
|
if (!prop.ignoreLowerLimit && prop.damage > prop.total) {
|
||||||
|
console.log(`reducing damage from ${prop.damage} to ${prop.total}`);
|
||||||
|
prop.damage = prop.total;
|
||||||
|
}
|
||||||
|
if (!prop.ignoreUpperLimit && prop.damage < 0) {
|
||||||
|
console.log(`increasing damage from ${prop.damage} to 0`);
|
||||||
|
prop.damage = 0;
|
||||||
|
}
|
||||||
|
// Apply damage
|
||||||
prop.value = prop.total - (prop.damage || 0);
|
prop.value = prop.total - (prop.damage || 0);
|
||||||
|
|
||||||
// Proficiency
|
// Proficiency
|
||||||
prop.proficiency = node.data.proficiency;
|
prop.proficiency = node.data.proficiency;
|
||||||
|
|
||||||
|
// Advantage/disadvantage
|
||||||
|
const aggregator = node.data.effectAggregator;
|
||||||
|
if (aggregator) {
|
||||||
|
if (aggregator.advantage && !aggregator.disadvantage) {
|
||||||
|
prop.advantage = 1;
|
||||||
|
} else if (aggregator.disadvantage && !aggregator.advantage) {
|
||||||
|
prop.advantage = -1;
|
||||||
|
} else {
|
||||||
|
prop.advantage = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Ability scores get modifiers
|
// Ability scores get modifiers
|
||||||
if (prop.attributeType === 'ability'){
|
if (prop.attributeType === 'ability') {
|
||||||
prop.modifier = Math.floor((prop.value - 10) / 2);
|
prop.modifier = Math.floor((prop.value - 10) / 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import aggregate from './aggregate/index.js';
|
import aggregate from './aggregate/index.js';
|
||||||
|
|
||||||
export default function computeVariableAsSkill(computation, node, prop){
|
export default function computeVariableAsSkill(computation, node, prop) {
|
||||||
// Skills are based on some ability Modifier
|
// Skills are based on some ability Modifier
|
||||||
let ability = computation.scope[prop.ability];
|
let ability = computation.scope[prop.ability];
|
||||||
prop.abilityMod = ability?.modifier || 0;
|
prop.abilityMod = ability?.modifier || 0;
|
||||||
|
|
||||||
// Inherit the ability's skill effects and proficiencies if skill is not a save
|
// Inherit the ability's skill effects and proficiencies if skill is not a save
|
||||||
if (prop.skillType !== 'save' && ability){
|
if (prop.skillType !== 'save' && ability) {
|
||||||
aggregateAbilityEffects({
|
aggregateAbilityEffects({
|
||||||
computation,
|
computation,
|
||||||
skillNode: node,
|
skillNode: node,
|
||||||
@@ -21,7 +21,7 @@ export default function computeVariableAsSkill(computation, node, prop){
|
|||||||
let profBonus = computation.scope['proficiencyBonus']?.value || 0;
|
let profBonus = computation.scope['proficiencyBonus']?.value || 0;
|
||||||
|
|
||||||
// Multiply the proficiency bonus by the actual proficiency
|
// Multiply the proficiency bonus by the actual proficiency
|
||||||
if(prop.proficiency === 0.49){
|
if (prop.proficiency === 0.49) {
|
||||||
// Round down proficiency bonus in the special case
|
// Round down proficiency bonus in the special case
|
||||||
profBonus = Math.floor(profBonus * 0.5);
|
profBonus = Math.floor(profBonus * 0.5);
|
||||||
} else {
|
} else {
|
||||||
@@ -37,7 +37,7 @@ export default function computeVariableAsSkill(computation, node, prop){
|
|||||||
prop.effects = node.data.effects;
|
prop.effects = node.data.effects;
|
||||||
|
|
||||||
// If there is no aggregator, determine if the prop can hide, then exit
|
// If there is no aggregator, determine if the prop can hide, then exit
|
||||||
if (!aggregator){
|
if (!aggregator) {
|
||||||
prop.hide = statBase === undefined &&
|
prop.hide = statBase === undefined &&
|
||||||
prop.proficiency == 0 ||
|
prop.proficiency == 0 ||
|
||||||
undefined;
|
undefined;
|
||||||
@@ -52,20 +52,32 @@ export default function computeVariableAsSkill(computation, node, prop){
|
|||||||
if (aggregator.set !== undefined) {
|
if (aggregator.set !== undefined) {
|
||||||
result = aggregator.set;
|
result = aggregator.set;
|
||||||
}
|
}
|
||||||
if (Number.isFinite(result)){
|
if (Number.isFinite(result)) {
|
||||||
result = Math.floor(result);
|
result = Math.floor(result);
|
||||||
}
|
}
|
||||||
prop.value = result;
|
prop.value = result;
|
||||||
// Advantage/disadvantage
|
// Advantage/disadvantage
|
||||||
if (aggregator.advantage && !aggregator.disadvantage){
|
if (aggregator.advantage && !aggregator.disadvantage) {
|
||||||
prop.advantage = 1;
|
prop.advantage = 1;
|
||||||
} else if (aggregator.disadvantage && !aggregator.advantage){
|
} else if (aggregator.disadvantage && !aggregator.advantage) {
|
||||||
prop.advantage = -1;
|
prop.advantage = -1;
|
||||||
} else {
|
} else {
|
||||||
prop.advantage = 0;
|
prop.advantage = 0;
|
||||||
}
|
}
|
||||||
// Passive bonus
|
// Passive bonus
|
||||||
prop.passiveBonus = aggregator.passiveAdd;
|
prop.passiveBonus = aggregator.passiveAdd;
|
||||||
|
// +/- 5 to passive bonus if the skill has advantage/disadvantage
|
||||||
|
if (
|
||||||
|
prop.advantage === 1
|
||||||
|
&& Number.isFinite(prop.passiveBonus)
|
||||||
|
) {
|
||||||
|
prop.passiveBonus += 5;
|
||||||
|
} else if (
|
||||||
|
prop.advantage === -1
|
||||||
|
&& Number.isFinite(prop.passiveBonus)
|
||||||
|
) {
|
||||||
|
prop.bassiveBonus -= 5;
|
||||||
|
}
|
||||||
// conditional benefits
|
// conditional benefits
|
||||||
prop.conditionalBenefits = aggregator.conditional;
|
prop.conditionalBenefits = aggregator.conditional;
|
||||||
// Roll bonuses
|
// Roll bonuses
|
||||||
@@ -76,7 +88,8 @@ export default function computeVariableAsSkill(computation, node, prop){
|
|||||||
prop.rollBonuses = aggregator.rollBonus;
|
prop.rollBonuses = aggregator.rollBonus;
|
||||||
}
|
}
|
||||||
|
|
||||||
function aggregateAbilityEffects({computation, skillNode, abilityNode}){
|
function aggregateAbilityEffects({ computation, skillNode, abilityNode }) {
|
||||||
|
if (!abilityNode?.id) return;
|
||||||
computation.dependencyGraph.forEachLinkedNode(
|
computation.dependencyGraph.forEachLinkedNode(
|
||||||
abilityNode.id,
|
abilityNode.id,
|
||||||
(linkedNode, link) => {
|
(linkedNode, link) => {
|
||||||
@@ -85,15 +98,15 @@ function aggregateAbilityEffects({computation, skillNode, abilityNode}){
|
|||||||
if (linkedNode.data.inactive) return;
|
if (linkedNode.data.inactive) return;
|
||||||
// Check that the link is a valid effect/proficiency to pass on
|
// Check that the link is a valid effect/proficiency to pass on
|
||||||
// to a skill from its ability
|
// to a skill from its ability
|
||||||
if (link.data === 'effect'){
|
if (link.data === 'effect') {
|
||||||
if (![
|
if (![
|
||||||
'advantage', 'disadvantage', 'passiveAdd', 'fail', 'conditional'
|
'advantage', 'disadvantage', 'passiveAdd', 'fail', 'conditional'
|
||||||
].includes(linkedNode.data.operation)){
|
].includes(linkedNode.data.operation)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Apply the aggregations
|
// Apply the aggregations
|
||||||
let arg = {node: skillNode, linkedNode, link};
|
let arg = { node: skillNode, linkedNode, link };
|
||||||
aggregate.effect(arg);
|
aggregate.effect(arg);
|
||||||
aggregate.proficiency(arg);
|
aggregate.proficiency(arg);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
export default function evaluateToggles(computation, node){
|
export default function evaluateToggles(computation, node) {
|
||||||
let prop = node.data;
|
let prop = node.data;
|
||||||
if (!prop) return;
|
if (!prop) return;
|
||||||
let toggles = prop._computationDetails?.toggleAncestors;
|
let toggles = prop._computationDetails?.toggleAncestors;
|
||||||
if (!toggles) return;
|
if (!toggles) return;
|
||||||
toggles.forEach(toggle => {
|
toggles.forEach(toggle => {
|
||||||
if (!toggle.condition) return;
|
if (
|
||||||
if (!toggle.condition.value){
|
(!toggle.enabled && !toggle.disabled && toggle.condition && !toggle.condition.value)
|
||||||
|
|| (toggle.disabled)
|
||||||
|
) {
|
||||||
prop.inactive = true;
|
prop.inactive = true;
|
||||||
prop.deactivatedByToggle = true;
|
prop.deactivatedByToggle = true;
|
||||||
|
prop.deactivatingToggleId = toggle._id;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation.js';
|
||||||
|
import { assert } from 'chai';
|
||||||
|
import computeCreatureComputation from '../../computeCreatureComputation.js';
|
||||||
|
import clean from '../../utility/cleanProp.testFn.js';
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
const computation = buildComputationFromProps(testProperties);
|
||||||
|
computeCreatureComputation(computation);
|
||||||
|
const prop = id => computation.propsById[id];
|
||||||
|
assert.equal(
|
||||||
|
prop('strengthId').value, 8,
|
||||||
|
'The proficiency bonus should not change the strength score'
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
prop('strengthId').modifier, -1,
|
||||||
|
'The proficiency bonus should not change the strength modifier'
|
||||||
|
);
|
||||||
|
assert.exists(prop('actionId').attackRoll.proficiencies, 'The proficiency aggregator should be here')
|
||||||
|
assert.exists(prop('actionId').attackRoll.proficiencies[0], 'The proficiency should be here')
|
||||||
|
// attack roll = strength.mod + proficiencyBonus/2 rounded down
|
||||||
|
// = -1 + 13/2 = -1 + 6 = 5
|
||||||
|
assert.equal(
|
||||||
|
prop('actionId').attackRoll.value, 5,
|
||||||
|
'The proficiency should apply correctly to modify the attack roll'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
var testProperties = [
|
||||||
|
clean({
|
||||||
|
_id: 'strengthId',
|
||||||
|
variableName: 'strength',
|
||||||
|
type: 'attribute',
|
||||||
|
attributeType: 'ability',
|
||||||
|
baseValue: {
|
||||||
|
calculation: '8'
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
clean({
|
||||||
|
_id: 'actionId',
|
||||||
|
type: 'action',
|
||||||
|
ancestors: [{ id: 'charId' }],
|
||||||
|
attackRoll: {
|
||||||
|
calculation: 'strength.modifier',
|
||||||
|
},
|
||||||
|
tags: ['rapier', 'martial weapon', 'weapon', 'attack']
|
||||||
|
}),
|
||||||
|
clean({
|
||||||
|
_id: 'profBonusId',
|
||||||
|
type: 'attribute',
|
||||||
|
variableName: 'proficiencyBonus',
|
||||||
|
ancestors: [{ id: 'charId' }],
|
||||||
|
baseValue: {
|
||||||
|
calculation: '13'
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
clean({
|
||||||
|
_id: 'tagTargetedProficiency',
|
||||||
|
type: 'proficiency',
|
||||||
|
stats: ['strength'], // Should be ignored, we are targeting by tags
|
||||||
|
value: 0.49,
|
||||||
|
targetByTags: true,
|
||||||
|
targetTags: ['martial weapon']
|
||||||
|
}),
|
||||||
|
];
|
||||||
@@ -6,29 +6,33 @@ import computeInventory from './computeInventory.testFn.js';
|
|||||||
import computeDamageMultipliers from './computeDamageMultipliers.testFn.js';
|
import computeDamageMultipliers from './computeDamageMultipliers.testFn.js';
|
||||||
import computeEffects from './computeEffects.testFn.js';
|
import computeEffects from './computeEffects.testFn.js';
|
||||||
import computeSkills from './computeSkills.testFn.js';
|
import computeSkills from './computeSkills.testFn.js';
|
||||||
|
import computeProficiencies from './computeProficiencies.testFn.js';
|
||||||
|
|
||||||
export default [{
|
export default [{
|
||||||
text: 'Computes actions',
|
text: 'Computes actions',
|
||||||
fn: computeAction,
|
fn: computeAction,
|
||||||
},{
|
}, {
|
||||||
text: 'Computes attributes',
|
text: 'Computes attributes',
|
||||||
fn: computeAttribute,
|
fn: computeAttribute,
|
||||||
},{
|
}, {
|
||||||
text: 'Computes classes',
|
text: 'Computes classes',
|
||||||
fn: computeClasses,
|
fn: computeClasses,
|
||||||
},{
|
}, {
|
||||||
text: 'Computes constants',
|
text: 'Computes constants',
|
||||||
fn: computeConstants,
|
fn: computeConstants,
|
||||||
},{
|
}, {
|
||||||
text: 'Computes inventory',
|
text: 'Computes inventory',
|
||||||
fn: computeInventory,
|
fn: computeInventory,
|
||||||
},{
|
}, {
|
||||||
text: 'Computes damage multipliers',
|
text: 'Computes damage multipliers',
|
||||||
fn: computeDamageMultipliers,
|
fn: computeDamageMultipliers,
|
||||||
},{
|
}, {
|
||||||
text: 'Computes effects',
|
text: 'Computes effects',
|
||||||
fn: computeEffects,
|
fn: computeEffects,
|
||||||
},{
|
}, {
|
||||||
text: 'Computes skills',
|
text: 'Computes skills',
|
||||||
fn: computeSkills,
|
fn: computeSkills,
|
||||||
|
}, {
|
||||||
|
text: 'Computes proficiencies',
|
||||||
|
fn: computeProficiencies,
|
||||||
}];
|
}];
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ import { EJSON } from 'meteor/ejson';
|
|||||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||||
import propertySchemasIndex from '/imports/api/properties/computedOnlyPropertySchemasIndex.js';
|
import propertySchemasIndex from '/imports/api/properties/computedOnlyPropertySchemasIndex.js';
|
||||||
|
|
||||||
export default function writeAlteredProperties(computation){
|
export default function writeAlteredProperties(computation) {
|
||||||
let bulkWriteOperations = [];
|
let bulkWriteOperations = [];
|
||||||
// Loop through all properties on the memo
|
// Loop through all properties on the memo
|
||||||
computation.props.forEach(changed => {
|
computation.props.forEach(changed => {
|
||||||
let schema = propertySchemasIndex[changed.type];
|
let schema = propertySchemasIndex[changed.type];
|
||||||
if (!schema){
|
if (!schema) {
|
||||||
console.warn('No schema for ' + changed.type);
|
console.warn('No schema for ' + changed.type);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -20,12 +20,13 @@ export default function writeAlteredProperties(computation){
|
|||||||
'deactivatedBySelf',
|
'deactivatedBySelf',
|
||||||
'deactivatedByAncestor',
|
'deactivatedByAncestor',
|
||||||
'deactivatedByToggle',
|
'deactivatedByToggle',
|
||||||
|
'deactivatingToggleId',
|
||||||
'damage',
|
'damage',
|
||||||
'dirty',
|
'dirty',
|
||||||
...schema.objectKeys(),
|
...schema.objectKeys(),
|
||||||
];
|
];
|
||||||
op = addChangedKeysToOp(op, keys, original, changed);
|
op = addChangedKeysToOp(op, keys, original, changed);
|
||||||
if (op){
|
if (op) {
|
||||||
bulkWriteOperations.push(op);
|
bulkWriteOperations.push(op);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -37,10 +38,10 @@ function addChangedKeysToOp(op, keys, original, changed) {
|
|||||||
// Loop through all keys that can be changed by computation
|
// Loop through all keys that can be changed by computation
|
||||||
// and compile an operation that sets all those keys
|
// and compile an operation that sets all those keys
|
||||||
for (let key of keys) {
|
for (let key of keys) {
|
||||||
if (!EJSON.equals(original[key], changed[key])){
|
if (!EJSON.equals(original[key], changed[key])) {
|
||||||
if (!op) op = newOperation(original._id, changed.type);
|
if (!op) op = newOperation(original._id, changed.type);
|
||||||
let value = changed[key];
|
let value = changed[key];
|
||||||
if (value === undefined){
|
if (value === undefined) {
|
||||||
// Unset values that become undefined
|
// Unset values that become undefined
|
||||||
addUnsetOp(op, key);
|
addUnsetOp(op, key);
|
||||||
} else {
|
} else {
|
||||||
@@ -52,32 +53,32 @@ function addChangedKeysToOp(op, keys, original, changed) {
|
|||||||
return op;
|
return op;
|
||||||
}
|
}
|
||||||
|
|
||||||
function newOperation(_id, type){
|
function newOperation(_id, type) {
|
||||||
let newOp = {
|
let newOp = {
|
||||||
updateOne: {
|
updateOne: {
|
||||||
filter: {_id},
|
filter: { _id },
|
||||||
update: {},
|
update: {},
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if (Meteor.isClient){
|
if (Meteor.isClient) {
|
||||||
newOp.type = type;
|
newOp.type = type;
|
||||||
}
|
}
|
||||||
return newOp;
|
return newOp;
|
||||||
}
|
}
|
||||||
|
|
||||||
function addSetOp(op, key, value){
|
function addSetOp(op, key, value) {
|
||||||
if (op.updateOne.update.$set){
|
if (op.updateOne.update.$set) {
|
||||||
op.updateOne.update.$set[key] = value;
|
op.updateOne.update.$set[key] = value;
|
||||||
} else {
|
} else {
|
||||||
op.updateOne.update.$set = {[key]: value};
|
op.updateOne.update.$set = { [key]: value };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function addUnsetOp(op, key){
|
function addUnsetOp(op, key) {
|
||||||
if (op.updateOne.update.$unset){
|
if (op.updateOne.update.$unset) {
|
||||||
op.updateOne.update.$unset[key] = 1;
|
op.updateOne.update.$unset[key] = 1;
|
||||||
} else {
|
} else {
|
||||||
op.updateOne.update.$unset = {[key]: 1};
|
op.updateOne.update.$unset = { [key]: 1 };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,14 +101,14 @@ function writePropertiesSequentially(bulkWriteOps) {
|
|||||||
// in the UI because of incompatibility with latency compensation. If the
|
// in the UI because of incompatibility with latency compensation. If the
|
||||||
// duplicate redraws can be fixed, this is a strictly better way of processing
|
// duplicate redraws can be fixed, this is a strictly better way of processing
|
||||||
// writes
|
// writes
|
||||||
function bulkWriteProperties(bulkWriteOps){
|
function bulkWriteProperties(bulkWriteOps) {
|
||||||
if (!bulkWriteOps.length) return;
|
if (!bulkWriteOps.length) return;
|
||||||
// bulkWrite is only available on the server
|
// bulkWrite is only available on the server
|
||||||
if (Meteor.isServer) {
|
if (Meteor.isServer) {
|
||||||
CreatureProperties.rawCollection().bulkWrite(
|
CreatureProperties.rawCollection().bulkWrite(
|
||||||
bulkWriteOps,
|
bulkWriteOps,
|
||||||
{ordered : false},
|
{ ordered: false },
|
||||||
function(e){
|
function (e) {
|
||||||
if (e) {
|
if (e) {
|
||||||
console.error('Bulk write failed: ');
|
console.error('Bulk write failed: ');
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
|||||||
@@ -15,18 +15,21 @@ export default function writeScope(creatureId, computation) {
|
|||||||
|
|
||||||
let $set, $unset;
|
let $set, $unset;
|
||||||
|
|
||||||
for (const key in scope){
|
for (const key in scope) {
|
||||||
|
// Mongo can't handle keys that start with a dollar sign
|
||||||
|
if (key[0] === '$' || key[0] === '_') continue;
|
||||||
|
|
||||||
// Remove large properties that aren't likely to be accessed
|
// Remove large properties that aren't likely to be accessed
|
||||||
delete scope[key].parent;
|
delete scope[key].parent;
|
||||||
delete scope[key].ancestors;
|
delete scope[key].ancestors;
|
||||||
|
|
||||||
// Remove empty keys
|
// Remove empty keys
|
||||||
for (const subKey in scope[key]) {
|
for (const subKey in scope[key]) {
|
||||||
if (scope[key][subKey] === undefined) {
|
if (scope[key][subKey] === undefined) {
|
||||||
delete scope[key][subKey]
|
delete scope[key][subKey]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only update changed fields
|
// Only update changed fields
|
||||||
if (!EJSON.equals(variables[key], scope[key])) {
|
if (!EJSON.equals(variables[key], scope[key])) {
|
||||||
if (!$set) $set = {};
|
if (!$set) $set = {};
|
||||||
@@ -53,9 +56,19 @@ export default function writeScope(creatureId, computation) {
|
|||||||
const update = {};
|
const update = {};
|
||||||
if ($set) update.$set = $set;
|
if ($set) update.$set = $set;
|
||||||
if ($unset) update.$unset = $unset;
|
if ($unset) update.$unset = $unset;
|
||||||
CreatureVariables.update({_creatureId: creatureId}, update);
|
CreatureVariables.update({ _creatureId: creatureId }, update);
|
||||||
}
|
}
|
||||||
if (computation.creature?.dirty) {
|
if (computation.creature?.dirty) {
|
||||||
Creatures.update({_id: creatureId}, {$unset: { dirty: 1 }});
|
Creatures.update({ _id: creatureId }, { $unset: { dirty: 1 } });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
|
function calculateSize(computation) {
|
||||||
|
const sizeEstimator = {
|
||||||
|
creature: computation.creature,
|
||||||
|
variables: computation.variables,
|
||||||
|
props: computation.originalPropsById,
|
||||||
|
};
|
||||||
|
return MongoInternals.NpmModule.BSON.calculateObjectSize(sizeEstimator, { checkKeys: false })
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import writeAlteredProperties from './computation/writeComputation/writeAlteredP
|
|||||||
import writeScope from './computation/writeComputation/writeScope.js';
|
import writeScope from './computation/writeComputation/writeScope.js';
|
||||||
import writeErrors from './computation/writeComputation/writeErrors.js';
|
import writeErrors from './computation/writeComputation/writeErrors.js';
|
||||||
|
|
||||||
export default function computeCreature(creatureId){
|
export default function computeCreature(creatureId) {
|
||||||
if (Meteor.isClient) return;
|
if (Meteor.isClient) return;
|
||||||
// console.log('compute ' + creatureId);
|
// console.log('compute ' + creatureId);
|
||||||
const computation = buildCreatureComputation(creatureId);
|
const computation = buildCreatureComputation(creatureId);
|
||||||
@@ -16,7 +16,7 @@ function computeComputation(computation, creatureId) {
|
|||||||
computeCreatureComputation(computation);
|
computeCreatureComputation(computation);
|
||||||
writeAlteredProperties(computation);
|
writeAlteredProperties(computation);
|
||||||
writeScope(creatureId, computation);
|
writeScope(creatureId, computation);
|
||||||
} catch (e){
|
} catch (e) {
|
||||||
const errorText = e.reason || e.message || e.toString();
|
const errorText = e.reason || e.message || e.toString();
|
||||||
computation.errors.push({
|
computation.errors.push({
|
||||||
type: 'crash',
|
type: 'crash',
|
||||||
@@ -32,6 +32,19 @@ function computeComputation(computation, creatureId) {
|
|||||||
console.error(logError);
|
console.error(logError);
|
||||||
throw e;
|
throw e;
|
||||||
} finally {
|
} finally {
|
||||||
|
checkPropertyCount(computation)
|
||||||
writeErrors(creatureId, computation.errors);
|
writeErrors(creatureId, computation.errors);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MAX_PROPS = 1000;
|
||||||
|
function checkPropertyCount(computation) {
|
||||||
|
const count = computation.props.length;
|
||||||
|
if (count <= MAX_PROPS) return;
|
||||||
|
computation.errors.push({
|
||||||
|
type: 'warning',
|
||||||
|
details: {
|
||||||
|
error: `This character sheet has too many properties and may perform poorly ( ${count} / ${MAX_PROPS} )`
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -29,6 +29,16 @@ let LibrarySchema = new SimpleSchema({
|
|||||||
optional: true,
|
optional: true,
|
||||||
max: STORAGE_LIMITS.summary,
|
max: STORAGE_LIMITS.summary,
|
||||||
},
|
},
|
||||||
|
showInMarket: {
|
||||||
|
index: 1,
|
||||||
|
type: Boolean,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
subscriberCount: {
|
||||||
|
index: 1,
|
||||||
|
type: Number,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
LibrarySchema.extend(SharingSchema);
|
LibrarySchema.extend(SharingSchema);
|
||||||
@@ -104,6 +114,29 @@ const updateLibraryDescription = new ValidatedMethod({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const updateLibraryShowInMarket = new ValidatedMethod({
|
||||||
|
name: 'libraries.updateShowInMarket',
|
||||||
|
validate: new SimpleSchema({
|
||||||
|
_id: {
|
||||||
|
type: String,
|
||||||
|
regEx: SimpleSchema.RegEx.id
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
type: Boolean,
|
||||||
|
},
|
||||||
|
}).validator(),
|
||||||
|
mixins: [RateLimiterMixin],
|
||||||
|
rateLimit: {
|
||||||
|
numRequests: 5,
|
||||||
|
timeInterval: 5000,
|
||||||
|
},
|
||||||
|
run({ _id, value }) {
|
||||||
|
let library = Libraries.findOne(_id);
|
||||||
|
assertEditPermission(library, this.userId);
|
||||||
|
Libraries.update(_id, { $set: { showInMarket: value } });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const removeLibrary = new ValidatedMethod({
|
const removeLibrary = new ValidatedMethod({
|
||||||
name: 'libraries.remove',
|
name: 'libraries.remove',
|
||||||
validate: new SimpleSchema({
|
validate: new SimpleSchema({
|
||||||
@@ -130,4 +163,4 @@ export function removeLibaryWork(libraryId) {
|
|||||||
LibraryNodes.remove({ 'ancestors.id': libraryId });
|
LibraryNodes.remove({ 'ancestors.id': libraryId });
|
||||||
}
|
}
|
||||||
|
|
||||||
export { LibrarySchema, insertLibrary, updateLibraryName, updateLibraryDescription, removeLibrary };
|
export { LibrarySchema, insertLibrary, updateLibraryName, updateLibraryDescription, updateLibraryShowInMarket, removeLibrary };
|
||||||
|
|||||||
@@ -32,6 +32,16 @@ const LibraryCollectionSchema = new SimpleSchema({
|
|||||||
type: String,
|
type: String,
|
||||||
regEx: SimpleSchema.RegEx.Id,
|
regEx: SimpleSchema.RegEx.Id,
|
||||||
},
|
},
|
||||||
|
showInMarket: {
|
||||||
|
index: 1,
|
||||||
|
type: Boolean,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
subscriberCount: {
|
||||||
|
index: 1,
|
||||||
|
type: Number,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
LibraryCollectionSchema.extend(SharingSchema);
|
LibraryCollectionSchema.extend(SharingSchema);
|
||||||
@@ -48,12 +58,12 @@ const insertLibraryCollection = new ValidatedMethod({
|
|||||||
run(libraryCollection) {
|
run(libraryCollection) {
|
||||||
if (!this.userId) {
|
if (!this.userId) {
|
||||||
throw new Meteor.Error('LibraryCollections.methods.insert.denied',
|
throw new Meteor.Error('LibraryCollections.methods.insert.denied',
|
||||||
'You need to be logged in to insert a library');
|
'You need to be logged in to insert a library');
|
||||||
}
|
}
|
||||||
let tier = getUserTier(this.userId);
|
let tier = getUserTier(this.userId);
|
||||||
if (!tier.paidBenefits){
|
if (!tier.paidBenefits) {
|
||||||
throw new Meteor.Error('LibraryCollections.methods.insert.denied',
|
throw new Meteor.Error('LibraryCollections.methods.insert.denied',
|
||||||
`The ${tier.name} tier does not allow you to insert a library collection`);
|
`The ${tier.name} tier does not allow you to insert a library collection`);
|
||||||
}
|
}
|
||||||
libraryCollection.owner = this.userId;
|
libraryCollection.owner = this.userId;
|
||||||
return LibraryCollections.insert(libraryCollection);
|
return LibraryCollections.insert(libraryCollection);
|
||||||
@@ -72,7 +82,7 @@ const updateLibraryCollection = new ValidatedMethod({
|
|||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
type: LibraryCollectionSchema
|
type: LibraryCollectionSchema
|
||||||
.pick('name', 'description', 'libraries')
|
.pick('name', 'description', 'libraries', 'showInMarket')
|
||||||
.extend({ //make libraries optional
|
.extend({ //make libraries optional
|
||||||
libraries: {
|
libraries: {
|
||||||
optional: true,
|
optional: true,
|
||||||
@@ -85,7 +95,7 @@ const updateLibraryCollection = new ValidatedMethod({
|
|||||||
numRequests: 5,
|
numRequests: 5,
|
||||||
timeInterval: 5000,
|
timeInterval: 5000,
|
||||||
},
|
},
|
||||||
run({_id, update}){
|
run({ _id, update }) {
|
||||||
const libraryCollection = LibraryCollections.findOne(_id, {
|
const libraryCollection = LibraryCollections.findOne(_id, {
|
||||||
fields: {
|
fields: {
|
||||||
owner: 1,
|
owner: 1,
|
||||||
@@ -93,7 +103,7 @@ const updateLibraryCollection = new ValidatedMethod({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
assertEditPermission(libraryCollection, this.userId);
|
assertEditPermission(libraryCollection, this.userId);
|
||||||
return LibraryCollections.update(_id, {$set: update});
|
return LibraryCollections.update(_id, { $set: update });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -110,7 +120,7 @@ const removeLibraryCollection = new ValidatedMethod({
|
|||||||
numRequests: 5,
|
numRequests: 5,
|
||||||
timeInterval: 5000,
|
timeInterval: 5000,
|
||||||
},
|
},
|
||||||
run({_id}){
|
run({ _id }) {
|
||||||
const libraryCollection = LibraryCollections.findOne(_id, {
|
const libraryCollection = LibraryCollections.findOne(_id, {
|
||||||
fields: {
|
fields: {
|
||||||
owner: 1,
|
owner: 1,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
|||||||
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
||||||
import SimpleSchema from 'simpl-schema';
|
import SimpleSchema from 'simpl-schema';
|
||||||
import ColorSchema from '/imports/api/properties/subSchemas/ColorSchema.js';
|
import ColorSchema from '/imports/api/properties/subSchemas/ColorSchema.js';
|
||||||
import ChildSchema from '/imports/api/parenting/ChildSchema.js';
|
import ChildSchema, { RefSchema } from '/imports/api/parenting/ChildSchema.js';
|
||||||
import propertySchemasIndex from '/imports/api/properties/propertySchemasIndex.js';
|
import propertySchemasIndex from '/imports/api/properties/propertySchemasIndex.js';
|
||||||
import Libraries from '/imports/api/library/Libraries.js';
|
import Libraries from '/imports/api/library/Libraries.js';
|
||||||
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
|
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
|
||||||
@@ -15,6 +15,8 @@ import '/imports/api/library/methods/index.js';
|
|||||||
import { updateReferenceNodeWork } from '/imports/api/library/methods/updateReferenceNode.js';
|
import { updateReferenceNodeWork } from '/imports/api/library/methods/updateReferenceNode.js';
|
||||||
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
||||||
import { restore } from '/imports/api/parenting/softRemove.js';
|
import { restore } from '/imports/api/parenting/softRemove.js';
|
||||||
|
import { getAncestry } from '/imports/api/parenting/parenting.js';
|
||||||
|
import { reorderDocs } from '/imports/api/parenting/order.js';
|
||||||
|
|
||||||
let LibraryNodes = new Mongo.Collection('libraryNodes');
|
let LibraryNodes = new Mongo.Collection('libraryNodes');
|
||||||
|
|
||||||
@@ -36,20 +38,66 @@ let LibraryNodeSchema = new SimpleSchema({
|
|||||||
type: String,
|
type: String,
|
||||||
max: STORAGE_LIMITS.tagLength,
|
max: STORAGE_LIMITS.tagLength,
|
||||||
},
|
},
|
||||||
|
icon: {
|
||||||
|
type: storedIconsSchema,
|
||||||
|
optional: true,
|
||||||
|
max: STORAGE_LIMITS.icon,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Library-specific properties, these can be stripped from the resulting
|
||||||
|
// creature properties
|
||||||
|
|
||||||
|
// Will this property show up in the slot-fill dialog
|
||||||
|
fillSlots: {
|
||||||
|
type: Boolean,
|
||||||
|
optional: true,
|
||||||
|
index: 1,
|
||||||
|
},
|
||||||
|
// Will this property show up in the insert-from-library dialog
|
||||||
|
searchable: {
|
||||||
|
type: Boolean,
|
||||||
|
optional: true,
|
||||||
|
index: 1,
|
||||||
|
},
|
||||||
libraryTags: {
|
libraryTags: {
|
||||||
type: Array,
|
type: Array,
|
||||||
defaultValue: [],
|
optional: true,
|
||||||
maxCount: STORAGE_LIMITS.tagCount,
|
maxCount: STORAGE_LIMITS.tagCount,
|
||||||
},
|
},
|
||||||
'libraryTags.$': {
|
'libraryTags.$': {
|
||||||
type: String,
|
type: String,
|
||||||
max: STORAGE_LIMITS.tagLength,
|
max: STORAGE_LIMITS.tagLength,
|
||||||
},
|
},
|
||||||
icon: {
|
// Overrides the type when searching for properties
|
||||||
type: storedIconsSchema,
|
slotFillerType: {
|
||||||
|
type: String,
|
||||||
optional: true,
|
optional: true,
|
||||||
max: STORAGE_LIMITS.icon,
|
max: STORAGE_LIMITS.variableName,
|
||||||
}
|
},
|
||||||
|
// Image to display when filling the slot
|
||||||
|
slotFillImage: {
|
||||||
|
type: String,
|
||||||
|
optional: true,
|
||||||
|
max: STORAGE_LIMITS.url,
|
||||||
|
},
|
||||||
|
// Fill more than one quantity in a slot, like feats and ability score
|
||||||
|
// improvements, filtered out of UI if there isn't space in quantityExpected
|
||||||
|
slotQuantityFilled: {
|
||||||
|
type: SimpleSchema.Integer,
|
||||||
|
optional: true, // Undefined implies 1
|
||||||
|
},
|
||||||
|
// Filters out of UI if condition isn't met, but isn't otherwise enforced
|
||||||
|
slotFillerCondition: {
|
||||||
|
type: String,
|
||||||
|
optional: true,
|
||||||
|
max: STORAGE_LIMITS.calculation,
|
||||||
|
},
|
||||||
|
// Text to display if slot filler condition fails
|
||||||
|
slotFillerConditionNote: {
|
||||||
|
type: String,
|
||||||
|
optional: true,
|
||||||
|
max: STORAGE_LIMITS.calculation,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set up server side search index
|
// Set up server side search index
|
||||||
@@ -86,20 +134,56 @@ function assertNodeEditPermission(node, userId) {
|
|||||||
|
|
||||||
const insertNode = new ValidatedMethod({
|
const insertNode = new ValidatedMethod({
|
||||||
name: 'libraryNodes.insert',
|
name: 'libraryNodes.insert',
|
||||||
validate: null,
|
validate: new SimpleSchema({
|
||||||
|
libraryNode: {
|
||||||
|
type: Object,
|
||||||
|
blackbox: true,
|
||||||
|
},
|
||||||
|
parentRef: RefSchema,
|
||||||
|
}).validator(),
|
||||||
mixins: [RateLimiterMixin],
|
mixins: [RateLimiterMixin],
|
||||||
rateLimit: {
|
rateLimit: {
|
||||||
numRequests: 5,
|
numRequests: 5,
|
||||||
timeInterval: 5000,
|
timeInterval: 5000,
|
||||||
},
|
},
|
||||||
run(libraryNode) {
|
run({ libraryNode, parentRef }) {
|
||||||
|
// get the new ancestry
|
||||||
|
let { parentDoc, ancestors } = getAncestry({ parentRef });
|
||||||
|
|
||||||
|
// Check permission to edit
|
||||||
|
let root;
|
||||||
|
if (parentRef.collection === 'libraries') {
|
||||||
|
root = parentDoc;
|
||||||
|
} else if (parentRef.collection === 'libraryNodes') {
|
||||||
|
root = Libraries.findOne(parentDoc.ancestors[0].id);
|
||||||
|
} else {
|
||||||
|
throw `${parentRef.collection} is not a valid parent collection`
|
||||||
|
}
|
||||||
|
assertEditPermission(root, this.userId);
|
||||||
|
|
||||||
|
// Set the ancestry of the library node
|
||||||
|
libraryNode.parent = parentRef;
|
||||||
|
libraryNode.ancestors = ancestors;
|
||||||
|
// Remove its ID if it came with one to force a random one to be generated
|
||||||
|
// server-side
|
||||||
delete libraryNode._id;
|
delete libraryNode._id;
|
||||||
assertNodeEditPermission(libraryNode, this.userId);
|
|
||||||
let nodeId = LibraryNodes.insert(libraryNode);
|
// Insert the node
|
||||||
|
const nodeId = LibraryNodes.insert(libraryNode);
|
||||||
|
|
||||||
|
// Update the node if it was a reference node
|
||||||
if (libraryNode.type == 'reference') {
|
if (libraryNode.type == 'reference') {
|
||||||
libraryNode._id = nodeId;
|
libraryNode._id = nodeId;
|
||||||
updateReferenceNodeWork(libraryNode, this.userId);
|
updateReferenceNodeWork(libraryNode, this.userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tree structure changed by insert, reorder the tree
|
||||||
|
reorderDocs({
|
||||||
|
collection: LibraryNodes,
|
||||||
|
ancestorId: root._id,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Return the id of the inserted node
|
||||||
return nodeId;
|
return nodeId;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -119,7 +203,7 @@ const updateLibraryNode = new ValidatedMethod({
|
|||||||
},
|
},
|
||||||
mixins: [RateLimiterMixin],
|
mixins: [RateLimiterMixin],
|
||||||
rateLimit: {
|
rateLimit: {
|
||||||
numRequests: 5,
|
numRequests: 15,
|
||||||
timeInterval: 5000,
|
timeInterval: 5000,
|
||||||
},
|
},
|
||||||
run({ _id, path, value }) {
|
run({ _id, path, value }) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import getUserLibraryIds from './getUserLibraryIds';
|
|||||||
import { intersection, union } from 'lodash';
|
import { intersection, union } from 'lodash';
|
||||||
|
|
||||||
export default function getCreatureLibraryIds(creature, userId) {
|
export default function getCreatureLibraryIds(creature, userId) {
|
||||||
|
if (!userId) console.log('no userId, returning empty array');
|
||||||
if (!userId) return [];
|
if (!userId) return [];
|
||||||
|
|
||||||
// Get the ids of libraries the user is permitted to view
|
// Get the ids of libraries the user is permitted to view
|
||||||
@@ -17,14 +18,14 @@ export default function getCreatureLibraryIds(creature, userId) {
|
|||||||
allowedLibraryCollections: 1,
|
allowedLibraryCollections: 1,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (!creature) return [];
|
if (!creature) return userLibIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the creature does not restrict the libraries, let it use them all
|
// If the creature does not restrict the libraries, let it use them all
|
||||||
if (!creature.allowedLibraryCollections && !creature.allowedLibraries) {
|
if (!creature.allowedLibraryCollections && !creature.allowedLibraries) {
|
||||||
return userLibIds;
|
return userLibIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the ids of the libraries that the creature allows
|
// Get the ids of the libraries that the creature allows
|
||||||
const allowedCollections = creature.allowedLibraryCollections || [];
|
const allowedCollections = creature.allowedLibraryCollections || [];
|
||||||
let creatureLibIds = creature.allowedLibraries || [];
|
let creatureLibIds = creature.allowedLibraries || [];
|
||||||
|
|||||||
18
app/imports/api/library/methods/getDefaultSlotFiller.js
Normal file
18
app/imports/api/library/methods/getDefaultSlotFiller.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
export default function getDefaultSlotFiller(slot) {
|
||||||
|
if (typeof slot !== 'object') throw 'getDefaultSlotFiller requires a slot';
|
||||||
|
if (slot.type !== 'propertySlot') throw 'provided slot must be a propertySlot';
|
||||||
|
|
||||||
|
let slotType = slot.slotType;
|
||||||
|
if (!slotType || slot.slotType === 'slotFiller') {
|
||||||
|
slotType = 'folder';
|
||||||
|
}
|
||||||
|
|
||||||
|
const filler = {
|
||||||
|
type: slotType,
|
||||||
|
libraryTags: slot.slotTags || [],
|
||||||
|
name: 'Custom ' + slot.name || 'slot filler',
|
||||||
|
parent: { collection: 'creatureProperties', id: slot._id },
|
||||||
|
ancestors: [...slot.ancestors, { collection: 'creatureProperties', id: slot._id }],
|
||||||
|
};
|
||||||
|
return filler;
|
||||||
|
}
|
||||||
@@ -21,7 +21,7 @@ const updateReferenceNode = new ValidatedMethod({
|
|||||||
numRequests: 5,
|
numRequests: 5,
|
||||||
timeInterval: 5000,
|
timeInterval: 5000,
|
||||||
},
|
},
|
||||||
run({_id}) {
|
run({ _id }) {
|
||||||
let userId = this.userId;
|
let userId = this.userId;
|
||||||
let node = LibraryNodes.findOne(_id);
|
let node = LibraryNodes.findOne(_id);
|
||||||
assertDocEditPermission(node, userId);
|
assertDocEditPermission(node, userId);
|
||||||
@@ -29,15 +29,15 @@ const updateReferenceNode = new ValidatedMethod({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
function writeCache(_id, cache){
|
function writeCache(_id, cache) {
|
||||||
LibraryNodes.update(_id, {$set: {cache}}, {
|
LibraryNodes.update(_id, { $set: { cache } }, {
|
||||||
selector: {type: 'reference'},
|
selector: { type: 'reference' },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateReferenceNodeWork(node, userId){
|
function updateReferenceNodeWork(node, userId) {
|
||||||
let cache = {}
|
let cache = {}
|
||||||
if (!node.ref){
|
if (!node.ref?.collection || !node.ref?.id) {
|
||||||
writeCache(node._id, cache);
|
writeCache(node._id, cache);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -45,20 +45,23 @@ function updateReferenceNodeWork(node, userId){
|
|||||||
try {
|
try {
|
||||||
doc = fetchDocByRef(node.ref);
|
doc = fetchDocByRef(node.ref);
|
||||||
if (doc.removed) throw 'Property has been deleted';
|
if (doc.removed) throw 'Property has been deleted';
|
||||||
if (doc.ancestors[0].id !== node.ancestors[0].id){
|
if (doc.ancestors[0].id !== node.ancestors[0].id) {
|
||||||
library = fetchDocByRef(doc.ancestors[0]);
|
library = fetchDocByRef(doc.ancestors[0]);
|
||||||
assertViewPermission(library, userId)
|
assertViewPermission(library, userId)
|
||||||
}
|
}
|
||||||
} catch(e){
|
} catch (e) {
|
||||||
cache = {error: e.reason || e.message || e.toString()}
|
cache = { error: e.reason || e.message || e.toString() }
|
||||||
writeCache(node._id, cache);
|
writeCache(node._id, cache);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
cache = {
|
cache = {
|
||||||
node: doc,
|
node: doc,
|
||||||
};
|
};
|
||||||
if (library){
|
if (library) {
|
||||||
cache.library = {name: library.name};
|
cache.library = {
|
||||||
|
id: library._id,
|
||||||
|
name: library.name,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
writeCache(node._id, cache);
|
writeCache(node._id, cache);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export function nodeArrayToTree(nodes) {
|
|||||||
forest.push(treeNode);
|
forest.push(treeNode);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
forest.nodeIndex = nodeIndex;
|
||||||
return forest;
|
return forest;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,13 +23,20 @@ const organizeDoc = new ValidatedMethod({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
optional: true,
|
optional: true,
|
||||||
},
|
},
|
||||||
|
skipClient: {
|
||||||
|
type: Boolean,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
}).validator(),
|
}).validator(),
|
||||||
mixins: [RateLimiterMixin],
|
mixins: [RateLimiterMixin],
|
||||||
rateLimit: {
|
rateLimit: {
|
||||||
numRequests: 5,
|
numRequests: 5,
|
||||||
timeInterval: 5000,
|
timeInterval: 5000,
|
||||||
},
|
},
|
||||||
run({docRef, parentRef, order, skipRecompute}) {
|
run({ docRef, parentRef, order, skipRecompute, skipClient }) {
|
||||||
|
if (skipClient && this.isSimulation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let doc = fetchDocByRef(docRef);
|
let doc = fetchDocByRef(docRef);
|
||||||
let collection = getCollectionByName(docRef.collection);
|
let collection = getCollectionByName(docRef.collection);
|
||||||
// The user must be able to edit both the doc and its parent to move it
|
// The user must be able to edit both the doc and its parent to move it
|
||||||
@@ -39,23 +46,23 @@ const organizeDoc = new ValidatedMethod({
|
|||||||
assertDocEditPermission(parent, this.userId);
|
assertDocEditPermission(parent, this.userId);
|
||||||
|
|
||||||
// Change the doc's parent
|
// Change the doc's parent
|
||||||
updateParent({docRef, parentRef});
|
updateParent({ docRef, parentRef });
|
||||||
// Change the doc's order to be a half step ahead of its target location
|
// Change the doc's order to be a half step ahead of its target location
|
||||||
collection.update(doc._id, {$set: {order}}, {selector: {type: 'any'}});
|
collection.update(doc._id, { $set: { order } }, { selector: { type: 'any' } });
|
||||||
|
|
||||||
// Reorder both ancestors' documents
|
// Reorder both ancestors' documents
|
||||||
let oldAncestorId = doc.ancestors[0].id;
|
let oldAncestorId = doc.ancestors[0].id;
|
||||||
reorderDocs({collection, ancestorId: oldAncestorId});
|
reorderDocs({ collection, ancestorId: oldAncestorId });
|
||||||
|
|
||||||
let newAncestorId = getRootId(parent);
|
let newAncestorId = getRootId(parent);
|
||||||
if (newAncestorId !== oldAncestorId){
|
if (newAncestorId !== oldAncestorId) {
|
||||||
reorderDocs({collection, ancestorId: newAncestorId});
|
reorderDocs({ collection, ancestorId: newAncestorId });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Figure out which creatures need to be recalculated after this move
|
// Figure out which creatures need to be recalculated after this move
|
||||||
let docCreatures = getCreatureAncestors(doc);
|
let docCreatures = getCreatureAncestors(doc);
|
||||||
let parentCreatures = getCreatureAncestors(parent);
|
let parentCreatures = getCreatureAncestors(parent);
|
||||||
if (!skipRecompute){
|
if (!skipRecompute) {
|
||||||
let creaturesToRecompute = union(docCreatures, parentCreatures);
|
let creaturesToRecompute = union(docCreatures, parentCreatures);
|
||||||
// Mark the creatures for recompute
|
// Mark the creatures for recompute
|
||||||
Creatures.update({
|
Creatures.update({
|
||||||
@@ -81,10 +88,10 @@ const reorderDoc = new ValidatedMethod({
|
|||||||
numRequests: 5,
|
numRequests: 5,
|
||||||
timeInterval: 5000,
|
timeInterval: 5000,
|
||||||
},
|
},
|
||||||
run({docRef, order}) {
|
run({ docRef, order }) {
|
||||||
let doc = fetchDocByRef(docRef);
|
let doc = fetchDocByRef(docRef);
|
||||||
assertDocEditPermission(doc, this.userId);
|
assertDocEditPermission(doc, this.userId);
|
||||||
safeUpdateDocOrder({docRef, order});
|
safeUpdateDocOrder({ docRef, order });
|
||||||
// Recompute the affected creatures
|
// Recompute the affected creatures
|
||||||
const ancestors = getCreatureAncestors(doc);
|
const ancestors = getCreatureAncestors(doc);
|
||||||
if (ancestors.length) {
|
if (ancestors.length) {
|
||||||
@@ -97,22 +104,22 @@ const reorderDoc = new ValidatedMethod({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
function getRootId(doc){
|
function getRootId(doc) {
|
||||||
if (doc.ancestors && doc.ancestors.length && doc.ancestors[0]){
|
if (doc.ancestors && doc.ancestors.length && doc.ancestors[0]) {
|
||||||
return doc.ancestors[0].id;
|
return doc.ancestors[0].id;
|
||||||
} else {
|
} else {
|
||||||
return doc._id;
|
return doc._id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCreatureAncestors(doc){
|
function getCreatureAncestors(doc) {
|
||||||
let ids = [];
|
let ids = [];
|
||||||
if(doc.type === 'pc' || doc.type === 'npc' || doc.type === 'monster'){
|
if (doc.type === 'pc' || doc.type === 'npc' || doc.type === 'monster') {
|
||||||
ids.push(doc._id);
|
ids.push(doc._id);
|
||||||
}
|
}
|
||||||
if (doc.ancestors){
|
if (doc.ancestors) {
|
||||||
doc.ancestors.forEach(ancestorRef => {
|
doc.ancestors.forEach(ancestorRef => {
|
||||||
if (ancestorRef.collection === 'creatures'){
|
if (ancestorRef.collection === 'creatures') {
|
||||||
ids.push(ancestorRef.id);
|
ids.push(ancestorRef.id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ let ActionSchema = createPropertySchema({
|
|||||||
attackRoll: {
|
attackRoll: {
|
||||||
type: 'fieldToCompute',
|
type: 'fieldToCompute',
|
||||||
optional: true,
|
optional: true,
|
||||||
defaultValue: 'strength.modifier + proficiencyBonus',
|
|
||||||
},
|
},
|
||||||
// Calculation of how many times this action can be used
|
// Calculation of how many times this action can be used
|
||||||
uses: {
|
uses: {
|
||||||
|
|||||||
@@ -173,6 +173,13 @@ let ComputedOnlyAttributeSchema = createPropertySchema({
|
|||||||
optional: true,
|
optional: true,
|
||||||
removeBeforeCompute: true,
|
removeBeforeCompute: true,
|
||||||
},
|
},
|
||||||
|
// Attributes with advantage grant it to all skills based on the attribute
|
||||||
|
advantage: {
|
||||||
|
type: SimpleSchema.Integer,
|
||||||
|
optional: true,
|
||||||
|
allowedValues: [-1, 0, 1],
|
||||||
|
removeBeforeCompute: true,
|
||||||
|
},
|
||||||
// The computed creature constitution modifier for hit dice
|
// The computed creature constitution modifier for hit dice
|
||||||
constitutionMod: {
|
constitutionMod: {
|
||||||
type: Number,
|
type: Number,
|
||||||
|
|||||||
@@ -26,12 +26,6 @@ const ClassLevelSchema = createPropertySchema({
|
|||||||
defaultValue: 1,
|
defaultValue: 1,
|
||||||
max: STORAGE_LIMITS.levelMax,
|
max: STORAGE_LIMITS.levelMax,
|
||||||
},
|
},
|
||||||
// Filters out of UI if condition isn't met, but isn't otherwise enforced
|
|
||||||
slotFillerCondition: {
|
|
||||||
type: String,
|
|
||||||
optional: true,
|
|
||||||
max: STORAGE_LIMITS.calculation,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const ComputedOnlyClassLevelSchema = createPropertySchema({
|
const ComputedOnlyClassLevelSchema = createPropertySchema({
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import SimpleSchema from 'simpl-schema';
|
import SimpleSchema from 'simpl-schema';
|
||||||
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
||||||
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
|
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
|
||||||
|
import TagTargetingSchema from '/imports/api/properties/subSchemas/TagTargetingSchema.js';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Effects are reason-value attached to skills and abilities
|
* Effects are reason-value attached to skills and abilities
|
||||||
@@ -50,57 +51,7 @@ let EffectSchema = createPropertySchema({
|
|||||||
type: String,
|
type: String,
|
||||||
max: STORAGE_LIMITS.variableName,
|
max: STORAGE_LIMITS.variableName,
|
||||||
},
|
},
|
||||||
// True when targeting by tags instead of stats
|
}).extend(TagTargetingSchema);
|
||||||
targetByTags: {
|
|
||||||
type: Boolean,
|
|
||||||
optional: true,
|
|
||||||
},
|
|
||||||
// If targeting by tags, the field which will be targeted
|
|
||||||
targetField: {
|
|
||||||
type: String,
|
|
||||||
optional: true,
|
|
||||||
max: STORAGE_LIMITS.variableName,
|
|
||||||
},
|
|
||||||
// Which tags the effect is applied to
|
|
||||||
targetTags: {
|
|
||||||
type: Array,
|
|
||||||
optional: true,
|
|
||||||
maxCount: STORAGE_LIMITS.tagCount,
|
|
||||||
},
|
|
||||||
'targetTags.$': {
|
|
||||||
type: String,
|
|
||||||
max: STORAGE_LIMITS.tagLength,
|
|
||||||
},
|
|
||||||
extraTags: {
|
|
||||||
type: Array,
|
|
||||||
optional: true,
|
|
||||||
maxCount: STORAGE_LIMITS.extraTagsCount,
|
|
||||||
},
|
|
||||||
'extraTags.$': {
|
|
||||||
type: Object,
|
|
||||||
},
|
|
||||||
'extraTags.$._id': {
|
|
||||||
type: String,
|
|
||||||
regEx: SimpleSchema.RegEx.Id,
|
|
||||||
autoValue(){
|
|
||||||
if (!this.isSet) return Random.id();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'extraTags.$.operation': {
|
|
||||||
type: String,
|
|
||||||
allowedValues: ['OR', 'NOT'],
|
|
||||||
defaultValue: 'OR',
|
|
||||||
},
|
|
||||||
'extraTags.$.tags': {
|
|
||||||
type: Array,
|
|
||||||
defaultValue: [],
|
|
||||||
maxCount: STORAGE_LIMITS.tagCount,
|
|
||||||
},
|
|
||||||
'extraTags.$.tags.$': {
|
|
||||||
type: String,
|
|
||||||
max: STORAGE_LIMITS.tagLength,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const ComputedOnlyEffectSchema = createPropertySchema({
|
const ComputedOnlyEffectSchema = createPropertySchema({
|
||||||
amount: {
|
amount: {
|
||||||
|
|||||||
@@ -1,14 +1,19 @@
|
|||||||
|
import SimpleSchema from 'simpl-schema';
|
||||||
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
||||||
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
|
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
|
||||||
|
|
||||||
// Folders organize a character sheet into a tree, particularly to group things
|
// Folders organize a character sheet into a tree, particularly to group things
|
||||||
// like 'race' and 'background'
|
// like 'race' and 'background'
|
||||||
let FolderSchema = new createPropertySchema({
|
let FolderSchema = createPropertySchema({
|
||||||
name: {
|
name: {
|
||||||
type: String,
|
type: String,
|
||||||
max: STORAGE_LIMITS.name,
|
max: STORAGE_LIMITS.name,
|
||||||
optional: true,
|
optional: true,
|
||||||
},
|
},
|
||||||
|
description: {
|
||||||
|
type: 'inlineCalculationFieldToCompute',
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
groupStats: {
|
groupStats: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
optional: true,
|
optional: true,
|
||||||
@@ -33,6 +38,15 @@ let FolderSchema = new createPropertySchema({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const ComputedOnlyFolderSchema = new createPropertySchema({});
|
const ComputedOnlyFolderSchema = createPropertySchema({
|
||||||
|
description: {
|
||||||
|
type: 'computedOnlyInlineCalculationField',
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export { FolderSchema, ComputedOnlyFolderSchema };
|
const ComputedFolderSchema = new SimpleSchema()
|
||||||
|
.extend(FolderSchema)
|
||||||
|
.extend(ComputedOnlyFolderSchema);
|
||||||
|
|
||||||
|
export { FolderSchema, ComputedFolderSchema, ComputedOnlyFolderSchema };
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import SimpleSchema from 'simpl-schema';
|
import SimpleSchema from 'simpl-schema';
|
||||||
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
||||||
|
import TagTargetingSchema from '/imports/api/properties/subSchemas/TagTargetingSchema.js';
|
||||||
|
|
||||||
let ProficiencySchema = new SimpleSchema({
|
let ProficiencySchema = new SimpleSchema({
|
||||||
name: {
|
name: {
|
||||||
@@ -24,7 +25,7 @@ let ProficiencySchema = new SimpleSchema({
|
|||||||
allowedValues: [0.49, 0.5, 1, 2],
|
allowedValues: [0.49, 0.5, 1, 2],
|
||||||
defaultValue: 1,
|
defaultValue: 1,
|
||||||
},
|
},
|
||||||
});
|
}).extend(TagTargetingSchema);
|
||||||
|
|
||||||
const ComputedOnlyProficiencySchema = new SimpleSchema({});
|
const ComputedOnlyProficiencySchema = new SimpleSchema({});
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,10 @@ let ReferenceSchema = new SimpleSchema({
|
|||||||
type: Object,
|
type: Object,
|
||||||
optional: true,
|
optional: true,
|
||||||
},
|
},
|
||||||
|
'cache.library.id': {
|
||||||
|
type: String,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
'cache.library.name': {
|
'cache.library.name': {
|
||||||
type: String,
|
type: String,
|
||||||
optional: true,
|
optional: true,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import SimpleSchema from 'simpl-schema';
|
|||||||
import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js';
|
import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js';
|
||||||
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
||||||
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
|
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
|
||||||
|
import TagTargetingSchema from '/imports/api/properties/subSchemas/TagTargetingSchema.js';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Skills are anything that results in a modifier to be added to a D20
|
* Skills are anything that results in a modifier to be added to a D20
|
||||||
@@ -59,7 +60,8 @@ let SkillSchema = createPropertySchema({
|
|||||||
type: 'inlineCalculationFieldToCompute',
|
type: 'inlineCalculationFieldToCompute',
|
||||||
optional: true,
|
optional: true,
|
||||||
},
|
},
|
||||||
});
|
// Skills can apply their value to other calculations as a proficiency using tag targeting
|
||||||
|
}).extend(TagTargetingSchema);
|
||||||
|
|
||||||
let ComputedOnlySkillSchema = createPropertySchema({
|
let ComputedOnlySkillSchema = createPropertySchema({
|
||||||
// Computed value of skill to be added to skill rolls
|
// Computed value of skill to be added to skill rolls
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
import SimpleSchema from 'simpl-schema';
|
|
||||||
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
|
||||||
|
|
||||||
// SlotFiller fillers specifically fill a slot with a bit more control than
|
|
||||||
// other properties
|
|
||||||
let SlotFillerSchema = new SimpleSchema({
|
|
||||||
name: {
|
|
||||||
type: String,
|
|
||||||
optional: true,
|
|
||||||
max: STORAGE_LIMITS.name,
|
|
||||||
},
|
|
||||||
picture: {
|
|
||||||
type: String,
|
|
||||||
optional: true,
|
|
||||||
max: STORAGE_LIMITS.url,
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
type: String,
|
|
||||||
optional: true,
|
|
||||||
max: STORAGE_LIMITS.description,
|
|
||||||
},
|
|
||||||
// Overrides the type when searching for properties
|
|
||||||
slotFillerType: {
|
|
||||||
type: String,
|
|
||||||
optional: true,
|
|
||||||
max: STORAGE_LIMITS.variableName,
|
|
||||||
},
|
|
||||||
// Fill more than one quantity in a slot, like feats and ability score
|
|
||||||
// improvements, filtered out of UI if there isn't space in quantityExpected
|
|
||||||
slotQuantityFilled: {
|
|
||||||
type: SimpleSchema.Integer,
|
|
||||||
defaultValue: 1,
|
|
||||||
},
|
|
||||||
// Filters out of UI if condition isn't met, but isn't otherwise enforced
|
|
||||||
slotFillerCondition: {
|
|
||||||
type: String,
|
|
||||||
optional: true,
|
|
||||||
max: STORAGE_LIMITS.calculation,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const ComputedOnlySlotFillerSchema = new SimpleSchema({});
|
|
||||||
|
|
||||||
export { SlotFillerSchema, ComputedOnlySlotFillerSchema };
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import SimpleSchema from 'simpl-schema';
|
import SimpleSchema from 'simpl-schema';
|
||||||
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
||||||
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
|
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
|
||||||
|
import TagTargetingSchema from '/imports/api/properties/subSchemas/TagTargetingSchema.js';
|
||||||
|
|
||||||
const ToggleSchema = createPropertySchema({
|
const ToggleSchema = createPropertySchema({
|
||||||
name: {
|
name: {
|
||||||
@@ -31,7 +32,7 @@ const ToggleSchema = createPropertySchema({
|
|||||||
type: 'fieldToCompute',
|
type: 'fieldToCompute',
|
||||||
optional: true,
|
optional: true,
|
||||||
},
|
},
|
||||||
});
|
}).extend(TagTargetingSchema);
|
||||||
|
|
||||||
const ComputedOnlyToggleSchema = createPropertySchema({
|
const ComputedOnlyToggleSchema = createPropertySchema({
|
||||||
condition: {
|
condition: {
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import { ComputedOnlyRollSchema } from '/imports/api/properties/Rolls.js';
|
|||||||
import { ComputedOnlySavingThrowSchema } from '/imports/api/properties/SavingThrows.js';
|
import { ComputedOnlySavingThrowSchema } from '/imports/api/properties/SavingThrows.js';
|
||||||
import { ComputedOnlySkillSchema } from '/imports/api/properties/Skills.js';
|
import { ComputedOnlySkillSchema } from '/imports/api/properties/Skills.js';
|
||||||
import { ComputedOnlySlotSchema } from '/imports/api/properties/Slots.js';
|
import { ComputedOnlySlotSchema } from '/imports/api/properties/Slots.js';
|
||||||
import { ComputedOnlySlotFillerSchema } from '/imports/api/properties/SlotFillers.js';
|
|
||||||
import { ComputedOnlySpellSchema } from '/imports/api/properties/Spells.js';
|
import { ComputedOnlySpellSchema } from '/imports/api/properties/Spells.js';
|
||||||
import { ComputedOnlySpellListSchema } from '/imports/api/properties/SpellLists.js';
|
import { ComputedOnlySpellListSchema } from '/imports/api/properties/SpellLists.js';
|
||||||
import { ComputedOnlyToggleSchema } from '/imports/api/properties/Toggles.js';
|
import { ComputedOnlyToggleSchema } from '/imports/api/properties/Toggles.js';
|
||||||
@@ -54,7 +53,6 @@ const propertySchemasIndex = {
|
|||||||
roll: ComputedOnlyRollSchema,
|
roll: ComputedOnlyRollSchema,
|
||||||
savingThrow: ComputedOnlySavingThrowSchema,
|
savingThrow: ComputedOnlySavingThrowSchema,
|
||||||
skill: ComputedOnlySkillSchema,
|
skill: ComputedOnlySkillSchema,
|
||||||
slotFiller: ComputedOnlySlotFillerSchema,
|
|
||||||
spellList: ComputedOnlySpellListSchema,
|
spellList: ComputedOnlySpellListSchema,
|
||||||
spell: ComputedOnlySpellSchema,
|
spell: ComputedOnlySpellSchema,
|
||||||
toggle: ComputedOnlyToggleSchema,
|
toggle: ComputedOnlyToggleSchema,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { ComputedDamageSchema } from '/imports/api/properties/Damages.js';
|
|||||||
import { DamageMultiplierSchema } from '/imports/api/properties/DamageMultipliers.js';
|
import { DamageMultiplierSchema } from '/imports/api/properties/DamageMultipliers.js';
|
||||||
import { ComputedEffectSchema } from '/imports/api/properties/Effects.js';
|
import { ComputedEffectSchema } from '/imports/api/properties/Effects.js';
|
||||||
import { ComputedFeatureSchema } from '/imports/api/properties/Features.js';
|
import { ComputedFeatureSchema } from '/imports/api/properties/Features.js';
|
||||||
import { FolderSchema } from '/imports/api/properties/Folders.js';
|
import { ComputedFolderSchema } from '/imports/api/properties/Folders.js';
|
||||||
import { ComputedItemSchema } from '/imports/api/properties/Items.js';
|
import { ComputedItemSchema } from '/imports/api/properties/Items.js';
|
||||||
import { ComputedNoteSchema } from '/imports/api/properties/Notes.js';
|
import { ComputedNoteSchema } from '/imports/api/properties/Notes.js';
|
||||||
import { ComputedPointBuySchema } from '/imports/api/properties/PointBuys.js';
|
import { ComputedPointBuySchema } from '/imports/api/properties/PointBuys.js';
|
||||||
@@ -23,7 +23,6 @@ import { ComputedRollSchema } from '/imports/api/properties/Rolls.js';
|
|||||||
import { ComputedSavingThrowSchema } from '/imports/api/properties/SavingThrows.js';
|
import { ComputedSavingThrowSchema } from '/imports/api/properties/SavingThrows.js';
|
||||||
import { ComputedSkillSchema } from '/imports/api/properties/Skills.js';
|
import { ComputedSkillSchema } from '/imports/api/properties/Skills.js';
|
||||||
import { ComputedSlotSchema } from '/imports/api/properties/Slots.js';
|
import { ComputedSlotSchema } from '/imports/api/properties/Slots.js';
|
||||||
import { SlotFillerSchema } from '/imports/api/properties/SlotFillers.js';
|
|
||||||
import { ComputedSpellSchema } from '/imports/api/properties/Spells.js';
|
import { ComputedSpellSchema } from '/imports/api/properties/Spells.js';
|
||||||
import { ComputedSpellListSchema } from '/imports/api/properties/SpellLists.js';
|
import { ComputedSpellListSchema } from '/imports/api/properties/SpellLists.js';
|
||||||
import { ComputedToggleSchema } from '/imports/api/properties/Toggles.js';
|
import { ComputedToggleSchema } from '/imports/api/properties/Toggles.js';
|
||||||
@@ -43,7 +42,7 @@ const propertySchemasIndex = {
|
|||||||
damageMultiplier: DamageMultiplierSchema,
|
damageMultiplier: DamageMultiplierSchema,
|
||||||
effect: ComputedEffectSchema,
|
effect: ComputedEffectSchema,
|
||||||
feature: ComputedFeatureSchema,
|
feature: ComputedFeatureSchema,
|
||||||
folder: FolderSchema,
|
folder: ComputedFolderSchema,
|
||||||
note: ComputedNoteSchema,
|
note: ComputedNoteSchema,
|
||||||
pointBuy: ComputedPointBuySchema,
|
pointBuy: ComputedPointBuySchema,
|
||||||
proficiency: ProficiencySchema,
|
proficiency: ProficiencySchema,
|
||||||
@@ -52,7 +51,6 @@ const propertySchemasIndex = {
|
|||||||
roll: ComputedRollSchema,
|
roll: ComputedRollSchema,
|
||||||
savingThrow: ComputedSavingThrowSchema,
|
savingThrow: ComputedSavingThrowSchema,
|
||||||
skill: ComputedSkillSchema,
|
skill: ComputedSkillSchema,
|
||||||
slotFiller: SlotFillerSchema,
|
|
||||||
spellList: ComputedSpellListSchema,
|
spellList: ComputedSpellListSchema,
|
||||||
spell: ComputedSpellSchema,
|
spell: ComputedSpellSchema,
|
||||||
toggle: ComputedToggleSchema,
|
toggle: ComputedToggleSchema,
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import { RollSchema } from '/imports/api/properties/Rolls.js';
|
|||||||
import { SavingThrowSchema } from '/imports/api/properties/SavingThrows.js';
|
import { SavingThrowSchema } from '/imports/api/properties/SavingThrows.js';
|
||||||
import { SkillSchema } from '/imports/api/properties/Skills.js';
|
import { SkillSchema } from '/imports/api/properties/Skills.js';
|
||||||
import { SlotSchema } from '/imports/api/properties/Slots.js';
|
import { SlotSchema } from '/imports/api/properties/Slots.js';
|
||||||
import { SlotFillerSchema } from '/imports/api/properties/SlotFillers.js';
|
|
||||||
import { SpellListSchema } from '/imports/api/properties/SpellLists.js';
|
import { SpellListSchema } from '/imports/api/properties/SpellLists.js';
|
||||||
import { SpellSchema } from '/imports/api/properties/Spells.js';
|
import { SpellSchema } from '/imports/api/properties/Spells.js';
|
||||||
import { ToggleSchema } from '/imports/api/properties/Toggles.js';
|
import { ToggleSchema } from '/imports/api/properties/Toggles.js';
|
||||||
@@ -52,7 +51,6 @@ const propertySchemasIndex = {
|
|||||||
roll: RollSchema,
|
roll: RollSchema,
|
||||||
savingThrow: SavingThrowSchema,
|
savingThrow: SavingThrowSchema,
|
||||||
skill: SkillSchema,
|
skill: SkillSchema,
|
||||||
slotFiller: SlotFillerSchema,
|
|
||||||
spellList: SpellListSchema,
|
spellList: SpellListSchema,
|
||||||
spell: SpellSchema,
|
spell: SpellSchema,
|
||||||
toggle: ToggleSchema,
|
toggle: ToggleSchema,
|
||||||
|
|||||||
57
app/imports/api/properties/subSchemas/TagTargetingSchema.js
Normal file
57
app/imports/api/properties/subSchemas/TagTargetingSchema.js
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import SimpleSchema from 'simpl-schema';
|
||||||
|
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
||||||
|
|
||||||
|
const TagTargetingSchema = new SimpleSchema({
|
||||||
|
// True when targeting by tags instead of stats
|
||||||
|
targetByTags: {
|
||||||
|
type: Boolean,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
// If targeting by tags, the field which will be targeted
|
||||||
|
targetField: {
|
||||||
|
type: String,
|
||||||
|
optional: true,
|
||||||
|
max: STORAGE_LIMITS.variableName,
|
||||||
|
},
|
||||||
|
// Which tags the effect is applied to
|
||||||
|
targetTags: {
|
||||||
|
type: Array,
|
||||||
|
optional: true,
|
||||||
|
maxCount: STORAGE_LIMITS.tagCount,
|
||||||
|
},
|
||||||
|
'targetTags.$': {
|
||||||
|
type: String,
|
||||||
|
max: STORAGE_LIMITS.tagLength,
|
||||||
|
},
|
||||||
|
extraTags: {
|
||||||
|
type: Array,
|
||||||
|
optional: true,
|
||||||
|
maxCount: STORAGE_LIMITS.extraTagsCount,
|
||||||
|
},
|
||||||
|
'extraTags.$': {
|
||||||
|
type: Object,
|
||||||
|
},
|
||||||
|
'extraTags.$._id': {
|
||||||
|
type: String,
|
||||||
|
regEx: SimpleSchema.RegEx.Id,
|
||||||
|
autoValue() {
|
||||||
|
if (!this.isSet) return Random.id();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'extraTags.$.operation': {
|
||||||
|
type: String,
|
||||||
|
allowedValues: ['OR', 'NOT'],
|
||||||
|
defaultValue: 'OR',
|
||||||
|
},
|
||||||
|
'extraTags.$.tags': {
|
||||||
|
type: Array,
|
||||||
|
defaultValue: [],
|
||||||
|
maxCount: STORAGE_LIMITS.tagCount,
|
||||||
|
},
|
||||||
|
'extraTags.$.tags.$': {
|
||||||
|
type: String,
|
||||||
|
max: STORAGE_LIMITS.tagLength,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default TagTargetingSchema;
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
import SimpleSchema from 'simpl-schema';
|
import SimpleSchema from 'simpl-schema';
|
||||||
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
||||||
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
||||||
|
import Libraries from '/imports/api/library/Libraries.js';
|
||||||
|
import LibraryCollections from '/imports/api/library/LibraryCollections.js';
|
||||||
import '/imports/api/users/methods/deleteMyAccount.js';
|
import '/imports/api/users/methods/deleteMyAccount.js';
|
||||||
import '/imports/api/users/methods/addEmail.js';
|
import '/imports/api/users/methods/addEmail.js';
|
||||||
import '/imports/api/users/methods/removeEmail.js';
|
import '/imports/api/users/methods/removeEmail.js';
|
||||||
import '/imports/api/users/methods/updateFileStorageUsed.js';
|
import '/imports/api/users/methods/updateFileStorageUsed.js';
|
||||||
|
|
||||||
import { some } from 'lodash';
|
import { some } from 'lodash';
|
||||||
const defaultLibraries = process.env.DEFAULT_LIBRARIES && process.env.DEFAULT_LIBRARIES.split(',') || [];
|
const defaultLibraries = process.env.DEFAULT_LIBRARIES && process.env.DEFAULT_LIBRARIES.split(',') || [];
|
||||||
const defaultLibraryCollections = process.env.DEFAULT_LIBRARY_COLLECTIONS && process.env.DEFAULT_LIBRARY_COLLECTIONS.split(',') || [];
|
const defaultLibraryCollections = process.env.DEFAULT_LIBRARY_COLLECTIONS && process.env.DEFAULT_LIBRARY_COLLECTIONS.split(',') || [];
|
||||||
@@ -142,12 +143,12 @@ Meteor.users.generateApiKey = new ValidatedMethod({
|
|||||||
Meteor.users.setDarkMode = new ValidatedMethod({
|
Meteor.users.setDarkMode = new ValidatedMethod({
|
||||||
name: 'users.setDarkMode',
|
name: 'users.setDarkMode',
|
||||||
validate: new SimpleSchema({
|
validate: new SimpleSchema({
|
||||||
darkMode: { type: Boolean },
|
darkMode: { type: Boolean, optional: true },
|
||||||
}).validator(),
|
}).validator(),
|
||||||
mixins: [RateLimiterMixin],
|
mixins: [RateLimiterMixin],
|
||||||
rateLimit: {
|
rateLimit: {
|
||||||
numRequests: 5,
|
numRequests: 5,
|
||||||
timeInterval: 5000,
|
timeInterval: 2000,
|
||||||
},
|
},
|
||||||
run({ darkMode }) {
|
run({ darkMode }) {
|
||||||
if (!this.userId) return;
|
if (!this.userId) return;
|
||||||
@@ -250,6 +251,29 @@ Meteor.users.setPreference = new ValidatedMethod({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (Meteor.isServer) {
|
||||||
|
Accounts.onCreateUser(() => {
|
||||||
|
if (defaultLibraries?.length) {
|
||||||
|
Libraries.update({
|
||||||
|
_id: { $in: defaultLibraries }
|
||||||
|
}, {
|
||||||
|
$inc: { subscriberCount: 1 }
|
||||||
|
}, {
|
||||||
|
multi: true,
|
||||||
|
}, () => {/**/ });
|
||||||
|
}
|
||||||
|
if (defaultLibraryCollections?.length) {
|
||||||
|
LibraryCollections.update({
|
||||||
|
_id: { $in: defaultLibraryCollections }
|
||||||
|
}, {
|
||||||
|
$inc: { subscriberCount: 1 }
|
||||||
|
}, {
|
||||||
|
multi: true,
|
||||||
|
}, () => {/**/ });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Meteor.users.subscribeToLibrary = new ValidatedMethod({
|
Meteor.users.subscribeToLibrary = new ValidatedMethod({
|
||||||
name: 'users.subscribeToLibrary',
|
name: 'users.subscribeToLibrary',
|
||||||
validate: new SimpleSchema({
|
validate: new SimpleSchema({
|
||||||
@@ -264,15 +288,17 @@ Meteor.users.subscribeToLibrary = new ValidatedMethod({
|
|||||||
mixins: [RateLimiterMixin],
|
mixins: [RateLimiterMixin],
|
||||||
rateLimit: {
|
rateLimit: {
|
||||||
numRequests: 5,
|
numRequests: 5,
|
||||||
timeInterval: 5000,
|
timeInterval: 2000,
|
||||||
},
|
},
|
||||||
run({ libraryId, subscribe }) {
|
run({ libraryId, subscribe }) {
|
||||||
if (!this.userId) throw 'Can only subscribe if logged in';
|
if (!this.userId) throw 'Can only subscribe if logged in';
|
||||||
if (subscribe) {
|
if (subscribe) {
|
||||||
|
Libraries.update({ _id: libraryId }, { $inc: { subscriberCount: 1 } }, () => {/**/ });
|
||||||
return Meteor.users.update(this.userId, {
|
return Meteor.users.update(this.userId, {
|
||||||
$addToSet: { subscribedLibraries: libraryId },
|
$addToSet: { subscribedLibraries: libraryId },
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
Libraries.update({ _id: libraryId }, { $inc: { subscriberCount: -1 } }, () => {/**/ });
|
||||||
return Meteor.users.update(this.userId, {
|
return Meteor.users.update(this.userId, {
|
||||||
$pullAll: { subscribedLibraries: libraryId },
|
$pullAll: { subscribedLibraries: libraryId },
|
||||||
});
|
});
|
||||||
@@ -299,10 +325,12 @@ Meteor.users.subscribeToLibraryCollection = new ValidatedMethod({
|
|||||||
run({ libraryCollectionId, subscribe }) {
|
run({ libraryCollectionId, subscribe }) {
|
||||||
if (!this.userId) throw 'Can only subscribe if logged in';
|
if (!this.userId) throw 'Can only subscribe if logged in';
|
||||||
if (subscribe) {
|
if (subscribe) {
|
||||||
|
LibraryCollections.update({ _id: libraryCollectionId }, { $inc: { subscriberCount: 1 } }, () => {/**/ });
|
||||||
return Meteor.users.update(this.userId, {
|
return Meteor.users.update(this.userId, {
|
||||||
$addToSet: { subscribedLibraryCollections: libraryCollectionId },
|
$addToSet: { subscribedLibraryCollections: libraryCollectionId },
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
LibraryCollections.update({ _id: libraryCollectionId }, { $inc: { subscriberCount: -1 } }, () => {/**/ });
|
||||||
return Meteor.users.update(this.userId, {
|
return Meteor.users.update(this.userId, {
|
||||||
$pullAll: { subscribedLibraryCollections: libraryCollectionId },
|
$pullAll: { subscribedLibraryCollections: libraryCollectionId },
|
||||||
});
|
});
|
||||||
|
|||||||
3
app/imports/api/utility/escapeRegex.js
Normal file
3
app/imports/api/utility/escapeRegex.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export default function escapeRegex(string) {
|
||||||
|
return string.replace(/[/\-\\^$*+?.()|[\]{}]/g, '');
|
||||||
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
export default function numberToSignedString(number, spaced){
|
export default function numberToSignedString(number, spaced) {
|
||||||
if (typeof number !== 'number') return number;
|
if (typeof number !== 'number') return number;
|
||||||
if (number === 0){
|
if (number === 0) {
|
||||||
return spaced ? '+ 0' : '+0';
|
return spaced ? '+ 0' : '+0';
|
||||||
} else if (number > 0){
|
} else if (number > 0) {
|
||||||
return spaced ? `+ ${number}` : `+${number}`;
|
return spaced ? `+ ${number}` : `+${number}`;
|
||||||
} else {
|
} else {
|
||||||
return spaced ? `- ${Math.abs(number) || number}` : `${number}`;
|
// Uses the unicode minus sign '−' instead of a dash '-' to help line up numbers nicely
|
||||||
|
return spaced ? `− ${Math.abs(number) || number}` : `−${Math.abs(number) || number}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,16 +7,18 @@
|
|||||||
>
|
>
|
||||||
<template #activator="{ on }">
|
<template #activator="{ on }">
|
||||||
<v-btn
|
<v-btn
|
||||||
:outlined="!!label"
|
|
||||||
:icon="!label"
|
:icon="!label"
|
||||||
|
:tile="!label"
|
||||||
:min-width="label && 108"
|
:min-width="label && 108"
|
||||||
|
:height="height"
|
||||||
|
:width="width"
|
||||||
:disabled="context.editPermission === false"
|
:disabled="context.editPermission === false"
|
||||||
v-on="on"
|
v-on="on"
|
||||||
>
|
>
|
||||||
{{ label }}
|
{{ label }}
|
||||||
<v-icon
|
<v-icon
|
||||||
:right="!!label"
|
:right="!!label"
|
||||||
:color="label && value"
|
:color="value"
|
||||||
>
|
>
|
||||||
mdi-format-paint
|
mdi-format-paint
|
||||||
</v-icon>
|
</v-icon>
|
||||||
@@ -137,7 +139,15 @@
|
|||||||
label: {
|
label: {
|
||||||
type: String,
|
type: String,
|
||||||
default: undefined,
|
default: undefined,
|
||||||
}
|
},
|
||||||
|
height: {
|
||||||
|
type: Number,
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
|
width: {
|
||||||
|
type: Number,
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data(){ return {
|
data(){ return {
|
||||||
colors: [
|
colors: [
|
||||||
|
|||||||
@@ -16,6 +16,14 @@ export default {
|
|||||||
wideColumns: Boolean,
|
wideColumns: Boolean,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Removed to improve chrome layout performance, put it back if there are rendering errors
|
||||||
|
.column-layout>span>div {
|
||||||
|
display: table;
|
||||||
|
table-layout: fixed;
|
||||||
|
}
|
||||||
|
*/
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="css">
|
<style lang="css">
|
||||||
@@ -39,17 +47,9 @@ export default {
|
|||||||
|
|
||||||
.column-layout>div,
|
.column-layout>div,
|
||||||
.column-layout>span>div {
|
.column-layout>span>div {
|
||||||
/*
|
|
||||||
Table and width set because firefox does not support break-inside: avoid
|
|
||||||
*/
|
|
||||||
display: table;
|
|
||||||
table-layout: fixed;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
backface-visibility: hidden;
|
backface-visibility: hidden;
|
||||||
-webkit-backface-visibility: hidden;
|
|
||||||
transform: translateX(0);
|
transform: translateX(0);
|
||||||
-webkit-transform: translateX(0);
|
|
||||||
-webkit-column-break-inside: avoid;
|
|
||||||
page-break-inside: avoid;
|
page-break-inside: avoid;
|
||||||
break-inside: avoid;
|
break-inside: avoid;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
|
|||||||
@@ -19,7 +19,10 @@
|
|||||||
</v-btn>
|
</v-btn>
|
||||||
</template>
|
</template>
|
||||||
<v-sheet class="d-flex flex-column align-center justify-center">
|
<v-sheet class="d-flex flex-column align-center justify-center">
|
||||||
<v-btn-toggle v-model="dataAdvantage">
|
<v-btn-toggle
|
||||||
|
v-model="dataAdvantage"
|
||||||
|
color="accent"
|
||||||
|
>
|
||||||
<v-btn :value="-1">
|
<v-btn :value="-1">
|
||||||
Disadvantage
|
Disadvantage
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
<v-divider vertical />
|
<v-divider vertical />
|
||||||
<div
|
<div
|
||||||
class="flex layout column"
|
class="flex layout column"
|
||||||
style="background-color: inherit; overflow: hidden;"
|
style="background-color: inherit; overflow: hidden; min-height: 100%;"
|
||||||
data-id="selected-node-card"
|
data-id="selected-node-card"
|
||||||
>
|
>
|
||||||
<slot name="detail" />
|
<slot name="detail" />
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
<v-icon
|
<v-icon
|
||||||
class="handle"
|
class="handle"
|
||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
@click.stop="() => { }"
|
@click.native="e => { }"
|
||||||
@touchstart.native.stop="() => { }"
|
@touchstart.native.stop="e => { }"
|
||||||
@touchend.native="portalEvent"
|
@touchend.native="portalEvent"
|
||||||
>
|
>
|
||||||
mdi-drag
|
mdi-drag
|
||||||
|
|||||||
@@ -5,26 +5,32 @@
|
|||||||
transition="slide-y-transition"
|
transition="slide-y-transition"
|
||||||
min-width="290px"
|
min-width="290px"
|
||||||
style="overflow-y: auto;"
|
style="overflow-y: auto;"
|
||||||
|
left
|
||||||
>
|
>
|
||||||
<template #activator="{ on }">
|
<template #activator="{ on }">
|
||||||
<v-btn
|
<v-btn
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
outlined
|
:outlined="!!label"
|
||||||
:min-width="108"
|
:icon="!label"
|
||||||
v-bind="$attrs"
|
:tile="!label"
|
||||||
|
:min-width="label && 108"
|
||||||
|
:height="height"
|
||||||
|
:width="width"
|
||||||
:style="buttonStyle"
|
:style="buttonStyle"
|
||||||
|
:disabled="context.editPermission === false"
|
||||||
|
v-bind="$attrs"
|
||||||
v-on="on"
|
v-on="on"
|
||||||
>
|
>
|
||||||
{{ label }}
|
{{ label }}
|
||||||
<svg-icon
|
<svg-icon
|
||||||
v-if="safeValue && safeValue.shape"
|
v-if="safeValue && safeValue.shape"
|
||||||
right
|
right
|
||||||
class="ml-2"
|
:class="{'ml-2': !!label}"
|
||||||
:shape="safeValue.shape"
|
:shape="safeValue.shape"
|
||||||
/>
|
/>
|
||||||
<v-icon
|
<v-icon
|
||||||
v-else
|
v-else
|
||||||
right
|
:right="!!label"
|
||||||
>
|
>
|
||||||
mdi-select-search
|
mdi-select-search
|
||||||
</v-icon>
|
</v-icon>
|
||||||
@@ -87,15 +93,26 @@ export default {
|
|||||||
SvgIcon,
|
SvgIcon,
|
||||||
},
|
},
|
||||||
mixins: [SmartInput],
|
mixins: [SmartInput],
|
||||||
|
inject: {
|
||||||
|
context: { default: {} }
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
label: {
|
label: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'Icon',
|
default: undefined,
|
||||||
},
|
},
|
||||||
buttonStyle: {
|
buttonStyle: {
|
||||||
type: String,
|
type: String,
|
||||||
default: undefined,
|
default: undefined,
|
||||||
},
|
},
|
||||||
|
height: {
|
||||||
|
type: Number,
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
|
width: {
|
||||||
|
type: Number,
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
:disabled="isDisabled"
|
:disabled="isDisabled"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
@click="click"
|
@click.stop.prevent="click"
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</v-btn>
|
</v-btn>
|
||||||
@@ -41,7 +41,7 @@ export default {
|
|||||||
} else if (Number.isFinite(this.context.debounceTime)){
|
} else if (Number.isFinite(this.context.debounceTime)){
|
||||||
return this.context.debounceTime;
|
return this.context.debounceTime;
|
||||||
} else {
|
} else {
|
||||||
return 750;
|
return 400;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -62,11 +62,12 @@ export default {
|
|||||||
this.$emit('click', this.acknowledgeChange);
|
this.$emit('click', this.acknowledgeChange);
|
||||||
},
|
},
|
||||||
clicks() {
|
clicks() {
|
||||||
|
if (!this.$listeners?.clicks) return;
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.$emit('clicks', this.timesClicked, this.acknowledgeChange);
|
this.$emit('clicks', this.timesClicked, this.acknowledgeChange);
|
||||||
this.timesClicked = 0;
|
this.timesClicked = 0;
|
||||||
},
|
},
|
||||||
acknowledgeChange(error){
|
acknowledgeChange(error) {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
|
|||||||
76
app/imports/client/ui/components/global/SmartToggle.vue
Normal file
76
app/imports/client/ui/components/global/SmartToggle.vue
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
<template lang="html">
|
||||||
|
<outlined-input
|
||||||
|
:name="label"
|
||||||
|
class="mb-6 pt-1"
|
||||||
|
>
|
||||||
|
<v-btn-toggle
|
||||||
|
v-bind="$attrs"
|
||||||
|
mandatory
|
||||||
|
tile
|
||||||
|
group
|
||||||
|
:value="safeValue"
|
||||||
|
color="accent"
|
||||||
|
style="flex-wrap: wrap;"
|
||||||
|
>
|
||||||
|
<v-btn
|
||||||
|
v-for="(option, i) in options"
|
||||||
|
:key="`toggle-option-${i}`"
|
||||||
|
:value="option.value"
|
||||||
|
:disabled="isDisabled || (clickedValue != option.value && loading)"
|
||||||
|
:plain="clickedValue != option.value && loading"
|
||||||
|
:loading="clickedValue == option.value && loading"
|
||||||
|
height="42"
|
||||||
|
v-on="(value == option.value) ? {} : { click() { click(option.value) } }"
|
||||||
|
>
|
||||||
|
<v-icon
|
||||||
|
v-if="option.icon"
|
||||||
|
left
|
||||||
|
>
|
||||||
|
{{ option.icon }}
|
||||||
|
</v-icon>
|
||||||
|
{{ option.name }}
|
||||||
|
</v-btn>
|
||||||
|
</v-btn-toggle>
|
||||||
|
<v-expand-transition>
|
||||||
|
<div
|
||||||
|
v-if="errors.length"
|
||||||
|
class="pa-2 error--text"
|
||||||
|
>
|
||||||
|
{{ errors.join('\n\n') }}
|
||||||
|
</div>
|
||||||
|
</v-expand-transition>
|
||||||
|
</outlined-input>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="js">
|
||||||
|
import SmartInput from '/imports/client/ui/components/global/SmartInputMixin.js';
|
||||||
|
import OutlinedInput from '/imports/client/ui/properties/viewers/shared/OutlinedInput.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
OutlinedInput,
|
||||||
|
},
|
||||||
|
mixins: [SmartInput],
|
||||||
|
props: {
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
clickedValue: undefined,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
click(val) {
|
||||||
|
this.clickedValue = val;
|
||||||
|
this.change(val);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -16,6 +16,9 @@
|
|||||||
<template #append>
|
<template #append>
|
||||||
<slot name="value" />
|
<slot name="value" />
|
||||||
</template>
|
</template>
|
||||||
|
<template #prepend>
|
||||||
|
<slot name="prepend" />
|
||||||
|
</template>
|
||||||
</v-text-field>
|
</v-text-field>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import SmartBtn from '/imports/client/ui/components/global/SmartBtn.vue';
|
|||||||
import SmartCombobox from '/imports/client/ui/components/global/SmartCombobox.vue';
|
import SmartCombobox from '/imports/client/ui/components/global/SmartCombobox.vue';
|
||||||
import SmartCheckbox from '/imports/client/ui/components/global/SmartCheckbox.vue';
|
import SmartCheckbox from '/imports/client/ui/components/global/SmartCheckbox.vue';
|
||||||
import SmartSwitch from '/imports/client/ui/components/global/SmartSwitch.vue';
|
import SmartSwitch from '/imports/client/ui/components/global/SmartSwitch.vue';
|
||||||
|
import SmartToggle from '/imports/client/ui/components/global/SmartToggle.vue';
|
||||||
import SvgIcon from '/imports/client/ui/components/global/SvgIcon.vue';
|
import SvgIcon from '/imports/client/ui/components/global/SvgIcon.vue';
|
||||||
import SmartSlider from '/imports/client/ui/components/global/SmartSlider.vue';
|
import SmartSlider from '/imports/client/ui/components/global/SmartSlider.vue';
|
||||||
|
|
||||||
@@ -24,4 +25,5 @@ Vue.component('SmartCombobox', SmartCombobox);
|
|||||||
Vue.component('SmartCheckbox', SmartCheckbox);
|
Vue.component('SmartCheckbox', SmartCheckbox);
|
||||||
Vue.component('SmartSlider', SmartSlider);
|
Vue.component('SmartSlider', SmartSlider);
|
||||||
Vue.component('SmartSwitch', SmartSwitch);
|
Vue.component('SmartSwitch', SmartSwitch);
|
||||||
|
Vue.component('SmartToggle', SmartToggle);
|
||||||
Vue.component('SvgIcon', SvgIcon);
|
Vue.component('SvgIcon', SvgIcon);
|
||||||
|
|||||||
@@ -29,11 +29,6 @@
|
|||||||
style="flex-shrink: 0;"
|
style="flex-shrink: 0;"
|
||||||
>
|
>
|
||||||
<v-spacer />
|
<v-spacer />
|
||||||
<color-picker
|
|
||||||
v-if="$listeners && $listeners['color-changed']"
|
|
||||||
:value="model.color"
|
|
||||||
@input="colorChanged"
|
|
||||||
/>
|
|
||||||
<v-menu
|
<v-menu
|
||||||
v-if="$listeners && (
|
v-if="$listeners && (
|
||||||
$listeners.move ||
|
$listeners.move ||
|
||||||
@@ -109,6 +104,20 @@
|
|||||||
<v-icon>mdi-send</v-icon>
|
<v-icon>mdi-send</v-icon>
|
||||||
</v-list-item-action>
|
</v-list-item-action>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
|
<v-list-item
|
||||||
|
v-if="$listeners && $listeners['copy-to-library'] && userPaid"
|
||||||
|
:disabled="context.editPermission === false"
|
||||||
|
@click="$emit('copy-to-library')"
|
||||||
|
>
|
||||||
|
<v-list-item-content>
|
||||||
|
<v-list-item-title>
|
||||||
|
Copy to library
|
||||||
|
</v-list-item-title>
|
||||||
|
</v-list-item-content>
|
||||||
|
<v-list-item-action>
|
||||||
|
<v-icon>mdi-content-duplicate</v-icon>
|
||||||
|
</v-list-item-action>
|
||||||
|
</v-list-item>
|
||||||
<v-list-item
|
<v-list-item
|
||||||
v-if="$listeners && $listeners.remove"
|
v-if="$listeners && $listeners.remove"
|
||||||
:disabled="context.editPermission === false"
|
:disabled="context.editPermission === false"
|
||||||
@@ -165,14 +174,13 @@
|
|||||||
import isDarkColor from '/imports/client/ui/utility/isDarkColor.js';
|
import isDarkColor from '/imports/client/ui/utility/isDarkColor.js';
|
||||||
import PropertyIcon from '/imports/client/ui/properties/shared/PropertyIcon.vue';
|
import PropertyIcon from '/imports/client/ui/properties/shared/PropertyIcon.vue';
|
||||||
import { getPropertyName } from '/imports/constants/PROPERTIES.js';
|
import { getPropertyName } from '/imports/constants/PROPERTIES.js';
|
||||||
import ColorPicker from '/imports/client/ui/components/ColorPicker.vue';
|
|
||||||
import getThemeColor from '/imports/client/ui/utility/getThemeColor.js';
|
import getThemeColor from '/imports/client/ui/utility/getThemeColor.js';
|
||||||
import PROPERTIES from '/imports/constants/PROPERTIES.js';
|
import PROPERTIES from '/imports/constants/PROPERTIES.js';
|
||||||
|
import { assertUserHasPaidBenefits } from '/imports/api/users/patreon/tiers.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
PropertyIcon,
|
PropertyIcon,
|
||||||
ColorPicker,
|
|
||||||
},
|
},
|
||||||
inject: {
|
inject: {
|
||||||
context: { default: {} }
|
context: { default: {} }
|
||||||
@@ -211,6 +219,16 @@ export default {
|
|||||||
return propDef && propDef.docsPath;
|
return propDef && propDef.docsPath;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
meteor: {
|
||||||
|
userPaid() {
|
||||||
|
try {
|
||||||
|
assertUserHasPaidBenefits(Meteor.user())
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
colorChanged(value){
|
colorChanged(value){
|
||||||
this.$emit('color-changed', value);
|
this.$emit('color-changed', value);
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
<v-snackbar
|
<v-snackbar
|
||||||
bottom
|
bottom
|
||||||
left
|
left
|
||||||
|
outlined
|
||||||
|
color="accent"
|
||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
:value="isShown"
|
:value="isShown"
|
||||||
:timeout="timeout"
|
:timeout="timeout"
|
||||||
@@ -50,7 +52,7 @@ export default {
|
|||||||
props: {
|
props: {
|
||||||
timeout: {
|
timeout: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 6000,
|
default: 6000000,
|
||||||
},
|
},
|
||||||
pause: {
|
pause: {
|
||||||
type: Number,
|
type: Number,
|
||||||
|
|||||||
@@ -2,18 +2,21 @@
|
|||||||
<div class="creature-form">
|
<div class="creature-form">
|
||||||
<text-field
|
<text-field
|
||||||
label="Name"
|
label="Name"
|
||||||
|
:disabled="!editPermission"
|
||||||
:value="model.name"
|
:value="model.name"
|
||||||
:error-messages="errors.name"
|
:error-messages="errors.name"
|
||||||
@change="(value, ack) => $emit('change', {path: ['name'], value, ack})"
|
@change="(value, ack) => $emit('change', {path: ['name'], value, ack})"
|
||||||
/>
|
/>
|
||||||
<text-field
|
<text-field
|
||||||
label="Alignment"
|
label="Alignment"
|
||||||
|
:disabled="!editPermission"
|
||||||
:value="model.alignment"
|
:value="model.alignment"
|
||||||
:error-messages="errors.alignment"
|
:error-messages="errors.alignment"
|
||||||
@change="(value, ack) => $emit('change', {path: ['alignment'], value, ack})"
|
@change="(value, ack) => $emit('change', {path: ['alignment'], value, ack})"
|
||||||
/>
|
/>
|
||||||
<text-field
|
<text-field
|
||||||
label="Gender"
|
label="Gender"
|
||||||
|
:disabled="!editPermission"
|
||||||
:value="model.gender"
|
:value="model.gender"
|
||||||
:error-messages="errors.gender"
|
:error-messages="errors.gender"
|
||||||
@change="(value, ack) => $emit('change', {path: ['gender'], value, ack})"
|
@change="(value, ack) => $emit('change', {path: ['gender'], value, ack})"
|
||||||
@@ -21,6 +24,7 @@
|
|||||||
<text-field
|
<text-field
|
||||||
label="Picture URL"
|
label="Picture URL"
|
||||||
hint="A link to a high resolution image"
|
hint="A link to a high resolution image"
|
||||||
|
:disabled="!editPermission"
|
||||||
:value="model.picture"
|
:value="model.picture"
|
||||||
:error-messages="errors.picture"
|
:error-messages="errors.picture"
|
||||||
@change="(value, ack) => $emit('change', {path: ['picture'], value, ack})"
|
@change="(value, ack) => $emit('change', {path: ['picture'], value, ack})"
|
||||||
@@ -28,6 +32,7 @@
|
|||||||
<text-field
|
<text-field
|
||||||
label="Avatar picture URL"
|
label="Avatar picture URL"
|
||||||
hint="A link to a smaller, square image to use as an avatar"
|
hint="A link to a smaller, square image to use as an avatar"
|
||||||
|
:disabled="!editPermission"
|
||||||
:value="model.avatarPicture"
|
:value="model.avatarPicture"
|
||||||
:error-messages="errors.avatarPicture"
|
:error-messages="errors.avatarPicture"
|
||||||
@change="(value, ack) => $emit('change', {path: ['avatarPicture'], value, ack})"
|
@change="(value, ack) => $emit('change', {path: ['avatarPicture'], value, ack})"
|
||||||
@@ -36,21 +41,25 @@
|
|||||||
<form-section name="Settings">
|
<form-section name="Settings">
|
||||||
<v-switch
|
<v-switch
|
||||||
label="Hide redundant stats"
|
label="Hide redundant stats"
|
||||||
|
:disabled="!editPermission"
|
||||||
:input-value="model.settings.hideUnusedStats"
|
:input-value="model.settings.hideUnusedStats"
|
||||||
@change="value => $emit('change', {path: ['settings','hideUnusedStats'], value: !!value})"
|
@change="value => $emit('change', {path: ['settings','hideUnusedStats'], value: !!value})"
|
||||||
/>
|
/>
|
||||||
<v-switch
|
<v-switch
|
||||||
label="Hide rest buttons"
|
label="Hide rest buttons"
|
||||||
|
:disabled="!editPermission"
|
||||||
:input-value="model.settings.hideRestButtons"
|
:input-value="model.settings.hideRestButtons"
|
||||||
@change="value => $emit('change', {path: ['settings','hideRestButtons'], value: !!value})"
|
@change="value => $emit('change', {path: ['settings','hideRestButtons'], value: !!value})"
|
||||||
/>
|
/>
|
||||||
<v-switch
|
<v-switch
|
||||||
label="Show spells tab"
|
label="Show spells tab"
|
||||||
|
:disabled="!editPermission"
|
||||||
:input-value="!model.settings.hideSpellsTab"
|
:input-value="!model.settings.hideSpellsTab"
|
||||||
@change="changeHideSpellsTab"
|
@change="changeHideSpellsTab"
|
||||||
/>
|
/>
|
||||||
<v-switch
|
<v-switch
|
||||||
label="Show tree tab"
|
label="Show tree tab"
|
||||||
|
:disabled="!editPermission"
|
||||||
:input-value="model.settings.showTreeTab"
|
:input-value="model.settings.showTreeTab"
|
||||||
@change="changeShowTreeTab"
|
@change="changeShowTreeTab"
|
||||||
/>
|
/>
|
||||||
@@ -62,6 +71,7 @@
|
|||||||
min="0"
|
min="0"
|
||||||
max="1"
|
max="1"
|
||||||
step="0.1"
|
step="0.1"
|
||||||
|
:disabled="!editPermission"
|
||||||
:value="model.settings.hitDiceResetMultiplier"
|
:value="model.settings.hitDiceResetMultiplier"
|
||||||
@change="(value, ack) => $emit('change', {path: ['settings','hitDiceResetMultiplier'], value, ack})"
|
@change="(value, ack) => $emit('change', {path: ['settings','hitDiceResetMultiplier'], value, ack})"
|
||||||
/>
|
/>
|
||||||
@@ -69,6 +79,7 @@
|
|||||||
label="Discord Webhook URL"
|
label="Discord Webhook URL"
|
||||||
hint="This creature's logs will be posted to the discord channel"
|
hint="This creature's logs will be posted to the discord channel"
|
||||||
placeholder="https://discordapp.com/api/webhooks/<id>/<token>"
|
placeholder="https://discordapp.com/api/webhooks/<id>/<token>"
|
||||||
|
:disabled="!editPermission"
|
||||||
:value="model.settings.discordWebhook"
|
:value="model.settings.discordWebhook"
|
||||||
@change="(value, ack) => $emit('change', {path: ['settings','discordWebhook'], value, ack})"
|
@change="(value, ack) => $emit('change', {path: ['settings','discordWebhook'], value, ack})"
|
||||||
/>
|
/>
|
||||||
@@ -96,12 +107,13 @@
|
|||||||
<form-section name="Libraries">
|
<form-section name="Libraries">
|
||||||
<smart-switch
|
<smart-switch
|
||||||
label="All user libraries"
|
label="All user libraries"
|
||||||
|
:disabled="!editPermission"
|
||||||
:value="allUserLibraries"
|
:value="allUserLibraries"
|
||||||
@change="allUserLibrariesChange"
|
@change="allUserLibrariesChange"
|
||||||
/>
|
/>
|
||||||
<library-list
|
<library-list
|
||||||
selection
|
selection
|
||||||
:disabled="!model.allowedLibraries && !model.allowedLibraryCollections"
|
:disabled="!editPermission || (!model.allowedLibraries && !model.allowedLibraryCollections)"
|
||||||
:libraries-selected="model.allowedLibraries"
|
:libraries-selected="model.allowedLibraries"
|
||||||
:library-collections-selected="model.allowedLibraryCollections"
|
:library-collections-selected="model.allowedLibraryCollections"
|
||||||
:libraries-selected-by-collections="librariesSelectedByCollections"
|
:libraries-selected-by-collections="librariesSelectedByCollections"
|
||||||
@@ -120,6 +132,18 @@
|
|||||||
{{ libraryWriteError }}
|
{{ libraryWriteError }}
|
||||||
</p>
|
</p>
|
||||||
</form-section>
|
</form-section>
|
||||||
|
<form-section name="Debug">
|
||||||
|
<v-btn
|
||||||
|
data-id="dependency-graph-button"
|
||||||
|
text
|
||||||
|
@click="showDependencyGraph"
|
||||||
|
>
|
||||||
|
<v-icon left>
|
||||||
|
mdi-graph
|
||||||
|
</v-icon>
|
||||||
|
Dependency Graph
|
||||||
|
</v-btn>
|
||||||
|
</form-section>
|
||||||
</form-sections>
|
</form-sections>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -130,6 +154,7 @@ import FormSection, { FormSections } from '/imports/client/ui/properties/forms/s
|
|||||||
import LibraryList from '/imports/client/ui/library/LibraryList.vue';
|
import LibraryList from '/imports/client/ui/library/LibraryList.vue';
|
||||||
import LibraryCollections from '/imports/api/library/LibraryCollections.js';
|
import LibraryCollections from '/imports/api/library/LibraryCollections.js';
|
||||||
import { changeAllowedLibraries, toggleAllUserLibraries } from '/imports/api/creature/creatures/methods/changeAllowedLibraries.js';
|
import { changeAllowedLibraries, toggleAllUserLibraries } from '/imports/api/creature/creatures/methods/changeAllowedLibraries.js';
|
||||||
|
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@@ -215,33 +240,41 @@ export default {
|
|||||||
});
|
});
|
||||||
return ids;
|
return ids;
|
||||||
},
|
},
|
||||||
|
editPermission() {
|
||||||
|
try {
|
||||||
|
assertEditPermission(this.model, Meteor.userId());
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
changeShowTreeTab(value) {
|
changeShowTreeTab(value) {
|
||||||
|
let currentTab = this.$store.getters.tabNameById(this.model._id);
|
||||||
|
if (!value && currentTab === 'tree') {
|
||||||
|
this.$store.commit(
|
||||||
|
'setTabForCharacterSheet',
|
||||||
|
{ id: this.model._id, tab: 'build' }
|
||||||
|
);
|
||||||
|
}
|
||||||
this.$emit('change', {
|
this.$emit('change', {
|
||||||
path: ['settings', 'showTreeTab'],
|
path: ['settings', 'showTreeTab'],
|
||||||
value: !!value
|
value: !!value
|
||||||
});
|
});
|
||||||
let currentTab = this.$store.getters.tabById(this.model._id);
|
|
||||||
if (!value && currentTab === 5) {
|
|
||||||
this.$store.commit(
|
|
||||||
'setTabForCharacterSheet',
|
|
||||||
{ id: this.model._id, tab: 4 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
changeHideSpellsTab(value) {
|
changeHideSpellsTab(value) {
|
||||||
|
let currentTab = this.$store.getters.tabNameById(this.model._id);
|
||||||
|
if (!value && currentTab === 'spells') {
|
||||||
|
this.$store.commit(
|
||||||
|
'setTabForCharacterSheet',
|
||||||
|
{ id: this.model._id, tab: 'actions' }
|
||||||
|
);
|
||||||
|
}
|
||||||
this.$emit('change', {
|
this.$emit('change', {
|
||||||
path: ['settings', 'hideSpellsTab'],
|
path: ['settings', 'hideSpellsTab'],
|
||||||
value: !value
|
value: !value
|
||||||
});
|
});
|
||||||
let currentTab = this.$store.getters.tabById(this.model._id);
|
|
||||||
if (!value && currentTab === 3) {
|
|
||||||
this.$store.commit(
|
|
||||||
'setTabForCharacterSheet',
|
|
||||||
{ id: this.model._id, tab: 4 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
allUserLibrariesChange(value, ack) {
|
allUserLibrariesChange(value, ack) {
|
||||||
toggleAllUserLibraries.call({
|
toggleAllUserLibraries.call({
|
||||||
@@ -267,6 +300,15 @@ export default {
|
|||||||
this.dirty = true;
|
this.dirty = true;
|
||||||
this.updateAllowedLibraryCollections();
|
this.updateAllowedLibraryCollections();
|
||||||
},
|
},
|
||||||
|
showDependencyGraph() {
|
||||||
|
this.$store.commit('pushDialogStack', {
|
||||||
|
component: 'dependency-graph-dialog',
|
||||||
|
elementId: 'dependency-graph-button',
|
||||||
|
data: {
|
||||||
|
creatureId: this.model._id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
:class="{
|
:class="{
|
||||||
'empty': !hasChildren,
|
'empty': !hasChildren,
|
||||||
}"
|
}"
|
||||||
:data-id="`build-tree-node-${node._id}`"
|
:data-id="`tree-node-${node._id}`"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="layout align-center justify-start tree-node-title"
|
class="layout align-center justify-start tree-node-title"
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
{{ node.name }}
|
{{ node.name }}
|
||||||
</span>
|
</span>
|
||||||
<fill-slot-button
|
<fill-slot-button
|
||||||
v-if="(node.quantityExpected && node.quantityExpected.value === 1) && node.spaceLeft === 1"
|
v-if="canFillWithOne"
|
||||||
:model="node"
|
:model="node"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -93,6 +93,7 @@
|
|||||||
v-if="showExpanded"
|
v-if="showExpanded"
|
||||||
:children="computedChildren"
|
:children="computedChildren"
|
||||||
:parent-slot-id="computedSlotId"
|
:parent-slot-id="computedSlotId"
|
||||||
|
:depth="depth"
|
||||||
@selected="e => $emit('selected', e)"
|
@selected="e => $emit('selected', e)"
|
||||||
/>
|
/>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
@@ -141,6 +142,10 @@ export default {
|
|||||||
context: { default: {} }
|
context: { default: {} }
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
depth: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
node: {
|
node: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
@@ -155,7 +160,7 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
data(){return {
|
data(){return {
|
||||||
expanded: false,
|
expanded: this.depth <= 2,
|
||||||
/* expand if there's a slot needing attention:
|
/* expand if there's a slot needing attention:
|
||||||
this.node._descendantCanFill || (
|
this.node._descendantCanFill || (
|
||||||
this.node.type === 'propertySlot' &&
|
this.node.type === 'propertySlot' &&
|
||||||
@@ -170,7 +175,8 @@ export default {
|
|||||||
this.children.length === 1 &&
|
this.children.length === 1 &&
|
||||||
this.children[0].node.type !== 'propertySlot' &&
|
this.children[0].node.type !== 'propertySlot' &&
|
||||||
this.node.quantityExpected &&
|
this.node.quantityExpected &&
|
||||||
this.node.quantityExpected.value === 1;
|
this.node.quantityExpected.value === 1 &&
|
||||||
|
!this.canFill;
|
||||||
},
|
},
|
||||||
isSlot(){
|
isSlot(){
|
||||||
return this.node.type === 'propertySlot';
|
return this.node.type === 'propertySlot';
|
||||||
@@ -180,15 +186,18 @@ export default {
|
|||||||
},
|
},
|
||||||
canFillWithOne(){
|
canFillWithOne(){
|
||||||
return this.isSlot &&
|
return this.isSlot &&
|
||||||
this.node.quantityExpected &&
|
this.canFill &&
|
||||||
|
this.node.quantityExpected &&
|
||||||
this.node.quantityExpected.value === 1 &&
|
this.node.quantityExpected.value === 1 &&
|
||||||
this.node.spaceLeft === 1
|
this.node.spaceLeft === 1 &&
|
||||||
|
!this.children?.length;
|
||||||
},
|
},
|
||||||
canFillWithMany(){
|
canFillWithMany(){
|
||||||
return this.isSlot && (
|
return this.isSlot && this.canFill && (
|
||||||
!this.node.quantityExpected ||
|
!this.node.quantityExpected ||
|
||||||
this.node.quantityExpected.value === 0 ||
|
this.node.quantityExpected.value === 0 ||
|
||||||
(this.node.quantityExpected.value > 1 && this.node.spaceLeft > 0)
|
(this.node.quantityExpected.value > 1 && this.node.spaceLeft > 0) ||
|
||||||
|
(this.node.quantityExpected.value === 1 && this.children?.length)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
hasChildren(){
|
hasChildren(){
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
:node="child.node"
|
:node="child.node"
|
||||||
:children="child.children"
|
:children="child.children"
|
||||||
:parent-slot-id="parentSlotId"
|
:parent-slot-id="parentSlotId"
|
||||||
|
:depth="depth + 1"
|
||||||
@selected="e => $emit('selected', e)"
|
@selected="e => $emit('selected', e)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -27,6 +28,10 @@ export default {
|
|||||||
type: String,
|
type: String,
|
||||||
default: undefined,
|
default: undefined,
|
||||||
},
|
},
|
||||||
|
depth: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -195,7 +195,7 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
this.$store.commit(
|
this.$store.commit(
|
||||||
'setTabForCharacterSheet',
|
'setTabForCharacterSheet',
|
||||||
{id: creatureId, tab: 5}
|
{id: creatureId, tab: 'build'}
|
||||||
);
|
);
|
||||||
this.$emit('pop', creatureId);
|
this.$emit('pop', creatureId);
|
||||||
defer(() => {
|
defer(() => {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
</v-toolbar-title>
|
</v-toolbar-title>
|
||||||
<div>
|
<div>
|
||||||
<p v-if="name">
|
<p v-if="name">
|
||||||
Type "{{ name }}" to permanenetly delete the character
|
Type "{{ name }}" to permanently delete the character
|
||||||
</p>
|
</p>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-if="name"
|
v-if="name"
|
||||||
|
|||||||
@@ -47,9 +47,6 @@
|
|||||||
<v-tab-item>
|
<v-tab-item>
|
||||||
<stats-tab :creature-id="creatureId" />
|
<stats-tab :creature-id="creatureId" />
|
||||||
</v-tab-item>
|
</v-tab-item>
|
||||||
<v-tab-item>
|
|
||||||
<features-tab :creature-id="creatureId" />
|
|
||||||
</v-tab-item>
|
|
||||||
<v-tab-item>
|
<v-tab-item>
|
||||||
<actions-tab :creature-id="creatureId" />
|
<actions-tab :creature-id="creatureId" />
|
||||||
</v-tab-item>
|
</v-tab-item>
|
||||||
@@ -59,6 +56,9 @@
|
|||||||
<v-tab-item>
|
<v-tab-item>
|
||||||
<inventory-tab :creature-id="creatureId" />
|
<inventory-tab :creature-id="creatureId" />
|
||||||
</v-tab-item>
|
</v-tab-item>
|
||||||
|
<v-tab-item>
|
||||||
|
<features-tab :creature-id="creatureId" />
|
||||||
|
</v-tab-item>
|
||||||
<v-tab-item>
|
<v-tab-item>
|
||||||
<character-tab :creature-id="creatureId" />
|
<character-tab :creature-id="creatureId" />
|
||||||
</v-tab-item>
|
</v-tab-item>
|
||||||
@@ -96,22 +96,22 @@
|
|||||||
<span>Stats</span>
|
<span>Stats</span>
|
||||||
<v-icon>mdi-chart-box</v-icon>
|
<v-icon>mdi-chart-box</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn>
|
|
||||||
<span>Features</span>
|
|
||||||
<v-icon>mdi-text</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
<v-btn>
|
<v-btn>
|
||||||
<span>Actions</span>
|
<span>Actions</span>
|
||||||
<v-icon>mdi-lightning-bolt</v-icon>
|
<v-icon>mdi-lightning-bolt</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn>
|
<v-btn v-if="!creature.settings.hideSpellsTab">
|
||||||
<span v-if="!creature.settings.hideSpellsTab">Spells</span>
|
<span>Spells</span>
|
||||||
<v-icon>mdi-fire</v-icon>
|
<v-icon>mdi-fire</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn>
|
<v-btn>
|
||||||
<span>Inventory</span>
|
<span>Inventory</span>
|
||||||
<v-icon>mdi-cube</v-icon>
|
<v-icon>mdi-cube</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
<v-btn>
|
||||||
|
<span>Features</span>
|
||||||
|
<v-icon>mdi-text</v-icon>
|
||||||
|
</v-btn>
|
||||||
<v-btn>
|
<v-btn>
|
||||||
<span>Journal</span>
|
<span>Journal</span>
|
||||||
<v-icon>mdi-book-open-variant</v-icon>
|
<v-icon>mdi-book-open-variant</v-icon>
|
||||||
|
|||||||
@@ -153,12 +153,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.$store.commit('pushDialogStack', {
|
this.$store.commit('pushDialogStack', {
|
||||||
component: 'add-creature-property-dialog',
|
component: 'insert-property-dialog',
|
||||||
elementId: 'insert-creature-property-type-' + forcedType,
|
elementId: 'insert-creature-property-type-' + forcedType,
|
||||||
data: {
|
data: {
|
||||||
parentDoc: forcedType ? undefined : parent,
|
parentDoc: forcedType ? undefined : parent,
|
||||||
forcedType,
|
forcedType,
|
||||||
creatureId: this.creatureId,
|
creatureId: this.creatureId,
|
||||||
|
noBackdropClose: true,
|
||||||
},
|
},
|
||||||
callback(result){
|
callback(result){
|
||||||
if (!result){
|
if (!result){
|
||||||
|
|||||||
@@ -29,37 +29,78 @@
|
|||||||
bottom
|
bottom
|
||||||
left
|
left
|
||||||
transition="slide-y-transition"
|
transition="slide-y-transition"
|
||||||
data-id="creature-menu"
|
|
||||||
>
|
>
|
||||||
<template #activator="{ on }">
|
<template #activator="{ on }">
|
||||||
<v-btn
|
<v-btn
|
||||||
|
data-id="creature-menu"
|
||||||
icon
|
icon
|
||||||
v-on="on"
|
v-on="on"
|
||||||
>
|
>
|
||||||
<v-icon>mdi-dots-vertical</v-icon>
|
<v-icon>mdi-dots-vertical</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</template>
|
</template>
|
||||||
<v-list v-if="editPermission">
|
<v-list>
|
||||||
<v-list-item @click="deleteCharacter">
|
<v-list-item
|
||||||
|
v-if="!isOwner && ownerName"
|
||||||
|
two-line
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
<v-list-item-avatar>
|
||||||
|
<v-icon>
|
||||||
|
mdi-account
|
||||||
|
</v-icon>
|
||||||
|
</v-list-item-avatar>
|
||||||
|
<v-list-item-content>
|
||||||
|
<v-list-item-title>
|
||||||
|
{{ ownerName }}
|
||||||
|
</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>
|
||||||
|
Sheet owner
|
||||||
|
</v-list-item-subtitle>
|
||||||
|
</v-list-item-content>
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item
|
||||||
|
v-if="!isOwner"
|
||||||
|
@click="unshareWithMe"
|
||||||
|
>
|
||||||
<v-list-item-title>
|
<v-list-item-title>
|
||||||
<v-icon>mdi-delete</v-icon> Delete
|
<v-icon left>
|
||||||
|
mdi-cancel
|
||||||
|
</v-icon> Unshare with me
|
||||||
|
</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item :to="printUrl">
|
||||||
|
<v-list-item-title>
|
||||||
|
<v-icon left>
|
||||||
|
mdi-printer
|
||||||
|
</v-icon> Print
|
||||||
</v-list-item-title>
|
</v-list-item-title>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
<v-list-item @click="showCharacterForm">
|
<v-list-item @click="showCharacterForm">
|
||||||
<v-list-item-title>
|
<v-list-item-title>
|
||||||
<v-icon>mdi-pencil</v-icon> Edit details
|
<v-icon left>
|
||||||
|
mdi-pencil
|
||||||
|
</v-icon> Edit details
|
||||||
</v-list-item-title>
|
</v-list-item-title>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
<v-list-item @click="showShareDialog">
|
<v-list-item
|
||||||
|
:disabled="!isOwner"
|
||||||
|
@click="showShareDialog"
|
||||||
|
>
|
||||||
<v-list-item-title>
|
<v-list-item-title>
|
||||||
<v-icon>mdi-share-variant</v-icon> Sharing
|
<v-icon left>
|
||||||
|
mdi-share-variant
|
||||||
|
</v-icon> Sharing
|
||||||
</v-list-item-title>
|
</v-list-item-title>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
</v-list>
|
<v-list-item
|
||||||
<v-list v-else>
|
:disabled="!isOwner"
|
||||||
<v-list-item @click="unshareWithMe">
|
@click="deleteCharacter"
|
||||||
|
>
|
||||||
<v-list-item-title>
|
<v-list-item-title>
|
||||||
<v-icon>mdi-delete</v-icon> Unshare with me
|
<v-icon left>
|
||||||
|
mdi-delete
|
||||||
|
</v-icon> Delete
|
||||||
</v-list-item-title>
|
</v-list-item-title>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
</v-list>
|
</v-list>
|
||||||
@@ -101,9 +142,6 @@
|
|||||||
<v-tab>
|
<v-tab>
|
||||||
Stats
|
Stats
|
||||||
</v-tab>
|
</v-tab>
|
||||||
<v-tab>
|
|
||||||
Features
|
|
||||||
</v-tab>
|
|
||||||
<v-tab>
|
<v-tab>
|
||||||
Actions
|
Actions
|
||||||
</v-tab>
|
</v-tab>
|
||||||
@@ -113,6 +151,9 @@
|
|||||||
<v-tab>
|
<v-tab>
|
||||||
Inventory
|
Inventory
|
||||||
</v-tab>
|
</v-tab>
|
||||||
|
<v-tab>
|
||||||
|
Features
|
||||||
|
</v-tab>
|
||||||
<v-tab>
|
<v-tab>
|
||||||
Journal
|
Journal
|
||||||
</v-tab>
|
</v-tab>
|
||||||
@@ -144,6 +185,7 @@ import isDarkColor from '/imports/client/ui/utility/isDarkColor.js';
|
|||||||
import CharacterSheetFab from '/imports/client/ui/creature/character/CharacterSheetFab.vue';
|
import CharacterSheetFab from '/imports/client/ui/creature/character/CharacterSheetFab.vue';
|
||||||
import getThemeColor from '/imports/client/ui/utility/getThemeColor.js';
|
import getThemeColor from '/imports/client/ui/utility/getThemeColor.js';
|
||||||
import SharedIcon from '/imports/client/ui/components/SharedIcon.vue';
|
import SharedIcon from '/imports/client/ui/components/SharedIcon.vue';
|
||||||
|
import getCreatureUrlName from '/imports/api/creature/creatures/getCreatureUrlName.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@@ -167,6 +209,9 @@ export default {
|
|||||||
isDark() {
|
isDark() {
|
||||||
return isDarkColor(this.toolbarColor);
|
return isDarkColor(this.toolbarColor);
|
||||||
},
|
},
|
||||||
|
printUrl() {
|
||||||
|
return `/print-character/${this.creature._id}/${getCreatureUrlName(this.creature)}`;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapMutations([
|
...mapMutations([
|
||||||
@@ -244,6 +289,15 @@ export default {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
isOwner() {
|
||||||
|
if (!this.creature) return;
|
||||||
|
return Meteor.userId() === this.creature.owner;
|
||||||
|
},
|
||||||
|
ownerName() {
|
||||||
|
if (!this.creature) return;
|
||||||
|
const username = Meteor.users.findOne(this.creature.owner)?.username;
|
||||||
|
return username;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -150,11 +150,12 @@ export default {
|
|||||||
addProperty(){
|
addProperty(){
|
||||||
let parentPropertyId = this._id;
|
let parentPropertyId = this._id;
|
||||||
this.$store.commit('pushDialogStack', {
|
this.$store.commit('pushDialogStack', {
|
||||||
component: 'add-creature-property-dialog',
|
component: 'insert-property-dialog',
|
||||||
elementId: 'insert-creature-property-btn',
|
elementId: 'insert-creature-property-btn',
|
||||||
data: {
|
data: {
|
||||||
parentDoc: this.creature,
|
parentDoc: this.creature,
|
||||||
creatureId: this._id,
|
creatureId: this._id,
|
||||||
|
noBackdropClose: true,
|
||||||
},
|
},
|
||||||
callback(result){
|
callback(result){
|
||||||
if (!result) return;
|
if (!result) return;
|
||||||
|
|||||||
@@ -58,8 +58,22 @@ export default {
|
|||||||
}},
|
}},
|
||||||
meteor: {
|
meteor: {
|
||||||
actions() {
|
actions() {
|
||||||
return CreatureProperties.find({
|
const folderIds = CreatureProperties.find({
|
||||||
'ancestors.id': this.creatureId,
|
'ancestors.id': this.creatureId,
|
||||||
|
type: 'folder',
|
||||||
|
groupStats: true,
|
||||||
|
hideStatsGroup: true,
|
||||||
|
removed: { $ne: true },
|
||||||
|
inactive: { $ne: true },
|
||||||
|
}, { fields: { _id: 1 } }).map(folder => folder._id);
|
||||||
|
|
||||||
|
return CreatureProperties.find({
|
||||||
|
'ancestors.id': {
|
||||||
|
$eq: this.creatureId,
|
||||||
|
},
|
||||||
|
'parent.id': {
|
||||||
|
$nin: folderIds,
|
||||||
|
},
|
||||||
type: 'action',
|
type: 'action',
|
||||||
actionType: { $ne: 'event' },
|
actionType: { $ne: 'event' },
|
||||||
removed: { $ne: true },
|
removed: { $ne: true },
|
||||||
|
|||||||
@@ -86,7 +86,7 @@
|
|||||||
<build-tree-node-list
|
<build-tree-node-list
|
||||||
:children="slotBuildTree"
|
:children="slotBuildTree"
|
||||||
class="mx-2"
|
class="mx-2"
|
||||||
@selected="_id => propertyClicked({_id, prefix: 'build-tree-node-'})"
|
@selected="_id => propertyClicked({_id, prefix: 'tree-node-'})"
|
||||||
/>
|
/>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-col>
|
</v-col>
|
||||||
|
|||||||
@@ -56,8 +56,22 @@ export default {
|
|||||||
},
|
},
|
||||||
meteor: {
|
meteor: {
|
||||||
features() {
|
features() {
|
||||||
return CreatureProperties.find({
|
const folderIds = CreatureProperties.find({
|
||||||
'ancestors.id': this.creatureId,
|
'ancestors.id': this.creatureId,
|
||||||
|
type: 'folder',
|
||||||
|
groupStats: true,
|
||||||
|
hideStatsGroup: true,
|
||||||
|
removed: { $ne: true },
|
||||||
|
inactive: { $ne: true },
|
||||||
|
}, { fields: { _id: 1 } }).map(folder => folder._id);
|
||||||
|
|
||||||
|
return CreatureProperties.find({
|
||||||
|
'ancestors.id': {
|
||||||
|
$eq: this.creatureId,
|
||||||
|
},
|
||||||
|
'parent.id': {
|
||||||
|
$nin: folderIds,
|
||||||
|
},
|
||||||
type: 'feature',
|
type: 'feature',
|
||||||
removed: { $ne: true },
|
removed: { $ne: true },
|
||||||
inactive: { $ne: true },
|
inactive: { $ne: true },
|
||||||
|
|||||||
@@ -141,9 +141,24 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
meteor: {
|
meteor: {
|
||||||
containers() {
|
folderIds() {
|
||||||
return CreatureProperties.find({
|
return CreatureProperties.find({
|
||||||
'ancestors.id': this.creatureId,
|
'ancestors.id': this.creatureId,
|
||||||
|
type: 'folder',
|
||||||
|
groupStats: true,
|
||||||
|
hideStatsGroup: true,
|
||||||
|
removed: { $ne: true },
|
||||||
|
inactive: { $ne: true },
|
||||||
|
}, { fields: { _id: 1 } }).map(folder => folder._id);
|
||||||
|
},
|
||||||
|
containers() {
|
||||||
|
return CreatureProperties.find({
|
||||||
|
'ancestors.id': {
|
||||||
|
$eq: this.creatureId,
|
||||||
|
},
|
||||||
|
'parent.id': {
|
||||||
|
$nin: this.folderIds,
|
||||||
|
},
|
||||||
type: 'container',
|
type: 'container',
|
||||||
removed: { $ne: true },
|
removed: { $ne: true },
|
||||||
inactive: { $ne: true },
|
inactive: { $ne: true },
|
||||||
@@ -166,7 +181,10 @@ export default {
|
|||||||
return CreatureProperties.find({
|
return CreatureProperties.find({
|
||||||
'ancestors.id': {
|
'ancestors.id': {
|
||||||
$eq: this.creatureId,
|
$eq: this.creatureId,
|
||||||
$nin: this.containerIds
|
$nin: this.containerIds,
|
||||||
|
},
|
||||||
|
'parent.id': {
|
||||||
|
$nin: this.folderIds,
|
||||||
},
|
},
|
||||||
type: 'container',
|
type: 'container',
|
||||||
removed: { $ne: true },
|
removed: { $ne: true },
|
||||||
@@ -179,12 +197,16 @@ export default {
|
|||||||
return CreatureProperties.find({
|
return CreatureProperties.find({
|
||||||
'ancestors.id': {
|
'ancestors.id': {
|
||||||
$eq: this.creatureId,
|
$eq: this.creatureId,
|
||||||
$nin: this.containerIds
|
$nin: this.containerIds,
|
||||||
|
},
|
||||||
|
'parent.id': {
|
||||||
|
$nin: this.folderIds,
|
||||||
},
|
},
|
||||||
type: 'item',
|
type: 'item',
|
||||||
equipped: { $ne: true },
|
equipped: { $ne: true },
|
||||||
removed: { $ne: true },
|
removed: { $ne: true },
|
||||||
deactivatedByAncestor: { $ne: true },
|
deactivatedByAncestor: { $ne: true },
|
||||||
|
deactivatedByToggle: { $ne: true },
|
||||||
}, {
|
}, {
|
||||||
sort: { order: 1 },
|
sort: { order: 1 },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -60,8 +60,22 @@ export default {
|
|||||||
},
|
},
|
||||||
meteor: {
|
meteor: {
|
||||||
notes(){
|
notes(){
|
||||||
return CreatureProperties.find({
|
const folderIds = CreatureProperties.find({
|
||||||
'ancestors.id': this.creatureId,
|
'ancestors.id': this.creatureId,
|
||||||
|
type: 'folder',
|
||||||
|
groupStats: true,
|
||||||
|
hideStatsGroup: true,
|
||||||
|
removed: { $ne: true },
|
||||||
|
inactive: { $ne: true },
|
||||||
|
}, { fields: { _id: 1 } }).map(folder => folder._id);
|
||||||
|
|
||||||
|
return CreatureProperties.find({
|
||||||
|
'ancestors.id': {
|
||||||
|
$eq: this.creatureId,
|
||||||
|
},
|
||||||
|
'parent.id': {
|
||||||
|
$nin: folderIds,
|
||||||
|
},
|
||||||
type: 'note',
|
type: 'note',
|
||||||
removed: {$ne: true},
|
removed: {$ne: true},
|
||||||
inactive: {$ne: true},
|
inactive: {$ne: true},
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
@remove="softRemove"
|
@remove="softRemove"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="spellSlots && spellSlots.length || hasSpells"
|
v-if="hasSpellSlots || hasSpells"
|
||||||
class="spell-slots"
|
class="spell-slots"
|
||||||
>
|
>
|
||||||
<spell-slot-card
|
<spell-slot-card
|
||||||
@@ -77,9 +77,35 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
meteor: {
|
meteor: {
|
||||||
spellSlots() {
|
folderIds() {
|
||||||
return CreatureProperties.find({
|
return CreatureProperties.find({
|
||||||
'ancestors.id': this.creatureId,
|
'ancestors.id': this.creatureId,
|
||||||
|
type: 'folder',
|
||||||
|
groupStats: true,
|
||||||
|
hideStatsGroup: true,
|
||||||
|
removed: { $ne: true },
|
||||||
|
inactive: { $ne: true },
|
||||||
|
}, { fields: { _id: 1 } }).map(folder => folder._id);
|
||||||
|
},
|
||||||
|
hasSpellSlots() {
|
||||||
|
return !!CreatureProperties.findOne({
|
||||||
|
'ancestors.id': this.creatureId,
|
||||||
|
inactive: { $ne: true },
|
||||||
|
removed: { $ne: true },
|
||||||
|
overridden: { $ne: true },
|
||||||
|
level: { $ne: 0 },
|
||||||
|
type: 'attribute',
|
||||||
|
attributeType: 'spellSlot',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
spellSlots() {
|
||||||
|
return CreatureProperties.find({
|
||||||
|
'ancestors.id': {
|
||||||
|
$eq: this.creatureId,
|
||||||
|
},
|
||||||
|
'parent.id': {
|
||||||
|
$nin: this.folderIds,
|
||||||
|
},
|
||||||
inactive: { $ne: true },
|
inactive: { $ne: true },
|
||||||
removed: { $ne: true },
|
removed: { $ne: true },
|
||||||
overridden: { $ne: true },
|
overridden: { $ne: true },
|
||||||
@@ -89,11 +115,18 @@ export default {
|
|||||||
{ hideWhenTotalZero: true, total: 0 },
|
{ hideWhenTotalZero: true, total: 0 },
|
||||||
{ hideWhenValueZero: true, value: 0 },
|
{ hideWhenValueZero: true, value: 0 },
|
||||||
],
|
],
|
||||||
|
}, {
|
||||||
|
sort: { order: 1 }
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
spellLists() {
|
spellLists() {
|
||||||
return CreatureProperties.find({
|
return CreatureProperties.find({
|
||||||
'ancestors.id': this.creatureId,
|
'ancestors.id': {
|
||||||
|
$eq: this.creatureId,
|
||||||
|
},
|
||||||
|
'parent.id': {
|
||||||
|
$nin: this.folderIds,
|
||||||
|
},
|
||||||
type: 'spellList',
|
type: 'spellList',
|
||||||
removed: { $ne: true },
|
removed: { $ne: true },
|
||||||
inactive: { $ne: true },
|
inactive: { $ne: true },
|
||||||
@@ -115,6 +148,9 @@ export default {
|
|||||||
$eq: this.creatureId,
|
$eq: this.creatureId,
|
||||||
$nin: this.spellListIds,
|
$nin: this.spellListIds,
|
||||||
},
|
},
|
||||||
|
'parent.id': {
|
||||||
|
$nin: this.folderIds,
|
||||||
|
},
|
||||||
type: 'spell',
|
type: 'spell',
|
||||||
removed: { $ne: true },
|
removed: { $ne: true },
|
||||||
deactivatedByAncestor: { $ne: true },
|
deactivatedByAncestor: { $ne: true },
|
||||||
@@ -132,6 +168,9 @@ export default {
|
|||||||
$eq: this.creatureId,
|
$eq: this.creatureId,
|
||||||
$nin: this.spellListIds,
|
$nin: this.spellListIds,
|
||||||
},
|
},
|
||||||
|
'parent.id': {
|
||||||
|
$nin: this.folderIds,
|
||||||
|
},
|
||||||
type: 'spellList',
|
type: 'spellList',
|
||||||
removed: { $ne: true },
|
removed: { $ne: true },
|
||||||
inactive: { $ne: true },
|
inactive: { $ne: true },
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user