Compare commits
403 Commits
2.0-beta.3
...
2.0.52
| 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 | ||
|
|
e19e91f7e0 | ||
|
|
bac9fc98dd | ||
|
|
420663c149 | ||
|
|
23d44e54e3 | ||
|
|
881496e9c1 | ||
|
|
002c767d1a | ||
|
|
9aaf31d5cf | ||
|
|
d05cd2fa19 | ||
|
|
f13774df11 | ||
|
|
cc78ba948e | ||
|
|
c6bfb84bb0 | ||
|
|
7e49100d14 | ||
|
|
c3ac49a8c4 | ||
|
|
fd9d525ba9 | ||
|
|
d947b62ba4 | ||
|
|
046509224e | ||
|
|
63bcf023ee | ||
|
|
9741d1d56c | ||
|
|
0f12c98408 | ||
|
|
254390e1c1 | ||
|
|
ad2f43712d | ||
|
|
55f8dac0db | ||
|
|
9f8c3f0f3d | ||
|
|
56bd41f435 | ||
|
|
063d4288ef | ||
|
|
a3355dd988 | ||
|
|
d2649fd66e | ||
|
|
e619734ee1 | ||
|
|
5108b32624 | ||
|
|
a9b389023e | ||
|
|
e06d2befc4 | ||
|
|
04c9c4cfc2 | ||
|
|
cc7dc257fb | ||
|
|
f3deadb3f1 | ||
|
|
dcfb380e57 | ||
|
|
a568cdfb1e | ||
|
|
cea63e6a8e | ||
|
|
b6b0cfbb9b | ||
|
|
428aeef635 | ||
|
|
e3644eb9e8 | ||
|
|
060b5f93ca | ||
|
|
0f3a96da17 | ||
|
|
c55d572134 | ||
|
|
0a2b60990e | ||
|
|
a437ff5aef | ||
|
|
3d31d62860 | ||
|
|
8377231254 | ||
|
|
1ec29365cb | ||
|
|
60b21c1901 | ||
|
|
03f87b0afa | ||
|
|
48291d2c8f | ||
|
|
1cedf55fbf | ||
|
|
bed4d4b162 | ||
|
|
a1d992ec8d | ||
|
|
008ef62517 | ||
|
|
c436309ba8 | ||
|
|
0bfdb73b47 | ||
|
|
a462cc5ca2 | ||
|
|
5d57a74667 | ||
|
|
21b0029df7 | ||
|
|
c0ccafa787 | ||
|
|
d63ad9ea8f | ||
|
|
8f56a60fb1 | ||
|
|
358ae46627 | ||
|
|
cc915410da | ||
|
|
0b1db3c40c | ||
|
|
0ad7e659d2 | ||
|
|
58c3875dc7 | ||
|
|
84f506f1fe | ||
|
|
a5b4b20324 | ||
|
|
d0a3ccc76a | ||
|
|
93ac9215c2 | ||
|
|
a6b501a62c | ||
|
|
e956bacf07 | ||
|
|
60b6b283b1 | ||
|
|
1c9b390551 | ||
|
|
21a487635d | ||
|
|
c92a26d5e6 | ||
|
|
49b514b8f3 | ||
|
|
5cb835c536 | ||
|
|
aa8f2d230d | ||
|
|
2fa913b09a | ||
|
|
de598c70a7 | ||
|
|
baecdeff24 | ||
|
|
d4b7d22b5f | ||
|
|
87f79737e8 | ||
|
|
9f0ffe13f8 | ||
|
|
adaa31d76c | ||
|
|
b051d764f8 | ||
|
|
ffb5b4a4f3 | ||
|
|
fd87b7fb75 | ||
|
|
f035902842 | ||
|
|
dbc5f7253f | ||
|
|
f0e7253374 | ||
|
|
ffe37bf907 | ||
|
|
a63e2099d3 | ||
|
|
0308e4e7a7 | ||
|
|
43f8df09f0 | ||
|
|
b6ed9ffb74 | ||
|
|
a84da7d8a5 | ||
|
|
249aebea0f | ||
|
|
11a527481e | ||
|
|
8d729216b5 | ||
|
|
1677e8c424 | ||
|
|
987aacbb67 | ||
|
|
2714d0b9d5 | ||
|
|
1d98c41168 | ||
|
|
e42ec4b862 | ||
|
|
59fc5ab851 | ||
|
|
5d14c392e8 | ||
|
|
c6ca8c1fa4 | ||
|
|
28307e26c3 | ||
|
|
6d42eb62f0 | ||
|
|
877c9ca099 | ||
|
|
9b652fc133 | ||
|
|
7d66c06107 | ||
|
|
21629138f0 | ||
|
|
59a488256b | ||
|
|
766519b4a3 | ||
|
|
e7f73d0e54 | ||
|
|
193d5eec50 | ||
|
|
9284c9ad76 | ||
|
|
f86152675f | ||
|
|
cbac5264cd | ||
|
|
34e3325464 | ||
|
|
79c9e67ce2 | ||
|
|
4c2aabf90d | ||
|
|
48331d3806 | ||
|
|
45f05d0d34 | ||
|
|
58629c92f4 | ||
|
|
719af548f0 | ||
|
|
f2a1861279 | ||
|
|
38c3b6ff1f | ||
|
|
23e848fe40 | ||
|
|
4d6cdf50bd | ||
|
|
1cf9f3b5fd | ||
|
|
8164b79667 | ||
|
|
360df79004 | ||
|
|
a8f163ff33 | ||
|
|
36b3b80850 | ||
|
|
1d22f4c054 | ||
|
|
99e4e8d6bb | ||
|
|
2bb3265356 | ||
|
|
263f2d8424 | ||
|
|
ee0e764294 | ||
|
|
13fc0c0b12 | ||
|
|
ecfeeaccd9 | ||
|
|
b324fb1f03 | ||
|
|
8d34cc1369 | ||
|
|
839c2488b2 | ||
|
|
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 |
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
build
|
||||
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
|
||||
========
|
||||
|
||||
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.
|
||||
|
||||
|
||||
1
app/.gitignore
vendored
1
app/.gitignore
vendored
@@ -3,6 +3,7 @@
|
||||
.demeteorized
|
||||
.cache
|
||||
.vscode
|
||||
fileStorage
|
||||
settings.json
|
||||
public/components
|
||||
public/_imports.html
|
||||
|
||||
@@ -3,29 +3,30 @@
|
||||
# 'meteor add' and 'meteor remove' will edit this file for you,
|
||||
# but you can also edit it by hand.
|
||||
|
||||
accounts-password@2.3.1
|
||||
random@1.2.0
|
||||
underscore@1.0.10
|
||||
zegenie:redis-oplog
|
||||
accounts-password@2.3.4
|
||||
random@1.2.1
|
||||
underscore@1.0.13
|
||||
dburles:mongo-collection-instances
|
||||
accounts-google@1.4.0
|
||||
email@2.2.1
|
||||
email@2.2.5
|
||||
meteor-base@1.5.1
|
||||
mobile-experience@1.1.0
|
||||
mongo@1.15.0
|
||||
session@1.2.0
|
||||
tracker@1.2.0
|
||||
logging@1.3.1
|
||||
mongo@1.16.6
|
||||
session@1.2.1
|
||||
tracker@1.3.2
|
||||
logging@1.3.2
|
||||
reload@1.3.1
|
||||
ejson@1.1.2
|
||||
check@1.3.1
|
||||
standard-minifier-js@2.8.0
|
||||
ejson@1.1.3
|
||||
check@1.3.2
|
||||
standard-minifier-js@2.8.1
|
||||
shell-server@0.5.0
|
||||
ecmascript@0.16.2
|
||||
ecmascript@0.16.7
|
||||
es5-shim@4.8.0
|
||||
service-configuration@1.3.0
|
||||
dynamic-import@0.7.2
|
||||
ddp-rate-limiter@1.1.0
|
||||
rate-limit@1.0.9
|
||||
service-configuration@1.3.1
|
||||
dynamic-import@0.7.3
|
||||
ddp-rate-limiter@1.2.0
|
||||
rate-limit@1.1.1
|
||||
mdg:validated-method
|
||||
static-html@1.3.2
|
||||
aldeed:collection2
|
||||
@@ -37,9 +38,8 @@ simple:rest
|
||||
simple:rest-method-mixin
|
||||
mikowals:batch-insert
|
||||
peerlibrary:subscription-data
|
||||
seba:minifiers-autoprefixer
|
||||
zer0th:meteor-vuetify-loader
|
||||
akryum:vue-component
|
||||
akryum:vue-component@0.15.2
|
||||
akryum:vue-router2
|
||||
percolate:migrations
|
||||
meteortesting:mocha
|
||||
@@ -47,4 +47,7 @@ ostrio:files
|
||||
simple:rest-bearer-token-parser
|
||||
simple:rest-json-error-handler
|
||||
littledata:synced-cron
|
||||
mdg:meteor-apm-agent
|
||||
#mdg:meteor-apm-agent
|
||||
typescript@4.9.4
|
||||
seba:minifiers-autoprefixer
|
||||
mixmax:smart-disconnect
|
||||
|
||||
@@ -1 +1 @@
|
||||
METEOR@2.7.3
|
||||
METEOR@2.12
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
accounts-base@2.2.3
|
||||
accounts-base@2.2.8
|
||||
accounts-google@1.4.0
|
||||
accounts-oauth@1.4.1
|
||||
accounts-password@2.3.1
|
||||
accounts-oauth@1.4.2
|
||||
accounts-password@2.3.4
|
||||
accounts-patreon@0.1.0
|
||||
akryum:npm-check@0.1.2
|
||||
akryum:vue-component@0.15.2
|
||||
@@ -12,7 +12,7 @@ aldeed:collection2@3.5.0
|
||||
aldeed:schema-index@3.0.0
|
||||
allow-deny@1.1.1
|
||||
autoupdate@1.8.0
|
||||
babel-compiler@7.9.0
|
||||
babel-compiler@7.10.4
|
||||
babel-runtime@1.5.1
|
||||
base64@1.0.12
|
||||
binary-heap@1.0.11
|
||||
@@ -21,28 +21,28 @@ boilerplate-generator@1.7.1
|
||||
bozhao:link-accounts@2.6.1
|
||||
caching-compiler@1.2.2
|
||||
caching-html-compiler@1.2.1
|
||||
callback-hook@1.4.0
|
||||
check@1.3.1
|
||||
callback-hook@1.5.1
|
||||
check@1.3.2
|
||||
coffeescript@2.4.1
|
||||
coffeescript-compiler@2.4.1
|
||||
dburles:mongo-collection-instances@0.3.6
|
||||
ddp@1.4.0
|
||||
ddp-client@2.5.0
|
||||
ddp@1.4.1
|
||||
ddp-client@2.6.1
|
||||
ddp-common@1.4.0
|
||||
ddp-rate-limiter@1.1.0
|
||||
ddp-server@2.5.0
|
||||
diff-sequence@1.1.1
|
||||
dynamic-import@0.7.2
|
||||
ecmascript@0.16.2
|
||||
ecmascript-runtime@0.8.0
|
||||
ddp-rate-limiter@1.2.0
|
||||
ddp-server@2.6.1
|
||||
diff-sequence@1.1.2
|
||||
dynamic-import@0.7.3
|
||||
ecmascript@0.16.7
|
||||
ecmascript-runtime@0.8.1
|
||||
ecmascript-runtime-client@0.12.1
|
||||
ecmascript-runtime-server@0.11.0
|
||||
ejson@1.1.2
|
||||
email@2.2.1
|
||||
ejson@1.1.3
|
||||
email@2.2.5
|
||||
es5-shim@4.8.0
|
||||
fetch@0.1.1
|
||||
geojson-utils@1.0.10
|
||||
google-oauth@1.4.2
|
||||
fetch@0.1.3
|
||||
geojson-utils@1.0.11
|
||||
google-oauth@1.4.3
|
||||
hot-code-push@1.0.4
|
||||
html-tools@1.1.3
|
||||
htmljs@1.1.1
|
||||
@@ -52,36 +52,34 @@ inter-process-messaging@0.1.1
|
||||
lai:collection-extensions@0.3.0
|
||||
launch-screen@1.3.0
|
||||
littledata:synced-cron@1.5.1
|
||||
livedata@1.0.18
|
||||
localstorage@1.2.0
|
||||
logging@1.3.1
|
||||
mdg:meteor-apm-agent@3.5.0
|
||||
mdg:validated-method@1.2.0
|
||||
meteor@1.10.0
|
||||
logging@1.3.2
|
||||
mdg:validated-method@1.3.0
|
||||
meteor@1.11.2
|
||||
meteor-base@1.5.1
|
||||
meteortesting:browser-tests@1.3.5
|
||||
meteortesting:mocha@2.0.3
|
||||
meteortesting:browser-tests@1.4.2
|
||||
meteortesting:mocha@2.1.0
|
||||
meteortesting:mocha-core@8.1.2
|
||||
mikowals:batch-insert@1.3.0
|
||||
minifier-css@1.6.0
|
||||
minifier-js@2.7.4
|
||||
minimongo@1.8.0
|
||||
minifier-css@1.6.4
|
||||
minifier-js@2.7.5
|
||||
minimongo@1.9.3
|
||||
mixmax:smart-disconnect@0.0.5
|
||||
mobile-experience@1.1.0
|
||||
mobile-status-bar@1.1.0
|
||||
modern-browsers@0.1.8
|
||||
modules@0.18.0
|
||||
modules-runtime@0.13.0
|
||||
mongo@1.15.0
|
||||
modern-browsers@0.1.9
|
||||
modules@0.19.0
|
||||
modules-runtime@0.13.1
|
||||
mongo@1.16.6
|
||||
mongo-decimal@0.1.3
|
||||
mongo-dev-server@1.1.0
|
||||
mongo-id@1.0.8
|
||||
mongo-livedata@1.0.12
|
||||
npm-mongo@4.3.1
|
||||
oauth@2.1.2
|
||||
oauth2@1.3.1
|
||||
npm-mongo@4.16.0
|
||||
oauth@2.2.0
|
||||
oauth2@1.3.2
|
||||
ordered-dict@1.1.0
|
||||
ostrio:cookies@2.7.2
|
||||
ostrio:files@2.0.1
|
||||
ostrio:files@2.3.3
|
||||
patreon-oauth@0.1.0
|
||||
peerlibrary:assert@0.3.0
|
||||
peerlibrary:check-extension@0.7.0
|
||||
@@ -93,20 +91,20 @@ peerlibrary:reactive-mongo@0.4.1
|
||||
peerlibrary:reactive-publish@0.10.0
|
||||
peerlibrary:server-autorun@0.8.0
|
||||
peerlibrary:subscription-data@0.8.0
|
||||
percolate:migrations@1.0.3
|
||||
promise@0.12.0
|
||||
percolate:migrations@1.1.0
|
||||
promise@0.12.2
|
||||
raix:eventemitter@1.0.0
|
||||
random@1.2.0
|
||||
rate-limit@1.0.9
|
||||
react-fast-refresh@0.2.3
|
||||
reactive-dict@1.3.0
|
||||
reactive-var@1.0.11
|
||||
random@1.2.1
|
||||
rate-limit@1.1.1
|
||||
react-fast-refresh@0.2.7
|
||||
reactive-dict@1.3.1
|
||||
reactive-var@1.0.12
|
||||
reload@1.3.1
|
||||
retry@1.1.0
|
||||
routepolicy@1.1.1
|
||||
seba:minifiers-autoprefixer@2.0.1
|
||||
service-configuration@1.3.0
|
||||
session@1.2.0
|
||||
service-configuration@1.3.1
|
||||
session@1.2.1
|
||||
sha@1.0.9
|
||||
shell-server@0.5.0
|
||||
simple:json-routes@2.3.1
|
||||
@@ -114,16 +112,17 @@ simple:rest@1.2.1
|
||||
simple:rest-bearer-token-parser@1.1.1
|
||||
simple:rest-json-error-handler@1.1.1
|
||||
simple:rest-method-mixin@1.1.0
|
||||
socket-stream-client@0.5.0
|
||||
socket-stream-client@0.5.1
|
||||
spacebars-compiler@1.3.1
|
||||
standard-minifier-js@2.8.0
|
||||
standard-minifier-js@2.8.1
|
||||
static-html@1.3.2
|
||||
templating-tools@1.2.2
|
||||
tmeasday:check-npm-versions@1.0.2
|
||||
tracker@1.2.0
|
||||
typescript@4.5.4
|
||||
underscore@1.0.10
|
||||
tracker@1.3.2
|
||||
typescript@4.9.4
|
||||
underscore@1.0.13
|
||||
url@1.3.2
|
||||
webapp@1.13.1
|
||||
webapp-hashing@1.1.0
|
||||
webapp@1.13.5
|
||||
webapp-hashing@1.1.1
|
||||
zegenie:redis-oplog@2.0.16
|
||||
zer0th:meteor-vuetify-loader@0.1.41
|
||||
|
||||
@@ -2,38 +2,38 @@
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/@mdi/font@5.x/css/materialdesignicons.min.css" rel="stylesheet">
|
||||
<meta name="viewport" content="width=device-width initial-scale=1.0, user-scalable=no">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<link rel="apple-touch-icon" sizes="57x57" href="/apple-touch-icon-57x57.png?v=lk6WXp6Pmj">
|
||||
<link rel="apple-touch-icon" sizes="60x60" href="/apple-touch-icon-60x60.png?v=lk6WXp6Pmj">
|
||||
<link rel="apple-touch-icon" sizes="72x72" href="/apple-touch-icon-72x72.png?v=lk6WXp6Pmj">
|
||||
<link rel="apple-touch-icon" sizes="76x76" href="/apple-touch-icon-76x76.png?v=lk6WXp6Pmj">
|
||||
<link rel="apple-touch-icon" sizes="114x114" href="/apple-touch-icon-114x114.png?v=lk6WXp6Pmj">
|
||||
<link rel="apple-touch-icon" sizes="120x120" href="/apple-touch-icon-120x120.png?v=lk6WXp6Pmj">
|
||||
<link rel="apple-touch-icon" sizes="144x144" href="/apple-touch-icon-144x144.png?v=lk6WXp6Pmj">
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="/apple-touch-icon-152x152.png?v=lk6WXp6Pmj">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon-180x180.png?v=lk6WXp6Pmj">
|
||||
<link rel="icon" type="image/png" href="/favicon-32x32.png?v=lk6WXp6Pmj" sizes="32x32">
|
||||
<link rel="icon" type="image/png" href="/favicon-194x194.png?v=lk6WXp6Pmj" sizes="194x194">
|
||||
<link rel="icon" type="image/png" href="/favicon-96x96.png?v=lk6WXp6Pmj" sizes="96x96">
|
||||
<link rel="icon" type="image/png" href="/android-chrome-192x192.png?v=lk6WXp6Pmj" sizes="192x192">
|
||||
<link rel="icon" type="image/png" href="/favicon-16x16.png?v=lk6WXp6Pmj" sizes="16x16">
|
||||
<link rel="manifest" href="/manifest.json?v=lk6WXp6Pmj">
|
||||
<link rel="shortcut icon" href="/favicon.ico?v=lk6WXp6Pmj">
|
||||
<meta name="msapplication-TileColor" content="#b91d1d">
|
||||
<meta name="msapplication-TileImage" content="/mstile-144x144.png?v=lk6WXp6Pmj">
|
||||
<meta name="theme-color" content="#d12929">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<link rel="apple-touch-icon" sizes="57x57" href="/apple-touch-icon-57x57.png?v=lk6WXp6Pmj">
|
||||
<link rel="apple-touch-icon" sizes="60x60" href="/apple-touch-icon-60x60.png?v=lk6WXp6Pmj">
|
||||
<link rel="apple-touch-icon" sizes="72x72" href="/apple-touch-icon-72x72.png?v=lk6WXp6Pmj">
|
||||
<link rel="apple-touch-icon" sizes="76x76" href="/apple-touch-icon-76x76.png?v=lk6WXp6Pmj">
|
||||
<link rel="apple-touch-icon" sizes="114x114" href="/apple-touch-icon-114x114.png?v=lk6WXp6Pmj">
|
||||
<link rel="apple-touch-icon" sizes="120x120" href="/apple-touch-icon-120x120.png?v=lk6WXp6Pmj">
|
||||
<link rel="apple-touch-icon" sizes="144x144" href="/apple-touch-icon-144x144.png?v=lk6WXp6Pmj">
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="/apple-touch-icon-152x152.png?v=lk6WXp6Pmj">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon-180x180.png?v=lk6WXp6Pmj">
|
||||
<link rel="icon" type="image/png" href="/favicon-32x32.png?v=lk6WXp6Pmj" sizes="32x32">
|
||||
<link rel="icon" type="image/png" href="/favicon-194x194.png?v=lk6WXp6Pmj" sizes="194x194">
|
||||
<link rel="icon" type="image/png" href="/favicon-96x96.png?v=lk6WXp6Pmj" sizes="96x96">
|
||||
<link rel="icon" type="image/png" href="/android-chrome-192x192.png?v=lk6WXp6Pmj" sizes="192x192">
|
||||
<link rel="icon" type="image/png" href="/favicon-16x16.png?v=lk6WXp6Pmj" sizes="16x16">
|
||||
<link rel="manifest" href="/manifest.json?v=lk6WXp6Pmj">
|
||||
<link rel="shortcut icon" href="/favicon.ico?v=lk6WXp6Pmj">
|
||||
<meta name="msapplication-TileColor" content="#b91d1d">
|
||||
<meta name="msapplication-TileImage" content="/mstile-144x144.png?v=lk6WXp6Pmj">
|
||||
<meta name="theme-color" content="#d12929">
|
||||
|
||||
<style type="text/css" media="print">
|
||||
@page {
|
||||
margin: 0mm;
|
||||
}
|
||||
html {
|
||||
margin: 0px;
|
||||
}
|
||||
* {
|
||||
-webkit-transition: none !important;
|
||||
transition: none !important;
|
||||
}
|
||||
</style>
|
||||
<style type="text/css" media="print">
|
||||
@page {
|
||||
margin: 0mm;
|
||||
}
|
||||
html {
|
||||
margin: 0px;
|
||||
}
|
||||
* {
|
||||
-webkit-transition: none !important;
|
||||
transition: none !important;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import '/imports/api/simpleSchemaConfig.js';
|
||||
import '/imports/ui/vueSetup.js';
|
||||
import '/imports/ui/styles/stylesIndex.js';
|
||||
import '/imports/client/ui/vueSetup.js';
|
||||
import '/imports/client/ui/styles/stylesIndex.js';
|
||||
import '/imports/client/config.js';
|
||||
import '/imports/client/serviceWorker.js';
|
||||
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
@@ -1,18 +1,24 @@
|
||||
import { createS3FilesCollection } from '/imports/api/files/s3FileStorage.js';
|
||||
|
||||
import SimpleSchema from 'simpl-schema';
|
||||
import { incrementFileStorageUsed } from '/imports/api/users/methods/updateFileStorageUsed.js';
|
||||
import { CreaturePropertySchema } from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import { CreatureSchema } from '/imports/api/creature/creatures/Creatures.js';
|
||||
let createS3FilesCollection;
|
||||
if (Meteor.isServer) {
|
||||
createS3FilesCollection = require('/imports/api/files/server/s3FileStorage.js').createS3FilesCollection
|
||||
} else {
|
||||
createS3FilesCollection = require('/imports/api/files/client/s3FileStorage.js').createS3FilesCollection
|
||||
}
|
||||
|
||||
const ArchiveCreatureFiles = createS3FilesCollection({
|
||||
collectionName: 'archiveCreatureFiles',
|
||||
storagePath: Meteor.isDevelopment ? '/DiceCloud/archiveCreatures/' : 'assets/app/archiveCreatures',
|
||||
storagePath: Meteor.isDevelopment ? '../../../../../fileStorage/archiveCreatures' : 'assets/app/archiveCreatures',
|
||||
onBeforeUpload(file) {
|
||||
// Allow upload files under 10MB, and only in json format
|
||||
if (file.size > 10485760) {
|
||||
return 'Please upload with size equal or less than 10MB';
|
||||
}
|
||||
if (!/json/i.test(file.extension)){
|
||||
if (!/json/i.test(file.extension)) {
|
||||
return 'Please upload only a JSON file';
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -13,12 +13,12 @@ import { incrementFileStorageUsed } from '/imports/api/users/methods/updateFileS
|
||||
import verifyArchiveSafety from '/imports/api/creature/archive/methods/verifyArchiveSafety.js';
|
||||
|
||||
let migrateArchive;
|
||||
if (Meteor.isServer){
|
||||
migrateArchive = require('/imports/migrations/server/migrateArchive.js').default;
|
||||
if (Meteor.isServer) {
|
||||
migrateArchive = require('/imports/migrations/archive/migrateArchive.js').default;
|
||||
}
|
||||
|
||||
function restoreCreature(archive, userId){
|
||||
if (SCHEMA_VERSION < archive.meta.schemaVersion){
|
||||
function restoreCreature(archive, userId) {
|
||||
if (SCHEMA_VERSION < archive.meta.schemaVersion) {
|
||||
throw new Meteor.Error('Incompatible',
|
||||
'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',
|
||||
'The creature you are trying to restore already exists.')
|
||||
|
||||
|
||||
// Ensure the user owns the restored creature
|
||||
archive.creature.owner = userId;
|
||||
|
||||
@@ -44,13 +44,13 @@ function restoreCreature(archive, userId){
|
||||
Creatures.insert(archive.creature);
|
||||
try {
|
||||
// Add all the properties
|
||||
if (archive.properties && archive.properties.length){
|
||||
if (archive.properties && archive.properties.length) {
|
||||
CreatureProperties.batchInsert(archive.properties);
|
||||
}
|
||||
if (archive.experiences && archive.experiences.length){
|
||||
if (archive.experiences && archive.experiences.length) {
|
||||
Experiences.batchInsert(archive.experiences);
|
||||
}
|
||||
if (archive.logs && archive.logs.length){
|
||||
if (archive.logs && archive.logs.length) {
|
||||
CreatureLogs.batchInsert(archive.logs);
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -73,23 +73,23 @@ const restoreCreaturefromFile = new ValidatedMethod({
|
||||
numRequests: 10,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
async run({fileId}) {
|
||||
async run({ fileId }) {
|
||||
// fetch the file
|
||||
const file = ArchiveCreatureFiles.findOne({_id: fileId}).get();
|
||||
if (!file){
|
||||
const file = ArchiveCreatureFiles.findOne({ _id: fileId }).get();
|
||||
if (!file) {
|
||||
throw new Meteor.Error('File not found',
|
||||
'The requested creature archive does not exist');
|
||||
'The requested creature archive does not exist');
|
||||
}
|
||||
// Assert ownership
|
||||
const userId = file?.userId;
|
||||
if (!userId || userId !== this.userId){
|
||||
if (!userId || userId !== this.userId) {
|
||||
throw new Meteor.Error('Permission denied',
|
||||
'You can only restore creatures you own');
|
||||
'You can only restore creatures you own');
|
||||
}
|
||||
|
||||
assertHasCharactersSlots(this.userId);
|
||||
|
||||
if (Meteor.isServer){
|
||||
if (Meteor.isServer) {
|
||||
// Read the file data
|
||||
const archive = await ArchiveCreatureFiles.readJSONFile(file);
|
||||
restoreCreature(archive, this.userId);
|
||||
|
||||
@@ -4,25 +4,25 @@ import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
||||
let CreatureFolders = new Mongo.Collection('creatureFolders');
|
||||
|
||||
let creatureFolderSchema = new SimpleSchema({
|
||||
name: {
|
||||
type: String,
|
||||
trim: false,
|
||||
optional: true,
|
||||
name: {
|
||||
type: String,
|
||||
trim: false,
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.name,
|
||||
},
|
||||
creatures: {
|
||||
type: Array,
|
||||
defaultValue: [],
|
||||
},
|
||||
'creatures.$': {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
owner: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
creatures: {
|
||||
type: Array,
|
||||
defaultValue: [],
|
||||
},
|
||||
'creatures.$': {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
owner: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
index: 1,
|
||||
},
|
||||
},
|
||||
archived: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
|
||||
@@ -18,23 +18,23 @@ let CreaturePropertySchema = new SimpleSchema({
|
||||
type: String,
|
||||
optional: true,
|
||||
},
|
||||
type: {
|
||||
type: {
|
||||
type: String,
|
||||
allowedValues: Object.keys(propertySchemasIndex),
|
||||
},
|
||||
tags: {
|
||||
type: Array,
|
||||
defaultValue: [],
|
||||
tags: {
|
||||
type: Array,
|
||||
defaultValue: [],
|
||||
maxCount: STORAGE_LIMITS.tagCount,
|
||||
},
|
||||
'tags.$': {
|
||||
type: String,
|
||||
},
|
||||
'tags.$': {
|
||||
type: String,
|
||||
max: STORAGE_LIMITS.tagLength,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
icon: {
|
||||
type: storedIconsSchema,
|
||||
optional: true,
|
||||
@@ -46,6 +46,12 @@ let CreaturePropertySchema = new SimpleSchema({
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
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({
|
||||
@@ -82,6 +88,12 @@ const DenormalisedOnlyCreaturePropertySchema = new SimpleSchema({
|
||||
index: 1,
|
||||
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
|
||||
dirty: {
|
||||
type: Boolean,
|
||||
@@ -93,20 +105,20 @@ const DenormalisedOnlyCreaturePropertySchema = new SimpleSchema({
|
||||
|
||||
CreaturePropertySchema.extend(DenormalisedOnlyCreaturePropertySchema);
|
||||
|
||||
for (let key in propertySchemasIndex){
|
||||
let schema = new SimpleSchema({});
|
||||
schema.extend(propertySchemasIndex[key]);
|
||||
schema.extend(CreaturePropertySchema);
|
||||
for (let key in propertySchemasIndex) {
|
||||
let schema = new SimpleSchema({});
|
||||
schema.extend(propertySchemasIndex[key]);
|
||||
schema.extend(CreaturePropertySchema);
|
||||
schema.extend(ColorSchema);
|
||||
schema.extend(ChildSchema);
|
||||
schema.extend(SoftRemovableSchema);
|
||||
CreatureProperties.attachSchema(schema, {
|
||||
selector: {type: key}
|
||||
});
|
||||
schema.extend(ChildSchema);
|
||||
schema.extend(SoftRemovableSchema);
|
||||
CreatureProperties.attachSchema(schema, {
|
||||
selector: { type: key }
|
||||
});
|
||||
}
|
||||
|
||||
export default CreatureProperties;
|
||||
export {
|
||||
DenormalisedOnlyCreaturePropertySchema,
|
||||
CreaturePropertySchema,
|
||||
CreaturePropertySchema,
|
||||
};
|
||||
|
||||
@@ -20,33 +20,33 @@ const adjustQuantity = new ValidatedMethod({
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({_id, operation, value}) {
|
||||
run({ _id, operation, value }) {
|
||||
// Permissions
|
||||
let property = CreatureProperties.findOne(_id);
|
||||
let property = CreatureProperties.findOne(_id);
|
||||
let rootCreature = getRootCreatureAncestor(property);
|
||||
assertEditPermission(rootCreature, this.userId);
|
||||
assertEditPermission(rootCreature, this.userId);
|
||||
|
||||
// Do work
|
||||
adjustQuantityWork({property, operation, value});
|
||||
adjustQuantityWork({ property, operation, value });
|
||||
},
|
||||
});
|
||||
|
||||
export function adjustQuantityWork({property, operation, value}){
|
||||
export function adjustQuantityWork({ property, operation, value }) {
|
||||
// Check if property has quantity
|
||||
let schema = CreatureProperties.simpleSchema(property);
|
||||
if (!schema.allowsKey('quantity')){
|
||||
if (!schema.allowsKey('quantity')) {
|
||||
throw new Meteor.Error(
|
||||
'Adjust quantity failed',
|
||||
`Property of type "${property.type}" doesn't have a quantity`
|
||||
);
|
||||
}
|
||||
if (operation === 'set'){
|
||||
if (operation === 'set') {
|
||||
CreatureProperties.update(property._id, {
|
||||
$set: {quantity: value, dirty: true}
|
||||
$set: { quantity: value, dirty: true }
|
||||
}, {
|
||||
selector: property
|
||||
});
|
||||
} else if (operation === 'increment'){
|
||||
} else if (operation === 'increment') {
|
||||
// value here is 'damage'
|
||||
value = -value;
|
||||
let currentQuantity = property.quantity;
|
||||
|
||||
@@ -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;
|
||||
@@ -1,53 +0,0 @@
|
||||
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
||||
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
||||
import SimpleSchema from 'simpl-schema';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
|
||||
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
|
||||
|
||||
const damagePropertiesByName = new ValidatedMethod({
|
||||
name: 'CreatureProperties.damagePropertiesByName',
|
||||
validate: new SimpleSchema({
|
||||
creatureId: SimpleSchema.RegEx.Id,
|
||||
variableName: {
|
||||
type: String,
|
||||
},
|
||||
operation: {
|
||||
type: String,
|
||||
allowedValues: ['set', 'increment']
|
||||
},
|
||||
value: Number,
|
||||
}).validator(),
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 20,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({creatureId, variableName, operation, value}) {
|
||||
// Check permissions
|
||||
let creature = Creatures.findOne(creatureId, {
|
||||
fields: {
|
||||
variables: 1,
|
||||
owner: 1,
|
||||
readers: 1,
|
||||
writers: 1,
|
||||
},
|
||||
});
|
||||
assertEditPermission(creature, this.userId);
|
||||
CreatureProperties.find({
|
||||
'ancestors.id': creatureId,
|
||||
variableName,
|
||||
removed: {$ne: false},
|
||||
inactive: {$ne: true},
|
||||
}).forEach(property => {
|
||||
// Check if property can take damage
|
||||
let schema = CreatureProperties.simpleSchema(property);
|
||||
if (!schema.allowsKey('damage')) return;
|
||||
// Damage the property
|
||||
damagePropertyWork({property, operation, value});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export default damagePropertiesByName;
|
||||
@@ -2,8 +2,9 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
||||
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
||||
import SimpleSchema from 'simpl-schema';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
|
||||
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
|
||||
import { applyTriggers } from '/imports/api/engine/actions/applyTriggers.js';
|
||||
import ActionContext from '/imports/api/engine/actions/ActionContext.js';
|
||||
|
||||
const damageProperty = new ValidatedMethod({
|
||||
name: 'creatureProperties.damage',
|
||||
@@ -20,58 +21,121 @@ const damageProperty = new ValidatedMethod({
|
||||
numRequests: 20,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({_id, operation, value}) {
|
||||
// Check permissions
|
||||
let property = CreatureProperties.findOne(_id);
|
||||
if (!property) throw new Meteor.Error(
|
||||
run({ _id, operation, value }) {
|
||||
|
||||
// Get action context
|
||||
let prop = CreatureProperties.findOne(_id);
|
||||
if (!prop) throw new Meteor.Error(
|
||||
'Damage property failed', 'Property doesn\'t exist'
|
||||
);
|
||||
let rootCreature = getRootCreatureAncestor(property);
|
||||
assertEditPermission(rootCreature, this.userId);
|
||||
// Check if property can take damage
|
||||
let schema = CreatureProperties.simpleSchema(property);
|
||||
if (!schema.allowsKey('damage')){
|
||||
throw new Meteor.Error(
|
||||
'Damage property failed',
|
||||
`Property of type "${property.type}" can't be damaged`
|
||||
);
|
||||
}
|
||||
let result = damagePropertyWork({ property, operation, value });
|
||||
const creatureId = prop.ancestors[0].id;
|
||||
const actionContext = new ActionContext(creatureId, [creatureId], this);
|
||||
|
||||
// Check permissions
|
||||
assertEditPermission(actionContext.creature, this.userId);
|
||||
|
||||
// Check if property can take damage
|
||||
let schema = CreatureProperties.simpleSchema(prop);
|
||||
if (!schema.allowsKey('damage')) {
|
||||
throw new Meteor.Error(
|
||||
'Damage property failed',
|
||||
`Property of type "${prop.type}" can't be damaged`
|
||||
);
|
||||
}
|
||||
|
||||
// Replace the prop by its actionContext counterpart if possible
|
||||
if (prop.variableName) {
|
||||
const actionContextProp = actionContext.scope[prop.variableName];
|
||||
if (actionContextProp?._id === prop._id) {
|
||||
prop = actionContextProp;
|
||||
}
|
||||
}
|
||||
|
||||
const result = damagePropertyWork({ prop, operation, value, actionContext });
|
||||
|
||||
// Insert the log
|
||||
actionContext.writeLog();
|
||||
return result;
|
||||
},
|
||||
});
|
||||
|
||||
export function damagePropertyWork({property, operation, value}){
|
||||
let damage, newValue;
|
||||
if (operation === 'set'){
|
||||
const total = property.total || 0;
|
||||
export function damagePropertyWork({ prop, operation, value, actionContext, logFunction }) {
|
||||
|
||||
// Save the value to the scope before applying the before triggers
|
||||
if (operation === 'increment') {
|
||||
if (value >= 0) {
|
||||
actionContext.scope['~damage'] = { value };
|
||||
} else {
|
||||
actionContext.scope['~healing'] = { value: -value };
|
||||
}
|
||||
} else {
|
||||
actionContext.scope['~set'] = { value };
|
||||
}
|
||||
|
||||
applyTriggers(actionContext.triggers?.damageProperty?.before, prop, actionContext);
|
||||
|
||||
// fetch the value from the scope after the before triggers, in case they changed them
|
||||
if (operation === 'increment') {
|
||||
if (value >= 0) {
|
||||
value = actionContext.scope['~damage']?.value;
|
||||
} else {
|
||||
value = -actionContext.scope['~healing']?.value;
|
||||
}
|
||||
} else {
|
||||
value = actionContext.scope['~set']?.value;
|
||||
}
|
||||
|
||||
let damage, newValue, increment;
|
||||
if (operation === 'set') {
|
||||
const total = prop.total || 0;
|
||||
// Set represents what we want the value to be after damage
|
||||
// So we need the actual damage to get to that value
|
||||
damage = total - value;
|
||||
// Damage can't exceed total value
|
||||
if (damage > total) damage = total;
|
||||
if (damage > total && !prop.ignoreLowerLimit) damage = total;
|
||||
// Damage must be positive
|
||||
if (damage < 0) damage = 0;
|
||||
newValue = property.total - damage;
|
||||
} else if (operation === 'increment'){
|
||||
let currentValue = property.value || 0;
|
||||
let currentDamage = property.damage || 0;
|
||||
let increment = value;
|
||||
if (damage < 0 && !prop.ignoreUpperLimit) damage = 0;
|
||||
newValue = prop.total - damage;
|
||||
// Write the results
|
||||
CreatureProperties.update(prop._id, {
|
||||
$set: { damage, value: newValue, dirty: true }
|
||||
}, {
|
||||
selector: prop
|
||||
});
|
||||
// Also write it straight to the prop so that it is updated in the actionContext
|
||||
prop.damage = damage;
|
||||
prop.value = newValue;
|
||||
logFunction?.(newValue);
|
||||
} else if (operation === 'increment') {
|
||||
let currentValue = prop.value || 0;
|
||||
let currentDamage = prop.damage || 0;
|
||||
increment = value;
|
||||
// Can't increase damage above the remaining value
|
||||
if (increment > currentValue) increment = currentValue;
|
||||
if (increment > currentValue && !prop.ignoreLowerLimit) increment = currentValue;
|
||||
// Can't decrease damage below zero
|
||||
if (-increment > currentDamage) increment = -currentDamage;
|
||||
if (-increment > currentDamage && !prop.ignoreUpperLimit) increment = -currentDamage;
|
||||
damage = currentDamage + increment;
|
||||
newValue = property.total - damage;
|
||||
newValue = prop.total - damage;
|
||||
// Write the results
|
||||
CreatureProperties.update(prop._id, {
|
||||
$inc: { damage: increment, value: -increment },
|
||||
$set: { dirty: true },
|
||||
}, {
|
||||
selector: prop
|
||||
});
|
||||
// Also write it straight to the prop so that it is updated in the actionContext
|
||||
prop.damage += increment;
|
||||
prop.value -= increment;
|
||||
logFunction?.(increment);
|
||||
}
|
||||
|
||||
// Write the results
|
||||
CreatureProperties.update(property._id, {
|
||||
$set: {damage, value: newValue, dirty: true}
|
||||
}, {
|
||||
selector: property
|
||||
});
|
||||
return damage;
|
||||
applyTriggers(actionContext.triggers?.damageProperty?.after, prop, actionContext);
|
||||
|
||||
if (operation === 'set') {
|
||||
return damage;
|
||||
} else if (operation === 'increment') {
|
||||
return increment;
|
||||
}
|
||||
}
|
||||
|
||||
export default damageProperty;
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
||||
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
||||
import SimpleSchema from 'simpl-schema';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
|
||||
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
|
||||
|
||||
const dealDamage = new ValidatedMethod({
|
||||
name: 'creatureProperties.dealDamage',
|
||||
validate: new SimpleSchema({
|
||||
creatureId: SimpleSchema.RegEx.Id,
|
||||
damageType: {
|
||||
type: String,
|
||||
},
|
||||
amount: Number,
|
||||
}).validator(),
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 20,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({creatureId, damageType, amount}) {
|
||||
// permissions
|
||||
let creature = Creatures.findOne(creatureId, {
|
||||
fields: {
|
||||
owner: 1,
|
||||
readers: 1,
|
||||
writers: 1,
|
||||
},
|
||||
});
|
||||
assertEditPermission(creature, this.userId);
|
||||
|
||||
const totalDamage = dealDamageWork({creature, damageType, amount})
|
||||
return totalDamage;
|
||||
},
|
||||
});
|
||||
|
||||
export function dealDamageWork({creature, damageType, amount}){
|
||||
// Get all the health bars and do damage to them
|
||||
let healthBars = CreatureProperties.find({
|
||||
'ancestors.id': creature._id,
|
||||
type: 'attribute',
|
||||
attributeType:'healthBar',
|
||||
removed: {$ne: true},
|
||||
inactive: {$ne: true},
|
||||
}, {
|
||||
sort: {order: -1},
|
||||
});
|
||||
//let multiplier = creature.damageMultipliers[damageType];
|
||||
//if (multiplier === undefined) multiplier = 1;
|
||||
//let totalDamage = Math.floor(amount * multiplier);
|
||||
const totalDamage = amount;
|
||||
let damageLeft = totalDamage;
|
||||
if (damageType === 'healing') damageLeft = -totalDamage;
|
||||
let propertyIds = [];
|
||||
healthBars.forEach(healthBar => {
|
||||
if (damageLeft === 0) return;
|
||||
let damageAdded = damagePropertyWork({
|
||||
property: healthBar,
|
||||
operation: 'increment',
|
||||
value: damageLeft,
|
||||
});
|
||||
damageLeft -= damageAdded;
|
||||
propertyIds.push(healthBar._id);
|
||||
});
|
||||
return totalDamage;
|
||||
}
|
||||
|
||||
export default dealDamage;
|
||||
@@ -5,14 +5,14 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur
|
||||
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
|
||||
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
|
||||
import {
|
||||
setLineageOfDocs,
|
||||
renewDocIds
|
||||
setLineageOfDocs,
|
||||
renewDocIds
|
||||
} from '/imports/api/parenting/parenting.js';
|
||||
import { reorderDocs } from '/imports/api/parenting/order.js';
|
||||
var snackbar;
|
||||
if (Meteor.isClient){
|
||||
if (Meteor.isClient) {
|
||||
snackbar = require(
|
||||
'/imports/ui/components/snackbars/SnackbarQueue.js'
|
||||
'/imports/client/ui/components/snackbars/SnackbarQueue.js'
|
||||
).snackbar
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ const duplicateProperty = new ValidatedMethod({
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({_id}) {
|
||||
run({ _id }) {
|
||||
let property = CreatureProperties.findOne(_id);
|
||||
let creature = getRootCreatureAncestor(property);
|
||||
|
||||
@@ -42,19 +42,24 @@ const duplicateProperty = new ValidatedMethod({
|
||||
let propertyId = randomSrc.id();
|
||||
property._id = propertyId;
|
||||
|
||||
// Change the variableName so it isn't immediately overridden
|
||||
if (property.variableName) {
|
||||
property.variableName += 'Copy'
|
||||
}
|
||||
|
||||
// Get all the descendants
|
||||
let nodes = CreatureProperties.find({
|
||||
'ancestors.id': _id,
|
||||
removed: {$ne: true},
|
||||
}, {
|
||||
'ancestors.id': _id,
|
||||
removed: { $ne: true },
|
||||
}, {
|
||||
limit: DUPLICATE_CHILDREN_LIMIT + 1,
|
||||
sort: {order: 1},
|
||||
sort: { order: 1 },
|
||||
}).fetch();
|
||||
|
||||
// Alert the user if the limit was hit
|
||||
if (nodes.length > DUPLICATE_CHILDREN_LIMIT){
|
||||
if (nodes.length > DUPLICATE_CHILDREN_LIMIT) {
|
||||
nodes.pop();
|
||||
if (Meteor.isClient){
|
||||
if (Meteor.isClient) {
|
||||
snackbar({
|
||||
text: `Only the first ${DUPLICATE_CHILDREN_LIMIT} children were duplicated`,
|
||||
});
|
||||
@@ -63,25 +68,25 @@ const duplicateProperty = new ValidatedMethod({
|
||||
|
||||
// re-map all the ancestors
|
||||
setLineageOfDocs({
|
||||
docArray: nodes,
|
||||
newAncestry : [
|
||||
docArray: nodes,
|
||||
newAncestry: [
|
||||
...property.ancestors,
|
||||
{id: propertyId, collection: 'creatureProperties'}
|
||||
{ id: propertyId, collection: 'creatureProperties' }
|
||||
],
|
||||
oldParent : {id: _id, collection: 'creatureProperties'},
|
||||
});
|
||||
oldParent: { id: _id, collection: 'creatureProperties' },
|
||||
});
|
||||
|
||||
// Give the docs new IDs without breaking internal references
|
||||
renewDocIds({docArray: nodes});
|
||||
renewDocIds({ docArray: nodes });
|
||||
|
||||
// Order the root node
|
||||
property.order += 0.5;
|
||||
|
||||
|
||||
// Mark the sheet as needing recompute
|
||||
property.dirty = true;
|
||||
|
||||
// Insert the properties
|
||||
CreatureProperties.batchInsert([property, ...nodes]);
|
||||
CreatureProperties.batchInsert([property, ...nodes]);
|
||||
|
||||
// Tree structure changed by inserts, reorder the tree
|
||||
reorderDocs({
|
||||
|
||||
@@ -10,8 +10,8 @@ import getParentRefByTag from '/imports/api/creature/creatureProperties/methods/
|
||||
// Equipping or unequipping an item will also change its parent
|
||||
const equipItem = new ValidatedMethod({
|
||||
name: 'creatureProperties.equip',
|
||||
validate({_id, equipped}){
|
||||
if (!_id) throw new Meteor.Error('No _id', '_id is required');
|
||||
validate({ _id, equipped }) {
|
||||
if (!_id) throw new Meteor.Error('No _id', '_id is required');
|
||||
if (equipped !== true && equipped !== false) {
|
||||
throw new Meteor.Error('No equipped', 'equipped is required to be true or false');
|
||||
}
|
||||
@@ -21,20 +21,20 @@ const equipItem = new ValidatedMethod({
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({_id, equipped}) {
|
||||
run({ _id, equipped }) {
|
||||
let item = CreatureProperties.findOne(_id);
|
||||
if (item.type !== 'item') throw new Meteor.Error('wrong type',
|
||||
'Equip and unequip can only be performed on items');
|
||||
'Equip and unequip can only be performed on items');
|
||||
let creature = getRootCreatureAncestor(item);
|
||||
assertEditPermission(creature, this.userId);
|
||||
CreatureProperties.update(_id, {
|
||||
$set: { equipped, dirty: true },
|
||||
}, {
|
||||
selector: {type: 'item'},
|
||||
});
|
||||
selector: { type: 'item' },
|
||||
});
|
||||
let tag = equipped ? BUILT_IN_TAGS.equipment : BUILT_IN_TAGS.carried;
|
||||
let parentRef = getParentRefByTag(creature._id, tag);
|
||||
if (!parentRef) parentRef = {id: creature._id, collection: 'creatures'};
|
||||
if (!parentRef) parentRef = { id: creature._id, collection: 'creatures' };
|
||||
|
||||
organizeDoc.call({
|
||||
docRef: {
|
||||
|
||||
@@ -6,24 +6,24 @@ import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/ge
|
||||
|
||||
const flipToggle = new ValidatedMethod({
|
||||
name: 'creatureProperties.flipToggle',
|
||||
validate({_id}){
|
||||
if (!_id) throw new Meteor.Error('No _id', '_id is required');
|
||||
validate({ _id }) {
|
||||
if (!_id) throw new Meteor.Error('No _id', '_id is required');
|
||||
},
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({_id}) {
|
||||
run({ _id }) {
|
||||
// Permission
|
||||
let property = CreatureProperties.findOne(_id, {
|
||||
fields: {type: 1, ancestors: 1, enabled: 1, disabled: 1}
|
||||
fields: { type: 1, ancestors: 1, enabled: 1, disabled: 1 }
|
||||
});
|
||||
if (property.type !== 'toggle'){
|
||||
if (property.type !== 'toggle') {
|
||||
throw new Meteor.Error('wrong property',
|
||||
'This method can only be applied to toggles');
|
||||
}
|
||||
if (!property.enabled && !property.disabled){
|
||||
if (!property.enabled && !property.disabled) {
|
||||
throw new Meteor.Error('Computed toggle',
|
||||
'Can\'t flip a toggle that is computed')
|
||||
}
|
||||
@@ -32,13 +32,15 @@ const flipToggle = new ValidatedMethod({
|
||||
|
||||
// Invert the current value, disabled is the canonical store of value
|
||||
const currentValue = !property.disabled;
|
||||
CreatureProperties.update(_id, {$set: {
|
||||
enabled: !currentValue,
|
||||
disabled: currentValue,
|
||||
dirty: true,
|
||||
}}, {
|
||||
selector: {type: 'toggle'},
|
||||
});
|
||||
CreatureProperties.update(_id, {
|
||||
$set: {
|
||||
enabled: !currentValue,
|
||||
disabled: currentValue,
|
||||
dirty: true,
|
||||
}
|
||||
}, {
|
||||
selector: { type: 'toggle' },
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
removed: {$ne: true},
|
||||
fillSlots: true,
|
||||
removed: { $ne: true },
|
||||
$and: []
|
||||
};
|
||||
if (libraryIds){
|
||||
filter['ancestors.id'] = {$in: libraryIds};
|
||||
}
|
||||
if (slot.slotType){
|
||||
filter['ancestors.id'] = { $in: libraryIds };
|
||||
if (slot.slotType) {
|
||||
filter.$and.push({
|
||||
$or: [{
|
||||
type: slot.slotType
|
||||
},{
|
||||
type: 'slotFiller',
|
||||
}, {
|
||||
slotFillerType: slot.slotType,
|
||||
}]
|
||||
});
|
||||
@@ -19,44 +21,43 @@ export default function getSlotFillFilter({slot, libraryIds}){
|
||||
filter.$and.push({
|
||||
$or: [{
|
||||
type: 'classLevel',
|
||||
},{
|
||||
type: 'slotFiller',
|
||||
}, {
|
||||
slotFillerType: 'classLevel',
|
||||
}]
|
||||
});
|
||||
if (slot.variableName) {
|
||||
if (slot.variableName) {
|
||||
filter.variableName = slot.variableName;
|
||||
}
|
||||
|
||||
// Only search for levels the class needs
|
||||
if (slot.missingLevels && slot.missingLevels.length) {
|
||||
filter.level = {$in: slot.missingLevels};
|
||||
filter.level = { $in: slot.missingLevels };
|
||||
} else {
|
||||
filter.level = (slot.level || 0) + 1;
|
||||
filter.level = { $gt: slot.level || 0 };
|
||||
}
|
||||
}
|
||||
let tagsOr = [];
|
||||
let tagsNin = [];
|
||||
if (slot.slotTags && slot.slotTags.length){
|
||||
tagsOr.push({tags: {$all: slot.slotTags}});
|
||||
if (slot.slotTags && slot.slotTags.length) {
|
||||
tagsOr.push({ libraryTags: { $all: slot.slotTags } });
|
||||
}
|
||||
if (slot.extraTags && slot.extraTags.length){
|
||||
if (slot.extraTags && slot.extraTags.length) {
|
||||
slot.extraTags.forEach(extra => {
|
||||
if (!extra.tags || !extra.tags.length) return;
|
||||
if (extra.operation === 'OR'){
|
||||
tagsOr.push({tags: {$all: extra.tags}});
|
||||
} else if (extra.operation === 'NOT'){
|
||||
if (extra.operation === 'OR') {
|
||||
tagsOr.push({ libraryTags: { $all: extra.tags } });
|
||||
} else if (extra.operation === 'NOT') {
|
||||
tagsNin.push(...extra.tags);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (tagsOr.length){
|
||||
if (tagsOr.length) {
|
||||
filter.$or = tagsOr;
|
||||
}
|
||||
if (tagsNin.length){
|
||||
filter.$and.push({tags: {$nin: tagsNin}});
|
||||
if (tagsNin.length) {
|
||||
filter.$and.push({ libraryTags: { $nin: tagsNin } });
|
||||
}
|
||||
if (!filter.$and.length){
|
||||
if (!filter.$and.length) {
|
||||
delete filter.$and;
|
||||
}
|
||||
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,7 +1,6 @@
|
||||
import '/imports/api/creature/creatureProperties/methods/adjustQuantity.js';
|
||||
import '/imports/api/creature/creatureProperties/methods/damagePropertiesByName.js';
|
||||
import '/imports/api/creature/creatureProperties/methods/copyPropertyToLibrary.js';
|
||||
import '/imports/api/creature/creatureProperties/methods/damageProperty.js';
|
||||
import '/imports/api/creature/creatureProperties/methods/dealDamage.js';
|
||||
import '/imports/api/creature/creatureProperties/methods/duplicateProperty.js';
|
||||
import '/imports/api/creature/creatureProperties/methods/equipItem.js';
|
||||
import '/imports/api/creature/creatureProperties/methods/insertProperty.js';
|
||||
|
||||
@@ -12,7 +12,7 @@ import { getHighestOrder } from '/imports/api/parenting/order.js';
|
||||
|
||||
const insertProperty = new ValidatedMethod({
|
||||
name: 'creatureProperties.insert',
|
||||
validate: new SimpleSchema({
|
||||
validate: new SimpleSchema({
|
||||
creatureProperty: {
|
||||
type: Object,
|
||||
blackbox: true,
|
||||
@@ -24,25 +24,25 @@ const insertProperty = new ValidatedMethod({
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({creatureProperty, parentRef}) {
|
||||
run({ creatureProperty, parentRef }) {
|
||||
// get the new ancestry for the properties
|
||||
let {parentDoc, ancestors} = getAncestry({parentRef});
|
||||
let { parentDoc, ancestors } = getAncestry({ parentRef });
|
||||
|
||||
// Check permission to edit
|
||||
// Check permission to edit
|
||||
let rootCreature;
|
||||
if (parentRef.collection === 'creatures'){
|
||||
if (parentRef.collection === 'creatures') {
|
||||
rootCreature = parentDoc;
|
||||
} else if (parentRef.collection === 'creatureProperties'){
|
||||
} else if (parentRef.collection === 'creatureProperties') {
|
||||
rootCreature = getRootCreatureAncestor(parentDoc);
|
||||
} else {
|
||||
throw `${parentRef.collection} is not a valid parent collection`
|
||||
}
|
||||
} else {
|
||||
throw `${parentRef.collection} is not a valid parent collection`
|
||||
}
|
||||
assertEditPermission(rootCreature, this.userId);
|
||||
|
||||
creatureProperty.parent = parentRef;
|
||||
creatureProperty.ancestors = ancestors;
|
||||
|
||||
return insertPropertyWork({
|
||||
return insertPropertyWork({
|
||||
property: creatureProperty,
|
||||
creature: rootCreature,
|
||||
});
|
||||
@@ -75,31 +75,31 @@ const insertPropertyAsChildOfTag = new ValidatedMethod({
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({creatureProperty, creatureId, tag, tagDefaultName}) {
|
||||
run({ creatureProperty, creatureId, tag, tagDefaultName }) {
|
||||
let parentRef = getParentRefByTag(creatureId, tag);
|
||||
|
||||
if (!parentRef){
|
||||
if (!parentRef) {
|
||||
// Use the creature as the parent and mark that we need to insert the folder first later
|
||||
var insertFolderFirst = true;
|
||||
parentRef = {id: creatureId, collection: 'creatures'};
|
||||
parentRef = { id: creatureId, collection: 'creatures' };
|
||||
}
|
||||
|
||||
// get the new ancestry for the properties
|
||||
let {parentDoc, ancestors} = getAncestry({parentRef});
|
||||
let { parentDoc, ancestors } = getAncestry({ parentRef });
|
||||
|
||||
// Check permission to edit
|
||||
let rootCreature;
|
||||
if (parentRef.collection === 'creatures'){
|
||||
if (parentRef.collection === 'creatures') {
|
||||
rootCreature = parentDoc;
|
||||
} else if (parentRef.collection === 'creatureProperties'){
|
||||
} else if (parentRef.collection === 'creatureProperties') {
|
||||
rootCreature = getRootCreatureAncestor(parentDoc);
|
||||
} else {
|
||||
throw `${parentRef.collection} is not a valid parent collection`
|
||||
}
|
||||
} else {
|
||||
throw `${parentRef.collection} is not a valid parent collection`
|
||||
}
|
||||
assertEditPermission(rootCreature, this.userId);
|
||||
|
||||
// Add the folder first if we need to
|
||||
if (insertFolderFirst){
|
||||
if (insertFolderFirst) {
|
||||
let order = getHighestOrder({
|
||||
collection: CreatureProperties,
|
||||
ancestorId: parentRef.id,
|
||||
@@ -113,7 +113,7 @@ const insertPropertyAsChildOfTag = new ValidatedMethod({
|
||||
order,
|
||||
});
|
||||
// Make the folder our new parent
|
||||
let newParentRef = {id, collection: 'creatureProperties'};
|
||||
let newParentRef = { id, collection: 'creatureProperties' };
|
||||
ancestors = [parentRef, newParentRef];
|
||||
parentRef = newParentRef;
|
||||
creatureProperty.order = order + 1;
|
||||
@@ -122,14 +122,14 @@ const insertPropertyAsChildOfTag = new ValidatedMethod({
|
||||
creatureProperty.parent = parentRef;
|
||||
creatureProperty.ancestors = ancestors;
|
||||
|
||||
return insertPropertyWork({
|
||||
return insertPropertyWork({
|
||||
property: creatureProperty,
|
||||
creature: rootCreature,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export function insertPropertyWork({property, creature}){
|
||||
export function insertPropertyWork({ property, creature }) {
|
||||
delete property._id;
|
||||
property.dirty = true;
|
||||
let _id = CreatureProperties.insert(property);
|
||||
|
||||
@@ -7,57 +7,57 @@ import { RefSchema } from '/imports/api/parenting/ChildSchema.js';
|
||||
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
|
||||
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
|
||||
import {
|
||||
setLineageOfDocs,
|
||||
getAncestry,
|
||||
renewDocIds
|
||||
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 fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
|
||||
|
||||
const insertPropertyFromLibraryNode = new ValidatedMethod({
|
||||
name: 'creatureProperties.insertPropertyFromLibraryNode',
|
||||
validate: new SimpleSchema({
|
||||
name: 'creatureProperties.insertPropertyFromLibraryNode',
|
||||
validate: new SimpleSchema({
|
||||
nodeIds: {
|
||||
type: Array,
|
||||
max: 20,
|
||||
},
|
||||
'nodeIds.$': {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
parentRef: {
|
||||
type: RefSchema,
|
||||
},
|
||||
'nodeIds.$': {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
parentRef: {
|
||||
type: RefSchema,
|
||||
},
|
||||
order: {
|
||||
type: Number,
|
||||
optional: true,
|
||||
},
|
||||
}).validator(),
|
||||
}).validator(),
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({nodeIds, parentRef, order}) {
|
||||
// get the new ancestry for the properties
|
||||
let {parentDoc, ancestors} = getAncestry({parentRef});
|
||||
run({ nodeIds, parentRef, order }) {
|
||||
// get the new ancestry for the properties
|
||||
let { parentDoc, ancestors } = getAncestry({ parentRef });
|
||||
|
||||
// Check permission to edit
|
||||
// Check permission to edit
|
||||
let rootCreature;
|
||||
if (parentRef.collection === 'creatures'){
|
||||
if (parentRef.collection === 'creatures') {
|
||||
rootCreature = parentDoc;
|
||||
} else if (parentRef.collection === 'creatureProperties'){
|
||||
} else if (parentRef.collection === 'creatureProperties') {
|
||||
rootCreature = getRootCreatureAncestor(parentDoc);
|
||||
} else {
|
||||
throw `${parentRef.collection} is not a valid parent collection`
|
||||
}
|
||||
} else {
|
||||
throw `${parentRef.collection} is not a valid parent collection`
|
||||
}
|
||||
assertEditPermission(rootCreature, this.userId);
|
||||
|
||||
// {libraryId: hasViewPermission}
|
||||
//let libraryPermissionMemoir = {};
|
||||
let node;
|
||||
nodeIds.forEach(nodeId => {
|
||||
nodeIds.forEach(nodeId => {
|
||||
// TODO: Check library view permission for each node before starting
|
||||
node = insertPropertyFromNode(nodeId, ancestors, order);
|
||||
});
|
||||
@@ -70,18 +70,18 @@ const insertPropertyFromLibraryNode = new ValidatedMethod({
|
||||
collection: CreatureProperties,
|
||||
ancestorId: rootCreature._id,
|
||||
});
|
||||
// Return the docId of the last property, the inserted root property
|
||||
return rootId;
|
||||
},
|
||||
// Return the docId of the last property, the inserted root property
|
||||
return rootId;
|
||||
},
|
||||
});
|
||||
|
||||
function insertPropertyFromNode(nodeId, ancestors, order){
|
||||
function insertPropertyFromNode(nodeId, ancestors, order) {
|
||||
// Fetch the library node and its decendents, provided they have not been
|
||||
// removed
|
||||
// TODO: Check permission to read the library this node is in
|
||||
let node = LibraryNodes.findOne({
|
||||
_id: nodeId,
|
||||
removed: {$ne: true},
|
||||
removed: { $ne: true },
|
||||
});
|
||||
if (!node) {
|
||||
if (Meteor.isClient) return;
|
||||
@@ -95,16 +95,17 @@ function insertPropertyFromNode(nodeId, ancestors, order){
|
||||
let oldParent = node.parent;
|
||||
let nodes = LibraryNodes.find({
|
||||
'ancestors.id': nodeId,
|
||||
removed: {$ne: true},
|
||||
removed: { $ne: true },
|
||||
}).fetch();
|
||||
|
||||
// Convert all references into actual nodes
|
||||
nodes = reifyNodeReferences(nodes);
|
||||
|
||||
// The root node is first in the array of nodes
|
||||
// It must get the first generated ID to prevent flickering
|
||||
nodes = [node, ...nodes];
|
||||
|
||||
// Convert all references into actual nodes
|
||||
nodes = reifyNodeReferences(nodes);
|
||||
|
||||
// set libraryNodeIds
|
||||
storeLibraryNodeReferences(nodes);
|
||||
|
||||
@@ -118,11 +119,11 @@ function insertPropertyFromNode(nodeId, ancestors, order){
|
||||
// Give the docs new IDs without breaking internal references
|
||||
renewDocIds({
|
||||
docArray: nodes,
|
||||
collectionMap: {'libraryNodes': 'creatureProperties'}
|
||||
collectionMap: { 'libraryNodes': 'creatureProperties' }
|
||||
});
|
||||
|
||||
// Order the root node
|
||||
if (order === undefined){
|
||||
if (order === undefined) {
|
||||
setDocToLastOrder({
|
||||
collection: CreatureProperties,
|
||||
doc: node,
|
||||
@@ -139,7 +140,7 @@ function insertPropertyFromNode(nodeId, ancestors, order){
|
||||
return node;
|
||||
}
|
||||
|
||||
function storeLibraryNodeReferences(nodes){
|
||||
function storeLibraryNodeReferences(nodes) {
|
||||
nodes.forEach(node => {
|
||||
if (node.libraryNodeId) return;
|
||||
node.libraryNodeId = node._id;
|
||||
@@ -154,7 +155,7 @@ function dirtyNodes(nodes) {
|
||||
|
||||
// Covert node references into actual nodes
|
||||
// TODO: check permissions for each library a reference node references
|
||||
function reifyNodeReferences(nodes, visitedRefs = new Set(), depth = 0){
|
||||
function reifyNodeReferences(nodes, visitedRefs = new Set(), depth = 0) {
|
||||
depth += 1;
|
||||
// New nodes added this function
|
||||
let newNodes = [];
|
||||
@@ -165,9 +166,9 @@ function reifyNodeReferences(nodes, visitedRefs = new Set(), depth = 0){
|
||||
if (node.type !== 'reference') return true;
|
||||
|
||||
// We have gone too deep, keep the reference node as an error
|
||||
if (depth >= 10){
|
||||
if (depth >= 10) {
|
||||
if (Meteor.isClient) console.warn('Reference depth limit exceeded');
|
||||
node.cache = {error: 'Reference depth limit exceeded'};
|
||||
node.cache = { error: 'Reference depth limit exceeded' };
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -177,17 +178,17 @@ function reifyNodeReferences(nodes, visitedRefs = new Set(), depth = 0){
|
||||
referencedNode.order = node.order;
|
||||
// We are definitely replacing this node, so add it to the list
|
||||
visitedRefs.add(node._id);
|
||||
} catch (e){
|
||||
node.cache = {error: e.reason || e.message || e.toString()};
|
||||
} catch (e) {
|
||||
node.cache = { error: e.reason || e.message || e.toString() };
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get all the descendants of the referenced node
|
||||
let descendents = LibraryNodes.find({
|
||||
'ancestors.id': referencedNode._id,
|
||||
removed: {$ne: true},
|
||||
removed: { $ne: true },
|
||||
}, {
|
||||
sort: {order: 1},
|
||||
sort: { order: 1 },
|
||||
}).fetch();
|
||||
|
||||
// We are adding the referenced node and its descendants
|
||||
@@ -195,20 +196,20 @@ function reifyNodeReferences(nodes, visitedRefs = new Set(), depth = 0){
|
||||
|
||||
// re-map all the ancestors to parent the new sub-tree into our existing
|
||||
// node tree
|
||||
setLineageOfDocs({
|
||||
docArray: addedNodes,
|
||||
newAncestry: node.ancestors,
|
||||
oldParent: referencedNode.parent,
|
||||
});
|
||||
setLineageOfDocs({
|
||||
docArray: addedNodes,
|
||||
newAncestry: node.ancestors,
|
||||
oldParent: referencedNode.parent,
|
||||
});
|
||||
|
||||
// Filter all the looped references
|
||||
addedNodes = addedNodes.filter(addedNode => {
|
||||
// Add all non-reference nodes
|
||||
if (addedNode.type !== 'reference'){
|
||||
if (addedNode.type !== 'reference') {
|
||||
return true;
|
||||
}
|
||||
// If this exact reference has already been resolved before, filter it out
|
||||
if (visitedRefs.has(addedNode._id)){
|
||||
if (visitedRefs.has(addedNode._id)) {
|
||||
return false;
|
||||
} else {
|
||||
// Otherwise mark it as visited, and keep it
|
||||
|
||||
@@ -5,28 +5,28 @@ import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js
|
||||
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
|
||||
|
||||
const pullFromProperty = new ValidatedMethod({
|
||||
name: 'creatureProperties.pull',
|
||||
validate: null,
|
||||
name: 'creatureProperties.pull',
|
||||
validate: null,
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({_id, path, itemId}){
|
||||
run({ _id, path, itemId }) {
|
||||
// Permissions
|
||||
let property = CreatureProperties.findOne(_id);
|
||||
let property = CreatureProperties.findOne(_id);
|
||||
let rootCreature = getRootCreatureAncestor(property);
|
||||
assertEditPermission(rootCreature, this.userId);
|
||||
|
||||
// Do work
|
||||
CreatureProperties.update(_id, {
|
||||
CreatureProperties.update(_id, {
|
||||
$pull: { [path.join('.')]: { _id: itemId } },
|
||||
$set: { dirty: true }
|
||||
}, {
|
||||
selector: {type: property.type},
|
||||
getAutoValues: false,
|
||||
});
|
||||
}
|
||||
}, {
|
||||
selector: { type: property.type },
|
||||
getAutoValues: false,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export default pullFromProperty;
|
||||
|
||||
@@ -6,16 +6,16 @@ import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/ge
|
||||
import { get } from 'lodash';
|
||||
|
||||
const pushToProperty = new ValidatedMethod({
|
||||
name: 'creatureProperties.push',
|
||||
validate: null,
|
||||
name: 'creatureProperties.push',
|
||||
validate: null,
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({_id, path, value}){
|
||||
run({ _id, path, value }) {
|
||||
// Permissions
|
||||
let property = CreatureProperties.findOne(_id);
|
||||
let property = CreatureProperties.findOne(_id);
|
||||
let rootCreature = getRootCreatureAncestor(property);
|
||||
assertEditPermission(rootCreature, this.userId);
|
||||
|
||||
@@ -25,10 +25,10 @@ const pushToProperty = new ValidatedMethod({
|
||||
let schema = CreatureProperties.simpleSchema(property);
|
||||
let maxCount = schema.get(joinedPath, 'maxCount');
|
||||
|
||||
if (Number.isFinite(maxCount)){
|
||||
if (Number.isFinite(maxCount)) {
|
||||
let array = get(property, path);
|
||||
let currentCount = array ? array.length : 0;
|
||||
if (currentCount >= maxCount){
|
||||
if (currentCount >= maxCount) {
|
||||
throw new Meteor.Error(
|
||||
'Array is full',
|
||||
`Cannot have more than ${maxCount} values`
|
||||
@@ -37,13 +37,13 @@ const pushToProperty = new ValidatedMethod({
|
||||
}
|
||||
|
||||
// Do work
|
||||
CreatureProperties.update(_id, {
|
||||
CreatureProperties.update(_id, {
|
||||
$push: { [joinedPath]: value },
|
||||
$set: { dirty: true },
|
||||
}, {
|
||||
selector: {type: property.type},
|
||||
});
|
||||
}
|
||||
}, {
|
||||
selector: { type: property.type },
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export default pushToProperty;
|
||||
|
||||
@@ -7,18 +7,18 @@ import { restore } from '/imports/api/parenting/softRemove.js';
|
||||
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
|
||||
|
||||
const restoreProperty = new ValidatedMethod({
|
||||
name: 'creatureProperties.restore',
|
||||
validate: new SimpleSchema({
|
||||
_id: SimpleSchema.RegEx.Id
|
||||
}).validator(),
|
||||
name: 'creatureProperties.restore',
|
||||
validate: new SimpleSchema({
|
||||
_id: SimpleSchema.RegEx.Id
|
||||
}).validator(),
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({_id}){
|
||||
run({ _id }) {
|
||||
// Permissions
|
||||
let property = CreatureProperties.findOne(_id);
|
||||
let property = CreatureProperties.findOne(_id);
|
||||
let rootCreature = getRootCreatureAncestor(property);
|
||||
assertEditPermission(rootCreature, this.userId);
|
||||
|
||||
@@ -30,7 +30,7 @@ const restoreProperty = new ValidatedMethod({
|
||||
$set: { dirty: true }
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export default restoreProperty;
|
||||
|
||||
@@ -17,20 +17,20 @@ const selectAmmoItem = new ValidatedMethod({
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({actionId, itemId, itemConsumedIndex}) {
|
||||
run({ actionId, itemId, itemConsumedIndex }) {
|
||||
// Permissions
|
||||
let action = CreatureProperties.findOne(actionId);
|
||||
let action = CreatureProperties.findOne(actionId);
|
||||
let rootCreature = getRootCreatureAncestor(action);
|
||||
assertEditPermission(rootCreature, this.userId);
|
||||
assertEditPermission(rootCreature, this.userId);
|
||||
|
||||
// Check that this index has a document to edit
|
||||
let itemConsumed = action.resources.itemsConsumed[itemConsumedIndex];
|
||||
if (!itemConsumed){
|
||||
if (!itemConsumed) {
|
||||
throw new Meteor.Error('Resouce not found',
|
||||
'Could not set ammo, because the ammo document was not found');
|
||||
}
|
||||
let itemToLink = CreatureProperties.findOne(itemId);
|
||||
if (!itemToLink){
|
||||
if (!itemToLink) {
|
||||
throw new Meteor.Error('Item not found',
|
||||
'Could not set ammo: the item was not found');
|
||||
}
|
||||
|
||||
@@ -7,24 +7,24 @@ import { softRemove } from '/imports/api/parenting/softRemove.js';
|
||||
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
|
||||
|
||||
const softRemoveProperty = new ValidatedMethod({
|
||||
name: 'creatureProperties.softRemove',
|
||||
validate: new SimpleSchema({
|
||||
_id: SimpleSchema.RegEx.Id
|
||||
}).validator(),
|
||||
name: 'creatureProperties.softRemove',
|
||||
validate: new SimpleSchema({
|
||||
_id: SimpleSchema.RegEx.Id
|
||||
}).validator(),
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({_id}){
|
||||
run({ _id }) {
|
||||
// Permissions
|
||||
let property = CreatureProperties.findOne(_id);
|
||||
let property = CreatureProperties.findOne(_id);
|
||||
let rootCreature = getRootCreatureAncestor(property);
|
||||
assertEditPermission(rootCreature, this.userId);
|
||||
|
||||
// Do work
|
||||
softRemove({_id, collection: CreatureProperties});
|
||||
}
|
||||
softRemove({ _id, collection: CreatureProperties });
|
||||
}
|
||||
});
|
||||
|
||||
export default softRemoveProperty;
|
||||
|
||||
@@ -6,28 +6,28 @@ import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/ge
|
||||
|
||||
const updateCreatureProperty = new ValidatedMethod({
|
||||
name: 'creatureProperties.update',
|
||||
validate({_id, path}){
|
||||
if (!_id) throw new Meteor.Error('No _id', '_id is required');
|
||||
// We cannot change these fields with a simple update
|
||||
switch (path[0]){
|
||||
case 'type':
|
||||
validate({ _id, path }) {
|
||||
if (!_id) throw new Meteor.Error('No _id', '_id is required');
|
||||
// We cannot change these fields with a simple update
|
||||
switch (path[0]) {
|
||||
case 'type':
|
||||
case 'order':
|
||||
case 'parent':
|
||||
case 'ancestors':
|
||||
case 'damage':
|
||||
throw new Meteor.Error('Permission denied',
|
||||
'This property can\'t be updated directly');
|
||||
}
|
||||
case 'damage':
|
||||
throw new Meteor.Error('Permission denied',
|
||||
'This property can\'t be updated directly');
|
||||
}
|
||||
},
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({_id, path, value}) {
|
||||
run({ _id, path, value }) {
|
||||
// Permission
|
||||
let property = CreatureProperties.findOne(_id, {
|
||||
fields: {type: 1, ancestors: 1}
|
||||
fields: { type: 1, ancestors: 1 }
|
||||
});
|
||||
let rootCreature = getRootCreatureAncestor(property);
|
||||
assertEditPermission(rootCreature, this.userId);
|
||||
@@ -35,14 +35,14 @@ const updateCreatureProperty = new ValidatedMethod({
|
||||
let pathString = path.join('.');
|
||||
let modifier;
|
||||
// unset empty values
|
||||
if (value === null || value === undefined){
|
||||
modifier = { $unset: {[pathString]: 1}, $set: { dirty: true } };
|
||||
if (value === null || value === undefined) {
|
||||
modifier = { $unset: { [pathString]: 1 }, $set: { dirty: true } };
|
||||
} else {
|
||||
modifier = { $set: {[pathString]: value, dirty: true } };
|
||||
modifier = { $set: { [pathString]: value, dirty: true } };
|
||||
}
|
||||
CreatureProperties.update(_id, modifier, {
|
||||
selector: {type: property.type},
|
||||
});
|
||||
CreatureProperties.update(_id, modifier, {
|
||||
selector: { type: property.type },
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@ import computeCreature from '/imports/api/engine/computeCreature.js';
|
||||
* Recomputes all ancestor creatures of this property
|
||||
*/
|
||||
export default function recomputeCreaturesByProperty(property){
|
||||
for (let ref of property.ancestors){
|
||||
if (ref.collection === 'creatures') {
|
||||
computeCreature.call(ref.id);
|
||||
}
|
||||
}
|
||||
for (let ref of property.ancestors){
|
||||
if (ref.collection === 'creatures') {
|
||||
computeCreature.call(ref.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,21 +8,26 @@ import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
||||
let Creatures = new Mongo.Collection('creatures');
|
||||
|
||||
let CreatureSettingsSchema = new SimpleSchema({
|
||||
//slowed down by carrying too much?
|
||||
useVariantEncumbrance: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
//hide spellcasting tab
|
||||
hideSpellcasting: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
// Swap around the modifier and stat
|
||||
swapStatAndModifier: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
//slowed down by carrying too much?
|
||||
useVariantEncumbrance: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
//hide spellcasting tab
|
||||
hideSpellcasting: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
//hide rest buttons
|
||||
hideRestButtons: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
// Swap around the modifier and stat
|
||||
swapStatAndModifier: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
// Hide all the unused stats
|
||||
hideUnusedStats: {
|
||||
type: Boolean,
|
||||
@@ -58,28 +63,28 @@ let CreatureSettingsSchema = new SimpleSchema({
|
||||
});
|
||||
|
||||
let CreatureSchema = new SimpleSchema({
|
||||
// Strings
|
||||
name: {
|
||||
type: String,
|
||||
defaultValue: '',
|
||||
optional: true,
|
||||
// Strings
|
||||
name: {
|
||||
type: String,
|
||||
defaultValue: '',
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.name,
|
||||
},
|
||||
alignment: {
|
||||
type: String,
|
||||
optional: true,
|
||||
},
|
||||
alignment: {
|
||||
type: String,
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.name,
|
||||
},
|
||||
gender: {
|
||||
type: String,
|
||||
optional: true,
|
||||
},
|
||||
gender: {
|
||||
type: String,
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.name,
|
||||
},
|
||||
picture: {
|
||||
type: String,
|
||||
optional: true,
|
||||
},
|
||||
picture: {
|
||||
type: String,
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.url,
|
||||
},
|
||||
},
|
||||
avatarPicture: {
|
||||
type: String,
|
||||
optional: true,
|
||||
@@ -90,37 +95,37 @@ let CreatureSchema = new SimpleSchema({
|
||||
allowedLibraries: {
|
||||
type: Array,
|
||||
optional: true,
|
||||
maxCount: 100,
|
||||
},
|
||||
'allowedLibraries.$': {
|
||||
type: String,
|
||||
maxCount: 100,
|
||||
},
|
||||
'allowedLibraries.$': {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
allowedLibraryCollections: {
|
||||
type: Array,
|
||||
},
|
||||
allowedLibraryCollections: {
|
||||
type: Array,
|
||||
optional: true,
|
||||
maxCount: 100,
|
||||
},
|
||||
'allowedLibraryCollections.$': {
|
||||
type: String,
|
||||
maxCount: 100,
|
||||
},
|
||||
'allowedLibraryCollections.$': {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
|
||||
// Mechanics
|
||||
deathSave: {
|
||||
type: deathSaveSchema,
|
||||
defaultValue: {},
|
||||
},
|
||||
// Mechanics
|
||||
deathSave: {
|
||||
type: deathSaveSchema,
|
||||
defaultValue: {},
|
||||
},
|
||||
// Stats that are computed and denormalised outside of recomputation
|
||||
denormalizedStats: {
|
||||
type: Object,
|
||||
defaultValue: {},
|
||||
},
|
||||
// Sum of all XP gained by this character
|
||||
'denormalizedStats.xp': {
|
||||
type: SimpleSchema.Integer,
|
||||
defaultValue: 0,
|
||||
},
|
||||
'denormalizedStats.xp': {
|
||||
type: SimpleSchema.Integer,
|
||||
defaultValue: 0,
|
||||
},
|
||||
// Sum of all levels granted by milestone XP
|
||||
'denormalizedStats.milestoneLevels': {
|
||||
type: SimpleSchema.Integer,
|
||||
@@ -133,24 +138,24 @@ let CreatureSchema = new SimpleSchema({
|
||||
},
|
||||
// Version of computation engine that was last used to compute this creature
|
||||
computeVersion: {
|
||||
type: String,
|
||||
type: String,
|
||||
optional: true,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
defaultValue: 'pc',
|
||||
allowedValues: ['pc', 'npc', 'monster'],
|
||||
},
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
defaultValue: 'pc',
|
||||
allowedValues: ['pc', 'npc', 'monster'],
|
||||
},
|
||||
damageMultipliers: {
|
||||
type: Object,
|
||||
blackbox: true,
|
||||
defaultValue: {}
|
||||
blackbox: true,
|
||||
defaultValue: {}
|
||||
},
|
||||
variables: {
|
||||
type: Object,
|
||||
blackbox: true,
|
||||
defaultValue: {}
|
||||
},
|
||||
variables: {
|
||||
type: Object,
|
||||
blackbox: true,
|
||||
defaultValue: {}
|
||||
},
|
||||
computeErrors: {
|
||||
type: Array,
|
||||
optional: true,
|
||||
@@ -161,7 +166,7 @@ let CreatureSchema = new SimpleSchema({
|
||||
'computeErrors.$.type': {
|
||||
type: String,
|
||||
},
|
||||
'computeErrors.$.details' : {
|
||||
'computeErrors.$.details': {
|
||||
type: Object,
|
||||
blackbox: true,
|
||||
optional: true,
|
||||
@@ -178,11 +183,11 @@ let CreatureSchema = new SimpleSchema({
|
||||
optional: true,
|
||||
},
|
||||
|
||||
// Settings
|
||||
settings: {
|
||||
type: CreatureSettingsSchema,
|
||||
defaultValue: {},
|
||||
},
|
||||
// Settings
|
||||
settings: {
|
||||
type: CreatureSettingsSchema,
|
||||
defaultValue: {},
|
||||
},
|
||||
});
|
||||
|
||||
CreatureSchema.extend(ColorSchema);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
||||
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||
import {assertEditPermission} from '/imports/api/sharing/sharingPermissions.js';
|
||||
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
|
||||
import SimpleSchema from 'simpl-schema';
|
||||
import simpleSchemaMixin from '/imports/api/creature/mixins/simpleSchemaMixin.js';
|
||||
|
||||
@@ -36,8 +36,8 @@ const changeAllowedLibraries = new ValidatedMethod({
|
||||
numRequests: 10,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({_id, allowedLibraries, allowedLibraryCollections}) {
|
||||
let creature = Creatures.findOne(_id);
|
||||
run({ _id, allowedLibraries, allowedLibraryCollections }) {
|
||||
let creature = Creatures.findOne(_id);
|
||||
assertEditPermission(creature, this.userId);
|
||||
let $set;
|
||||
if (allowedLibraries) {
|
||||
@@ -48,7 +48,7 @@ const changeAllowedLibraries = new ValidatedMethod({
|
||||
$set.allowedLibraryCollections = allowedLibraryCollections;
|
||||
}
|
||||
if (!$set) return;
|
||||
Creatures.update(_id, {$set});
|
||||
Creatures.update(_id, { $set });
|
||||
},
|
||||
});
|
||||
|
||||
@@ -68,7 +68,7 @@ const toggleAllUserLibraries = new ValidatedMethod({
|
||||
numRequests: 10,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({_id, value}) {
|
||||
run({ _id, value }) {
|
||||
if (value) {
|
||||
Creatures.update(_id, {
|
||||
$unset: {
|
||||
@@ -87,4 +87,4 @@ const toggleAllUserLibraries = new ValidatedMethod({
|
||||
},
|
||||
});
|
||||
|
||||
export {changeAllowedLibraries, toggleAllUserLibraries};
|
||||
export { changeAllowedLibraries, toggleAllUserLibraries };
|
||||
|
||||
@@ -51,7 +51,7 @@ const insertCreature = new ValidatedMethod({
|
||||
allowedLibraries,
|
||||
allowedLibraryCollections,
|
||||
});
|
||||
|
||||
|
||||
// Insert experience to get character to starting level
|
||||
if (startingLevel) {
|
||||
insertExperienceForCreature({
|
||||
@@ -70,7 +70,7 @@ const insertCreature = new ValidatedMethod({
|
||||
let baseId, rulesetSlot;
|
||||
defaultCharacterProperties(creatureId).forEach(prop => {
|
||||
let id = CreatureProperties.insert(prop);
|
||||
if (prop.name === 'Ruleset'){
|
||||
if (prop.name === 'Ruleset') {
|
||||
baseId = id;
|
||||
rulesetSlot = prop;
|
||||
}
|
||||
@@ -81,7 +81,7 @@ const insertCreature = new ValidatedMethod({
|
||||
insertDefaultRuleset(creatureId, baseId, userId, rulesetSlot);
|
||||
}
|
||||
|
||||
return creatureId;
|
||||
return creatureId;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -95,7 +95,7 @@ function insertDefaultRuleset(creatureId, baseId, userId, slot) {
|
||||
const ruleset = fillCursor.fetch()[0]
|
||||
insertPropertyFromLibraryNode.call({
|
||||
nodeIds: [ruleset._id],
|
||||
parentRef: {id: baseId, collection: 'creatureProperties'},
|
||||
parentRef: { id: baseId, collection: 'creatureProperties' },
|
||||
order: 0.5,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,12 +3,10 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
||||
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
|
||||
import { groupBy, remove, union } from 'lodash';
|
||||
import {
|
||||
getCreature, getVariables, getPropertiesOfType
|
||||
} from '/imports/api/engine/loadCreatures.js';
|
||||
import { CreatureLogSchema, insertCreatureLogWork } from '/imports/api/creature/log/CreatureLogs.js';
|
||||
import { applyTrigger } from '/imports/api/engine/actions/applyTriggers.js';
|
||||
import { union } from 'lodash';
|
||||
import ActionContext from '/imports/api/engine/actions/ActionContext.js';
|
||||
import { applyTriggers } from '/imports/api/engine/actions/applyTriggers.js';
|
||||
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
|
||||
|
||||
const restCreature = new ValidatedMethod({
|
||||
name: 'creature.methods.rest',
|
||||
@@ -27,143 +25,146 @@ const restCreature = new ValidatedMethod({
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({creatureId, restType}) {
|
||||
run({ creatureId, restType }) {
|
||||
// Get action context
|
||||
const actionContext = new ActionContext(creatureId, [creatureId], this);
|
||||
// Check permissions
|
||||
let creature = getCreature(creatureId);
|
||||
assertEditPermission(creature, this.userId);
|
||||
assertEditPermission(actionContext.creature, this.userId);
|
||||
|
||||
// Add the variables to the creature document
|
||||
const variables = getVariables(creatureId);
|
||||
delete variables._id;
|
||||
delete variables._creatureId;
|
||||
creature.variables = variables;
|
||||
const scope = creature.variables;
|
||||
// Join, sort, and apply before triggers
|
||||
const beforeTriggers = union(
|
||||
actionContext.triggers.anyRest?.before, actionContext.triggers[restType]?.before
|
||||
).sort((a, b) => a.order - b.order);
|
||||
applyTriggers(beforeTriggers, null, actionContext);
|
||||
|
||||
// Get the triggers
|
||||
let triggers = getPropertiesOfType(creatureId, 'trigger');
|
||||
remove(triggers, trigger =>
|
||||
trigger.event !== 'anyRest' &&
|
||||
trigger.event !== 'longRest' &&
|
||||
trigger.event !== 'shortRest'
|
||||
);
|
||||
triggers = groupBy(triggers, 'event');
|
||||
for (let type in triggers) {
|
||||
triggers[type] = groupBy(triggers[type], 'timing')
|
||||
}
|
||||
|
||||
// Create the log
|
||||
const log = CreatureLogSchema.clean({
|
||||
creatureId: creature._id,
|
||||
creatureName: creature.name,
|
||||
// Rest
|
||||
actionContext.addLog({
|
||||
name: restType === 'shortRest' ? 'Short rest' : 'Long rest',
|
||||
});
|
||||
doRestWork(restType, actionContext);
|
||||
|
||||
const targets = [creature];
|
||||
// Join, sort, and apply after triggers
|
||||
const afterTriggers = union(
|
||||
actionContext.triggers.anyRest?.after, actionContext.triggers[restType]?.after
|
||||
).sort((a, b) => a.order - b.order);
|
||||
applyTriggers(afterTriggers, null, actionContext);
|
||||
|
||||
applyTriggers(triggers, restType, 'before', { creature, targets, scope, log });
|
||||
doRestWork(creature, restType);
|
||||
applyTriggers(triggers, restType, 'after', { creature, targets, scope, log });
|
||||
|
||||
insertCreatureLogWork({log, creature, method: this});
|
||||
// Insert log
|
||||
actionContext.writeLog();
|
||||
},
|
||||
});
|
||||
|
||||
function applyTriggers(triggers, restType, timing, opts) {
|
||||
// Get matching triggers
|
||||
let selectedTriggers = triggers[restType]?.[timing] || [];
|
||||
// Get any rest triggers as well
|
||||
selectedTriggers = union(selectedTriggers, triggers['anyRest']?.[timing]);
|
||||
selectedTriggers.sort((a, b) => a.order - b.order);
|
||||
// Apply the triggers
|
||||
selectedTriggers.forEach(trigger => {
|
||||
applyTrigger(trigger, opts)
|
||||
});
|
||||
}
|
||||
|
||||
function doRestWork(creature, restType) {
|
||||
function doRestWork(restType, actionContext) {
|
||||
const creatureId = actionContext.creature._id;
|
||||
// Long rests reset short rest properties as well
|
||||
let resetFilter;
|
||||
if (restType === 'shortRest'){
|
||||
if (restType === 'shortRest') {
|
||||
resetFilter = 'shortRest'
|
||||
} else {
|
||||
resetFilter = {$in: ['shortRest', 'longRest']}
|
||||
resetFilter = { $in: ['shortRest', 'longRest'] }
|
||||
}
|
||||
resetProperties(creatureId, resetFilter, actionContext);
|
||||
|
||||
// Reset half hit dice on a long rest, starting with the highest dice
|
||||
if (restType === 'longRest') {
|
||||
resetHitDice(creatureId, actionContext);
|
||||
}
|
||||
}
|
||||
|
||||
export function resetProperties(creatureId, resetFilter, actionContext) {
|
||||
// Only apply to active properties
|
||||
let filter = {
|
||||
'ancestors.id': creature._id,
|
||||
const filter = {
|
||||
'ancestors.id': creatureId,
|
||||
reset: resetFilter,
|
||||
removed: { $ne: true },
|
||||
inactive: { $ne: true },
|
||||
};
|
||||
// update all attribute's damage
|
||||
filter.type = 'attribute';
|
||||
CreatureProperties.update(filter, {
|
||||
$set: {
|
||||
damage: 0,
|
||||
dirty: true,
|
||||
}
|
||||
}, {
|
||||
selector: {type: 'attribute'},
|
||||
multi: true,
|
||||
const attributeFilter = {
|
||||
...filter,
|
||||
type: 'attribute',
|
||||
damage: { $nin: [0, undefined] },
|
||||
}
|
||||
CreatureProperties.find(attributeFilter).forEach(prop => {
|
||||
damagePropertyWork({
|
||||
prop,
|
||||
operation: 'increment',
|
||||
value: -prop.damage ?? 0,
|
||||
actionContext,
|
||||
logFunction(increment) {
|
||||
actionContext.addLog({
|
||||
name: prop.name,
|
||||
value: increment < 0 ? `Restored ${-increment}` : `Removed ${-increment}`
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
// Update all action-like properties' usesUsed
|
||||
filter.type = {$in: [
|
||||
'action',
|
||||
'attack',
|
||||
'spell'
|
||||
]};
|
||||
CreatureProperties.update(filter, {
|
||||
const actionFilter = {
|
||||
...filter,
|
||||
type: {
|
||||
$in: ['action', 'spell']
|
||||
},
|
||||
usesUsed: { $nin: [0, undefined] },
|
||||
};
|
||||
CreatureProperties.find(actionFilter, {
|
||||
fields: { name: 1, usesUsed: 1 }
|
||||
}).forEach(prop => {
|
||||
actionContext.addLog({
|
||||
name: prop.name,
|
||||
value: prop.usesUsed >= 0 ? `Restored ${prop.usesUsed} uses` : `Removed ${-prop.usesUsed} uses`
|
||||
});
|
||||
});
|
||||
CreatureProperties.update(actionFilter, {
|
||||
$set: {
|
||||
usesUsed: 0,
|
||||
dirty: true,
|
||||
}
|
||||
}, {
|
||||
selector: {type: 'action'},
|
||||
selector: { type: 'action' },
|
||||
multi: true,
|
||||
});
|
||||
// Reset half hit dice on a long rest, starting with the highest dice
|
||||
if (restType === 'longRest'){
|
||||
let hitDice = CreatureProperties.find({
|
||||
'ancestors.id': creature._id,
|
||||
type: 'attribute',
|
||||
attributeType: 'hitDice',
|
||||
removed: {$ne: true},
|
||||
inactive: {$ne: true},
|
||||
}, {
|
||||
fields: {
|
||||
hitDiceSize: 1,
|
||||
damage: 1,
|
||||
value: 1,
|
||||
}
|
||||
|
||||
function resetHitDice(creatureId, actionContext) {
|
||||
let hitDice = CreatureProperties.find({
|
||||
'ancestors.id': creatureId,
|
||||
type: 'attribute',
|
||||
attributeType: 'hitDice',
|
||||
removed: { $ne: true },
|
||||
inactive: { $ne: true },
|
||||
}).fetch();
|
||||
// Use a collator to do sorting in natural order
|
||||
let collator = new Intl.Collator('en', {
|
||||
numeric: true, sensitivity: 'base'
|
||||
});
|
||||
// Get the hit dice in decending order of hitDiceSize
|
||||
let compare = (a, b) => collator.compare(b.hitDiceSize, a.hitDiceSize)
|
||||
hitDice.sort(compare);
|
||||
// Get the total number of hit dice that can be recovered this rest
|
||||
let totalHd = hitDice.reduce((sum, hd) => sum + (hd.total || 0), 0);
|
||||
let resetMultiplier = actionContext.creature.settings.hitDiceResetMultiplier || 0.5;
|
||||
let recoverableHd = Math.max(Math.floor(totalHd * resetMultiplier), 1);
|
||||
// recover each hit dice in turn until the recoverable amount is used up
|
||||
let amountToRecover;
|
||||
hitDice.forEach(hd => {
|
||||
if (!recoverableHd) return;
|
||||
amountToRecover = Math.min(recoverableHd, hd.damage ?? 0);
|
||||
if (!amountToRecover) return;
|
||||
recoverableHd -= amountToRecover;
|
||||
damagePropertyWork({
|
||||
prop: hd,
|
||||
operation: 'increment',
|
||||
value: -amountToRecover,
|
||||
actionContext,
|
||||
logFunction(increment) {
|
||||
actionContext.addLog({
|
||||
name: hd.name,
|
||||
value: increment < 0 ? `Restored ${-increment} hit dice` : `Removed ${increment} hit dice`
|
||||
});
|
||||
}
|
||||
}).fetch();
|
||||
// Use a collator to do sorting in natural order
|
||||
let collator = new Intl.Collator('en', {
|
||||
numeric: true, sensitivity: 'base'
|
||||
});
|
||||
// Get the hit dice in decending order of hitDiceSize
|
||||
let compare = (a, b) => collator.compare(b.hitDiceSize, a.hitDiceSize)
|
||||
hitDice.sort(compare);
|
||||
// Get the total number of hit dice that can be recovered this rest
|
||||
let totalHd = hitDice.reduce((sum, hd) => sum + (hd.value || 0), 0);
|
||||
let resetMultiplier = creature.settings.hitDiceResetMultiplier || 0.5;
|
||||
let recoverableHd = Math.max(Math.floor(totalHd*resetMultiplier), 1);
|
||||
// recover each hit dice in turn until the recoverable amount is used up
|
||||
let amountToRecover, resultingDamage;
|
||||
hitDice.forEach(hd => {
|
||||
if (!recoverableHd) return;
|
||||
amountToRecover = Math.min(recoverableHd, hd.damage || 0);
|
||||
if (!amountToRecover) return;
|
||||
recoverableHd -= amountToRecover;
|
||||
resultingDamage = hd.damage - amountToRecover;
|
||||
CreatureProperties.update(hd._id, {
|
||||
$set: {
|
||||
damage: resultingDamage,
|
||||
dirty: true,
|
||||
}
|
||||
}, {
|
||||
selector: {type: 'attribute'},
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export default restCreature;
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
||||
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||
import {assertEditPermission} from '/imports/api/sharing/sharingPermissions.js';
|
||||
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
|
||||
|
||||
const updateCreature = new ValidatedMethod({
|
||||
name: 'creatures.update',
|
||||
validate({_id, path}){
|
||||
if (!_id) return false;
|
||||
// Allowed fields
|
||||
let allowedFields = [
|
||||
validate({ _id, path }) {
|
||||
if (!_id) return false;
|
||||
// Allowed fields
|
||||
let allowedFields = [
|
||||
'name',
|
||||
'alignment',
|
||||
'gender',
|
||||
@@ -17,26 +17,26 @@ const updateCreature = new ValidatedMethod({
|
||||
'color',
|
||||
'settings',
|
||||
];
|
||||
if (!allowedFields.includes(path[0])){
|
||||
throw new Meteor.Error('Creatures.methods.update.denied',
|
||||
'This field can\'t be updated using this method');
|
||||
}
|
||||
if (!allowedFields.includes(path[0])) {
|
||||
throw new Meteor.Error('Creatures.methods.update.denied',
|
||||
'This field can\'t be updated using this method');
|
||||
}
|
||||
},
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({_id, path, value}) {
|
||||
let creature = Creatures.findOne(_id);
|
||||
run({ _id, path, value }) {
|
||||
let creature = Creatures.findOne(_id);
|
||||
assertEditPermission(creature, this.userId);
|
||||
if (value === undefined || value === null){
|
||||
if (value === undefined || value === null) {
|
||||
Creatures.update(_id, {
|
||||
$unset: {[path.join('.')]: 1},
|
||||
$unset: { [path.join('.')]: 1 },
|
||||
});
|
||||
} else {
|
||||
Creatures.update(_id, {
|
||||
$set: {[path.join('.')]: value},
|
||||
$set: { [path.join('.')]: value },
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -8,17 +8,17 @@ import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
||||
let Experiences = new Mongo.Collection('experiences');
|
||||
|
||||
let ExperienceSchema = new SimpleSchema({
|
||||
name: {
|
||||
type: String,
|
||||
optional: true,
|
||||
name: {
|
||||
type: String,
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.name,
|
||||
},
|
||||
// The amount of XP this experience gives
|
||||
xp: {
|
||||
type: SimpleSchema.Integer,
|
||||
optional: true,
|
||||
},
|
||||
// The amount of XP this experience gives
|
||||
xp: {
|
||||
type: SimpleSchema.Integer,
|
||||
optional: true,
|
||||
min: 0,
|
||||
},
|
||||
},
|
||||
// Setting levels instead of value grants whole levels
|
||||
levels: {
|
||||
type: SimpleSchema.Integer,
|
||||
@@ -26,17 +26,17 @@ let ExperienceSchema = new SimpleSchema({
|
||||
min: 0,
|
||||
index: 1,
|
||||
},
|
||||
// The real-world date that it occured, usually sorted by date
|
||||
date: {
|
||||
type: Date,
|
||||
autoValue: function() {
|
||||
// If the date isn't set, set it to now
|
||||
if (!this.isSet) {
|
||||
return new Date();
|
||||
}
|
||||
},
|
||||
// The real-world date that it occured, usually sorted by date
|
||||
date: {
|
||||
type: Date,
|
||||
autoValue: function () {
|
||||
// If the date isn't set, set it to now
|
||||
if (!this.isSet) {
|
||||
return new Date();
|
||||
}
|
||||
},
|
||||
index: 1,
|
||||
},
|
||||
},
|
||||
creatureId: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
@@ -46,8 +46,8 @@ let ExperienceSchema = new SimpleSchema({
|
||||
|
||||
Experiences.attachSchema(ExperienceSchema);
|
||||
|
||||
const insertExperienceForCreature = function({experience, creatureId, userId}){
|
||||
if (experience.xp){
|
||||
const insertExperienceForCreature = function ({ experience, creatureId }) {
|
||||
if (experience.xp) {
|
||||
Creatures.update(creatureId, {
|
||||
$inc: { 'denormalizedStats.xp': experience.xp },
|
||||
$set: { dirty: true },
|
||||
@@ -84,16 +84,16 @@ const insertExperience = new ValidatedMethod({
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({experience, creatureIds}) {
|
||||
run({ experience, creatureIds }) {
|
||||
let userId = this.userId;
|
||||
if (!userId) {
|
||||
throw new Meteor.Error('Experiences.methods.insert.denied',
|
||||
'You need to be logged in to insert an experience');
|
||||
'You need to be logged in to insert an experience');
|
||||
}
|
||||
let insertedIds = [];
|
||||
creatureIds.forEach(creatureId => {
|
||||
assertEditPermission(creatureId, userId);
|
||||
let id = insertExperienceForCreature({experience, creatureId, userId});
|
||||
let id = insertExperienceForCreature({ experience, creatureId, userId });
|
||||
insertedIds.push(id);
|
||||
});
|
||||
return insertedIds;
|
||||
@@ -113,17 +113,17 @@ const removeExperience = new ValidatedMethod({
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({experienceId}) {
|
||||
run({ experienceId }) {
|
||||
let userId = this.userId;
|
||||
if (!userId) {
|
||||
throw new Meteor.Error('Experiences.methods.remove.denied',
|
||||
'You need to be logged in to remove an experience');
|
||||
'You need to be logged in to remove an experience');
|
||||
}
|
||||
let experience = Experiences.findOne(experienceId);
|
||||
if (!experience) return;
|
||||
let creatureId = experience.creatureId
|
||||
assertEditPermission(creatureId, userId);
|
||||
if (experience.xp){
|
||||
if (experience.xp) {
|
||||
Creatures.update(creatureId, {
|
||||
$inc: { 'denormalizedStats.xp': -experience.xp },
|
||||
$set: { dirty: true },
|
||||
@@ -154,11 +154,11 @@ const recomputeExperiences = new ValidatedMethod({
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({creatureId}) {
|
||||
run({ creatureId }) {
|
||||
let userId = this.userId;
|
||||
if (!userId) {
|
||||
throw new Meteor.Error('Experiences.methods.recompute.denied',
|
||||
'You need to be logged in to recompute a creature\'s experiences');
|
||||
'You need to be logged in to recompute a creature\'s experiences');
|
||||
}
|
||||
assertEditPermission(creatureId, userId);
|
||||
|
||||
@@ -167,16 +167,18 @@ const recomputeExperiences = new ValidatedMethod({
|
||||
Experiences.find({
|
||||
creatureId
|
||||
}, {
|
||||
fields: {xp: 1, levels: 1}
|
||||
fields: { xp: 1, levels: 1 }
|
||||
}).forEach(experience => {
|
||||
xp += experience.xp || 0;
|
||||
milestoneLevels += experience.levels || 0;
|
||||
});
|
||||
Creatures.update(creatureId, {$set: {
|
||||
'denormalizedStats.xp': xp,
|
||||
'denormalizedStats.milestoneLevels': milestoneLevels,
|
||||
dirty: true,
|
||||
}});
|
||||
Creatures.update(creatureId, {
|
||||
$set: {
|
||||
'denormalizedStats.xp': xp,
|
||||
'denormalizedStats.milestoneLevels': milestoneLevels,
|
||||
dirty: true,
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -2,33 +2,33 @@ import SimpleSchema from 'simpl-schema';
|
||||
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
||||
|
||||
let ExperienceSchema = new SimpleSchema({
|
||||
title: {
|
||||
type: String,
|
||||
optional: true,
|
||||
title: {
|
||||
type: String,
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.name,
|
||||
},
|
||||
// Potentially long description of the event
|
||||
description: {
|
||||
type: String,
|
||||
optional: true,
|
||||
},
|
||||
// Potentially long description of the event
|
||||
description: {
|
||||
type: String,
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.description,
|
||||
},
|
||||
// The real-world date that it occured
|
||||
date: {
|
||||
type: Date,
|
||||
autoValue: function() {
|
||||
// If the date isn't set, set it to now
|
||||
if (!this.isSet) {
|
||||
return new Date();
|
||||
}
|
||||
},
|
||||
},
|
||||
// The date in-world of this event
|
||||
worldDate: {
|
||||
type: String,
|
||||
optional: true,
|
||||
},
|
||||
// The real-world date that it occured
|
||||
date: {
|
||||
type: Date,
|
||||
autoValue: function () {
|
||||
// If the date isn't set, set it to now
|
||||
if (!this.isSet) {
|
||||
return new Date();
|
||||
}
|
||||
},
|
||||
},
|
||||
// The date in-world of this event
|
||||
worldDate: {
|
||||
type: String,
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.name,
|
||||
},
|
||||
},
|
||||
// Tags to better find this entry later
|
||||
tags: {
|
||||
type: Array,
|
||||
|
||||
@@ -4,13 +4,14 @@ import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables
|
||||
import LogContentSchema from '/imports/api/creature/log/LogContentSchema.js';
|
||||
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
||||
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
||||
import {assertEditPermission} from '/imports/api/creature/creatures/creaturePermissions.js';
|
||||
import {parse, prettifyParseError} from '/imports/parser/parser.js';
|
||||
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
|
||||
import { parse, prettifyParseError } from '/imports/parser/parser.js';
|
||||
import resolve, { toString } from '/imports/parser/resolve.js';
|
||||
const PER_CREATURE_LOG_LIMIT = 100;
|
||||
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
||||
|
||||
if (Meteor.isServer){
|
||||
const PER_CREATURE_LOG_LIMIT = 100;
|
||||
|
||||
if (Meteor.isServer) {
|
||||
var sendWebhookAsCreature = require('/imports/server/discord/sendWebhook.js').sendWebhookAsCreature;
|
||||
}
|
||||
|
||||
@@ -25,17 +26,17 @@ let CreatureLogSchema = new SimpleSchema({
|
||||
'content.$': {
|
||||
type: LogContentSchema,
|
||||
},
|
||||
// The real-world date that it occured, usually sorted by date
|
||||
date: {
|
||||
type: Date,
|
||||
autoValue: function() {
|
||||
// If the date isn't set, set it to now
|
||||
if (!this.isSet) {
|
||||
return new Date();
|
||||
}
|
||||
},
|
||||
// The real-world date that it occured, usually sorted by date
|
||||
date: {
|
||||
type: Date,
|
||||
autoValue: function () {
|
||||
// If the date isn't set, set it to now
|
||||
if (!this.isSet) {
|
||||
return new Date();
|
||||
}
|
||||
},
|
||||
index: 1,
|
||||
},
|
||||
},
|
||||
creatureId: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
@@ -50,36 +51,47 @@ let CreatureLogSchema = new SimpleSchema({
|
||||
|
||||
CreatureLogs.attachSchema(CreatureLogSchema);
|
||||
|
||||
function removeOldLogs(creatureId){
|
||||
function removeOldLogs(creatureId) {
|
||||
// Find the first log that is over the limit
|
||||
let firstExpiredLog = CreatureLogs.find({
|
||||
creatureId
|
||||
}, {
|
||||
sort: {date: -1},
|
||||
sort: { date: -1 },
|
||||
skip: PER_CREATURE_LOG_LIMIT,
|
||||
});
|
||||
if (!firstExpiredLog) return;
|
||||
// Remove all logs older than the one over the limit
|
||||
CreatureLogs.remove({
|
||||
creatureId,
|
||||
date: {$lte: firstExpiredLog.date},
|
||||
date: { $lte: firstExpiredLog.date },
|
||||
});
|
||||
}
|
||||
|
||||
function logToMessageData(log){
|
||||
function logToMessageData(log) {
|
||||
let embed = {
|
||||
fields: [],
|
||||
};
|
||||
log.content.forEach(field => {
|
||||
log.content.forEach((field, index) => {
|
||||
// Empty character for blank names
|
||||
if (!field.name) field.name = '\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] };
|
||||
}
|
||||
|
||||
function logWebhook({log, creature}){
|
||||
if (Meteor.isServer){
|
||||
function logWebhook({ log, creature }) {
|
||||
if (Meteor.isServer) {
|
||||
sendWebhookAsCreature({
|
||||
creature,
|
||||
data: logToMessageData(log),
|
||||
@@ -94,47 +106,57 @@ const insertCreatureLog = new ValidatedMethod({
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
validate: new SimpleSchema({
|
||||
log: CreatureLogSchema.omit('date'),
|
||||
}).validator(),
|
||||
run({log}){
|
||||
validate: new SimpleSchema({
|
||||
log: CreatureLogSchema.omit('date'),
|
||||
}).validator(),
|
||||
run({ log }) {
|
||||
const creatureId = log.creatureId;
|
||||
const creature = Creatures.findOne(creatureId, {fields: {
|
||||
readers: 1,
|
||||
writers: 1,
|
||||
owner: 1,
|
||||
'settings.discordWebhook': 1,
|
||||
name: 1,
|
||||
avatarPicture: 1,
|
||||
}});
|
||||
const creature = Creatures.findOne(creatureId, {
|
||||
fields: {
|
||||
readers: 1,
|
||||
writers: 1,
|
||||
owner: 1,
|
||||
'settings.discordWebhook': 1,
|
||||
name: 1,
|
||||
avatarPicture: 1,
|
||||
}
|
||||
});
|
||||
assertEditPermission(creature, this.userId);
|
||||
// Build the new log
|
||||
let id = insertCreatureLogWork({log, creature, method: this})
|
||||
let id = insertCreatureLogWork({ log, creature, method: this })
|
||||
return id;
|
||||
},
|
||||
});
|
||||
|
||||
export function insertCreatureLogWork({log, creature, method}){
|
||||
export function insertCreatureLogWork({ log, creature, method }) {
|
||||
// Build the new log
|
||||
if (typeof log === 'string'){
|
||||
log = {content: [{value: log}]};
|
||||
if (typeof log === 'string') {
|
||||
log = { content: [{ value: log }] };
|
||||
}
|
||||
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();
|
||||
|
||||
// Insert it
|
||||
let id = CreatureLogs.insert(log);
|
||||
if (Meteor.isServer){
|
||||
if (Meteor.isServer) {
|
||||
method?.unblock();
|
||||
removeOldLogs(creature._id);
|
||||
logWebhook({log, creature});
|
||||
logWebhook({ log, creature });
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
|
||||
function equalIgnoringWhitespace(a, b){
|
||||
function equalIgnoringWhitespace(a, b) {
|
||||
if (typeof a !== 'string' || typeof b !== 'string') return a === b;
|
||||
return a.replace(/\s/g,'') === b.replace(/\s/g, '');
|
||||
return a.replace(/\s/g, '') === b.replace(/\s/g, '');
|
||||
}
|
||||
|
||||
const logRoll = new ValidatedMethod({
|
||||
@@ -144,33 +166,35 @@ const logRoll = new ValidatedMethod({
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
validate: new SimpleSchema({
|
||||
roll: {
|
||||
type: String,
|
||||
},
|
||||
creatureId: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
}).validator(),
|
||||
run({roll, creatureId}){
|
||||
const creature = Creatures.findOne(creatureId, {fields: {
|
||||
readers: 1,
|
||||
writers: 1,
|
||||
owner: 1,
|
||||
'settings.discordWebhook': 1,
|
||||
name: 1,
|
||||
avatarPicture: 1,
|
||||
}});
|
||||
validate: new SimpleSchema({
|
||||
roll: {
|
||||
type: String,
|
||||
},
|
||||
creatureId: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
}).validator(),
|
||||
run({ roll, creatureId }) {
|
||||
const creature = Creatures.findOne(creatureId, {
|
||||
fields: {
|
||||
readers: 1,
|
||||
writers: 1,
|
||||
owner: 1,
|
||||
'settings.discordWebhook': 1,
|
||||
name: 1,
|
||||
avatarPicture: 1,
|
||||
}
|
||||
});
|
||||
assertEditPermission(creature, this.userId);
|
||||
const variables = CreatureVariables.findOne({ _creatureId: creatureId });
|
||||
let logContent = []
|
||||
let parsedResult = undefined;
|
||||
try {
|
||||
parsedResult = parse(roll);
|
||||
} catch (e){
|
||||
} catch (e) {
|
||||
let error = prettifyParseError(e);
|
||||
logContent.push({name: 'Parse Error', value: error});
|
||||
logContent.push({ name: 'Parse Error', value: error });
|
||||
}
|
||||
if (parsedResult) try {
|
||||
let {
|
||||
@@ -184,19 +208,19 @@ const logRoll = new ValidatedMethod({
|
||||
logContent.push({
|
||||
value: compiledString
|
||||
});
|
||||
let {result: rolled} = resolve('roll', compiled, variables, context);
|
||||
let { result: rolled } = resolve('roll', compiled, variables, context);
|
||||
let rolledString = toString(rolled);
|
||||
if (rolledString !== compiledString) logContent.push({
|
||||
value: rolledString
|
||||
});
|
||||
let {result} = resolve('reduce', rolled, variables, context);
|
||||
let { result } = resolve('reduce', rolled, variables, context);
|
||||
let resultString = toString(result);
|
||||
if (resultString !== rolledString) logContent.push({
|
||||
value: resultString
|
||||
});
|
||||
} catch (e){
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
logContent = [{name: 'Calculation error'}];
|
||||
logContent = [{ name: 'Calculation error' }];
|
||||
}
|
||||
const log = {
|
||||
content: logContent,
|
||||
@@ -204,11 +228,11 @@ const logRoll = new ValidatedMethod({
|
||||
date: new Date(),
|
||||
};
|
||||
|
||||
let id = insertCreatureLogWork({log, creature, method: this});
|
||||
let id = insertCreatureLogWork({ log, creature, method: this });
|
||||
|
||||
return id;
|
||||
},
|
||||
});
|
||||
|
||||
export default CreatureLogs;
|
||||
export { CreatureLogSchema, insertCreatureLog, logRoll, PER_CREATURE_LOG_LIMIT};
|
||||
export { CreatureLogSchema, insertCreatureLog, logRoll, PER_CREATURE_LOG_LIMIT };
|
||||
|
||||
324
app/imports/api/docs/Docs.js
Normal file
324
app/imports/api/docs/Docs.js
Normal file
@@ -0,0 +1,324 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { Mongo } from 'meteor/mongo';
|
||||
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
||||
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
||||
import SimpleSchema from 'simpl-schema';
|
||||
import { softRemove } from '/imports/api/parenting/softRemove.js';
|
||||
import SoftRemovableSchema from '/imports/api/parenting/SoftRemovableSchema.js';
|
||||
import { storedIconsSchema } from '/imports/api/icons/Icons.js';
|
||||
import '/imports/api/library/methods/index.js';
|
||||
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
||||
import { restore } from '/imports/api/parenting/softRemove.js';
|
||||
import { reorderDocs } from '/imports/api/parenting/order.js';
|
||||
import { getAncestry } from '/imports/api/parenting/parenting.js';
|
||||
|
||||
const Docs = new Mongo.Collection('docs');
|
||||
|
||||
const RefSchema = new SimpleSchema({
|
||||
id: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
index: 1
|
||||
},
|
||||
collection: {
|
||||
type: String,
|
||||
max: STORAGE_LIMITS.collectionName,
|
||||
},
|
||||
urlName: {
|
||||
type: String,
|
||||
regEx: /[a-z]+(?:[a-z]|-)+/,
|
||||
min: 2,
|
||||
max: STORAGE_LIMITS.variableName,
|
||||
optional: true,
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
max: STORAGE_LIMITS.description,
|
||||
optional: true,
|
||||
},
|
||||
});
|
||||
|
||||
let ChildSchema = new SimpleSchema({
|
||||
order: {
|
||||
type: Number,
|
||||
},
|
||||
parent: {
|
||||
type: RefSchema,
|
||||
optional: true,
|
||||
},
|
||||
ancestors: {
|
||||
type: Array,
|
||||
defaultValue: [],
|
||||
maxCount: STORAGE_LIMITS.ancestorCount,
|
||||
},
|
||||
'ancestors.$': {
|
||||
type: RefSchema,
|
||||
},
|
||||
});
|
||||
|
||||
let DocSchema = new SimpleSchema({
|
||||
_id: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
max: STORAGE_LIMITS.description,
|
||||
},
|
||||
urlName: {
|
||||
type: String,
|
||||
regEx: /[a-z]+(?:[a-z]|-)+/,
|
||||
min: 2,
|
||||
max: STORAGE_LIMITS.variableName,
|
||||
},
|
||||
href: {
|
||||
type: String,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
optional: true,
|
||||
},
|
||||
published: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
icon: {
|
||||
type: storedIconsSchema,
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.icon,
|
||||
},
|
||||
});
|
||||
|
||||
let schema = new SimpleSchema({});
|
||||
schema.extend(DocSchema);
|
||||
schema.extend(ChildSchema);
|
||||
schema.extend(SoftRemovableSchema);
|
||||
Docs.attachSchema(schema);
|
||||
|
||||
function assertDocsEditPermission(userId) {
|
||||
if (!userId || typeof userId !== 'string') throw new Meteor.Error('No user id provided');
|
||||
const user = Meteor.users.findOne(userId);
|
||||
if (!user) throw new Meteor.Error('User does not exist');
|
||||
if (!user?.roles?.includes?.('docsWriter')) throw ('Permission denied')
|
||||
}
|
||||
|
||||
function getDocLink(doc, urlName) {
|
||||
if (!urlName) urlName = doc.urlName;
|
||||
const address = ['/docs'];
|
||||
doc.ancestors?.forEach(a => {
|
||||
address.push(a.urlName);
|
||||
});
|
||||
address.push(urlName);
|
||||
return address.join('/');
|
||||
}
|
||||
|
||||
function rebuildDocAncestors(docId) {
|
||||
const newDoc = Docs.findOne(docId);
|
||||
Docs.find({ 'ancestors.id': docId }).forEach(doc => {
|
||||
doc.ancestors.forEach((a, i) => {
|
||||
if (a.id === docId) {
|
||||
Docs.update(doc._id, {
|
||||
$set: {
|
||||
[`ancestors.${i}`]: {
|
||||
id: newDoc._id,
|
||||
collection: 'docs',
|
||||
urlName: newDoc.urlName,
|
||||
name: newDoc.name,
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
doc = Docs.findOne(doc._id);
|
||||
const newLink = getDocLink(doc);
|
||||
if (doc.href !== newLink) {
|
||||
Docs.update(doc._id, { $set: { href: newLink } })
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add a means of seeding new servers with documentation
|
||||
if (Meteor.isClient) {
|
||||
Docs.getJsonDocs = function () {
|
||||
return JSON.stringify(Docs.find({}).fetch(), null, 2);
|
||||
}
|
||||
} else if (Meteor.isServer) {
|
||||
Meteor.startup(() => {
|
||||
if (!Docs.findOne()) {
|
||||
Assets.getText('docs/defaultDocs.json', (error, string) => {
|
||||
const docs = JSON.parse(string)
|
||||
docs.forEach(doc => Docs.insert(doc));
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const insertDoc = new ValidatedMethod({
|
||||
name: 'docs.insert',
|
||||
validate: null,
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({ doc, parentRef }) {
|
||||
delete doc._id;
|
||||
assertDocsEditPermission(this.userId);
|
||||
// get the new ancestry for the properties
|
||||
if (parentRef) {
|
||||
var { ancestors } = getAncestry({
|
||||
parentRef,
|
||||
inheritedFields: { name: 1, urlName: 1 },
|
||||
});
|
||||
}
|
||||
doc.parent = parentRef;
|
||||
doc.ancestors = ancestors;
|
||||
|
||||
const lastOrder = Docs.find({}, { sort: { order: -1 } }).fetch()[0]?.order || 0;
|
||||
doc.order = lastOrder + 1;
|
||||
doc.urlName = 'new-doc-' + (lastOrder + 1);
|
||||
|
||||
doc.href = getDocLink(doc);
|
||||
if (Docs.findOne({ href: doc.href })) {
|
||||
throw new Meteor.Error('Link collision', 'A document with the same URL already exists');
|
||||
}
|
||||
|
||||
const docId = Docs.insert(doc);
|
||||
reorderDocs({
|
||||
collection: Docs,
|
||||
ancestorId: 'root',
|
||||
});
|
||||
return docId;
|
||||
},
|
||||
});
|
||||
|
||||
const updateDoc = new ValidatedMethod({
|
||||
name: 'docs.update',
|
||||
validate({ _id, path }) {
|
||||
if (!_id) return false;
|
||||
// We cannot change these fields with a simple update
|
||||
switch (path[0]) {
|
||||
case '_is':
|
||||
return false;
|
||||
}
|
||||
},
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({ _id, path, value }) {
|
||||
assertDocsEditPermission(this.userId);
|
||||
let pathString = path.join('.');
|
||||
let modifier;
|
||||
// unset empty values
|
||||
if (value === null || value === undefined) {
|
||||
modifier = { $unset: { [pathString]: 1 } };
|
||||
} else {
|
||||
modifier = { $set: { [pathString]: value } };
|
||||
}
|
||||
if (pathString === 'urlName') {
|
||||
const doc = Docs.findOne(_id);
|
||||
const newLink = getDocLink(doc, value);
|
||||
if (Docs.findOne({ href: newLink })) {
|
||||
throw new Meteor.Error('Link collision', 'A document with the same URL already exists');
|
||||
}
|
||||
modifier.$set = modifier.$set || {};
|
||||
modifier.$set.href = newLink;
|
||||
rebuildDocAncestors(_id);
|
||||
}
|
||||
const updates = Docs.update(_id, modifier);
|
||||
if (pathString === 'name' || pathString === 'urlName') {
|
||||
rebuildDocAncestors(_id);
|
||||
}
|
||||
reorderDocs({
|
||||
collection: Docs,
|
||||
ancestorId: 'root',
|
||||
});
|
||||
return updates;
|
||||
},
|
||||
});
|
||||
|
||||
const pushToDoc = new ValidatedMethod({
|
||||
name: 'docs.push',
|
||||
validate: null,
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({ _id, path, value }) {
|
||||
assertDocsEditPermission(this.userId);
|
||||
return Docs.update(_id, {
|
||||
$push: { [path.join('.')]: value },
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const pullFromDoc = new ValidatedMethod({
|
||||
name: 'docs.pull',
|
||||
validate: null,
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({ _id, path, itemId }) {
|
||||
assertDocsEditPermission(this.userId);
|
||||
return Docs.update(_id, {
|
||||
$pull: { [path.join('.')]: { _id: itemId } },
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const softRemoveDoc = new ValidatedMethod({
|
||||
name: 'docs.softRemove',
|
||||
validate: new SimpleSchema({
|
||||
_id: SimpleSchema.RegEx.Id
|
||||
}).validator(),
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({ _id }) {
|
||||
assertDocsEditPermission(this.userId);
|
||||
softRemove({ _id, collection: Docs });
|
||||
reorderDocs({
|
||||
collection: Docs,
|
||||
ancestorId: 'root',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const restoreDoc = new ValidatedMethod({
|
||||
name: 'docs.restore',
|
||||
validate: new SimpleSchema({
|
||||
_id: SimpleSchema.RegEx.Id
|
||||
}).validator(),
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({ _id }) {
|
||||
assertDocsEditPermission(this.userId);
|
||||
restore({ _id, collection: Docs });
|
||||
reorderDocs({
|
||||
collection: Docs,
|
||||
ancestorId: 'root',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export {
|
||||
DocSchema,
|
||||
insertDoc,
|
||||
updateDoc,
|
||||
pushToDoc,
|
||||
pullFromDoc,
|
||||
softRemoveDoc,
|
||||
restoreDoc,
|
||||
};
|
||||
|
||||
export default Docs;
|
||||
78
app/imports/api/engine/actions/ActionContext.js
Normal file
78
app/imports/api/engine/actions/ActionContext.js
Normal file
@@ -0,0 +1,78 @@
|
||||
import { CreatureLogSchema, insertCreatureLogWork } from '/imports/api/creature/log/CreatureLogs.js';
|
||||
import {
|
||||
getCreature, getVariables, getPropertiesOfType
|
||||
} from '/imports/api/engine/loadCreatures.js';
|
||||
import { groupBy, remove } from 'lodash';
|
||||
|
||||
export default class ActionContext{
|
||||
constructor(creatureId, targetIds = [], method) {
|
||||
// Get the creature
|
||||
this.creature = getCreature(creatureId)
|
||||
|
||||
if (!this.creature) {
|
||||
throw new Meteor.Error('No Creature', `No creature could be found with id: ${creatureId}`)
|
||||
}
|
||||
// Create a log
|
||||
this.log = CreatureLogSchema.clean({
|
||||
creatureId: creatureId,
|
||||
creatureName: this.creature.name,
|
||||
});
|
||||
|
||||
// Get the variables of the acting creature
|
||||
this.creature.variables = getVariables(creatureId);
|
||||
delete this.creature.variables._id;
|
||||
delete this.creature.variables._creatureId;
|
||||
// Alias as scope
|
||||
this.scope = this.creature.variables;
|
||||
|
||||
// Get the targets and their variables
|
||||
this.targets = [];
|
||||
targetIds.forEach(targetId => {
|
||||
let target;
|
||||
if (targetId === creatureId) {
|
||||
target = this.creature;
|
||||
} else {
|
||||
target = getCreature(targetId);
|
||||
target.variables = getVariables(targetId);
|
||||
delete target.variables._id;
|
||||
delete target.variables._creatureId;
|
||||
}
|
||||
this.targets.push(target);
|
||||
});
|
||||
|
||||
// Store a reference to the method for inserting the log
|
||||
this.method = method;
|
||||
|
||||
// Get triggers
|
||||
this.triggers = getPropertiesOfType(creatureId, 'trigger');
|
||||
// Remove deleted or inactive triggers
|
||||
remove(this.triggers, trigger => trigger.removed || trigger.inactive);
|
||||
// Sort triggers by order
|
||||
this.triggers.sort((a, b) => a.order - b.order);
|
||||
// Group the triggers into triggers.<event>.<timing> or
|
||||
// triggers.doActionProperty.<propertyType>.<timing>
|
||||
this.triggers = groupBy(this.triggers, 'event');
|
||||
for (let event in this.triggers) {
|
||||
if (event === 'doActionProperty') {
|
||||
this.triggers[event] = groupBy(this.triggers[event], 'actionPropertyType');
|
||||
for (let propertyType in this.triggers[event]) {
|
||||
this.triggers[event][propertyType] = groupBy(this.triggers[event][propertyType], 'timing');
|
||||
}
|
||||
} else {
|
||||
this.triggers[event] = groupBy(this.triggers[event], 'timing');
|
||||
}
|
||||
}
|
||||
}
|
||||
addLog(content) {
|
||||
if (content.name || content.value){
|
||||
this.log.content.push(content);
|
||||
}
|
||||
}
|
||||
writeLog() {
|
||||
insertCreatureLogWork({
|
||||
log: this.log,
|
||||
creature: this.creature,
|
||||
method: this.method,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2,19 +2,22 @@ import action from './applyPropertyByType/applyAction.js';
|
||||
import adjustment from './applyPropertyByType/applyAdjustment.js';
|
||||
import branch from './applyPropertyByType/applyBranch.js';
|
||||
import buff from './applyPropertyByType/applyBuff.js';
|
||||
import buffRemover from './applyPropertyByType/applyBuffRemover.js';
|
||||
import damage from './applyPropertyByType/applyDamage.js';
|
||||
import folder from './applyPropertyByType/applyFolder.js';
|
||||
import note from './applyPropertyByType/applyNote.js';
|
||||
import roll from './applyPropertyByType/applyRoll.js';
|
||||
import savingThrow from './applyPropertyByType/applySavingThrow.js';
|
||||
import toggle from './applyPropertyByType/applyToggle.js';
|
||||
import applyTriggers from '/imports/api/engine/actions/applyTriggers.js';
|
||||
|
||||
const applyPropertyByType = {
|
||||
action,
|
||||
adjustment,
|
||||
branch,
|
||||
buff,
|
||||
buffRemover,
|
||||
damage,
|
||||
folder,
|
||||
note,
|
||||
roll,
|
||||
savingThrow,
|
||||
@@ -22,9 +25,7 @@ const applyPropertyByType = {
|
||||
toggle,
|
||||
};
|
||||
|
||||
export default function applyProperty(node, opts, ...rest) {
|
||||
applyTriggers(node, opts, 'before');
|
||||
opts.scope[`#${node.node.type}`] = node.node;
|
||||
applyPropertyByType[node.node.type]?.(node, opts, ...rest);
|
||||
applyTriggers(node, opts, 'after');
|
||||
export default function applyProperty(node, actionContext, ...rest) {
|
||||
actionContext.scope[`#${node.node.type}`] = node.node;
|
||||
applyPropertyByType[node.node.type]?.(node, actionContext, ...rest);
|
||||
}
|
||||
|
||||
@@ -5,54 +5,62 @@ import applyProperty from '../applyProperty.js';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import { adjustQuantityWork } from '/imports/api/creature/creatureProperties/methods/adjustQuantity.js';
|
||||
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
|
||||
import numberToSignedString from '/imports/ui/utility/numberToSignedString.js';
|
||||
import numberToSignedString from '/imports/api/utility/numberToSignedString.js';
|
||||
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.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, {creature, targets, scope, log}){
|
||||
export default function applyAction(node, actionContext) {
|
||||
applyNodeTriggers(node, 'before', actionContext);
|
||||
const prop = node.node;
|
||||
if (prop.target === 'self') targets = [creature];
|
||||
if (prop.target === 'self') actionContext.targets = [actionContext.creature];
|
||||
const targets = actionContext.targets;
|
||||
|
||||
// Log the name and summary
|
||||
let content = { name: prop.name };
|
||||
if (prop.summary?.text){
|
||||
recalculateInlineCalculations(prop.summary, scope, log);
|
||||
if (prop.summary?.text) {
|
||||
recalculateInlineCalculations(prop.summary, actionContext);
|
||||
content.value = prop.summary.value;
|
||||
}
|
||||
if (content.name || content.value){
|
||||
log.content.push(content);
|
||||
}
|
||||
if (!prop.silent) actionContext.addLog(content);
|
||||
|
||||
// Spend the resources
|
||||
const failed = spendResources({prop, log, scope});
|
||||
const failed = spendResources(prop, actionContext);
|
||||
if (failed) return;
|
||||
|
||||
const attack = prop.attackRoll || prop.attackRollBonus;
|
||||
|
||||
// Attack if there is an attack roll
|
||||
if (attack && attack.calculation){
|
||||
if (targets.length){
|
||||
if (attack && attack.calculation) {
|
||||
if (targets.length) {
|
||||
targets.forEach(target => {
|
||||
applyAttackToTarget({attack, target, scope, log});
|
||||
applyAttackToTarget({ attack, target, actionContext });
|
||||
// Apply the children, but only to the current target
|
||||
applyChildren(node, {creature, targets: [target], scope, log});
|
||||
actionContext.targets = [target];
|
||||
applyChildren(node, actionContext);
|
||||
});
|
||||
} else {
|
||||
applyAttackWithoutTarget({attack, scope, log});
|
||||
applyChildren(node, {creature, targets, scope, log});
|
||||
applyAttackWithoutTarget({ attack, actionContext });
|
||||
applyChildren(node, actionContext);
|
||||
}
|
||||
} else {
|
||||
applyChildren(node, {creature, targets, scope, log});
|
||||
applyChildren(node, actionContext);
|
||||
}
|
||||
if (prop.actionType === 'event' && prop.variableName) {
|
||||
resetProperties(actionContext.creature._id, prop.variableName, actionContext);
|
||||
}
|
||||
}
|
||||
|
||||
function applyAttackWithoutTarget({attack, scope, log}){
|
||||
delete scope['$attackHit'];
|
||||
delete scope['$attackMiss'];
|
||||
delete scope['$criticalHit'];
|
||||
delete scope['$criticalMiss'];
|
||||
delete scope['$attackRoll'];
|
||||
|
||||
recalculateCalculation(attack, scope, log);
|
||||
function applyAttackWithoutTarget({ attack, actionContext }) {
|
||||
delete actionContext.scope['~attackHit'];
|
||||
delete actionContext.scope['~attackMiss'];
|
||||
delete actionContext.scope['~criticalHit'];
|
||||
delete actionContext.scope['~criticalMiss'];
|
||||
delete actionContext.scope['~attackRoll'];
|
||||
|
||||
recalculateCalculation(attack, actionContext);
|
||||
const scope = actionContext.scope;
|
||||
let {
|
||||
resultPrefix,
|
||||
result,
|
||||
@@ -60,34 +68,35 @@ function applyAttackWithoutTarget({attack, scope, log}){
|
||||
criticalMiss,
|
||||
} = rollAttack(attack, scope);
|
||||
let name = criticalHit ? 'Critical Hit!' : criticalMiss ? 'Critical Miss!' : 'To Hit';
|
||||
if (scope['$attackAdvantage'] === 1){
|
||||
if (scope['~attackAdvantage']?.value === 1) {
|
||||
name += ' (Advantage)';
|
||||
} else if(scope['$attackAdvantage'] === -1){
|
||||
} else if (scope['~attackAdvantage']?.value === -1) {
|
||||
name += ' (Disadvantage)';
|
||||
}
|
||||
if (!criticalMiss){
|
||||
scope['$attackHit'] = {value: true}
|
||||
if (!criticalMiss) {
|
||||
scope['~attackHit'] = { value: true }
|
||||
}
|
||||
if (!criticalHit){
|
||||
scope['$attackMiss'] = {value: true};
|
||||
if (!criticalHit) {
|
||||
scope['~attackMiss'] = { value: true };
|
||||
}
|
||||
|
||||
log.content.push({
|
||||
actionContext.addLog({
|
||||
name,
|
||||
value: `${resultPrefix}\n**${result}**`,
|
||||
inline: true,
|
||||
});
|
||||
}
|
||||
|
||||
function applyAttackToTarget({attack, target, scope, log}){
|
||||
delete scope['$attackHit'];
|
||||
delete scope['$attackMiss'];
|
||||
delete scope['$criticalHit'];
|
||||
delete scope['$criticalMiss'];
|
||||
delete scope['$attackDiceRoll'];
|
||||
delete scope['$attackRoll'];
|
||||
function applyAttackToTarget({ attack, target, actionContext }) {
|
||||
const scope = actionContext.scope;
|
||||
delete scope['~attackHit'];
|
||||
delete scope['~attackMiss'];
|
||||
delete scope['~criticalHit'];
|
||||
delete scope['~criticalMiss'];
|
||||
delete scope['~attackDiceRoll'];
|
||||
delete scope['~attackRoll'];
|
||||
|
||||
recalculateCalculation(attack, scope, log);
|
||||
recalculateCalculation(attack, actionContext);
|
||||
|
||||
let {
|
||||
resultPrefix,
|
||||
@@ -96,34 +105,34 @@ function applyAttackToTarget({attack, target, scope, log}){
|
||||
criticalMiss,
|
||||
} = rollAttack(attack, scope);
|
||||
|
||||
if (target.variables.armor){
|
||||
if (target.variables.armor) {
|
||||
const armor = target.variables.armor.value;
|
||||
|
||||
let name = criticalHit ? 'Critical Hit!' :
|
||||
criticalMiss ? 'Critical Miss!' :
|
||||
result > armor ? 'Hit!' : 'Miss!';
|
||||
if (scope['$attackAdvantage'] === 1){
|
||||
result > armor ? 'Hit!' : 'Miss!';
|
||||
if (scope['~attackAdvantage']?.value === 1) {
|
||||
name += ' (Advantage)';
|
||||
} else if(scope['$attackAdvantage'] === -1){
|
||||
} else if (scope['~attackAdvantage']?.value === -1) {
|
||||
name += ' (Disadvantage)';
|
||||
}
|
||||
|
||||
log.content.push({
|
||||
actionContext.addLog({
|
||||
name,
|
||||
value: `${resultPrefix}\n**${result}**`,
|
||||
inline: true,
|
||||
});
|
||||
if (criticalMiss || result < armor){
|
||||
scope['$attackMiss'] = {value: true};
|
||||
if (criticalMiss || result < armor) {
|
||||
scope['~attackMiss'] = { value: true };
|
||||
} else {
|
||||
scope['$attackHit'] = {value: true};
|
||||
scope['~attackHit'] = { value: true };
|
||||
}
|
||||
} else {
|
||||
log.content.push({
|
||||
actionContext.addLog({
|
||||
name: 'Error',
|
||||
value:'Target has no `armor`',
|
||||
value: 'Target has no `armor`',
|
||||
});
|
||||
log.content.push({
|
||||
actionContext.addLog({
|
||||
name: criticalHit ? 'Critical Hit!' : criticalMiss ? 'Critical Miss!' : 'To Hit',
|
||||
value: `${resultPrefix}\n**${result}**`,
|
||||
inline: true,
|
||||
@@ -131,10 +140,10 @@ function applyAttackToTarget({attack, target, scope, log}){
|
||||
}
|
||||
}
|
||||
|
||||
function rollAttack(attack, scope){
|
||||
function rollAttack(attack, scope) {
|
||||
const rollModifierText = numberToSignedString(attack.value, true);
|
||||
let value, resultPrefix;
|
||||
if (scope['$attackAdvantage'] === 1){
|
||||
if (scope['~attackAdvantage']?.value === 1) {
|
||||
const [a, b] = rollDice(2, 20);
|
||||
if (a >= b) {
|
||||
value = a;
|
||||
@@ -143,7 +152,7 @@ function rollAttack(attack, scope){
|
||||
value = b;
|
||||
resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`;
|
||||
}
|
||||
} else if (scope['$attackAdvantage'] === -1){
|
||||
} else if (scope['~attackAdvantage']?.value === -1) {
|
||||
const [a, b] = rollDice(2, 20);
|
||||
if (a <= b) {
|
||||
value = a;
|
||||
@@ -156,43 +165,45 @@ function rollAttack(attack, scope){
|
||||
value = rollDice(1, 20)[0];
|
||||
resultPrefix = `1d20 [${value}] ${rollModifierText}`
|
||||
}
|
||||
scope['$attackRoll'] = {value};
|
||||
scope['~attackDiceRoll'] = { value };
|
||||
const result = value + attack.value;
|
||||
const {criticalHit, criticalMiss} = applyCrits(value, scope);
|
||||
return {resultPrefix, result, value, criticalHit, criticalMiss};
|
||||
scope['~attackRoll'] = { value: result };
|
||||
const { criticalHit, criticalMiss } = applyCrits(value, scope);
|
||||
return { resultPrefix, result, value, criticalHit, criticalMiss };
|
||||
}
|
||||
|
||||
function applyCrits(value, scope){
|
||||
let criticalHitTarget = scope.criticalHitTarget?.value || 20;
|
||||
function applyCrits(value, scope) {
|
||||
const criticalHitTarget = scope['~criticalHitTarget']?.value || 20;
|
||||
let criticalHit = value >= criticalHitTarget;
|
||||
let criticalMiss;
|
||||
if (criticalHit){
|
||||
scope['$criticalHit'] = {value: true};
|
||||
if (criticalHit) {
|
||||
scope['~criticalHit'] = { value: true };
|
||||
} else {
|
||||
criticalMiss = value === 1;
|
||||
if (criticalMiss){
|
||||
scope['$criticalMiss'] = {value: true};
|
||||
if (criticalMiss) {
|
||||
scope['~criticalMiss'] = { value: true };
|
||||
}
|
||||
}
|
||||
return {criticalHit, criticalMiss};
|
||||
return { criticalHit, criticalMiss };
|
||||
}
|
||||
|
||||
function applyChildren(node, args){
|
||||
node.children.forEach(child => applyProperty(child, args));
|
||||
function applyChildren(node, actionContext) {
|
||||
applyNodeTriggers(node, 'after', actionContext);
|
||||
node.children.forEach(child => applyProperty(child, actionContext));
|
||||
}
|
||||
|
||||
function spendResources({prop, log, scope}){
|
||||
function spendResources(prop, actionContext) {
|
||||
// Check Uses
|
||||
if (prop.usesLeft <= 0){
|
||||
log.content.push({
|
||||
if (prop.usesLeft <= 0) {
|
||||
if (!prop.silent) actionContext.addLog({
|
||||
name: 'Error',
|
||||
value: `${prop.name || 'action'} does not have enough uses left`,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
// Resources
|
||||
if (prop.insufficientResources){
|
||||
log.content.push({
|
||||
if (prop.insufficientResources) {
|
||||
if (!prop.silent) actionContext.addLog({
|
||||
name: 'Error',
|
||||
value: 'This creature doesn\'t have sufficient resources to perform this action',
|
||||
});
|
||||
@@ -202,19 +213,17 @@ function spendResources({prop, log, scope}){
|
||||
let itemQuantityAdjustments = [];
|
||||
let spendLog = [];
|
||||
let gainLog = [];
|
||||
let ammoChildren = [];
|
||||
try {
|
||||
prop.resources.itemsConsumed.forEach(itemConsumed => {
|
||||
recalculateCalculation(itemConsumed.quantity, scope, log);
|
||||
if (!itemConsumed.itemId){
|
||||
recalculateCalculation(itemConsumed.quantity, actionContext);
|
||||
if (!itemConsumed.itemId) {
|
||||
throw 'No ammo was selected for this prop';
|
||||
}
|
||||
let item = CreatureProperties.findOne(itemConsumed.itemId);
|
||||
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';
|
||||
}
|
||||
if (!item.equipped){
|
||||
throw 'The selected ammo is not equipped';
|
||||
}
|
||||
if (
|
||||
!itemConsumed.quantity.value ||
|
||||
!isFinite(itemConsumed.quantity.value)
|
||||
@@ -225,17 +234,18 @@ function spendResources({prop, log, scope}){
|
||||
value: itemConsumed.quantity.value,
|
||||
});
|
||||
let logName = item.name;
|
||||
if (itemConsumed.quantity.value > 1 || itemConsumed.quantity.value < -1){
|
||||
if (itemConsumed.quantity.value > 1 || itemConsumed.quantity.value < -1) {
|
||||
logName = item.plural || logName;
|
||||
}
|
||||
if (itemConsumed.quantity.value > 0){
|
||||
if (itemConsumed.quantity.value > 0) {
|
||||
spendLog.push(logName + ': ' + itemConsumed.quantity.value);
|
||||
} else if (itemConsumed.quantity.value < 0){
|
||||
} else if (itemConsumed.quantity.value < 0) {
|
||||
gainLog.push(logName + ': ' + -itemConsumed.quantity.value);
|
||||
}
|
||||
ammoChildren.push(...getItemChildren(item, actionContext, prop));
|
||||
});
|
||||
} catch (e){
|
||||
log.content.push({
|
||||
} catch (e) {
|
||||
actionContext.addLog({
|
||||
name: 'Error',
|
||||
value: e,
|
||||
});
|
||||
@@ -247,13 +257,13 @@ function spendResources({prop, log, scope}){
|
||||
itemQuantityAdjustments.forEach(adjustQuantityWork);
|
||||
|
||||
// Use uses
|
||||
if (prop.usesLeft){
|
||||
if (prop.usesLeft) {
|
||||
CreatureProperties.update(prop._id, {
|
||||
$inc: {usesUsed: 1}
|
||||
$inc: { usesUsed: 1 }
|
||||
}, {
|
||||
selector: prop
|
||||
});
|
||||
log.content.push({
|
||||
if (!prop.silent) actionContext.addLog({
|
||||
name: 'Uses left',
|
||||
value: prop.usesLeft - 1,
|
||||
inline: true,
|
||||
@@ -262,35 +272,68 @@ function spendResources({prop, log, scope}){
|
||||
|
||||
// Damage stats
|
||||
prop.resources.attributesConsumed.forEach(attConsumed => {
|
||||
recalculateCalculation(attConsumed.quantity, scope, log);
|
||||
recalculateCalculation(attConsumed.quantity, actionContext);
|
||||
|
||||
if (!attConsumed.quantity?.value) return;
|
||||
let stat = scope[attConsumed.variableName];
|
||||
if (!stat){
|
||||
let stat = actionContext.scope[attConsumed.variableName];
|
||||
if (!stat) {
|
||||
spendLog.push(stat.name + ': ' + ' not found');
|
||||
return;
|
||||
}
|
||||
damagePropertyWork({
|
||||
property: stat,
|
||||
prop: stat,
|
||||
operation: 'increment',
|
||||
value: attConsumed.quantity.value,
|
||||
actionContext,
|
||||
});
|
||||
if (attConsumed.quantity.value > 0){
|
||||
if (attConsumed.quantity.value > 0) {
|
||||
spendLog.push(stat.name + ': ' + attConsumed.quantity.value);
|
||||
} else if (attConsumed.quantity.value < 0){
|
||||
} else if (attConsumed.quantity.value < 0) {
|
||||
gainLog.push(stat.name + ': ' + -attConsumed.quantity.value);
|
||||
}
|
||||
});
|
||||
|
||||
// Log all the spending
|
||||
if (gainLog.length) log.content.push({
|
||||
if (gainLog.length && !prop.silent) actionContext.addLog({
|
||||
name: 'Gained',
|
||||
value: gainLog.join('\n'),
|
||||
inline: true,
|
||||
});
|
||||
if (spendLog.length) log.content.push({
|
||||
if (spendLog.length && !prop.silent) actionContext.addLog({
|
||||
name: 'Spent',
|
||||
value: spendLog.join('\n'),
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1,41 +1,42 @@
|
||||
import applyProperty from '../applyProperty.js';
|
||||
import recalculateCalculation from './shared/recalculateCalculation.js';
|
||||
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
|
||||
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
|
||||
|
||||
export default function applyAdjustment(node, {
|
||||
creature, targets, scope, log
|
||||
}){
|
||||
export default function applyAdjustment(node, actionContext){
|
||||
applyNodeTriggers(node, 'before', actionContext);
|
||||
const prop = node.node;
|
||||
const damageTargets = prop.target === 'self' ? [creature] : targets;
|
||||
const damageTargets = prop.target === 'self' ? [actionContext.creature] : actionContext.targets;
|
||||
|
||||
if (!prop.amount) {
|
||||
return applyChildren(node, {creature, targets, scope, log});
|
||||
return applyChildren(node, actionContext);
|
||||
}
|
||||
|
||||
// Evaluate the amount
|
||||
recalculateCalculation(prop.amount, scope, log);
|
||||
recalculateCalculation(prop.amount, actionContext);
|
||||
|
||||
const value = +prop.amount.value;
|
||||
if (!isFinite(value)) {
|
||||
return applyChildren(node, {creature, targets, scope, log});
|
||||
return applyChildren(node, actionContext);
|
||||
}
|
||||
|
||||
if (damageTargets?.length) {
|
||||
damageTargets.forEach(target => {
|
||||
let stat = target.variables[prop.stat];
|
||||
if (!stat?.type) {
|
||||
log.content.push({
|
||||
if (!prop.silent) actionContext.addLog({
|
||||
name: 'Error',
|
||||
value: `Could not apply attribute damage, creature does not have \`${prop.stat}\` set`
|
||||
});
|
||||
return applyChildren(node, {creature, targets, scope, log});
|
||||
return applyChildren(node, actionContext);
|
||||
}
|
||||
damagePropertyWork({
|
||||
property: stat,
|
||||
prop: stat,
|
||||
operation: prop.operation,
|
||||
value: value,
|
||||
value,
|
||||
actionContext,
|
||||
});
|
||||
log.content.push({
|
||||
if (!prop.silent) actionContext.addLog({
|
||||
name: 'Attribute damage',
|
||||
value: `${prop.stat}${prop.operation === 'set' ? ' set to' : ''}` +
|
||||
` ${value}`,
|
||||
@@ -43,7 +44,7 @@ export default function applyAdjustment(node, {
|
||||
});
|
||||
});
|
||||
} else {
|
||||
log.content.push({
|
||||
if (!prop.silent) actionContext.addLog({
|
||||
name: 'Attribute damage',
|
||||
value: `${prop.stat}${prop.operation === 'set' ? ' set to' : ''}` +
|
||||
` ${value}`,
|
||||
@@ -51,9 +52,10 @@ export default function applyAdjustment(node, {
|
||||
});
|
||||
}
|
||||
|
||||
return applyChildren(node, {creature, targets, scope, log});
|
||||
return applyChildren(node, actionContext);
|
||||
}
|
||||
|
||||
function applyChildren(node, args){
|
||||
node.children.forEach(child => applyProperty(child, args));
|
||||
function applyChildren(node, actionContext){
|
||||
applyNodeTriggers(node, 'after', actionContext);
|
||||
node.children.forEach(child => applyProperty(child, actionContext));
|
||||
}
|
||||
|
||||
@@ -1,26 +1,27 @@
|
||||
import applyProperty from '../applyProperty.js';
|
||||
import recalculateCalculation from './shared/recalculateCalculation.js';
|
||||
import rollDice from '/imports/parser/rollDice.js';
|
||||
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
|
||||
|
||||
export default function applyBranch(node, {
|
||||
creature, targets, scope, log
|
||||
}){
|
||||
const applyChildren = function(){
|
||||
node.children.forEach(child => applyProperty(child, {
|
||||
creature, targets, scope, log
|
||||
}));
|
||||
export default function applyBranch(node, actionContext) {
|
||||
applyNodeTriggers(node, 'before', actionContext);
|
||||
const applyChildren = function () {
|
||||
applyNodeTriggers(node, 'after', actionContext);
|
||||
node.children.forEach(child => applyProperty(child, actionContext));
|
||||
};
|
||||
const scope = actionContext.scope;
|
||||
const targets = actionContext.targets;
|
||||
const prop = node.node;
|
||||
switch(prop.branchType){
|
||||
switch (prop.branchType) {
|
||||
case 'if':
|
||||
recalculateCalculation(prop.condition, scope, log);
|
||||
recalculateCalculation(prop.condition, actionContext);
|
||||
if (prop.condition?.value) applyChildren();
|
||||
break;
|
||||
case 'index':
|
||||
if (node.children.length){
|
||||
recalculateCalculation(prop.condition, scope, log);
|
||||
if (node.children.length) {
|
||||
recalculateCalculation(prop.condition, actionContext);
|
||||
if (!isFinite(prop.condition?.value)) {
|
||||
log.content.push({
|
||||
actionContext.addLog({
|
||||
name: 'Branch Error',
|
||||
value: 'Index did not resolve into a valid number'
|
||||
});
|
||||
@@ -29,49 +30,47 @@ export default function applyBranch(node, {
|
||||
let index = Math.floor(prop.condition?.value);
|
||||
if (index < 1) index = 1;
|
||||
if (index > node.children.length) index = node.children.length;
|
||||
applyProperty(node.children[index - 1], {
|
||||
creature, targets, scope, log
|
||||
});
|
||||
applyNodeTriggers(node, 'after', actionContext);
|
||||
applyProperty(node.children[index - 1], actionContext);
|
||||
}
|
||||
break;
|
||||
case 'hit':
|
||||
if (scope['$attackHit']?.value){
|
||||
if (!targets.length) log.content.push({value: '**On hit**'});
|
||||
if (scope['~attackHit']?.value) {
|
||||
if (!targets.length && !prop.silent) actionContext.addLog({ value: '**On hit**' });
|
||||
applyChildren();
|
||||
}
|
||||
break;
|
||||
case 'miss':
|
||||
if (scope['$attackMiss']?.value){
|
||||
if (!targets.length) log.content.push({value: '**On miss**'});
|
||||
if (scope['~attackMiss']?.value) {
|
||||
if (!targets.length && !prop.silent) actionContext.addLog({ value: '**On miss**' });
|
||||
applyChildren();
|
||||
}
|
||||
break;
|
||||
case 'failedSave':
|
||||
if (scope['$saveFailed']?.value){
|
||||
if (!targets.length) log.content.push({value: '**On failed save**'});
|
||||
if (scope['~saveFailed']?.value) {
|
||||
if (!targets.length && !prop.silent) actionContext.addLog({ value: '**On failed save**' });
|
||||
applyChildren();
|
||||
}
|
||||
break;
|
||||
case 'successfulSave':
|
||||
if (scope['$saveSucceeded']?.value){
|
||||
if (!targets.length) log.content.push({value: '**On save**',});
|
||||
if (scope['~saveSucceeded']?.value) {
|
||||
if (!targets.length && !prop.silent) actionContext.addLog({ value: '**On save**', });
|
||||
applyChildren();
|
||||
}
|
||||
break;
|
||||
case 'random':
|
||||
if (node.children.length){
|
||||
if (node.children.length) {
|
||||
let index = rollDice(1, node.children.length)[0] - 1;
|
||||
applyProperty(node.children[index], {
|
||||
creature, targets, scope, log
|
||||
});
|
||||
applyNodeTriggers(node, 'after', actionContext);
|
||||
applyProperty(node.children[index], actionContext);
|
||||
}
|
||||
break;
|
||||
case 'eachTarget':
|
||||
if (targets.length){
|
||||
if (targets.length) {
|
||||
targets.forEach(target => {
|
||||
node.children.forEach(child => applyProperty(child, {
|
||||
creature, targets: [target], scope, log
|
||||
}));
|
||||
applyNodeTriggers(node, 'after', actionContext);
|
||||
actionContext.targets = [target]
|
||||
node.children.forEach(child => applyProperty(child, actionContext));
|
||||
});
|
||||
} else {
|
||||
applyChildren();
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import {
|
||||
setLineageOfDocs,
|
||||
renewDocIds
|
||||
setLineageOfDocs,
|
||||
renewDocIds
|
||||
} from '/imports/api/parenting/parenting.js';
|
||||
import {setDocToLastOrder} from '/imports/api/parenting/order.js';
|
||||
import { setDocToLastOrder } from '/imports/api/parenting/order.js';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import computedSchemas from '/imports/api/properties/computedPropertySchemasIndex.js';
|
||||
import applyFnToKey from '/imports/api/engine/computation/utility/applyFnToKey.js';
|
||||
@@ -12,21 +12,34 @@ import symbol from '/imports/parser/parseTree/symbol.js';
|
||||
import logErrors from './shared/logErrors.js';
|
||||
import { insertCreatureLog } from '/imports/api/creature/log/CreatureLogs.js';
|
||||
import cyrb53 from '/imports/api/engine/computation/utility/cyrb53.js';
|
||||
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
|
||||
import INLINE_CALCULATION_REGEX from '/imports/constants/INLINE_CALCULTION_REGEX.js';
|
||||
import recalculateInlineCalculations from './shared/recalculateInlineCalculations.js';
|
||||
|
||||
export default function applyBuff(node, {creature, targets, scope, log}){
|
||||
export default function applyBuff(node, actionContext) {
|
||||
applyNodeTriggers(node, 'before', actionContext);
|
||||
const prop = node.node;
|
||||
let buffTargets = prop.target === 'self' ? [creature] : 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];
|
||||
function addChildrenToPropList(children){
|
||||
function addChildrenToPropList(children, { skipCrystalize } = {}) {
|
||||
children.forEach(child => {
|
||||
if (skipCrystalize) child.node._skipCrystalize = true;
|
||||
propList.push(child.node);
|
||||
addChildrenToPropList(child.children);
|
||||
// recursively add the child's children, but don't crystalize nested buffs
|
||||
addChildrenToPropList(child.children, {
|
||||
skipCrystalize: skipCrystalize || child.node.type === 'buff'
|
||||
});
|
||||
});
|
||||
}
|
||||
addChildrenToPropList(node.children);
|
||||
crystalizeVariables({propList, scope, log});
|
||||
if (!prop.skipCrystalization) {
|
||||
crystalizeVariables({ propList, actionContext });
|
||||
}
|
||||
|
||||
let oldParent = {
|
||||
id: prop.parent.id,
|
||||
@@ -37,12 +50,17 @@ export default function applyBuff(node, {creature, targets, scope, log}){
|
||||
copyNodeListToTarget(propList, target, oldParent);
|
||||
|
||||
//Log the buff
|
||||
if (prop.name || prop.description?.value){
|
||||
if (target._id === creature._id){
|
||||
let logValue = prop.description?.value
|
||||
if (prop.description?.text) {
|
||||
recalculateInlineCalculations(prop.description, actionContext);
|
||||
logValue = prop.description?.value;
|
||||
}
|
||||
if ((prop.name || prop.description?.value) && !prop.silent) {
|
||||
if (target._id === actionContext.creature._id) {
|
||||
// Targeting self
|
||||
log.content.push({
|
||||
actionContext.addLog({
|
||||
name: prop.name,
|
||||
value: prop.description?.value,
|
||||
value: logValue,
|
||||
});
|
||||
} else {
|
||||
// Targeting other
|
||||
@@ -51,19 +69,20 @@ export default function applyBuff(node, {creature, targets, scope, log}){
|
||||
creatureId: target._id,
|
||||
content: [{
|
||||
name: prop.name,
|
||||
value: prop.description?.value,
|
||||
value: logValue,
|
||||
}],
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
applyNodeTriggers(node, 'after', actionContext);
|
||||
|
||||
// Don't apply the children of the buff, they get copied to the target instead
|
||||
}
|
||||
|
||||
function copyNodeListToTarget(propList, target, oldParent){
|
||||
let ancestry = [{collection: 'creatures', id: target._id}];
|
||||
function copyNodeListToTarget(propList, target, oldParent) {
|
||||
let ancestry = [{ collection: 'creatures', id: target._id }];
|
||||
setLineageOfDocs({
|
||||
docArray: propList,
|
||||
newAncestry: ancestry,
|
||||
@@ -83,9 +102,14 @@ function copyNodeListToTarget(propList, target, oldParent){
|
||||
* Replaces all variables with their resolved values
|
||||
* except variables of the form `$target.thing.total` become `thing.total`
|
||||
*/
|
||||
function crystalizeVariables({propList, scope, log}){
|
||||
function crystalizeVariables({ propList, actionContext }) {
|
||||
propList.forEach(prop => {
|
||||
computedSchemas[prop.type].computedFields().forEach( calcKey => {
|
||||
if (prop._skipCrystalize) {
|
||||
delete prop._skipCrystalize;
|
||||
return;
|
||||
}
|
||||
// Iterate through all the calculations and crystalize them
|
||||
computedSchemas[prop.type].computedFields().forEach(calcKey => {
|
||||
applyFnToKey(prop, calcKey, (prop, key) => {
|
||||
const calcObj = get(prop, key);
|
||||
if (!calcObj?.parseNode) return;
|
||||
@@ -95,16 +119,16 @@ function crystalizeVariables({propList, scope, log}){
|
||||
node.parseType !== 'accessor' && node.parseType !== 'symbol'
|
||||
) return node;
|
||||
// Handle variables
|
||||
if (node.name === '$target'){
|
||||
if (node.name === '$target') {
|
||||
// strip $target
|
||||
if (node.parseType === 'accessor'){
|
||||
if (node.parseType === 'accessor') {
|
||||
node.name = node.path.shift();
|
||||
if (!node.path.length){
|
||||
return symbol.create({name: node.name})
|
||||
if (!node.path.length) {
|
||||
return symbol.create({ name: node.name })
|
||||
}
|
||||
} else {
|
||||
// Can't strip symbols
|
||||
log.content.push({
|
||||
actionContext.addLog({
|
||||
name: 'Error',
|
||||
value: 'Variable `$target` should not be used without a property: $target.property',
|
||||
});
|
||||
@@ -112,8 +136,8 @@ function crystalizeVariables({propList, scope, log}){
|
||||
return node;
|
||||
} else {
|
||||
// Resolve all other variables
|
||||
const {result, context} = resolve('reduce', node, scope);
|
||||
logErrors(context.errors, log);
|
||||
const { result, context } = resolve('reduce', node, actionContext.scope);
|
||||
logErrors(context.errors, actionContext);
|
||||
return result;
|
||||
}
|
||||
});
|
||||
@@ -121,5 +145,36 @@ function crystalizeVariables({propList, scope, log}){
|
||||
calcObj.hash = cyrb53(calcObj.calculation);
|
||||
});
|
||||
});
|
||||
// For each key in the schema
|
||||
computedSchemas[prop.type].inlineCalculationFields().forEach(calcKey => {
|
||||
// That ends in .inlineCalculations
|
||||
applyFnToKey(prop, calcKey, (prop, key) => {
|
||||
const inlineCalcObj = get(prop, key);
|
||||
if (!inlineCalcObj) return;
|
||||
|
||||
// If there is no text, skip
|
||||
if (!inlineCalcObj.text) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Replace all the existing calculations
|
||||
let index = -1;
|
||||
inlineCalcObj.text = inlineCalcObj.text.replace(INLINE_CALCULATION_REGEX, () => {
|
||||
index += 1;
|
||||
return `{${inlineCalcObj.inlineCalculations[index].calculation}}`;
|
||||
});
|
||||
|
||||
// Set the value to the uncomputed string
|
||||
inlineCalcObj.value = inlineCalcObj.text;
|
||||
|
||||
// Write a new hash
|
||||
const inlineCalcHash = cyrb53(inlineCalcObj.text);
|
||||
if (inlineCalcHash === inlineCalcObj.hash) {
|
||||
// Skip if nothing changed
|
||||
return;
|
||||
}
|
||||
inlineCalcObj.hash = inlineCalcHash;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import { findLast, difference, intersection, filter } from 'lodash';
|
||||
import applyProperty from '../applyProperty.js';
|
||||
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
|
||||
import { getProperyAncestors, getPropertiesOfType } from '/imports/api/engine/loadCreatures.js';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import { softRemove } from '/imports/api/parenting/softRemove.js';
|
||||
import getEffectivePropTags from '/imports/api/engine/computation/utility/getEffectivePropTags.js';
|
||||
|
||||
export default function applyBuffRemover(node, actionContext) {
|
||||
// Apply triggers
|
||||
applyNodeTriggers(node, 'before', actionContext);
|
||||
|
||||
const prop = node.node;
|
||||
|
||||
// Log Name
|
||||
if (prop.name && !prop.silent){
|
||||
actionContext.addLog({ name: prop.name });
|
||||
}
|
||||
|
||||
// Remove buffs
|
||||
if (prop.targetParentBuff) {
|
||||
// Remove nearest ancestor buff
|
||||
const ancestors = getProperyAncestors(actionContext.creature._id, prop._id);
|
||||
const nearestBuff = findLast(ancestors, ancestor => ancestor.type === 'buff');
|
||||
if (!nearestBuff) {
|
||||
actionContext.addLog({
|
||||
name: 'Error',
|
||||
value: 'Buff remover does not have a parent buff to remove',
|
||||
});
|
||||
return;
|
||||
}
|
||||
removeBuff(nearestBuff, actionContext, prop);
|
||||
} else {
|
||||
// Get all the buffs targeted by tags
|
||||
const allBuffs = getPropertiesOfType(actionContext.creature._id, 'buff');
|
||||
const targetedBuffs = filter(allBuffs, buff => {
|
||||
if (buff.inactive) return false;
|
||||
if (buffRemoverMatchTags(prop, buff)) return true;
|
||||
});
|
||||
// Remove the buffs
|
||||
if (prop.removeAll) {
|
||||
// Remove all matching buffs
|
||||
targetedBuffs.forEach(buff => {
|
||||
removeBuff(buff, actionContext, prop);
|
||||
});
|
||||
} else {
|
||||
// Sort in reverse order
|
||||
targetedBuffs.sort((a, b) => b.order - a.order);
|
||||
// Remove the one with the highest order
|
||||
const buff = targetedBuffs[0];
|
||||
if (buff) {
|
||||
removeBuff(buff, actionContext, prop);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply triggers
|
||||
applyNodeTriggers(node, 'after', actionContext);
|
||||
// Apply children
|
||||
node.children.forEach(child => applyProperty(child, actionContext));
|
||||
}
|
||||
|
||||
function removeBuff(buff, actionContext, prop) {
|
||||
if (!prop.silent) actionContext.addLog({
|
||||
name: 'Removed',
|
||||
value: `${buff.name || 'Buff'}`
|
||||
});
|
||||
softRemove({ _id: buff._id, collection: CreatureProperties });
|
||||
}
|
||||
|
||||
function buffRemoverMatchTags(buffRemover, prop) {
|
||||
let matched = false;
|
||||
const propTags = getEffectivePropTags(prop);
|
||||
// Check the target tags
|
||||
if (
|
||||
!buffRemover.targetTags?.length ||
|
||||
difference(buffRemover.targetTags, propTags).length === 0
|
||||
) {
|
||||
matched = true;
|
||||
}
|
||||
// Check the extra tags
|
||||
buffRemover.extraTags?.forEach(extra => {
|
||||
if (extra.operation === 'OR') {
|
||||
if (matched) return;
|
||||
if (
|
||||
!extra.tags.length ||
|
||||
difference(extra.tags, propTags).length === 0
|
||||
) {
|
||||
matched = true;
|
||||
}
|
||||
} else if (extra.operation === 'NOT') {
|
||||
if (
|
||||
extra.tags.length &&
|
||||
intersection(extra.tags, propTags)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
return matched;
|
||||
}
|
||||
@@ -1,34 +1,38 @@
|
||||
import { some, intersection, difference } from 'lodash';
|
||||
import { some, intersection, difference, remove, includes } from 'lodash';
|
||||
import applyProperty from '../applyProperty.js';
|
||||
import { dealDamageWork } from '/imports/api/creature/creatureProperties/methods/dealDamage.js';
|
||||
import {insertCreatureLog} from '/imports/api/creature/log/CreatureLogs.js';
|
||||
import { insertCreatureLog } from '/imports/api/creature/log/CreatureLogs.js';
|
||||
import resolve, { Context, toString } from '/imports/parser/resolve.js';
|
||||
import logErrors from './shared/logErrors.js';
|
||||
import applyEffectsToCalculationParseNode from '/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js';
|
||||
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
|
||||
import {
|
||||
getPropertiesOfType
|
||||
} from '/imports/api/engine/loadCreatures.js';
|
||||
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
|
||||
import getEffectivePropTags from '/imports/api/engine/computation/utility/getEffectivePropTags.js';
|
||||
|
||||
export default function applyDamage(node, {
|
||||
creature, targets, scope, log
|
||||
}){
|
||||
const applyChildren = function(){
|
||||
node.children.forEach(child => applyProperty(child, {
|
||||
creature, targets, scope, log
|
||||
}));
|
||||
export default function applyDamage(node, actionContext) {
|
||||
applyNodeTriggers(node, 'before', actionContext);
|
||||
const applyChildren = function () {
|
||||
applyNodeTriggers(node, 'after', actionContext);
|
||||
node.children.forEach(child => applyProperty(child, actionContext));
|
||||
};
|
||||
|
||||
const prop = node.node;
|
||||
const scope = actionContext.scope;
|
||||
|
||||
// Skip if there is no parse node to work with
|
||||
if (!prop.amount?.parseNode) return;
|
||||
|
||||
// Choose target
|
||||
let damageTargets = prop.target === 'self' ? [creature] : targets;
|
||||
let damageTargets = prop.target === 'self' ? [actionContext.creature] : actionContext.targets;
|
||||
// Determine if the hit is critical
|
||||
let criticalHit = scope['$criticalHit']?.value &&
|
||||
let criticalHit = scope['~criticalHit']?.value &&
|
||||
prop.damageType !== 'healing' // Can't critically heal
|
||||
;
|
||||
;
|
||||
// Double the damage rolls if the hit is critical
|
||||
let context = new Context({
|
||||
options: {doubleRolls: criticalHit},
|
||||
options: { doubleRolls: criticalHit },
|
||||
});
|
||||
|
||||
// Gather all the lines we need to log into an array
|
||||
@@ -36,24 +40,24 @@ export default function applyDamage(node, {
|
||||
const logName = prop.damageType === 'healing' ? 'Healing' : 'Damage';
|
||||
|
||||
// roll the dice only and store that string
|
||||
applyEffectsToCalculationParseNode(prop.amount, log);
|
||||
const {result: rolled} = resolve('roll', prop.amount.parseNode, scope, context);
|
||||
if (rolled.parseType !== 'constant'){
|
||||
applyEffectsToCalculationParseNode(prop.amount, actionContext.log);
|
||||
const { result: rolled } = resolve('roll', prop.amount.parseNode, scope, context);
|
||||
if (rolled.parseType !== 'constant') {
|
||||
logValue.push(toString(rolled));
|
||||
}
|
||||
logErrors(context.errors, log);
|
||||
logErrors(context.errors, actionContext);
|
||||
|
||||
// Reset the errors so we don't log the same errors twice
|
||||
context.errors = [];
|
||||
|
||||
// Resolve the roll to a final value
|
||||
const {result: reduced} = resolve('reduce', rolled, scope, context);
|
||||
logErrors(context.errors, log);
|
||||
const { result: reduced } = resolve('reduce', rolled, scope, context);
|
||||
logErrors(context.errors, actionContext);
|
||||
|
||||
// Store the result
|
||||
if (reduced.parseType === 'constant'){
|
||||
if (reduced.parseType === 'constant') {
|
||||
prop.amount.value = reduced.value;
|
||||
} else if (reduced.parseType === 'error'){
|
||||
} else if (reduced.parseType === 'error') {
|
||||
prop.amount.value = null;
|
||||
} else {
|
||||
prop.amount.value = toString(reduced);
|
||||
@@ -61,7 +65,7 @@ export default function applyDamage(node, {
|
||||
let damage = +reduced.value;
|
||||
|
||||
// If we didn't end up with a constant of finite amount, give up
|
||||
if (reduced?.parseType !== 'constant' || !isFinite(reduced.value)){
|
||||
if (reduced?.parseType !== 'constant' || !isFinite(reduced.value)) {
|
||||
return applyChildren();
|
||||
}
|
||||
|
||||
@@ -69,18 +73,18 @@ export default function applyDamage(node, {
|
||||
damage = Math.floor(damage);
|
||||
|
||||
// Convert extra damage into the stored type
|
||||
if (prop.damageType === 'extra' && scope['$lastDamageType']) {
|
||||
prop.damageType = scope['$lastDamageType'];
|
||||
if (prop.damageType === 'extra' && scope['~lastDamageType']?.value) {
|
||||
prop.damageType = scope['~lastDamageType']?.value;
|
||||
}
|
||||
// Store current damage type
|
||||
if (prop.damageType !== 'healing') {
|
||||
scope['$lastDamageType'] = prop.damageType;
|
||||
scope['~lastDamageType'] = { value: prop.damageType };
|
||||
}
|
||||
|
||||
// Memoise the damage suffix for the log
|
||||
let suffix = (criticalHit ? ' critical ' : ' ') +
|
||||
prop.damageType +
|
||||
(prop.damageType !== 'healing' ? ' damage ': '');
|
||||
(prop.damageType !== 'healing' ? ' damage ' : '');
|
||||
|
||||
if (damageTargets && damageTargets.length) {
|
||||
// Iterate through all the targets
|
||||
@@ -94,15 +98,17 @@ export default function applyDamage(node, {
|
||||
logValue
|
||||
});
|
||||
|
||||
actionContext.target = [target];
|
||||
// Deal the damage to the target
|
||||
let damageDealt = dealDamageWork({
|
||||
creature: target,
|
||||
let damageDealt = dealDamage({
|
||||
target,
|
||||
damageType: prop.damageType,
|
||||
amount: damage,
|
||||
actionContext
|
||||
});
|
||||
|
||||
// Log the damage done
|
||||
if (target._id === creature._id){
|
||||
if (target._id === actionContext.creature._id) {
|
||||
// Target is same as self, log damage as such
|
||||
logValue.push(`**${damageDealt}** ${suffix} to self`);
|
||||
} else {
|
||||
@@ -123,7 +129,7 @@ export default function applyDamage(node, {
|
||||
// There are no targets, just log the result
|
||||
logValue.push(`**${damage}** ${suffix}`);
|
||||
}
|
||||
log.content.push({
|
||||
if (!prop.silent) actionContext.addLog({
|
||||
name: logName,
|
||||
value: logValue.join('\n'),
|
||||
inline: true,
|
||||
@@ -131,33 +137,33 @@ export default function applyDamage(node, {
|
||||
return applyChildren();
|
||||
}
|
||||
|
||||
function applyDamageMultipliers({target, damage, damageProp, logValue}){
|
||||
function applyDamageMultipliers({ target, damage, damageProp, logValue }) {
|
||||
const damageType = damageProp?.damageType;
|
||||
if (!damageType) return damage;
|
||||
|
||||
const multiplier = target?.variables?.[damageType];
|
||||
if (!multiplier) return damage;
|
||||
|
||||
const damageTypeText = damageType == 'healing' ? 'healing': `${damageType} damage`;
|
||||
const damageTypeText = damageType == 'healing' ? 'healing' : `${damageType} damage`;
|
||||
|
||||
if (
|
||||
multiplier.immunity &&
|
||||
some(multiplier.immunities, multiplierAppliesTo(damageProp))
|
||||
){
|
||||
some(multiplier.immunities, multiplierAppliesTo(damageProp, 'immunity'))
|
||||
) {
|
||||
logValue.push(`Immune to ${damageTypeText}`);
|
||||
return 0;
|
||||
} else {
|
||||
if (
|
||||
multiplier.resistance &&
|
||||
some(multiplier.resistances, multiplierAppliesTo(damageProp))
|
||||
){
|
||||
some(multiplier.resistances, multiplierAppliesTo(damageProp, 'resistance'))
|
||||
) {
|
||||
logValue.push(`Resistant to ${damageTypeText}`);
|
||||
damage = Math.floor(damage / 2);
|
||||
}
|
||||
if (
|
||||
multiplier.vulnerability &&
|
||||
some(multiplier.vulnerabilities, multiplierAppliesTo(damageProp))
|
||||
){
|
||||
some(multiplier.vulnerabilities, multiplierAppliesTo(damageProp, 'vulnerability'))
|
||||
) {
|
||||
logValue.push(`Vulnerable to ${damageTypeText}`);
|
||||
damage = Math.floor(damage * 2);
|
||||
}
|
||||
@@ -165,16 +171,88 @@ function applyDamageMultipliers({target, damage, damageProp, logValue}){
|
||||
return damage;
|
||||
}
|
||||
|
||||
function multiplierAppliesTo(damageProp){
|
||||
function multiplierAppliesTo(damageProp, multiplierType) {
|
||||
return multiplier => {
|
||||
// Apply the default 'ignore x' tags
|
||||
const effectiveTags = getEffectivePropTags(damageProp);
|
||||
if (includes(effectiveTags, `ignore ${multiplierType}`)) return false;
|
||||
|
||||
const hasRequiredTags = difference(
|
||||
multiplier.includeTags, damageProp.tags
|
||||
multiplier.includeTags, effectiveTags
|
||||
).length === 0;
|
||||
|
||||
const hasNoExcludedTags = intersection(
|
||||
multiplier.excludeTags, damageProp.tags
|
||||
multiplier.excludeTags, effectiveTags
|
||||
).length === 0;
|
||||
|
||||
return hasRequiredTags && hasNoExcludedTags;
|
||||
}
|
||||
}
|
||||
|
||||
function dealDamage({ target, damageType, amount, actionContext }) {
|
||||
// Get all the health bars and do damage to them
|
||||
let healthBars = getPropertiesOfType(target._id, 'attribute');
|
||||
|
||||
// Keep only the healthbars that can take damage/healing
|
||||
healthBars = healthBars.filter((bar) => {
|
||||
if (bar.attributeType !== 'healthBar' || bar.inactive || bar.removed || bar.overridden) {
|
||||
return false;
|
||||
}
|
||||
if (damageType === 'healing' && bar.healthBarNoHealing) {
|
||||
return false;
|
||||
}
|
||||
if (damageType !== 'healing' && amount >= 0 && bar.healthBarNoDamage) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
// Sort healthbars by damage/healing order or tree order as a fallback
|
||||
healthBars.sort((a, b) => {
|
||||
let diff;
|
||||
if (amount >= 0) {
|
||||
diff = a.healthBarDamageOrder - b.healthBarDamageOrder;
|
||||
} else {
|
||||
diff = a.healthBarHealingOrder - b.healthBarHealingOrder;
|
||||
}
|
||||
if (Number.isFinite(diff)) {
|
||||
return diff;
|
||||
} else {
|
||||
return a.order - b.order;
|
||||
}
|
||||
});
|
||||
|
||||
// Deal the damage to each healthbar in order until all damage is done
|
||||
const totalDamage = amount;
|
||||
let damageLeft = totalDamage;
|
||||
if (damageType === 'healing') damageLeft = -totalDamage;
|
||||
healthBars.forEach(healthBar => {
|
||||
if (damageLeft === 0) return;
|
||||
// Replace the healthbar by the one in the action context if we can
|
||||
// The damagePropertyWork function bashes the prop with the damage
|
||||
// So we can use the new value in later action properties
|
||||
if (healthBar.variableName) {
|
||||
const targetHealthBar = target.variables[healthBar.variableName];
|
||||
if (targetHealthBar?._id === healthBar._id) {
|
||||
healthBar = targetHealthBar;
|
||||
}
|
||||
}
|
||||
// Do the damage
|
||||
let damageAdded = damagePropertyWork({
|
||||
prop: healthBar,
|
||||
operation: 'increment',
|
||||
value: damageLeft,
|
||||
actionContext
|
||||
});
|
||||
damageLeft -= damageAdded;
|
||||
// Prevent overflow
|
||||
if (
|
||||
damageType === 'healing' ?
|
||||
healthBar.healthBarNoHealingOverflow :
|
||||
healthBar.healthBarNoDamageOverflow
|
||||
) {
|
||||
damageLeft = 0;
|
||||
}
|
||||
});
|
||||
return totalDamage;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import recalculateInlineCalculations from './shared/recalculateInlineCalculations.js';
|
||||
import applyProperty from '../applyProperty.js';
|
||||
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
|
||||
|
||||
export default function applyFolder(node, actionContext) {
|
||||
// Apply triggers
|
||||
applyNodeTriggers(node, 'before', actionContext);
|
||||
applyNodeTriggers(node, 'after', actionContext);
|
||||
// Apply children
|
||||
node.children.forEach(child => applyProperty(child, actionContext));
|
||||
}
|
||||
@@ -1,25 +1,27 @@
|
||||
import recalculateInlineCalculations from './shared/recalculateInlineCalculations.js';
|
||||
import applyProperty from '../applyProperty.js';
|
||||
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
|
||||
|
||||
export default function applyNote(node, {creature, targets, scope, log}){
|
||||
export default function applyNote(node, actionContext){
|
||||
applyNodeTriggers(node, 'before', actionContext);
|
||||
const prop = node.node;
|
||||
|
||||
// Log Name, summary
|
||||
let content = { name: prop.name };
|
||||
if (prop.summary?.text){
|
||||
recalculateInlineCalculations(prop.summary, scope, log);
|
||||
recalculateInlineCalculations(prop.summary, actionContext);
|
||||
content.value = prop.summary.value;
|
||||
}
|
||||
if (content.name || content.value){
|
||||
log.content.push(content);
|
||||
actionContext.addLog(content);
|
||||
}
|
||||
// Log description
|
||||
if (prop.description?.text){
|
||||
recalculateInlineCalculations(prop.description, scope, log);
|
||||
log.content.push({value: prop.description.value});
|
||||
recalculateInlineCalculations(prop.description, actionContext);
|
||||
actionContext.addLog({value: prop.description.value});
|
||||
}
|
||||
// Apply triggers
|
||||
applyNodeTriggers(node, 'after', actionContext);
|
||||
// Apply children
|
||||
node.children.forEach(child => applyProperty(child, {
|
||||
creature, targets, scope, log
|
||||
}));
|
||||
node.children.forEach(child => applyProperty(child, actionContext));
|
||||
}
|
||||
|
||||
@@ -2,54 +2,55 @@ import applyProperty from '../applyProperty.js';
|
||||
import logErrors from './shared/logErrors.js';
|
||||
import applyEffectsToCalculationParseNode from '/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js';
|
||||
import resolve, { toString } from '/imports/parser/resolve.js';
|
||||
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
|
||||
|
||||
export default function applyRoll(node, {creature, targets, scope, log}){
|
||||
export default function applyRoll(node, actionContext) {
|
||||
applyNodeTriggers(node, 'before', actionContext);
|
||||
const prop = node.node;
|
||||
|
||||
const applyChildren = function(){
|
||||
node.children.forEach(child => applyProperty(child, {
|
||||
creature, targets, scope, log
|
||||
}));
|
||||
const applyChildren = function () {
|
||||
applyNodeTriggers(node, 'after', actionContext);
|
||||
node.children.forEach(child => applyProperty(child, actionContext));
|
||||
};
|
||||
|
||||
if (prop.roll?.calculation){
|
||||
if (prop.roll?.calculation) {
|
||||
const logValue = [];
|
||||
|
||||
// roll the dice only and store that string
|
||||
applyEffectsToCalculationParseNode(prop.roll, log);
|
||||
const {result: rolled, context} = resolve('roll', prop.roll.parseNode, scope);
|
||||
if (rolled.parseType !== 'constant'){
|
||||
applyEffectsToCalculationParseNode(prop.roll, actionContext);
|
||||
const { result: rolled, context } = resolve('roll', prop.roll.parseNode, actionContext.scope);
|
||||
if (rolled.parseType !== 'constant') {
|
||||
logValue.push(toString(rolled));
|
||||
}
|
||||
logErrors(context.errors, log);
|
||||
logErrors(context.errors, actionContext);
|
||||
|
||||
// Reset the errors so we don't log the same errors twice
|
||||
context.errors = [];
|
||||
|
||||
// Resolve the roll to a final value
|
||||
const {result: reduced} = resolve('reduce', rolled, scope, context);
|
||||
logErrors(context.errors, log);
|
||||
const { result: reduced } = resolve('reduce', rolled, actionContext.scope, context);
|
||||
logErrors(context.errors, actionContext);
|
||||
|
||||
// Store the result
|
||||
if (reduced.parseType === 'constant'){
|
||||
if (reduced.parseType === 'constant') {
|
||||
prop.roll.value = reduced.value;
|
||||
} else if (reduced.parseType === 'error'){
|
||||
} else if (reduced.parseType === 'error') {
|
||||
prop.roll.value = null;
|
||||
} else {
|
||||
prop.roll.value = toString(reduced);
|
||||
}
|
||||
|
||||
// If we didn't end up with a constant of finite amount, give up
|
||||
if (reduced?.parseType !== 'constant' || !isFinite(reduced.value)){
|
||||
// If we didn't end up with a constant or a number of finite value, give up
|
||||
if (reduced?.parseType !== 'constant' || (reduced.valueType === 'number' && !isFinite(reduced.value))) {
|
||||
return applyChildren();
|
||||
}
|
||||
const value = reduced.value;
|
||||
|
||||
scope[prop.variableName] = value;
|
||||
actionContext.scope[prop.variableName] = { value };
|
||||
logValue.push(`**${value}**`);
|
||||
|
||||
if (!prop.silent){
|
||||
log.content.push({
|
||||
if (!prop.silent) {
|
||||
actionContext.addLog({
|
||||
name: prop.name,
|
||||
value: logValue.join('\n'),
|
||||
inline: true,
|
||||
|
||||
@@ -1,68 +1,74 @@
|
||||
import rollDice from '/imports/parser/rollDice.js';
|
||||
import recalculateCalculation from './shared/recalculateCalculation.js';
|
||||
import applyProperty from '../applyProperty.js';
|
||||
import numberToSignedString from '/imports/ui/utility/numberToSignedString.js';
|
||||
import numberToSignedString from '/imports/api/utility/numberToSignedString.js';
|
||||
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
|
||||
import { applyUnresolvedEffects } from '/imports/api/engine/actions/doCheck.js';
|
||||
|
||||
export default function applySavingThrow(node, {creature, targets, scope, log}){
|
||||
export default function applySavingThrow(node, actionContext) {
|
||||
applyNodeTriggers(node, 'before', actionContext);
|
||||
const prop = node.node;
|
||||
const originalTargets = actionContext.targets;
|
||||
|
||||
let saveTargets = prop.target === 'self' ? [creature] : targets;
|
||||
let saveTargets = prop.target === 'self' ? [actionContext.creature] : actionContext.targets;
|
||||
|
||||
recalculateCalculation(prop.dc, scope, log);
|
||||
recalculateCalculation(prop.dc, actionContext);
|
||||
|
||||
const dc = (prop.dc?.value);
|
||||
if (!isFinite(dc)){
|
||||
log.content.push({
|
||||
if (!isFinite(dc)) {
|
||||
actionContext.addLog({
|
||||
name: 'Error',
|
||||
value: 'Saving throw requires a DC',
|
||||
});
|
||||
return node.children.forEach(child => applyProperty(child, {
|
||||
creature, targets, scope, log
|
||||
}));
|
||||
return node.children.forEach(child => applyProperty(child, actionContext));
|
||||
}
|
||||
log.content.push({
|
||||
if (!prop.silent) actionContext.addLog({
|
||||
name: prop.name,
|
||||
value: `DC **${dc}**`,
|
||||
inline: true,
|
||||
});
|
||||
const scope = actionContext.scope;
|
||||
|
||||
// If there are no save targets, apply all children as if the save both
|
||||
// succeeeded and failed
|
||||
if (!saveTargets?.length){
|
||||
scope['$saveFailed'] = {value: true};
|
||||
scope['$saveSucceeded'] = {value: true};
|
||||
return node.children.forEach(child => applyProperty(child, {
|
||||
creature, targets, scope, log
|
||||
}));
|
||||
if (!saveTargets?.length) {
|
||||
scope['~saveFailed'] = { value: true };
|
||||
scope['~saveSucceeded'] = { value: true };
|
||||
applyNodeTriggers(node, 'after', actionContext);
|
||||
return node.children.forEach(child => applyProperty(child, actionContext));
|
||||
}
|
||||
|
||||
// Each target makes the saving throw
|
||||
saveTargets.forEach(target => {
|
||||
delete scope['$saveFailed'];
|
||||
delete scope['$saveSucceeded'];
|
||||
delete scope['$saveDiceRoll'];
|
||||
delete scope['$saveRoll'];
|
||||
delete scope['~saveFailed'];
|
||||
delete scope['~saveSucceeded'];
|
||||
delete scope['~saveDiceRoll'];
|
||||
delete scope['~saveRoll'];
|
||||
|
||||
const applyChildren = function(){
|
||||
node.children.forEach(child => applyProperty(child, {
|
||||
creature, targets: [target], scope, log
|
||||
}));
|
||||
const applyChildren = function () {
|
||||
actionContext.targets = [target]
|
||||
applyNodeTriggers(node, 'after', actionContext);
|
||||
node.children.forEach(child => applyProperty(child, actionContext));
|
||||
};
|
||||
|
||||
const save = target.variables[prop.stat];
|
||||
|
||||
if (!save){
|
||||
log.content.push({
|
||||
if (!save) {
|
||||
actionContext.addLog({
|
||||
name: 'Saving throw error',
|
||||
value: 'No saving throw found: ' + prop.stat,
|
||||
});
|
||||
return applyChildren();
|
||||
}
|
||||
|
||||
const rollModifierText = numberToSignedString(save.value, true);
|
||||
let rollModifierText = numberToSignedString(save.value, true);
|
||||
let rollModifier = save.value
|
||||
const { effectBonus, effectString } = applyUnresolvedEffects(save, scope)
|
||||
rollModifierText += effectString;
|
||||
rollModifier += effectBonus;
|
||||
|
||||
let value, values, resultPrefix;
|
||||
if (save.advantage === 1){
|
||||
if (save.advantage === 1) {
|
||||
const [a, b] = rollDice(2, 20);
|
||||
if (a >= b) {
|
||||
value = a;
|
||||
@@ -71,7 +77,7 @@ export default function applySavingThrow(node, {creature, targets, scope, log}){
|
||||
value = b;
|
||||
resultPrefix = `Advantage\n1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`;
|
||||
}
|
||||
} else if (save.advantage === -1){
|
||||
} else if (save.advantage === -1) {
|
||||
const [a, b] = rollDice(2, 20);
|
||||
if (a <= b) {
|
||||
value = a;
|
||||
@@ -85,20 +91,22 @@ export default function applySavingThrow(node, {creature, targets, scope, log}){
|
||||
value = values[0];
|
||||
resultPrefix = `1d20 [ ${value} ] ${rollModifierText}`
|
||||
}
|
||||
scope['$saveDiceRoll'] = {value};
|
||||
const result = value + save.value || 0;
|
||||
scope['$saveRoll'] = {value: result};
|
||||
scope['~saveDiceRoll'] = { value };
|
||||
const result = value + rollModifier || 0;
|
||||
scope['~saveRoll'] = { value: result };
|
||||
const saveSuccess = result >= dc;
|
||||
if (saveSuccess){
|
||||
scope['$saveSucceeded'] = {value: true};
|
||||
if (saveSuccess) {
|
||||
scope['~saveSucceeded'] = { value: true };
|
||||
} else {
|
||||
scope['$saveFailed'] = {value: true};
|
||||
scope['~saveFailed'] = { value: true };
|
||||
}
|
||||
log.content.push({
|
||||
if (!prop.silent) actionContext.addLog({
|
||||
name: saveSuccess ? 'Successful save' : 'Failed save',
|
||||
value: resultPrefix + '\n**' + result + '**',
|
||||
inline: true,
|
||||
});
|
||||
return applyChildren();
|
||||
});
|
||||
// reset the targets after the save to each child
|
||||
actionContext.targets = originalTargets;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import applyProperty from '../applyProperty.js';
|
||||
import recalculateCalculation from './shared/recalculateCalculation.js';
|
||||
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
|
||||
|
||||
export default function applyToggle(node, {
|
||||
creature, targets, scope, log
|
||||
}){
|
||||
export default function applyToggle(node, actionContext){
|
||||
applyNodeTriggers(node, 'before', actionContext);
|
||||
const prop = node.node;
|
||||
recalculateCalculation(prop.condition, scope, log);
|
||||
recalculateCalculation(prop.condition, actionContext);
|
||||
if (prop.condition?.value) {
|
||||
return node.children.forEach(child => applyProperty(child, {
|
||||
creature, targets, scope, log
|
||||
}));
|
||||
applyNodeTriggers(node, 'after', actionContext);
|
||||
return node.children.forEach(child => applyProperty(child, actionContext));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import operator from '/imports/parser/parseTree/operator.js';
|
||||
import { parse } from '/imports/parser/parser.js';
|
||||
import logErrors from './logErrors.js';
|
||||
|
||||
export default function applyEffectsToCalculationParseNode(calcObj, log){
|
||||
export default function applyEffectsToCalculationParseNode(calcObj, actionContext){
|
||||
if (!calcObj.effects) return;
|
||||
calcObj.effects.forEach(effect => {
|
||||
if (effect.operation !== 'add') return;
|
||||
@@ -18,7 +18,7 @@ export default function applyEffectsToCalculationParseNode(calcObj, log){
|
||||
fn: 'add'
|
||||
});
|
||||
} catch (e){
|
||||
logErrors([e], log)
|
||||
logErrors([e], actionContext)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export default function logErrors(errors, log){
|
||||
export default function logErrors(errors, actionContext){
|
||||
errors?.forEach(error => {
|
||||
if (error.type !== 'info'){
|
||||
log.content.push({name: 'Error', value: error.message});
|
||||
actionContext.addLog({name: 'Error', value: error.message});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@ import evaluateCalculation from '/imports/api/engine/computation/utility/evaluat
|
||||
import applyEffectsToCalculationParseNode from '/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js';
|
||||
import logErrors from './logErrors.js';
|
||||
|
||||
export default function recalculateCalculation(calc, scope, log, context){
|
||||
export default function recalculateCalculation(calc, actionContext, context){
|
||||
if (!calc?.parseNode) return;
|
||||
calc._parseLevel = 'reduce';
|
||||
applyEffectsToCalculationParseNode(calc, log);
|
||||
evaluateCalculation(calc, scope, context);
|
||||
logErrors(calc.errors, log);
|
||||
applyEffectsToCalculationParseNode(calc, actionContext);
|
||||
evaluateCalculation(calc, actionContext.scope, context);
|
||||
logErrors(calc.errors, actionContext);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import embedInlineCalculations from '/imports/api/engine/computation/utility/embedInlineCalculations.js';
|
||||
import recalculateCalculation from './recalculateCalculation.js'
|
||||
|
||||
export default function recalculateInlineCalculations(inlineCalcObj, scope, log){
|
||||
export default function recalculateInlineCalculations(inlineCalcObj, actionContext){
|
||||
// Skip if there are no calculations
|
||||
if (!inlineCalcObj?.inlineCalculations?.length) return;
|
||||
// Recalculate each calculation with the current scope
|
||||
inlineCalcObj.inlineCalculations.forEach(calc => {
|
||||
recalculateCalculation(calc, scope, log);
|
||||
recalculateCalculation(calc, actionContext);
|
||||
});
|
||||
// Embed the new calculated values
|
||||
embedInlineCalculations(inlineCalcObj);
|
||||
|
||||
@@ -6,54 +6,39 @@ import applyProperty from '/imports/api/engine/actions/applyProperty.js';
|
||||
import { difference, intersection } from 'lodash';
|
||||
import getEffectivePropTags from '/imports/api/engine/computation/utility/getEffectivePropTags.js';
|
||||
|
||||
export default function applyTriggers(node, { creature, targets, scope, log }, timing) {
|
||||
export function applyNodeTriggers(node, timing, actionContext) {
|
||||
const prop = node.node;
|
||||
const type = prop.type;
|
||||
if (creature.triggers?.[type]?.[timing]) {
|
||||
creature.triggers[type][timing].forEach(trigger => {
|
||||
if (triggerMatchTags(trigger, prop)) {
|
||||
applyTrigger(trigger, { creature, targets, scope, log });
|
||||
}
|
||||
const triggers = actionContext.triggers?.doActionProperty?.[type]?.[timing];
|
||||
if (triggers) {
|
||||
triggers.forEach(trigger => {
|
||||
applyTrigger(trigger, prop, actionContext);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function triggerMatchTags(trigger, prop) {
|
||||
let matched = false;
|
||||
const propTags = getEffectivePropTags(prop);
|
||||
// Check the target tags
|
||||
if (
|
||||
!trigger.targetTags?.length ||
|
||||
difference(trigger.targetTags, propTags).length === 0
|
||||
) {
|
||||
matched = true;
|
||||
}
|
||||
// Check the extra tags
|
||||
trigger.extraTags?.forEach(extra => {
|
||||
if (extra.operation === 'OR') {
|
||||
if (matched) return;
|
||||
if (
|
||||
!extra.tags.length ||
|
||||
difference(extra.tags, propTags).length === 0
|
||||
) {
|
||||
matched = true;
|
||||
}
|
||||
} else if (extra.operation === 'NOT') {
|
||||
if (
|
||||
extra.tags.length &&
|
||||
intersection(extra.tags, propTags)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
export function applyTriggers(triggers = [], prop, actionContext) {
|
||||
// Apply the triggers
|
||||
triggers.forEach(trigger => {
|
||||
applyTrigger(trigger, prop, actionContext)
|
||||
});
|
||||
return matched;
|
||||
}
|
||||
|
||||
export function applyTrigger(trigger, { creature, targets, scope, log }) {
|
||||
export function applyTrigger(trigger, prop, actionContext) {
|
||||
// If there is a prop we are applying the trigger from,
|
||||
// don't fire if the tags don't match
|
||||
if (prop && !triggerMatchTags(trigger, prop)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent trigger from firing if it's inactive
|
||||
if (trigger.inactive) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent triggers from firing if their condition is false
|
||||
if (trigger.condition?.parseNode) {
|
||||
recalculateCalculation(trigger.condition, scope, log);
|
||||
recalculateCalculation(trigger.condition, actionContext);
|
||||
if (!trigger.condition.value) return;
|
||||
}
|
||||
|
||||
@@ -73,27 +58,57 @@ export function applyTrigger(trigger, { creature, targets, scope, log }) {
|
||||
// Fire the trigger
|
||||
const content = {
|
||||
name: trigger.name || 'Trigger',
|
||||
value: trigger.summary,
|
||||
value: trigger.description,
|
||||
inline: false,
|
||||
}
|
||||
if (trigger.summary?.text){
|
||||
recalculateInlineCalculations(trigger.summary, scope, log);
|
||||
content.value = trigger.summary.value;
|
||||
if (trigger.description?.text) {
|
||||
recalculateInlineCalculations(trigger.description, actionContext);
|
||||
content.value = trigger.description.value;
|
||||
}
|
||||
log.content.push(content);
|
||||
if (!trigger.silent) actionContext.addLog(content);
|
||||
|
||||
// Get all the trigger's properties and apply them
|
||||
const properties = getPropertyDecendants(creature._id, trigger._id);
|
||||
const properties = getPropertyDecendants(actionContext.creature._id, trigger._id);
|
||||
properties.sort((a, b) => a.order - b.order);
|
||||
const propertyForest = nodeArrayToTree(properties);
|
||||
propertyForest.forEach(node => {
|
||||
applyProperty(node, {
|
||||
creature,
|
||||
targets,
|
||||
scope,
|
||||
log,
|
||||
});
|
||||
applyProperty(node, actionContext);
|
||||
});
|
||||
|
||||
trigger.firing = false;
|
||||
}
|
||||
|
||||
export function triggerMatchTags(trigger, prop) {
|
||||
let matched = false;
|
||||
const propTags = getEffectivePropTags(prop);
|
||||
// Check the target tags
|
||||
if (
|
||||
!trigger.targetTags?.length ||
|
||||
difference(trigger.targetTags, propTags).length === 0
|
||||
) {
|
||||
matched = true;
|
||||
}
|
||||
// Check the extra tags
|
||||
if (trigger.extraTags) {
|
||||
for (const extra of trigger.extraTags) {
|
||||
if (extra.operation === 'OR') {
|
||||
if (matched) break;
|
||||
if (
|
||||
!extra.tags.length ||
|
||||
difference(extra.tags, propTags).length === 0
|
||||
) {
|
||||
matched = true;
|
||||
}
|
||||
} else if (extra.operation === 'NOT') {
|
||||
if (
|
||||
extra.tags.length &&
|
||||
intersection(extra.tags, propTags).length > 0
|
||||
) {
|
||||
matched = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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,15 @@
|
||||
import SimpleSchema from 'simpl-schema';
|
||||
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
||||
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
||||
import { CreatureLogSchema, insertCreatureLogWork } from '/imports/api/creature/log/CreatureLogs.js';
|
||||
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
|
||||
import { nodeArrayToTree } from '/imports/api/parenting/nodesToTree.js';
|
||||
import {
|
||||
getCreature, getVariables, getProperyAncestors, getPropertyDecendants, getPropertiesOfType
|
||||
getProperyAncestors, getPropertyDecendants
|
||||
} from '/imports/api/engine/loadCreatures.js';
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import applyProperty from './applyProperty.js';
|
||||
import { groupBy, remove } from 'lodash';
|
||||
import ActionContext from '/imports/api/engine/actions/ActionContext.js';
|
||||
|
||||
const doAction = new ValidatedMethod({
|
||||
name: 'creatureProperties.doAction',
|
||||
@@ -37,32 +36,16 @@ const doAction = new ValidatedMethod({
|
||||
numRequests: 10,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({actionId, targetIds = [], scope}) {
|
||||
run({ actionId, targetIds = [], scope }) {
|
||||
// Get action context
|
||||
let action = CreatureProperties.findOne(actionId);
|
||||
// Check permissions
|
||||
const creatureId = action.ancestors[0].id;
|
||||
let creature = getCreature(action.ancestors[0].id);
|
||||
assertEditPermission(creature, this.userId);
|
||||
const actionContext = new ActionContext(creatureId, targetIds, this);
|
||||
|
||||
// Add the variables to the creature document
|
||||
const variables = getVariables(creatureId);
|
||||
delete variables._id;
|
||||
delete variables._creatureId;
|
||||
creature.variables = variables;
|
||||
|
||||
// Get all the targets and make sure we can edit them
|
||||
let targets = [];
|
||||
targetIds.forEach(targetId => {
|
||||
let target = getCreature(targetId);
|
||||
// Check permissions
|
||||
assertEditPermission(actionContext.creature, this.userId);
|
||||
actionContext.targets.forEach(target => {
|
||||
assertEditPermission(target, this.userId);
|
||||
|
||||
// add the variables to the target documents
|
||||
const variables = getVariables(creatureId);
|
||||
delete variables._id;
|
||||
delete variables._creatureId;
|
||||
target.variables = variables;
|
||||
|
||||
targets.push(target);
|
||||
});
|
||||
|
||||
const ancestors = getProperyAncestors(creatureId, action._id);
|
||||
@@ -73,13 +56,13 @@ const doAction = new ValidatedMethod({
|
||||
properties.sort((a, b) => a.order - b.order);
|
||||
|
||||
// Do the action
|
||||
doActionWork({creature, targets, properties, ancestors, method: this, methodScope: scope});
|
||||
doActionWork({ properties, ancestors, actionContext, methodScope: scope });
|
||||
|
||||
// Recompute all involved creatures
|
||||
Creatures.update({
|
||||
_id: { $in: [creature._id, ...targetIds] }
|
||||
_id: { $in: [creatureId, ...targetIds] }
|
||||
}, {
|
||||
$set: {dirty: true},
|
||||
$set: { dirty: true },
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -87,49 +70,28 @@ const doAction = new ValidatedMethod({
|
||||
export default doAction;
|
||||
|
||||
export function doActionWork({
|
||||
creature, targets, properties, ancestors, method, methodScope = {}, log
|
||||
}){
|
||||
properties, ancestors, actionContext, methodScope = {},
|
||||
}) {
|
||||
// get the docs
|
||||
const ancestorScope = getAncestorScope(ancestors);
|
||||
const propertyForest = nodeArrayToTree(properties);
|
||||
if (propertyForest.length !== 1){
|
||||
if (propertyForest.length !== 1) {
|
||||
throw new Meteor.Error(`The action has ${propertyForest.length} top level properties, expected 1`);
|
||||
}
|
||||
|
||||
// Get the triggers
|
||||
const triggers = getPropertiesOfType(creature._id, 'trigger');
|
||||
remove(triggers, trigger => trigger.event !== 'doActionProperty');
|
||||
creature.triggers = groupBy(triggers, 'actionPropertyType');
|
||||
for (let type in creature.triggers) {
|
||||
creature.triggers[type] = groupBy(creature.triggers[type], 'timing')
|
||||
}
|
||||
|
||||
// Create the log
|
||||
if (!log) log = CreatureLogSchema.clean({
|
||||
creatureId: creature._id,
|
||||
creatureName: creature.name,
|
||||
});
|
||||
// Include the ancestry and method scope in the context scope
|
||||
Object.assign(actionContext.scope, ancestorScope, methodScope);
|
||||
|
||||
// Apply the top level property, it is responsible for applying its children
|
||||
// recursively
|
||||
const scope = {
|
||||
...creature.variables,
|
||||
...ancestorScope,
|
||||
...methodScope
|
||||
}
|
||||
applyProperty(propertyForest[0], {
|
||||
creature,
|
||||
targets,
|
||||
scope,
|
||||
log,
|
||||
});
|
||||
applyProperty(propertyForest[0], actionContext);
|
||||
|
||||
// Insert the log
|
||||
insertCreatureLogWork({log, creature, method});
|
||||
actionContext.writeLog();
|
||||
}
|
||||
|
||||
// Assumes ancestors are in tree order already
|
||||
function getAncestorScope(ancestors){
|
||||
function getAncestorScope(ancestors) {
|
||||
let scope = {};
|
||||
ancestors.forEach(prop => {
|
||||
scope[`#${prop.type}`] = prop;
|
||||
|
||||
@@ -1,11 +1,58 @@
|
||||
import '/imports/api/simpleSchemaConfig.js';
|
||||
//import testTypes from './testTypes/index.js';
|
||||
import applyTriggers from '/imports/api/engine/actions/applyTriggers.testFn.js';
|
||||
import { doActionWork } from './doAction.js';
|
||||
import createAction from './tests/createAction.testFn.js';
|
||||
import { CreatureLogSchema } from '/imports/api/creature/log/CreatureLogs.js';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||
|
||||
describe('Do Action', function(){
|
||||
it('Does an empty action', function(){
|
||||
doActionWork(createAction({properties: [{type: 'action'}]}));
|
||||
function cleanProp(prop) {
|
||||
let schema = CreatureProperties.simpleSchema(prop);
|
||||
return schema.clean(prop);
|
||||
}
|
||||
|
||||
function cleanCreature(creature) {
|
||||
let schema = Creatures.simpleSchema(creature);
|
||||
return schema.clean(creature);
|
||||
}
|
||||
|
||||
// Fake ActionContext to test actions with
|
||||
const creatureId = 'actionTestCreatureId';
|
||||
const creatureName = 'Action Test Creature';
|
||||
const testActionContext = {
|
||||
creature: cleanCreature({
|
||||
_id: creatureId,
|
||||
}),
|
||||
log: CreatureLogSchema.clean({
|
||||
creatureId: creatureId,
|
||||
creatureName: creatureName,
|
||||
}),
|
||||
scope: {},
|
||||
addLog(content) {
|
||||
if (content.name || content.value) {
|
||||
this.log.content.push(content);
|
||||
}
|
||||
},
|
||||
writeLog: () => { },
|
||||
}
|
||||
|
||||
const action = cleanProp({
|
||||
type: 'action',
|
||||
});
|
||||
const actionAncestors = [];
|
||||
|
||||
describe('Do Action', function () {
|
||||
it('Does an empty action', function () {
|
||||
doActionWork({
|
||||
properties: [action],
|
||||
ancestors: actionAncestors,
|
||||
actionContext: testActionContext,
|
||||
methodScope: {},
|
||||
});
|
||||
});
|
||||
//testTypes.forEach(test => it(test.text, test.fn));
|
||||
});
|
||||
|
||||
describe('Action utility functions', function () {
|
||||
it('Triggers match tags', applyTriggers);
|
||||
})
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import SimpleSchema from 'simpl-schema';
|
||||
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
||||
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
||||
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||
import {
|
||||
getProperyAncestors, getPropertyDecendants
|
||||
} from '/imports/api/engine/loadCreatures.js';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
|
||||
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
|
||||
import { doActionWork } from '/imports/api/engine/actions/doAction.js';
|
||||
import { CreatureLogSchema } from '/imports/api/creature/log/CreatureLogs.js';
|
||||
import ActionContext from '/imports/api/engine/actions/ActionContext.js';
|
||||
|
||||
const doAction = new ValidatedMethod({
|
||||
name: 'creatureProperties.doCastSpell',
|
||||
@@ -18,6 +20,10 @@ const doAction = new ValidatedMethod({
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
optional: true,
|
||||
},
|
||||
ritual: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
targetIds: {
|
||||
type: Array,
|
||||
defaultValue: [],
|
||||
@@ -39,99 +45,89 @@ const doAction = new ValidatedMethod({
|
||||
numRequests: 10,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({spellId, slotId, targetIds = [], scope = {}}) {
|
||||
run({ spellId, slotId, ritual, targetIds = [], scope = {} }) {
|
||||
// Get action context
|
||||
let spell = CreatureProperties.findOne(spellId);
|
||||
// Check permissions
|
||||
let creature = getRootCreatureAncestor(spell);
|
||||
const creatureId = spell.ancestors[0].id;
|
||||
const actionContext = new ActionContext(creatureId, targetIds, this);
|
||||
|
||||
assertEditPermission(creature, this.userId);
|
||||
|
||||
// Get all the targets and make sure we can edit them
|
||||
let targets = [];
|
||||
targetIds.forEach(targetId => {
|
||||
let target = Creatures.findOne(targetId);
|
||||
// Check permissions
|
||||
assertEditPermission(actionContext.creature, this.userId);
|
||||
actionContext.targets.forEach(target => {
|
||||
assertEditPermission(target, this.userId);
|
||||
targets.push(target);
|
||||
});
|
||||
|
||||
// Fetch all the action's ancestor creatureProperties
|
||||
const ancestorIds = [];
|
||||
spell.ancestors.forEach(ref => {
|
||||
if (ref.collection === 'creatureProperties') {
|
||||
ancestorIds.push(ref.id);
|
||||
}
|
||||
});
|
||||
const ancestors = getProperyAncestors(creatureId, spell._id);
|
||||
ancestors.sort((a, b) => a.order - b.order);
|
||||
|
||||
// Get cursor of ancestors
|
||||
const ancestors = CreatureProperties.find({
|
||||
_id: {$in: ancestorIds},
|
||||
}, {
|
||||
sort: {order: 1},
|
||||
});
|
||||
|
||||
// Get cursor of the properties
|
||||
const properties = CreatureProperties.find({
|
||||
$or: [{_id: spell._id}, {'ancestors.id': spell._id}],
|
||||
removed: {$ne: true},
|
||||
}, {
|
||||
sort: {order: 1},
|
||||
});
|
||||
const properties = getPropertyDecendants(creatureId, spell._id);
|
||||
properties.push(spell);
|
||||
properties.sort((a, b) => a.order - b.order);
|
||||
|
||||
// Spend the appropriate slot
|
||||
let slotLevel = spell.level || 0;
|
||||
let slot;
|
||||
if (slotId && !spell.castWithoutSpellSlots){
|
||||
|
||||
// If a spell requires a slot, make sure a slot is spent
|
||||
if (spell.level && !spell.castWithoutSpellSlots && !(ritual && spell.ritual)) {
|
||||
slot = CreatureProperties.findOne(slotId);
|
||||
if (!slot){
|
||||
if (!slot) {
|
||||
throw new Meteor.Error('No slot',
|
||||
'Slot not found to cast spell');
|
||||
}
|
||||
if (!slot.value){
|
||||
if (!slot.value) {
|
||||
throw new Meteor.Error('No slot',
|
||||
'Slot depleted');
|
||||
}
|
||||
if (slot.attributeType !== 'spellSlot'){
|
||||
if (slot.attributeType !== 'spellSlot') {
|
||||
throw new Meteor.Error('Not a slot',
|
||||
'The given property is not a valid spell slot');
|
||||
}
|
||||
if (!slot.spellSlotLevel?.value){
|
||||
if (!slot.spellSlotLevel?.value) {
|
||||
throw new Meteor.Error('No slot level',
|
||||
'Slot does not have a spell slot level');
|
||||
}
|
||||
if (slot.spellSlotLevel.value < spell.level){
|
||||
if (slot.spellSlotLevel.value < spell.level) {
|
||||
throw new Meteor.Error('Slot too small',
|
||||
'Slot is not large enough to cast spell');
|
||||
}
|
||||
slotLevel = slot.spellSlotLevel.value;
|
||||
damagePropertyWork({
|
||||
property: slot,
|
||||
prop: slot,
|
||||
operation: 'increment',
|
||||
value: 1,
|
||||
actionContext,
|
||||
});
|
||||
}
|
||||
|
||||
scope['slotLevel'] = slotLevel;
|
||||
|
||||
// Post the slot level spent to the log
|
||||
const log = CreatureLogSchema.clean({
|
||||
creatureId: creature._id,
|
||||
creatureName: creature.name,
|
||||
});
|
||||
if (slot?.spellSlotLevel?.value){
|
||||
log.content.push({
|
||||
if (slot?.spellSlotLevel?.value) {
|
||||
actionContext.addLog({
|
||||
name: `Casting using a level ${slotLevel} spell slot`
|
||||
});
|
||||
} else if (slotLevel) {
|
||||
log.content.push({
|
||||
name: `Casting at level ${slotLevel}`
|
||||
});
|
||||
if (ritual) {
|
||||
actionContext.addLog({
|
||||
name: `Ritual casting at level ${slotLevel}`
|
||||
});
|
||||
} else {
|
||||
actionContext.addLog({
|
||||
name: `Casting at level ${slotLevel}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
actionContext.scope['slotLevel'] = { value: slotLevel };
|
||||
actionContext.scope['~slotLevel'] = { value: slotLevel };
|
||||
|
||||
// Do the action
|
||||
doActionWork({ creature, targets, properties, ancestors, method: this, methodScope: scope, log });
|
||||
|
||||
doActionWork({
|
||||
properties, ancestors, actionContext, methodScope: scope,
|
||||
});
|
||||
|
||||
// Force the characters involved to recalculate
|
||||
Creatures.update({
|
||||
_id: { $in: [creature._id, ...targetIds] }
|
||||
_id: { $in: [creatureId, ...targetIds] }
|
||||
}, {
|
||||
$set: { dirty: true },
|
||||
});
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import SimpleSchema from 'simpl-schema';
|
||||
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
||||
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
||||
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import { CreatureLogSchema, insertCreatureLogWork } from '/imports/api/creature/log/CreatureLogs.js';
|
||||
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
|
||||
import rollDice from '/imports/parser/rollDice.js';
|
||||
import numberToSignedString from '/imports/ui/utility/numberToSignedString.js';
|
||||
import numberToSignedString from '/imports/api/utility/numberToSignedString.js';
|
||||
import { applyTriggers } from '/imports/api/engine/actions/applyTriggers.js';
|
||||
import ActionContext from '/imports/api/engine/actions/ActionContext.js';
|
||||
import evaluateCalculation from '/imports/api/engine/computation/utility/evaluateCalculation.js';
|
||||
|
||||
const doCheck = new ValidatedMethod({
|
||||
name: 'creatureProperties.doCheck',
|
||||
@@ -22,50 +23,49 @@ const doCheck = new ValidatedMethod({
|
||||
numRequests: 10,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({propId, scope}) {
|
||||
run({ propId, scope }) {
|
||||
const prop = CreatureProperties.findOne(propId);
|
||||
const creature = getRootCreatureAncestor(prop);
|
||||
const creatureId = prop.ancestors[0].id;
|
||||
const actionContext = new ActionContext(creatureId, [creatureId], this);
|
||||
Object.assign(actionContext.scope, scope);
|
||||
actionContext.scope[`#${prop.type}`] = prop;
|
||||
|
||||
// Check permissions
|
||||
assertEditPermission(creature, this.userId);
|
||||
assertEditPermission(actionContext.creature, this.userId);
|
||||
|
||||
// Do the check
|
||||
doCheckWork({creature, prop, method: this, methodScope: scope});
|
||||
doCheckWork({ prop, actionContext });
|
||||
},
|
||||
});
|
||||
|
||||
export default doCheck;
|
||||
|
||||
export function doCheckWork({
|
||||
creature, prop, method, methodScope = {}
|
||||
}){
|
||||
// Create the log
|
||||
let log = CreatureLogSchema.clean({
|
||||
creatureId: creature._id,
|
||||
creatureName: creature.name,
|
||||
});
|
||||
export function doCheckWork({ prop, actionContext }) {
|
||||
|
||||
rollCheck({prop, log, methodScope});
|
||||
applyTriggers(actionContext.triggers.check?.before, prop, actionContext);
|
||||
rollCheck(prop, actionContext);
|
||||
applyTriggers(actionContext.triggers.check?.after, prop, actionContext);
|
||||
|
||||
// Insert the log
|
||||
insertCreatureLogWork({log, creature, method});
|
||||
actionContext.writeLog();
|
||||
}
|
||||
|
||||
function rollCheck({prop, log, methodScope}){
|
||||
function rollCheck(prop, actionContext) {
|
||||
const scope = actionContext.scope;
|
||||
// get the modifier for the roll
|
||||
let rollModifier;
|
||||
let logName = `${prop.name} check`;
|
||||
if (prop.type === 'skill'){
|
||||
if (prop.type === 'skill') {
|
||||
rollModifier = prop.value;
|
||||
if (prop.skillType === 'save'){
|
||||
if (prop.name.match(/save/i)){
|
||||
if (prop.skillType === 'save') {
|
||||
if (prop.name.match(/save/i)) {
|
||||
logName = prop.name;
|
||||
} else {
|
||||
logName = prop.name ? `${prop.name} save` : 'Saving Throw';
|
||||
}
|
||||
}
|
||||
} else if (prop.type === 'attribute'){
|
||||
if (prop.attributeType === 'ability'){
|
||||
} else if (prop.type === 'attribute') {
|
||||
if (prop.attributeType === 'ability') {
|
||||
rollModifier = prop.modifier;
|
||||
} else {
|
||||
rollModifier = prop.value;
|
||||
@@ -74,10 +74,14 @@ function rollCheck({prop, log, methodScope}){
|
||||
throw (`${prop.type} not supported for checks`);
|
||||
}
|
||||
|
||||
const rollModifierText = numberToSignedString(rollModifier, true);
|
||||
let rollModifierText = numberToSignedString(rollModifier, true);
|
||||
|
||||
const { effectBonus, effectString } = applyUnresolvedEffects(prop, scope)
|
||||
rollModifierText += effectString;
|
||||
rollModifier += effectBonus;
|
||||
|
||||
let value, values, resultPrefix;
|
||||
if (methodScope['$checkAdvantage'] === 1){
|
||||
if (scope['~checkAdvantage']?.value === 1) {
|
||||
logName += ' (Advantage)';
|
||||
const [a, b] = rollDice(2, 20);
|
||||
if (a >= b) {
|
||||
@@ -87,7 +91,7 @@ function rollCheck({prop, log, methodScope}){
|
||||
value = b;
|
||||
resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText} = `;
|
||||
}
|
||||
} else if (methodScope['$checkAdvantage'] === -1){
|
||||
} else if (scope['~checkAdvantage']?.value === -1) {
|
||||
logName += ' (Disadvantage)';
|
||||
const [a, b] = rollDice(2, 20);
|
||||
if (a <= b) {
|
||||
@@ -103,8 +107,29 @@ function rollCheck({prop, log, methodScope}){
|
||||
resultPrefix = `1d20 [ ${value} ] ${rollModifierText} = `
|
||||
}
|
||||
const result = (value + rollModifier) || 0;
|
||||
log.content.push({
|
||||
scope['~checkDiceRoll'] = { value };
|
||||
scope['~checkRoll'] = { value: result };
|
||||
scope['~checkModifier'] = { value: rollModifier };
|
||||
actionContext.addLog({
|
||||
name: logName,
|
||||
value: `${resultPrefix} **${result}**`,
|
||||
});
|
||||
}
|
||||
|
||||
export function applyUnresolvedEffects(prop, scope) {
|
||||
let effectBonus = 0;
|
||||
let effectString = '';
|
||||
if (!prop.effects) {
|
||||
return { effectBonus, effectString };
|
||||
}
|
||||
prop.effects.forEach(effect => {
|
||||
if (!effect.amount?.parseNode) return;
|
||||
if (effect.operation !== 'add') return;
|
||||
effect.amount._parseLevel = 'reduce';
|
||||
evaluateCalculation(effect.amount, scope);
|
||||
if (typeof effect.amount?.value !== 'number') return;
|
||||
effectBonus += effect.amount.value;
|
||||
effectString += ` ${effect.amount.value < 0 ? '-' : '+'} [${effect.amount.calculation}] ${Math.abs(effect.amount.value)}`
|
||||
});
|
||||
return { effectBonus, effectString };
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||
|
||||
export default function createAction({
|
||||
creature = {_id: 'creatureId'},
|
||||
targets = [],
|
||||
properties = [],
|
||||
ancestors = [],
|
||||
method
|
||||
} = {}){
|
||||
properties = properties.map(cleanProp);
|
||||
ancestors = ancestors.map(cleanProp);
|
||||
creature = cleanCreature(creature);
|
||||
ancestors = ancestors.map(cleanCreature);
|
||||
return {creature, targets, properties, ancestors, method};
|
||||
}
|
||||
|
||||
function cleanProp(prop){
|
||||
let schema = CreatureProperties.simpleSchema(prop);
|
||||
return schema.clean(prop);
|
||||
}
|
||||
|
||||
function cleanCreature(creature){
|
||||
let schema = Creatures.simpleSchema(creature);
|
||||
return schema.clean(creature);
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import applyAction from './applyAction.testFn.js';
|
||||
|
||||
export default [{
|
||||
text: 'Applies actions',
|
||||
fn: applyAction,
|
||||
},];
|
||||
@@ -1,15 +1,30 @@
|
||||
import { EJSON } from 'meteor/ejson';
|
||||
import createGraph from 'ngraph.graph';
|
||||
import createGraph, { Graph } from 'ngraph.graph';
|
||||
import getEffectivePropTags from '/imports/api/engine/computation/utility/getEffectivePropTags.js';
|
||||
|
||||
interface CreatureProperty {
|
||||
_id: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export default class CreatureComputation {
|
||||
constructor(properties, creature, variables){
|
||||
originalPropsById: object;
|
||||
propsById: object;
|
||||
propsWithTag: object;
|
||||
scope: object;
|
||||
props: Array<CreatureProperty>;
|
||||
dependencyGraph: Graph;
|
||||
errors: Array<object>;
|
||||
creature: object;
|
||||
variables: object;
|
||||
|
||||
constructor(properties: Array<CreatureProperty>, creature: object, variables: object) {
|
||||
// Set up fields
|
||||
this.originalPropsById = {};
|
||||
this.propsById = {};
|
||||
this.originalPropsById = {};
|
||||
this.propsById = {};
|
||||
this.propsWithTag = {};
|
||||
this.scope = {};
|
||||
this.props = properties;
|
||||
this.props = properties;
|
||||
this.dependencyGraph = createGraph();
|
||||
this.errors = [];
|
||||
this.creature = creature;
|
||||
@@ -29,8 +29,8 @@ function childrenActive(prop){
|
||||
// Children of disabled properties are always inactive
|
||||
if (prop.disabled) return false;
|
||||
switch (prop.type){
|
||||
// Only equipped items have active children
|
||||
case 'item': return !!prop.equipped;
|
||||
// Only equipped items with non-zero quantity have active children
|
||||
case 'item': return !!prop.equipped && prop.quantity !== 0;
|
||||
// The children of actions, spells, and triggers are always inactive
|
||||
case 'action': return false;
|
||||
case 'spell': return false;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Only computes `totalFilled`, need to compute `quantityExpected.value`
|
||||
* before `spacesLeft` can be computed
|
||||
*/
|
||||
export default function computeSlotQuantityFilled(node, dependencyGraph){
|
||||
export default function computeSlotQuantityFilled(node, dependencyGraph) {
|
||||
let slot = node.node;
|
||||
if (slot.type !== 'propertySlot') return;
|
||||
slot.totalFilled = 0;
|
||||
@@ -10,9 +10,8 @@ export default function computeSlotQuantityFilled(node, dependencyGraph){
|
||||
let childProp = child.node;
|
||||
dependencyGraph.addLink(slot._id, childProp._id, 'slotFill');
|
||||
if (
|
||||
childProp.type === 'slotFiller' &&
|
||||
Number.isFinite(childProp.slotQuantityFilled)
|
||||
){
|
||||
) {
|
||||
slot.totalFilled += childProp.slotQuantityFilled;
|
||||
} else {
|
||||
slot.totalFilled++;
|
||||
|
||||
@@ -1,17 +1,32 @@
|
||||
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;
|
||||
// Only for toggles that aren't inactive and aren't set to enabled or disabled
|
||||
if (
|
||||
prop.inactive ||
|
||||
prop.type !== 'toggle' ||
|
||||
prop.disabled ||
|
||||
prop.enabled
|
||||
) return;
|
||||
// Only for toggles
|
||||
if (prop.type !== 'toggle') return;
|
||||
|
||||
if (prop.targetByTags) {
|
||||
// Find all the props targeted by tags, and disable them and their children
|
||||
getEffectTagTargets(prop, computation).forEach(targetId => {
|
||||
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 => {
|
||||
// The child nodes depend on the toggle
|
||||
child.node._computationDetails.toggleAncestors.push(prop);
|
||||
// The child nodes depend on the toggle condition compuation
|
||||
dependencyGraph.addLink(child.node._id, prop._id, 'toggle');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import findAncestorByType from '/imports/api/engine/computation/utility/findAncestorByType.js';
|
||||
import { traverse } from '/imports/parser/resolve.js';
|
||||
|
||||
export default function linkCalculationDependencies(dependencyGraph, prop, {propsById}){
|
||||
export default function linkCalculationDependencies(dependencyGraph, prop, { propsById }) {
|
||||
prop._computationDetails.calculations.forEach(calcObj => {
|
||||
// Store resolved ancestors
|
||||
const memo = {
|
||||
@@ -16,12 +16,13 @@ export default function linkCalculationDependencies(dependencyGraph, prop, {prop
|
||||
// Skip nodes that aren't symbols or accessors
|
||||
if (node.parseType !== 'symbol' && node.parseType !== 'accessor') return;
|
||||
// Link ancestor references as direct property dependencies
|
||||
if (node.name[0] === '#'){
|
||||
if (node.name[0] === '#') {
|
||||
let ancestorProp = getAncestorProp(
|
||||
node.name.slice(1), memo, prop, propsById
|
||||
);
|
||||
if (!ancestorProp) return;
|
||||
// Link the ancestor prop as a direct dependency
|
||||
// TODO: we might be referencing a calculation sub-field, depend on that instead
|
||||
dependencyGraph.addLink(
|
||||
calcNodeId, ancestorProp._id, 'ancestorReference'
|
||||
);
|
||||
@@ -34,16 +35,16 @@ export default function linkCalculationDependencies(dependencyGraph, prop, {prop
|
||||
});
|
||||
// Store the resolved ancestors in this calculation's local scope
|
||||
if (memo.ancestors) {
|
||||
calcObj._localScope = { ...calcObj._localScope, ...memo.ancestors};
|
||||
calcObj._localScope = { ...calcObj._localScope, ...memo.ancestors };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getAncestorProp(type, memo, prop, propsById){
|
||||
if (memo.ancestors && memo.ancestors['#' + type]){
|
||||
function getAncestorProp(type, memo, prop, propsById) {
|
||||
if (memo.ancestors && memo.ancestors['#' + type]) {
|
||||
return memo.ancestors['#' + type];
|
||||
} else {
|
||||
var ancestorProp = findAncestorByType( prop, type, propsById );
|
||||
var ancestorProp = findAncestorByType(prop, type, propsById);
|
||||
if (!memo.ancestors) memo.ancestors = {};
|
||||
memo.ancestors['#' + type] = ancestorProp;
|
||||
return ancestorProp;
|
||||
|
||||
@@ -14,6 +14,7 @@ const linkDependenciesByType = {
|
||||
effect: linkEffects,
|
||||
proficiency: linkProficiencies,
|
||||
roll: linkRoll,
|
||||
pointBuy: linkPointBuy,
|
||||
propertySlot: linkSlot,
|
||||
skill: linkSkill,
|
||||
spell: linkAction,
|
||||
@@ -22,23 +23,26 @@ const linkDependenciesByType = {
|
||||
toggle: linkToggle,
|
||||
}
|
||||
|
||||
export default function linkTypeDependencies(dependencyGraph, prop, computation){
|
||||
export default function linkTypeDependencies(dependencyGraph, prop, computation) {
|
||||
linkDependenciesByType[prop.type]?.(dependencyGraph, prop, computation);
|
||||
}
|
||||
|
||||
function dependOnCalc({dependencyGraph, prop, key}){
|
||||
function dependOnCalc({ dependencyGraph, prop, key }) {
|
||||
let calc = get(prop, key);
|
||||
if (!calc) return;
|
||||
if (calc.type !== '_calculation'){
|
||||
if (calc.type !== '_calculation') {
|
||||
throw `Expected calculation got ${calc.type}`
|
||||
}
|
||||
dependencyGraph.addLink(prop._id, `${prop._id}.${key}`, 'calculation');
|
||||
}
|
||||
|
||||
function linkAction(dependencyGraph, prop, {propsById}){
|
||||
function linkAction(dependencyGraph, prop, { propsById }) {
|
||||
if (prop.variableName) {
|
||||
dependencyGraph.addLink(prop.variableName, prop._id, 'eventDefinition');
|
||||
}
|
||||
// The action depends on its attack roll and uses calculations
|
||||
dependOnCalc({dependencyGraph, prop, key: 'attackRoll'});
|
||||
dependOnCalc({dependencyGraph, prop, key: 'uses'});
|
||||
dependOnCalc({ dependencyGraph, prop, key: 'attackRoll' });
|
||||
dependOnCalc({ dependencyGraph, prop, key: 'uses' });
|
||||
|
||||
// Link the resources the action uses
|
||||
if (!prop.resources) return;
|
||||
@@ -46,7 +50,7 @@ function linkAction(dependencyGraph, prop, {propsById}){
|
||||
prop.resources.itemsConsumed.forEach((itemConsumed, index) => {
|
||||
if (!itemConsumed.itemId) return;
|
||||
const item = propsById[itemConsumed.itemId];
|
||||
if (!item || item.inactive){
|
||||
if (!item || item.inactive) {
|
||||
// Unlink if the item doesn't exist or is inactive
|
||||
itemConsumed.itemId = undefined;
|
||||
return;
|
||||
@@ -78,54 +82,60 @@ function linkAction(dependencyGraph, prop, {propsById}){
|
||||
});
|
||||
}
|
||||
|
||||
function linkAdjustment(dependencyGraph, prop){
|
||||
function linkAdjustment(dependencyGraph, prop) {
|
||||
// Adjustment depends on its amount
|
||||
dependOnCalc({dependencyGraph, prop, key: 'amount'});
|
||||
dependOnCalc({ dependencyGraph, prop, key: 'amount' });
|
||||
}
|
||||
|
||||
function linkAttribute(dependencyGraph, prop){
|
||||
function linkAttribute(dependencyGraph, prop) {
|
||||
linkVariableName(dependencyGraph, prop);
|
||||
// Depends on spellSlotLevel
|
||||
dependOnCalc({dependencyGraph, prop, key: 'spellSlotLevel'});
|
||||
dependOnCalc({ dependencyGraph, prop, key: 'spellSlotLevel' });
|
||||
|
||||
// Depends on base value
|
||||
dependOnCalc({dependencyGraph, prop, key: 'baseValue'});
|
||||
dependOnCalc({ dependencyGraph, prop, key: 'baseValue' });
|
||||
|
||||
// hit dice depend on constitution
|
||||
if (prop.attributeType === 'hitDice'){
|
||||
if (prop.attributeType === 'hitDice') {
|
||||
dependencyGraph.addLink(prop._id, 'constitution', 'hitDiceConMod');
|
||||
}
|
||||
}
|
||||
|
||||
function linkBranch(dependencyGraph, prop){
|
||||
dependOnCalc({dependencyGraph, prop, key: 'condition'});
|
||||
function linkBranch(dependencyGraph, prop) {
|
||||
dependOnCalc({ dependencyGraph, prop, key: 'condition' });
|
||||
}
|
||||
|
||||
function linkBuff(dependencyGraph, prop){
|
||||
dependOnCalc({dependencyGraph, prop, key: 'duration'});
|
||||
function linkBuff(dependencyGraph, prop) {
|
||||
dependOnCalc({ dependencyGraph, prop, key: 'duration' });
|
||||
}
|
||||
|
||||
function linkClassLevel(dependencyGraph, prop){
|
||||
function linkClassLevel(dependencyGraph, prop) {
|
||||
if (prop.inactive) return;
|
||||
// The variableName of the prop depends on the prop
|
||||
if (prop.variableName && prop.level){
|
||||
if (prop.variableName && prop.level) {
|
||||
dependencyGraph.addLink(prop.variableName, prop._id, 'classLevel');
|
||||
// The level variable depends on the class variableName variable
|
||||
let existingLevelLink = dependencyGraph.getLink('level', prop.variableName);
|
||||
if (!existingLevelLink){
|
||||
if (!existingLevelLink) {
|
||||
dependencyGraph.addLink('level', prop.variableName, 'level');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function linkDamage(dependencyGraph, prop){
|
||||
dependOnCalc({dependencyGraph, prop, key: 'amount'});
|
||||
function linkDamage(dependencyGraph, prop) {
|
||||
dependOnCalc({ dependencyGraph, prop, key: 'amount' });
|
||||
}
|
||||
|
||||
function linkEffects(dependencyGraph, prop, computation){
|
||||
function linkEffects(dependencyGraph, prop, computation) {
|
||||
// The effect depends on its amount calculation
|
||||
dependOnCalc({dependencyGraph, prop, key: 'amount'});
|
||||
dependOnCalc({ dependencyGraph, prop, key: 'amount' });
|
||||
// Inactive effects aren't going to impact their targeted stats
|
||||
if (prop.inactive) return;
|
||||
// The stats depend on the effect
|
||||
if (prop.targetByTags){
|
||||
if (prop.inactive) {
|
||||
// Inactive effects apply to no stats
|
||||
return;
|
||||
} else if (prop.targetByTags) {
|
||||
getEffectTagTargets(prop, computation).forEach(targetId => {
|
||||
const targetProp = computation.propsById[targetId];
|
||||
if (
|
||||
@@ -140,8 +150,8 @@ function linkEffects(dependencyGraph, prop, computation){
|
||||
// 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 , 'effect');
|
||||
if (calcObj && calcObj.calculation) {
|
||||
dependencyGraph.addLink(`${targetProp._id}.${key}`, prop._id, 'effect');
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -154,14 +164,14 @@ function linkEffects(dependencyGraph, prop, computation){
|
||||
}
|
||||
|
||||
// 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 notIds = [];
|
||||
if (effect.extraTags){
|
||||
if (effect.extraTags) {
|
||||
effect.extraTags.forEach(ex => {
|
||||
if (ex.operation === 'OR') {
|
||||
targets = union(targets, getTargetListFromTags(ex.tags, computation));
|
||||
} else if (ex.operation === 'NOT'){
|
||||
} else if (ex.operation === 'NOT') {
|
||||
ex.tags.forEach(tag => {
|
||||
const idList = computation.propsWithTag[tag];
|
||||
if (idList) {
|
||||
@@ -174,19 +184,19 @@ function getEffectTagTargets(effect, computation){
|
||||
return difference(targets, notIds);
|
||||
}
|
||||
|
||||
function getTargetListFromTags(tags, computation){
|
||||
function getTargetListFromTags(tags, computation) {
|
||||
const targetTagIdLists = [];
|
||||
if (!tags) return [];
|
||||
tags.forEach(tag => {
|
||||
const idList = computation.propsWithTag[tag];
|
||||
if (idList) targetTagIdLists.push(idList);
|
||||
const idList = computation.propsWithTag[tag] || [];
|
||||
targetTagIdLists.push(idList);
|
||||
});
|
||||
const targets = intersection(...targetTagIdLists);
|
||||
return targets;
|
||||
}
|
||||
|
||||
function getDefaultCalculationField(prop){
|
||||
switch (prop.type){
|
||||
function getDefaultCalculationField(prop) {
|
||||
switch (prop.type) {
|
||||
case 'action': return 'attackRoll';
|
||||
case 'adjustment': return 'amount';
|
||||
case 'attribute': return 'baseValue';
|
||||
@@ -208,7 +218,6 @@ function getDefaultCalculationField(prop){
|
||||
case 'roll': return 'roll';
|
||||
case 'savingThrow': return 'dc';
|
||||
case 'skill': return 'baseValue';
|
||||
case 'slotFiller': return null;
|
||||
case 'slot': return 'quantityExpected';
|
||||
case 'spellList': return 'attackRollBonus';
|
||||
case 'spell': return null;
|
||||
@@ -216,18 +225,19 @@ function getDefaultCalculationField(prop){
|
||||
}
|
||||
}
|
||||
|
||||
function linkRoll(dependencyGraph, prop){
|
||||
dependOnCalc({dependencyGraph, prop, key: 'roll'});
|
||||
function linkRoll(dependencyGraph, prop) {
|
||||
dependOnCalc({ dependencyGraph, prop, key: 'roll' });
|
||||
}
|
||||
|
||||
function linkVariableName(dependencyGraph, prop){
|
||||
// The variableName of the prop depends on the prop
|
||||
if (prop.variableName){
|
||||
function linkVariableName(dependencyGraph, prop) {
|
||||
// The variableName of the prop depends on the prop if the prop is active
|
||||
if (prop.variableName && !prop.inactive) {
|
||||
dependencyGraph.addLink(prop.variableName, prop._id, 'definition');
|
||||
}
|
||||
}
|
||||
|
||||
function linkDamageMultiplier(dependencyGraph, prop){
|
||||
function linkDamageMultiplier(dependencyGraph, prop) {
|
||||
if (prop.inactive) return;
|
||||
prop.damageTypes.forEach(damageType => {
|
||||
// Remove all non-letter characters from the damage name
|
||||
const damageName = damageType.replace(/[^a-z]/gi, '')
|
||||
@@ -235,43 +245,106 @@ function linkDamageMultiplier(dependencyGraph, prop){
|
||||
});
|
||||
}
|
||||
|
||||
function linkProficiencies(dependencyGraph, prop){
|
||||
// The stats depend on the proficiency
|
||||
prop.stats.forEach(statName => {
|
||||
if (!statName) return;
|
||||
dependencyGraph.addLink(statName, prop._id, prop.type);
|
||||
function linkPointBuy(dependencyGraph, prop) {
|
||||
dependOnCalc({ dependencyGraph, prop, key: 'min' });
|
||||
dependOnCalc({ dependencyGraph, prop, key: 'max' });
|
||||
dependOnCalc({ dependencyGraph, prop, key: 'cost' });
|
||||
dependOnCalc({ dependencyGraph, prop, key: 'total' });
|
||||
prop.values?.forEach(row => {
|
||||
// Wrap the document in a new object so we don't bash it unintentionally
|
||||
const pointBuyRow = {
|
||||
...row,
|
||||
type: 'pointBuyRow',
|
||||
tableName: prop.name,
|
||||
tableId: prop._id,
|
||||
}
|
||||
dependencyGraph.addNode(row._id, pointBuyRow);
|
||||
linkVariableName(dependencyGraph, pointBuyRow);
|
||||
dependOnCalc({ dependencyGraph, pointBuyRow, key: 'row.min' });
|
||||
dependOnCalc({ dependencyGraph, pointBuyRow, key: 'row.max' });
|
||||
dependOnCalc({ dependencyGraph, pointBuyRow, key: 'row.cost' });
|
||||
});
|
||||
if (prop.inactive) return;
|
||||
}
|
||||
|
||||
function linkSavingThrow(dependencyGraph, prop){
|
||||
dependOnCalc({dependencyGraph, prop, key: 'dc'});
|
||||
function linkProficiencies(dependencyGraph, prop, computation) {
|
||||
// The stats depend on the proficiency
|
||||
if (prop.inactive) return;
|
||||
if (prop.targetByTags) {
|
||||
// Tag targeted proficiencies depend on the creature's proficiencyBonus,
|
||||
// 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 linkSkill(dependencyGraph, prop){
|
||||
function linkSavingThrow(dependencyGraph, prop) {
|
||||
dependOnCalc({ dependencyGraph, prop, key: 'dc' });
|
||||
}
|
||||
|
||||
function linkSkill(dependencyGraph, prop, computation) {
|
||||
// Depends on base value
|
||||
dependOnCalc({ dependencyGraph, prop, key: 'baseValue' });
|
||||
// Link dependents
|
||||
if (prop.inactive) return;
|
||||
linkVariableName(dependencyGraph, prop);
|
||||
// The prop depends on the variable references as the ability
|
||||
if (prop.ability){
|
||||
if (prop.ability) {
|
||||
dependencyGraph.addLink(prop._id, prop.ability, 'skillAbilityScore');
|
||||
}
|
||||
// Skills depend on the creature's proficiencyBonus
|
||||
dependencyGraph.addLink(prop._id, 'proficiencyBonus', 'skillProficiencyBonus');
|
||||
|
||||
// Depends on base value
|
||||
dependOnCalc({dependencyGraph, prop, key: 'baseValue'});
|
||||
// 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){
|
||||
dependOnCalc({dependencyGraph, prop, key: 'quantityExpected'});
|
||||
dependOnCalc({dependencyGraph, prop, key: 'slotCondition'});
|
||||
function linkSlot(dependencyGraph, prop) {
|
||||
dependOnCalc({ dependencyGraph, prop, key: 'quantityExpected' });
|
||||
dependOnCalc({ dependencyGraph, prop, key: 'slotCondition' });
|
||||
}
|
||||
|
||||
function linkSpellList(dependencyGraph, prop){
|
||||
dependOnCalc({dependencyGraph, prop, key: 'maxPrepared'});
|
||||
dependOnCalc({dependencyGraph, prop, key: 'attackRollBonus'});
|
||||
dependOnCalc({dependencyGraph, prop, key: 'dc'});
|
||||
function linkSpellList(dependencyGraph, prop) {
|
||||
dependOnCalc({ dependencyGraph, prop, key: 'maxPrepared' });
|
||||
dependOnCalc({ dependencyGraph, prop, key: 'attackRollBonus' });
|
||||
dependOnCalc({ dependencyGraph, prop, key: 'dc' });
|
||||
}
|
||||
|
||||
function linkToggle(dependencyGraph, prop){
|
||||
function linkToggle(dependencyGraph, prop) {
|
||||
linkVariableName(dependencyGraph, prop);
|
||||
dependOnCalc({dependencyGraph, prop, key: 'condition'});
|
||||
dependOnCalc({ dependencyGraph, prop, key: 'condition' });
|
||||
}
|
||||
|
||||
@@ -5,14 +5,14 @@ import { get, unset } from 'lodash';
|
||||
import errorNode from '/imports/parser/parseTree/error.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);
|
||||
parseAllCalculationFields(prop, schemas);
|
||||
}
|
||||
|
||||
function discoverInlineCalculationFields(prop, schemas){
|
||||
function discoverInlineCalculationFields(prop, schemas) {
|
||||
// For each key in the schema
|
||||
schemas[prop.type].inlineCalculationFields().forEach( calcKey => {
|
||||
schemas[prop.type]?.inlineCalculationFields?.()?.forEach(calcKey => {
|
||||
// That ends in .inlineCalculations
|
||||
applyFnToKey(prop, calcKey, (prop, key) => {
|
||||
const inlineCalcObj = get(prop, key);
|
||||
@@ -22,7 +22,7 @@ function discoverInlineCalculationFields(prop, schemas){
|
||||
// Extract the calculations and store them on the property
|
||||
let string = inlineCalcObj.text;
|
||||
// If there is no text, delete the whole field
|
||||
if (!string){
|
||||
if (!string) {
|
||||
unset(prop, calcKey);
|
||||
return;
|
||||
}
|
||||
@@ -32,7 +32,7 @@ function discoverInlineCalculationFields(prop, schemas){
|
||||
|
||||
// Has the text, if it matches the existing hash, stop
|
||||
const inlineCalcHash = cyrb53(inlineCalcObj.text);
|
||||
if (inlineCalcHash === inlineCalcObj.hash){
|
||||
if (inlineCalcHash === inlineCalcObj.hash) {
|
||||
return;
|
||||
}
|
||||
inlineCalcObj.hash = inlineCalcHash;
|
||||
@@ -41,7 +41,7 @@ function discoverInlineCalculationFields(prop, schemas){
|
||||
// It will be re set including the embedded calculation at the end of
|
||||
// the computation
|
||||
let matches = string.matchAll(INLINE_CALCULATION_REGEX);
|
||||
for (let match of matches){
|
||||
for (let match of matches) {
|
||||
let calculation = match[1];
|
||||
inlineCalcObj.inlineCalculations.push({
|
||||
calculation,
|
||||
@@ -51,9 +51,9 @@ function discoverInlineCalculationFields(prop, schemas){
|
||||
});
|
||||
}
|
||||
|
||||
function parseAllCalculationFields(prop, schemas){
|
||||
function parseAllCalculationFields(prop, schemas) {
|
||||
// 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
|
||||
let parseLevel = schemas[prop.type].getDefinition(calcKey).parseLevel || 'reduce';
|
||||
|
||||
@@ -66,7 +66,7 @@ function parseAllCalculationFields(prop, schemas){
|
||||
const calcObj = get(prop, key);
|
||||
if (!calcObj) return;
|
||||
// Delete the whole calculation object if the calculation string isn't set
|
||||
if (!calcObj.calculation){
|
||||
if (!calcObj.calculation) {
|
||||
unset(prop, calcKey);
|
||||
return;
|
||||
}
|
||||
@@ -84,10 +84,10 @@ function parseAllCalculationFields(prop, schemas){
|
||||
});
|
||||
}
|
||||
|
||||
function parseCalculation(calcObj){
|
||||
function parseCalculation(calcObj) {
|
||||
const calcHash = cyrb53(calcObj.calculation);
|
||||
// If the cached parse calculation is equal to the calculation, skip
|
||||
if (calcHash === calcObj.hash){
|
||||
if (calcHash === calcObj.hash) {
|
||||
return;
|
||||
}
|
||||
calcObj.hash = calcHash;
|
||||
@@ -100,6 +100,6 @@ function parseCalculation(calcObj){
|
||||
message: prettifyParseError(e),
|
||||
};
|
||||
calcObj.parseError = error;
|
||||
calcObj.parseNode = errorNode.create({error});
|
||||
calcObj.parseNode = errorNode.create({ error });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import applyFnToKey from '../utility/applyFnToKey.js';
|
||||
import { unset } from 'lodash';
|
||||
|
||||
export default function removeSchemaFields(schemas, prop){
|
||||
export default function removeSchemaFields(schemas, prop) {
|
||||
schemas.forEach(schema => {
|
||||
schema.removeBeforeComputeFields().forEach(
|
||||
schema?.removeBeforeComputeFields?.().forEach(
|
||||
key => applyFnToKey(prop, key, unset)
|
||||
);
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import { buildComputationFromProps } from '/imports/api/engine/computation/build
|
||||
import { assert } from 'chai';
|
||||
import clean from '../../utility/cleanProp.testFn.js';
|
||||
|
||||
export default function(){
|
||||
export default function () {
|
||||
const computation = buildComputationFromProps(testProperties);
|
||||
const totalFilled = computation.propsById['slotId'].totalFilled;
|
||||
assert.equal(totalFilled, 4);
|
||||
@@ -13,24 +13,24 @@ var testProperties = [
|
||||
clean({
|
||||
_id: 'slotId',
|
||||
type: 'propertySlot',
|
||||
ancestors: [{id: 'charId'}],
|
||||
ancestors: [{ id: 'charId' }],
|
||||
}),
|
||||
// Children
|
||||
clean({
|
||||
_id: 'slotFillerId',
|
||||
type: 'slotFiller',
|
||||
type: 'folder',
|
||||
slotQuantityFilled: 3,
|
||||
slotFillerType: 'item',
|
||||
ancestors: [{id: 'charId'}, {id: 'slotId'}],
|
||||
ancestors: [{ id: 'charId' }, { id: 'slotId' }],
|
||||
}),
|
||||
clean({
|
||||
_id: 'slotChildId',
|
||||
type: 'item',
|
||||
ancestors: [{id: 'charId'}, {id: 'slotId'}],
|
||||
ancestors: [{ id: 'charId' }, { id: 'slotId' }],
|
||||
}),
|
||||
clean({
|
||||
_id: 'slotGrandchildId',
|
||||
type: 'effect',
|
||||
ancestors: [{id: 'charId'}, {id: 'slotId'}, {id: 'slotChildId'}],
|
||||
ancestors: [{ id: 'charId' }, { id: 'slotId' }, { id: 'slotChildId' }],
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -12,7 +12,7 @@ import computeToggleDependencies from './buildComputation/computeToggleDependenc
|
||||
import linkCalculationDependencies from './buildComputation/linkCalculationDependencies.js';
|
||||
import linkTypeDependencies from './buildComputation/linkTypeDependencies.js';
|
||||
import computeSlotQuantityFilled from './buildComputation/computeSlotQuantityFilled.js';
|
||||
import CreatureComputation from './CreatureComputation.js';
|
||||
import CreatureComputation from './CreatureComputation.ts';
|
||||
import removeSchemaFields from './buildComputation/removeSchemaFields.js';
|
||||
|
||||
/**
|
||||
@@ -29,7 +29,7 @@ import removeSchemaFields from './buildComputation/removeSchemaFields.js';
|
||||
* computed toggles
|
||||
*/
|
||||
|
||||
export default function buildCreatureComputation(creatureId){
|
||||
export default function buildCreatureComputation(creatureId) {
|
||||
const creature = getCreature(creatureId);
|
||||
const variables = getVariables(creatureId);
|
||||
const properties = getProperties(creatureId);
|
||||
@@ -37,7 +37,7 @@ export default function buildCreatureComputation(creatureId){
|
||||
return computation;
|
||||
}
|
||||
|
||||
export function buildComputationFromProps(properties, creature, variables){
|
||||
export function buildComputationFromProps(properties, creature, variables) {
|
||||
|
||||
const computation = new CreatureComputation(properties, creature, variables);
|
||||
// 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;
|
||||
|
||||
// Link the denormalizedStats from the creature
|
||||
if (creature && creature.denormalizedStats){
|
||||
if (creature.denormalizedStats.xp){
|
||||
if (creature && creature.denormalizedStats) {
|
||||
if (creature.denormalizedStats.xp) {
|
||||
dependencyGraph.addNode('xp', {
|
||||
baseValue: creature.denormalizedStats.xp,
|
||||
type: '_variable'
|
||||
});
|
||||
}
|
||||
if (creature.denormalizedStats.milestoneLevels){
|
||||
if (creature.denormalizedStats.milestoneLevels) {
|
||||
dependencyGraph.addNode('milestoneLevels', {
|
||||
baseValue: creature.denormalizedStats.milestoneLevels,
|
||||
type: '_variable'
|
||||
@@ -89,7 +89,11 @@ export function buildComputationFromProps(properties, creature, variables){
|
||||
// Walk the property trees computing things that need to be inherited
|
||||
walkDown(forest, node => {
|
||||
computeInactiveStatus(node);
|
||||
computeToggleDependencies(node, dependencyGraph);
|
||||
});
|
||||
// Inactive status must be complete for the whole tree before toggle deps
|
||||
// are calculated
|
||||
walkDown(forest, node => {
|
||||
computeToggleDependencies(node, dependencyGraph, computation, forest);
|
||||
computeSlotQuantityFilled(node, dependencyGraph);
|
||||
});
|
||||
|
||||
|
||||
@@ -2,8 +2,11 @@ import _variable from './computeByType/computeVariable.js';
|
||||
import action from './computeByType/computeAction.js';
|
||||
import attribute from './computeByType/computeAttribute.js';
|
||||
import skill from './computeByType/computeSkill.js';
|
||||
import pointBuy from './computeByType/computePointBuy.js';
|
||||
import propertySlot from './computeByType/computeSlot.js';
|
||||
import container from './computeByType/computeContainer.js';
|
||||
import spellList from './computeByType/computeSpellList.js';
|
||||
import toggle from './computeByType/computeToggle.js';
|
||||
import _calculation from './computeByType/computeCalculation.js';
|
||||
|
||||
export default Object.freeze({
|
||||
@@ -13,6 +16,9 @@ export default Object.freeze({
|
||||
attribute,
|
||||
container,
|
||||
skill,
|
||||
pointBuy,
|
||||
propertySlot,
|
||||
spell: action,
|
||||
spellList,
|
||||
toggle,
|
||||
});
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
export default function computeAction(computation, node){
|
||||
export default function computeAction(computation, node) {
|
||||
const prop = node.data;
|
||||
if (prop.uses){
|
||||
if (prop.uses) {
|
||||
prop.usesLeft = prop.uses.value - (prop.usesUsed || 0);
|
||||
if (!prop.usesLeft){
|
||||
if (!prop.usesLeft) {
|
||||
prop.insufficientResources = true;
|
||||
}
|
||||
}
|
||||
@@ -10,19 +10,19 @@ export default function computeAction(computation, node){
|
||||
if (!prop.resources) return;
|
||||
prop.resources.itemsConsumed.forEach(itemConsumed => {
|
||||
if (!itemConsumed.itemId) return;
|
||||
if (itemConsumed.available < itemConsumed.quantity?.value){
|
||||
if (itemConsumed.available < itemConsumed.quantity?.value) {
|
||||
prop.insufficientResources = true;
|
||||
}
|
||||
});
|
||||
prop.resources.attributesConsumed.forEach(attConsumed => {
|
||||
if (!attConsumed.variableName) return;
|
||||
if (attConsumed.available < attConsumed.quantity?.value){
|
||||
if (attConsumed.available < attConsumed.quantity?.value) {
|
||||
prop.insufficientResources = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function computeResources(computation, node){
|
||||
function computeResources(computation, node) {
|
||||
const resources = node.data?.resources;
|
||||
if (!resources) return;
|
||||
resources.attributesConsumed.forEach(attConsumed => {
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
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;
|
||||
evaluateCalculation(calcObj, computation.scope);
|
||||
if (calcObj.effects || calcObj.proficiencies) {
|
||||
calcObj.baseValue = calcObj.value;
|
||||
}
|
||||
aggregateCalculationEffects(node, computation);
|
||||
aggregateCalculationProficiencies(node, computation);
|
||||
}
|
||||
|
||||
export function aggregateCalculationEffects(node, computation){
|
||||
function aggregateCalculationEffects(node, computation) {
|
||||
const calcObj = node.data;
|
||||
delete calcObj.effects;
|
||||
computation.dependencyGraph.forEachLinkedNode(
|
||||
@@ -34,15 +39,77 @@ export function aggregateCalculationEffects(node, computation){
|
||||
},
|
||||
true // enumerate only outbound links
|
||||
);
|
||||
if (calcObj.effects && typeof calcObj.value === 'number'){
|
||||
calcObj.baseValue = calcObj.value;
|
||||
if (calcObj.effects && typeof calcObj.value === 'number') {
|
||||
calcObj.effects.forEach(effect => {
|
||||
if (
|
||||
effect.operation === 'add' &&
|
||||
effect.amount && typeof effect.amount.value === 'number'
|
||||
){
|
||||
) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { has } from 'lodash';
|
||||
import evaluateCalculation from '../../utility/evaluateCalculation.js';
|
||||
|
||||
export default function computePointBuy(computation, node) {
|
||||
const prop = node.data;
|
||||
const tableMin = prop.min?.value || null;
|
||||
const tableMax = prop.max?.value || null;
|
||||
prop.spent = 0;
|
||||
prop.values?.forEach(row => {
|
||||
// Clean up added properties
|
||||
// delete row.tableId;
|
||||
// delete row.tableName;
|
||||
// delete row.type;
|
||||
|
||||
row.spent = 0;
|
||||
if (row.value === undefined) return;
|
||||
const min = has(row, 'min.value') ? row.min.value : tableMin;
|
||||
const max = has(row, 'max.value') ? row.max.value : tableMax;
|
||||
const costFunction = EJSON.clone(row.cost || prop.cost);
|
||||
if (costFunction) costFunction.parseLevel = 'reduce';
|
||||
|
||||
// Check min and max
|
||||
if (min !== null && row.value < min) {
|
||||
row.value = min;
|
||||
}
|
||||
if (max !== null && row.value > max) {
|
||||
row.value = max;
|
||||
}
|
||||
// Evaluate the cost function
|
||||
if (!costFunction) return;
|
||||
evaluateCalculation(costFunction, { ...computation.scope, value: row.value });
|
||||
// Write calculation errors
|
||||
costFunction.errors?.forEach(error => {
|
||||
if (error?.message) {
|
||||
row.errors = row.errors || [];
|
||||
error.message = 'Cost calculation error.\n' + error.message;
|
||||
row.errors.push(error);
|
||||
}
|
||||
});
|
||||
if (Number.isFinite(costFunction.value)) {
|
||||
row.spent = costFunction.value;
|
||||
prop.spent += costFunction.value;
|
||||
}
|
||||
});
|
||||
prop.pointsLeft = (prop.total?.value || 0) - (prop.spent || 0);
|
||||
if (prop.spent > prop.total?.value) {
|
||||
prop.errors = prop.errors || [];
|
||||
prop.errors.push({
|
||||
type: 'pointBuyError',
|
||||
message: 'Spent more than total points available',
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
// by computeVariableAsSkill
|
||||
export default function computeSkill(computation, node){
|
||||
const prop = node.data;
|
||||
prop.proficiency = prop.baseProficiency;
|
||||
prop.proficiency = prop.baseProficiency || 0;
|
||||
let profBonus = computation.scope['proficiencyBonus']?.value || 0;
|
||||
// Multiply the proficiency bonus by the actual proficiency
|
||||
if(prop.proficiency === 0.49){
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export default function computSlot(computation, node){
|
||||
export default function computeSlot(computation, node) {
|
||||
const prop = node.data;
|
||||
if (prop.quantityExpected && prop.quantityExpected.value){
|
||||
if (prop.quantityExpected && prop.quantityExpected.value) {
|
||||
prop.spaceLeft = prop.quantityExpected.value - prop.totalFilled;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
export default function computeSpelllist(computation, node) {
|
||||
const prop = node.data;
|
||||
|
||||
const ability = computation.scope[prop.ability];
|
||||
if (Number.isFinite(ability?.modifier)) {
|
||||
prop.abilityMod = ability.modifier;
|
||||
} else if (Number.isFinite(ability?.value)) {
|
||||
prop.abilityMod = ability.value;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import computeVariableAsToggle from './computeVariable/computeVariableAsToggle.j
|
||||
import computeImplicitVariable from './computeVariable/computeImplicitVariable.js';
|
||||
import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js';
|
||||
|
||||
export default function computeVariable(computation, node){
|
||||
export default function computeVariable(computation, node) {
|
||||
const scope = computation.scope;
|
||||
if (!node.data) node.data = {};
|
||||
aggregateLinks(computation, node);
|
||||
@@ -15,7 +15,7 @@ export default function computeVariable(computation, node){
|
||||
// Don't add to the scope if the node id is not a legitimate variable name
|
||||
// Without this `some.thing` could break the entire sheet as a database key
|
||||
if (!VARIABLE_NAME_REGEX.test(node.id)) return;
|
||||
if (node.data.definingProp){
|
||||
if (node.data.definingProp) {
|
||||
// Add the defining variable to the scope
|
||||
scope[node.id] = node.data.definingProp
|
||||
} else {
|
||||
@@ -24,7 +24,7 @@ export default function computeVariable(computation, node){
|
||||
}
|
||||
}
|
||||
|
||||
function aggregateLinks(computation, node){
|
||||
function aggregateLinks(computation, node) {
|
||||
computation.dependencyGraph.forEachLinkedNode(
|
||||
node.id,
|
||||
(linkedNode, link) => {
|
||||
@@ -32,11 +32,12 @@ function aggregateLinks(computation, node){
|
||||
// Ignore inactive props
|
||||
if (linkedNode.data.inactive) return;
|
||||
// Apply all the aggregations
|
||||
let arg = {node, linkedNode, link, computation};
|
||||
let arg = { node, linkedNode, link, computation };
|
||||
aggregate.classLevel(arg);
|
||||
aggregate.damageMultiplier(arg);
|
||||
aggregate.definition(arg);
|
||||
aggregate.effect(arg);
|
||||
aggregate.eventDefinition(arg);
|
||||
aggregate.inventory(arg);
|
||||
aggregate.proficiency(arg);
|
||||
},
|
||||
@@ -44,7 +45,7 @@ function aggregateLinks(computation, node){
|
||||
);
|
||||
}
|
||||
|
||||
function combineAggregations(computation, node){
|
||||
function combineAggregations(computation, node) {
|
||||
combineMultiplierAggregator(node);
|
||||
node.data.overridenProps?.forEach(prop => {
|
||||
computeVariableProp(computation, node, prop);
|
||||
@@ -52,51 +53,51 @@ function combineAggregations(computation, node){
|
||||
computeVariableProp(computation, node, node.data.definingProp);
|
||||
}
|
||||
|
||||
function computeVariableProp(computation, node, prop){
|
||||
function computeVariableProp(computation, node, prop) {
|
||||
if (!prop) return;
|
||||
|
||||
// Combine damage multipliers in all props so that they can't be overridden
|
||||
if (node.data.immunity){
|
||||
if (node.data.immunity) {
|
||||
prop.immunity = node.data.immunity;
|
||||
prop.immunities = node.data.immunities;
|
||||
}
|
||||
if (node.data.resistance){
|
||||
if (node.data.resistance) {
|
||||
prop.resistance = node.data.resistance;
|
||||
prop.resistances = node.data.resistances;
|
||||
}
|
||||
if (node.data.vulnerability){
|
||||
if (node.data.vulnerability) {
|
||||
prop.vulnerability = node.data.vulnerability;
|
||||
prop.vulnerabilities = node.data.vulnerabilities;
|
||||
}
|
||||
|
||||
if (prop.type === 'attribute'){
|
||||
if (prop.type === 'attribute') {
|
||||
computeVariableAsAttribute(computation, node, prop);
|
||||
} else if (prop.type === 'skill'){
|
||||
} else if (prop.type === 'skill') {
|
||||
computeVariableAsSkill(computation, node, prop);
|
||||
} else if (prop.type === 'constant'){
|
||||
} else if (prop.type === 'constant') {
|
||||
computeVariableAsConstant(computation, node, prop);
|
||||
} else if (prop.type === 'class'){
|
||||
} else if (prop.type === 'class') {
|
||||
computeVariableAsClass(computation, node, prop);
|
||||
} else if (prop.type === 'toggle'){
|
||||
} else if (prop.type === 'toggle') {
|
||||
computeVariableAsToggle(computation, node, prop);
|
||||
}
|
||||
}
|
||||
|
||||
function combineMultiplierAggregator(node){
|
||||
function combineMultiplierAggregator(node) {
|
||||
// get a reference to the aggregator
|
||||
const aggregator = node.data.multiplierAggregator;
|
||||
if (!aggregator) return;
|
||||
|
||||
// Combine
|
||||
if (aggregator.immunities?.length){
|
||||
if (aggregator.immunities?.length) {
|
||||
node.data.immunity = true;
|
||||
node.data.immunities = aggregator.immunities;
|
||||
}
|
||||
if (aggregator.resistances?.length){
|
||||
if (aggregator.resistances?.length) {
|
||||
node.data.resistance = true;
|
||||
node.data.resistances = aggregator.resistances;
|
||||
}
|
||||
if (aggregator.vulnerabilities?.length){
|
||||
if (aggregator.vulnerabilities?.length) {
|
||||
node.data.vulnerability = true;
|
||||
node.data.vulnerabilities = aggregator.vulnerabilities;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,13 @@ export default function aggregateDefinition({node, linkedNode, link}){
|
||||
// get current defining prop
|
||||
const definingProp = node.data.definingProp;
|
||||
// Find the last defining prop
|
||||
if (!definingProp || prop.order > definingProp.order){
|
||||
if (
|
||||
!definingProp ||
|
||||
prop.type !== 'pointBuyRow' && (
|
||||
definingProp.type === 'pointBuyRow' ||
|
||||
prop.order > definingProp.order
|
||||
)
|
||||
) {
|
||||
// override the current defining prop
|
||||
overrideProp(definingProp, node);
|
||||
// set this prop as the new defining prop
|
||||
@@ -18,9 +24,32 @@ export default function aggregateDefinition({node, linkedNode, link}){
|
||||
}
|
||||
|
||||
// Aggregate the base value due to the defining properties
|
||||
const propBaseValue = prop.baseValue?.value;
|
||||
let propBaseValue = prop.baseValue?.value;
|
||||
// Point buy rows use prop.value instead of prop.baseValue
|
||||
if (prop.type === 'pointBuyRow') {
|
||||
propBaseValue = prop.value;
|
||||
}
|
||||
|
||||
if (propBaseValue === undefined) return;
|
||||
// Store a summary of the definition as a base value effect
|
||||
node.data.effects = node.data.effects || [];
|
||||
if (prop.type === 'pointBuyRow') {
|
||||
node.data.effects.push({
|
||||
_id: prop.tableId,
|
||||
name: prop.tableName,
|
||||
operation: 'base',
|
||||
amount: { value: propBaseValue },
|
||||
type: 'pointBuy',
|
||||
});
|
||||
} else {
|
||||
node.data.effects.push({
|
||||
_id: prop._id,
|
||||
name: prop.name,
|
||||
operation: 'base',
|
||||
amount: { value: propBaseValue },
|
||||
type: prop.type,
|
||||
});
|
||||
}
|
||||
if (node.data.baseValue === undefined || propBaseValue > node.data.baseValue){
|
||||
node.data.baseValue = propBaseValue;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
export default function aggregateEffect({node, linkedNode, link}){
|
||||
import { pick } from 'lodash';
|
||||
|
||||
export default function aggregateEffect({ node, linkedNode, link }) {
|
||||
if (link.data !== 'effect') return;
|
||||
// store the effect aggregator, its presence indicates that the variable is
|
||||
// targeted by effects
|
||||
@@ -19,26 +21,39 @@ export default function aggregateEffect({node, linkedNode, link}){
|
||||
|
||||
// Store a summary of the effect itself
|
||||
node.data.effects = node.data.effects || [];
|
||||
// Store either just
|
||||
let effectAmount;
|
||||
if (!linkedNode.data.amount) {
|
||||
effectAmount = undefined;
|
||||
} else if (typeof linkedNode.data.amount.value === 'string') {
|
||||
effectAmount = pick(linkedNode.data.amount, [
|
||||
'calculation', 'parseNode', 'parseError', 'value'
|
||||
]);
|
||||
} else {
|
||||
effectAmount = pick(linkedNode.data.amount, ['value']);
|
||||
}
|
||||
node.data.effects.push({
|
||||
_id: linkedNode.data._id,
|
||||
name: linkedNode.data.name,
|
||||
operation: linkedNode.data.operation,
|
||||
amount: linkedNode.data.amount && {value: linkedNode.data.amount.value},
|
||||
amount: effectAmount,
|
||||
type: linkedNode.data.type,
|
||||
text: linkedNode.data.text,
|
||||
// ancestors: linkedNode.data.ancestors,
|
||||
});
|
||||
|
||||
// get a shorter reference to the aggregator document
|
||||
const aggregator = node.data.effectAggregator;
|
||||
// Get the result of the effect
|
||||
const result = linkedNode.data.amount?.value;
|
||||
// Skip aggregating if the result is not resolved completely
|
||||
if (typeof result === 'string') return;
|
||||
let result = linkedNode.data.amount?.value;
|
||||
if (typeof result !== 'number') result = undefined;
|
||||
|
||||
// Aggregate the effect based on its operation
|
||||
switch(linkedNode.data.operation){
|
||||
switch (linkedNode.data.operation) {
|
||||
case 'base':
|
||||
// Take the largest base value
|
||||
if (Number.isFinite(result)){
|
||||
if(Number.isFinite(aggregator.base)){
|
||||
if (Number.isFinite(result)) {
|
||||
if (Number.isFinite(aggregator.base)) {
|
||||
aggregator.base = Math.max(aggregator.base, result);
|
||||
} else {
|
||||
aggregator.base = result;
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
|
||||
export default function aggregateEventDefinition({ node, linkedNode, link }) {
|
||||
// Look at all event definition links
|
||||
if (link.data !== 'eventDefinition') return;
|
||||
|
||||
// Store which property is THE defining event and which are overridden
|
||||
const prop = linkedNode.data;
|
||||
// get current defining event
|
||||
const definingEvent = node.data.definingEvent;
|
||||
// Find the last defining event
|
||||
if (
|
||||
!definingEvent ||
|
||||
prop.order > definingEvent.order
|
||||
) {
|
||||
// override the current defining prop
|
||||
if (definingEvent) definingEvent.overridden = true;
|
||||
// set this prop as the new defining prop
|
||||
node.data.definingEvent = prop;
|
||||
} else {
|
||||
prop.overridden = true;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import definition from './aggregateDefinition.js';
|
||||
import damageMultiplier from './aggregateDamageMultiplier.js';
|
||||
import effect from './aggregateEffect.js';
|
||||
import eventDefinition from './aggregateEventDefinition.js';
|
||||
import proficiency from './aggregateProficiency.js';
|
||||
import classLevel from './aggregateClassLevel.js';
|
||||
import inventory from './aggregateInventory.js';
|
||||
@@ -10,6 +11,7 @@ export default Object.freeze({
|
||||
damageMultiplier,
|
||||
definition,
|
||||
effect,
|
||||
eventDefinition,
|
||||
inventory,
|
||||
proficiency,
|
||||
});
|
||||
|
||||
@@ -7,19 +7,19 @@ import getAggregatorResult from './getAggregatorResult.js';
|
||||
export default function computeImplicitVariable(node){
|
||||
const prop = {};
|
||||
|
||||
// Combine damage multipliers
|
||||
if (node.data.immunity){
|
||||
prop.immunity = node.data.immunity;
|
||||
prop.immunities = node.data.immunities;
|
||||
}
|
||||
if (node.data.resistance){
|
||||
prop.resistance = node.data.resistance;
|
||||
prop.resistances = node.data.resistances;
|
||||
}
|
||||
if (node.data.vulnerability){
|
||||
prop.vulnerability = node.data.vulnerability;
|
||||
prop.vulnerabilities = node.data.vulnerabilities;
|
||||
}
|
||||
// Combine damage multipliers
|
||||
if (node.data.immunity){
|
||||
prop.immunity = node.data.immunity;
|
||||
prop.immunities = node.data.immunities;
|
||||
}
|
||||
if (node.data.resistance){
|
||||
prop.resistance = node.data.resistance;
|
||||
prop.resistances = node.data.resistances;
|
||||
}
|
||||
if (node.data.vulnerability){
|
||||
prop.vulnerability = node.data.vulnerability;
|
||||
prop.vulnerabilities = node.data.vulnerabilities;
|
||||
}
|
||||
|
||||
const result = getAggregatorResult(node);
|
||||
if (result !== undefined){
|
||||
|
||||
@@ -1,16 +1,40 @@
|
||||
import getAggregatorResult from './getAggregatorResult.js';
|
||||
|
||||
export default function computeVariableAsAttribute(computation, node, prop){
|
||||
export default function computeVariableAsAttribute(computation, node, prop) {
|
||||
let result = getAggregatorResult(node) || 0;
|
||||
|
||||
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);
|
||||
|
||||
// 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
|
||||
if (prop.attributeType === 'ability'){
|
||||
if (prop.attributeType === 'ability') {
|
||||
prop.modifier = Math.floor((prop.value - 10) / 2);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
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
|
||||
let ability = computation.scope[prop.ability];
|
||||
prop.abilityMod = ability?.modifier || 0;
|
||||
|
||||
// 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({
|
||||
computation,
|
||||
skillNode: node,
|
||||
@@ -21,7 +21,7 @@ export default function computeVariableAsSkill(computation, node, prop){
|
||||
let profBonus = computation.scope['proficiencyBonus']?.value || 0;
|
||||
|
||||
// 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
|
||||
profBonus = Math.floor(profBonus * 0.5);
|
||||
} else {
|
||||
@@ -33,8 +33,11 @@ export default function computeVariableAsSkill(computation, node, prop){
|
||||
const aggregator = node.data.effectAggregator;
|
||||
const aggregatorBase = aggregator?.base || 0;
|
||||
|
||||
// Store effects
|
||||
prop.effects = node.data.effects;
|
||||
|
||||
// If there is no aggregator, determine if the prop can hide, then exit
|
||||
if (!aggregator){
|
||||
if (!aggregator) {
|
||||
prop.hide = statBase === undefined &&
|
||||
prop.proficiency == 0 ||
|
||||
undefined;
|
||||
@@ -49,20 +52,32 @@ export default function computeVariableAsSkill(computation, node, prop){
|
||||
if (aggregator.set !== undefined) {
|
||||
result = aggregator.set;
|
||||
}
|
||||
if (Number.isFinite(result)){
|
||||
if (Number.isFinite(result)) {
|
||||
result = Math.floor(result);
|
||||
}
|
||||
prop.value = result;
|
||||
// Advantage/disadvantage
|
||||
if (aggregator.advantage && !aggregator.disadvantage){
|
||||
if (aggregator.advantage && !aggregator.disadvantage) {
|
||||
prop.advantage = 1;
|
||||
} else if (aggregator.disadvantage && !aggregator.advantage){
|
||||
} else if (aggregator.disadvantage && !aggregator.advantage) {
|
||||
prop.advantage = -1;
|
||||
} else {
|
||||
prop.advantage = 0;
|
||||
}
|
||||
// Passive bonus
|
||||
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
|
||||
prop.conditionalBenefits = aggregator.conditional;
|
||||
// Roll bonuses
|
||||
@@ -71,11 +86,10 @@ export default function computeVariableAsSkill(computation, node, prop){
|
||||
prop.fail = aggregator.fail;
|
||||
// Rollbonus
|
||||
prop.rollBonuses = aggregator.rollBonus;
|
||||
// Store effects
|
||||
prop.effects = node.data.effects;
|
||||
}
|
||||
|
||||
function aggregateAbilityEffects({computation, skillNode, abilityNode}){
|
||||
function aggregateAbilityEffects({ computation, skillNode, abilityNode }) {
|
||||
if (!abilityNode?.id) return;
|
||||
computation.dependencyGraph.forEachLinkedNode(
|
||||
abilityNode.id,
|
||||
(linkedNode, link) => {
|
||||
@@ -84,15 +98,15 @@ function aggregateAbilityEffects({computation, skillNode, abilityNode}){
|
||||
if (linkedNode.data.inactive) return;
|
||||
// Check that the link is a valid effect/proficiency to pass on
|
||||
// to a skill from its ability
|
||||
if (link.data === 'effect'){
|
||||
if (link.data === 'effect') {
|
||||
if (![
|
||||
'advantage', 'disadvantage', 'passiveAdd', 'fail'
|
||||
].includes(linkedNode.data.operation)){
|
||||
'advantage', 'disadvantage', 'passiveAdd', 'fail', 'conditional'
|
||||
].includes(linkedNode.data.operation)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Apply the aggregations
|
||||
let arg = {node: skillNode, linkedNode, link};
|
||||
let arg = { node: skillNode, linkedNode, link };
|
||||
aggregate.effect(arg);
|
||||
aggregate.proficiency(arg);
|
||||
},
|
||||
|
||||
@@ -29,7 +29,7 @@ export default function getAggregatorResult(node){
|
||||
if (aggregator.set !== undefined) {
|
||||
result = aggregator.set;
|
||||
}
|
||||
if (!node.definingProp?.decimal && Number.isFinite(result)){
|
||||
if (!node.data.definingProp?.decimal && Number.isFinite(result)){
|
||||
result = Math.floor(result);
|
||||
} else if (Number.isFinite(result)){
|
||||
result = stripFloatingPointOddities(result);
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
export default function evaluateToggles(computation, node){
|
||||
export default function evaluateToggles(computation, node) {
|
||||
let prop = node.data;
|
||||
if (!prop) return;
|
||||
let toggles = prop._computationDetails?.toggleAncestors;
|
||||
if (!toggles) return;
|
||||
toggles.forEach(toggle => {
|
||||
if (prop.inactive || !toggle.condition) return;
|
||||
if (!toggle.condition.value){
|
||||
if (
|
||||
(!toggle.enabled && !toggle.disabled && toggle.condition && !toggle.condition.value)
|
||||
|| (toggle.disabled)
|
||||
) {
|
||||
prop.inactive = 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 computeEffects from './computeEffects.testFn.js';
|
||||
import computeSkills from './computeSkills.testFn.js';
|
||||
import computeProficiencies from './computeProficiencies.testFn.js';
|
||||
|
||||
export default [{
|
||||
text: 'Computes actions',
|
||||
fn: computeAction,
|
||||
},{
|
||||
}, {
|
||||
text: 'Computes attributes',
|
||||
fn: computeAttribute,
|
||||
},{
|
||||
}, {
|
||||
text: 'Computes classes',
|
||||
fn: computeClasses,
|
||||
},{
|
||||
}, {
|
||||
text: 'Computes constants',
|
||||
fn: computeConstants,
|
||||
},{
|
||||
}, {
|
||||
text: 'Computes inventory',
|
||||
fn: computeInventory,
|
||||
},{
|
||||
}, {
|
||||
text: 'Computes damage multipliers',
|
||||
fn: computeDamageMultipliers,
|
||||
},{
|
||||
}, {
|
||||
text: 'Computes effects',
|
||||
fn: computeEffects,
|
||||
},{
|
||||
}, {
|
||||
text: 'Computes skills',
|
||||
fn: computeSkills,
|
||||
}, {
|
||||
text: 'Computes proficiencies',
|
||||
fn: computeProficiencies,
|
||||
}];
|
||||
|
||||
@@ -51,11 +51,22 @@ function compute(computation, node){
|
||||
|
||||
function pushDependenciesToStack(nodeId, graph, stack, computation){
|
||||
graph.forEachLinkedNode(nodeId, linkedNode => {
|
||||
if (linkedNode._visitedChildren && !linkedNode._visited){
|
||||
const pather = path.nba(graph, {
|
||||
oriented: true
|
||||
});
|
||||
const loop = pather.find(nodeId, nodeId);
|
||||
if (linkedNode._visitedChildren && !linkedNode._visited) {
|
||||
// This is a dependency loop, find a path from the node to itself
|
||||
// and store that path as a dependency loop error
|
||||
const pather = path.nba(graph, { oriented: true });
|
||||
let loop = [];
|
||||
// Pather doesn't like going from a node to iteself, so find all the
|
||||
// paths going from the next node back to the original node
|
||||
// and return the shortest one
|
||||
graph.forEachLinkedNode(nodeId, nextNode => {
|
||||
const newLoop = pather.find(nextNode.id, nodeId);
|
||||
if (!newLoop.length) return;
|
||||
if (!loop.length || newLoop.length < loop.length - 1) {
|
||||
loop = [linkedNode, ...newLoop];
|
||||
}
|
||||
}, true);
|
||||
|
||||
if (loop.length) {
|
||||
computation.errors.push({
|
||||
type: 'dependencyLoop',
|
||||
|
||||
@@ -9,8 +9,10 @@ export default function getEffectivePropTags(prop) {
|
||||
}
|
||||
|
||||
// Tags for some string properties
|
||||
if (prop.variableName) tags.push(prop.variableName);
|
||||
if (prop.damageType) tags.push(prop.damageType);
|
||||
if (prop.skillType) tags.push(prop.skillType);
|
||||
if (prop.actionType) tags.push(prop.actionType);
|
||||
if (prop.attributeType) tags.push(prop.attributeType);
|
||||
if (prop.reset) tags.push(prop.reset);
|
||||
return tags;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user