Compare commits

..

153 Commits

Author SHA1 Message Date
Stefan Zermatten
b088a2d433 Bumped version 2022-12-02 09:44:44 +02:00
Stefan Zermatten
8aa5ee81d5 Merge branch 'version-2-dev' into version-2 2022-12-02 09:43:10 +02:00
Stefan Zermatten
ef26153bb2 Improved [redacted], added routes and navigation 2022-12-01 13:28:33 +02:00
Stefan Zermatten
77597e8056 Updated static pages, home, about, sign-in 2022-11-30 15:37:28 +02:00
Stefan Zermatten
ee1b876259 Bumped version 2022-11-29 14:53:23 +02:00
Stefan Zermatten
12fbca5c78 Merge branch 'version-2-dev' into version-2 2022-11-29 14:53:01 +02:00
Stefan Zermatten
da6fb55ca0 Fixed automated tab navs. going to the wrong tab 2022-11-29 14:52:22 +02:00
Stefan Zermatten
8551e318c2 Demoted features tab back in tab order 2022-11-29 14:35:27 +02:00
Stefan Zermatten
f175cffab8 Bumped version 2022-11-29 14:27:53 +02:00
Stefan Zermatten
2bca582af6 Merge branch 'version-2-dev' into version-2 2022-11-29 14:26:59 +02:00
Stefan Zermatten
5815c7ca34 Padded character list to reveal add folder button
It was hiding behind FAB
2022-11-29 14:10:28 +02:00
Stefan Zermatten
c237162475 Fixed sidebar party closing on route change 2022-11-29 14:05:24 +02:00
Stefan Zermatten
e87772c2a3 Fixed folder groupStats behaviour when !groupStats 2022-11-29 12:06:27 +02:00
Stefan Zermatten
704314a7eb Udpated npm packages 2022-11-29 11:48:28 +02:00
Stefan Zermatten
7ffd0bf61d Fixed menus in dialogs in firefox
Also improved look of scrollbars incl. dark mode
2022-11-29 11:48:20 +02:00
Stefan Zermatten
69b3ba781d Disabled tabletop routing for now 2022-11-28 23:41:10 +02:00
Stefan Zermatten
bf8eb52a96 bumped number of writers limit from 20 to 32 2022-11-28 16:54:21 +02:00
Stefan Zermatten
684d672028 Removed column layout hacks
Fixes drag fallbacks not being in front of cards
Might fix flashy shit on ios
2022-11-28 15:40:47 +02:00
Stefan Zermatten
fb98544ae1 Fixed drag and drop on Firefox 2022-11-28 15:39:47 +02:00
Stefan Zermatten
ec8b9c209c fixed rests on actions with undefined usesUsed 2022-11-28 14:50:41 +02:00
Stefan Zermatten
bee90a7a80 Fixed rests on attributes with undefined damage 2022-11-28 14:49:38 +02:00
Stefan Zermatten
5ad0de9eb7 Bumped version 2022-11-25 16:36:44 +02:00
Stefan Zermatten
0b377fcb71 Attributes show their children in stats cards 2022-11-25 16:27:18 +02:00
Stefan Zermatten
1f26fbf00e Iterated on stat grouping cards
adde slots, spell lists, children of slot fillers
hid properties in most places
spell slots in correct order
2022-11-25 13:25:38 +02:00
Stefan Zermatten
bb1e9624ad Fixed hit dice missing on stats tab 2022-11-25 12:14:47 +02:00
Stefan Zermatten
bda446858e Fixed spell tab btn not hiding correctly on mobile 2022-11-24 15:04:35 +02:00
Stefan Zermatten
e19e91f7e0 Fixed broken $attackRoll always returning 0 2022-11-24 14:51:05 +02:00
Stefan Zermatten
bac9fc98dd Fixed order of stats tab, unhid sneaky folders 2022-11-24 14:48:58 +02:00
Stefan Zermatten
420663c149 Hotfix 2 crashy boogaloo 2022-11-24 14:10:22 +02:00
Stefan Zermatten
23d44e54e3 Hotfixed S3 not loading 2022-11-24 13:39:28 +02:00
Stefan Zermatten
881496e9c1 Removed all md docs 2022-11-24 13:09:03 +02:00
Stefan Zermatten
002c767d1a Bumped version 2022-11-24 12:57:53 +02:00
Stefan Zermatten
9aaf31d5cf Merge branch 'version-2-dev' into version-2 2022-11-24 12:56:30 +02:00
Stefan Zermatten
d05cd2fa19 Iterated on docs, fixed links 2022-11-24 12:29:00 +02:00
Stefan Zermatten
f13774df11 Removed old mardown docs 2022-11-24 11:56:20 +02:00
Stefan Zermatten
cc78ba948e Merge branch 'version-2-dev' of https://github.com/ThaumRystra/DiceCloud into version-2-dev 2022-11-24 11:45:17 +02:00
Stefan Zermatten
c6bfb84bb0 Updated default docs 2022-11-24 11:45:13 +02:00
Stefan Zermatten
7e49100d14 Fixed dragging on mobile 2022-11-23 15:52:45 +02:00
Stefan Zermatten
c3ac49a8c4 Fixed default doc importing 2022-11-23 15:51:09 +02:00
Stefan Zermatten
fd9d525ba9 fixed: Show only the last event with a var name 2022-11-22 21:59:44 +02:00
Stefan Zermatten
d947b62ba4 Fixed events not being selectable for reset in libraries 2022-11-22 21:29:08 +02:00
Stefan Zermatten
046509224e Fixed library page janky loading 2022-11-22 21:21:53 +02:00
Stefan Zermatten
63bcf023ee Big UI overhaul
Moved tabs to bottom nav on mobile
Added actions tab
Conditional benefits on skills/saves show on stats tab
2022-11-22 20:51:21 +02:00
Stefan Zermatten
9741d1d56c Got in-dialog help working with new docs 2022-11-22 03:01:59 +02:00
Stefan Zermatten
0f12c98408 Merge branch 'version-2-dev' of https://github.com/ThaumRystra/DiceCloud into version-2-dev 2022-11-22 02:34:38 +02:00
Stefan Zermatten
254390e1c1 Added default doc seeding for new servers 2022-11-22 02:34:34 +02:00
Stefan Zermatten
ad2f43712d Merge pull request #300 from Jonpot/patch-1
Fix skill.md typo
2022-11-22 02:33:13 +02:00
Stefan Zermatten
55f8dac0db Merge pull request #302 from Jonpot/patch-3
Fix Action scope variables
2022-11-22 02:32:17 +02:00
Jonpot
9f8c3f0f3d Update applyAction.js
As per the docs, $attackDiceRoll should be the value of the d20 before modifiers, and $attackRoll should the the total value, after modifiers. Pre-patch, the former variable is never defined, and the latter variable has the wrong value.
2022-11-21 16:20:48 -08:00
Jonpot
56bd41f435 Fix skill.md typo 2022-11-21 15:49:01 -08:00
Stefan Zermatten
063d4288ef Point buys can now guess cost while dragging slide 2022-11-22 01:45:25 +02:00
Stefan Zermatten
a3355dd988 stat grouping is now everywhere
This lead to a complete refactor of the stats page
Some things might break
2022-11-22 00:56:10 +02:00
Stefan Zermatten
d2649fd66e Overhauled how documentation works 2022-11-21 18:22:49 +02:00
Stefan Zermatten
e619734ee1 Fixed checks not having access to #skill 2022-11-21 13:16:54 +02:00
Stefan Zermatten
5108b32624 Added line breaks in parser strings
Actual line breaks and \n both work
2022-11-20 00:19:41 +02:00
Stefan Zermatten
a9b389023e added todo 2022-11-19 23:44:02 +02:00
Stefan Zermatten
e06d2befc4 Fixed damage multipliers not using implicit tags 2022-11-19 23:24:11 +02:00
Stefan Zermatten
cc7dc257fb Fixed attributes not showing base value in lib 2022-11-19 19:35:46 +02:00
Stefan Zermatten
f3deadb3f1 Fixed buff descriptions not calced before logged 2022-11-19 19:25:51 +02:00
Stefan Zermatten
dcfb380e57 Fixed saving throw tag targeted effects
They will now roll dice effects before saving
2022-11-19 19:15:53 +02:00
Stefan Zermatten
a568cdfb1e Fixed NaNing of skills that have rolled effects 2022-11-19 18:57:26 +02:00
Stefan Zermatten
cea63e6a8e Moved dev file storage to inside meteor app folder
Prevented verbose logging of file storage
2022-11-19 18:34:52 +02:00
Stefan Zermatten
b6b0cfbb9b Fixed triggers on attribute reset on rest 2022-11-19 18:12:51 +02:00
Stefan Zermatten
428aeef635 Removed HMR test text 2022-11-19 17:56:36 +02:00
Stefan Zermatten
e3644eb9e8 Moved UI to client folder to fix HMR 2022-11-19 17:51:50 +02:00
Stefan Zermatten
060b5f93ca Reduced bundle size and updates packages 2022-11-19 17:19:07 +02:00
Stefan Zermatten
0f3a96da17 Spell list ability modifier can take non-abilities
defaults to .value if .modifier is undefined
is now undefined for no .modifier or .value
2022-11-18 14:21:22 +02:00
Stefan Zermatten
c55d572134 Bumped version 2022-11-16 23:52:08 +02:00
Stefan Zermatten
0a2b60990e Merge branch 'version-2-dev' into version-2 2022-11-16 23:51:37 +02:00
Stefan Zermatten
a437ff5aef Fixed log of recovering HD not having names 2022-11-09 15:02:41 +02:00
Stefan Zermatten
3d31d62860 Completed folder stat grouping UI 2022-11-09 14:58:52 +02:00
Stefan Zermatten
8377231254 Began work on stat grouping by folder 2022-11-09 00:00:54 +02:00
Stefan Zermatten
1ec29365cb Added custom sheet events
Made rest buttons optional
2022-11-08 23:01:09 +02:00
Stefan Zermatten
60b21c1901 Fixed bugs with effects
they were not providing advantage or conditional benefits
2022-11-08 18:29:27 +02:00
Stefan Zermatten
03f87b0afa Added spellcasting ability to spell lists 2022-11-08 18:09:00 +02:00
Stefan Zermatten
48291d2c8f Added help to property creation forms 2022-11-08 17:17:26 +02:00
Stefan Zermatten
1cedf55fbf Merge branch 'version-2-print' into version-2-dev 2022-11-08 17:00:52 +02:00
Stefan Zermatten
bed4d4b162 Fixed logo not showing 2022-11-08 16:59:52 +02:00
Stefan Zermatten
a1d992ec8d Fixed blank multipliers box 2022-11-07 16:38:54 +02:00
Stefan Zermatten
008ef62517 Printing implemented, needs print button on sheet 2022-11-07 16:18:35 +02:00
Stefan Zermatten
c436309ba8 Work on column-based print layout 2022-11-07 00:07:42 +02:00
Stefan Zermatten
0bfdb73b47 Added a quick exit for migrations if the database is new 2022-11-04 12:34:37 +02:00
Stefan Zermatten
a462cc5ca2 Updated packages 2022-11-04 12:34:22 +02:00
Stefan Zermatten
5d57a74667 Merge branch 'version-2-dev' into version-2 2022-11-03 20:52:26 +02:00
Stefan Zermatten
21b0029df7 bumped version 2022-11-03 20:51:58 +02:00
Stefan Zermatten
c0ccafa787 Added overflow stops to health bars 2022-11-03 20:50:10 +02:00
Stefan Zermatten
d63ad9ea8f Added hide when total/value zero to attributes 2022-11-03 20:39:02 +02:00
Stefan Zermatten
8f56a60fb1 Added copy-to and related sharing permissions 2022-11-03 20:18:59 +02:00
Stefan Zermatten
358ae46627 Began work on copy to for library nodes 2022-11-03 19:08:44 +02:00
Stefan Zermatten
0b1db3c40c Updated meteor 2022-10-18 15:40:41 +02:00
Stefan Zermatten
0ad7e659d2 updated docs to include create a class guide 2022-10-18 15:40:17 +02:00
Stefan Zermatten
58c3875dc7 Hotifix: Casting cantrips without a spell slot 2022-10-12 07:36:42 +02:00
Stefan Zermatten
84f506f1fe Added $checkDiceRoll $checkRoll $checkModifier variables 2022-10-12 07:32:39 +02:00
Stefan Zermatten
d0a3ccc76a bumped version 2022-10-10 16:54:57 +02:00
Stefan Zermatten
93ac9215c2 Merge branch 'version-2-dev' into version-2 2022-10-10 16:53:10 +02:00
Stefan Zermatten
a6b501a62c Fixed error on missing group prop in tree node 2022-10-10 16:51:02 +02:00
Stefan Zermatten
e956bacf07 Added actionType to effective tags 2022-10-10 16:49:10 +02:00
Stefan Zermatten
60b6b283b1 Folders now get their children applied by actions 2022-10-10 16:45:53 +02:00
Stefan Zermatten
1c9b390551 Added ritual casting 2022-10-09 23:11:06 +02:00
Stefan Zermatten
21a487635d Removed unused code from action cards 2022-10-09 21:56:42 +02:00
Stefan Zermatten
c92a26d5e6 Action cards no longer display folder or the descendants of buffs 2022-10-09 21:56:01 +02:00
Stefan Zermatten
49b514b8f3 Load common dialogs more aggressively 2022-10-09 20:55:50 +02:00
Stefan Zermatten
5cb835c536 Got basic typescript tools working 2022-10-09 17:33:43 +02:00
Stefan Zermatten
aa8f2d230d Hunted the last of the \t's to extinction 2022-10-09 16:56:28 +02:00
Stefan Zermatten
2fa913b09a Applied style rules to genocide all \t characters 2022-10-09 16:01:36 +02:00
Stefan Zermatten
de598c70a7 Fixed rolled effects not applying to checks 2022-10-09 11:10:50 +02:00
Stefan Zermatten
baecdeff24 Fixed bug where items with zero quantity have active children 2022-10-09 10:10:21 +02:00
Stefan Zermatten
d4b7d22b5f Fixed toggled off spells showing in spell list 2022-09-26 09:43:00 +02:00
Stefan Zermatten
87f79737e8 Fixed empty calculated inline fields showing calc 2022-09-25 12:39:49 +02:00
Stefan Zermatten
9f0ffe13f8 Updated meteor to fix observer bugs 2022-09-13 17:34:46 +02:00
Stefan Zermatten
adaa31d76c damage tags to ignore multipliers 2022-09-13 17:34:30 +02:00
Stefan Zermatten
b051d764f8 Slot cards have slot color as outline 2022-09-13 15:47:31 +02:00
Stefan Zermatten
ffb5b4a4f3 Libraries show name in page title 2022-09-13 15:44:37 +02:00
Stefan Zermatten
fd87b7fb75 Added advantage popup to spell cast 2022-09-09 13:20:54 +02:00
Stefan Zermatten
f035902842 Removed unused file 2022-09-08 14:47:12 +02:00
Stefan Zermatten
dbc5f7253f Finished basic docs 2022-09-05 14:36:39 +02:00
Stefan Zermatten
f0e7253374 Updated docs 2022-09-01 13:33:28 +02:00
Stefan Zermatten
ffe37bf907 Added more property help docs 2022-09-01 12:18:29 +02:00
Stefan Zermatten
a63e2099d3 Added documentation UI and began documenting props 2022-08-31 14:43:38 +02:00
Stefan Zermatten
0308e4e7a7 Merge branch 'version-2' into version-2-dev 2022-08-29 11:30:55 +02:00
Stefan Zermatten
43f8df09f0 Fixed client crash when effects target calcs 2022-08-26 09:42:34 +02:00
Stefan Zermatten
b6ed9ffb74 Merge branch 'version-2-dev' into version-2 2022-08-25 15:24:25 +02:00
Stefan Zermatten
a84da7d8a5 Buffs can skip var freezing, freeze inline calcs 2022-08-25 15:10:56 +02:00
Stefan Zermatten
249aebea0f Allowed some properties to return damaged action values
When a prop is damaged during an action, it now tries
to show its new value during the rest of that action
2022-08-25 15:10:36 +02:00
Stefan Zermatten
11a527481e Show Point buy on Build tab 2022-08-25 13:18:24 +02:00
Stefan Zermatten
8d729216b5 Properties now have their variable name as a default tag 2022-08-25 12:15:12 +02:00
Stefan Zermatten
1677e8c424 Fixed silence missing from trigger form 2022-08-25 12:14:32 +02:00
Stefan Zermatten
987aacbb67 Silence for triggers also 2022-08-25 12:12:07 +02:00
Stefan Zermatten
2714d0b9d5 Added the ability to silence most action props 2022-08-25 12:10:51 +02:00
Stefan Zermatten
1d98c41168 Fixed slotLevel not having the right value in spell scope 2022-08-25 11:40:30 +02:00
Stefan Zermatten
e42ec4b862 Continued work on point buy UI 2022-08-23 14:44:35 +02:00
Stefan Zermatten
59fc5ab851 Continued work on point buy UI 2022-08-22 15:07:40 +02:00
Stefan Zermatten
5d14c392e8 Added creature new variables to API 2022-08-22 11:58:48 +02:00
Stefan Zermatten
c6ca8c1fa4 Added point buy to computation engine 2022-08-19 14:03:12 +02:00
Stefan Zermatten
28307e26c3 Fixed some issues with skill display 2022-08-19 14:03:03 +02:00
Stefan Zermatten
6d42eb62f0 Merge branch 'version-2' into version-2-dev 2022-08-19 09:18:55 +02:00
Stefan Zermatten
877c9ca099 Fixed cache bashing in checks
Cache should only return clones of data,
not references to the cached data
2022-08-17 17:21:18 +02:00
Stefan Zermatten
9b652fc133 Added point buy form 2022-08-17 13:42:47 +02:00
Stefan Zermatten
7d66c06107 Fixed class level up w/ subscribed collections 2022-08-17 12:45:54 +02:00
Stefan Zermatten
21629138f0 Added Buff Removed action trigger 2022-08-17 12:28:00 +02:00
Stefan Zermatten
59a488256b Added buff removers 2022-08-17 11:38:30 +02:00
Stefan Zermatten
766519b4a3 Prevented inactive properties from creating deps
Engine might work differently
2022-08-17 09:40:26 +02:00
Stefan Zermatten
e7f73d0e54 Stopped crystalizing variables in nested buffs 2022-08-17 09:39:45 +02:00
Stefan Zermatten
193d5eec50 Changed slot cards to column layout 2022-08-16 13:40:58 +02:00
Stefan Zermatten
9284c9ad76 Fixed decimal stats being rounded down 2022-08-16 13:05:56 +02:00
Stefan Zermatten
f86152675f Added button to unhide hidden slots 2022-08-16 12:31:37 +02:00
Stefan Zermatten
cbac5264cd Added delete buttons to slot fill card 2022-08-16 11:44:08 +02:00
Stefan Zermatten
34e3325464 Fixed dependency loops created by inactive props
depending on their parent toggles
2022-08-16 11:19:16 +02:00
Stefan Zermatten
79c9e67ce2 Fixed icons being missing from buff-applied props 2022-08-16 10:11:13 +02:00
Stefan Zermatten
4c2aabf90d Fixed character sheet toolbar alignment on mobile 2022-08-16 10:03:07 +02:00
Stefan Zermatten
48331d3806 Fixed added properties being added based on tree
tab selection even when on other tabs
2022-08-16 09:49:34 +02:00
Stefan Zermatten
45f05d0d34 Fixed bug where actions targeting self
weren't applying props to self
2022-08-16 09:26:40 +02:00
Stefan Zermatten
58629c92f4 Added build command to package.json 2022-08-15 16:10:40 +02:00
582 changed files with 20187 additions and 11218 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
build

1
app/.gitignore vendored
View File

@@ -3,6 +3,7 @@
.demeteorized .demeteorized
.cache .cache
.vscode .vscode
fileStorage
settings.json settings.json
public/components public/components
public/_imports.html public/_imports.html

View File

@@ -4,27 +4,27 @@
# but you can also edit it by hand. # but you can also edit it by hand.
accounts-password@2.3.1 accounts-password@2.3.1
random@1.2.0 random@1.2.1
underscore@1.0.10 underscore@1.0.11
dburles:mongo-collection-instances dburles:mongo-collection-instances
accounts-google@1.4.0 accounts-google@1.4.0
email@2.2.1 email@2.2.2
meteor-base@1.5.1 meteor-base@1.5.1
mobile-experience@1.1.0 mobile-experience@1.1.0
mongo@1.15.0 mongo@1.16.1
session@1.2.0 session@1.2.1
tracker@1.2.0 tracker@1.2.1
logging@1.3.1 logging@1.3.1
reload@1.3.1 reload@1.3.1
ejson@1.1.2 ejson@1.1.3
check@1.3.1 check@1.3.2
standard-minifier-js@2.8.0 standard-minifier-js@2.8.1
shell-server@0.5.0 shell-server@0.5.0
ecmascript@0.16.2 ecmascript@0.16.3
es5-shim@4.8.0 es5-shim@4.8.0
service-configuration@1.3.0 service-configuration@1.3.1
dynamic-import@0.7.2 dynamic-import@0.7.2
ddp-rate-limiter@1.1.0 ddp-rate-limiter@1.1.1
rate-limit@1.0.9 rate-limit@1.0.9
mdg:validated-method mdg:validated-method
static-html@1.3.2 static-html@1.3.2
@@ -37,7 +37,6 @@ simple:rest
simple:rest-method-mixin simple:rest-method-mixin
mikowals:batch-insert mikowals:batch-insert
peerlibrary:subscription-data peerlibrary:subscription-data
seba:minifiers-autoprefixer
zer0th:meteor-vuetify-loader zer0th:meteor-vuetify-loader
akryum:vue-component akryum:vue-component
akryum:vue-router2 akryum:vue-router2
@@ -48,3 +47,5 @@ simple:rest-bearer-token-parser
simple:rest-json-error-handler simple:rest-json-error-handler
littledata:synced-cron littledata:synced-cron
mdg:meteor-apm-agent mdg:meteor-apm-agent
typescript@4.5.4
seba:minifiers-autoprefixer

View File

@@ -1 +1 @@
METEOR@2.7.3 METEOR@2.8.1

View File

@@ -1,4 +1,4 @@
accounts-base@2.2.3 accounts-base@2.2.5
accounts-google@1.4.0 accounts-google@1.4.0
accounts-oauth@1.4.1 accounts-oauth@1.4.1
accounts-password@2.3.1 accounts-password@2.3.1
@@ -12,7 +12,7 @@ aldeed:collection2@3.5.0
aldeed:schema-index@3.0.0 aldeed:schema-index@3.0.0
allow-deny@1.1.1 allow-deny@1.1.1
autoupdate@1.8.0 autoupdate@1.8.0
babel-compiler@7.9.0 babel-compiler@7.9.2
babel-runtime@1.5.1 babel-runtime@1.5.1
base64@1.0.12 base64@1.0.12
binary-heap@1.0.11 binary-heap@1.0.11
@@ -22,26 +22,26 @@ bozhao:link-accounts@2.6.1
caching-compiler@1.2.2 caching-compiler@1.2.2
caching-html-compiler@1.2.1 caching-html-compiler@1.2.1
callback-hook@1.4.0 callback-hook@1.4.0
check@1.3.1 check@1.3.2
coffeescript@2.4.1 coffeescript@2.4.1
coffeescript-compiler@2.4.1 coffeescript-compiler@2.4.1
dburles:mongo-collection-instances@0.3.6 dburles:mongo-collection-instances@0.3.6
ddp@1.4.0 ddp@1.4.1
ddp-client@2.5.0 ddp-client@2.6.1
ddp-common@1.4.0 ddp-common@1.4.0
ddp-rate-limiter@1.1.0 ddp-rate-limiter@1.1.1
ddp-server@2.5.0 ddp-server@2.6.0
diff-sequence@1.1.1 diff-sequence@1.1.2
dynamic-import@0.7.2 dynamic-import@0.7.2
ecmascript@0.16.2 ecmascript@0.16.3
ecmascript-runtime@0.8.0 ecmascript-runtime@0.8.0
ecmascript-runtime-client@0.12.1 ecmascript-runtime-client@0.12.1
ecmascript-runtime-server@0.11.0 ecmascript-runtime-server@0.11.0
ejson@1.1.2 ejson@1.1.3
email@2.2.1 email@2.2.2
es5-shim@4.8.0 es5-shim@4.8.0
fetch@0.1.1 fetch@0.1.2
geojson-utils@1.0.10 geojson-utils@1.0.11
google-oauth@1.4.2 google-oauth@1.4.2
hot-code-push@1.0.4 hot-code-push@1.0.4
html-tools@1.1.3 html-tools@1.1.3
@@ -55,33 +55,33 @@ littledata:synced-cron@1.5.1
livedata@1.0.18 livedata@1.0.18
localstorage@1.2.0 localstorage@1.2.0
logging@1.3.1 logging@1.3.1
mdg:meteor-apm-agent@3.5.0 mdg:meteor-apm-agent@3.5.1
mdg:validated-method@1.2.0 mdg:validated-method@1.2.0
meteor@1.10.0 meteor@1.10.2
meteor-base@1.5.1 meteor-base@1.5.1
meteortesting:browser-tests@1.3.5 meteortesting:browser-tests@1.3.5
meteortesting:mocha@2.0.3 meteortesting:mocha@2.0.3
meteortesting:mocha-core@8.1.2 meteortesting:mocha-core@8.1.2
mikowals:batch-insert@1.3.0 mikowals:batch-insert@1.3.0
minifier-css@1.6.0 minifier-css@1.6.1
minifier-js@2.7.4 minifier-js@2.7.5
minimongo@1.8.0 minimongo@1.9.0
mobile-experience@1.1.0 mobile-experience@1.1.0
mobile-status-bar@1.1.0 mobile-status-bar@1.1.0
modern-browsers@0.1.8 modern-browsers@0.1.9
modules@0.18.0 modules@0.19.0
modules-runtime@0.13.0 modules-runtime@0.13.1
mongo@1.15.0 mongo@1.16.1
mongo-decimal@0.1.3 mongo-decimal@0.1.3
mongo-dev-server@1.1.0 mongo-dev-server@1.1.0
mongo-id@1.0.8 mongo-id@1.0.8
mongo-livedata@1.0.12 mongo-livedata@1.0.12
npm-mongo@4.3.1 npm-mongo@4.11.0
oauth@2.1.2 oauth@2.1.2
oauth2@1.3.1 oauth2@1.3.1
ordered-dict@1.1.0 ordered-dict@1.1.0
ostrio:cookies@2.7.2 ostrio:cookies@2.7.2
ostrio:files@2.0.1 ostrio:files@2.3.2
patreon-oauth@0.1.0 patreon-oauth@0.1.0
peerlibrary:assert@0.3.0 peerlibrary:assert@0.3.0
peerlibrary:check-extension@0.7.0 peerlibrary:check-extension@0.7.0
@@ -93,20 +93,20 @@ peerlibrary:reactive-mongo@0.4.1
peerlibrary:reactive-publish@0.10.0 peerlibrary:reactive-publish@0.10.0
peerlibrary:server-autorun@0.8.0 peerlibrary:server-autorun@0.8.0
peerlibrary:subscription-data@0.8.0 peerlibrary:subscription-data@0.8.0
percolate:migrations@1.0.3 percolate:migrations@1.1.0
promise@0.12.0 promise@0.12.1
raix:eventemitter@1.0.0 raix:eventemitter@1.0.0
random@1.2.0 random@1.2.1
rate-limit@1.0.9 rate-limit@1.0.9
react-fast-refresh@0.2.3 react-fast-refresh@0.2.3
reactive-dict@1.3.0 reactive-dict@1.3.1
reactive-var@1.0.11 reactive-var@1.0.12
reload@1.3.1 reload@1.3.1
retry@1.1.0 retry@1.1.0
routepolicy@1.1.1 routepolicy@1.1.1
seba:minifiers-autoprefixer@2.0.1 seba:minifiers-autoprefixer@2.0.1
service-configuration@1.3.0 service-configuration@1.3.1
session@1.2.0 session@1.2.1
sha@1.0.9 sha@1.0.9
shell-server@0.5.0 shell-server@0.5.0
simple:json-routes@2.3.1 simple:json-routes@2.3.1
@@ -116,14 +116,14 @@ simple:rest-json-error-handler@1.1.1
simple:rest-method-mixin@1.1.0 simple:rest-method-mixin@1.1.0
socket-stream-client@0.5.0 socket-stream-client@0.5.0
spacebars-compiler@1.3.1 spacebars-compiler@1.3.1
standard-minifier-js@2.8.0 standard-minifier-js@2.8.1
static-html@1.3.2 static-html@1.3.2
templating-tools@1.2.2 templating-tools@1.2.2
tmeasday:check-npm-versions@1.0.2 tmeasday:check-npm-versions@1.0.2
tracker@1.2.0 tracker@1.2.1
typescript@4.5.4 typescript@4.5.4
underscore@1.0.10 underscore@1.0.11
url@1.3.2 url@1.3.2
webapp@1.13.1 webapp@1.13.2
webapp-hashing@1.1.0 webapp-hashing@1.1.1
zer0th:meteor-vuetify-loader@0.1.41 zer0th:meteor-vuetify-loader@0.1.41

View File

@@ -1,6 +1,6 @@
import '/imports/api/simpleSchemaConfig.js'; import '/imports/api/simpleSchemaConfig.js';
import '/imports/ui/vueSetup.js'; import '/imports/client/ui/vueSetup.js';
import '/imports/ui/styles/stylesIndex.js'; import '/imports/client/ui/styles/stylesIndex.js';
import '/imports/client/config.js'; import '/imports/client/config.js';
import '/imports/client/serviceWorker.js'; import '/imports/client/serviceWorker.js';

View File

@@ -1,12 +1,18 @@
import { createS3FilesCollection } from '/imports/api/files/s3FileStorage.js';
import SimpleSchema from 'simpl-schema'; import SimpleSchema from 'simpl-schema';
import { incrementFileStorageUsed } from '/imports/api/users/methods/updateFileStorageUsed.js'; import { incrementFileStorageUsed } from '/imports/api/users/methods/updateFileStorageUsed.js';
import { CreaturePropertySchema } from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import { CreaturePropertySchema } from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { CreatureSchema } from '/imports/api/creature/creatures/Creatures.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({ const ArchiveCreatureFiles = createS3FilesCollection({
collectionName: 'archiveCreatureFiles', collectionName: 'archiveCreatureFiles',
storagePath: Meteor.isDevelopment ? '/DiceCloud/archiveCreatures/' : 'assets/app/archiveCreatures', storagePath: Meteor.isDevelopment ? '../../../../../fileStorage/archiveCreatures' : 'assets/app/archiveCreatures',
onBeforeUpload(file) { onBeforeUpload(file) {
// Allow upload files under 10MB, and only in json format // Allow upload files under 10MB, and only in json format
if (file.size > 10485760) { if (file.size > 10485760) {

View File

@@ -24,7 +24,7 @@ const damageProperty = new ValidatedMethod({
run({ _id, operation, value }) { run({ _id, operation, value }) {
// Get action context // Get action context
const prop = CreatureProperties.findOne(_id); let prop = CreatureProperties.findOne(_id);
if (!prop) throw new Meteor.Error( if (!prop) throw new Meteor.Error(
'Damage property failed', 'Property doesn\'t exist' 'Damage property failed', 'Property doesn\'t exist'
); );
@@ -43,6 +43,14 @@ const damageProperty = new ValidatedMethod({
); );
} }
// 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 }); const result = damagePropertyWork({ prop, operation, value, actionContext });
// Insert the log // Insert the log
@@ -51,7 +59,7 @@ const damageProperty = new ValidatedMethod({
}, },
}); });
export function damagePropertyWork({ prop, operation, value, actionContext }) { export function damagePropertyWork({ prop, operation, value, actionContext, logFunction }) {
// Save the value to the scope before applying the before triggers // Save the value to the scope before applying the before triggers
if (operation === 'increment') { if (operation === 'increment') {
@@ -94,6 +102,10 @@ export function damagePropertyWork({ prop, operation, value, actionContext }) {
}, { }, {
selector: prop 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') { } else if (operation === 'increment') {
let currentValue = prop.value || 0; let currentValue = prop.value || 0;
let currentDamage = prop.damage || 0; let currentDamage = prop.damage || 0;
@@ -111,6 +123,10 @@ export function damagePropertyWork({ prop, operation, value, actionContext }) {
}, { }, {
selector: prop 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);
} }
applyTriggers(actionContext.triggers?.damageProperty?.after, prop, actionContext); applyTriggers(actionContext.triggers?.damageProperty?.after, prop, actionContext);

View File

@@ -12,7 +12,7 @@ import { reorderDocs } from '/imports/api/parenting/order.js';
var snackbar; var snackbar;
if (Meteor.isClient) { if (Meteor.isClient) {
snackbar = require( snackbar = require(
'/imports/ui/components/snackbars/SnackbarQueue.js' '/imports/client/ui/components/snackbars/SnackbarQueue.js'
).snackbar ).snackbar
} }

View File

@@ -32,11 +32,13 @@ const flipToggle = new ValidatedMethod({
// Invert the current value, disabled is the canonical store of value // Invert the current value, disabled is the canonical store of value
const currentValue = !property.disabled; const currentValue = !property.disabled;
CreatureProperties.update(_id, {$set: { CreatureProperties.update(_id, {
$set: {
enabled: !currentValue, enabled: !currentValue,
disabled: currentValue, disabled: currentValue,
dirty: true, dirty: true,
}}, { }
}, {
selector: { type: 'toggle' }, selector: { type: 'toggle' },
}); });
}, },

View File

@@ -18,6 +18,11 @@ let CreatureSettingsSchema = new SimpleSchema({
type: Boolean, type: Boolean,
optional: true, optional: true,
}, },
//hide rest buttons
hideRestButtons: {
type: Boolean,
optional: true,
},
// Swap around the modifier and stat // Swap around the modifier and stat
swapStatAndModifier: { swapStatAndModifier: {
type: Boolean, type: Boolean,

View File

@@ -6,6 +6,7 @@ import { assertEditPermission } from '/imports/api/creature/creatures/creaturePe
import { union } from 'lodash'; import { union } from 'lodash';
import ActionContext from '/imports/api/engine/actions/ActionContext.js'; import ActionContext from '/imports/api/engine/actions/ActionContext.js';
import { applyTriggers } from '/imports/api/engine/actions/applyTriggers.js'; import { applyTriggers } from '/imports/api/engine/actions/applyTriggers.js';
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
const restCreature = new ValidatedMethod({ const restCreature = new ValidatedMethod({
name: 'creature.methods.rest', name: 'creature.methods.rest',
@@ -62,31 +63,59 @@ function doRestWork(restType, actionContext) {
} else { } 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 // Only apply to active properties
let filter = { const filter = {
'ancestors.id': creatureId, 'ancestors.id': creatureId,
reset: resetFilter, reset: resetFilter,
removed: { $ne: true }, removed: { $ne: true },
inactive: { $ne: true }, inactive: { $ne: true },
}; };
// update all attribute's damage // update all attribute's damage
filter.type = 'attribute'; const attributeFilter = {
CreatureProperties.update(filter, { ...filter,
$set: { type: 'attribute',
damage: 0, damage: { $nin: [0, undefined] },
dirty: true,
} }
}, { CreatureProperties.find(attributeFilter).forEach(prop => {
selector: {type: 'attribute'}, damagePropertyWork({
multi: true, 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 // Update all action-like properties' usesUsed
filter.type = {$in: [ const actionFilter = {
'action', ...filter,
'attack', type: {
'spell' $in: ['action', 'spell']
]}; },
CreatureProperties.update(filter, { 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: { $set: {
usesUsed: 0, usesUsed: 0,
dirty: true, dirty: true,
@@ -95,20 +124,15 @@ function doRestWork(restType, actionContext) {
selector: { type: 'action' }, selector: { type: 'action' },
multi: true, multi: true,
}); });
// Reset half hit dice on a long rest, starting with the highest dice }
if (restType === 'longRest'){
function resetHitDice(creatureId, actionContext) {
let hitDice = CreatureProperties.find({ let hitDice = CreatureProperties.find({
'ancestors.id': creatureId, 'ancestors.id': creatureId,
type: 'attribute', type: 'attribute',
attributeType: 'hitDice', attributeType: 'hitDice',
removed: { $ne: true }, removed: { $ne: true },
inactive: { $ne: true }, inactive: { $ne: true },
}, {
fields: {
hitDiceSize: 1,
damage: 1,
total: 1,
}
}).fetch(); }).fetch();
// Use a collator to do sorting in natural order // Use a collator to do sorting in natural order
let collator = new Intl.Collator('en', { let collator = new Intl.Collator('en', {
@@ -122,23 +146,25 @@ function doRestWork(restType, actionContext) {
let resetMultiplier = actionContext.creature.settings.hitDiceResetMultiplier || 0.5; let resetMultiplier = actionContext.creature.settings.hitDiceResetMultiplier || 0.5;
let recoverableHd = Math.max(Math.floor(totalHd * resetMultiplier), 1); let recoverableHd = Math.max(Math.floor(totalHd * resetMultiplier), 1);
// recover each hit dice in turn until the recoverable amount is used up // recover each hit dice in turn until the recoverable amount is used up
let amountToRecover, resultingDamage; let amountToRecover;
hitDice.forEach(hd => { hitDice.forEach(hd => {
if (!recoverableHd) return; if (!recoverableHd) return;
amountToRecover = Math.min(recoverableHd, hd.damage || 0); amountToRecover = Math.min(recoverableHd, hd.damage ?? 0);
if (!amountToRecover) return; if (!amountToRecover) return;
recoverableHd -= amountToRecover; recoverableHd -= amountToRecover;
resultingDamage = hd.damage - amountToRecover; damagePropertyWork({
CreatureProperties.update(hd._id, { prop: hd,
$set: { operation: 'increment',
damage: resultingDamage, value: -amountToRecover,
dirty: true, actionContext,
} logFunction(increment) {
}, { actionContext.addLog({
selector: {type: 'attribute'}, name: hd.name,
}); value: increment < 0 ? `Restored ${-increment} hit dice` : `Removed ${increment} hit dice`
}); });
} }
});
});
} }
export default restCreature; export default restCreature;

View File

@@ -46,7 +46,7 @@ let ExperienceSchema = new SimpleSchema({
Experiences.attachSchema(ExperienceSchema); Experiences.attachSchema(ExperienceSchema);
const insertExperienceForCreature = function({experience, creatureId, userId}){ const insertExperienceForCreature = function ({ experience, creatureId }) {
if (experience.xp) { if (experience.xp) {
Creatures.update(creatureId, { Creatures.update(creatureId, {
$inc: { 'denormalizedStats.xp': experience.xp }, $inc: { 'denormalizedStats.xp': experience.xp },
@@ -172,11 +172,13 @@ const recomputeExperiences = new ValidatedMethod({
xp += experience.xp || 0; xp += experience.xp || 0;
milestoneLevels += experience.levels || 0; milestoneLevels += experience.levels || 0;
}); });
Creatures.update(creatureId, {$set: { Creatures.update(creatureId, {
$set: {
'denormalizedStats.xp': xp, 'denormalizedStats.xp': xp,
'denormalizedStats.milestoneLevels': milestoneLevels, 'denormalizedStats.milestoneLevels': milestoneLevels,
dirty: true, dirty: true,
}}); }
});
}, },
}); });

View File

@@ -99,14 +99,16 @@ const insertCreatureLog = new ValidatedMethod({
}).validator(), }).validator(),
run({ log }) { run({ log }) {
const creatureId = log.creatureId; const creatureId = log.creatureId;
const creature = Creatures.findOne(creatureId, {fields: { const creature = Creatures.findOne(creatureId, {
fields: {
readers: 1, readers: 1,
writers: 1, writers: 1,
owner: 1, owner: 1,
'settings.discordWebhook': 1, 'settings.discordWebhook': 1,
name: 1, name: 1,
avatarPicture: 1, avatarPicture: 1,
}}); }
});
assertEditPermission(creature, this.userId); assertEditPermission(creature, this.userId);
// Build the new log // Build the new log
let id = insertCreatureLogWork({ log, creature, method: this }) let id = insertCreatureLogWork({ log, creature, method: this })
@@ -154,14 +156,16 @@ const logRoll = new ValidatedMethod({
}, },
}).validator(), }).validator(),
run({ roll, creatureId }) { run({ roll, creatureId }) {
const creature = Creatures.findOne(creatureId, {fields: { const creature = Creatures.findOne(creatureId, {
fields: {
readers: 1, readers: 1,
writers: 1, writers: 1,
owner: 1, owner: 1,
'settings.discordWebhook': 1, 'settings.discordWebhook': 1,
name: 1, name: 1,
avatarPicture: 1, avatarPicture: 1,
}}); }
});
assertEditPermission(creature, this.userId); assertEditPermission(creature, this.userId);
const variables = CreatureVariables.findOne({ _creatureId: creatureId }); const variables = CreatureVariables.findOne({ _creatureId: creatureId });
let logContent = [] let logContent = []

View 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;

View File

@@ -2,7 +2,9 @@ import action from './applyPropertyByType/applyAction.js';
import adjustment from './applyPropertyByType/applyAdjustment.js'; import adjustment from './applyPropertyByType/applyAdjustment.js';
import branch from './applyPropertyByType/applyBranch.js'; import branch from './applyPropertyByType/applyBranch.js';
import buff from './applyPropertyByType/applyBuff.js'; import buff from './applyPropertyByType/applyBuff.js';
import buffRemover from './applyPropertyByType/applyBuffRemover.js';
import damage from './applyPropertyByType/applyDamage.js'; import damage from './applyPropertyByType/applyDamage.js';
import folder from './applyPropertyByType/applyFolder.js';
import note from './applyPropertyByType/applyNote.js'; import note from './applyPropertyByType/applyNote.js';
import roll from './applyPropertyByType/applyRoll.js'; import roll from './applyPropertyByType/applyRoll.js';
import savingThrow from './applyPropertyByType/applySavingThrow.js'; import savingThrow from './applyPropertyByType/applySavingThrow.js';
@@ -13,7 +15,9 @@ const applyPropertyByType = {
adjustment, adjustment,
branch, branch,
buff, buff,
buffRemover,
damage, damage,
folder,
note, note,
roll, roll,
savingThrow, savingThrow,

View File

@@ -5,14 +5,15 @@ import applyProperty from '../applyProperty.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { adjustQuantityWork } from '/imports/api/creature/creatureProperties/methods/adjustQuantity.js'; import { adjustQuantityWork } from '/imports/api/creature/creatureProperties/methods/adjustQuantity.js';
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.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 { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
import { resetProperties } from '/imports/api/creature/creatures/methods/restCreature.js';
export default function applyAction(node, actionContext) { export default function applyAction(node, actionContext) {
applyNodeTriggers(node, 'before', actionContext); applyNodeTriggers(node, 'before', actionContext);
const prop = node.node; const prop = node.node;
let targets = actionContext.targets; if (prop.target === 'self') actionContext.targets = [actionContext.creature];
if (prop.target === 'self') targets = [actionContext.creature]; const targets = actionContext.targets;
// Log the name and summary // Log the name and summary
let content = { name: prop.name }; let content = { name: prop.name };
@@ -20,7 +21,7 @@ export default function applyAction(node, actionContext) {
recalculateInlineCalculations(prop.summary, actionContext); recalculateInlineCalculations(prop.summary, actionContext);
content.value = prop.summary.value; content.value = prop.summary.value;
} }
actionContext.addLog(content); if (!prop.silent) actionContext.addLog(content);
// Spend the resources // Spend the resources
const failed = spendResources(prop, actionContext); const failed = spendResources(prop, actionContext);
@@ -44,6 +45,9 @@ export default function applyAction(node, actionContext) {
} else { } else {
applyChildren(node, actionContext); applyChildren(node, actionContext);
} }
if (prop.actionType === 'event' && prop.variableName) {
resetProperties(actionContext.creature._id, prop.variableName, actionContext);
}
} }
function applyAttackWithoutTarget({ attack, actionContext }) { function applyAttackWithoutTarget({ attack, actionContext }) {
@@ -159,8 +163,9 @@ function rollAttack(attack, scope){
value = rollDice(1, 20)[0]; value = rollDice(1, 20)[0];
resultPrefix = `1d20 [${value}] ${rollModifierText}` resultPrefix = `1d20 [${value}] ${rollModifierText}`
} }
scope['$attackRoll'] = {value}; scope['$attackDiceRoll'] = { value };
const result = value + attack.value; const result = value + attack.value;
scope['$attackRoll'] = { value: result };
const { criticalHit, criticalMiss } = applyCrits(value, scope); const { criticalHit, criticalMiss } = applyCrits(value, scope);
return { resultPrefix, result, value, criticalHit, criticalMiss }; return { resultPrefix, result, value, criticalHit, criticalMiss };
} }
@@ -188,7 +193,7 @@ function applyChildren(node, actionContext) {
function spendResources(prop, actionContext) { function spendResources(prop, actionContext) {
// Check Uses // Check Uses
if (prop.usesLeft <= 0) { if (prop.usesLeft <= 0) {
actionContext.addLog({ if (!prop.silent) actionContext.addLog({
name: 'Error', name: 'Error',
value: `${prop.name || 'action'} does not have enough uses left`, value: `${prop.name || 'action'} does not have enough uses left`,
}); });
@@ -196,7 +201,7 @@ function spendResources(prop, actionContext){
} }
// Resources // Resources
if (prop.insufficientResources) { if (prop.insufficientResources) {
actionContext.addLog({ if (!prop.silent) actionContext.addLog({
name: 'Error', name: 'Error',
value: 'This creature doesn\'t have sufficient resources to perform this action', value: 'This creature doesn\'t have sufficient resources to perform this action',
}); });
@@ -257,7 +262,7 @@ function spendResources(prop, actionContext){
}, { }, {
selector: prop selector: prop
}); });
actionContext.addLog({ if (!prop.silent) actionContext.addLog({
name: 'Uses left', name: 'Uses left',
value: prop.usesLeft - 1, value: prop.usesLeft - 1,
inline: true, inline: true,
@@ -288,12 +293,12 @@ function spendResources(prop, actionContext){
}); });
// Log all the spending // Log all the spending
if (gainLog.length) actionContext.addLog({ if (gainLog.length && !prop.silent) actionContext.addLog({
name: 'Gained', name: 'Gained',
value: gainLog.join('\n'), value: gainLog.join('\n'),
inline: true, inline: true,
}); });
if (spendLog.length) actionContext.addLog({ if (spendLog.length && !prop.silent) actionContext.addLog({
name: 'Spent', name: 'Spent',
value: spendLog.join('\n'), value: spendLog.join('\n'),
inline: true, inline: true,

View File

@@ -24,7 +24,7 @@ export default function applyAdjustment(node, actionContext){
damageTargets.forEach(target => { damageTargets.forEach(target => {
let stat = target.variables[prop.stat]; let stat = target.variables[prop.stat];
if (!stat?.type) { if (!stat?.type) {
actionContext.addLog({ if (!prop.silent) actionContext.addLog({
name: 'Error', name: 'Error',
value: `Could not apply attribute damage, creature does not have \`${prop.stat}\` set` value: `Could not apply attribute damage, creature does not have \`${prop.stat}\` set`
}); });
@@ -36,7 +36,7 @@ export default function applyAdjustment(node, actionContext){
value, value,
actionContext, actionContext,
}); });
actionContext.addLog({ if (!prop.silent) actionContext.addLog({
name: 'Attribute damage', name: 'Attribute damage',
value: `${prop.stat}${prop.operation === 'set' ? ' set to' : ''}` + value: `${prop.stat}${prop.operation === 'set' ? ' set to' : ''}` +
` ${value}`, ` ${value}`,
@@ -44,7 +44,7 @@ export default function applyAdjustment(node, actionContext){
}); });
}); });
} else { } else {
actionContext.addLog({ if (!prop.silent) actionContext.addLog({
name: 'Attribute damage', name: 'Attribute damage',
value: `${prop.stat}${prop.operation === 'set' ? ' set to' : ''}` + value: `${prop.stat}${prop.operation === 'set' ? ' set to' : ''}` +
` ${value}`, ` ${value}`,

View File

@@ -36,25 +36,25 @@ export default function applyBranch(node, actionContext){
break; break;
case 'hit': case 'hit':
if (scope['$attackHit']?.value){ if (scope['$attackHit']?.value){
if (!targets.length) actionContext.addLog({value: '**On hit**'}); if (!targets.length && !prop.silent) actionContext.addLog({value: '**On hit**'});
applyChildren(); applyChildren();
} }
break; break;
case 'miss': case 'miss':
if (scope['$attackMiss']?.value){ if (scope['$attackMiss']?.value){
if (!targets.length) actionContext.addLog({value: '**On miss**'}); if (!targets.length && !prop.silent) actionContext.addLog({value: '**On miss**'});
applyChildren(); applyChildren();
} }
break; break;
case 'failedSave': case 'failedSave':
if (scope['$saveFailed']?.value){ if (scope['$saveFailed']?.value){
if (!targets.length) actionContext.addLog({value: '**On failed save**'}); if (!targets.length && !prop.silent) actionContext.addLog({value: '**On failed save**'});
applyChildren(); applyChildren();
} }
break; break;
case 'successfulSave': case 'successfulSave':
if (scope['$saveSucceeded']?.value){ if (scope['$saveSucceeded']?.value){
if (!targets.length) actionContext.addLog({value: '**On save**',}); if (!targets.length && !prop.silent) actionContext.addLog({value: '**On save**',});
applyChildren(); applyChildren();
} }
break; break;

View File

@@ -13,6 +13,8 @@ import logErrors from './shared/logErrors.js';
import { insertCreatureLog } from '/imports/api/creature/log/CreatureLogs.js'; import { insertCreatureLog } from '/imports/api/creature/log/CreatureLogs.js';
import cyrb53 from '/imports/api/engine/computation/utility/cyrb53.js'; import cyrb53 from '/imports/api/engine/computation/utility/cyrb53.js';
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.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, actionContext) { export default function applyBuff(node, actionContext) {
applyNodeTriggers(node, 'before', actionContext); applyNodeTriggers(node, 'before', actionContext);
@@ -21,14 +23,20 @@ export default function applyBuff(node, actionContext){
// Then copy the decendants of the buff to the targets // Then copy the decendants of the buff to the targets
let propList = [prop]; let propList = [prop];
function addChildrenToPropList(children){ function addChildrenToPropList(children, { skipCrystalize } = {}) {
children.forEach(child => { children.forEach(child => {
if (skipCrystalize) child.node._skipCrystalize = true;
propList.push(child.node); 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); addChildrenToPropList(node.children);
if (!prop.skipCrystalization) {
crystalizeVariables({ propList, actionContext }); crystalizeVariables({ propList, actionContext });
}
let oldParent = { let oldParent = {
id: prop.parent.id, id: prop.parent.id,
@@ -39,12 +47,17 @@ export default function applyBuff(node, actionContext){
copyNodeListToTarget(propList, target, oldParent); copyNodeListToTarget(propList, target, oldParent);
//Log the buff //Log the buff
if (prop.name || prop.description?.value){ 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) { if (target._id === actionContext.creature._id) {
// Targeting self // Targeting self
actionContext.addLog({ actionContext.addLog({
name: prop.name, name: prop.name,
value: prop.description?.value, value: logValue,
}); });
} else { } else {
// Targeting other // Targeting other
@@ -53,7 +66,7 @@ export default function applyBuff(node, actionContext){
creatureId: target._id, creatureId: target._id,
content: [{ content: [{
name: prop.name, name: prop.name,
value: prop.description?.value, value: logValue,
}], }],
} }
}); });
@@ -88,6 +101,11 @@ function copyNodeListToTarget(propList, target, oldParent){
*/ */
function crystalizeVariables({ propList, actionContext }) { function crystalizeVariables({ propList, actionContext }) {
propList.forEach(prop => { propList.forEach(prop => {
if (prop._skipCrystalize) {
delete prop._skipCrystalize;
return;
}
// Iterate through all the calculations and crystalize them
computedSchemas[prop.type].computedFields().forEach(calcKey => { computedSchemas[prop.type].computedFields().forEach(calcKey => {
applyFnToKey(prop, calcKey, (prop, key) => { applyFnToKey(prop, calcKey, (prop, key) => {
const calcObj = get(prop, key); const calcObj = get(prop, key);
@@ -124,5 +142,36 @@ function crystalizeVariables({propList, actionContext}){
calcObj.hash = cyrb53(calcObj.calculation); 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;
});
});
}); });
} }

View File

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

View File

@@ -1,4 +1,4 @@
import { some, intersection, difference, remove } from 'lodash'; import { some, intersection, difference, remove, includes } from 'lodash';
import applyProperty from '../applyProperty.js'; import applyProperty from '../applyProperty.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 resolve, { Context, toString } from '/imports/parser/resolve.js';
@@ -9,6 +9,7 @@ import {
getPropertiesOfType getPropertiesOfType
} from '/imports/api/engine/loadCreatures.js'; } from '/imports/api/engine/loadCreatures.js';
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.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, actionContext) { export default function applyDamage(node, actionContext) {
applyNodeTriggers(node, 'before', actionContext); applyNodeTriggers(node, 'before', actionContext);
@@ -128,7 +129,7 @@ export default function applyDamage(node, actionContext){
// There are no targets, just log the result // There are no targets, just log the result
logValue.push(`**${damage}** ${suffix}`); logValue.push(`**${damage}** ${suffix}`);
} }
actionContext.addLog({ if (!prop.silent) actionContext.addLog({
name: logName, name: logName,
value: logValue.join('\n'), value: logValue.join('\n'),
inline: true, inline: true,
@@ -147,21 +148,21 @@ function applyDamageMultipliers({target, damage, damageProp, logValue}){
if ( if (
multiplier.immunity && multiplier.immunity &&
some(multiplier.immunities, multiplierAppliesTo(damageProp)) some(multiplier.immunities, multiplierAppliesTo(damageProp, 'immunity'))
) { ) {
logValue.push(`Immune to ${damageTypeText}`); logValue.push(`Immune to ${damageTypeText}`);
return 0; return 0;
} else { } else {
if ( if (
multiplier.resistance && multiplier.resistance &&
some(multiplier.resistances, multiplierAppliesTo(damageProp)) some(multiplier.resistances, multiplierAppliesTo(damageProp, 'resistance'))
) { ) {
logValue.push(`Resistant to ${damageTypeText}`); logValue.push(`Resistant to ${damageTypeText}`);
damage = Math.floor(damage / 2); damage = Math.floor(damage / 2);
} }
if ( if (
multiplier.vulnerability && multiplier.vulnerability &&
some(multiplier.vulnerabilities, multiplierAppliesTo(damageProp)) some(multiplier.vulnerabilities, multiplierAppliesTo(damageProp, 'vulnerability'))
) { ) {
logValue.push(`Vulnerable to ${damageTypeText}`); logValue.push(`Vulnerable to ${damageTypeText}`);
damage = Math.floor(damage * 2); damage = Math.floor(damage * 2);
@@ -170,14 +171,18 @@ function applyDamageMultipliers({target, damage, damageProp, logValue}){
return damage; return damage;
} }
function multiplierAppliesTo(damageProp){ function multiplierAppliesTo(damageProp, multiplierType) {
return multiplier => { return multiplier => {
// Apply the default 'ignore x' tags
const effectiveTags = getEffectivePropTags(damageProp);
if (includes(effectiveTags, `ignore ${multiplierType}`)) return false;
const hasRequiredTags = difference( const hasRequiredTags = difference(
multiplier.includeTags, damageProp.tags multiplier.includeTags, effectiveTags
).length === 0; ).length === 0;
const hasNoExcludedTags = intersection( const hasNoExcludedTags = intersection(
multiplier.excludeTags, damageProp.tags multiplier.excludeTags, effectiveTags
).length === 0; ).length === 0;
return hasRequiredTags && hasNoExcludedTags; return hasRequiredTags && hasNoExcludedTags;
@@ -219,6 +224,16 @@ function dealDamage({target, damageType, amount, actionContext}){
if (damageType === 'healing') damageLeft = -totalDamage; if (damageType === 'healing') damageLeft = -totalDamage;
healthBars.forEach(healthBar => { healthBars.forEach(healthBar => {
if (damageLeft === 0) return; 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({ let damageAdded = damagePropertyWork({
prop: healthBar, prop: healthBar,
operation: 'increment', operation: 'increment',
@@ -226,6 +241,14 @@ function dealDamage({target, damageType, amount, actionContext}){
actionContext actionContext
}); });
damageLeft -= damageAdded; damageLeft -= damageAdded;
// Prevent overflow
if (
damageType === 'healing' ?
healthBar.healthBarNoHealingOverflow :
healthBar.healthBarNoDamageOverflow
) {
damageLeft = 0;
}
}); });
return totalDamage; return totalDamage;
} }

View File

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

View File

@@ -1,8 +1,9 @@
import rollDice from '/imports/parser/rollDice.js'; import rollDice from '/imports/parser/rollDice.js';
import recalculateCalculation from './shared/recalculateCalculation.js'; import recalculateCalculation from './shared/recalculateCalculation.js';
import applyProperty from '../applyProperty.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 { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
import { applyUnresolvedEffects } from '/imports/api/engine/actions/doCheck.js';
export default function applySavingThrow(node, actionContext) { export default function applySavingThrow(node, actionContext) {
applyNodeTriggers(node, 'before', actionContext); applyNodeTriggers(node, 'before', actionContext);
@@ -20,7 +21,7 @@ export default function applySavingThrow(node, actionContext){
}); });
return node.children.forEach(child => applyProperty(child, actionContext)); return node.children.forEach(child => applyProperty(child, actionContext));
} }
actionContext.addLog({ if (!prop.silent) actionContext.addLog({
name: prop.name, name: prop.name,
value: `DC **${dc}**`, value: `DC **${dc}**`,
inline: true, inline: true,
@@ -59,7 +60,11 @@ export default function applySavingThrow(node, actionContext){
return applyChildren(); 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; let value, values, resultPrefix;
if (save.advantage === 1) { if (save.advantage === 1) {
@@ -86,7 +91,7 @@ export default function applySavingThrow(node, actionContext){
resultPrefix = `1d20 [ ${value} ] ${rollModifierText}` resultPrefix = `1d20 [ ${value} ] ${rollModifierText}`
} }
scope['$saveDiceRoll'] = { value }; scope['$saveDiceRoll'] = { value };
const result = value + save.value || 0; const result = value + rollModifier || 0;
scope['$saveRoll'] = { value: result }; scope['$saveRoll'] = { value: result };
const saveSuccess = result >= dc; const saveSuccess = result >= dc;
if (saveSuccess) { if (saveSuccess) {
@@ -94,7 +99,7 @@ export default function applySavingThrow(node, actionContext){
} else { } else {
scope['$saveFailed'] = { value: true }; scope['$saveFailed'] = { value: true };
} }
actionContext.addLog({ if (!prop.silent) actionContext.addLog({
name: saveSuccess ? 'Successful save' : 'Failed save', name: saveSuccess ? 'Successful save' : 'Failed save',
value: resultPrefix + '\n**' + result + '**', value: resultPrefix + '\n**' + result + '**',
inline: true, inline: true,

View File

@@ -5,7 +5,7 @@ import logErrors from './logErrors.js';
export default function recalculateCalculation(calc, actionContext, context){ export default function recalculateCalculation(calc, actionContext, context){
if (!calc?.parseNode) return; if (!calc?.parseNode) return;
calc._parseLevel = 'reduce'; calc._parseLevel = 'reduce';
applyEffectsToCalculationParseNode(calc, actionContext.log); applyEffectsToCalculationParseNode(calc, actionContext);
evaluateCalculation(calc, actionContext.scope, context); evaluateCalculation(calc, actionContext.scope, context);
logErrors(calc.errors, actionContext.log); logErrors(calc.errors, actionContext);
} }

View File

@@ -65,7 +65,7 @@ export function applyTrigger(trigger, prop, actionContext) {
recalculateInlineCalculations(trigger.description, actionContext); recalculateInlineCalculations(trigger.description, actionContext);
content.value = trigger.description.value; content.value = trigger.description.value;
} }
actionContext.addLog(content); if(!trigger.silent) actionContext.addLog(content);
// Get all the trigger's properties and apply them // Get all the trigger's properties and apply them
const properties = getPropertyDecendants(actionContext.creature._id, trigger._id); const properties = getPropertyDecendants(actionContext.creature._id, trigger._id);

View File

@@ -9,7 +9,6 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js'; import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js'; import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
import { doActionWork } from '/imports/api/engine/actions/doAction.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'; import ActionContext from '/imports/api/engine/actions/ActionContext.js';
const doAction = new ValidatedMethod({ const doAction = new ValidatedMethod({
@@ -21,6 +20,10 @@ const doAction = new ValidatedMethod({
regEx: SimpleSchema.RegEx.Id, regEx: SimpleSchema.RegEx.Id,
optional: true, optional: true,
}, },
ritual: {
type: Boolean,
optional: true,
},
targetIds: { targetIds: {
type: Array, type: Array,
defaultValue: [], defaultValue: [],
@@ -42,7 +45,7 @@ const doAction = new ValidatedMethod({
numRequests: 10, numRequests: 10,
timeInterval: 5000, timeInterval: 5000,
}, },
run({ spellId, slotId, targetIds = [], scope = {} }) { run({ spellId, slotId, ritual, targetIds = [], scope = {} }) {
// Get action context // Get action context
let spell = CreatureProperties.findOne(spellId); let spell = CreatureProperties.findOne(spellId);
const creatureId = spell.ancestors[0].id; const creatureId = spell.ancestors[0].id;
@@ -65,9 +68,8 @@ const doAction = new ValidatedMethod({
let slotLevel = spell.level || 0; let slotLevel = spell.level || 0;
let slot; let slot;
actionContext.scope['slotLevel'] = slotLevel; // If a spell requires a slot, make sure a slot is spent
if (spell.level && !spell.castWithoutSpellSlots && !(ritual && spell.ritual)) {
if (slotId && !spell.castWithoutSpellSlots){
slot = CreatureProperties.findOne(slotId); slot = CreatureProperties.findOne(slotId);
if (!slot) { if (!slot) {
throw new Meteor.Error('No slot', throw new Meteor.Error('No slot',
@@ -104,10 +106,18 @@ const doAction = new ValidatedMethod({
name: `Casting using a level ${slotLevel} spell slot` name: `Casting using a level ${slotLevel} spell slot`
}); });
} else if (slotLevel) { } else if (slotLevel) {
if (ritual) {
actionContext.addLog({
name: `Ritual casting at level ${slotLevel}`
});
} else {
actionContext.addLog({ actionContext.addLog({
name: `Casting at level ${slotLevel}` name: `Casting at level ${slotLevel}`
}); });
} }
}
actionContext.scope['slotLevel'] = slotLevel;
// Do the action // Do the action
doActionWork({ doActionWork({

View File

@@ -4,9 +4,10 @@ import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js'; import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import rollDice from '/imports/parser/rollDice.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 { applyTriggers } from '/imports/api/engine/actions/applyTriggers.js';
import ActionContext from '/imports/api/engine/actions/ActionContext.js'; import ActionContext from '/imports/api/engine/actions/ActionContext.js';
import evaluateCalculation from '/imports/api/engine/computation/utility/evaluateCalculation.js';
const doCheck = new ValidatedMethod({ const doCheck = new ValidatedMethod({
name: 'creatureProperties.doCheck', name: 'creatureProperties.doCheck',
@@ -27,6 +28,7 @@ const doCheck = new ValidatedMethod({
const creatureId = prop.ancestors[0].id; const creatureId = prop.ancestors[0].id;
const actionContext = new ActionContext(creatureId, [creatureId], this); const actionContext = new ActionContext(creatureId, [creatureId], this);
Object.assign(actionContext.scope, scope); Object.assign(actionContext.scope, scope);
actionContext.scope[`#${prop.type}`] = prop;
// Check permissions // Check permissions
assertEditPermission(actionContext.creature, this.userId); assertEditPermission(actionContext.creature, this.userId);
@@ -72,7 +74,11 @@ function rollCheck(prop, actionContext) {
throw (`${prop.type} not supported for checks`); 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; let value, values, resultPrefix;
if (scope['$checkAdvantage'] === 1) { if (scope['$checkAdvantage'] === 1) {
@@ -101,8 +107,29 @@ function rollCheck(prop, actionContext) {
resultPrefix = `1d20 [ ${value} ] ${rollModifierText} = ` resultPrefix = `1d20 [ ${value} ] ${rollModifierText} = `
} }
const result = (value + rollModifier) || 0; const result = (value + rollModifier) || 0;
scope['$checkDiceRoll'] = value;
scope['$checkRoll'] = result;
scope['$checkModifier'] = rollModifier;
actionContext.addLog({ actionContext.addLog({
name: logName, name: logName,
value: `${resultPrefix} **${result}**`, 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 };
}

View File

@@ -1,9 +1,24 @@
import { EJSON } from 'meteor/ejson'; 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'; import getEffectivePropTags from '/imports/api/engine/computation/utility/getEffectivePropTags.js';
interface CreatureProperty {
_id: string;
type: string;
}
export default class CreatureComputation { 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 // Set up fields
this.originalPropsById = {}; this.originalPropsById = {};
this.propsById = {}; this.propsById = {};

View File

@@ -29,8 +29,8 @@ function childrenActive(prop){
// Children of disabled properties are always inactive // Children of disabled properties are always inactive
if (prop.disabled) return false; if (prop.disabled) return false;
switch (prop.type){ switch (prop.type){
// Only equipped items have active children // Only equipped items with non-zero quantity have active children
case 'item': return !!prop.equipped; case 'item': return !!prop.equipped && prop.quantity !== 0;
// The children of actions, spells, and triggers are always inactive // The children of actions, spells, and triggers are always inactive
case 'action': return false; case 'action': return false;
case 'spell': return false; case 'spell': return false;

View File

@@ -10,8 +10,8 @@ export default function computeToggleDependencies(node, dependencyGraph){
prop.enabled prop.enabled
) return; ) return;
walkDown(node.children, child => { walkDown(node.children, child => {
child.node._computationDetails.toggleAncestors.push(prop);
// The child nodes depend on the toggle condition compuation // The child nodes depend on the toggle condition compuation
child.node._computationDetails.toggleAncestors.push(prop);
dependencyGraph.addLink(child.node._id, prop._id, 'toggle'); dependencyGraph.addLink(child.node._id, prop._id, 'toggle');
}); });
} }

View File

@@ -22,6 +22,7 @@ export default function linkCalculationDependencies(dependencyGraph, prop, {prop
); );
if (!ancestorProp) return; if (!ancestorProp) return;
// Link the ancestor prop as a direct dependency // Link the ancestor prop as a direct dependency
// TODO: we might be referencing a calculation sub-field, depend on that instead
dependencyGraph.addLink( dependencyGraph.addLink(
calcNodeId, ancestorProp._id, 'ancestorReference' calcNodeId, ancestorProp._id, 'ancestorReference'
); );

View File

@@ -14,6 +14,7 @@ const linkDependenciesByType = {
effect: linkEffects, effect: linkEffects,
proficiency: linkProficiencies, proficiency: linkProficiencies,
roll: linkRoll, roll: linkRoll,
pointBuy: linkPointBuy,
propertySlot: linkSlot, propertySlot: linkSlot,
skill: linkSkill, skill: linkSkill,
spell: linkAction, spell: linkAction,
@@ -36,6 +37,9 @@ function dependOnCalc({dependencyGraph, prop, key}){
} }
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 // The action depends on its attack roll and uses calculations
dependOnCalc({ dependencyGraph, prop, key: 'attackRoll' }); dependOnCalc({ dependencyGraph, prop, key: 'attackRoll' });
dependOnCalc({ dependencyGraph, prop, key: 'uses' }); dependOnCalc({ dependencyGraph, prop, key: 'uses' });
@@ -106,6 +110,7 @@ function linkBuff(dependencyGraph, prop){
} }
function linkClassLevel(dependencyGraph, prop) { function linkClassLevel(dependencyGraph, prop) {
if (prop.inactive) return;
// The variableName of the prop depends on the prop // 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'); dependencyGraph.addLink(prop.variableName, prop._id, 'classLevel');
@@ -124,8 +129,13 @@ function linkDamage(dependencyGraph, prop){
function linkEffects(dependencyGraph, prop, computation) { function linkEffects(dependencyGraph, prop, computation) {
// The effect depends on its amount calculation // 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 // 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 => { getEffectTagTargets(prop, computation).forEach(targetId => {
const targetProp = computation.propsById[targetId]; const targetProp = computation.propsById[targetId];
if ( if (
@@ -221,13 +231,14 @@ function linkRoll(dependencyGraph, prop){
} }
function linkVariableName(dependencyGraph, prop) { function linkVariableName(dependencyGraph, prop) {
// The variableName of the prop depends on the prop // The variableName of the prop depends on the prop if the prop is active
if (prop.variableName){ if (prop.variableName && !prop.inactive) {
dependencyGraph.addLink(prop.variableName, prop._id, 'definition'); dependencyGraph.addLink(prop.variableName, prop._id, 'definition');
} }
} }
function linkDamageMultiplier(dependencyGraph, prop) { function linkDamageMultiplier(dependencyGraph, prop) {
if (prop.inactive) return;
prop.damageTypes.forEach(damageType => { prop.damageTypes.forEach(damageType => {
// Remove all non-letter characters from the damage name // Remove all non-letter characters from the damage name
const damageName = damageType.replace(/[^a-z]/gi, '') const damageName = damageType.replace(/[^a-z]/gi, '')
@@ -235,8 +246,31 @@ function linkDamageMultiplier(dependencyGraph, prop){
}); });
} }
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 linkProficiencies(dependencyGraph, prop) { function linkProficiencies(dependencyGraph, prop) {
// The stats depend on the proficiency // The stats depend on the proficiency
if (prop.inactive) return;
prop.stats.forEach(statName => { prop.stats.forEach(statName => {
if (!statName) return; if (!statName) return;
dependencyGraph.addLink(statName, prop._id, prop.type); dependencyGraph.addLink(statName, prop._id, prop.type);
@@ -248,6 +282,10 @@ function linkSavingThrow(dependencyGraph, prop){
} }
function linkSkill(dependencyGraph, prop) { function linkSkill(dependencyGraph, prop) {
// Depends on base value
dependOnCalc({ dependencyGraph, prop, key: 'baseValue' });
// Link dependents
if (prop.inactive) return;
linkVariableName(dependencyGraph, prop); linkVariableName(dependencyGraph, prop);
// The prop depends on the variable references as the ability // The prop depends on the variable references as the ability
if (prop.ability) { if (prop.ability) {
@@ -255,9 +293,6 @@ function linkSkill(dependencyGraph, prop){
} }
// Skills depend on the creature's proficiencyBonus // Skills depend on the creature's proficiencyBonus
dependencyGraph.addLink(prop._id, 'proficiencyBonus', 'skillProficiencyBonus'); dependencyGraph.addLink(prop._id, 'proficiencyBonus', 'skillProficiencyBonus');
// Depends on base value
dependOnCalc({dependencyGraph, prop, key: 'baseValue'});
} }
function linkSlot(dependencyGraph, prop) { function linkSlot(dependencyGraph, prop) {

View File

@@ -12,7 +12,7 @@ import computeToggleDependencies from './buildComputation/computeToggleDependenc
import linkCalculationDependencies from './buildComputation/linkCalculationDependencies.js'; import linkCalculationDependencies from './buildComputation/linkCalculationDependencies.js';
import linkTypeDependencies from './buildComputation/linkTypeDependencies.js'; import linkTypeDependencies from './buildComputation/linkTypeDependencies.js';
import computeSlotQuantityFilled from './buildComputation/computeSlotQuantityFilled.js'; import computeSlotQuantityFilled from './buildComputation/computeSlotQuantityFilled.js';
import CreatureComputation from './CreatureComputation.js'; import CreatureComputation from './CreatureComputation.ts';
import removeSchemaFields from './buildComputation/removeSchemaFields.js'; import removeSchemaFields from './buildComputation/removeSchemaFields.js';
/** /**
@@ -89,6 +89,10 @@ export function buildComputationFromProps(properties, creature, variables){
// Walk the property trees computing things that need to be inherited // Walk the property trees computing things that need to be inherited
walkDown(forest, node => { walkDown(forest, node => {
computeInactiveStatus(node); computeInactiveStatus(node);
});
// Inactive status must be complete for the whole tree before toggle deps
// are calculated
walkDown(forest, node => {
computeToggleDependencies(node, dependencyGraph); computeToggleDependencies(node, dependencyGraph);
computeSlotQuantityFilled(node, dependencyGraph); computeSlotQuantityFilled(node, dependencyGraph);
}); });

View File

@@ -2,8 +2,10 @@ import _variable from './computeByType/computeVariable.js';
import action from './computeByType/computeAction.js'; import action from './computeByType/computeAction.js';
import attribute from './computeByType/computeAttribute.js'; import attribute from './computeByType/computeAttribute.js';
import skill from './computeByType/computeSkill.js'; import skill from './computeByType/computeSkill.js';
import pointBuy from './computeByType/computePointBuy.js';
import propertySlot from './computeByType/computeSlot.js'; import propertySlot from './computeByType/computeSlot.js';
import container from './computeByType/computeContainer.js'; import container from './computeByType/computeContainer.js';
import spellList from './computeByType/computeSpellList.js';
import _calculation from './computeByType/computeCalculation.js'; import _calculation from './computeByType/computeCalculation.js';
export default Object.freeze({ export default Object.freeze({
@@ -13,6 +15,8 @@ export default Object.freeze({
attribute, attribute,
container, container,
skill, skill,
pointBuy,
propertySlot, propertySlot,
spell: action, spell: action,
spellList,
}); });

View File

@@ -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',
});
}
}

View File

@@ -4,7 +4,7 @@
// by computeVariableAsSkill // by computeVariableAsSkill
export default function computeSkill(computation, node){ export default function computeSkill(computation, node){
const prop = node.data; const prop = node.data;
prop.proficiency = prop.baseProficiency; prop.proficiency = prop.baseProficiency || 0;
let profBonus = computation.scope['proficiencyBonus']?.value || 0; let profBonus = computation.scope['proficiencyBonus']?.value || 0;
// Multiply the proficiency bonus by the actual proficiency // Multiply the proficiency bonus by the actual proficiency
if(prop.proficiency === 0.49){ if(prop.proficiency === 0.49){

View File

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

View File

@@ -37,6 +37,7 @@ function aggregateLinks(computation, node){
aggregate.damageMultiplier(arg); aggregate.damageMultiplier(arg);
aggregate.definition(arg); aggregate.definition(arg);
aggregate.effect(arg); aggregate.effect(arg);
aggregate.eventDefinition(arg);
aggregate.inventory(arg); aggregate.inventory(arg);
aggregate.proficiency(arg); aggregate.proficiency(arg);
}, },

View File

@@ -8,7 +8,13 @@ export default function aggregateDefinition({node, linkedNode, link}){
// get current defining prop // get current defining prop
const definingProp = node.data.definingProp; const definingProp = node.data.definingProp;
// Find the last defining prop // 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 // override the current defining prop
overrideProp(definingProp, node); overrideProp(definingProp, node);
// set this prop as the new defining prop // 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 // 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; 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){ if (node.data.baseValue === undefined || propBaseValue > node.data.baseValue){
node.data.baseValue = propBaseValue; node.data.baseValue = propBaseValue;
} }

View File

@@ -1,3 +1,5 @@
import { pick } from 'lodash';
export default function aggregateEffect({ node, linkedNode, link }) { export default function aggregateEffect({ node, linkedNode, link }) {
if (link.data !== 'effect') return; if (link.data !== 'effect') return;
// store the effect aggregator, its presence indicates that the variable is // store the effect aggregator, its presence indicates that the variable is
@@ -19,20 +21,33 @@ export default function aggregateEffect({node, linkedNode, link}){
// Store a summary of the effect itself // Store a summary of the effect itself
node.data.effects = node.data.effects || []; 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({ node.data.effects.push({
_id: linkedNode.data._id, _id: linkedNode.data._id,
name: linkedNode.data.name, name: linkedNode.data.name,
operation: linkedNode.data.operation, 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, // ancestors: linkedNode.data.ancestors,
}); });
// get a shorter reference to the aggregator document // get a shorter reference to the aggregator document
const aggregator = node.data.effectAggregator; const aggregator = node.data.effectAggregator;
// Get the result of the effect // Get the result of the effect
const result = linkedNode.data.amount?.value; let result = linkedNode.data.amount?.value;
// Skip aggregating if the result is not resolved completely if (typeof result !== 'number') result = undefined;
if (typeof result === 'string') return;
// Aggregate the effect based on its operation // Aggregate the effect based on its operation
switch (linkedNode.data.operation) { switch (linkedNode.data.operation) {
case 'base': case 'base':

View File

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

View File

@@ -1,6 +1,7 @@
import definition from './aggregateDefinition.js'; import definition from './aggregateDefinition.js';
import damageMultiplier from './aggregateDamageMultiplier.js'; import damageMultiplier from './aggregateDamageMultiplier.js';
import effect from './aggregateEffect.js'; import effect from './aggregateEffect.js';
import eventDefinition from './aggregateEventDefinition.js';
import proficiency from './aggregateProficiency.js'; import proficiency from './aggregateProficiency.js';
import classLevel from './aggregateClassLevel.js'; import classLevel from './aggregateClassLevel.js';
import inventory from './aggregateInventory.js'; import inventory from './aggregateInventory.js';
@@ -10,6 +11,7 @@ export default Object.freeze({
damageMultiplier, damageMultiplier,
definition, definition,
effect, effect,
eventDefinition,
inventory, inventory,
proficiency, proficiency,
}); });

View File

@@ -33,6 +33,9 @@ export default function computeVariableAsSkill(computation, node, prop){
const aggregator = node.data.effectAggregator; const aggregator = node.data.effectAggregator;
const aggregatorBase = aggregator?.base || 0; 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 there is no aggregator, determine if the prop can hide, then exit
if (!aggregator){ if (!aggregator){
prop.hide = statBase === undefined && prop.hide = statBase === undefined &&
@@ -71,8 +74,6 @@ export default function computeVariableAsSkill(computation, node, prop){
prop.fail = aggregator.fail; prop.fail = aggregator.fail;
// Rollbonus // Rollbonus
prop.rollBonuses = aggregator.rollBonus; prop.rollBonuses = aggregator.rollBonus;
// Store effects
prop.effects = node.data.effects;
} }
function aggregateAbilityEffects({computation, skillNode, abilityNode}){ function aggregateAbilityEffects({computation, skillNode, abilityNode}){

View File

@@ -29,7 +29,7 @@ export default function getAggregatorResult(node){
if (aggregator.set !== undefined) { if (aggregator.set !== undefined) {
result = aggregator.set; result = aggregator.set;
} }
if (!node.definingProp?.decimal && Number.isFinite(result)){ if (!node.data.definingProp?.decimal && Number.isFinite(result)){
result = Math.floor(result); result = Math.floor(result);
} else if (Number.isFinite(result)){ } else if (Number.isFinite(result)){
result = stripFloatingPointOddities(result); result = stripFloatingPointOddities(result);

View File

@@ -4,7 +4,7 @@ export default function evaluateToggles(computation, node){
let toggles = prop._computationDetails?.toggleAncestors; let toggles = prop._computationDetails?.toggleAncestors;
if (!toggles) return; if (!toggles) return;
toggles.forEach(toggle => { toggles.forEach(toggle => {
if (prop.inactive || !toggle.condition) return; if (!toggle.condition) return;
if (!toggle.condition.value){ if (!toggle.condition.value){
prop.inactive = true; prop.inactive = true;
prop.deactivatedByToggle = true; prop.deactivatedByToggle = true;

View File

@@ -52,10 +52,21 @@ function compute(computation, node){
function pushDependenciesToStack(nodeId, graph, stack, computation){ function pushDependenciesToStack(nodeId, graph, stack, computation){
graph.forEachLinkedNode(nodeId, linkedNode => { graph.forEachLinkedNode(nodeId, linkedNode => {
if (linkedNode._visitedChildren && !linkedNode._visited) { if (linkedNode._visitedChildren && !linkedNode._visited) {
const pather = path.nba(graph, { // This is a dependency loop, find a path from the node to itself
oriented: true // and store that path as a dependency loop error
}); const pather = path.nba(graph, { oriented: true });
const loop = pather.find(nodeId, nodeId); 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) { if (loop.length) {
computation.errors.push({ computation.errors.push({
type: 'dependencyLoop', type: 'dependencyLoop',

View File

@@ -9,8 +9,10 @@ export default function getEffectivePropTags(prop) {
} }
// Tags for some string properties // Tags for some string properties
if (prop.variableName) tags.push(prop.variableName);
if (prop.damageType) tags.push(prop.damageType); if (prop.damageType) tags.push(prop.damageType);
if (prop.skillType) tags.push(prop.skillType); if (prop.skillType) tags.push(prop.skillType);
if (prop.actionType) tags.push(prop.actionType);
if (prop.attributeType) tags.push(prop.attributeType); if (prop.attributeType) tags.push(prop.attributeType);
if (prop.reset) tags.push(prop.reset); if (prop.reset) tags.push(prop.reset);
return tags; return tags;

View File

@@ -46,7 +46,6 @@ export function getSingleProperty(creatureId, propertyId) {
'removed': {$ne: true}, 'removed': {$ne: true},
}, { }, {
sort: { order: 1 }, sort: { order: 1 },
fields: { icon: 0 },
}); });
// console.timeEnd(`Cache miss on creature properties: ${creatureId}`); // console.timeEnd(`Cache miss on creature properties: ${creatureId}`);
return prop; return prop;
@@ -65,7 +64,6 @@ export function getProperties(creatureId) {
'removed': {$ne: true}, 'removed': {$ne: true},
}, { }, {
sort: { order: 1 }, sort: { order: 1 },
fields: { icon: 0 },
}).fetch(); }).fetch();
// console.timeEnd(`Cache miss on creature properties: ${creatureId}`); // console.timeEnd(`Cache miss on creature properties: ${creatureId}`);
return props; return props;
@@ -90,7 +88,6 @@ export function getPropertiesOfType(creatureId, propType) {
'type': propType, 'type': propType,
}, { }, {
sort: { order: 1 }, sort: { order: 1 },
fields: { icon: 0 },
}).fetch(); }).fetch();
// console.timeEnd(`Cache miss on creature properties: ${creatureId}`); // console.timeEnd(`Cache miss on creature properties: ${creatureId}`);
return props; return props;
@@ -100,14 +97,13 @@ export function getCreature(creatureId) {
if (loadedCreatures.has(creatureId)) { if (loadedCreatures.has(creatureId)) {
const loadedCreature = loadedCreatures.get(creatureId); const loadedCreature = loadedCreatures.get(creatureId);
const creature = loadedCreature.creature; const creature = loadedCreature.creature;
if (creature) return creature; if (creature) {
const cloneCreature = EJSON.clone(creature);
return cloneCreature;
}
} }
// console.time(`Cache miss on Creature: ${creatureId}`); // console.time(`Cache miss on Creature: ${creatureId}`);
const creature = Creatures.findOne(creatureId, { const creature = Creatures.findOne(creatureId);
denormalizedStats: 1,
variables: 1,
dirty: 1,
});
// console.timeEnd(`Cache miss on Creature: ${creatureId}`); // console.timeEnd(`Cache miss on Creature: ${creatureId}`);
return creature; return creature;
} }
@@ -116,7 +112,10 @@ export function getVariables(creatureId) {
if (loadedCreatures.has(creatureId)) { if (loadedCreatures.has(creatureId)) {
const loadedCreature = loadedCreatures.get(creatureId); const loadedCreature = loadedCreatures.get(creatureId);
const variables = loadedCreature.variables; const variables = loadedCreature.variables;
if (variables) return variables; if (variables) {
const cloneVarables = EJSON.clone(variables);
return cloneVarables;
}
} }
// console.time(`Cache miss on variables: ${creatureId}`); // console.time(`Cache miss on variables: ${creatureId}`);
const variables = CreatureVariables.findOne({_creatureId: creatureId}); const variables = CreatureVariables.findOne({_creatureId: creatureId});
@@ -149,6 +148,7 @@ export function getProperyAncestors(creatureId, propertyId) {
// Fetch from database // Fetch from database
return CreatureProperties.find({ return CreatureProperties.find({
_id: { $in: ancestorIds }, _id: { $in: ancestorIds },
removed: {$ne: true},
}, { }, {
sort: { order: 1 }, sort: { order: 1 },
}).fetch(); }).fetch();
@@ -175,6 +175,8 @@ export function getPropertyDecendants(creatureId, propertyId) {
return CreatureProperties.find({ return CreatureProperties.find({
'ancestors.id': propertyId, 'ancestors.id': propertyId,
removed: { $ne: true }, removed: { $ne: true },
}, {
sort: { order: 1 },
}).fetch(); }).fetch();
} }
} }
@@ -199,7 +201,6 @@ class LoadedCreature {
removed: { $ne: true }, removed: { $ne: true },
}, { }, {
sort: { order: 1 }, sort: { order: 1 },
fields: { icon: 0 },
}).observeChanges({ }).observeChanges({
added(id, fields) { added(id, fields) {
fields._id = id; fields._id = id;

View File

@@ -1,8 +1,13 @@
import { createS3FilesCollection } from '/imports/api/files/s3FileStorage.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 UserImages = createS3FilesCollection({ const UserImages = createS3FilesCollection({
collectionName: 'userImages', collectionName: 'userImages',
storagePath: Meteor.isDevelopment ? '/DiceCloud/userImages/' : 'assets/app/userImages', storagePath: Meteor.isDevelopment ? '../../../../../fileStorage/userImages' : 'assets/app/userImages',
onBeforeUpload(file) { onBeforeUpload(file) {
// Allow upload files under 10MB // Allow upload files under 10MB
if (file.size > 10485760) { if (file.size > 10485760) {

View File

@@ -0,0 +1,24 @@
// https://github.com/VeliovGroup/Meteor-Files/blob/master/docs/aws-s3-integration.md
import { FilesCollection } from 'meteor/ostrio:files';
const createS3FilesCollection = function ({
collectionName,
storagePath,
onBeforeUpload,
onAfterUpload,
debug,// = !Meteor.isProduction,
allowClientCode = false,
}) {
const collection = new FilesCollection({
collectionName,
storagePath,
onBeforeUpload,
onAfterUpload,
debug,
allowClientCode,
});
return collection;
}
export { createS3FilesCollection };

View File

@@ -29,7 +29,7 @@ let createS3FilesCollection;
/* Check settings existence in `Meteor.settings` */ /* Check settings existence in `Meteor.settings` */
/* This is the best practice for app security */ /* This is the best practice for app security */
if (Meteor.isServer && Meteor.settings.useS3) { if (Meteor.settings.useS3) {
// Create a new S3 object // Create a new S3 object
const s3 = new S3({ const s3 = new S3({
accessKeyId: s3Conf.key, accessKeyId: s3Conf.key,
@@ -48,7 +48,7 @@ if (Meteor.isServer && Meteor.settings.useS3) {
storagePath, storagePath,
onBeforeUpload, onBeforeUpload,
onAfterUpload, onAfterUpload,
debug = !Meteor.isProduction, debug,// = !Meteor.isProduction,
allowClientCode = false, allowClientCode = false,
}) { }) {
const collection = new FilesCollection({ const collection = new FilesCollection({
@@ -222,7 +222,7 @@ if (Meteor.isServer && Meteor.settings.useS3) {
storagePath, storagePath,
onBeforeUpload, onBeforeUpload,
onAfterUpload, onAfterUpload,
debug = !Meteor.isProduction, debug,// = !Meteor.isProduction,
allowClientCode = false, allowClientCode = false,
}) { }) {
const collection = new FilesCollection({ const collection = new FilesCollection({
@@ -234,13 +234,11 @@ if (Meteor.isServer && Meteor.settings.useS3) {
allowClientCode, allowClientCode,
}); });
if (Meteor.isServer) {
// Use the normal file system to read files // Use the normal file system to read files
collection.readJSONFile = async function (file) { collection.readJSONFile = async function (file) {
const fileString = await fsp.readFile(file.path, 'utf8'); const fileString = await fsp.readFile(file.path, 'utf8');
return JSON.parse(fileString); return JSON.parse(fileString);
}; };
}
return collection; return collection;
} }

View File

@@ -0,0 +1,97 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import SimpleSchema from 'simpl-schema';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import { RefSchema } from '/imports/api/parenting/ChildSchema.js';
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
import {
assertDocCopyPermission,
assertDocEditPermission
} from '/imports/api/sharing/sharingPermissions.js';
import {
setLineageOfDocs,
renewDocIds
} from '/imports/api/parenting/parenting.js';
import { reorderDocs } from '/imports/api/parenting/order.js';
import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
var snackbar;
if (Meteor.isClient) {
snackbar = require(
'/imports/client/ui/components/snackbars/SnackbarQueue.js'
).snackbar
}
const DUPLICATE_CHILDREN_LIMIT = 500;
const copyLibraryNodeTo = new ValidatedMethod({
name: 'libraryNodes.copyTo',
validate: new SimpleSchema({
_id: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
parent: {
type: RefSchema,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 1,
timeInterval: 10000,
},
run({ _id, parent }) {
if (parent.collection !== 'libraryNodes' && parent.collection !== 'libraries') {
throw new Meteor.Error('Invalid destination',
'Library documents can only be copied to destinations inside other libraries'
);
}
const libraryNode = LibraryNodes.findOne(_id);
const parentDoc = fetchDocByRef(parent);
assertDocCopyPermission(libraryNode, this.userId);
assertDocEditPermission(parentDoc, this.userId);
let decendants = LibraryNodes.find({
'ancestors.id': _id,
removed: { $ne: true },
}, {
limit: DUPLICATE_CHILDREN_LIMIT + 1,
sort: { order: 1 },
}).fetch();
if (decendants.length > DUPLICATE_CHILDREN_LIMIT) {
decendants.pop();
if (Meteor.isClient) {
snackbar({
text: `Only the first ${DUPLICATE_CHILDREN_LIMIT} children were duplicated`,
});
}
}
const nodes = [libraryNode, ...decendants];
const newAncestry = parentDoc.ancestors || [];
newAncestry.push(parent);
// re-map all the ancestors
setLineageOfDocs({
docArray: nodes,
newAncestry,
oldParent: libraryNode.parent,
});
// Give the docs new IDs without breaking internal references
renewDocIds({ docArray: nodes });
// Order the root node
libraryNode.order = (parentDoc.order || 0) + 0.5;
LibraryNodes.batchInsert(nodes);
// Tree structure changed by inserts, reorder the tree
reorderDocs({
collection: LibraryNodes,
ancestorId: parent.collection === 'libraries' ? parent.id : parentDoc.ancestors[0].id,
});
},
});
export default copyLibraryNodeTo;

View File

@@ -12,11 +12,11 @@ import { reorderDocs } from '/imports/api/parenting/order.js';
var snackbar; var snackbar;
if (Meteor.isClient) { if (Meteor.isClient) {
snackbar = require( snackbar = require(
'/imports/ui/components/snackbars/SnackbarQueue.js' '/imports/client/ui/components/snackbars/SnackbarQueue.js'
).snackbar ).snackbar
} }
const DUPLICATE_CHILDREN_LIMIT = 50; const DUPLICATE_CHILDREN_LIMIT = 500;
const duplicateLibraryNode = new ValidatedMethod({ const duplicateLibraryNode = new ValidatedMethod({
name: 'libraryNodes.duplicate', name: 'libraryNodes.duplicate',
@@ -28,7 +28,7 @@ const duplicateLibraryNode = new ValidatedMethod({
}).validator(), }).validator(),
mixins: [RateLimiterMixin], mixins: [RateLimiterMixin],
rateLimit: { rateLimit: {
numRequests: 5, numRequests: 1,
timeInterval: 5000, timeInterval: 5000,
}, },
run({ _id }) { run({ _id }) {

View File

@@ -1,2 +1,3 @@
import '/imports/api/library/methods/copyLibraryNodeTo.js';
import '/imports/api/library/methods/duplicateLibraryNode.js'; import '/imports/api/library/methods/duplicateLibraryNode.js';
import '/imports/api/library/methods/updateReferenceNode.js'; import '/imports/api/library/methods/updateReferenceNode.js';

View File

@@ -1,17 +1,17 @@
import SimpleSchema from 'simpl-schema'; import SimpleSchema from 'simpl-schema';
let SoftRemovableSchema = new SimpleSchema({ let SoftRemovableSchema = new SimpleSchema({
"removed": { 'removed': {
type: Boolean, type: Boolean,
optional: true, optional: true,
index: 1, index: 1,
}, },
"removedAt": { 'removedAt': {
type: Date, type: Date,
optional: true, optional: true,
index: 1, index: 1,
}, },
"removedWith": { 'removedWith': {
optional: true, optional: true,
type: String, type: String,
regEx: SimpleSchema.RegEx.Id, regEx: SimpleSchema.RegEx.Id,

View File

@@ -2,6 +2,7 @@ import SimpleSchema from 'simpl-schema';
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
import { storedIconsSchema } from '/imports/api/icons/Icons.js'; import { storedIconsSchema } from '/imports/api/icons/Icons.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js';
/* /*
* Actions are things a character can do * Actions are things a character can do
@@ -24,9 +25,17 @@ let ActionSchema = createPropertySchema({
// long actions take longer than 1 round to cast // long actions take longer than 1 round to cast
actionType: { actionType: {
type: String, type: String,
allowedValues: ['action', 'bonus', 'attack', 'reaction', 'free', 'long'], allowedValues: ['action', 'bonus', 'attack', 'reaction', 'free', 'long', 'event'],
defaultValue: 'action', defaultValue: 'action',
}, },
// If the action type is an event, what is the variable name of that event?
variableName: {
type: String,
optional: true,
regEx: VARIABLE_NAME_REGEX,
min: 2,
max: STORAGE_LIMITS.variableName,
},
// Who is the action directed at // Who is the action directed at
target: { target: {
type: String, type: String,
@@ -56,8 +65,10 @@ let ActionSchema = createPropertySchema({
// How this action's uses are reset automatically // How this action's uses are reset automatically
reset: { reset: {
type: String, type: String,
allowedValues: ['longRest', 'shortRest'],
optional: true, optional: true,
regEx: VARIABLE_NAME_REGEX,
min: 2,
max: STORAGE_LIMITS.variableName,
}, },
// Resources // Resources
resources: { resources: {
@@ -114,6 +125,11 @@ let ActionSchema = createPropertySchema({
type: 'fieldToCompute', type: 'fieldToCompute',
optional: true, optional: true,
}, },
// Prevent the property from showing up in the log
silent: {
type: Boolean,
optional: true,
},
}); });
const ComputedOnlyActionSchema = createPropertySchema({ const ComputedOnlyActionSchema = createPropertySchema({
@@ -146,6 +162,12 @@ const ComputedOnlyActionSchema = createPropertySchema({
optional: true, optional: true,
removeBeforeCompute: true, removeBeforeCompute: true,
}, },
// Denormalised tag if event is overridden by one with the same variable name
overridden: {
type: Boolean,
optional: true,
removeBeforeCompute: true,
},
// Resources // Resources
resources: { resources: {
type: Object, type: Object,

View File

@@ -31,6 +31,11 @@ const AdjustmentSchema = createPropertySchema({
allowedValues: ['set', 'increment'], allowedValues: ['set', 'increment'],
defaultValue: 'increment', defaultValue: 'increment',
}, },
// Prevent the property from showing up in the log
silent: {
type: Boolean,
optional: true,
},
}); });
const ComputedOnlyAdjustmentSchema = createPropertySchema({ const ComputedOnlyAdjustmentSchema = createPropertySchema({

View File

@@ -28,8 +28,7 @@ let AttributeSchema = createPropertySchema({
'stat', // Speed, Armor Class 'stat', // Speed, Armor Class
'modifier', // Proficiency Bonus, displayed as +x 'modifier', // Proficiency Bonus, displayed as +x
'hitDice', // d12 hit dice 'hitDice', // d12 hit dice
'healthBar', // Hitpoints, Temporary Hitpoints, can take damage 'healthBar', // Hitpoints, Temporary Hitpoints
'bar', // Displayed as a health bar, can't take damage
'resource', // Rages, sorcery points 'resource', // Rages, sorcery points
'spellSlot', // Level 1, 2, 3... spell slots 'spellSlot', // Level 1, 2, 3... spell slots
'utility', // Aren't displayed, Jump height, Carry capacity 'utility', // Aren't displayed, Jump height, Carry capacity
@@ -69,6 +68,16 @@ let AttributeSchema = createPropertySchema({
type: Boolean, type: Boolean,
optional: true, optional: true,
}, },
// Control how the health bar handles overflow
healthBarNoDamageOverflow: {
type: Boolean,
optional: true,
},
healthBarNoHealingOverflow: {
type: Boolean,
optional: true,
},
// Control when the health bar takes damage or healing
healthBarDamageOrder: { healthBarDamageOrder: {
type: SimpleSchema.Integer, type: SimpleSchema.Integer,
optional: true, optional: true,
@@ -107,11 +116,21 @@ let AttributeSchema = createPropertySchema({
type: Boolean, type: Boolean,
optional: true, optional: true,
}, },
hideWhenTotalZero: {
type: Boolean,
optional: true,
},
hideWhenValueZero: {
type: Boolean,
optional: true,
},
// Automatically zero the adjustment on these conditions // Automatically zero the adjustment on these conditions
reset: { reset: {
type: String, type: String,
optional: true, optional: true,
allowedValues: ['shortRest', 'longRest'], regEx: VARIABLE_NAME_REGEX,
min: 2,
max: STORAGE_LIMITS.variableName,
}, },
}); });
@@ -176,6 +195,7 @@ let ComputedOnlyAttributeSchema = createPropertySchema({
effects: { effects: {
type: Array, type: Array,
optional: true, optional: true,
removeBeforeCompute: true,
}, },
'effects.$': { 'effects.$': {
type: Object, type: Object,

View File

@@ -37,6 +37,11 @@ let BranchSchema = createPropertySchema({
optional: true, optional: true,
parseLevel: 'compile', parseLevel: 'compile',
}, },
// Prevent the property from showing up in the log
silent: {
type: Boolean,
optional: true,
},
}); });
let ComputedOnlyBranchSchema = createPropertySchema({ let ComputedOnlyBranchSchema = createPropertySchema({

View File

@@ -0,0 +1,84 @@
import SimpleSchema from 'simpl-schema';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
let BuffRemoverSchema = createPropertySchema({
name: {
type: String,
optional: true,
max: STORAGE_LIMITS.name,
},
// This will remove just the nearest ancestor buff
targetParentBuff: {
type: Boolean,
optional: true,
},
// The following only applies when not targeting the parent buff
// Which character to remove buffs from
target: {
type: String,
allowedValues: [
'self',
'target',
],
defaultValue: 'target',
},
// remove 1 or remove all
removeAll: {
type: Boolean,
optional: true,
defaultValue: true,
},
// Buffs to remove based on tags:
targetTags: {
type: Array,
optional: true,
maxCount: STORAGE_LIMITS.tagCount,
},
'targetTags.$': {
type: String,
max: STORAGE_LIMITS.tagLength,
},
extraTags: {
type: Array,
optional: true,
maxCount: STORAGE_LIMITS.extraTagsCount,
},
'extraTags.$': {
type: Object,
},
'extraTags.$._id': {
type: String,
regEx: SimpleSchema.RegEx.Id,
autoValue() {
if (!this.isSet) return Random.id();
}
},
'extraTags.$.operation': {
type: String,
allowedValues: ['OR', 'NOT'],
defaultValue: 'OR',
},
'extraTags.$.tags': {
type: Array,
defaultValue: [],
maxCount: STORAGE_LIMITS.tagCount,
},
'extraTags.$.tags.$': {
type: String,
max: STORAGE_LIMITS.tagLength,
},
// Prevent the property from showing up in the log
silent: {
type: Boolean,
optional: true,
},
});
let ComputedOnlyBuffRemoverSchema = createPropertySchema({});
const ComputedBuffRemoverSchema = new SimpleSchema()
.extend(BuffRemoverSchema)
.extend(ComputedOnlyBuffRemoverSchema);
export { BuffRemoverSchema, ComputedOnlyBuffRemoverSchema, ComputedBuffRemoverSchema };

View File

@@ -12,6 +12,10 @@ let BuffSchema = createPropertySchema({
type: 'inlineCalculationFieldToCompute', type: 'inlineCalculationFieldToCompute',
optional: true, optional: true,
}, },
hideRemoveButton: {
type: Boolean,
optional: true,
},
// How many rounds this buff lasts // How many rounds this buff lasts
duration: { duration: {
type: 'fieldToCompute', type: 'fieldToCompute',
@@ -25,6 +29,16 @@ let BuffSchema = createPropertySchema({
], ],
defaultValue: 'target', defaultValue: 'target',
}, },
// Prevent the property from showing up in the log
silent: {
type: Boolean,
optional: true,
},
// Prevent the children from being crystalized
skipCrystalization: {
type: Boolean,
optional: true,
},
}); });
let ComputedOnlyBuffSchema = createPropertySchema({ let ComputedOnlyBuffSchema = createPropertySchema({

View File

@@ -27,6 +27,11 @@ const DamageSchema = createPropertySchema({
defaultValue: 'slashing', defaultValue: 'slashing',
regEx: VARIABLE_NAME_REGEX, regEx: VARIABLE_NAME_REGEX,
}, },
// Prevent the property from showing up in the log
silent: {
type: Boolean,
optional: true,
},
}); });
const ComputedOnlyDamageSchema = createPropertySchema({ const ComputedOnlyDamageSchema = createPropertySchema({

View File

@@ -1,15 +1,38 @@
import SimpleSchema from 'simpl-schema';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
// Folders organize a character sheet into a tree, particularly to group things // Folders organize a character sheet into a tree, particularly to group things
// like 'race' and 'background' // like 'race' and 'background'
let FolderSchema = new SimpleSchema({ let FolderSchema = new createPropertySchema({
name: { name: {
type: String, type: String,
max: STORAGE_LIMITS.name, max: STORAGE_LIMITS.name,
optional: true,
},
groupStats: {
type: Boolean,
optional: true,
},
hideStatsGroup: {
type: Boolean,
optional: true,
},
tab: {
type: String,
optional: true,
allowedValues: [
'stats', 'features', 'actions', 'spells', 'inventory', 'journal', 'build'
],
},
location: {
type: String,
optional: true,
allowedValues: [
'start', 'events', 'stats', 'skills', 'proficiencies', 'end'
],
}, },
}); });
const ComputedOnlyFolderSchema = new SimpleSchema({}); const ComputedOnlyFolderSchema = new createPropertySchema({});
export { FolderSchema, ComputedOnlyFolderSchema }; export { FolderSchema, ComputedOnlyFolderSchema };

View File

@@ -2,6 +2,7 @@ import SimpleSchema from 'simpl-schema';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js'; import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js';
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js';
/* /*
* PointBuys are reason-value attached to skills and abilities * PointBuys are reason-value attached to skills and abilities
@@ -13,13 +14,6 @@ let PointBuySchema = createPropertySchema({
optional: true, optional: true,
max: STORAGE_LIMITS.name, max: STORAGE_LIMITS.name,
}, },
variableName: {
type: String,
optional: true,
regEx: VARIABLE_NAME_REGEX,
min: 2,
max: STORAGE_LIMITS.variableName,
},
ignored: { ignored: {
type: Boolean, type: Boolean,
optional: true, optional: true,
@@ -27,10 +21,18 @@ let PointBuySchema = createPropertySchema({
'values': { 'values': {
type: Array, type: Array,
defaultValue: [], defaultValue: [],
maxCount: STORAGE_LIMITS.pointBuyRowsCount,
}, },
'values.$': { 'values.$': {
type: Object, type: Object,
}, },
'values.$._id': {
type: String,
regEx: SimpleSchema.RegEx.Id,
autoValue(){
if (!this.isSet) return Random.id();
}
},
'values.$.name': { 'values.$.name': {
type: String, type: String,
optional: true, optional: true,
@@ -47,6 +49,18 @@ let PointBuySchema = createPropertySchema({
type: Number, type: Number,
optional: true, optional: true,
}, },
'values.$.min': {
type: 'fieldToCompute',
optional: true,
},
'values.$.max': {
type: 'fieldToCompute',
optional: true,
},
'values.$.cost': {
type: 'fieldToCompute',
optional: true,
},
min: { min: {
type: 'fieldToCompute', type: 'fieldToCompute',
optional: true, optional: true,
@@ -62,6 +76,7 @@ let PointBuySchema = createPropertySchema({
cost: { cost: {
type: 'fieldToCompute', type: 'fieldToCompute',
optional: true, optional: true,
parseLevel: 'compile',
}, },
}); });
@@ -74,11 +89,46 @@ const ComputedOnlyPointBuySchema = createPropertySchema({
type: 'computedOnlyField', type: 'computedOnlyField',
optional: true, optional: true,
}, },
total: { cost: {
type: 'computedOnlyField',
optional: true,
parseLevel: 'compile',
},
'values': {
type: Array,
defaultValue: [],
maxCount: STORAGE_LIMITS.pointBuyRowsCount,
},
'values.$': {
type: Object,
},
'values.$.min': {
type: 'computedOnlyField', type: 'computedOnlyField',
optional: true, optional: true,
}, },
cost: { 'values.$.max': {
type: 'computedOnlyField',
optional: true,
},
'values.$.cost': {
type: 'computedOnlyField',
optional: true,
parseLevel: 'compile',
},
'values.$.spent': {
type: Number,
optional: true,
removeBeforeCompute: true,
},
'values.$.errors': {
type: Array,
optional: true,
removeBeforeCompute: true,
},
'values.$.errors.$': {
type: ErrorSchema,
},
total: {
type: 'computedOnlyField', type: 'computedOnlyField',
optional: true, optional: true,
}, },
@@ -87,6 +137,19 @@ const ComputedOnlyPointBuySchema = createPropertySchema({
optional: true, optional: true,
removeBeforeCompute: true, removeBeforeCompute: true,
}, },
pointsLeft: {
type: Number,
optional: true,
removeBeforeCompute: true,
},
errors: {
type: Array,
optional: true,
removeBeforeCompute: true,
},
'errors.$': {
type: ErrorSchema,
},
}); });
const ComputedPointBuySchema = new SimpleSchema() const ComputedPointBuySchema = new SimpleSchema()

View File

@@ -30,6 +30,11 @@ let SavingThrowSchema = createPropertySchema({
optional: true, optional: true,
max: STORAGE_LIMITS.variableName, max: STORAGE_LIMITS.variableName,
}, },
// Prevent the property from showing up in the log
silent: {
type: Boolean,
optional: true,
},
}); });
const ComputedOnlySavingThrowSchema = createPropertySchema({ const ComputedOnlySavingThrowSchema = createPropertySchema({

View File

@@ -135,6 +135,7 @@ let ComputedOnlySkillSchema = createPropertySchema({
effects: { effects: {
type: Array, type: Array,
optional: true, optional: true,
removeBeforeCompute: true,
}, },
'effects.$': { 'effects.$': {
type: Object, type: Object,

View File

@@ -17,6 +17,12 @@ let SpellListSchema = createPropertySchema({
type: 'fieldToCompute', type: 'fieldToCompute',
optional: true, optional: true,
}, },
// The variable name of the ability this spell relies on
ability: {
type: String,
optional: true,
max: STORAGE_LIMITS.variableName,
},
// Calculation of The attack roll bonus used by spell attacks in this list // Calculation of The attack roll bonus used by spell attacks in this list
attackRollBonus: { attackRollBonus: {
type: 'fieldToCompute', type: 'fieldToCompute',
@@ -38,6 +44,12 @@ const ComputedOnlySpellListSchema = createPropertySchema({
type: 'computedOnlyField', type: 'computedOnlyField',
optional: true, optional: true,
}, },
// Computed value determined by the ability
abilityMod: {
type: SimpleSchema.Integer,
optional: true,
removeBeforeCompute: true,
},
attackRollBonus: { attackRollBonus: {
type: 'computedOnlyField', type: 'computedOnlyField',
optional: true, optional: true,

View File

@@ -25,6 +25,7 @@ const actionPropertyTypeOptions = {
adjustment: 'Attribute damage', adjustment: 'Attribute damage',
branch: 'Branch', branch: 'Branch',
buff: 'Buff', buff: 'Buff',
buffRemover: 'Buff Removed',
damage: 'Damage', damage: 'Damage',
note: 'Note', note: 'Note',
roll: 'Roll', roll: 'Roll',
@@ -108,6 +109,11 @@ let TriggerSchema = createPropertySchema({
type: String, type: String,
max: STORAGE_LIMITS.tagLength, max: STORAGE_LIMITS.tagLength,
}, },
// Prevent the property from showing up in the log
silent: {
type: Boolean,
optional: true,
},
}); });
const ComputedOnlyTriggerSchema = createPropertySchema({ const ComputedOnlyTriggerSchema = createPropertySchema({

View File

@@ -3,6 +3,7 @@ import { ComputedOnlyActionSchema } from '/imports/api/properties/Actions.js';
import { ComputedOnlyAdjustmentSchema } from '/imports/api/properties/Adjustments.js'; import { ComputedOnlyAdjustmentSchema } from '/imports/api/properties/Adjustments.js';
import { ComputedOnlyAttributeSchema } from '/imports/api/properties/Attributes.js'; import { ComputedOnlyAttributeSchema } from '/imports/api/properties/Attributes.js';
import { ComputedOnlyBuffSchema } from '/imports/api/properties/Buffs.js'; import { ComputedOnlyBuffSchema } from '/imports/api/properties/Buffs.js';
import { ComputedOnlyBuffRemoverSchema } from '/imports/api/properties/BuffRemovers.js';
import { ComputedOnlyBranchSchema } from '/imports/api/properties/Branches.js'; import { ComputedOnlyBranchSchema } from '/imports/api/properties/Branches.js';
import { ComputedOnlyClassSchema } from '/imports/api/properties/Classes.js'; import { ComputedOnlyClassSchema } from '/imports/api/properties/Classes.js';
import { ComputedOnlyClassLevelSchema } from '/imports/api/properties/ClassLevels.js'; import { ComputedOnlyClassLevelSchema } from '/imports/api/properties/ClassLevels.js';
@@ -15,6 +16,7 @@ import { ComputedOnlyFeatureSchema } from '/imports/api/properties/Features.js';
import { ComputedOnlyFolderSchema } from '/imports/api/properties/Folders.js'; import { ComputedOnlyFolderSchema } from '/imports/api/properties/Folders.js';
import { ComputedOnlyItemSchema } from '/imports/api/properties/Items.js'; import { ComputedOnlyItemSchema } from '/imports/api/properties/Items.js';
import { ComputedOnlyNoteSchema } from '/imports/api/properties/Notes.js'; import { ComputedOnlyNoteSchema } from '/imports/api/properties/Notes.js';
import { ComputedOnlyPointBuySchema } from '/imports/api/properties/PointBuys.js';
import { ComputedOnlyProficiencySchema } from '/imports/api/properties/Proficiencies.js'; import { ComputedOnlyProficiencySchema } from '/imports/api/properties/Proficiencies.js';
import { ComputedOnlyReferenceSchema } from '/imports/api/properties/References.js'; import { ComputedOnlyReferenceSchema } from '/imports/api/properties/References.js';
import { ComputedOnlyRollSchema } from '/imports/api/properties/Rolls.js'; import { ComputedOnlyRollSchema } from '/imports/api/properties/Rolls.js';
@@ -32,6 +34,7 @@ const propertySchemasIndex = {
adjustment: ComputedOnlyAdjustmentSchema, adjustment: ComputedOnlyAdjustmentSchema,
attribute: ComputedOnlyAttributeSchema, attribute: ComputedOnlyAttributeSchema,
buff: ComputedOnlyBuffSchema, buff: ComputedOnlyBuffSchema,
buffRemover: ComputedOnlyBuffRemoverSchema,
branch: ComputedOnlyBranchSchema, branch: ComputedOnlyBranchSchema,
class: ComputedOnlyClassSchema, class: ComputedOnlyClassSchema,
classLevel: ComputedOnlyClassLevelSchema, classLevel: ComputedOnlyClassLevelSchema,
@@ -44,6 +47,7 @@ const propertySchemasIndex = {
folder: ComputedOnlyFolderSchema, folder: ComputedOnlyFolderSchema,
item: ComputedOnlyItemSchema, item: ComputedOnlyItemSchema,
note: ComputedOnlyNoteSchema, note: ComputedOnlyNoteSchema,
pointBuy: ComputedOnlyPointBuySchema,
proficiency: ComputedOnlyProficiencySchema, proficiency: ComputedOnlyProficiencySchema,
propertySlot: ComputedOnlySlotSchema, propertySlot: ComputedOnlySlotSchema,
reference: ComputedOnlyReferenceSchema, reference: ComputedOnlyReferenceSchema,

View File

@@ -3,6 +3,7 @@ import { ComputedActionSchema } from '/imports/api/properties/Actions.js';
import { ComputedAdjustmentSchema } from '/imports/api/properties/Adjustments.js'; import { ComputedAdjustmentSchema } from '/imports/api/properties/Adjustments.js';
import { ComputedAttributeSchema } from '/imports/api/properties/Attributes.js'; import { ComputedAttributeSchema } from '/imports/api/properties/Attributes.js';
import { ComputedBuffSchema } from '/imports/api/properties/Buffs.js'; import { ComputedBuffSchema } from '/imports/api/properties/Buffs.js';
import { ComputedBuffRemoverSchema } from '/imports/api/properties/BuffRemovers.js';
import { ComputedBranchSchema } from '/imports/api/properties/Branches.js'; import { ComputedBranchSchema } from '/imports/api/properties/Branches.js';
import { ComputedClassSchema } from '/imports/api/properties/Classes.js'; import { ComputedClassSchema } from '/imports/api/properties/Classes.js';
import { ComputedClassLevelSchema } from '/imports/api/properties/ClassLevels.js'; import { ComputedClassLevelSchema } from '/imports/api/properties/ClassLevels.js';
@@ -15,6 +16,7 @@ import { ComputedFeatureSchema } from '/imports/api/properties/Features.js';
import { FolderSchema } from '/imports/api/properties/Folders.js'; import { FolderSchema } from '/imports/api/properties/Folders.js';
import { ComputedItemSchema } from '/imports/api/properties/Items.js'; import { ComputedItemSchema } from '/imports/api/properties/Items.js';
import { ComputedNoteSchema } from '/imports/api/properties/Notes.js'; import { ComputedNoteSchema } from '/imports/api/properties/Notes.js';
import { ComputedPointBuySchema } from '/imports/api/properties/PointBuys.js';
import { ProficiencySchema } from '/imports/api/properties/Proficiencies.js'; import { ProficiencySchema } from '/imports/api/properties/Proficiencies.js';
import { ReferenceSchema } from '/imports/api/properties/References.js'; import { ReferenceSchema } from '/imports/api/properties/References.js';
import { ComputedRollSchema } from '/imports/api/properties/Rolls.js'; import { ComputedRollSchema } from '/imports/api/properties/Rolls.js';
@@ -32,6 +34,7 @@ const propertySchemasIndex = {
adjustment: ComputedAdjustmentSchema, adjustment: ComputedAdjustmentSchema,
attribute: ComputedAttributeSchema, attribute: ComputedAttributeSchema,
buff: ComputedBuffSchema, buff: ComputedBuffSchema,
buffRemover: ComputedBuffRemoverSchema,
branch: ComputedBranchSchema, branch: ComputedBranchSchema,
class: ComputedClassSchema, class: ComputedClassSchema,
classLevel: ComputedClassLevelSchema, classLevel: ComputedClassLevelSchema,
@@ -42,6 +45,7 @@ const propertySchemasIndex = {
feature: ComputedFeatureSchema, feature: ComputedFeatureSchema,
folder: FolderSchema, folder: FolderSchema,
note: ComputedNoteSchema, note: ComputedNoteSchema,
pointBuy: ComputedPointBuySchema,
proficiency: ProficiencySchema, proficiency: ProficiencySchema,
propertySlot: ComputedSlotSchema, propertySlot: ComputedSlotSchema,
reference: ReferenceSchema, reference: ReferenceSchema,

View File

@@ -3,6 +3,7 @@ import { ActionSchema } from '/imports/api/properties/Actions.js';
import { AdjustmentSchema } from '/imports/api/properties/Adjustments.js'; import { AdjustmentSchema } from '/imports/api/properties/Adjustments.js';
import { AttributeSchema } from '/imports/api/properties/Attributes.js'; import { AttributeSchema } from '/imports/api/properties/Attributes.js';
import { BuffSchema } from '/imports/api/properties/Buffs.js'; import { BuffSchema } from '/imports/api/properties/Buffs.js';
import { BuffRemoverSchema } from '/imports/api/properties/BuffRemovers.js';
import { BranchSchema } from '/imports/api/properties/Branches.js'; import { BranchSchema } from '/imports/api/properties/Branches.js';
import { ClassSchema } from '/imports/api/properties/Classes.js'; import { ClassSchema } from '/imports/api/properties/Classes.js';
import { ClassLevelSchema } from '/imports/api/properties/ClassLevels.js'; import { ClassLevelSchema } from '/imports/api/properties/ClassLevels.js';
@@ -13,6 +14,7 @@ import { EffectSchema } from '/imports/api/properties/Effects.js';
import { FeatureSchema } from '/imports/api/properties/Features.js'; import { FeatureSchema } from '/imports/api/properties/Features.js';
import { FolderSchema } from '/imports/api/properties/Folders.js'; import { FolderSchema } from '/imports/api/properties/Folders.js';
import { NoteSchema } from '/imports/api/properties/Notes.js'; import { NoteSchema } from '/imports/api/properties/Notes.js';
import { PointBuySchema } from '/imports/api/properties/PointBuys.js';
import { ProficiencySchema } from '/imports/api/properties/Proficiencies.js'; import { ProficiencySchema } from '/imports/api/properties/Proficiencies.js';
import { ReferenceSchema } from '/imports/api/properties/References.js'; import { ReferenceSchema } from '/imports/api/properties/References.js';
import { RollSchema } from '/imports/api/properties/Rolls.js'; import { RollSchema } from '/imports/api/properties/Rolls.js';
@@ -32,6 +34,7 @@ const propertySchemasIndex = {
adjustment: AdjustmentSchema, adjustment: AdjustmentSchema,
attribute: AttributeSchema, attribute: AttributeSchema,
buff: BuffSchema, buff: BuffSchema,
buffRemover: BuffRemoverSchema,
branch: BranchSchema, branch: BranchSchema,
class: ClassSchema, class: ClassSchema,
classLevel: ClassLevelSchema, classLevel: ClassLevelSchema,
@@ -42,6 +45,7 @@ const propertySchemasIndex = {
feature: FeatureSchema, feature: FeatureSchema,
folder: FolderSchema, folder: FolderSchema,
note: NoteSchema, note: NoteSchema,
pointBuy: PointBuySchema,
proficiency: ProficiencySchema, proficiency: ProficiencySchema,
propertySlot: SlotSchema, propertySlot: SlotSchema,
reference: ReferenceSchema, reference: ReferenceSchema,

View File

@@ -33,6 +33,10 @@ let SharingSchema = new SimpleSchema({
defaultValue: false, defaultValue: false,
index: 1, index: 1,
}, },
readersCanCopy: {
type: Boolean,
optional: true,
},
}); });
export default SharingSchema; export default SharingSchema;

View File

@@ -27,6 +27,26 @@ const setPublic = new ValidatedMethod({
}, },
}); });
const setReadersCanCopy = new ValidatedMethod({
name: 'sharing.setReadersCanCopy',
validate: new SimpleSchema({
docRef: RefSchema,
readersCanCopy: { type: Boolean },
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({ docRef, readersCanCopy }) {
let doc = fetchDocByRef(docRef);
assertOwnership(doc, this.userId);
return getCollectionByName(docRef.collection).update(docRef.id, {
$set: { readersCanCopy },
});
},
});
const updateUserSharePermissions = new ValidatedMethod({ const updateUserSharePermissions = new ValidatedMethod({
name: 'sharing.updateUserSharePermissions', name: 'sharing.updateUserSharePermissions',
validate: new SimpleSchema({ validate: new SimpleSchema({
@@ -129,4 +149,4 @@ const transferOwnership = new ValidatedMethod({
}, },
}); });
export { setPublic, updateUserSharePermissions, transferOwnership }; export { setPublic, setReadersCanCopy, updateUserSharePermissions, transferOwnership };

View File

@@ -1,4 +1,4 @@
import { _ } from 'meteor/underscore'; import { includes } from 'lodash';
import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js'; import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
function assertIdValid(userId) { function assertIdValid(userId) {
@@ -18,6 +18,7 @@ function assertdocExists(doc){
export function assertOwnership(doc, userId) { export function assertOwnership(doc, userId) {
assertIdValid(userId); assertIdValid(userId);
assertdocExists(doc); assertdocExists(doc);
if (doc.owner === userId) { if (doc.owner === userId) {
return true; return true;
} else { } else {
@@ -37,7 +38,6 @@ export function assertEditPermission(doc, userId) {
assertdocExists(doc); assertdocExists(doc);
const user = Meteor.users.findOne(userId, { const user = Meteor.users.findOne(userId, {
fields: { fields: {
'services.patreon': 1,
'roles': 1, 'roles': 1,
} }
}); });
@@ -50,7 +50,7 @@ export function assertEditPermission(doc, userId) {
// Ensure the user is authorized for this specific document // Ensure the user is authorized for this specific document
if ( if (
doc.owner === userId || doc.owner === userId ||
_.contains(doc.writers, userId) includes(doc.writers, userId)
) { ) {
return true; return true;
} else { } else {
@@ -59,6 +59,43 @@ export function assertEditPermission(doc, userId) {
} }
} }
/**
* Assert that the user can edit the root document which manages its own sharing
* permissions.
*
* Warning: the doc and userId must be set by a trusted source
*/
export function assertCopyPermission(doc, userId) {
assertIdValid(userId);
assertdocExists(doc);
const user = Meteor.users.findOne(userId, {
fields: {
'roles': 1,
}
});
// Admin override
if (user.roles && user.roles.includes('admin')) {
return true;
}
// Ensure the user is authorized for this specific document
if (
doc.owner === userId ||
includes(doc.writers, userId)
) {
return true;
} else if (
(includes(doc.readers, userId) || doc.public) &&
doc.readersCanCopy
) {
return true;
} else {
throw new Meteor.Error('Copy permission denied',
'You do not have permission to copy this document');
}
}
function getRoot(doc) { function getRoot(doc) {
assertdocExists(doc); assertdocExists(doc);
if (doc.ancestors && doc.ancestors.length && doc.ancestors[0]) { if (doc.ancestors && doc.ancestors.length && doc.ancestors[0]) {
@@ -79,6 +116,17 @@ export function assertDocEditPermission(doc, userId){
assertEditPermission(root, userId); assertEditPermission(root, userId);
} }
/**
* Assert that the user can copy a descendant document whose root ancestor
* implements sharing permissions.
*
* Warning: the doc and userId must be set by a trusted source
*/
export function assertDocCopyPermission(doc, userId) {
let root = getRoot(doc);
assertCopyPermission(root, userId);
}
export function assertViewPermission(doc, userId) { export function assertViewPermission(doc, userId) {
assertdocExists(doc); assertdocExists(doc);
if (doc.public) return true; if (doc.public) return true;
@@ -86,8 +134,8 @@ export function assertViewPermission(doc, userId) {
if ( if (
doc.owner === userId || doc.owner === userId ||
_.contains(doc.readers, userId) || includes(doc.readers, userId) ||
_.contains(doc.writers, userId) includes(doc.writers, userId)
) { ) {
return true; return true;
} else { } else {

View File

@@ -19,7 +19,7 @@
</template> </template>
<script lang="js"> <script lang="js">
import valueToCoins from '/imports/ui/utility/valueToCoins.js'; import valueToCoins from '/imports/client/ui/utility/valueToCoins.js';
export default { export default {
props:{ props:{

View File

@@ -10,6 +10,7 @@
:outlined="!!label" :outlined="!!label"
:icon="!label" :icon="!label"
:min-width="label && 108" :min-width="label && 108"
:disabled="context.editPermission === false"
v-on="on" v-on="on"
> >
{{ label }} {{ label }}
@@ -98,9 +99,9 @@
</template> </template>
<script lang="js"> <script lang="js">
import isDarkColor from '/imports/ui/utility/isDarkColor.js'; import isDarkColor from '/imports/client/ui/utility/isDarkColor.js';
import vuetifyColors from 'vuetify/es5/util/colors'; import vuetifyColors from 'vuetify/es5/util/colors';
import { kebabToCamelCase, camelToKebabCase } from '/imports/ui/utility/swapCase.js'; import { kebabToCamelCase, camelToKebabCase } from '/imports/client/ui/utility/swapCase.js';
function colorToHex(color, shade = 'base'){ function colorToHex(color, shade = 'base'){
if (!color) return; if (!color) return;
@@ -124,6 +125,9 @@
} }
export default { export default {
inject: {
context: { default: {} }
},
props: { props: {
//hex string //hex string
value: { value: {

View File

@@ -0,0 +1,46 @@
<template
lang="html"
functional
>
<div
class="column-layout"
:class="wideColumns ? 'wide-columns' : ''"
>
<slot />
</div>
</template>
<script lang="js">
export default {
props: {
wideColumns: Boolean,
},
};
</script>
<style lang="css">
.column-layout {
column-count: 12;
column-fill: balance;
column-gap: 0;
column-width: 240px;
transform: translateZ(0);
padding: 4px;
}
.column-layout.wide-columns {
column-count: 12;
column-fill: balance;
column-gap: 0;
column-width: 320px;
transform: translateZ(0);
padding: 4px;
}
.column-layout>div,
.column-layout>span>div {
page-break-inside: avoid;
break-inside: avoid;
padding: 4px;
}
</style>

View File

@@ -32,7 +32,7 @@
</template> </template>
<script lang="js"> <script lang="js">
import IncrementMenu from '/imports/ui/components/IncrementMenu.vue'; import IncrementMenu from '/imports/client/ui/components/IncrementMenu.vue';
export default { export default {
components: { components: {

View File

@@ -0,0 +1,167 @@
<template>
<v-layout
align-center
justify-center
class="increment-menu"
>
<v-spacer />
<v-btn-toggle
:value="operation === 'add' ? 0: operation === 'subtract' ? 1 : null"
class="mx-2"
@click="$refs.editInput.focus()"
>
<v-btn
:disabled="context.editPermission === false"
class="filled"
@click="toggleAdd(); $nextTick(() => $refs.editInput.focus())"
>
<v-icon>mdi-plus</v-icon>
</v-btn>
<v-btn
:disabled="context.editPermission === false"
class="filled"
@click="toggleSubtract(); $nextTick(() => $refs.editInput.focus())"
>
<v-icon>mdi-minus</v-icon>
</v-btn>
</v-btn-toggle>
<v-text-field
ref="editInput"
:solo="!flat"
:class="flat && 'ma-0 pa-0'"
hide-details
type="number"
style="max-width: 120px;"
min="0"
:value="editValue"
:prepend-inner-icon="operationIcon(operation)"
:disabled="context.editPermission === false"
@focus="$event.target.select()"
@keypress="keypress"
@input="input"
/>
<v-btn
:small="!flat"
:fab="!flat"
:text="flat"
:icon="flat"
class="mx-2 filled"
@click="commitEdit"
>
<v-icon>mdi-check</v-icon>
</v-btn>
<v-btn
:small="!flat"
:fab="!flat"
:text="flat"
:icon="flat"
class="filled"
@click="cancelEdit"
>
<v-icon>mdi-close</v-icon>
</v-btn>
<v-spacer />
</v-layout>
</template>
<script lang="js">
export default {
inject: {
context: { default: {} }
},
props: {
value: {
type: Number,
default: 0,
},
open: Boolean,
flat: Boolean,
},
data() {
return {
editValue: this.value,
operation: 'set',
hover: false,
};
},
watch: {
open: {
immediate: true,
handler(isOpen) {
if (isOpen) this.resetData();
},
},
},
methods: {
resetData() {
this.editValue = this.value;
this.operation = 'set';
// this.$nextTick didn't work, using timeout instead did
setTimeout(() => {
if (this.$refs.editInput) {
this.$refs.editInput.focus();
}
}, 100);
},
cancelEdit() {
this.$emit('close');
},
commitEdit() {
this.editing = false;
let value = +this.$refs.editInput.lazyValue;
if (this.operation === 'add') {
value = -value;
}
let type = this.operation === 'set' ? 'set' : 'increment';
this.$emit('change', { type, value });
},
operationIcon(operation) {
switch (operation) {
case 'set':
return 'mdi-forward';
case 'add':
return 'mdi-plus';
case 'subtract':
return 'mdi-minus';
}
},
toggleAdd() {
this.operation = (this.operation === 'add') ? 'set' : 'add';
},
toggleSubtract() {
this.operation = (this.operation === 'subtract') ? 'set' : 'subtract';
},
keypress(event) {
let digitsOnly = /[0-9]/;
let key = event.key;
if (key === '+') {
this.toggleAdd();
event.preventDefault();
} else if (key === '-') {
this.toggleSubtract();
event.preventDefault();
} else if (key === 'Enter') {
this.commitEdit();
} else if (!digitsOnly.test(key)) {
event.preventDefault();
}
},
input(value) {
if (+value < 0) {
this.editValue = -value;
this.operation = 'subtract';
}
}
}
};
</script>
<style scoped>
.filled.theme--light {
background: #fff !important;
}
.filled.theme--dark {
background: #424242 !important;
}
</style>

View File

@@ -0,0 +1,27 @@
<template lang="html">
<!-- eslint-disable vue/no-v-html -->
<div
class="markdown"
@click="e => $emit('click', e)"
v-html="compiledMarkdown"
/>
</template>
<script lang="js">
import { marked } from 'marked';
export default {
props: {
markdown: {
type: String,
default: undefined,
},
},
computed: {
compiledMarkdown() {
if (!this.markdown) return;
return marked(this.markdown);
},
},
}
</script>

View File

@@ -0,0 +1,46 @@
<template>
<smart-select
label="Reset"
clearable
style="flex-basis: 300px;"
:hint="hint"
:items="resetOptions"
:value="value"
:error-messages="errorMessages"
:menu-props="{auto: true, lazy: true}"
@change="(value, ack) => $emit('change', value, ack)"
/>
</template>
<script lang="js">
import createListOfProperties from '/imports/client/ui/properties/forms/shared/lists/createListOfProperties.js';
export default {
props: {
value: [String, Number, Date, Array, Object, Boolean],
errorMessages: [String, Array],
hint: {
type: String,
default: undefined,
}
},
meteor: {
resetOptions() {
const eventActions = createListOfProperties({
type: 'action',
actionType: 'event',
}, true);
const defaultEvents = [
{
text: 'Short rest',
value: 'shortRest',
}, {
text: 'Long rest',
value: 'longRest',
}
];
return [...defaultEvents, ...eventActions];
},
},
}
</script>

View File

@@ -64,7 +64,7 @@
</template> </template>
<script lang="js"> <script lang="js">
import VerticalHex from '/imports/ui/components/VerticalHex.vue'; import VerticalHex from '/imports/client/ui/components/VerticalHex.vue';
export default { export default {
components: { components: {

View File

@@ -27,9 +27,9 @@
</template> </template>
<script lang="js"> <script lang="js">
import isDarkColor from '/imports/ui/utility/isDarkColor.js'; import isDarkColor from '/imports/client/ui/utility/isDarkColor.js';
import getThemeColor from '/imports/ui/utility/getThemeColor.js'; import getThemeColor from '/imports/client/ui/utility/getThemeColor.js';
import CardHighlight from '/imports/ui/components/CardHighlight.vue'; import CardHighlight from '/imports/client/ui/components/CardHighlight.vue';
export default { export default {
components: { components: {
@@ -44,9 +44,11 @@
}, },
transparentToolbar: Boolean, transparentToolbar: Boolean,
}, },
data(){ return { data() {
return {
hovering: false, hovering: false,
}}, }
},
computed: { computed: {
isDark() { isDark() {
return isDarkColor(this.color); return isDarkColor(this.color);
@@ -72,9 +74,11 @@
.toolbar-card .v-toolbar__title { .toolbar-card .v-toolbar__title {
font-size: 15px; font-size: 15px;
} }
.toolbar-card { .toolbar-card {
transition: box-shadow .4s cubic-bezier(0.25, 0.8, 0.25, 1); transition: box-shadow .4s cubic-bezier(0.25, 0.8, 0.25, 1);
} }
.toolbar-card.transparent-toolbar .theme--dark.v-toolbar.v-sheet { .toolbar-card.transparent-toolbar .theme--dark.v-toolbar.v-sheet {
background-color: #303030; background-color: #303030;
} }

View File

@@ -30,14 +30,16 @@
</template> </template>
<script lang="js"> <script lang="js">
import SmartInput from '/imports/ui/components/global/SmartInputMixin.js'; import SmartInput from '/imports/client/ui/components/global/SmartInputMixin.js';
import { format } from 'date-fns'; import { format } from 'date-fns';
export default { export default {
mixins: [SmartInput], mixins: [SmartInput],
data(){return { data() {
return {
menu: false, menu: false,
};}, };
},
computed: { computed: {
formattedSafeValue() { formattedSafeValue() {
return format(this.safeValue, 'YYYY-MM-DD') return format(this.safeValue, 'YYYY-MM-DD')
@@ -53,4 +55,5 @@ export default {
</script> </script>
<style lang="css" scoped> <style lang="css" scoped>
</style> </style>

View File

@@ -0,0 +1,45 @@
<template>
<v-icon
class="handle"
v-bind="$attrs"
@click.native="e => { }"
@touchstart.native.stop="e => { }"
@touchend.native="portalEvent"
>
mdi-drag
</v-icon>
</template>
<script lang="js">
import { defer } from 'lodash'
export default {
methods: {
portalEvent(e) {
// Stop everything in the document listening for this touch event
e.stopPropagation();
// But also send it to straight to the root for draggable.js
defer(() => {
e.target.ownerDocument.dispatchEvent(e);
});
}
}
}
</script>
<style scoped>
.handle {
cursor: move !important;
cursor: -webkit-grab !important;
}
.handle::after {
opacity: 0 !important;
}
</style>
<style>
.sortable-drag.handle {
cursor: move !important;
cursor: -webkit-grabbing !important;
}
</style>

View File

@@ -78,8 +78,8 @@
</template> </template>
<script lang="js"> <script lang="js">
import SvgIcon from '/imports/ui/components/global/SvgIcon.vue'; import SvgIcon from '/imports/client/ui/components/global/SvgIcon.vue';
import SmartInput from '/imports/ui/components/global/SmartInputMixin.js'; import SmartInput from '/imports/client/ui/components/global/SmartInputMixin.js';
import { findIcons } from '/imports/api/icons/Icons.js'; import { findIcons } from '/imports/api/icons/Icons.js';
export default { export default {
@@ -97,11 +97,13 @@ export default {
default: undefined, default: undefined,
}, },
}, },
data(){return { data() {
return {
menu: false, menu: false,
searchString: '', searchString: '',
icons: [], icons: [],
};}, };
},
watch: { watch: {
menu(value) { menu(value) {
if (value) { if (value) {
@@ -131,4 +133,5 @@ export default {
</script> </script>
<style lang="css" scoped> <style lang="css" scoped>
</style> </style>

View File

@@ -0,0 +1,78 @@
<template lang="html">
<v-btn
v-bind="$attrs"
:disabled="isDisabled"
:loading="loading"
@click="click"
>
<slot />
</v-btn>
</template>
<script lang="js">
import { debounce } from 'lodash';
import { snackbar } from '/imports/client/ui/components/snackbars/SnackbarQueue.js';
export default {
inject: {
context: { default: {} }
},
props: {
disabled: Boolean,
debounce: {
type: Number,
default: undefined,
},
singleClick: Boolean,
},
data() {
return {
loading: false,
timesClicked: 0,
};
},
computed: {
isDisabled(){
return this.context.editPermission === false || this.disabled;
},
debounceTime() {
if (Number.isFinite(this.debounce)){
return this.debounce;
} else if (Number.isFinite(this.context.debounceTime)){
return this.context.debounceTime;
} else {
return 750;
}
},
},
created(){
this.debounceClicks = debounce(this.clicks, this.debounceTime);
},
beforeDestroy(){
this.debounceClicks.flush();
},
methods: {
click() {
if (this.singleClick) {
this.loading = true;
} else {
this.timesClicked += 1;
this.debounceClicks();
}
this.$emit('click', this.acknowledgeChange);
},
clicks() {
this.loading = true;
this.$emit('clicks', this.timesClicked, this.acknowledgeChange);
this.timesClicked = 0;
},
acknowledgeChange(error){
this.loading = false;
if (error) {
console.error(error)
snackbar({ text: error.reason || error.message || error.toString() });
}
},
},
};
</script>

View File

@@ -10,7 +10,7 @@
</template> </template>
<script lang="js"> <script lang="js">
import SmartInput from '/imports/ui/components/global/SmartInputMixin.js'; import SmartInput from '/imports/client/ui/components/global/SmartInputMixin.js';
export default { export default {
mixins: [SmartInput], mixins: [SmartInput],

View File

@@ -0,0 +1,56 @@
<template lang="html">
<v-combobox
v-bind="$attrs"
:loading="loading"
:error-messages="errors"
:value="safeValue"
:menu-props="{auto: true, lazy: true}"
:search-input.sync="searchInput"
:disabled="isDisabled"
:multiple="multiple"
outlined
@change="customChange"
@focus="focused = true"
@blur="focused = false"
>
<slot
slot="prepend"
name="prepend"
/>
</v-combobox>
</template>
<script lang="js">
import SmartInput from '/imports/client/ui/components/global/SmartInputMixin.js';
export default {
mixins: [SmartInput],
props: {
multiple: Boolean,
},
data() {
return {
searchInput: '',
}
},
computed: {
// Multiple combobox gets a long default debounce time while single
// value gets a shorter one
debounceTime() {
if (Number.isFinite(this.debounce)) {
return this.debounce;
} else if (Number.isFinite(this.context.debounceTime)) {
return this.context.debounceTime;
} else {
return this.multiple ? 1000 : 100;
}
},
},
methods: {
customChange(val) {
this.input(val);
this.searchInput = '';
},
}
};
</script>

View File

@@ -13,7 +13,8 @@ export default {
context: { default: {} } context: { default: {} }
}, },
inheritAttrs: false, inheritAttrs: false,
data(){ return { data() {
return {
error: false, error: false,
ackErrors: null, ackErrors: null,
rulesErrors: null, rulesErrors: null,
@@ -22,7 +23,8 @@ export default {
dirty: false, dirty: false,
safeValue: this.value, safeValue: this.value,
inputValue: this.value, inputValue: this.value,
};}, };
},
props: { props: {
value: [String, Number, Date, Array, Object, Boolean], value: [String, Number, Date, Array, Object, Boolean],
errorMessages: [String, Array], errorMessages: [String, Array],
@@ -115,7 +117,7 @@ export default {
}, },
change(val) { change(val) {
this.dirty = true; this.dirty = true;
if (this.hasChangeListener) this.loading = true; if (this.hasChangeListener()) this.loading = true;
this.$emit('change', val, this.acknowledgeChange); this.$emit('change', val, this.acknowledgeChange);
}, },
hasChangeListener() { hasChangeListener() {

View File

@@ -23,7 +23,7 @@
</template> </template>
<script lang="js"> <script lang="js">
import SmartInput from '/imports/ui/components/global/SmartInputMixin.js'; import SmartInput from '/imports/client/ui/components/global/SmartInputMixin.js';
export default { export default {
mixins: [SmartInput], mixins: [SmartInput],

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