Compare commits

...

132 Commits

Author SHA1 Message Date
Stefan Zermatten
4faea42371 Merge branch 'version-2-dev' into version-2 2021-04-29 15:53:24 +02:00
Stefan Zermatten
9825872576 Implemented Reference properties 2021-04-29 15:52:24 +02:00
Stefan Zermatten
85b536bc46 Added default array for stat proficiencies as well 2021-04-29 11:52:47 +02:00
Stefan Zermatten
9aa8203dcc Fixed bug where effects in stat computation could be undefined 2021-04-29 11:50:16 +02:00
Stefan Zermatten
217133137b Added note to improve query performance with root ancestor targeting 2021-04-29 11:34:58 +02:00
Stefan Zermatten
aef7dbcbb3 Fixed bug in stat computation dependency tracking 2021-04-29 11:22:13 +02:00
Stefan Zermatten
6ff750417f Fixed error in stat computation 2021-04-24 23:25:58 +02:00
Stefan Zermatten
a9eacfab03 Unprepared spells without lists now correctly show up when unprepared 2021-04-22 16:06:31 +02:00
Stefan Zermatten
1f633621b7 Fixed a bug with functions accepting rolled arguments 2021-04-22 15:59:12 +02:00
Stefan Zermatten
9f3c8bef34 Removed stray console log 2021-04-22 15:54:41 +02:00
Stefan Zermatten
8a83e7d8a1 Fixed back button appearing in embedded dialogs 2021-04-22 15:42:44 +02:00
Stefan Zermatten
a28182f3e9 Added missing half rounded down icon for skills in stats tab 2021-04-22 15:40:26 +02:00
Stefan Zermatten
3d122e062f Added the distinction between half rounded up or down for proficiencies 2021-04-22 15:39:14 +02:00
Stefan Zermatten
e9a273244a Improved Effect and Proficiency UI in attribute and skill viewers 2021-04-22 15:12:49 +02:00
Stefan Zermatten
1de3122254 Updated UI to hide extra attributes and skills with same variable name 2021-04-22 15:12:21 +02:00
Stefan Zermatten
298db01e5b Updated computation engine to handle multiple attributes and skills with the same variable name 2021-04-22 15:11:49 +02:00
Stefan Zermatten
727101cd63 Updated Meteor 2021-04-22 15:10:47 +02:00
Stefan Zermatten
d4d002cf31 Fixed an error when targeting an ability score with a proficiency 2021-04-15 12:00:11 +02:00
Stefan Zermatten
2150bd6da4 Added breadcrumbs to creature properties 2021-04-13 14:17:31 +02:00
Stefan Zermatten
e1df145675 Add property button now in creature property dialogs 2021-04-13 11:53:50 +02:00
Stefan Zermatten
1eb78756ac Fixed console error if creature is deleted while sheet is still showing 2021-04-13 11:53:18 +02:00
Stefan Zermatten
ce9b9199ec Fixed dialog stacking animation 2021-04-13 11:41:14 +02:00
Stefan Zermatten
cfb1414494 Start character sheet on the character details dialog instead of build 2021-04-13 11:06:46 +02:00
Stefan Zermatten
4abd689c9f Use DiceCloud 5e base as default for new characters 2021-04-13 11:06:25 +02:00
Stefan Zermatten
f0e443fba2 When hiding the spells tab or tree tab, only change tabs if on one of those 2021-04-13 11:05:25 +02:00
Stefan Zermatten
52e7deedc6 Leave character page before deleting to prevent UI errors 2021-04-13 10:50:35 +02:00
Stefan Zermatten
15d593db79 Properties quick-inserted from the sheet now go into folders in the tree 2021-04-12 16:04:04 +02:00
Stefan Zermatten
e30754ef26 Added method to insert property to a tagged parent 2021-04-12 15:35:25 +02:00
Stefan Zermatten
255ac529b3 Added more default properties to creatures 2021-04-12 15:35:12 +02:00
Stefan Zermatten
c8b5ada5b9 Changed all form input fields to outlined style instead of filled 2021-04-12 14:21:50 +02:00
Stefan Zermatten
0c24238069 Fixed not found page for Vuetify 2 2021-04-11 18:15:56 +02:00
Stefan Zermatten
66847430ad Fixed sign in and register pages not being built with Vuetify 2 components 2021-04-11 18:05:36 +02:00
Stefan Zermatten
bfb860605f Creature properties now duplicate with up to 50 children 2021-04-11 14:47:41 +02:00
Stefan Zermatten
d87524418a Fixed bug where character sheet fab could be permanently hidden by closing the insert from library dialog 2021-04-11 13:52:25 +02:00
Stefan Zermatten
5f97592ed3 Updated package-lock for last commit 2021-04-11 13:33:38 +02:00
Stefan Zermatten
562991216f Moved ignore-styles dep from devDependencies to dependencies 2021-04-11 13:23:47 +02:00
Stefan Zermatten
109b89022e removed id from comment 2021-04-11 13:09:16 +02:00
Stefan Zermatten
d4ca07ce9c Began working on character migration code 2021-04-11 13:08:41 +02:00
Stefan Zermatten
885607f685 Moved the tree fab to the toolbar with smart parenting 2021-04-11 12:36:14 +02:00
Stefan Zermatten
81460f8835 Added file missed in last commit 2021-04-11 12:15:51 +02:00
Stefan Zermatten
cce5f9b926 Merge branch 'version-2-dev' of https://github.com/ThaumRystra/DiceCloud into version-2-dev 2021-04-11 12:15:33 +02:00
Stefan Zermatten
7d3a51de9d Moved ancestry setting responsibility to trusted code 2021-04-11 12:15:30 +02:00
Stefan Zermatten
fc774fcc5e Moved ancestry setting responsibility to trusted code 2021-04-11 12:14:39 +02:00
Stefan Zermatten
0f37a49b95 Fixed bug where wrong fab would show on character tab if spell tab was hidden 2021-04-11 11:01:31 +02:00
Stefan Zermatten
9814e20091 Added the ability to hide spells and tree tab. Tree tab hidden by default 2021-04-11 10:43:33 +02:00
Stefan Zermatten
d89cb77040 Fixed no-experiences Icon 2021-04-11 10:29:09 +02:00
Stefan Zermatten
8590d29abf Improved animation feel of character sheet fab 2021-04-11 10:21:14 +02:00
Stefan Zermatten
9298754dc9 Fixed character sheet title not updating correctly 2021-04-09 12:44:54 +02:00
Stefan Zermatten
e2d6d40bb3 Duplicating library nodes now duplicates up to 50 descendants 2021-04-09 12:36:44 +02:00
Stefan Zermatten
152677b023 Library nodes are now smarter about where in the tree they are inserted based on the currently selected node 2021-04-09 12:36:14 +02:00
Stefan Zermatten
838e2ed35f Fixed toolbar colors for vuetify 2 2021-03-28 14:29:56 +02:00
Stefan Zermatten
60ae1ef604 Fixed bug where editing a field and immediately changing selected property would apply the change to the new property rather than the old one 2021-03-28 13:56:45 +02:00
Stefan Zermatten
ecfe5f1360 Fixed spell casting buttons having the wrong color in dark mode 2021-03-28 13:14:38 +02:00
Stefan Zermatten
292d3c3f37 Tree titles now have hover. Fixed primary color theme switching 2021-03-28 13:09:47 +02:00
Stefan Zermatten
3c26bb2fc6 Reworked log data format, overhauled snackbar 2021-03-28 12:31:39 +02:00
Stefan Zermatten
ada1355c29 Added UI for filtered out slot fillers allowing loading more 2021-03-27 14:39:00 +02:00
Stefan Zermatten
2662af8ea2 Fixed misalignment on ability score tiles 2021-03-27 13:47:02 +02:00
Stefan Zermatten
26b68dccef Fixed style of attribute cards 2021-03-27 13:45:10 +02:00
Stefan Zermatten
0717f8e8d7 Fixed broken and insecure packages 2021-03-27 13:34:33 +02:00
Stefan Zermatten
5cf0330e03 Added library node insert button to library page, no automatic parenting 2021-03-26 12:49:08 +02:00
Stefan Zermatten
1978a2e4c7 Fixed Error when referencing slotLevel in a spell 2021-03-26 11:18:14 +02:00
Stefan Zermatten
5a2e500348 Fixed constants under toggles triggering calculation of those toggles before class levels are defined 2021-03-26 11:11:15 +02:00
Stefan Zermatten
7d4356592a Iterated on card color scheme 2021-03-26 11:06:52 +02:00
Stefan Zermatten
2c448d1748 Fixed some pages background colors 2021-03-26 10:00:37 +02:00
Stefan Zermatten
4f96d817d5 Change card and background color scheme 2021-03-26 09:48:29 +02:00
Stefan Zermatten
f9998eabc4 Fixed tabs using the wrong primary color 2021-03-26 09:35:19 +02:00
Stefan Zermatten
623cff584c Fixed form section expansion 2021-03-26 09:26:31 +02:00
Stefan Zermatten
aa34508cb3 Fixed hovering on toolbar cards 2021-03-26 09:26:16 +02:00
Stefan Zermatten
6678bc1cea Fixed library subscriptions, again 2021-03-25 14:12:02 +02:00
Stefan Zermatten
0324b9f7c3 Fixed some tooltips to vuetify 2 2021-03-25 13:14:40 +02:00
Stefan Zermatten
ccac142ec6 Fixed some cards not animating elevation change 2021-03-25 13:08:48 +02:00
Stefan Zermatten
fe3fa56541 Continued migrating UI to vuetify 2 2021-03-25 12:54:44 +02:00
Stefan Zermatten
480da6fc7d ES Lint fix migration to vuetify 2 2021-03-25 10:20:13 +02:00
Stefan Zermatten
6ffb48b7b6 Began migration to Vuetify 2.x expect a lot to be broken 2021-03-24 16:23:39 +02:00
Stefan Zermatten
82150df5e0 Updated packages and dependencies 2021-03-24 14:38:47 +02:00
Stefan Zermatten
9a120a6e9a Added description to class level viewer 2021-03-23 15:07:47 +02:00
Stefan Zermatten
f385c2857e Fixed library forward arrow being disabled if you can't edit the library 2021-03-12 09:34:20 +02:00
Stefan Zermatten
11a2851ac4 Fixed slots with computed expected quantity not hiding when full 2021-03-10 14:51:38 +02:00
Stefan Zermatten
313382fb82 Fixed library subscription issues 2021-03-10 14:40:14 +02:00
Stefan Zermatten
b9ae337a64 Merge branch 'version-2-dev' of https://github.com/ThaumRystra/DiceCloud into version-2-dev 2021-03-02 14:32:08 +02:00
Stefan Zermatten
4dc0a6159b Animated log entries 2021-03-02 14:32:05 +02:00
Stefan Zermatten
e00dfe1532 Changed the color of the log background 2021-03-02 14:31:35 +02:00
Stefan Zermatten
28e1fcabd5 Fixed damage properties by name failing if no properties were found 2021-03-02 14:10:14 +02:00
Stefan Zermatten
2c0496b44b Fixed properties not being made inactive by toggles 2021-03-02 13:56:53 +02:00
Stefan Zermatten
89adda60ec Reworked single page libraries to be more in line with the library view 2021-03-02 13:05:38 +02:00
Stefan Zermatten
8c3710cda3 Started work on single page libraries 2021-03-02 00:24:54 +02:00
Stefan Zermatten
b501b9d830 Fixed crash in skill calculation when level is overridden by an attribute 2021-03-01 18:40:55 +02:00
Stefan Zermatten
574f8373e7 Fixed crash when indexing a non-array node, added more array node errors 2021-03-01 14:47:46 +02:00
Stefan Zermatten
a7ecdecec1 Prevented contextual variables #type from being written to creature variable list 2021-03-01 14:22:03 +02:00
Stefan Zermatten
0aa59a4bfc Fixed creature not recomputing correctly when weight carried changes 2021-03-01 14:15:21 +02:00
Stefan Zermatten
8f0ff3245e Fixed containers still carrying their own weight if their contents are weightless and they aren't carried 2021-03-01 14:15:01 +02:00
Stefan Zermatten
9a2d10b7ed Fixed new library button hiding and not coming back 2021-03-01 14:08:12 +02:00
Stefan Zermatten
a8aa1923a8 Fixed spells having a stray deativatedBySelf flag 2021-03-01 14:01:34 +02:00
Stefan Zermatten
57fa162c89 Fixed stray errors from unepexted types 2021-03-01 13:37:19 +02:00
Stefan Zermatten
4d548c901c Ensured property exists before attempting to damage it 2021-03-01 13:32:46 +02:00
Stefan Zermatten
a97be2f93a Made constants work in calculations performed after recomputation 2021-03-01 13:27:48 +02:00
Stefan Zermatten
1276f872a0 Removed unused function 2021-03-01 12:11:22 +02:00
Stefan Zermatten
7daab97297 Made toggles function properly when nested under inactive properties and each other 2021-03-01 11:55:43 +02:00
Stefan Zermatten
2e3704d096 Prevented resources from writing unchanged data to the database 2021-03-01 11:42:50 +02:00
Stefan Zermatten
7283a27727 Constants should now respect toggles 2021-03-01 11:42:23 +02:00
Stefan Zermatten
3517636b8b Reworked toggles, again, to try and catch more edge cases. Made toggles set the inactive status of their property children in the compute step instead of the inactive denormalisation step 2021-03-01 11:41:59 +02:00
Stefan Zermatten
e617ef9b75 Merge branch 'version-2' into version-2-dev 2021-03-01 10:18:55 +02:00
Stefan Zermatten
cd45ae1442 Fixed buffs not recomputing correctly because of inactive properties not being activated 2021-03-01 10:07:24 +02:00
Stefan Zermatten
bcedd548c7 Fixed: If usesUsed was undefined, usesLeft of an action was NaN 2021-03-01 10:06:31 +02:00
Stefan Zermatten
dc53e38efe Libraries only fetch their data whene expanded 2021-02-27 10:49:10 +02:00
Stefan Zermatten
e381b3b24d Merge branch 'version-2' of https://github.com/ThaumRystra/DiceCloud into version-2 2021-02-26 09:48:22 +02:00
Stefan Zermatten
111d971bc2 Added attacks and actions to stats tab quick insert 2021-02-26 09:48:18 +02:00
Stefan Zermatten
bf4ce4f9f7 Hotfix: Adding properties to the tree, type selection fixed 2021-02-25 19:43:17 +02:00
Stefan Zermatten
2a983b0a94 User accounts can now be deleted with some UI to prevent accidental deletion 2021-02-25 14:28:51 +02:00
Stefan Zermatten
a5460bba0b Added floating action button to add properties directly to the sheet 2021-02-25 12:37:32 +02:00
Stefan Zermatten
df361236f5 Hotfix: Containers total weight now showing correctly on inventory tab 2021-02-24 15:18:32 +02:00
Stefan Zermatten
e1d670fe9f Fixed: buffs 2021-02-24 15:05:53 +02:00
Stefan Zermatten
1e9f0515e5 Contents that are weightless are now summed and stored on the container 2021-02-24 14:22:52 +02:00
Stefan Zermatten
0404020335 Added weights and content weight to containers UI 2021-02-24 14:07:20 +02:00
Stefan Zermatten
c248d8f4a0 Weight carried, Net worth, and Attunement implemented and exposed in UI 2021-02-24 13:41:30 +02:00
Stefan Zermatten
8d95da8b7a Fixed a bug where certain base values would be strings instead of numbers in effect aggregators 2021-02-24 11:58:04 +02:00
Stefan Zermatten
e11ab39864 Added tableLookup function 2021-02-24 11:57:40 +02:00
Stefan Zermatten
331fcef9ad Fixed: Error message when focus grabbing element is missing on form 2021-02-24 10:06:25 +02:00
Stefan Zermatten
7e3bff9677 Show creature milestone level and xp if creature has both 2021-02-24 10:05:11 +02:00
Stefan Zermatten
1b650b26b6 Fixed: using creature stats like XP in calculations 2021-02-24 10:01:02 +02:00
Stefan Zermatten
5925605962 Fixed property edit buttons no longer get pushed by long property name 2021-02-24 09:52:51 +02:00
Stefan Zermatten
dee1265b69 Fixed: Inline calculations in libarries now display as expected 2021-02-24 09:46:52 +02:00
Stefan Zermatten
3d3ec3bcf2 Increaed number of slot fillers loaded by the slot fill dialog to 20 2021-02-24 09:23:55 +02:00
Stefan Zermatten
dce2c92516 Added attack roll bonus and dc to spell list. Use them in spells with #spellList.dcResult and #spellList.attackRollBonusResult 2021-02-23 15:21:20 +02:00
Stefan Zermatten
0fe2780983 Added property viewer for Toggle properties 2021-02-23 15:07:07 +02:00
Stefan Zermatten
e126cdd3cb Added property viewer for slot filler 2021-02-23 14:59:53 +02:00
Stefan Zermatten
d69ada0db4 Slot quantity is now a computed value, added property viewer for slots 2021-02-23 14:53:47 +02:00
Stefan Zermatten
858915b25b Added viewer for Saving Throw properties 2021-02-23 14:38:20 +02:00
Stefan Zermatten
d10a7eca14 Added viewer for Roll properties 2021-02-23 14:29:48 +02:00
Stefan Zermatten
671d17018c Added a viewer for Constant properties 2021-02-23 14:23:00 +02:00
Stefan Zermatten
f2883d320f Improved Attribute damage viewer 2021-02-23 13:59:26 +02:00
Stefan Zermatten
aad0c7249e Removed stray log to console 2021-02-23 12:47:34 +02:00
326 changed files with 6184 additions and 3272 deletions

View File

@@ -3,50 +3,44 @@
# 'meteor add' and 'meteor remove' will edit this file for you,
# but you can also edit it by hand.
accounts-password@1.6.2
accounts-ui@1.3.1
accounts-password@1.7.0
random@1.2.0
dburles:collection-helpers
reactive-var@1.0.11
underscore@1.0.10
momentjs:moment
dburles:mongo-collection-instances
percolate:migrations
accounts-google@1.3.3
email@2.0.0
meteorhacks:subs-manager
chuangbo:marked
meteor-base@1.4.0
mobile-experience@1.1.0
mongo@1.10.0
mongo@1.11.0
session@1.2.0
jquery@1.11.10
tracker@1.2.0
logging@1.1.20
reload@1.3.0
logging@1.2.0
reload@1.3.1
ejson@1.1.1
check@1.3.1
standard-minifier-js@2.6.0
shell-server@0.5.0
seba:minifiers-autoprefixer
templates:array
ecmascript@0.14.3
ecmascript@0.15.1
es5-shim@4.8.0
reactive-dict@1.3.0
percolate:synced-cron
ongoworks:speakingurl
service-configuration@1.0.11
google-config-ui@1.0.1
dynamic-import@0.5.2
dynamic-import@0.6.0
ddp-rate-limiter@1.0.9
rate-limit@1.0.9
meteortesting:mocha
mdg:validated-method
akryum:vue-router2
static-html
aldeed:collection2@3.0.0
aldeed:schema-index
akryum:vue-component
zer0th:meteor-vuetify-loader
accounts-patreon
bozhao:link-accounts
peerlibrary:reactive-publish
@@ -54,3 +48,6 @@ simple:rest
simple:rest-method-mixin
mikowals:batch-insert
peerlibrary:subscription-data
seba:minifiers-autoprefixer
akryum:vue-component
akryum:vue-sass

View File

@@ -1 +1 @@
METEOR@1.11.1
METEOR@2.2

View File

@@ -1,29 +1,27 @@
accounts-base@1.7.0
accounts-base@1.9.0
accounts-google@1.3.3
accounts-oauth@1.2.0
accounts-password@1.6.2
accounts-password@1.7.0
accounts-patreon@0.1.0
accounts-ui@1.3.1
accounts-ui-unstyled@1.4.2
akryum:npm-check@0.1.2
akryum:vue-component@0.15.2
akryum:vue-component-dev-client@0.4.7
akryum:vue-component-dev-server@0.1.4
akryum:vue-router2@0.2.3
aldeed:collection2@3.2.1
akryum:vue-sass@0.1.2
aldeed:collection2@3.3.0
aldeed:schema-index@3.0.0
allow-deny@1.1.0
autoupdate@1.6.0
babel-compiler@7.5.3
autoupdate@1.7.0
babel-compiler@7.6.1
babel-runtime@1.5.0
base64@1.0.12
binary-heap@1.0.11
blaze@2.3.4
blaze-tools@1.0.10
blaze-tools@1.1.1
boilerplate-generator@1.7.1
bozhao:link-accounts@2.2.1
bozhao:link-accounts@2.3.2
caching-compiler@1.2.2
caching-html-compiler@1.1.3
caching-html-compiler@1.2.0
callback-hook@1.3.0
check@1.3.1
chuangbo:marked@0.3.5_1
@@ -32,14 +30,14 @@ coffeescript-compiler@2.4.1
dburles:collection-helpers@1.1.0
dburles:mongo-collection-instances@0.3.5
ddp@1.4.0
ddp-client@2.3.3
ddp-client@2.4.0
ddp-common@1.4.0
ddp-rate-limiter@1.0.9
ddp-server@2.3.2
deps@1.0.12
diff-sequence@1.1.1
dynamic-import@0.5.3
ecmascript@0.14.3
dynamic-import@0.6.0
ecmascript@0.15.1
ecmascript-runtime@0.7.0
ecmascript-runtime-client@0.11.0
ecmascript-runtime-server@0.10.0
@@ -48,49 +46,40 @@ email@2.0.0
es5-shim@4.8.0
fetch@0.1.1
geojson-utils@1.0.10
google-config-ui@1.0.1
google-oauth@1.3.0
hot-code-push@1.0.4
html-tools@1.0.11
htmljs@1.0.11
http@1.4.2
html-tools@1.1.1
htmljs@1.1.0
http@1.4.3
id-map@1.1.0
inter-process-messaging@0.1.1
jquery@1.11.11
lai:collection-extensions@0.2.1_1
launch-screen@1.2.0
less@2.8.0
launch-screen@1.2.1
livedata@1.0.18
lmieulet:meteor-coverage@1.1.4
localstorage@1.2.0
logging@1.1.20
logging@1.2.0
mdg:validated-method@1.2.0
meteor@1.9.3
meteor-base@1.4.0
meteorhacks:picker@1.0.3
meteorhacks:subs-manager@1.6.4
meteortesting:browser-tests@1.3.4
meteortesting:mocha@1.1.5
meteortesting:mocha-core@7.0.1
mikowals:batch-insert@1.2.0
minifier-css@1.5.3
minifier-js@2.6.0
minimongo@1.6.0
minimongo@1.6.2
mobile-experience@1.1.0
mobile-status-bar@1.1.0
modern-browsers@0.1.5
modules@0.15.0
modules@0.16.0
modules-runtime@0.12.0
momentjs:moment@2.29.1
mongo@1.10.0
mongo-decimal@0.1.1
mongo@1.11.0
mongo-decimal@0.1.2
mongo-dev-server@1.1.0
mongo-id@1.0.7
npm-bcrypt@0.9.3
npm-mongo@3.8.1
npm-mongo@3.9.0
oauth@1.3.2
oauth2@1.3.0
observe-sequence@1.0.16
ongoworks:speakingurl@9.0.0
ordered-dict@1.1.0
patreon-oauth@0.1.0
@@ -104,18 +93,18 @@ peerlibrary:reactive-mongo@0.4.0
peerlibrary:reactive-publish@0.10.0
peerlibrary:server-autorun@0.8.0
peerlibrary:subscription-data@0.8.0
percolate:migrations@0.9.8
percolate:synced-cron@1.3.2
promise@0.11.2
raix:eventemitter@0.1.3
raix:eventemitter@1.0.0
random@1.2.0
rate-limit@1.0.9
react-fast-refresh@0.1.0
reactive-dict@1.3.0
reactive-var@1.0.11
reload@1.3.0
reload@1.3.1
retry@1.1.0
routepolicy@1.1.0
seba:minifiers-autoprefixer@1.2.1
seba:minifiers-autoprefixer@2.0.1
service-configuration@1.0.11
session@1.2.0
sha@1.0.9
@@ -124,19 +113,17 @@ simple:json-routes@2.1.0
simple:rest@1.1.1
simple:rest-method-mixin@1.0.1
socket-stream-client@0.3.1
spacebars@1.0.15
spacebars-compiler@1.1.3
spacebars-compiler@1.2.1
srp@1.1.0
standard-minifier-js@2.6.0
static-html@1.2.2
static-html@1.3.0
templates:array@1.0.3
templating@1.3.2
templating-compiler@1.3.3
templating-runtime@1.3.2
templating-tools@1.1.2
tmeasday:check-npm-versions@0.3.2
templating-tools@1.2.0
tmeasday:check-npm-versions@1.0.1
tracker@1.2.0
typescript@4.2.2
underscore@1.0.10
url@1.3.1
webapp@1.9.1
webapp-hashing@1.0.9
webapp@1.10.1
webapp-hashing@1.1.0
zer0th:meteor-vuetify-loader@0.1.30

View File

@@ -7,7 +7,8 @@ import SharingSchema from '/imports/api/sharing/SharingSchema.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import {assertEditPermission} from '/imports/api/sharing/sharingPermissions.js';
import { assertUserHasPaidBenefits } from '/imports/api/users/patreon/tiers.js';
import defaultCharacterProperties from '/imports/api/creature/defaultCharacterProperties.js';
import insertPropertyFromLibraryNode from '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js';
import '/imports/api/creature/removeCreature.js';
import '/imports/api/creature/restCreature.js';
@@ -35,6 +36,16 @@ let CreatureSettingsSchema = new SimpleSchema({
type: Boolean,
optional: true,
},
// Show the tree tab
showTreeTab: {
type: Boolean,
optional: true,
},
// Hide the spells tab
hideSpellsTab: {
type: Boolean,
optional: true,
},
// How much each hitDice resets on a long rest
hitDiceResetMultiplier: {
type: Number,
@@ -92,8 +103,32 @@ let CreatureSchema = new SimpleSchema({
type: SimpleSchema.Integer,
defaultValue: 0,
},
// Sum of all weights of items and containers that are carried
'denormalizedStats.weightCarried': {
// Inventory
'denormalizedStats.weightTotal': {
type: Number,
defaultValue: 0,
},
'denormalizedStats.weightEquipment': {
type: Number,
defaultValue: 0,
},
'denormalizedStats.weightCarried': {
type: Number,
defaultValue: 0,
},
'denormalizedStats.valueTotal': {
type: Number,
defaultValue: 0,
},
'denormalizedStats.valueEquipment': {
type: Number,
defaultValue: 0,
},
'denormalizedStats.valueCarried': {
type: Number,
defaultValue: 0,
},
'denormalizedStats.itemsAttuned': {
type: Number,
defaultValue: 0,
},
@@ -164,24 +199,29 @@ const insertCreature = new ValidatedMethod({
let creatureId = Creatures.insert({
owner: this.userId,
});
CreatureProperties.insert({
slotTags: ['base'],
quantityExpected: 1,
type: 'propertySlot',
name: 'Base',
description: 'Choose a starting point for your character, this will define the basic setup of your character sheet. Without a base, your sheet will be empty.',
hideWhenFull: true,
parent: {collection: 'creatures', id: creatureId},
ancestors: [{collection: 'creatures', id: creatureId}],
order: 0,
tags: [],
spaceLeft: 1,
totalFilled: 0,
// Insert the default properties
// Not batchInsert because we want the properties cleaned by the schema
let baseId;
defaultCharacterProperties(creatureId).forEach(prop => {
let id = CreatureProperties.insert(prop);
if (prop.name === 'Ruleset'){
baseId = id;
}
});
if (Meteor.isServer){
// Insert the 5e ruleset as the default base
insertPropertyFromLibraryNode.call({
nodeId: 'iHbhfcg3AL5isSWbw',
parentRef: {id: baseId, collection: 'creatureProperties'},
order: 0.5,
});
}
this.unblock();
return creatureId;
},
});
const updateCreature = new ValidatedMethod({

View File

@@ -4,7 +4,7 @@ import embedInlineCalculations from '/imports/api/creature/computation/afterComp
export default function applyAction({prop, log}){
let content = { name: prop.name };
if (prop.summary){
content.description = embedInlineCalculations(
content.value = embedInlineCalculations(
prop.summary, prop.summaryCalculations
);
}

View File

@@ -13,42 +13,43 @@ export default function applyAdjustment({
...creature.variables,
...actionContext,
};
try {
var {result, errors} = evaluateString(prop.amount, scope, 'reduce');
if (typeof result !== 'number') {
log.content.push({
name: 'Attribute damage',
error: errors.join(', ') || 'Something went wrong',
});
}
} catch (e){
var {result, context} = evaluateString({
string: prop.amount,
scope,
fn: 'reduce'
});
context.errors.forEach(e => {
log.content.push({
name: 'Attribute damage',
error: e.toString(),
name: 'Attribute damage error',
value: e.message || e.toString(),
});
}
});
if (damageTargets) {
damageTargets.forEach(target => {
if (prop.target === 'each'){
result = evaluateString(prop.amount, scope, 'reduce');
({result} = evaluateString({
string: prop.amount,
scope,
fn: 'reduce'
}));
}
damagePropertiesByName.call({
creatureId: target._id,
variableName: prop.stat,
operation: prop.operation || 'increment',
value: result
value: result.value,
});
log.content.push({
name: 'Attribute damage',
resultPrefix: `${prop.stat} ${prop.operation === 'set' ? 'set to' : ''}`,
result: `${-result}`,
value: `${prop.stat}${prop.operation === 'set' ? ' set to' : ''}` +
` ${result.isNumber ? -result.value : result.toString()}`,
});
});
} else {
log.content.push({
name: 'Attribute damage',
resultPrefix: `${prop.stat} ${prop.operation === 'set' ? 'set to' : ''}`,
result: `${-result}`,
value: `${prop.stat}${prop.operation === 'set' ? ' set to' : ''}` +
` ${result.isNumber ? -result.value : result.toString()}`,
});
}
}

View File

@@ -17,7 +17,6 @@ export default function applyAttack({
log.content.push({
name: criticalHit ? 'Critical Hit!' : 'To Hit',
resultPrefix: `1d20 [${value}] + ${prop.rollBonusResult} = `,
result,
value: `1d20 [${value}] + ${prop.rollBonusResult} = ` + result,
});
}

View File

@@ -15,75 +15,101 @@ export default function applyDamage({
...creature.variables,
...actionContext,
};
// Add the target's variables to the scope
if (targets.length === 1){
scope.target = targets[0].variables;
}
// Determine if the hit is critical
let criticalHit = !!(
actionContext.criticalHit &&
actionContext.criticalHit.value &&
prop.damageType !== 'healing' // Can't critically heal
);
// Double the damage rolls if the hit is critical
let context = new CompilationContext({
doubleRolls: criticalHit,
});
try {
var {result, errors} = evaluateString(prop.amount, scope, 'reduce', context);
if (typeof result !== 'number') {
log.content.push({
error: errors.join(', '),
});
}
} catch (e){
// Compute the roll the first time, logging any errors
var {result} = evaluateString({
string: prop.amount,
scope,
fn: 'reduce',
context
});
// If the result is an error bail out now
if (result.constructor.name === 'ErrorNode'){
log.content.push({
error: e.toString(),
name: 'Damage error',
value: result.toString(),
});
return;
}
let suffix = (criticalHit ? ' critical ' : '') +
// Memoise the damage suffix for the log
let suffix = (criticalHit ? ' critical ' : ' ') +
prop.damageType +
(prop.damageType !== 'healing' ? ' damage': '');
(prop.damageType !== ' healing ' ? ' damage ': '');
if (damageTargets && damageTargets.length) {
// Iterate through all the targets
damageTargets.forEach(target => {
let name = prop.damageType === 'healing' ? 'Healing' : 'Damage';
// Reroll the damage if needed
if (prop.target === 'each'){
result = evaluateString(prop.amount, scope, 'reduce');
({result, context} = evaluateString({
string: prop.amount,
scope,
fn: 'reduce'
}));
}
// If the result is an error or not a number bail out now
if (result.constructor.name === 'ErrorNode' || !result.isNumber){
log.content.push({
name: 'Damage error',
value: result.toString(),
});
return;
}
// Deal the damage to the target
let damageDealt = dealDamage.call({
creatureId: target._id,
damageType: prop.damageType,
amount: result,
amount: result.value,
});
// Log the damage done
if (target._id === creature._id){
// Target is same as self, log damage as such
log.content.push({
name,
result: damageDealt,
details: suffix + 'to self',
value: damageDealt + suffix + ' to self',
});
} else {
log.content.push({
name,
resultPrefix: 'Dealt ',
result: damageDealt,
details: suffix + `${target.name && ' to '}${target.name}`,
value: 'Dealt ' + damageDealt + suffix + ` ${target.name && ' to '}${target.name}`,
});
// Log the damage received on that creature's log as well
insertCreatureLog.call({
log: {
creatureId: target._id,
content: [{
name,
resultPrefix: 'Recieved ',
result: damageDealt,
details: suffix,
value: 'Recieved ' + damageDealt + suffix,
}],
creatureId: target._id,
}
});
}
});
} else {
// There are no targets, just log the result
log.content.push({
name: prop.damageType === 'healing' ? 'Healing' : 'Damage',
result,
details: suffix,
value: result.toString() + suffix,
});
}
}

View File

@@ -10,7 +10,7 @@ import applySave from '/imports/api/creature/actions/applySave.js';
function applyProperty(options){
let prop = options.prop;
if (prop.type === 'buff'){
// ignore only applied buffs
// ignore only applied buffs, don't apply them again
if (prop.applied === true){
return false;
}
@@ -40,7 +40,7 @@ function applyProperty(options){
break;
case 'buff':
applyBuff(options);
break;
return false;
case 'toggle':
return applyToggle(options);
case 'roll':
@@ -52,23 +52,24 @@ function applyProperty(options){
return true;
}
function applyPropertyAndWalkChildren({prop, child, targets, ...options}){
let shouldKeepWalking = applyProperty({ prop, targets, ...options });
function applyPropertyAndWalkChildren({prop, children, targets, ...options}){
let shouldKeepWalking = applyProperty({ prop, children, targets, ...options });
if (shouldKeepWalking){
applyProperties({ forest: child.children, targets, ...options,});
applyProperties({ forest: children, targets, ...options,});
}
}
export default function applyProperties({ forest, targets, ...options}){
forest.forEach(child => {
let prop = child.node;
forest.forEach(node => {
let prop = node.node;
let children = node.children;
if (shouldSplit(prop) && targets.length){
targets.forEach(target => {
let targets = [target]
applyPropertyAndWalkChildren({ targets, prop, child, ...options});
applyPropertyAndWalkChildren({ targets, prop, children, ...options});
});
} else {
applyPropertyAndWalkChildren({prop, child, targets, ...options});
applyPropertyAndWalkChildren({prop, children, targets, ...options});
}
});
}

View File

@@ -10,23 +10,16 @@ export default function applyRoll({
...creature.variables,
...actionContext,
};
try {
var {result, errors} = evaluateString(prop.roll, scope, 'reduce');
actionContext[prop.variableName] = result;
log.content.push({
name: prop.name,
resultPrefix: prop.variableName + ' = ' + prop.roll + ' = ',
result,
});
if (errors.length) {
log.content.push({
error: errors.join(', '),
});
}
} catch (e){
log.content.push({
error: e.toString(),
});
var {result} = evaluateString({
string: prop.roll,
scope,
fn: 'reduce'
});
if (result.isNumber){
actionContext[prop.variableName] = result.value;
}
log.content.push({
name: prop.name,
value: prop.variableName + ' = ' + prop.roll + ' = ' + result.toString(),
});
}

View File

@@ -14,19 +14,16 @@ export default function applySave({
};
try {
// Calculate the DC
var {result, errors} = evaluateString(prop.dc, scope, 'reduce');
let dc = result;
var {result} = evaluateString({
string: prop.dc,
scope,
fn: 'reduce'
});
let dc = result.value;
log.content.push({
name: prop.name,
resultPrefix: ' DC ',
result,
value: ' DC ' + result.toString(),
});
if (errors.length) {
log.content.push({
error: errors.join(', '),
});
return false;
}
if (prop.target === 'self'){
let save = CreaturesProperties.findOne({
'ancestors.id': creature._id,
@@ -38,7 +35,8 @@ export default function applySave({
});
if (!save){
log.content.push({
error: 'No saving throw found: ' + prop.stat,
name: 'Saving throw error',
value: 'No saving throw found: ' + prop.stat,
});
return;
}
@@ -62,9 +60,7 @@ export default function applySave({
let saveSuccess = result >= dc;
log.content.push({
name: 'Save',
resultPrefix,
result,
details: saveSuccess ? 'Passed' : 'Failed'
value: resultPrefix + result + (saveSuccess ? 'Passed' : 'Failed')
});
return !saveSuccess;
} else {
@@ -73,7 +69,8 @@ export default function applySave({
}
} catch (e){
log.content.push({
error: e.toString(),
name: 'Save error',
value: e.toString(),
});
}
}

View File

@@ -13,23 +13,21 @@ export default function applyToggle({
if (Number.isFinite(+prop.condition)){
return !!+prop.condition;
}
try {
var {result, errors} = evaluateString(prop.condition, scope, 'reduce');
if (typeof result !== 'number' && typeof result !== 'boolean') {
log.content.push({
error: errors.join(', '),
});
return false;
}
var {result} = evaluateString({
string: prop.condition,
scope,
fn: 'reduce'
});
if (result.constructor.name === 'ErrorNode') {
log.content.push({
name: prop.name,
resultPrefix: prop.condition + ' = ',
result,
});
return !!result;
} catch (e){
log.content.push({
error: e.toString(),
name: 'Toggle error',
value: result.toString(),
});
return false;
}
log.content.push({
name: prop.name || 'Toggle',
value: prop.condition + ' = ' + result.toString(),
});
return !!result.value;
}

View File

@@ -65,7 +65,7 @@ const castSpellWithSlot = new ValidatedMethod({
action: spell,
context: {slotLevel},
creature,
target,
targets: target ? [target] : [],
method: this,
});
// Note this only recomputes the top-level creature, not the nearest one

View File

@@ -9,6 +9,8 @@ import { assertEditPermission } from '/imports/api/creature/creaturePermissions.
import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js';
import { nodesToTree } from '/imports/api/parenting/parenting.js';
import applyProperties from '/imports/api/creature/actions/applyProperties.js';
import recomputeInventory from '/imports/api/creature/denormalise/recomputeInventory.js';
import recomputeInactiveProperties from '/imports/api/creature/denormalise/recomputeInactiveProperties.js';
const doAction = new ValidatedMethod({
name: 'creatureProperties.doAction',
@@ -43,9 +45,16 @@ const doAction = new ValidatedMethod({
});
doActionWork({action, creature, targets, method: this});
// The acting creature might have used ammo
recomputeInventory(creature._id);
// The action might add properties which need to be activated
recomputeInactiveProperties(creature._id);
// recompute creatures
recomputeCreatureByDoc(creature);
targets.forEach(target => {
recomputeInactiveProperties(target._id);
recomputeCreatureByDoc(target);
});
},

View File

@@ -61,7 +61,7 @@ export default function spendResources({prop, log}){
});
log.content.push({
name: 'Uses left',
result: prop.usesResult - prop.usesUsed - 1,
value: prop.usesResult - (prop.usesUsed || 0) - 1,
});
}
@@ -84,10 +84,10 @@ export default function spendResources({prop, log}){
// Log all the spending
if (gainLog.length) log.content.push({
name: 'Gained',
description: gainLog.join('\n'),
value: gainLog.join('\n'),
});
if (spendLog.length) log.content.push({
name: 'Spent',
description: spendLog.join('\n'),
value: spendLog.join('\n'),
});
}

View File

@@ -4,8 +4,8 @@ export default function embedInlineCalculations(string, calculations){
if (!string) return '';
if (!calculations) return string;
let index = 0;
return string.replace(INLINE_CALCULATION_REGEX, () => {
return string.replace(INLINE_CALCULATION_REGEX, substring => {
let comp = calculations && calculations[index++];
return comp && comp.result ? comp.result : string;
return (comp && 'result' in comp) ? comp.result : substring;
});
}

View File

@@ -1,30 +1,67 @@
import { parse, CompilationContext } from '/imports/parser/parser.js';
import ConstantNode from '/imports/parser/parseTree/ConstantNode.js';
import SymbolNode from '/imports/parser/parseTree/SymbolNode.js';
import ErrorNode from '/imports/parser/parseTree/ErrorNode.js';
export default function evaluateString(string, scope, fn = 'compile', context){
let errors = [];
//TODO replace constants with their parsed node
export default function evaluateString({string, scope, fn = 'compile', context}){
if (!context){
context = new CompilationContext({});
}
if (!string){
errors.push('No string provided');
return {result: string, errors};
context.storeError('No string provided');
return {result: {value: string}, context};
}
if (!scope) errors.push('No scope provided');
if (!scope) context.storeError('No scope provided');
// Parse the string using mathjs
let node;
try {
node = parse(string);
} catch (e) {
errors.push(e);
return {result: string, errors};
}
if (!context){
context = new CompilationContext({});
context.storeError(e);
return {result: {value: string}, context};
}
node = replaceConstants({calc: node, context, scope});
let result = node[fn](scope, context);
if (result instanceof ConstantNode){
return {result: result.value, errors: context.errors}
} else {
return {result: result.toString(), errors: context.errors};
}
return {result, context};
}
// Replace constants in the calc with the right ParseNodes
function replaceConstants({calc, context, scope}){
let constFailed = [];
calc = calc.replaceNodes(node => {
if (!(node instanceof SymbolNode)) return;
let constant = scope[node.name];
// replace constants that aren't overridden by stats or disabled by a toggle
if (constant && constant.type === 'constant'){
// Fail if the constant has errors
if (constant.errors && constant.errors.length){
constFailed.push(node.name);
return;
}
let parsedConstantNode;
try {
parsedConstantNode = parse(constant.calculation);
} catch(e){
constFailed.push(node.name);
return;
}
if (!parsedConstantNode) constFailed.push(node.name);
return parsedConstantNode;
}
});
constFailed.forEach(name => {
context.storeError({
type: 'error',
message: `${name} is a constant property with parsing errors`
});
});
let failed = !!constFailed.length;
if (failed){
calc = new ErrorNode({error: 'Failed to replace constants'});
}
return calc;
}

View File

@@ -1,13 +0,0 @@
import evaluateString from '/imports/api/creature/computation/afterComputation/evaluateString.js';
// Strings can have computations in bracers like so: {computation}
export default function evalutateStringWithEmbeddedCalculations(string, scope){
console.warn('evalutateStringWithEmbeddedCalculations should be replaced with ' +
'fetching the result from the compuations on the property doc');
if (!string) return string;
// Compute everything inside bracers
return string.replace(/\{([^{}]*)\}/g, function(match, p1){
let {result} = evaluateString(p1, scope);
return result;
});
}

View File

@@ -7,6 +7,7 @@ export default class ComputationMemo {
constructor(props, creature){
this.statsByVariableName = {};
this.constantsByVariableName = {};
this.constantsById = {};
this.extraStatsByVariableName = {};
this.statsById = {};
this.originalPropsById = {};
@@ -77,11 +78,7 @@ export default class ComputationMemo {
}
addConstant(prop){
prop = this.registerProperty(prop);
if (
!this.constantsByVariableName[prop.variableName]
){
this.constantsByVariableName[prop.variableName] = prop
}
this.constantsById[prop._id] = prop;
}
registerProperty(prop){
this.originalPropsById[prop._id] = cloneDeep(prop);
@@ -107,31 +104,10 @@ export default class ComputationMemo {
let variableName = prop.variableName;
if (!variableName) return;
let existingStat = this.statsByVariableName[variableName];
prop = this.registerProperty(prop);
if (existingStat){
existingStat.computationDetails.idsOfSameName.push(prop._id);
this.originalPropsById[prop._id] = cloneDeep(prop);
if (prop.baseValueCalculation){
existingStat.computationDetails.effects.push({
operation: 'base',
calculation: prop.baseValueCalculation,
stats: [variableName],
computationDetails: propDetailsByType.effect(),
statBase: true,
dependencies: [],
});
}
if (prop.baseProficiency){
existingStat.computationDetails.proficiencies.push({
value: prop.baseProficiency,
stats: [variableName],
computationDetails: propDetailsByType.proficiency(),
type: 'proficiency',
statBase: true,
dependencies: [],
});
}
} else {
prop = this.registerProperty(prop);
this.statsById[prop._id] = prop;
this.statsByVariableName[variableName] = prop;
if (
@@ -193,7 +169,9 @@ export default class ComputationMemo {
prop = this.registerProperty(prop);
let targets = this.getProficiencyTargets(prop);
targets.forEach(target => {
target.computationDetails.proficiencies.push(prop);
if(target.computationDetails.proficiencies){
target.computationDetails.proficiencies.push(prop);
}
});
}
getProficiencyTargets(prop){
@@ -256,7 +234,6 @@ const propDetailsByType = {
default(){
return {
toggleAncestors: [],
disabledByToggle: false,
};
},
toggle(){
@@ -264,7 +241,6 @@ const propDetailsByType = {
computed: false,
busyComputing: false,
toggleAncestors: [],
disabledByToggle: false,
};
},
attribute(){
@@ -272,8 +248,8 @@ const propDetailsByType = {
computed: false,
busyComputing: false,
effects: [],
proficiencies: [],
toggleAncestors: [],
disabledByToggle: false,
idsOfSameName: [],
};
},
@@ -284,7 +260,6 @@ const propDetailsByType = {
effects: [],
proficiencies: [],
toggleAncestors: [],
disabledByToggle: false,
idsOfSameName: [],
};
},
@@ -293,26 +268,22 @@ const propDetailsByType = {
computed: false,
busyComputing: false,
toggleAncestors: [],
disabledByToggle: false,
};
},
classLevel(){
return {
computed: true,
toggleAncestors: [],
disabledByToggle: false,
};
},
proficiency(){
return {
toggleAncestors: [],
disabledByToggle: false,
};
},
denormalizedStat(){
return {
toggleAncestors: [],
disabledByToggle: false,
};
}
}

View File

@@ -1,31 +1,6 @@
import evaluateCalculation from '/imports/api/creature/computation/engine/evaluateCalculation.js';
import { union } from 'lodash';
export default class EffectAggregator{
constructor(stat, memo){
delete this.baseValueErrors;
if (stat.baseValueCalculation){
let {
result,
context,
dependencies
} = evaluateCalculation({
string: stat.baseValueCalculation,
prop: stat,
memo
});
this.statBaseValue = result.value;
stat.dependencies = union(
stat.dependencies,
dependencies,
);
if (context.errors.length){
this.baseValueErrors = context.errors;
}
this.base = this.statBaseValue;
} else {
this.base = 0;
}
constructor(){
this.base = 0;
this.add = 0;
this.mul = 1;
this.min = Number.NEGATIVE_INFINITY;
@@ -46,11 +21,6 @@ export default class EffectAggregator{
case 'base':
// Take the largest base value
this.base = result > this.base ? result : this.base;
if (effect.statBase){
if (this.statBaseValue === undefined || result > this.statBaseValue){
this.statBaseValue = result;
}
}
break;
case 'add':
// Add all adds together

View File

@@ -2,6 +2,11 @@ import computeToggle from '/imports/api/creature/computation/engine/computeToggl
import { union } from 'lodash';
export default function applyToggles(prop, memo){
// If it used to be inactive delete those fields
if (prop.inactive) prop.inactive = undefined;
if (prop.deactivatedByAncestor) prop.deactivatedByAncestor = undefined;
if (prop.deactivatedByToggle) prop.deactivatedByToggle = undefined;
// Iterate through the toggle ancestors from oldest to nearest
prop.computationDetails.toggleAncestors.forEach(toggleId => {
let toggle = memo.togglesById[toggleId];
computeToggle(toggle, memo);
@@ -10,8 +15,11 @@ export default function applyToggles(prop, memo){
[toggle._id],
toggle.dependencies,
);
// Deactivate if the toggle is false
if (!toggle.toggleResult){
prop.computationDetails.disabledByToggle = true;
prop.inactive = true;
prop.deactivatedByAncestor = true;
prop.deactivatedByToggle = true;
}
});
}

View File

@@ -1,5 +1,5 @@
import computeStat from '/imports/api/creature/computation/engine/computeStat.js';
import applyToggles from '/imports/api/creature/computation/engine/applyToggles.js';
import computeProficiency from '/imports/api/creature/computation/engine/computeProficiency.js';
import evaluateCalculation from '/imports/api/creature/computation/engine/evaluateCalculation.js';
import { union } from 'lodash';
@@ -14,7 +14,8 @@ export default function combineStat(stat, aggregator, memo){
}
function getAggregatorResult(stat, aggregator){
let result = (aggregator.base + aggregator.add) * aggregator.mul;
let base = Math.max(aggregator.base, stat.baseValue || 0);
let result = (base + aggregator.add) * aggregator.mul;
if (result < aggregator.min) {
result = aggregator.min;
}
@@ -32,8 +33,6 @@ function getAggregatorResult(stat, aggregator){
function combineAttribute(stat, aggregator, memo){
stat.value = getAggregatorResult(stat, aggregator);
stat.baseValue = aggregator.statBaseValue;
stat.baseValueErrors = aggregator.baseValueErrors;
if (stat.attributeType === 'spellSlot'){
let {
result,
@@ -78,9 +77,7 @@ function combineSkill(stat, aggregator, memo){
// Skills are based on some ability Modifier
let ability = stat.ability && memo.statsByVariableName[stat.ability]
if (stat.ability && ability){
if (!ability.computationDetails.computed){
computeStat(ability, memo);
}
computeStat(ability, memo);
stat.abilityMod = ability.modifier;
stat.dependencies = union(
stat.dependencies,
@@ -91,12 +88,12 @@ function combineSkill(stat, aggregator, memo){
stat.abilityMod = 0;
}
// Combine all the child proficiencies
stat.proficiency = stat.baseProficiency || 0;
stat.proficiency = 0;
for (let i in stat.computationDetails.proficiencies){
let prof = stat.computationDetails.proficiencies[i];
applyToggles(prof, memo);
computeProficiency(prof, memo);
if (
!prof.computationDetails.disabledByToggle &&
!prof.deactivatedByToggle &&
prof.value > stat.proficiency
){
stat.proficiency = prof.value;
@@ -111,27 +108,34 @@ function combineSkill(stat, aggregator, memo){
let profBonusStat = memo.statsByVariableName['proficiencyBonus'];
let profBonus = profBonusStat && profBonusStat.value;
if (typeof profBonus !== 'number' && memo.statsByVariableName['level']){
let level = memo.statsByVariableName['level'].value;
profBonus = Math.ceil(level / 4) + 1;
if (level._id){
stat.dependencies = union(stat.dependencies, [level._id]);
}
if (level.dependencies){
stat.dependencies = union(stat.dependencies, level.dependencies);
}
} else {
if (profBonusStat){
stat.dependencies = union(
stat.dependencies,
[profBonusStat._id],
profBonusStat.dependencies,
);
}
if (typeof profBonus !== 'number' && memo.statsByVariableName['level']){
let levelProp = memo.statsByVariableName['level'];
let level = levelProp.value;
profBonus = Math.ceil(level / 4) + 1;
if (levelProp._id){
stat.dependencies = union(stat.dependencies, [levelProp._id]);
}
if (levelProp.dependencies){
stat.dependencies = union(stat.dependencies, levelProp.dependencies);
}
}
// Multiply the proficiency bonus by the actual proficiency
profBonus *= stat.proficiency;
// Base value
stat.baseValue = aggregator.statBaseValue;
stat.baseValueErrors = aggregator.baseValueErrors;
if(stat.proficiency === 0.49){
// Round down proficiency bonus in the special case
profBonus = Math.floor(profBonus * 0.5);
} else {
profBonus = Math.ceil(profBonus * stat.proficiency);
}
// Combine everything to get the final result
let result = (aggregator.base + stat.abilityMod + profBonus + aggregator.add) * aggregator.mul;
if (result < aggregator.min) result = aggregator.min;

View File

@@ -0,0 +1,12 @@
import applyToggles from '/imports/api/creature/computation/engine/applyToggles.js';
export default function computeConstant(constant, memo){
// Apply any toggles
applyToggles(constant, memo);
if (constant.deactivatedByToggle) return;
if (
!memo.constantsByVariableName[constant.variableName]
){
memo.constantsByVariableName[constant.variableName] = constant
}
}

View File

@@ -1,8 +1,11 @@
import evaluateCalculation from '/imports/api/creature/computation/engine/evaluateCalculation.js';
import ConstantNode from '/imports/parser/parseTree/ConstantNode.js';
import applyToggles from '/imports/api/creature/computation/engine/applyToggles.js';
import { union } from 'lodash';
export default function computeEndStepProperty(prop, memo){
applyToggles(prop, memo);
switch (prop.type){
case 'action':
case 'spell':
@@ -14,16 +17,22 @@ export default function computeEndStepProperty(prop, memo){
break;
case 'attack':
computeAction(prop, memo);
computeAttack(prop, memo);
computePropertyField(prop, memo, 'rollBonus');
break;
case 'savingThrow':
computeSavingThrow(prop, memo);
computePropertyField(prop, memo, 'dc');
break;
case 'spellList':
computeSpellList(prop, memo);
computePropertyField(prop, memo, 'maxPrepared');
computePropertyField(prop, memo, 'attackRollBonus');
computePropertyField(prop, memo, 'dc');
break;
case 'propertySlot':
computeSlot(prop, memo);
computePropertyField(prop, memo, 'quantityExpected');
computePropertyField(prop, memo, 'slotCondition');
break;
case 'roll':
computePropertyField(prop, memo, 'roll', 'compile');
break;
}
}
@@ -69,26 +78,33 @@ function computeAction(prop, memo){
});
// Items consumed
prop.resources.itemsConsumed.forEach((itemConsumed, i) => {
let item = itemConsumed.itemId && memo.equipmentById[itemConsumed.itemId];
prop.resources.itemsConsumed[i].itemId = item && item._id;
let available = item && item.quantity || 0;
let item = itemConsumed.itemId ?
memo.equipmentById[itemConsumed.itemId] :
undefined;
let available = item ? item.quantity : 0;
prop.resources.itemsConsumed[i].available = available;
let name = item && item.name;
if (item && item.quantity !== 1 && item.plural){
name = item.plural;
}
prop.resources.itemsConsumed[i].itemName = name;
prop.resources.itemsConsumed[i].itemIcon = item && item.icon;
prop.resources.itemsConsumed[i].itemColor = item && item.color;
if (!item || available < itemConsumed.quantity){
prop.insufficientResources = true;
}
if (item){
prop.resources.itemsConsumed[i].itemId = item._id;
let name = item.name;
if (item.quantity !== 1 && item.plural){
name = item.plural;
}
if (name) prop.resources.itemsConsumed[i].itemName = name;
if (item.icon) prop.resources.itemsConsumed[i].itemIcon = item.icon;
if (item.color) prop.resources.itemsConsumed[i].itemColor = item.color;
prop.dependencies = union(
prop.dependencies,
[item._id],
item.dependencies
);
} else {
delete prop.resources.itemsConsumed[i].itemId;
delete prop.resources.itemsConsumed[i].itemName;
delete prop.resources.itemsConsumed[i].itemIcon;
delete prop.resources.itemsConsumed[i].itemColor;
}
});
}
@@ -111,19 +127,3 @@ function computePropertyField(prop, memo, fieldName, fn){
delete prop[`${fieldName}Errors`];
}
}
function computeAttack(prop, memo){
computePropertyField(prop, memo, 'rollBonus');
}
function computeSavingThrow(prop, memo){
computePropertyField(prop, memo, 'dc');
}
function computeSpellList(prop, memo){
computePropertyField(prop, memo, 'maxPrepared');
}
function computeSlot(prop, memo){
computePropertyField(prop, memo, 'slotCondition');
}

View File

@@ -1,4 +1,5 @@
import { forOwn, has, union } from 'lodash';
import applyToggles from '/imports/api/creature/computation/engine/applyToggles.js';
export default function computeLevels(memo){
computeClassLevels(memo);
@@ -7,11 +8,13 @@ export default function computeLevels(memo){
function computeClassLevels(memo){
forOwn(memo.classLevelsById, classLevel => {
applyToggles(classLevel, memo);
// class levels are mutually dependent
classLevel.dependencies = union(
classLevel.dependencies,
Object.keys(memo.classLevelsById)
);
if (classLevel.deactivatedByToggle) return;
let name = classLevel.variableName;
let stat = memo.statsByVariableName[name];
if (!stat){
@@ -29,7 +32,7 @@ function computeClassLevels(memo){
function computeTotalLevel(memo){
let currentLevel = memo.statsByVariableName['level'];
if (!currentLevel){
if (!currentLevel || currentLevel.deactivatedByToggle){
currentLevel = {
value: 0,
dependencies: [],

View File

@@ -5,10 +5,15 @@ import computeEffect from '/imports/api/creature/computation/engine/computeEffec
import computeToggle from '/imports/api/creature/computation/engine/computeToggle.js';
import computeEndStepProperty from '/imports/api/creature/computation/engine/computeEndStepProperty.js';
import computeInlineCalculations from '/imports/api/creature/computation/engine/computeInlineCalculations.js';
import computeConstant from '/imports/api/creature/computation/engine/computeConstant.js';
export default function computeMemo(memo){
// Compute level
computeLevels(memo);
// Compute all constants that could be used
forOwn(memo.constantsById, constant => {
computeConstant (constant, memo);
});
// Compute all stats, even if they are overriden
forOwn(memo.statsById, stat => {
computeStat (stat, memo);

View File

@@ -0,0 +1,23 @@
import applyToggles from '/imports/api/creature/computation/engine/applyToggles.js';
export default function computeEffect(proficiency, memo){
if (proficiency.computationDetails.computed) return;
if (proficiency.computationDetails.busyComputing){
// Trying to compute this proficiency again while it is already computing.
// We must be in a dependency loop.
proficiency.computationDetails.computed = true;
proficiency.result = NaN;
proficiency.computationDetails.busyComputing = false;
proficiency.computationDetails.error = 'dependencyLoop';
if (Meteor.isClient) console.warn('dependencyLoop', proficiency);
return;
}
// Before doing any work, mark this proficiency as busy
proficiency.computationDetails.busyComputing = true;
// Apply any toggles
applyToggles(proficiency, memo);
proficiency.computationDetails.computed = true;
proficiency.computationDetails.busyComputing = false;
}

View File

@@ -1,8 +1,9 @@
import combineStat from '/imports/api/creature/computation/engine/combineStat.js';
import computeEffect from '/imports/api/creature/computation/engine/computeEffect.js';
import EffectAggregator from '/imports/api/creature/computation/engine/EffectAggregator.js';
import evaluateCalculation from '/imports/api/creature/computation/engine/evaluateCalculation.js';
import applyToggles from '/imports/api/creature/computation/engine/applyToggles.js';
import { each, union } from 'lodash';
import { each, union, without } from 'lodash';
export default function computeStat(stat, memo){
// If the stat is already computed, skip it
@@ -19,32 +20,135 @@ export default function computeStat(stat, memo){
}
// Before doing any work, mark this stat as busy
stat.computationDetails.busyComputing = true;
// Apply any toggles
applyToggles(stat, memo);
if (!stat.computationDetails.disabledByToggle){
// Compute and aggregate all the effects
let aggregator = new EffectAggregator(stat, memo)
each(stat.computationDetails.effects, (effect) => {
computeEffect(effect, memo);
if (effect._id){
stat.dependencies = union(
stat.dependencies,
[effect._id]
);
}
stat.dependencies = union(
stat.dependencies,
effect.dependencies
)
if (!effect.computationDetails.disabledByToggle){
aggregator.addEffect(effect);
}
});
// Conglomerate all the effects to compute the final stat values
combineStat(stat, aggregator, memo);
let effects = stat.computationDetails.effects || [];
let proficiencies = stat.computationDetails.proficiencies || [];
// Get references to all the stats that share the variable name
let sameNameStats
if (stat.computationDetails.idsOfSameName){
sameNameStats = stat.computationDetails.idsOfSameName.map(
id => memo.propsById[id]
);
} else {
sameNameStats = [];
}
// Mark the attribute as computed
stat.computationDetails.computed = true;
stat.computationDetails.busyComputing = false;
let allStats = [stat, ...sameNameStats];
// Decide which stat is the last active stat
// The last active stat is considered the cannonical stat
let lastActiveStat;
allStats.forEach(candidateStat => {
applyToggles(candidateStat, memo);
if (!candidateStat.inactive) lastActiveStat = candidateStat;
candidateStat.overridden = undefined;
});
if (!lastActiveStat){
delete memo.statsByVariableName[stat.variableName];
return;
}
// Make sure the active stat has all the effects and proficiencies
lastActiveStat.computationDetails.effects = effects;
lastActiveStat.computationDetails.proficiencies = proficiencies;
// Update the memo's stat with the chosen stat
memo.statsByVariableName[stat.variableName] = lastActiveStat;
// Recreate list of the non-cannonical stats
sameNameStats = without(allStats, lastActiveStat);
sameNameStats.forEach(statInstance => {
// Mark the non-cannonical stats as overridden
statInstance.overridden = true;
// Apply the cannonical damage
statInstance.damage = lastActiveStat.damage;
});
let baseDependencies = [];
allStats.forEach(statInstance => {
// Add this stat and its deps to the dependencies
baseDependencies = union(
baseDependencies,
[statInstance._id],
statInstance.dependencies,
);
// Apply all the base proficiencies
if (statInstance.baseProficiency && !statInstance.inactive){
proficiencies.push({
value: statInstance.baseProficiency,
stats: [statInstance.variableName],
type: 'proficiency',
dependencies: statInstance.overridden ?
union(statInstance.dependencies, [statInstance._id]) :
[],
computationDetails: {
computed: true,
}
});
}
// Compute each active stat's baseValue calculation and apply it
if (!statInstance.inactive) {
let {
result,
context,
dependencies
} = evaluateCalculation({
string: statInstance.baseValueCalculation,
prop: statInstance,
memo
});
statInstance.baseValue = +result.value;
statInstance.dependencies = union(statInstance.dependencies, dependencies);
if (context.errors.length){
statInstance.baseValueErrors = context.errors;
}
// Apply all the base values
effects.push({
operation: 'base',
calculation: statInstance.baseValueCalculation,
result: statInstance.baseValue,
stats: [statInstance.variableName],
dependencies: statInstance.overridden ?
union(statInstance.dependencies, [statInstance._id]) :
[],
computationDetails: {
computed: true,
},
});
}
});
// Compute and aggregate all the effects
let aggregator = new EffectAggregator();
let effectDeps = [];
each(effects, (effect) => {
// Compute
computeEffect(effect, memo);
if (effect.deactivatedByToggle) return;
// dependencies
if (effect._id) effectDeps = union(effectDeps, [effect._id]);
effectDeps = union(effectDeps, effect.dependencies);
// Add computed effect to aggregator
aggregator.addEffect(effect);
});
// Combine the effects into the stats
allStats.forEach(statInstance => {
// Conglomerate all the effects to compute the final stat values
combineStat(statInstance, aggregator, memo);
// Mark the stats as computed
statInstance.computationDetails.computed = true;
statInstance.computationDetails.busyComputing = false;
// Only the active stat instance depeneds on the effects
if (!statInstance.overridden){
statInstance.dependencies = union(statInstance.dependencies, effectDeps);
}
});
}

View File

@@ -1,4 +1,5 @@
import evaluateCalculation from '/imports/api/creature/computation/engine/evaluateCalculation.js';
import applyToggles from '/imports/api/creature/computation/engine/applyToggles.js';
import { union } from 'lodash';
export default function computeToggle(toggle, memo){
@@ -16,6 +17,9 @@ export default function computeToggle(toggle, memo){
// Before doing any work, mark this toggle as busy
toggle.computationDetails.busyComputing = true;
// Apply any parent toggles
applyToggles(toggle, memo);
// Do work
delete toggle.errors;
if (toggle.enabled){
@@ -41,6 +45,11 @@ export default function computeToggle(toggle, memo){
toggle.errors = context.errors;
}
}
if (!toggle.toggleResult){
toggle.inactive = true;
toggle.deactivatedBySelf = true;
toggle.deactivatedByToggle = true;
}
toggle.computationDetails.computed = true;
toggle.computationDetails.busyComputing = false;
}

View File

@@ -21,6 +21,9 @@ export default function evaluateCalculation({
context,
dependencies,
};
if (typeof string !== 'string'){
string = string.toString();
}
// Parse the string
let calc;
try {
@@ -68,8 +71,8 @@ function replaceConstants({calc, memo, prop, dependencies, context}){
} else if (node.name === '#constant'){
constant = findAncestorByType({type: 'constant', prop, memo});
}
// replace constants that aren't overridden by stats
if (constant && !stat){
// replace constants that aren't overridden by stats or disabled by a toggle
if (constant && !constant.deactivatedByToggle && !stat){
dependencies = union(dependencies, [
constant._id,
...constant.dependencies
@@ -119,10 +122,14 @@ function computeSymbols({calc, memo, prop, dependencies}){
computeStat(stat, memo);
}
if (stat){
dependencies = union(dependencies, [
stat._id || node.name,
...stat.dependencies
]);
if (stat.dependencies){
dependencies = union(dependencies, [
stat._id || node.name,
...stat.dependencies
]);
} else {
dependencies = union(dependencies, [stat._id || node.name]);
}
}
}
});

View File

@@ -1,15 +1,6 @@
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
export default function getComputationProperties(creatureId){
// find ids of all toggles that have conditions, even if they are inactive
let toggleIds = CreatureProperties.find({
'ancestors.id': creatureId,
type: 'toggle',
removed: {$ne: true},
condition: { $exists: true },
}, {
fields: {_id: 1},
}).map(t => t._id);
// Find all the relevant properties
return CreatureProperties.find({
'ancestors.id': creatureId,
@@ -17,17 +8,15 @@ export default function getComputationProperties(creatureId){
$or: [
// All active properties
{inactive: {$ne: true}},
// All active and inactive toggles with conditions
// Same as {$in: toggleIds}, but should be slightly faster
{type: 'toggle', condition: { $exists: true }},
// All decendents of the above toggles
{'ancestors.id': {$in: toggleIds}},
// Unless they were deactivated because of a toggle
{deactivatedByToggle: true},
]
}, {
// Filter out fields never used by calculations
fields: {
icon: 0,
},
// Obey tree order
sort: {
order: 1,
}

View File

@@ -8,23 +8,26 @@ export default function writeAlteredProperties(memo){
// Loop through all properties on the memo
forOwn(memo.propsById, changed => {
let schema = propertySchemasIndex[changed.type];
if (!schema) return;
let extraIds = changed.computationDetails.idsOfSameName;
let ids;
if (extraIds && extraIds.length){
ids = [changed._id, ...extraIds];
} else {
ids = [changed._id];
if (!schema){
console.warn('No schema for ' + changed.type);
return;
}
let id = changed._id;
let op = undefined;
let original = memo.originalPropsById[id];
let keys = [
'dependencies',
'inactive',
'deactivatedBySelf',
'deactivatedByAncestor',
'deactivatedByToggle',
'damage',
...schema.objectKeys(),
];
op = addChangedKeysToOp(op, keys, original, changed);
if (op){
bulkWriteOperations.push(op);
}
ids.forEach(id => {
let op = undefined;
let original = memo.originalPropsById[id];
let keys = ['dependencies', ...schema.objectKeys()];
op = addChangedKeysToOp(op, keys, original, changed);
if (op){
bulkWriteOperations.push(op);
}
});
});
writePropertiesSequentially(bulkWriteOperations);
}

View File

@@ -4,36 +4,46 @@ import VERSION from '/imports/constants/VERSION.js';
export default function writeCreatureVariables(memo, creatureId, fullRecompute = true) {
const fields = [
'name',
'attributeType',
'baseValue',
'spellSlotLevelValue',
'damage',
'decimal',
'reset',
'resetMultiplier',
'value',
'currentValue',
'modifier',
'ability',
'skillType',
'baseProficiency',
'abilityMod',
'advantage',
'passiveBonus',
'proficiency',
'attributeType',
'baseProficiency',
'baseValue',
'calculation',
'conditionalBenefits',
'rollBonuses',
'currentValue',
'damage',
'decimal',
'fail',
'level',
'modifier',
'name',
'passiveBonus',
'proficiency',
'reset',
'resetMultiplier',
'rollBonuses',
'skillType',
'spellSlotLevelValue',
'type',
'value',
];
if (fullRecompute){
memo.creatureVariables = {};
forOwn(memo.statsByVariableName, (stat, variableName) => {
// Don't save context variables
if (variableName[0] === '#') return;
let condensedStat = pick(stat, fields);
memo.creatureVariables[variableName] = condensedStat;
});
forOwn(memo.constantsByVariableName, (stat, variableName) => {
let condensedStat = pick(stat, fields);
if (!memo.creatureVariables[variableName]){
memo.creatureVariables[variableName] = condensedStat;
}
});
Creatures.update(creatureId, {$set: {
variables: memo.creatureVariables,
computeVersion: VERSION,

View File

@@ -89,7 +89,6 @@ export function recomputeCreatureByDoc(creature){
writeCreatureVariables(computationMemo, creatureId);
recomputeDamageMultipliersById(creatureId);
recomputeSlotFullness(creatureId);
recomputeInactiveProperties(creatureId);
return computationMemo;
}

View File

@@ -50,6 +50,13 @@ let CreaturePropertySchema = new SimpleSchema({
optional: true,
index: 1,
},
// Denormalised flag if this property was made inactive because of a toggle
// calculation. Either an ancestor toggle calculation or its own.
deactivatedByToggle: {
type: Boolean,
optional: true,
index: 1,
},
// Denormalised list of all properties or creatures this property depends on
dependencies: {
type: Array,
@@ -58,7 +65,6 @@ let CreaturePropertySchema = new SimpleSchema({
},
'dependencies.$': {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
});

View File

@@ -4,7 +4,8 @@ import SimpleSchema from 'simpl-schema';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import { recomputePropertyDependencies } from '/imports/api/creature/computation/methods/recomputeCreature.js';
import recomputeInventory from '/imports/api/creature/denormalise/recomputeInventory.js';
import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js';
const adjustQuantity = new ValidatedMethod({
name: 'creatureProperties.adjustQuantity',
@@ -30,8 +31,10 @@ const adjustQuantity = new ValidatedMethod({
// Do work
adjustQuantityWork({property, operation, value});
// Changing quantity does not change dependencies, recompute deps
recomputePropertyDependencies(property);
// Changing quantity does not change dependencies, but recomputing the
// inventory changes many deps at once, so recompute fully
recomputeCreatureByDoc(rootCreature);
recomputeInventory(rootCreature._id);
},
});

View File

@@ -50,7 +50,7 @@ const damagePropertiesByName = new ValidatedMethod({
damagePropertyWork({property, operation, value});
lastProperty = property;
});
recomputePropertyDependencies(lastProperty);
if (lastProperty) recomputePropertyDependencies(lastProperty);
}
});

View File

@@ -24,6 +24,9 @@ const damageProperty = new ValidatedMethod({
run({_id, operation, value}) {
// Check permissions
let property = CreatureProperties.findOne(_id);
if (!property) throw new Meteor.Error(
'Damage property failed', 'Property doesn\'t exist'
);
let rootCreature = getRootCreatureAncestor(property);
assertEditPermission(rootCreature, this.userId);
// Check if property can take damage
@@ -34,9 +37,10 @@ const damageProperty = new ValidatedMethod({
`Property of type "${property.type}" can't be damaged`
);
}
damagePropertyWork({property, operation, value});
let result = damagePropertyWork({property, operation, value});
// Dependencies can't be changed through damage, only recompute deps
recomputePropertyDependencies(property);
return result;
},
});

View File

@@ -2,9 +2,24 @@ import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import { insertPropertyWork } from '/imports/api/creature/creatureProperties/methods/insertProperty.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import {
setLineageOfDocs,
renewDocIds
} from '/imports/api/parenting/parenting.js';
import recomputeInactiveProperties from '/imports/api/creature/denormalise/recomputeInactiveProperties.js';
import { reorderDocs } from '/imports/api/parenting/order.js';
import recomputeInventory from '/imports/api/creature/denormalise/recomputeInventory.js';
import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js';
var snackbar;
if (Meteor.isClient){
snackbar = require(
'/imports/ui/components/snackbars/SnackbarQueue.js'
).snackbar
}
const DUPLICATE_CHILDREN_LIMIT = 50;
const duplicateProperty = new ValidatedMethod({
name: 'creatureProperties.duplicate',
@@ -20,13 +35,70 @@ const duplicateProperty = new ValidatedMethod({
timeInterval: 5000,
},
run({_id}) {
let creatureProperty = CreatureProperties.findOne(_id);
let rootCreature = getRootCreatureAncestor(creatureProperty);
assertEditPermission(rootCreature, this.userId);
insertPropertyWork({
property: creatureProperty,
creature: rootCreature,
let property = CreatureProperties.findOne(_id);
let creature = getRootCreatureAncestor(property);
assertEditPermission(creature, this.userId);
// Renew the doc ID
let randomSrc = DDP.randomStream('duplicateProperty');
let propertyId = randomSrc.id();
property._id = propertyId;
// Get all the descendants
let nodes = CreatureProperties.find({
'ancestors.id': _id,
removed: {$ne: true},
}, {
limit: DUPLICATE_CHILDREN_LIMIT + 1,
sort: {order: 1},
}).fetch();
// Alert the user if the limit was hit
if (nodes.length > DUPLICATE_CHILDREN_LIMIT){
nodes.pop();
if (Meteor.isClient){
snackbar({
text: `Only the first ${DUPLICATE_CHILDREN_LIMIT} children were duplicated`,
});
}
}
// re-map all the ancestors
setLineageOfDocs({
docArray: nodes,
newAncestry : [
...property.ancestors,
{id: propertyId, collection: 'creatureProperties'}
],
oldParent : {id: _id, collection: 'creatureProperties'},
});
// Give the docs new IDs without breaking internal references
renewDocIds({docArray: nodes});
// Order the root node
property.order += 0.5;
// Insert the properties
CreatureProperties.batchInsert([property, ...nodes]);
// Tree structure changed by inserts, reorder the tree
reorderDocs({
collection: CreatureProperties,
ancestorId: property.ancestors[0].id,
});
// Inserting the active status of the property needs to be denormalised
recomputeInactiveProperties(creature._id);
// Recompute the inventory
recomputeInventory(creature._id);
// Inserting a creature property invalidates dependencies: full recompute
recomputeCreatureByDoc(creature);
return propertyId;
},
});

View File

@@ -4,23 +4,11 @@ import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import { organizeDoc } from '/imports/api/parenting/organizeMethods.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import INVENTORY_TAGS from '/imports/constants/INVENTORY_TAGS.js';
export function getParentRefByTag(creatureId, tag){
let prop = CreatureProperties.findOne({
'ancestors.id': creatureId,
removed: {$ne: true},
inactive: {$ne: true},
tags: tag,
}, {
sort: {order: 1},
});
if (prop){
return {id: prop._id, collection: 'creatureProperties'};
} else {
return {id: creatureId, collection: 'creatures'};
}
}
import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js';
import recomputeInactiveProperties from '/imports/api/creature/denormalise/recomputeInactiveProperties.js';
import recomputeInventory from '/imports/api/creature/denormalise/recomputeInventory.js';
import BUILT_IN_TAGS from '/imports/constants/BUILT_IN_TAGS.js';
import getParentRefByTag from '/imports/api/creature/creatureProperties/methods/getParentRefByTag.js';
// Equipping or unequipping an item will also change its parent
const equipItem = new ValidatedMethod({
@@ -47,9 +35,10 @@ const equipItem = new ValidatedMethod({
}, {
selector: {type: 'item'},
});
let tag = equipped ? INVENTORY_TAGS.equipment : INVENTORY_TAGS.carried;
let tag = equipped ? BUILT_IN_TAGS.equipment : BUILT_IN_TAGS.carried;
let parentRef = getParentRefByTag(creature._id, tag);
// organizeDoc handles recompuation
if (!parentRef) parentRef = {id: creature._id, collection: 'creatures'};
organizeDoc.call({
docRef: {
id: _id,
@@ -57,7 +46,12 @@ const equipItem = new ValidatedMethod({
},
parentRef,
order: Number.MAX_SAFE_INTEGER,
skipRecompute: true,
});
recomputeInactiveProperties(creature._id);
recomputeInventory(creature._id);
recomputeCreatureByDoc(creature);
},
});

View File

@@ -0,0 +1,13 @@
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
export default function getParentRefByTag(creatureId, tag){
let prop = CreatureProperties.findOne({
'ancestors.id': creatureId,
removed: {$ne: true},
inactive: {$ne: true},
tags: tag,
}, {
sort: {order: 1},
});
return prop && {id: prop._id, collection: 'creatureProperties'};
}

View File

@@ -1,4 +1,4 @@
import '//imports/api/creature/creatureProperties/methods/adjustQuantity.js';
import '/imports/api/creature/creatureProperties/methods/adjustQuantity.js';
import '/imports/api/creature/creatureProperties/methods/damagePropertiesByName.js';
import '/imports/api/creature/creatureProperties/methods/damageProperty.js';
import '/imports/api/creature/creatureProperties/methods/dealDamage.js';

View File

@@ -2,22 +2,129 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import SimpleSchema from 'simpl-schema';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import { reorderDocs } from '/imports/api/parenting/order.js';
import recomputeInactiveProperties from '/imports/api/creature/denormalise/recomputeInactiveProperties.js';
import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js';
import recomputeInventory from '/imports/api/creature/denormalise/recomputeInventory.js';
import { getAncestry } from '/imports/api/parenting/parenting.js';
import getParentRefByTag from '/imports/api/creature/creatureProperties/methods/getParentRefByTag.js';
import { RefSchema } from '/imports/api/parenting/ChildSchema.js';
import { getHighestOrder } from '/imports/api/parenting/order.js';
const insertProperty = new ValidatedMethod({
name: 'creatureProperties.insert',
validate: null,
validate: new SimpleSchema({
creatureProperty: {
type: Object,
blackbox: true,
},
parentRef: RefSchema,
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({creatureProperty}) {
let rootCreature = getRootCreatureAncestor(creatureProperty);
run({creatureProperty, parentRef}) {
// get the new ancestry for the properties
let {parentDoc, ancestors} = getAncestry({parentRef});
// Check permission to edit
let rootCreature;
if (parentRef.collection === 'creatures'){
rootCreature = parentDoc;
} else if (parentRef.collection === 'creatureProperties'){
rootCreature = getRootCreatureAncestor(parentDoc);
} else {
throw `${parentRef.collection} is not a valid parent collection`
}
assertEditPermission(rootCreature, this.userId);
creatureProperty.parent = parentRef;
creatureProperty.ancestors = ancestors;
return insertPropertyWork({
property: creatureProperty,
creature: rootCreature,
});
},
});
const insertPropertyAsChildOfTag = new ValidatedMethod({
name: 'creatureProperties.insertAsChildOfTag',
validate: new SimpleSchema({
creatureProperty: {
type: Object,
blackbox: true,
},
creatureId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
tag: {
type: String,
max: 20,
},
tagDefaultName: {
type: String,
max: 20,
optional: true,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({creatureProperty, creatureId, tag, tagDefaultName}) {
let parentRef = getParentRefByTag(creatureId, tag);
if (!parentRef){
// Use the creature as the parent and mark that we need to insert the folder first later
var insertFolderFirst = true;
parentRef = {id: creatureId, collection: 'creatures'};
}
// get the new ancestry for the properties
let {parentDoc, ancestors} = getAncestry({parentRef});
// Check permission to edit
let rootCreature;
if (parentRef.collection === 'creatures'){
rootCreature = parentDoc;
} else if (parentRef.collection === 'creatureProperties'){
rootCreature = getRootCreatureAncestor(parentDoc);
} else {
throw `${parentRef.collection} is not a valid parent collection`
}
assertEditPermission(rootCreature, this.userId);
// Add the folder first if we need to
if (insertFolderFirst){
let order = getHighestOrder({
collection: CreatureProperties,
ancestorId: parentRef.id,
}) + 1;
let id = CreatureProperties.insert({
type: 'folder',
name: tagDefaultName || (tag.charAt(0).toUpperCase() + tag.slice(1)),
tags: [tag],
parent: parentRef,
ancestors: [parentRef],
order,
});
// Make the folder our new parent
let newParentRef = {id, collection: 'creatureProperties'};
ancestors = [parentRef, newParentRef];
parentRef = newParentRef;
creatureProperty.order = order + 1;
}
creatureProperty.parent = parentRef;
creatureProperty.ancestors = ancestors;
return insertPropertyWork({
property: creatureProperty,
creature: rootCreature,
@@ -35,9 +142,15 @@ export function insertPropertyWork({property, creature}){
});
// Inserting the active status of the property needs to be denormalised
recomputeInactiveProperties(creature._id);
// Recompute the inventory if it has changed
if (property.type === 'item' || property.type === 'container'){
recomputeInventory(creature._id);
}
// Inserting a creature property invalidates dependencies: full recompute
recomputeCreatureByDoc(creature);
return _id;
}
export default insertProperty;
export { insertPropertyAsChildOfTag };

View File

@@ -15,6 +15,8 @@ import {
} from '/imports/api/parenting/parenting.js';
import { reorderDocs } from '/imports/api/parenting/order.js';
import { setDocToLastOrder } from '/imports/api/parenting/order.js';
import recomputeInventory from '/imports/api/creature/denormalise/recomputeInventory.js';
import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
const insertPropertyFromLibraryNode = new ValidatedMethod({
name: 'creatureProperties.insertPropertyFromLibraryNode',
@@ -26,13 +28,17 @@ const insertPropertyFromLibraryNode = new ValidatedMethod({
parentRef: {
type: RefSchema,
},
order: {
type: Number,
optional: true,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({nodeId, parentRef}) {
run({nodeId, parentRef, order}) {
// get the new ancestry for the properties
let {parentDoc, ancestors} = getAncestry({parentRef});
@@ -49,6 +55,7 @@ const insertPropertyFromLibraryNode = new ValidatedMethod({
// Fetch the library node and its decendents, provided they have not been
// removed
// TODO: Check permission to read the library this node is in
let node = LibraryNodes.findOne({
_id: nodeId,
removed: {$ne: true},
@@ -60,6 +67,9 @@ const insertPropertyFromLibraryNode = new ValidatedMethod({
removed: {$ne: true},
}).fetch();
// Convert all references into actual nodes
nodes = reifyNodeReferences(nodes);
// The root node is first in the array of nodes
// It must get the first generated ID to prevent flickering
nodes = [node, ...nodes];
@@ -78,10 +88,14 @@ const insertPropertyFromLibraryNode = new ValidatedMethod({
});
// Order the root node
setDocToLastOrder({
collection: CreatureProperties,
doc: node,
});
if (order === undefined){
setDocToLastOrder({
collection: CreatureProperties,
doc: node,
});
} else {
node.order = order;
}
// Insert the creature properties
CreatureProperties.batchInsert(nodes);
@@ -97,6 +111,8 @@ const insertPropertyFromLibraryNode = new ValidatedMethod({
// The library properties need to denormalise which of them are inactive
recomputeInactiveProperties(rootId);
// Some of the library properties may be items or containers
recomputeInventory(rootCreature._id);
// Inserting a creature property invalidates dependencies: full recompute
recomputeCreatureByDoc(rootCreature);
// Return the docId of the last property, the inserted root property
@@ -104,4 +120,95 @@ const insertPropertyFromLibraryNode = new ValidatedMethod({
},
});
// Covert node references into actual nodes
// TODO: check permissions for each library a reference node references
function reifyNodeReferences(nodes, visitedRefs = new Set(), depth = 0){
depth += 1;
// New nodes added this function
let newNodes = [];
// Filter out the reference nodes we replace
let resultingNodes = nodes.filter(node => {
// We have already visited this ref and replaced it
if (visitedRefs.has(node._id)) return false;
// Already replaced an ancestor node
for (let i; i < node.ancestors.length; i++){
if (visitedRefs.has(node.ancestors[i].id)) return false;
}
// This isn't a reference node, continue as normal
if (node.type !== 'reference') return true;
// We have gone too deep, keep the reference node as an error
if (depth > 10){
if (Meteor.isClient) console.warn('Reference depth limit exceeded');
node.cache = {error: 'Reference depth limit exceeded'};
return true;
}
let referencedNode
try {
referencedNode = fetchDocByRef(node.ref);
referencedNode.order = node.order;
// We are definitely replacing this node, so add it to the list
visitedRefs.add(node._id);
} catch (e){
node.cache = {error: e.reason || e.message || e.toString()};
return true;
}
// Get all the descendants of the referenced node
let descendents = LibraryNodes.find({
'ancestors.id': referencedNode._id,
removed: {$ne: true},
}, {
sort: {order: 1},
}).fetch();
// We are adding the referenced node and its descendants
let addedNodes = [referencedNode, ...descendents];
// re-map all the ancestors to parent the new sub-tree into our existing
// node tree
setLineageOfDocs({
docArray: addedNodes,
newAncestry: node.ancestors,
oldParent: referencedNode.parent,
});
// Remove all the looped references and descendents from the new nodes
// We can't rely on the reify recursion to do this, since the IDs are
// getting renewed before it is called
addedNodes = addedNodes.filter(node => {
// Exclude removed referenced
if (visitedRefs.has(node._id)) return false;
// Exclude descendants of removed references
for (let i; i < node.ancestors.length; i++){
if (visitedRefs.has(node.ancestors[i].id)) return false;
}
return true;
});
// Give the new referenced sub-tree new ids
renewDocIds({
docArray: addedNodes,
});
// Reify the subtree as well with recursion
addedNodes = reifyNodeReferences(addedNodes, visitedRefs, depth);
// Store the new nodes from this inner loop without altering the array
// we are looping over
newNodes.push(...addedNodes);
});
// We are done filtering the array, we can add the new nodes to it
resultingNodes.push(...newNodes);
return resultingNodes;
}
export default insertPropertyFromLibraryNode;

View File

@@ -7,6 +7,7 @@ import { restore } from '/imports/api/parenting/softRemove.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import recomputeInactiveProperties from '/imports/api/creature/denormalise/recomputeInactiveProperties.js';
import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js';
import recomputeInventory from '/imports/api/creature/denormalise/recomputeInventory.js';
const restoreProperty = new ValidatedMethod({
name: 'creatureProperties.restore',
@@ -27,6 +28,8 @@ const restoreProperty = new ValidatedMethod({
// Do work
restore({_id, collection: CreatureProperties});
// Items and containers might be restored
recomputeInventory(rootCreature._id);
// Parents active status may have changed while it was deleted
recomputeInactiveProperties(rootCreature._id);
// Changes dependency tree by restoring children

View File

@@ -6,6 +6,7 @@ import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js
import { softRemove } from '/imports/api/parenting/softRemove.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js';
import recomputeInventory from '/imports/api/creature/denormalise/recomputeInventory.js';
const softRemoveProperty = new ValidatedMethod({
name: 'creatureProperties.softRemove',
@@ -26,6 +27,8 @@ const softRemoveProperty = new ValidatedMethod({
// Do work
softRemove({_id, collection: CreatureProperties});
// Potentially changes items and containers
recomputeInventory(rootCreature._id);
// Changes dependency tree by removing children
recomputeCreatureByDoc(rootCreature);
}

View File

@@ -3,8 +3,9 @@ import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js';
import { recomputeCreatureById } from '/imports/api/creature/computation/methods/recomputeCreature.js';
import recomputeInactiveProperties from '/imports/api/creature/denormalise/recomputeInactiveProperties.js';
import recomputeInventory from '/imports/api/creature/denormalise/recomputeInventory.js';
const updateCreatureProperty = new ValidatedMethod({
name: 'creatureProperties.update',
@@ -52,8 +53,14 @@ const updateCreatureProperty = new ValidatedMethod({
].includes(path[0])){
recomputeInactiveProperties(rootCreature._id);
}
if (property.type === 'item' || property.type === 'container'){
// Potentially changes items and containers
recomputeInventory(rootCreature._id);
}
// Updating a property is likely to change dependencies, do a full recompute
recomputeCreatureByDoc(rootCreature);
// denormalised stats might change, so fetch the creature again
recomputeCreatureById(rootCreature._id);
},
});

View File

@@ -0,0 +1,47 @@
import BUILT_IN_TAGS from '/imports/constants/BUILT_IN_TAGS.js';
export default function defaultCharacterProperties(creatureId){
if (!creatureId) throw 'creatureId is required';
const creatureRef = {collection: 'creatures', id: creatureId};
let randomSrc = DDP.randomStream('defaultProperties');
const inventoryId = randomSrc.id();
const inventoryRef = {collection: 'creatureProperties', id: inventoryId};
return [
{
type: 'propertySlot',
name: 'Ruleset',
description: 'Choose a starting point for your character, this will define the basic setup of your character sheet. Without a base, your sheet will be empty.',
slotTags: ['base'],
tags: [],
quantityExpected: 1,
hideWhenFull: true,
spaceLeft: 1,
totalFilled: 0,
order: 0,
parent: creatureRef,
ancestors: [creatureRef],
}, {
_id: inventoryId,
type: 'folder',
name: 'Inventory',
tags: [BUILT_IN_TAGS.inventory],
order: 1,
parent: creatureRef,
ancestors: [creatureRef],
}, {
type: 'folder',
name: 'Equipment',
tags: [BUILT_IN_TAGS.equipment],
order: 2,
parent: inventoryRef,
ancestors: [creatureRef, inventoryRef],
}, {
type: 'folder',
name: 'Carried',
tags: [BUILT_IN_TAGS.carried],
order: 3,
parent: inventoryRef,
ancestors: [creatureRef, inventoryRef],
},
];
}

View File

@@ -7,7 +7,6 @@ export default function recomputeInactiveProperties(ancestorId){
{disabled: true}, // Everything can be disabled
{type: 'buff', applied: false}, // Buffs can be applied
{type: 'item', equipped: {$ne: true}},
{type: 'toggle', toggleResult: false},
{type: 'spell', prepared: {$ne: true}, alwaysPrepared: {$ne: true}},
],
};
@@ -56,14 +55,18 @@ export default function recomputeInactiveProperties(ancestorId){
CreatureProperties.update({
'ancestors.id': {$eq: ancestorId, $nin: disabledIds},
'_id': {$nin: disabledIds},
// if it was a toggle responsible, we leave it alone
deactivatedByToggle: {$ne: true},
$or: [
{inactive: true},
{deactivatedByAncestor: true},
{deactivatedBySelf: true}
],
}, {
$unset: {
inactive: 1,
deactivatedByAncestor: 1,
deactivatedBySelf: 1,
},
}, {
multi: true,

View File

@@ -1,5 +1,6 @@
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import nodesToTree from '/imports/api/parenting/parenting.js';
import Creatures from '/imports/api/creature/Creatures.js';
import { nodesToTree } from '/imports/api/parenting/parenting.js';
export default function recomputeInventory(creatureId){
let inventoryForest = nodesToTree({
@@ -10,27 +11,27 @@ export default function recomputeInventory(creatureId){
},
deactivatedByAncestor: {$ne: true},
});
return getChildrenInventoryData(inventoryForest);
}
function getChildrenInventoryData(forest){
let data = {
weightTotal: 0,
weightEquipment: 0,
weightCarried: 0,
valueTotal: 0,
valueEquipment: 0,
valueCarried: 0,
}
forest.forEach(tree => {
let treeData = getInventoryData(tree);
for (let key in data){
data[key] += treeData[key];
}
let containersToWrite = [];
let data = getChildrenInventoryData(inventoryForest, containersToWrite);
containersToWrite.forEach(container => {
CreatureProperties.update(container._id, {$set: {
contentsWeight: container.contentsWeight,
contentsValue: container.contentsValue,
}}, {selector: {type: 'container'}});
});
Creatures.update(creatureId, {$set: {
'denormalizedStats.weightTotal': data.weightTotal,
'denormalizedStats.weightEquipment': data.weightEquipment,
'denormalizedStats.weightCarried': data.weightCarried,
'denormalizedStats.valueTotal': data.valueTotal,
'denormalizedStats.valueEquipment': data.valueEquipment,
'denormalizedStats.valueCarried': data.valueCarried,
'denormalizedStats.itemsAttuned': data.itemsAttuned,
}});
return data;
}
function getInventoryData(tree){
function getChildrenInventoryData(forest, containersToWrite){
let data = {
weightTotal: 0,
weightEquipment: 0,
@@ -40,24 +41,41 @@ function getInventoryData(tree){
valueCarried: 0,
itemsAttuned: 0,
}
let childData = getChildrenInventoryData(tree.children);
forest.forEach(tree => {
let treeData = getInventoryData(tree, containersToWrite);
for (let key in data){
data[key] += treeData[key] || 0;
}
});
return data;
}
function getInventoryData(tree, containersToWrite){
let data = {
weightTotal: 0,
weightEquipment: 0,
weightCarried: 0,
valueTotal: 0,
valueEquipment: 0,
valueCarried: 0,
itemsAttuned: 0,
}
let childData = getChildrenInventoryData(tree.children, containersToWrite);
let node = tree.node;
if (node.type === 'container'){
data.weightTotal += node.weight;
data.valueTotal += node.value;
if (node.carried){
data.weightCarried += node.weight;
data.valueCarried += node.valueCarried;
}
storeContentsData(node, childData);
data.weightTotal += node.weight || 0;
data.valueTotal += node.value || 0;
data.weightCarried += node.weight || 0;
data.valueCarried += node.value || 0;
storeContentsData(node, childData, containersToWrite);
} else if (node.type === 'item'){
data.weightTotal += node.weight * node.quantity;
data.valueTotal += node.value * node.quantity;
data.weightCarried += node.weight * node.quantity;
data.valueCarried += node.valueCarried * node.quantity;
data.weightTotal += (node.weight * node.quantity) || 0;
data.valueTotal += (node.value * node.quantity) || 0;
data.weightCarried += (node.weight * node.quantity) || 0;
data.valueCarried += (node.value * node.quantity) || 0;
if (node.equipped){
data.weightEquipment += node.weight * node.quantity;
data.valueEquipment += node.valueCarried * node.quantity;
data.weightEquipment += (node.weight * node.quantity) || 0;
data.valueEquipment += (node.value * node.quantity) || 0;
}
if (node.attuned){
data.itemsAttuned += 1;
@@ -66,16 +84,18 @@ function getInventoryData(tree){
for (let key in data){
data[key] += childData[key];
}
if (node.contentsWeightless){
data.weightCarried = node.weight;
}
if (node.carried === false){
data.weightCarried = 0;
data.valueCarried = 0;
}
return data
}
function storeContentsData(node, childData){
let newContentsWeight;
if (node.contentsWeightless){
newContentsWeight = 0;
} else {
newContentsWeight = childData.weightCarried
}
function storeContentsData(node, childData, containersToWrite){
let newContentsWeight = childData.weightCarried
if (node.contentsWeight !== newContentsWeight){
node.contentsWeight = newContentsWeight;
node.contentsWeightChanged = true;
@@ -85,4 +105,7 @@ function storeContentsData(node, childData){
node.contentsValue = newContentsValue;
node.contentsValueChanged = true;
}
if (node.contentsWeightChanged || node.contentsValueChanged){
containersToWrite.push(node);
}
}

View File

@@ -23,10 +23,14 @@ export default function recomputeSlotFullness(ancestorId){
}
});
let spaceLeft;
if (slot.quantityExpected === 0){
let expected = slot.quantityExpectedResult;
if (typeof expected !== 'number'){
expected = 1;
}
if (expected === 0){
spaceLeft = null;
} else {
spaceLeft = slot.quantityExpected - totalFilled;
spaceLeft = expected - totalFilled;
}
if (slot.totalFilled !== totalFilled || slot.spaceLeft !== spaceLeft){
CreatureProperties.update(slot._id, {

View File

@@ -58,6 +58,7 @@ function removeOldLogs(creatureId){
sort: {date: -1},
skip: PER_CREATURE_LOG_LIMIT,
});
if (!firstExpiredLog) return;
// Remove all logs older than the one over the limit
CreatureLogs.remove({
creatureId,
@@ -69,32 +70,10 @@ function logToMessageData(log){
let embed = {
fields: [],
};
log.content.forEach(c => {
let field = {};
let descriptionField = {};
if (c.name) field.name = c.name;
let valueArray = [];
if (c.error) valueArray.push(`*${c.error}*`);
if (c.resultPrefix) valueArray.push(`${c.resultPrefix}`);
if (c.result) valueArray.push(`\`${c.result}\``);
if (c.details) valueArray.push(c.details);
if (valueArray.length) field.value = valueArray.join(' ');
if (c.description){
if (!field.value){
field.value = c.description;
} else {
descriptionField.value = c.description;
}
}
if (field.name || field.value){
if (!field.name) field.name = '\u200b';
if (!field.value) field.value = '\u200b';
embed.fields.push(field);
}
if (descriptionField.value){
descriptionField.name = '\u200b';
embed.fields.push(descriptionField);
}
log.content.forEach(field => {
if (!field.name) field.name = '\u200b';
if (!field.value) field.value = '\u200b';
embed.fields.push(field);
});
return { embeds: [embed] };
}
@@ -109,7 +88,7 @@ function logWebhook({log, creature}){
}
const insertCreatureLog = new ValidatedMethod({
name: 'creatureLogs.methods.insertCreatureLog',
name: 'creatureLogs.methods.insert',
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
@@ -138,7 +117,7 @@ const insertCreatureLog = new ValidatedMethod({
export function insertCreatureLogWork({log, creature, method}){
// Build the new log
if (typeof log === 'string'){
log = {text: log};
log = {content: [{value: log}]};
}
log.date = new Date();
// Insert it
@@ -190,30 +169,30 @@ const logRoll = new ValidatedMethod({
parsedResult = parse(roll);
} catch (e){
let error = prettifyParseError(e);
logContent.push({error});
logContent.push({name: 'Parse Error', value: error});
}
if (parsedResult) try {
let rollContext = new CompilationContext();
let compiled = parsedResult.compile(creature.variables, rollContext);
let compiledString = compiled.toString();
if (!equalIgnoringWhitespace(compiledString, roll)) logContent.push({
details: roll
value: roll
});
logContent.push({
details: compiledString
value: compiledString
});
let rolled = compiled.roll(creature.variables, rollContext);
let rolledString = rolled.toString();
if (rolledString !== compiledString) logContent.push({
result: rolled.toString()
value: rolled.toString()
});
let result = rolled.reduce(creature.variables, rollContext);
let resultString = result.toString();
if (resultString !== rolledString) logContent.push({
result: resultString
value: resultString
});
} catch (e){
logContent = [{error: 'Calculation error'}];
logContent = [{name: 'Calculation error'}];
}
const log = {
content: logContent,

View File

@@ -3,31 +3,14 @@ import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js';
import RollDetailsSchema from '/imports/api/properties/subSchemas/RollDetailsSchema.js';
let LogContentSchema = new SimpleSchema({
// The name of the field, included in discord webhook message
name: {
type: String,
optional: true,
},
error: {
type: String,
optional: true,
},
resultPrefix: {
type: String,
optional: true,
},
result: {
type: String,
optional: true,
},
expandedResult: {
type: String,
optional: true,
},
details: {
type: String,
optional: true,
},
description: {
// The details of the field, included in discord webhook message
// Markdown support
value: {
type: String,
optional: true,
},

View File

@@ -28,10 +28,14 @@ const removeCreature = new ValidatedMethod({
},
run({charId}) {
assertOwnership(charId, this.userId)
Creatures.remove(charId);
this.unblock();
removeRelatedDocuments(charId);
this.unblock();
removeCreatureWork(charId)
},
});
export function removeCreatureWork(creatureId){
Creatures.remove(creatureId);
removeRelatedDocuments(creatureId);
}
export default removeCreature;

View File

@@ -0,0 +1,12 @@
import { fetch } from 'meteor/fetch'
export default function importCharacter(url){
// Using v1's JSON API to fetch the character data in a usable format
// url -> https://dicecloud.com/character/<id>/json?key=<key>
fetch(url)
.then(response => response.json())
.then(data => {
let character = data.characters[0];
console.log(character.name + ' fetched successfuly')
});
}

View File

@@ -118,10 +118,14 @@ const removeLibrary = new ValidatedMethod({
run({_id}){
let library = Libraries.findOne(_id);
assertOwnership(library, this.userId);
Libraries.remove(_id);
this.unblock();
LibraryNodes.remove({'ancestors.id': _id});
this.unblock();
removeLibaryWork(_id)
}
})
});
export function removeLibaryWork(libraryId){
Libraries.remove(libraryId);
LibraryNodes.remove({'ancestors.id': libraryId});
}
export { LibrarySchema, insertLibrary, setLibraryDefault, updateLibraryName, removeLibrary };

View File

@@ -11,6 +11,8 @@ import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js
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 { updateReferenceNodeWork } from '/imports/api/library/methods/updateReferenceNode.js';
let LibraryNodes = new Mongo.Collection('libraryNodes');
@@ -75,31 +77,15 @@ const insertNode = new ValidatedMethod({
run(libraryNode) {
delete libraryNode._id;
assertNodeEditPermission(libraryNode, this.userId);
return LibraryNodes.insert(libraryNode);
let nodeId = LibraryNodes.insert(libraryNode);
if (libraryNode.type == 'reference'){
libraryNode._id = nodeId;
updateReferenceNodeWork(libraryNode, this.userId);
}
return nodeId;
},
});
const duplicateNode = new ValidatedMethod({
name: 'libraryNodes.duplicate',
validate: new SimpleSchema({
_id: {
type: String,
regEx: SimpleSchema.RegEx.Id,
}
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({_id}) {
let libraryNode = LibraryNodes.findOne(_id);
assertNodeEditPermission(libraryNode, this.userId);
delete libraryNode._id;
return LibraryNodes.insert(libraryNode);
},
})
const updateLibraryNode = new ValidatedMethod({
name: 'libraryNodes.update',
validate({_id, path}){
@@ -129,9 +115,14 @@ const updateLibraryNode = new ValidatedMethod({
} else {
modifier = {$set: {[pathString]: value}};
}
return LibraryNodes.update(_id, modifier, {
let numUpdated = LibraryNodes.update(_id, modifier, {
selector: {type: node.type},
});
if (node.type == 'reference'){
node = LibraryNodes.findOne(_id);
updateReferenceNodeWork(node, this.userId);
}
return numUpdated;
},
});
@@ -195,7 +186,6 @@ export default LibraryNodes;
export {
LibraryNodeSchema,
insertNode,
duplicateNode,
updateLibraryNode,
pullFromLibraryNode,
pushToLibraryNode,

View File

@@ -0,0 +1,87 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import SimpleSchema from 'simpl-schema';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
import { assertDocEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import {
setLineageOfDocs,
renewDocIds
} from '/imports/api/parenting/parenting.js';
import { reorderDocs } from '/imports/api/parenting/order.js';
var snackbar;
if (Meteor.isClient){
snackbar = require(
'/imports/ui/components/snackbars/SnackbarQueue.js'
).snackbar
}
const DUPLICATE_CHILDREN_LIMIT = 50;
const duplicateLibraryNode = new ValidatedMethod({
name: 'libraryNodes.duplicate',
validate: new SimpleSchema({
_id: {
type: String,
regEx: SimpleSchema.RegEx.Id,
}
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({_id}) {
let libraryNode = LibraryNodes.findOne(_id);
assertDocEditPermission(libraryNode, this.userId);
let randomSrc = DDP.randomStream('duplicateLibraryNode');
let libraryNodeId = randomSrc.id();
libraryNode._id = libraryNodeId;
let nodes = LibraryNodes.find({
'ancestors.id': _id,
removed: {$ne: true},
}, {
limit: DUPLICATE_CHILDREN_LIMIT + 1,
sort: {order: 1},
}).fetch();
if (nodes.length > DUPLICATE_CHILDREN_LIMIT){
nodes.pop();
if (Meteor.isClient){
snackbar({
text: `Only the first ${DUPLICATE_CHILDREN_LIMIT} children were duplicated`,
});
}
}
// re-map all the ancestors
setLineageOfDocs({
docArray: nodes,
newAncestry : [
...libraryNode.ancestors,
{id: libraryNodeId, collection: 'libraryNodes'}
],
oldParent : {id: _id, collection: 'libraryNodes'},
});
// Give the docs new IDs without breaking internal references
renewDocIds({docArray: nodes});
// Order the root node
libraryNode.order += 0.5;
LibraryNodes.batchInsert([libraryNode, ...nodes]);
// Tree structure changed by inserts, reorder the tree
reorderDocs({
collection: LibraryNodes,
ancestorId: libraryNode.ancestors[0].id,
});
return libraryNodeId;
},
});
export default duplicateLibraryNode;

View File

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

View File

@@ -0,0 +1,67 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import SimpleSchema from 'simpl-schema';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
import {
assertDocEditPermission,
assertViewPermission,
} from '/imports/api/sharing/sharingPermissions.js';
import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
const updateReferenceNode = new ValidatedMethod({
name: 'libraryNodes.updateReferenceNode',
validate: new SimpleSchema({
_id: {
type: String,
regEx: SimpleSchema.RegEx.Id,
}
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({_id}) {
let userId = this.userId;
let node = LibraryNodes.findOne(_id);
assertDocEditPermission(node, userId);
updateReferenceNodeWork(node, userId);
},
});
function writeCache(_id, cache){
LibraryNodes.update(_id, {$set: {cache}}, {
selector: {type: 'reference'},
});
}
function updateReferenceNodeWork(node, userId){
let cache = {}
if (!node.ref){
writeCache(node._id, cache);
return;
}
let doc, library;
try {
doc = fetchDocByRef(node.ref);
if (doc.removed) throw 'Property has been deleted';
if (doc.ancestors[0].id !== node.ancestors[0].id){
library = fetchDocByRef(doc.ancestors[0]);
assertViewPermission(library, userId)
}
} catch(e){
cache = {error: e.reason || e.message || e.toString()}
writeCache(node._id, cache);
return;
}
cache = {
node: {name: doc.name, type: doc.type},
};
if (library){
cache.library = {name: library.name};
}
writeCache(node._id, cache);
}
export default updateReferenceNode;
export { updateReferenceNodeWork }

View File

@@ -4,6 +4,11 @@ const RefSchema = new SimpleSchema({
id: {
type: String,
regEx: SimpleSchema.RegEx.Id,
// TODO: Rather than indexing this field, index `ancestors.0.id` to only
// index the root of the ancestor heirarchy to significantly reduce
// index size and improve performance
// All queries on an ancestor document need to target `ancestors.0.id` first
// before targeting a younger ancestor
index: 1
},
collection: {

View File

@@ -10,7 +10,7 @@ import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
import getCollectionByName from '/imports/api/parenting/getCollectionByName.js';
import { recomputeCreatureById } from '/imports/api/creature/computation/methods/recomputeCreature.js';
import recomputeInactiveProperties from '/imports/api/creature/denormalise/recomputeInactiveProperties.js';
import recomputeInventory from '/imports/api/creature/denormalise/recomputeInventory.js';
const organizeDoc = new ValidatedMethod({
name: 'organize.organizeDoc',
validate: new SimpleSchema({
@@ -20,13 +20,17 @@ const organizeDoc = new ValidatedMethod({
type: Number,
// Should end in 0.5 to place it reliably between two existing documents
},
skipRecompute: {
type: Boolean,
optional: true,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({docRef, parentRef, order}) {
run({docRef, parentRef, order, skipRecompute}) {
let doc = fetchDocByRef(docRef);
let collection = getCollectionByName(docRef.collection);
// The user must be able to edit both the doc and its parent to move it
@@ -52,15 +56,20 @@ const organizeDoc = new ValidatedMethod({
// Figure out which creatures need to be recalculated after this move
let docCreatures = getCreatureAncestors(doc);
let parentCreatures = getCreatureAncestors(parent);
let creaturesToRecompute = union(docCreatures, parentCreatures);
// Recompute the creatures
creaturesToRecompute.forEach(id => {
// The active status of some properties might change due to a change in
// ancestry
recomputeInactiveProperties(id);
// Some Dependencies depend on ancestry, so a full recompute is needed
recomputeCreatureById(id);
});
if (!skipRecompute){
let creaturesToRecompute = union(docCreatures, parentCreatures);
// Recompute the creatures
creaturesToRecompute.forEach(id => {
// The active status of some properties might change due to a change in
// ancestry
recomputeInactiveProperties(id);
if (doc.type === 'container' || doc.type === 'item'){
recomputeInventory(id);
}
// Some Dependencies depend on ancestry, so a full recompute is needed
recomputeCreatureById(id);
});
}
},
});

View File

@@ -33,7 +33,7 @@ const AdjustmentSchema = new SimpleSchema({
const ComputedOnlyAdjustmentSchema = new SimpleSchema({
amountResult: {
type: SimpleSchema.Integer,
type: SimpleSchema.oneOf(String, Number),
optional: true,
},
amountErrors: {

View File

@@ -133,6 +133,11 @@ let ComputedOnlyAttributeSchema = new SimpleSchema({
type: Boolean,
optional: true,
},
// Denormalised tag if stat is overridden by one with the same variable name
overridden: {
type: Boolean,
optional: true,
},
});
const ComputedAttributeSchema = new SimpleSchema()

View File

@@ -29,7 +29,7 @@ const DamageSchema = new SimpleSchema({
const ComputedOnlyDamageSchema = new SimpleSchema({
amountResult: {
type: SimpleSchema.oneOf(String, SimpleSchema.Integer),
type: SimpleSchema.oneOf(String, Number),
optional: true,
},
amountErrors: {

View File

@@ -14,9 +14,10 @@ let ProficiencySchema = new SimpleSchema({
type: String,
},
// A number representing how proficient the character is
// where 0.49 is half rounded down and 0.5 is half rounded up
value: {
type: Number,
allowedValues: [0.5, 1, 2],
allowedValues: [0.49, 0.5, 1, 2],
defaultValue: 1,
},
});

View File

@@ -0,0 +1,47 @@
import SimpleSchema from 'simpl-schema';
let ReferenceSchema = new SimpleSchema({
ref: {
type: Object,
defaultValue: {},
},
'ref.id': {
type: String,
regEx: SimpleSchema.RegEx.Id,
optional: true,
},
'ref.collection': {
type: String,
optional: true,
},
// Denormalised store of referenced property's details
cache: {
type: Object,
defaultValue: {},
},
'cache.error': {
type: String,
optional: true,
},
'cache.node': {
type: Object,
optional: true,
},
'cache.node.name': {
type: String,
optional: true,
},
'cache.node.type': {
type: String,
},
'cache.library': {
type: Object,
optional: true,
},
'cache.library.name': {
type: String,
optional: true,
},
});
export { ReferenceSchema };

View File

@@ -121,6 +121,11 @@ let ComputedOnlySkillSchema = new SimpleSchema({
type: Boolean,
optional: true,
},
// Denormalised tag if stat is overridden by one with the same variable name
overridden: {
type: Boolean,
optional: true,
},
})
const ComputedSkillSchema = new SimpleSchema()

View File

@@ -22,9 +22,9 @@ let SlotSchema = new SimpleSchema({
type: String,
},
quantityExpected: {
type: SimpleSchema.Integer,
defaultValue: 1,
min: 0,
type: String,
optional: true,
defaultValue: '1',
},
ignored: {
type: Boolean,

View File

@@ -23,6 +23,16 @@ let SpellListSchema = new SimpleSchema({
type: String,
optional: true,
},
// Calculation of The attack roll bonus used by spell attacks in this list
attackRollBonus: {
type: String,
optional: true,
},
// Calculation of the save dc used by spells in this list
dc: {
type: String,
optional: true,
},
});
const ComputedOnlySpellListSchema = new SimpleSchema({
@@ -33,6 +43,7 @@ const ComputedOnlySpellListSchema = new SimpleSchema({
},
'descriptionCalculations.$': InlineComputationSchema,
// maxPrepared
maxPreparedResult: {
type: Number,
optional: true,
@@ -44,6 +55,32 @@ const ComputedOnlySpellListSchema = new SimpleSchema({
'maxPreparedErrors.$':{
type: ErrorSchema,
},
// attackRollBonus
attackRollBonusResult: {
type: Number,
optional: true,
},
attackRollBonusErrors: {
type: Array,
optional: true,
},
'attackRollBonusErrors.$':{
type: ErrorSchema,
},
// dc
dcResult: {
type: Number,
optional: true,
},
dcErrors: {
type: Array,
optional: true,
},
'dcErrors.$':{
type: ErrorSchema,
},
});
const ComputedSpellListSchema = new SimpleSchema()

View File

@@ -4,21 +4,23 @@ import { ComputedOnlyAdjustmentSchema } from '/imports/api/properties/Adjustment
import { ComputedOnlyAttackSchema } from '/imports/api/properties/Attacks.js';
import { ComputedOnlyAttributeSchema } from '/imports/api/properties/Attributes.js';
import { ComputedOnlyBuffSchema } from '/imports/api/properties/Buffs.js';
// import { ClassLevelSchema } from '/imports/api/properties/ClassLevels.js';
import { ClassLevelSchema } from '/imports/api/properties/ClassLevels.js';
import { ConstantSchema } from '/imports/api/properties/Constants.js';
import { ComputedOnlyContainerSchema } from '/imports/api/properties/Containers.js';
import { ComputedOnlyDamageSchema } from '/imports/api/properties/Damages.js';
import { DamageMultiplierSchema } from '/imports/api/properties/DamageMultipliers.js';
import { ComputedOnlyEffectSchema } from '/imports/api/properties/Effects.js';
import { ComputedOnlyFeatureSchema } from '/imports/api/properties/Features.js';
// import { FolderSchema } from '/imports/api/properties/Folders.js';
import { FolderSchema } from '/imports/api/properties/Folders.js';
import { ComputedOnlyItemSchema } from '/imports/api/properties/Items.js';
import { ComputedOnlyNoteSchema } from '/imports/api/properties/Notes.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 { ComputedOnlyRollSchema } from '/imports/api/properties/Rolls.js';
import { ComputedOnlySavingThrowSchema } from '/imports/api/properties/SavingThrows.js';
import { ComputedOnlySkillSchema } from '/imports/api/properties/Skills.js';
import { ComputedOnlySlotSchema } from '/imports/api/properties/Slots.js';
// import { SlotFillerSchema } from '/imports/api/properties/SlotFillers.js';
import { SlotFillerSchema } from '/imports/api/properties/SlotFillers.js';
import { ComputedOnlySpellSchema } from '/imports/api/properties/Spells.js';
import { ComputedOnlySpellListSchema } from '/imports/api/properties/SpellLists.js';
import { ComputedOnlyToggleSchema } from '/imports/api/properties/Toggles.js';
@@ -29,23 +31,26 @@ const propertySchemasIndex = {
attack: ComputedOnlyAttackSchema,
attribute: ComputedOnlyAttributeSchema,
buff: ComputedOnlyBuffSchema,
// classLevel: ClassLevelSchema,
classLevel: ClassLevelSchema,
constant: ConstantSchema,
container: ComputedOnlyContainerSchema,
damage: ComputedOnlyDamageSchema,
damageMultiplier: DamageMultiplierSchema,
effect: ComputedOnlyEffectSchema,
feature: ComputedOnlyFeatureSchema,
// folder: FolderSchema,
folder: FolderSchema,
item: ComputedOnlyItemSchema,
note: ComputedOnlyNoteSchema,
// proficiency: ProficiencySchema,
proficiency: ProficiencySchema,
propertySlot: ComputedOnlySlotSchema,
reference: ReferenceSchema,
roll: ComputedOnlyRollSchema,
savingThrow: ComputedOnlySavingThrowSchema,
skill: ComputedOnlySkillSchema,
slotFiller: SlotFillerSchema,
spellList: ComputedOnlySpellListSchema,
spell: ComputedOnlySpellSchema,
toggle: ComputedOnlyToggleSchema,
container: ComputedOnlyContainerSchema,
item: ComputedOnlyItemSchema,
any: new SimpleSchema({}),
};

View File

@@ -15,6 +15,7 @@ import { FolderSchema } from '/imports/api/properties/Folders.js';
import { ComputedItemSchema } from '/imports/api/properties/Items.js';
import { ComputedNoteSchema } from '/imports/api/properties/Notes.js';
import { ProficiencySchema } from '/imports/api/properties/Proficiencies.js';
import { ReferenceSchema } from '/imports/api/properties/References.js';
import { ComputedRollSchema } from '/imports/api/properties/Rolls.js';
import { ComputedSavingThrowSchema } from '/imports/api/properties/SavingThrows.js';
import { ComputedSkillSchema } from '/imports/api/properties/Skills.js';
@@ -40,6 +41,7 @@ const propertySchemasIndex = {
note: ComputedNoteSchema,
proficiency: ProficiencySchema,
propertySlot: ComputedSlotSchema,
reference: ReferenceSchema,
roll: ComputedRollSchema,
savingThrow: ComputedSavingThrowSchema,
skill: ComputedSkillSchema,

View File

@@ -13,6 +13,7 @@ import { FeatureSchema } from '/imports/api/properties/Features.js';
import { FolderSchema } from '/imports/api/properties/Folders.js';
import { NoteSchema } from '/imports/api/properties/Notes.js';
import { ProficiencySchema } from '/imports/api/properties/Proficiencies.js';
import { ReferenceSchema } from '/imports/api/properties/References.js';
import { RollSchema } from '/imports/api/properties/Rolls.js';
import { SavingThrowSchema } from '/imports/api/properties/SavingThrows.js';
import { SkillSchema } from '/imports/api/properties/Skills.js';
@@ -40,6 +41,7 @@ const propertySchemasIndex = {
note: NoteSchema,
proficiency: ProficiencySchema,
propertySlot: SlotSchema,
reference: ReferenceSchema,
roll: RollSchema,
savingThrow: SavingThrowSchema,
skill: SkillSchema,

View File

@@ -10,7 +10,11 @@ const InlineComputationSchema = new SimpleSchema({
type: String,
optional: true,
},
errors: ErrorSchema,
errors: {
type: Array,
optional: true,
},
'errors.$': ErrorSchema,
});
export default InlineComputationSchema;

View File

@@ -12,7 +12,7 @@ function assertIdValid(userId){
function assertdocExists(doc){
if (!doc){
throw new Meteor.Error('Permission denied',
'No such document exists');
'Permission denied: No such document exists');
}
}

View File

@@ -1,6 +1,7 @@
import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import '/imports/api/users/deleteMyAccount.js';
const userSchema = new SimpleSchema({
username: {

View File

@@ -0,0 +1,61 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import Libraries, {removeLibaryWork} from '/imports/api/library/Libraries.js';
import Creatures from '/imports/api/creature/Creatures.js';
import {removeCreatureWork} from '/imports/api/creature/removeCreature.js';
Meteor.users.deleteMyAccount = new ValidatedMethod({
name: 'users.deleteMyAccount',
validate: null,
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 1,
timeInterval: 5000,
},
run(){
let userId = Meteor.userId();
if (!userId) throw new Meteor.Error('No user',
'You must be logged into to delete your account');
// Delete all creatures
let creatures = Creatures.find({owner: userId}, {fields: {_id: 1}}).fetch();
creatures.forEach(creature => removeCreatureWork(creature._id));
// Remove permissions from all creatures
Creatures.update({
$or: [
{writers: userId},
{readers: userId},
],
}, {
$pull: {
writers: userId,
readers: userId
},
}, {
multi: true,
});
// Delete all libraries
let libraries = Libraries.find({owner: userId}, {fields: {_id: 1}}).fetch();
libraries.forEach(library => removeLibaryWork(library._id));
// Remove permissions from all creatures
Libraries.update({
$or: [
{writers: userId},
{readers: userId},
],
}, {
$pull: {
writers: userId,
readers: userId
},
}, {
multi: true,
});
// delete the account
Meteor.users.remove(userId);
}
});

View File

@@ -1,7 +1,7 @@
const INVENTORY_TAGS = Object.freeze({
const BUILT_IN_TAGS = Object.freeze({
inventory: 'inventory',
equipment: 'equipment',
carried: 'carried',
});
export default INVENTORY_TAGS;
export default BUILT_IN_TAGS;

View File

@@ -67,6 +67,11 @@ const PROPERTIES = Object.freeze({
icon: '$vuetify.icons.roll',
name: 'Roll'
},
reference: {
icon: 'link',
name: 'Reference',
libraryOnly: true,
},
savingThrow: {
icon: '$vuetify.icons.saving_throw',
name: 'Saving throw'

View File

@@ -91,6 +91,10 @@ const SVG_ICONS = Object.freeze({
name: 'weight',
shape: 'M256 46c-45.074 0-82 36.926-82 82 0 25.812 12.123 48.936 30.938 64H128L32 480h448l-96-288h-76.938C325.877 176.936 338 153.812 338 128c0-45.074-36.926-82-82-82zm0 36c25.618 0 46 20.382 46 46s-20.382 46-46 46-46-20.382-46-46 20.382-46 46-46z',
},
'weightless': {
name: 'weightless',
shape: 'M470.72 20L368.186 49.813l41.563-28.094c-26.254 5.922-59.36 17.502-100.97 36.186l-67.874 70.78L264.97 79.25c-23.247 12.958-47.95 29.99-71.814 49.844l-15.78 64.312L174 145.844c-23.55 21.548-45.624 45.6-63.875 70.812-19.25 26.59-34.28 54.506-41.813 82.438L40.19 280.28c6.138 19.613 11.892 39.232 22.906 58.845.032 1.468.1 2.944.187 4.406L29.657 333.19c11.227 18.284 23.577 35.893 43 49.125.45 1.003.953 1.973 1.438 2.968-11.838 33.33-20.568 67.004-26.53 101.69l18.405 3.155c4.952-28.808 11.836-56.842 20.905-84.563.04.053.084.105.125.157 44.277-156.11 142.813-266.846 287.03-324l6.876 17.374c-129.048 51.143-219.303 145.15-265.78 279.062 18.106.102 35.796-2.088 52.218-6.22l4.875-60.967 13.093 55.5c10.84-3.922 20.88-8.762 29.812-14.376l-20.688-43.47 32.782 34.813c7.944-6.468 14.613-13.678 19.624-21.53 30.308-47.507 62.195-94.728 124.75-134.188l-45.72-16.25 70.157 2.124c2.044-1.085 4.087-2.18 6.19-3.25 9.087-4.63 17.916-10.182 26.31-16.375L378.814 150l74.718-17.625c5.788-5.81 11.174-11.836 16.033-17.97 17.384-21.94 29.034-44.784 26.28-65.56-1.376-10.39-7.556-20.154-17.624-25.626-2.333-1.27-4.832-2.337-7.5-3.22zM106.25 406c-.89 3.06-1.778 6.122-2.625 9.22l2.625-9.22z'
}
});
export default SVG_ICONS;

View File

@@ -1,3 +1,5 @@
import ArrayNode from '/imports/parser/parseTree/ArrayNode.js';
export default {
'abs': {
comment: 'Returns the absolute value of a number',
@@ -5,7 +7,7 @@ export default {
{input: 'abs(9)', result: '9'},
{input: 'abs(-3)', result: '3'},
],
argumentType: 'number',
arguments: ['number'],
resultType: 'number',
fn: Math.abs,
},
@@ -15,21 +17,21 @@ export default {
{input: 'sqrt(16)', result: '4'},
{input: 'sqrt(10)', result: '3.1622776601683795'},
],
argumentType: 'number',
arguments: ['number'],
resultType: 'number',
fn: Math.sqrt,
},
'max': {
comment: 'Returns the largest of the given numbers',
examples: [{input: 'min(12, 6, 3, 168)', result: '168'}],
argumentType: 'number',
examples: [{input: 'max(12, 6, 3, 168)', result: '168'}],
arguments: anyNumberOf('number'),
resultType: 'number',
fn: Math.max,
},
'min': {
comment: 'Returns the smallest of the given numbers',
examples: [{input: 'min(12, 6, 3, 168)', result: '3'}],
argumentType: 'number',
arguments: anyNumberOf('number'),
resultType: 'number',
fn: Math.min,
},
@@ -40,7 +42,7 @@ export default {
{input: 'round(5.5)', result: '6'},
{input: 'round(5.05)', result: '5'},
],
argumentType: 'number',
arguments: ['number'],
resultType: 'number',
fn: Math.round,
},
@@ -52,7 +54,7 @@ export default {
{input: 'floor(5)', result: '5'},
{input: 'floor(-5.5)', result: '-6'},
],
argumentType: 'number',
arguments: ['number'],
resultType: 'number',
fn: Math.floor,
},
@@ -64,7 +66,7 @@ export default {
{input: 'ceil(5)', result: '5'},
{input: 'ceil(-5.5)', result: '-5'},
],
argumentType: 'number',
arguments: ['number'],
resultType: 'number',
fn: Math.ceil,
},
@@ -76,7 +78,7 @@ export default {
{input: 'trunc(5)', result: '5'},
{input: 'trunc(-5.5)', result: '-5'},
],
argumentType: 'number',
arguments:[ 'number'],
resultType: 'number',
fn: Math.trunc,
},
@@ -87,8 +89,32 @@ export default {
{input: 'sign(3)', result: '1'},
{input: 'sign(0)', result: '0'},
],
argumentType: 'number',
arguments: ['number'],
resultType: 'number',
fn: Math.sign,
},
'tableLookup': {
comment: 'Returns the index of the last value in the array that is less than the specified amount',
examples: [
{input: 'tableLookup([100, 300, 900], 457)', result: '2'},
{input: 'tableLookup([100, 300, 900], 23)', result: '0'},
{input: 'tableLookup([100, 300, 900, 1200], 900)', result: '3'},
{input: 'tableLookup([100, 300], 594)', result: '2'},
],
arguments: [ArrayNode, 'number'],
resultType: 'number',
fn: function tableLookup(arrayNode, number){
for(let i in arrayNode.values){
let node = arrayNode.values[i];
if (node.value > number) return i;
}
return arrayNode.values.length;
}
}
}
function anyNumberOf(type){
let argumentArray = [type];
argumentArray.anyLength = true;
return argumentArray;
}

View File

@@ -11,40 +11,63 @@ export default class CallNode extends ParseNode {
}
resolve(fn, scope, context){
let func = functions[this.functionName];
// Check that the function exists
if (!func) return new ErrorNode({
node: this,
error: `${this.functionName} is not a function`,
error: `${this.functionName} is not a supported function`,
context,
});
let args = castArgsToType({fn, scope, context, args: this.args, type: func.argumentType});
if (args.failed){
// Resolve the arguments
let resolvedArgs = this.args.map(node => node[fn](scope, context));
// Check that the arguments match what is expected
let checkFailed = this.checkArugments({
fn,
context,
resolvedArgs,
argumentsExpected: func.arguments
});
if (checkFailed){
if (fn === 'reduce'){
return new ErrorNode({
node: this,
error: 'Could not convert all arguments to the correct type',
context,
error: `Invalid arguments to ${this.functionName} function`,
});
} else {
return new CallNode({
functionName: this.functionName,
args: args,
args: resolvedArgs,
});
}
} else {
try {
let value = func.fn.apply(null, args);
return new ConstantNode({
value,
type: 'number',
previousNodes: [this],
});
} catch (error) {
return new ErrorNode({
node: this,
error,
context,
});
}
// Map contant nodes to constants before attempting to run the function
let mappedArgs = resolvedArgs.map(node => {
if (node instanceof ConstantNode){
return node.value;
} else {
return node;
}
});
try {
// Run the function
let value = func.fn.apply(null, mappedArgs);
let type = typeof value;
if (type === 'number' || type === 'string' || type === 'boolean'){
// Convert constant results into constant nodes
return new ConstantNode({ value, type });
} else {
return value;
}
} catch (error) {
return new ErrorNode({
node: this,
error: error.message || error,
context,
});
}
}
toString(){
@@ -57,20 +80,47 @@ export default class CallNode extends ParseNode {
replaceChildren(fn){
this.args = this.args.map(arg => arg.replaceNodes(fn));
}
}
checkArugments({fn, context, argumentsExpected, resolvedArgs}){
// Check that the number of arguments matches the number expected
if (
!argumentsExpected.anyLength &&
argumentsExpected.length !== resolvedArgs.length
){
context.storeError({
type: 'error',
message: 'Incorrect number of arguments ' +
`to ${this.functionName} function, ` +
`expected ${argumentsExpected.length} got ${resolvedArgs.length}`
});
return true;
}
function castArgsToType({fn, scope, context, args, type}){
let resolvedArgs = args.map(node => node[fn](scope, context))
let result = [];
if (type === 'number'){
resolvedArgs.forEach(node => {
if (node.isNumber){
result.push(node.value);
let failed = false;
// Check that each argument is of the correct type
resolvedArgs.forEach((node, index) => {
let type;
if (argumentsExpected.anyLength){
type = argumentsExpected[0];
} else {
resolvedArgs.failed = true;
type = argumentsExpected[index];
}
})
if (typeof type === 'string'){
// Type being a string means a constant node with matching type
if (node.type !== type) failed = true;
} else {
// Otherwise check that the node is an instance of the given type
if (!(node instanceof type)) failed = true;
}
if (failed && fn === 'reduce'){
let typeName = typeof type === 'string' ? type : type.constructor.name;
let nodeName = node.type || node.constructor.name
context.storeError({
type: 'error',
message: `Incorrect arguments to ${this.functionName} function` +
`expected ${typeName} got ${nodeName}`
});
}
});
return failed;
}
if (resolvedArgs.failed) return resolvedArgs;
return result;
}

View File

@@ -1,4 +1,6 @@
import ParseNode from '/imports/parser/parseTree/ParseNode.js';
import ArrayNode from '/imports/parser/parseTree/ArrayNode.js';
import ErrorNode from '/imports/parser/parseTree/ErrorNode.js';
export default class IndexNode extends ParseNode {
constructor({array, index}) {
@@ -8,16 +10,42 @@ export default class IndexNode extends ParseNode {
}
resolve(fn, scope, context){
let index = this.index[fn](scope, context);
if (index.isInteger){
let selection = this.array.values[index.value - 1];
let array = this.array[fn](scope, context);
if (index.isInteger && array instanceof ArrayNode){
if (index.value < 1 || index.value > array.values.length){
if (context){
context.storeError({
type: 'warning',
message: `Index of ${index.value} is out of range for an array` +
` of length ${array.values.length}`,
});
}
}
let selection = array.values[index.value - 1];
if (selection){
let result = selection[fn](scope, context);
return result;
}
} else if (fn === 'reduce'){
if (!(array instanceof ArrayNode)){
return new ErrorNode({
node: this,
error: 'Can not get the index of a non-array node: ' +
this.array.toString() + ' = ' + array.toString(),
context,
});
} else if (!index.isInteger){
return new ErrorNode({
node: this,
error: array.toString() + ' is not an integer index of the array',
context,
});
}
}
return new IndexNode({
index,
array: this.array[fn](scope, context),
array,
previousNodes: [this],
});
}

View File

@@ -53,6 +53,10 @@ export default class OperatorNode extends ParseNode {
}
toString(){
let {left, right, operator} = this;
// special case of adding a negative number
if (operator === '+' && right.isNumber && right.value < 0){
return `${left.toString()} - ${-right.value}`
}
return `${left.toString()} ${operator} ${right.toString()}`;
}
traverse(fn){

View File

@@ -8,6 +8,7 @@ export default class ParenthesisNode extends ParseNode {
resolve(fn, scope, context){
let content = this.content[fn](scope, context);
if (
fn === 'reduce' ||
content.constructor.name === 'ConstantNode' ||
content.constructor.name === 'ErrorNode'
){

View File

@@ -0,0 +1,6 @@
import { SyncedCron } from 'meteor/percolate:synced-cron';
SyncedCron.config({
// Log job run details to console
log: false,
});

View File

@@ -10,7 +10,6 @@ export default function sendWebhook({webhookURL, data = {}}){
const hook = new Discord.WebhookClient(id, token);
// Send a message using the webhook
console.log(JSON.stringify(data, null, 2));
hook.send(data);
}

View File

@@ -7,3 +7,4 @@ import '/imports/server/publications/users.js';
import '/imports/server/publications/icons.js';
import '/imports/server/publications/tabletops.js';
import '/imports/server/publications/slotFillers.js'
import '/imports/server/publications/ownedDocuments.js'

View File

@@ -25,31 +25,48 @@ Meteor.publish('libraries', function(){
{owner: this.userId},
{writers: this.userId},
{readers: this.userId},
{_id: {$in: subs}},
{ _id: {$in: subs}, public: true },
]
}, {
sort: {name: 1}
});
});
});
Meteor.publish('library', function(libraryId){
if (!libraryId) return [];
libraryIdSchema.validate({libraryId});
this.autorun(function (){
let userId = this.userId;
let library = Libraries.findOne(libraryId);
try { assertViewPermission(library, userId) }
catch(e){
return this.error(e);
}
return Libraries.find({
_id: libraryId,
});
});
});
let libraryIdSchema = new SimpleSchema({
libraryId: {
libraryId:{
type: String,
regEx: SimpleSchema.RegEx.Id,
},
});
Meteor.publish('library', function(libraryId){
Meteor.publish('libraryNodes', function(libraryId){
if (!libraryId) return [];
libraryIdSchema.validate({libraryId});
this.autorun(function (){
let userId = this.userId;
let libraryCursor = Libraries.find({
_id: libraryId,
});
let library = libraryCursor.fetch()[0];
let library = Libraries.findOne(libraryId);
try { assertViewPermission(library, userId) }
catch(e){ return [] }
catch(e){
return this.error(e);
}
return [
libraryCursor,
LibraryNodes.find({
'ancestors.id': libraryId,
}, {

View File

@@ -0,0 +1,15 @@
import Creatures from '/imports/api/creature/Creatures.js';
import Libraries from '/imports/api/library/Libraries.js';
Meteor.publish('ownedDocuments', function(){
this.autorun(function (){
let userId = this.userId;
if (!userId) {
return [];
}
return [
Creatures.find({owner: userId}),
Libraries.find({owner: userId}),
]
});
});

View File

@@ -3,6 +3,7 @@ import Creatures from '/imports/api/creature/Creatures.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js';
import { assertViewPermission } from '/imports/api/creature/creaturePermissions.js';
import recomputeInvetory from '/imports/api/creature/denormalise/recomputeInventory.js';
import { recomputeCreatureById } from '/imports/api/creature/computation/methods/recomputeCreature.js';
import VERSION from '/imports/constants/VERSION.js';
@@ -25,7 +26,10 @@ Meteor.publish('singleCharacter', function(creatureId){
try { assertViewPermission(creature, userId) }
catch(e){ return [] }
if (creature.computeVersion !== VERSION){
try { recomputeCreatureById(creatureId) }
try {
recomputeInvetory(creatureId);
recomputeCreatureById(creatureId)
}
catch(e){ console.error(e) }
}
return [

View File

@@ -50,7 +50,7 @@ Meteor.publish('slotFillers', function(slotId){
}
this.autorun(function(){
// Get the limit of the documents the user can fetch
var limit = self.data('limit') || 16;
var limit = self.data('limit') || 20;
check(limit, Number);
// Get the search term

View File

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

View File

@@ -3,19 +3,19 @@
v-model="opened"
:close-on-content-click="false"
transition="slide-y-transition"
lazy
left
>
<v-btn
slot="activator"
icon
>
<v-icon>format_paint</v-icon>
</v-btn>
<template #activator="{ on }">
<v-btn
icon
v-on="on"
>
<v-icon>format_paint</v-icon>
</v-btn>
</template>
<v-card class="overflow-hidden">
<v-card-text>
<v-layout
row
wrap
>
<div
@@ -72,14 +72,14 @@
</v-card-text>
<v-card-actions>
<v-btn
flat
text
@click="$emit('input')"
>
Clear
</v-btn>
<v-spacer />
<v-btn
flat
text
@click="opened = false"
>
Done
@@ -89,7 +89,7 @@
</v-menu>
</template>
<script>
<script lang="js">
import isDarkColor from '/imports/ui/utility/isDarkColor.js';
import vuetifyColors from 'vuetify/es5/util/colors';
import { kebabToCamelCase, camelToKebabCase } from '/imports/ui/utility/swapCase.js';

View File

@@ -10,7 +10,7 @@
</div>
</template>
<script>
<script lang="js">
export default {
props: {
wideColumns: Boolean,

View File

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

View File

@@ -1,6 +1,5 @@
<template>
<v-layout
row
align-center
justify-center
class="increment-menu"
@@ -44,9 +43,9 @@
<v-btn
:small="!flat"
:fab="!flat"
:flat="flat"
:text="flat"
:icon="flat"
class="filled"
class="mx-2 filled"
@click="commitEdit"
>
<v-icon>done</v-icon>
@@ -54,9 +53,9 @@
<v-btn
:small="!flat"
:fab="!flat"
:flat="flat"
:text="flat"
:icon="flat"
class="mx-0 filled"
class="filled"
@click="cancelEdit"
>
<v-icon>close</v-icon>
@@ -65,7 +64,7 @@
</v-layout>
</template>
<script>
<script lang="js">
export default {
inject: {
context: { default: {} }
@@ -73,7 +72,7 @@
props: {
value: {
type: Number,
required: true,
default: 0,
},
open: Boolean,
flat: Boolean,

View File

@@ -3,6 +3,8 @@
fab
small
v-bind="$attrs"
:disabled="disabled"
:style="disabled ? 'background-color: #616161 !important;' : ''"
@click="$emit('click')"
>
<v-icon>{{ icon }}</v-icon>
@@ -12,13 +14,13 @@
</v-btn>
</template>
<script>
<script lang="js">
/*
* Because speed dials only work well with v-btn's as children, this hacky
* component creates a v-btn with a label.
*/
export default {
props: ['icon', 'label'],
props: ['icon', 'label', 'disabled'],
}
</script>

View File

@@ -6,7 +6,7 @@
/>
</template>
<script>
<script lang="js">
import marked from 'marked';
export default {

View File

@@ -1,9 +1,9 @@
<template lang="html">
<v-card
:hover="hasClickListener"
:elevation="hovering ? 8 : undefined"
class="toolbar-card"
@click="$emit('click')"
:class="hovering ? 'elevation-8': ''"
@click.native="$emit('click')"
>
<v-toolbar
flat
@@ -23,14 +23,16 @@
</v-card>
</template>
<script>
<script lang="js">
import isDarkColor from '/imports/ui/utility/isDarkColor.js';
import getThemeColor from '/imports/ui/utility/getThemeColor.js';
export default {
props: {
color: {
type: String,
default(){
return this.$vuetify.theme.secondary;
return getThemeColor('secondary');
},
},
},
@@ -62,4 +64,7 @@
.toolbar-card .v-toolbar__title {
font-size: 14px;
}
.toolbar-card {
transition: box-shadow .4s cubic-bezier(0.25, 0.8, 0.25, 1);
}
</style>

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