Compare commits

...

586 Commits

Author SHA1 Message Date
Stefan Zermatten
6e51df363b Added increment buttons to all attributes with a value 2021-08-27 13:21:08 +02:00
Stefan Zermatten
608ab4e588 Added undo delete and recycle bin to library 2021-08-27 13:00:01 +02:00
Stefan Zermatten
518880fa5c Fixed error where searching for properties to insert while having other properties selected could prevent any insert from happening at all 2021-08-27 12:24:01 +02:00
Stefan Zermatten
f043c41e12 Fixed fab being confused by hiding the spells tab 2021-08-14 10:44:17 +02:00
Stefan Zermatten
0277de76c4 Removed stray console log 2021-08-12 19:56:01 +02:00
Stefan Zermatten
ffc3211ff9 Fixed some issues with slot filler searching 2021-08-12 18:28:53 +02:00
Stefan Zermatten
8162c76185 Added basic ownership transfer for shared documents 2021-08-10 19:01:31 +02:00
Stefan Zermatten
e21586e9ce Added reasonable storage limits to most string and array schemas 2021-08-10 18:12:55 +02:00
Stefan Zermatten
4c2155d8ff Buffs applied property can now be set in both library and character 2021-08-10 17:02:27 +02:00
Stefan Zermatten
44cc46ed22 Added the ability to add buffs as already applied straight from a library 2021-08-10 16:47:05 +02:00
Stefan Zermatten
d2b5d5f01d Fixed swiping between tabs when spells tab is hidden 2021-08-10 15:20:04 +02:00
Stefan Zermatten
4492c47b00 Fixed item tree node view only using quantity display if quantity is a number 2021-08-10 15:03:30 +02:00
Stefan Zermatten
6600cea9fa Fixed references display in tree views 2021-08-10 15:02:24 +02:00
Stefan Zermatten
741a9b080a Finished implementing new slot fill dialog, including ability to test slots 2021-08-10 14:35:27 +02:00
Stefan Zermatten
b041db22e4 Merge branch 'version-2' into version-2-dev 2021-08-10 13:31:32 +02:00
Stefan Zermatten
a465e2ce87 Continued implementing new slot dialog 2021-08-10 13:29:11 +02:00
Stefan Zermatten
8ecefb03ad Started re-implementing slot fill dialog with different design pattern 2021-08-10 10:32:56 +02:00
Stefan Zermatten
9f62a78eb0 Began work on implementing string and array storage limits 2021-08-09 22:34:04 +02:00
Stefan Zermatten
16e2b1249f Increased slot filler publication load quantity to 50 2021-08-09 20:54:29 +02:00
Stefan Zermatten
a35f9221a2 Fixed granting XP being accidentally locked behind patreon paid benefits 2021-08-09 18:06:04 +02:00
Stefan Zermatten
6364549d50 Added pretty url's from v1 2021-08-09 18:00:55 +02:00
Stefan Zermatten
d999fb46a7 Fixed a warning about source map failure when trying to define a scss variable 2021-08-09 17:57:35 +02:00
Stefan Zermatten
ec01a2adb5 Merge pull request #274 from GuillaumeDerval/version-2
Fix items/buffs/... refresh after casting a spell
2021-08-09 17:11:41 +02:00
Guillaume Derval
1f64558100 Fixed items/buffs/... refresh after casting a spell 2021-08-07 23:20:26 +02:00
Stefan Zermatten
19a2798bf7 Fixed tree item search highlighting in dark mode 2021-08-02 01:13:15 +02:00
Stefan Zermatten
a5f2c2e0d2 Removed duplicate property button on tree tab 2021-08-02 00:54:59 +02:00
Stefan Zermatten
ee174210fd Added search to library tree views 2021-08-02 00:29:56 +02:00
Stefan Zermatten
1e38295164 All properties added to the sheet now use the type/library/create UX 2021-08-01 23:28:04 +02:00
Stefan Zermatten
758cb2f8bc Fixed search icons 2021-07-31 21:53:18 +02:00
Stefan Zermatten
36bb3c3181 New UX for inserting properties from libraries including text search and multi-add 2021-07-31 21:49:15 +02:00
Stefan Zermatten
02434de34c Drastically improved tree tab search UX for locating parts of the sheet 2021-07-31 15:19:54 +02:00
Stefan Zermatten
0dc0bea53e fixes #262, emails from DiceCloud should now be from no-reply@dicecloud.com 2021-07-27 16:59:09 +02:00
Stefan Zermatten
c392119430 Added suggested parents to PROPERTIES for use later with user guidance 2021-07-27 16:36:19 +02:00
Stefan Zermatten
4e2e0ca364 Improved display of referenced properties 2021-07-27 16:21:55 +02:00
Stefan Zermatten
4a8b72f163 Made sure the unsubscribe button always shows on libraries you are subscribed to 2021-07-27 15:51:09 +02:00
Stefan Zermatten
d916dc2b78 Removed add library node buttons from libraries the user deosn't have edit permissions for 2021-07-27 15:44:26 +02:00
Stefan Zermatten
56860ba96d Fixed proficiency Tree node view not showing the name if it is defined 2021-07-27 15:40:16 +02:00
Stefan Zermatten
b607755f9f Fixed attribute base value calculation errors not being cleared if no new errors were made 2021-07-27 15:37:57 +02:00
Stefan Zermatten
86d8fa4325 Fixed items not animating correctly on insert 2021-07-27 15:33:41 +02:00
Stefan Zermatten
2b08249e5e removed lazy from v-menu which caused a console warning 2021-07-27 15:29:05 +02:00
Stefan Zermatten
3133e664d5 Reduced stats computation precision to round off small decimal floating point errors 2021-07-27 15:28:44 +02:00
Stefan Zermatten
48f32e0a8d Removed floating point small decimal oddities in parts of inventory tab 2021-07-27 15:21:35 +02:00
Stefan Zermatten
c72785c9e7 Added components to spell viewer 2021-07-27 15:00:10 +02:00
Stefan Zermatten
421ff2aa7d Fixed DISABLE_PATREON not working, it's now a Meteor setting instead of an ENV variable 2021-07-27 14:31:54 +02:00
Stefan Zermatten
9a9e6491b9 Improved usability with better hints in property forms and property type selection 2021-07-26 18:19:29 +02:00
Stefan Zermatten
332258705c Added service worker 2021-07-21 13:44:55 +02:00
Stefan Zermatten
73ef109d4d Added calculation errors that were missing 2021-07-20 10:37:32 +02:00
Stefan Zermatten
fc240a34c4 Changed tiers to their open beta configuration 2021-07-17 12:50:38 +02:00
Stefan Zermatten
8ac4028f38 Removed limit from guest tiers for closed beta 2021-07-14 00:59:32 +02:00
Stefan Zermatten
2849df1974 Merge branch 'version-2-dev' into version-2 2021-07-13 12:45:59 +02:00
Stefan Zermatten
3fa2cca7ae Locked dark mode to paid accounts only 2021-07-13 12:39:56 +02:00
Stefan Zermatten
a0b53af6d7 Fixed awkward padding in character limit alert 2021-07-13 12:25:15 +02:00
Stefan Zermatten
51c709f7a5 Changed tier limitations for closed beta 2021-07-13 12:23:56 +02:00
Stefan Zermatten
28d67409aa Changed some patreon nudging 2021-07-13 12:23:28 +02:00
Stefan Zermatten
7e97bcb6d8 Updated packages 2021-07-13 12:04:36 +02:00
Stefan Zermatten
4e87737a3e Fixed add-property button not re-appearing when adding a reference property 2021-07-12 18:11:48 +02:00
Stefan Zermatten
822fa4619f Fixed missing icons in calculation errors 2021-07-12 18:00:21 +02:00
Stefan Zermatten
80ba44a28f Added ancestor references to action context so that #spellList references work, closes #270 2021-07-12 17:56:35 +02:00
Stefan Zermatten
c3c079731e Fixed equipment creating ghost items on drag if the equipment folder is deleted from the character 2021-07-12 17:13:18 +02:00
Stefan Zermatten
039b7046b2 Fixed attributes not hiding when redundant 2021-07-12 17:03:32 +02:00
Stefan Zermatten
8eaad3600f Fixed computation error with base values 2021-07-12 14:56:12 +02:00
Stefan Zermatten
5cf78932e6 removed stray debug text 2021-07-12 14:47:41 +02:00
Stefan Zermatten
d43d364175 Removed stray console log 2021-07-12 14:46:37 +02:00
Stefan Zermatten
0ad4c71189 Fixed some missing icons 2021-07-12 14:45:24 +02:00
Stefan Zermatten
e8c6f26a0b fix skills UI bugs and icon consistency for skills 2021-07-12 14:39:05 +02:00
Stefan Zermatten
8804c80a56 Fixed skills not showing their base value in the effects list 2021-07-12 14:15:17 +02:00
Stefan Zermatten
0d21ab758e Fixed negative base values being ignored 2021-07-12 14:12:20 +02:00
Stefan Zermatten
2ecb0e2671 Fix: DC missing from spell list viewer in library 2021-07-12 13:39:08 +02:00
Stefan Zermatten
f3b9b62486 Fixed styling of inventory for denser item list tiles 2021-07-12 12:50:56 +02:00
Stefan Zermatten
29a4575760 Fixed a stray empty list item action taking space on creatures in lists 2021-06-22 15:19:46 +02:00
Stefan Zermatten
a6fbf71b36 Added folders to the sidebar character list 2021-06-22 15:13:59 +02:00
Stefan Zermatten
86d9383af0 Archive UI 2021-06-22 14:59:18 +02:00
Stefan Zermatten
3db589f775 Fixed button property for new Vuetify version 2021-06-22 14:59:02 +02:00
Stefan Zermatten
e96755927f Added archive dialog, empty for now 2021-06-21 16:42:47 +02:00
Stefan Zermatten
66dc0ee34f Added icon to indicate sharing status of characters 2021-06-21 16:37:02 +02:00
Stefan Zermatten
54bf21c57c Edit permission is no longer patreon-only 2021-06-21 16:32:24 +02:00
Stefan Zermatten
5f5fe801f6 Added method to transfer character ownership 2021-06-21 16:06:29 +02:00
Stefan Zermatten
a451afcbaf Prevented new characters being added if you are at your character limit 2021-06-21 16:06:10 +02:00
Stefan Zermatten
848e961e3b Refactored creature methods to their own folders 2021-06-21 15:20:04 +02:00
Stefan Zermatten
c5aca81131 Removed stray console logs 2021-06-20 13:35:23 +02:00
Stefan Zermatten
814e371148 Can now move creatures between folders using drag and drop 2021-06-20 13:32:28 +02:00
Stefan Zermatten
69f4bbf360 Added CRUD API and UI for creature folders 2021-06-20 12:41:08 +02:00
Stefan Zermatten
6b2d74a165 Fixed parties -> creatureFolders publications and ui 2021-06-18 12:59:39 +02:00
Stefan Zermatten
cf05aea80a Fixed Parties references 2021-06-18 10:49:01 +02:00
Stefan Zermatten
1a2d4b22bb Renamed parties to creatureFolders 2021-06-18 10:48:12 +02:00
Stefan Zermatten
81d52a1847 Enabled editing of attribute damage in library forms 2021-06-18 10:43:27 +02:00
Stefan Zermatten
e3fc56a844 Added backend architecture to archive and restore creatures. 2021-06-11 12:03:31 +02:00
Stefan Zermatten
64fceb9c38 Added environmental variables to readme for self-hosting 2021-06-10 15:19:14 +02:00
Stefan Zermatten
9994c1f32a New users now get subscribed to the default libraries as defined by env 2021-06-10 15:18:54 +02:00
Stefan Zermatten
7056c5b37b Added character slot limitations to tiers; added no-patreon tier for self hosting 2021-06-10 12:25:17 +02:00
Stefan Zermatten
1ad1d1f23d Migrated from Google material design icons to vuetify default MDI 2021-06-01 12:34:51 +02:00
Stefan Zermatten
c65c8f3299 Added note to attempt to keep children of reference nodes 2021-04-29 16:13:22 +02:00
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
Stefan Zermatten
612fcca68c Only split properties accross targets if there are targets 2021-02-22 14:30:50 +02:00
Stefan Zermatten
12939c46de made saves walk children when not targeted at self 2021-02-22 14:28:38 +02:00
Stefan Zermatten
3801b17fde Attacks can now critical hit. criticalHitTarget overrides the roll required 2021-02-22 14:07:12 +02:00
Stefan Zermatten
88133a2fa3 Saving throws now work in actions 2021-02-22 12:38:21 +02:00
Stefan Zermatten
d00eedac19 Rolls now work in actions 2021-02-22 11:55:08 +02:00
Stefan Zermatten
6571fb860a Toggles now work in actions to make choices based on action context 2021-02-22 11:36:30 +02:00
Stefan Zermatten
8148f4d701 Fixed: Library nodes are published in order to prevent disordered building of the tree 2021-02-21 17:05:09 +02:00
Stefan Zermatten
523c34b719 Fixed: Slots that use conditions now only hide on falsey value (false, 0, '') 2021-02-21 17:01:24 +02:00
Stefan Zermatten
e833fba870 Fixed: bug that stopped buffs being deleted 2021-02-21 16:35:35 +02:00
Stefan Zermatten
f3e191c12e Fixed: Inserting properties to the tree now animate correctly to the inserted property 2021-02-20 16:00:40 +02:00
Stefan Zermatten
33415275a3 Item tiles are now smaller in the inventory view 2021-02-20 15:53:58 +02:00
Stefan Zermatten
3b1151d987 Fixed notes without summaries are no longer oversized 2021-02-20 15:52:15 +02:00
Stefan Zermatten
4288f98f7c Fixed: stats with no ability selected have an ability modifier of 0 instead of NaN 2021-02-20 15:50:25 +02:00
Stefan Zermatten
1a2ef8a4a2 Fixed: markdown images no longer overflow their container width 2021-02-20 15:45:45 +02:00
Stefan Zermatten
10e9a5faa8 Notes now show both summary and description in viewer 2021-02-20 15:41:30 +02:00
Stefan Zermatten
53594c0004 Fixed: items in containers not following tree order 2021-02-20 15:38:51 +02:00
Stefan Zermatten
e068675b46 Fixed and improved: Discord webhooks are working again with a new format 2021-02-20 15:27:20 +02:00
Stefan Zermatten
067f5df36e Fixed: Emptying the search bar in slot filler dialog now correctly clears the search 2021-02-20 10:17:35 +02:00
Stefan Zermatten
6113d86059 Fixed typo in calling a function in Constants autovalue 2021-02-16 11:19:50 +02:00
Stefan Zermatten
e3862bcdd9 Fixed constants autovalue 2021-02-16 10:49:18 +02:00
Stefan Zermatten
299f5a06dd Moved inline cacultion regex to a constant to ensure constistency 2021-02-16 10:14:26 +02:00
Stefan Zermatten
2776850311 Fixed missing import 2021-02-12 11:45:32 +02:00
Stefan Zermatten
3e6221309e Notes now have a summary and a description, some data migration my be needed 2021-02-12 11:44:03 +02:00
Stefan Zermatten
a078ce3d5d Description fields should now show calculation errors 2021-02-12 11:43:13 +02:00
Stefan Zermatten
b9e0475d07 Markdown now follow Github Flavor Markdown, including single line breaks 2021-02-12 11:11:12 +02:00
Stefan Zermatten
2b345c1f77 Improved error handling for most calculations 2021-02-12 11:00:44 +02:00
Stefan Zermatten
fed87f0a84 Fixed: items and spells should no longer be draggable when you don't have edit permission 2021-02-12 09:41:24 +02:00
Stefan Zermatten
b116be1238 Fixed flickering when inserting properties from library by ensuring consistent ID generation 2021-02-12 00:43:56 +02:00
Stefan Zermatten
ae373330ab Improved slot fill dialog UI, abandoned column layout in favour of wrapping flex rows 2021-02-12 00:18:29 +02:00
Stefan Zermatten
dcb535c84e Moved slot filler search to server side, limited docs in subscription 2021-02-11 22:09:43 +02:00
Stefan Zermatten
8c477ad4b1 Fixed constants not being found when used as the only thing in a calculation 2021-02-11 18:01:30 +02:00
Stefan Zermatten
eb2dd3bba1 Fixed computation dependency aggregation broken by refactoring 2021-02-11 16:27:22 +02:00
Stefan Zermatten
dc4808c70a fixed snackbars being blank 2021-02-11 16:20:33 +02:00
Stefan Zermatten
16c8c1db81 Improved descriptions in log entries 2021-02-11 16:16:22 +02:00
Stefan Zermatten
92a5c1e6c3 Improved log entries for actions 2021-02-11 16:08:31 +02:00
Stefan Zermatten
439eadf079 Condensed logs to a single card per action 2021-02-11 15:48:23 +02:00
Stefan Zermatten
d7083cf242 Hid unused cards on the stats tab 2021-02-11 13:48:10 +02:00
Stefan Zermatten
3af5e820ca Stopped inactive effects showing up in attributes 2021-02-11 13:38:34 +02:00
Stefan Zermatten
20aaab4dea Hid inactive notes 2021-02-11 13:14:31 +02:00
Stefan Zermatten
81cdf282ea Stopped inactive properties from showing computed inline fields, since they are not recomputed while inactive 2021-02-11 13:12:35 +02:00
Stefan Zermatten
3313ed0297 Added constants to the UI and Computation Engine 2021-02-11 13:03:31 +02:00
Stefan Zermatten
25fd5c18e8 Fixed casting broken by refactoring 2021-02-11 10:13:35 +02:00
Stefan Zermatten
5b9bb6e4bc Fixed spending resources for actions broken by refactoring 2021-02-11 10:13:23 +02:00
Stefan Zermatten
74370f6fec Performance optimization: Removed creature document from injection to prevent uneccessary Vue re-rendering 2021-02-11 10:04:28 +02:00
Stefan Zermatten
565ddccba6 fixed broken import 2021-02-04 16:55:50 +02:00
Stefan Zermatten
4ea4348a02 Now writing partial recalculations to creature variables 2021-02-04 16:52:26 +02:00
Stefan Zermatten
280f30dab5 Improved dependencies-only recalculations and fixed many calculation bugs 2021-02-04 16:16:51 +02:00
Stefan Zermatten
6d1e3f078c Optimised when certain recompute functions are called to prevent unccessary work 2021-02-04 13:59:08 +02:00
Stefan Zermatten
326d1bd165 Refactored computation into folders 2021-02-04 13:33:20 +02:00
Stefan Zermatten
87fa941f63 Merge branch 'version-2' of https://github.com/ThaumRystra/DiceCloud into version-2 2021-02-04 11:38:55 +02:00
Stefan Zermatten
449a4fba7d Refactored creature property methods into separate documents, might have broken a lot of things 2021-02-04 11:38:29 +02:00
Stefan Zermatten
9ff096ec0f Added modulo operator 2021-02-03 09:33:00 +02:00
Stefan Zermatten
f9f0186d95 Fixed error where parser was not creating accessor nodes correctly 2021-02-03 00:39:11 +02:00
Stefan Zermatten
60ea545ee9 Started implementing constant property 2021-02-02 16:36:23 +02:00
Stefan Zermatten
aaa5d0b63b Allowed effects and calculations to target nearest ancestors of #type 2021-02-02 16:11:59 +02:00
Stefan Zermatten
69c72e0987 Fixed parenthesis being discarded from compiled calculations in cases where they should not be 2021-02-02 15:13:49 +02:00
Stefan Zermatten
a6df4df534 Fixed some fields not storing strings when compiling calculations 2021-02-02 15:07:31 +02:00
Stefan Zermatten
8b8f9bea6f Removed character variables from creatures injected into character sheet as context 2021-01-31 20:13:45 +02:00
Stefan Zermatten
e7a27e4b83 Fixed hit dice breaking long rests 2021-01-31 19:45:14 +02:00
Stefan Zermatten
e14e875c42 Fixed conMod not being signed in hit dice tiles 2021-01-31 19:43:09 +02:00
Stefan Zermatten
a7898bfe4b Fixed props not having a default array for embedded calculations 2021-01-31 19:42:49 +02:00
Stefan Zermatten
aee899e181 Removed all UI computations from viewers and components 2021-01-31 18:42:17 +02:00
Stefan Zermatten
a5284bf6e8 Made 'always prepared' spells show up in casting list 2021-01-30 11:22:29 +02:00
Stefan Zermatten
0ea3f7a975 Migrated fields to embedded property calculations 2021-01-29 13:04:53 +02:00
Stefan Zermatten
1167538977 Denormalised inline calculations to property documents, needs to be referenced by UI still 2021-01-29 12:29:01 +02:00
Stefan Zermatten
9c799e3dc9 Added search to slot fill dialog 2021-01-28 17:09:37 +02:00
Stefan Zermatten
2bf749c4f1 Fixed: Layout of slot fill dialog is no longer broken by tall images, background is now the correct color 2021-01-28 16:48:33 +02:00
Stefan Zermatten
db83d5f82a Fix: Tabs and tab headers no longer lose sync when changing characters 2021-01-28 16:17:03 +02:00
Stefan Zermatten
d596061fa8 Fixed: Attributes that aren't ability scores now have their modifiers removed correctly 2021-01-28 16:05:47 +02:00
Stefan Zermatten
78efe639ed Fixed: Zero effect passive bonus now causes passive skill value to show. 2021-01-28 16:01:58 +02:00
Stefan Zermatten
e5bde38745 Removed console timers from recompute function: it's not the primary slowdown 2021-01-28 15:00:36 +02:00
Stefan Zermatten
83f2047dbe Replaced expensive getActiveProperties with cheaper filter by inactive field 2021-01-28 14:29:10 +02:00
Stefan Zermatten
fc03097ed8 Patreon access is now given by tier instead of by price paid 2021-01-28 12:15:45 +02:00
Stefan Zermatten
252ac23391 Merge branch 'version-2' of https://github.com/ThaumRystra/DiceCloud into version-2 2021-01-27 22:25:19 +02:00
Stefan Zermatten
531ddce6a0 Added dependency tracking to computations for future optimization effort 2021-01-27 22:24:28 +02:00
Stefan Zermatten
3cdeb73c30 Stopped removed items from showing up in inventory containers 2021-01-25 12:38:46 +02:00
Stefan Zermatten
c780c11e3f Improved spell slot casting dialog with search, filters, spell details 2021-01-22 14:09:23 +02:00
Stefan Zermatten
100c93b5ae Stopped dialog animation mocking transparent elements' background color 2021-01-22 12:20:33 +02:00
Stefan Zermatten
a4e6dd1d66 Added spellcasting to the stats page, click the icon next to a spell slot to cast 2021-01-19 16:10:34 +02:00
Stefan Zermatten
1b3b6362f7 Exposed spellSlotLevelValue to creature variables store 2021-01-19 12:07:40 +02:00
Stefan Zermatten
bdf4074e3c Added first pass at denormalizing inventory data, needs testing and integration 2021-01-15 17:41:06 +02:00
Stefan Zermatten
9492b2d8b8 Characters now start with a slot for a base. New characters start on the character tab with the build dialog open. 2021-01-14 12:28:51 +02:00
Stefan Zermatten
27f1f4e720 Fixed a bug with dialog animations hiding buttons by failing to clean up 2021-01-12 13:51:36 +02:00
Stefan Zermatten
d63b8c835d Fixed bug in last release where unlimited slots were always hidden on hide when full 2021-01-12 13:51:11 +02:00
Stefan Zermatten
85f3881935 Removed alert about data loss, most data structures are stable and it's being alarmist 2021-01-12 13:40:02 +02:00
Stefan Zermatten
5e4299e6db removed stray console logging 2021-01-12 13:24:08 +02:00
Stefan Zermatten
0e663f36db Slot fillers that count as more than one slot now update the space left correctly.
Slot fillers removed from a library can no longer be added to a slot.
If a slot has limited space left, this will be reflected on the error 
finding slots message
2021-01-12 13:22:48 +02:00
Stefan Zermatten
de9ea5922c Fixed bugs when a spell list does not have limit on prepared spells 2021-01-12 13:10:36 +02:00
Stefan Zermatten
a2cfe43e74 Disabled the direct damage input in the attribute form: it can't be edited anyway 2021-01-12 13:04:25 +02:00
Stefan Zermatten
026c11c13b Made sure attributes show their currentValue instead of Value 2021-01-12 13:03:33 +02:00
Stefan Zermatten
403f2663c2 Fixed bugs with item display, equipment will now automatically move to the first property with the 'equipment' tag, carried items will move to the first property with the 'carried' tag 2021-01-12 12:54:02 +02:00
Stefan Zermatten
28c042343e All children of infinite slots will now hide when "hide when full" is active 2021-01-12 10:36:07 +02:00
Stefan Zermatten
3f116875a1 Children of slots now display in the correct order 2021-01-12 10:35:37 +02:00
Stefan Zermatten
ae5b4b7d5c Made inactive toggle decendants specifically included when recomputing active properties 2021-01-11 22:03:54 +02:00
Stefan Zermatten
75835d74f6 Merge pull request #259 from Ganonsmasher/version-2
Found a fix for toggles.
2020-12-17 17:19:54 +02:00
Ganonsmasher
583b652fc4 Found a fix for toggles.
It doesn't fix the problem of their property being absent on the stats page cards, but they can compute properly and disable their children with this.
2020-12-15 18:08:29 -05:00
Stefan Zermatten
df317a8942 Merge branch 'version-2' of https://github.com/ThaumRystra/DiceCloud into version-2 2020-11-20 14:21:52 +02:00
Stefan Zermatten
ff0e9b1ff9 Added Lord of Junk to Patreon Paragons 2020-11-20 14:13:47 +02:00
Stefan Zermatten
fa24430a7f Fixed parsing of variable names with numbers and stacked dice rolls like dd8-> 1d(1d8) 2020-11-13 10:04:37 +02:00
Stefan Zermatten
fde2f821e7 Fixed parser not handling whitespace 2020-11-12 21:44:08 +02:00
Stefan Zermatten
827430c987 Fixed edit permission errors for some creature prop methods 2020-11-12 21:25:48 +02:00
Stefan Zermatten
2a1aa02e97 Added true and false keywords, fixed grammar ambiguity in if statements 2020-11-12 15:11:24 +02:00
Stefan Zermatten
aeb347084f Fixed ambiguitiy in grammar caused by previous fixes 2020-11-12 13:47:10 +02:00
Stefan Zermatten
005bc162cb Fixed != not being matched because ! was matched first. Fixed presedence for & | and relational operators 2020-11-12 13:45:14 +02:00
Stefan Zermatten
9941d91bb8 Fixed != operator, separated == and ===, != and !== for strictness control 2020-11-12 13:44:01 +02:00
Stefan Zermatten
23c77690a1 Healing damage type now heals instead of damaging 2020-11-12 13:01:09 +02:00
Stefan Zermatten
525b528d9a Added attribute damage and self damage results to actions and log. 2020-11-12 12:57:48 +02:00
Stefan Zermatten
3917f63d5e Increased subscription rate limit to prevent infinitely spinning characters 2020-11-12 11:04:04 +02:00
Stefan Zermatten
bd056ab042 Improved subscription permissions, should now work as expected for public documents 2020-11-12 10:48:46 +02:00
Stefan Zermatten
cd84b2562a Fixed an error with finding deployed version SHA 2020-11-10 14:48:09 +02:00
Stefan Zermatten
c7436ffb1e Caught error where git couldn't be used to get version 2020-11-10 14:37:32 +02:00
Stefan Zermatten
be7d7f898f disabled short and log rest buttons if user has no edit permission 2020-11-10 14:22:09 +02:00
Stefan Zermatten
3024168e95 Replaced old parser with new parser 2020-11-10 14:07:22 +02:00
Stefan Zermatten
1f0678b50b Added not operator to the parser 2020-11-05 15:32:01 +02:00
Stefan Zermatten
4dad2c41e5 Updated parser to accept underscores in variable names 2020-11-05 14:50:44 +02:00
Stefan Zermatten
46385dd9b2 Build details with no slots hidden moved to a dialog 2020-11-05 14:27:01 +02:00
Stefan Zermatten
7cb65954b5 Added the ability to hide slots when full 2020-11-05 14:05:17 +02:00
Stefan Zermatten
749799d869 Denormalised slot fill total to database 2020-11-05 13:35:55 +02:00
Stefan Zermatten
3293dad671 Limited what fields are included when fetching recompute documents to improve performance 2020-11-05 12:59:48 +02:00
Stefan Zermatten
88df942c59 Fixed an error with missing identity details in patreon request 2020-11-05 12:59:26 +02:00
Stefan Zermatten
9722bbc667 Characters now recompute on subscribe if they haven't been computed in the current version 2020-11-04 14:27:31 +02:00
Stefan Zermatten
2fb0ba79c6 began work to get inactive state of properties denormalised 2020-11-03 15:57:14 +02:00
Stefan Zermatten
3f7ddd62fc Merge branch 'version-2' of https://github.com/ThaumRystra/DiceCloud1 into version-2 2020-10-27 10:41:06 +02:00
Stefan Zermatten
227d6c5aae Markdown and calculations now supported in slot filler descriptions 2020-10-27 10:39:20 +02:00
Stefan Zermatten
147ef97576 Markdown now supported in slot filler descriptions 2020-10-27 10:34:01 +02:00
Stefan Zermatten
54806b0f3c Merge branch 'version-2' of https://github.com/ThaumRystra/DiceCloud1 into version-2 2020-10-19 11:39:24 +02:00
Stefan Zermatten
1165158d46 Forced creatures to reorder their docs before recomputing 2020-10-19 11:39:21 +02:00
Stefan Zermatten
32de70cd45 Class levels now have a description field 2020-10-17 19:33:56 +02:00
Stefan Zermatten
68499e4de5 You can now click on properties filling slots to view their details dialog 2020-10-17 19:28:54 +02:00
Stefan Zermatten
3f4cb8e26b Added undo buttons for deleting properties off a creature 2020-10-17 19:10:37 +02:00
Stefan Zermatten
ebab41838c Used tree node views in slot fill selection 2020-10-17 16:56:23 +02:00
Stefan Zermatten
e8da7a6c17 Moved snackbars to their own store and component 2020-10-17 16:06:27 +02:00
Stefan Zermatten
46189c68df All property forms now allow tags 2020-10-17 13:42:24 +02:00
Stefan Zermatten
9fa997ed24 Started moving snackbars to vue store, still needs to be separated into its own module 2020-10-16 13:38:58 +02:00
Stefan Zermatten
e3bf6557ec fixed bug in dialog store 2020-10-16 10:35:51 +02:00
Stefan Zermatten
7aa3e5a217 Stringified errors from scheduled deletion 2020-10-16 10:23:34 +02:00
Stefan Zermatten
dc1b025090 Increased delete job frequency to 10 minutes 2020-10-16 10:00:07 +02:00
Stefan Zermatten
2e370a9884 Fixed removed slots not being hidden 2020-10-15 16:42:57 +02:00
Stefan Zermatten
384fa076f1 hotfix tags not filtering correctly 2020-10-15 16:12:32 +02:00
Stefan Zermatten
7922e30ddc Added tags to some properties. Added condition to class levels 2020-10-15 16:00:32 +02:00
Stefan Zermatten
1ba4f76763 Class levels can now have conditions 2020-10-15 15:57:19 +02:00
Stefan Zermatten
839f91c3b2 Fixed icons not going to dark mode when slot filling cards are selected 2020-10-15 15:30:11 +02:00
Stefan Zermatten
26567ce840 slot fill cards with pictures no longer get icons 2020-10-15 15:25:10 +02:00
Stefan Zermatten
2a729a4eca Made slot fill dialog a list of cards to leverage pretty slotFillers 2020-10-15 15:24:14 +02:00
Stefan Zermatten
ed17d9e2d2 Added slotfiller property type to increase control over slot filling 2020-10-15 14:54:58 +02:00
Stefan Zermatten
8e9405b5ad Allowed slots with unlimited children, improved slot ui text 2020-10-15 13:50:46 +02:00
Stefan Zermatten
7fc783dcad Removed recompute button 2020-10-15 13:50:14 +02:00
Stefan Zermatten
b15ad7e51a Merge branch 'version-2' of https://github.com/ThaumRystra/DiceCloud1 into version-2 2020-10-15 13:49:49 +02:00
Stefan Zermatten
8a3d2474fc Merge pull request #254 from JoeZwet/version-2
fix: prevent discord mention exploit
2020-10-15 13:48:30 +02:00
Joe van der Zwet
09371e7d54 add requested changes 2020-10-16 00:47:32 +13:00
Joe van der Zwet
0776d33909 fix: prevent discord mention exploit 2020-10-16 00:36:32 +13:00
Stefan Zermatten
6e98d71c3c Improved slot UI look and feel 2020-10-15 13:00:29 +02:00
Stefan Zermatten
8f89f4b63f Ensured all subscriptions return empty arrays instead of errors or ready 2020-10-15 12:34:46 +02:00
Stefan Zermatten
c0070d017e Removed debugging code 2020-10-14 15:54:16 +02:00
Stefan Zermatten
51569592ab First implementation on Slots UI 2020-10-14 14:45:26 +02:00
Stefan Zermatten
d2cb86ac27 Fixed broken logging for actions 2020-10-14 11:33:25 +02:00
Stefan Zermatten
bde9183158 Log optimistic UI now fixed, rolls are now instant 2020-10-14 11:25:05 +02:00
Stefan Zermatten
0cc9e01754 Renamed, moved LogTab to CharacterLog 2020-10-14 09:37:00 +02:00
Stefan Zermatten
9856471202 Stopped log making toast if it's visible 2020-10-14 09:33:17 +02:00
Stefan Zermatten
4f77782a7a log messages are now aligned right 2020-10-13 13:53:30 +02:00
Stefan Zermatten
5f13aaa031 Fixed empty strings in log input 2020-10-13 13:44:06 +02:00
Stefan Zermatten
1321cf6a96 Moved log tab to right drawer 2020-10-13 13:42:18 +02:00
Stefan Zermatten
dee8249f61 Creature logs are now removed with creatures 2020-10-13 12:43:55 +02:00
Stefan Zermatten
0af0afc0d0 Discord webhooks now mirror character log 2020-10-13 12:42:02 +02:00
Stefan Zermatten
a104fc8a87 Fixing broken casing on file pt.2 2020-10-06 10:55:12 +02:00
Stefan Zermatten
46f452987f Fixing broken casing on file 2020-10-06 10:54:52 +02:00
Stefan Zermatten
a87cb1286a Improved custom rolls on log tab 2020-10-06 09:53:08 +02:00
Stefan Zermatten
844588cdbf Started adding text input to log tab 2020-09-30 16:24:33 +02:00
Stefan Zermatten
a6a96fc19f Started work on character log for rolls to be stored 2020-09-29 22:34:30 +02:00
Stefan Zermatten
75ab43da00 Started work on UI for rolling checks 2020-09-29 16:37:28 +02:00
Stefan Zermatten
df7000889b fixed security deps 2020-09-29 10:54:37 +02:00
Stefan Zermatten
65754dea80 removed damage multipliers from health bar card, it has its own card 2020-09-29 10:54:27 +02:00
Stefan Zermatten
30cca39e7c Merge branch 'version-2' of https://github.com/ThaumRystra/DiceCloud1 into version-2 2020-09-28 13:58:03 +02:00
Stefan Zermatten
5ad5c914fb Added gitignore file for renders 2020-09-24 19:00:28 +02:00
Stefan Zermatten
f27550362a Fixed various parser bugs, implemented unary operators 2020-09-18 22:13:12 +02:00
Stefan Zermatten
50f7977a60 Fixed patreon update write location 2020-09-18 20:37:16 +02:00
Stefan Zermatten
bc5c465a32 Started work on checks 2020-09-18 14:00:29 +02:00
Stefan Zermatten
c8ddf9d547 Added the ability to double all number of dice to roll using context 2020-09-18 12:24:08 +02:00
Stefan Zermatten
6570665c1e Added functions and ensured the context was being passed around correctly 2020-09-18 11:52:44 +02:00
Stefan Zermatten
06f17a6d33 Parser now uses context to store details of the computation 2020-09-18 10:14:53 +02:00
Stefan Zermatten
b69ad6c306 Removed unused parser code 2020-09-10 11:39:27 +02:00
Stefan Zermatten
5dec760452 Parser now works with variables passed into scope 2020-09-10 11:38:28 +02:00
Stefan Zermatten
ede4e1367d Continued work on parser, now calling functions and rolling correctly 2020-09-10 00:14:24 +02:00
Stefan Zermatten
81645df2a6 Lots of work on the parser including testing interface 2020-09-09 17:09:50 +02:00
Stefan Zermatten
445171ce80 Added preferences subheader to accounts page 2020-09-09 13:58:48 +02:00
Stefan Zermatten
dedab7b046 Added patreon tier refresh button, autorefresh tier on login daily 2020-09-09 13:55:21 +02:00
Stefan Zermatten
a5c16ba83a Overhauled inventory tab again. Closer in functionality to V1 2020-08-22 00:36:17 +02:00
Stefan Zermatten
46501f2759 Spells that aren't prepared no longer count as active properties 2020-08-21 16:34:52 +02:00
Stefan Zermatten
a6ed1004be Added Resistance, Vulnerability, and Immunity to the health bar card 2020-08-21 16:27:01 +02:00
Stefan Zermatten
8539356b9e Added UI to prepare spells 2020-08-21 16:04:31 +02:00
Stefan Zermatten
93db5e9288 Made library link readonly instead of disabled so it can be copied 2020-08-10 04:23:55 +02:00
Stefan Zermatten
b4cb91a892 Multiple libraries can now be opened, allowing library items to be moved 2020-08-10 04:19:54 +02:00
Stefan Zermatten
9c93747845 Fixed bug where array accessors were attempting to use the substitution engine prematurely 2020-08-10 04:14:53 +02:00
Stefan Zermatten
a51154e434 Prevented test webhooks being sent in production 2020-07-26 19:56:08 +02:00
Stefan Zermatten
1bde0db0ba Fixed features showing up when disabled by an ancestor 2020-07-26 19:53:07 +02:00
Stefan Zermatten
bc001202ec Fixed spell list tiles not being opaque 2020-07-26 19:47:44 +02:00
Stefan Zermatten
c7985af83b Continued work on tabletops, hidden from main app and all methods disallowed for non-admins 2020-07-26 19:45:07 +02:00
Stefan Zermatten
0f20cd4bd9 Implemented drag and drop on spells page 2020-07-26 19:39:50 +02:00
Stefan Zermatten
1ac01941c7 Fixed bug where multiple classes woudn't show up in persona tab 2020-07-26 16:43:26 +02:00
Stefan Zermatten
95d8d2cb9a Started work on tabletop view 2020-07-17 23:31:12 +02:00
Stefan Zermatten
47345b3694 Experimenting with webhooks. 2020-07-13 16:38:24 +02:00
Stefan Zermatten
308168791b Made dX rolls work as 1dX 2020-06-30 15:15:55 +02:00
Stefan Zermatten
7be4280508 Began implementing dice rolls in the maths parser 2020-06-30 14:40:20 +02:00
Stefan Zermatten
56f9e82326 Improved spells tab to be more in line with the v1 implementation 2020-06-29 14:52:47 +02:00
Stefan Zermatten
6ddea8a8ab Improved slot schema, added ui for slots 2020-06-29 14:15:49 +02:00
Stefan Zermatten
e1ddfb2cab Fixed a bug where registering a property without disabledByToggle breaks recompuation 2020-06-24 18:20:56 +02:00
Stefan Zermatten
d36d5b15d0 hide unused spell card 2020-06-24 18:17:35 +02:00
Stefan Zermatten
2af687361e Improved spell appearance in spell tab 2020-06-23 01:36:48 +02:00
Stefan Zermatten
e572807082 Attributes of type spell slot now store their slot level 2020-06-23 01:36:21 +02:00
Stefan Zermatten
c44aeac198 Added 'prepared' field to spells 2020-06-22 13:45:47 +02:00
Stefan Zermatten
757cf5c34b Fixed spells not being able to be inserted or editing in characters 2020-06-22 13:45:35 +02:00
Stefan Zermatten
8cbfec25b3 Added the setting to swap ability scores and modifiers to the account page 2020-06-22 13:22:53 +02:00
Stefan Zermatten
c4dc5895aa Relaxed rate limiting on icon search, improved error messaging 2020-06-22 00:20:40 +02:00
Stefan Zermatten
cffe0ee574 Added minimal UI to display applied buffs 2020-06-22 00:14:07 +02:00
Stefan Zermatten
ce51be7b8e moved proficiencies after actions on the stats tab 2020-06-21 23:57:19 +02:00
Stefan Zermatten
315073bd8e Refactored actions and let actions apply buffs to self 2020-06-21 23:54:51 +02:00
Stefan Zermatten
50b99ef54f Improved performance of adding library properties with many decendants 2020-06-21 23:24:07 +02:00
Stefan Zermatten
9b01f5fb45 Improved actions UI, Actions (including spells) can now have icons 2020-06-17 13:23:13 +02:00
Stefan Zermatten
389785f5db Fixed bug where library large screen view won't scroll 2020-06-17 13:22:48 +02:00
Stefan Zermatten
e1bfb173ab Overhauled action detail view 2020-06-16 13:51:58 +02:00
Stefan Zermatten
ecba587253 Fixed a bug with proficiency forms not editing proficiency correctly 2020-06-16 12:35:50 +02:00
Stefan Zermatten
3f540d0f14 Overhaul of character action components, actions now consume resources 2020-06-15 22:30:27 +02:00
Stefan Zermatten
dc18734d1f Backend work to support actions consuming their resources on use 2020-06-13 23:11:49 +02:00
Stefan Zermatten
1535e00093 Denormalized some calculations into recomputation step 2020-06-07 21:08:53 +02:00
Stefan Zermatten
5198c655e9 Added subscription rate limiting 2020-06-06 14:30:15 +02:00
Stefan Zermatten
8d41643136 Increased damage property rate limit to 4/s 2020-06-06 14:25:23 +02:00
Stefan Zermatten
ea8d036c72 Added rate limiting to all methods 2020-06-06 14:23:13 +02:00
Stefan Zermatten
93d566e263 Exposed methods and publications to http requests, changed method names 2020-06-06 12:31:07 +02:00
Stefan Zermatten
b4da32f9ab Fixed soft removed documents never getting permanently removed 2020-06-05 23:08:31 +02:00
Stefan Zermatten
986fe8fd93 Added an autofocus field to most forms 2020-06-05 22:39:21 +02:00
Stefan Zermatten
dd4596851e Improved class level viewer and tree node view 2020-06-05 22:25:22 +02:00
Stefan Zermatten
bc3fc9574a Added loading and empty state to experience list 2020-06-05 22:20:40 +02:00
Stefan Zermatten
db1ae5db3d Iterated on XP system 2020-06-05 21:48:28 +02:00
Stefan Zermatten
d1e7eb2fa0 Added basic XP system 2020-06-05 16:14:26 +02:00
Stefan Zermatten
efb8b87a2d Alphabetized properties by displayed name 2020-05-31 22:39:15 +02:00
Stefan Zermatten
b04b915c7b Removed stray logging 2020-05-31 22:36:27 +02:00
Stefan Zermatten
21b823f85c Dark mode now with 20% more dark 2020-05-31 22:25:04 +02:00
Stefan Zermatten
4631579181 Character toolbar now correctly uses dark and light text where appropriate 2020-05-31 22:22:42 +02:00
Stefan Zermatten
edf3920e84 Character sheet toolbars now match the color of the character 2020-05-31 22:16:38 +02:00
Stefan Zermatten
fb91fd12df Set up custom icons for most properties 2020-05-31 21:03:45 +02:00
Stefan Zermatten
19f4735412 Icon search field now focuses when the menu is opened 2020-05-31 19:18:49 +02:00
Stefan Zermatten
fb2f1efa72 Property insert forms now have color selectors 2020-05-31 19:00:32 +02:00
Stefan Zermatten
f7ee09470e Improved container and item forms and viewers 2020-05-31 18:50:00 +02:00
Stefan Zermatten
a5c42fea19 Made custom svg icons work anywhere a regular icon would work 2020-05-31 18:49:46 +02:00
Stefan Zermatten
8f81614294 Weight and value are no longer required on containers 2020-05-31 15:59:37 +02:00
Stefan Zermatten
c56cebc652 Fixed dark/light font color swapping not working in dark mode 2020-05-31 15:58:21 +02:00
Stefan Zermatten
d24fb5661d re-enabled computation on client side for optimistic UI 2020-05-30 23:56:55 +02:00
Stefan Zermatten
4bdc254627 Improved item viewer significantly, including increment button. 2020-05-30 23:36:27 +02:00
Stefan Zermatten
db652ac47f Update paragon avatar 2020-05-30 19:28:54 +02:00
Stefan Zermatten
32c9283569 Library items can now correctly store icons 2020-05-30 18:19:57 +02:00
Stefan Zermatten
060c44f384 Added svg icons, currently only for items 2020-05-30 18:04:48 +02:00
Stefan Zermatten
8138cd98f1 Added quantities to item tree views 2020-05-30 12:52:15 +02:00
Stefan Zermatten
5195e3fad5 Made health bar input accept negative sign from copy-paste and mobile (hopefully) 2020-05-30 12:45:11 +02:00
Stefan Zermatten
eb97a98644 removed dead reference to reset multipliers 2020-05-30 12:34:00 +02:00
Stefan Zermatten
51845c62a7 Skill base values now work in a way consistent with attribute base values 2020-05-30 12:32:05 +02:00
Stefan Zermatten
56cd48da9d Fixed health bars not hiding 2020-05-29 00:38:12 +02:00
Stefan Zermatten
298f659829 Fixed broken import 2020-05-28 23:48:01 +02:00
Stefan Zermatten
b99d1a00f5 Fixed small issues with hit dice on long rest. rests trigger recomputations now 2020-05-28 23:43:03 +02:00
Thaum Rystra
15ad8b1f5d Added short and long rest buttons, closes #87 2020-05-28 23:17:25 +02:00
Thaum Rystra
d4804e5292 Made minimum variable name 2 characters long 2020-05-28 21:26:31 +02:00
Thaum Rystra
36c23e1eb5 Made hiding stats that aren't targeted by effects or proficiencies an option 2020-05-28 21:06:40 +02:00
Thaum Rystra
9236f3e477 Added calculation errors to attributes and toggles 2020-05-28 20:33:08 +02:00
Thaum Rystra
cd413ba64f Added icon for set effects 2020-05-28 20:17:16 +02:00
Thaum Rystra
2c671acf72 Made sure effects without calculations don't have computed results 2020-05-28 20:14:19 +02:00
Thaum Rystra
44e726417e Convert mathjs objects to strings in evaluations 2020-05-28 20:10:33 +02:00
Thaum Rystra
7f2401da81 Referencing a missing variable in an effect now returns zero, not an error 2020-05-28 19:58:52 +02:00
Thaum Rystra
d31f980002 Added paragon's title 2020-05-28 17:25:44 +02:00
Thaum Rystra
4c8512af80 Rounding only occurs on numbers, preventing uneccessary type casting of attribute values 2020-05-28 16:06:00 +02:00
Thaum Rystra
edf68b1355 Properties in dropdowns are sorted by order again, rather than name 2020-05-28 15:59:04 +02:00
Thaum Rystra
868b9e11fa Added 'set' operation to effects, it overrides all other numerical effects 2020-05-28 15:58:48 +02:00
Thaum Rystra
14f5c3e797 improved field naming for damage multiplier tag targeting 2020-05-28 15:47:02 +02:00
Thaum Rystra
66e25c53d0 Fixed paragon's avatar image 2020-05-28 15:46:39 +02:00
Thaum Rystra
7a75d34246 Added healing damage type 2020-05-28 15:41:46 +02:00
Thaum Rystra
70a6c817cb Organised images, added about page, tweaked home page 2020-05-28 15:27:55 +02:00
Thaum Rystra
56879f1911 Removing a property in the character sheet tree now unselects that property 2020-05-28 13:03:35 +02:00
Thaum Rystra
6d12bcb063 Public libraries no longer require login to view 2020-05-28 13:00:03 +02:00
Thaum Rystra
1c26b7717c Fixed saving throw fields that weren't working, added name to saving throws 2020-05-28 12:29:41 +02:00
Thaum Rystra
a41b267364 Use embedded property dialog in tree tab. Colors for creature properties 2020-05-25 19:36:14 +02:00
Thaum Rystra
dfb144b8dc Added color picking to library properties 2020-05-25 19:09:55 +02:00
Thaum Rystra
2859bf0e00 Added fade transition to library dialog 2020-05-25 18:41:22 +02:00
Thaum Rystra
469822d4d7 In organize mode, new library properties get placed under the selected node 2020-05-25 18:33:38 +02:00
Thaum Rystra
c7de96c8c3 Added "move" button to library property menu 2020-05-25 18:15:35 +02:00
Thaum Rystra
f7cbee27f9 Made selecting a property from a library use the mobile friendly library 2020-05-25 17:39:25 +02:00
Thaum Rystra
b61dd6e81a Added maximum length of ancestors array 2020-05-25 17:25:49 +02:00
Thaum Rystra
add0cac31d Added "duplicate" option to library properties 2020-05-25 17:23:36 +02:00
Thaum Rystra
e9c643699c Made tab swiping sync with the tab list 2020-05-25 17:07:38 +02:00
Thaum Rystra
3ec0f9500c Overhauled library UI to work on small screens 2020-05-25 16:43:28 +02:00
Thaum Rystra
a55c1382b1 Fixed skills not computing below zero 2020-05-24 04:30:14 +02:00
Thaum Rystra
7571806cd0 Made user profiles optional 2020-05-23 12:07:42 +02:00
Thaum Rystra
afa641a290 made ids optional in users publication 2020-05-21 16:52:36 +02:00
Thaum Rystra
81131ddb9f Allowed non-patreons to view, but not edit, sheets and libraries 2020-05-21 16:50:06 +02:00
Thaum Rystra
7a442d8fb9 Improved publications to be reactive to permission changes 2020-05-21 15:13:30 +02:00
Thaum Rystra
26d836767b Fixed some broken forms 2020-05-21 14:57:35 +02:00
Thaum Rystra
c4a52ca744 Display an error if the character can't be found or viewed 2020-05-21 13:36:59 +02:00
Thaum Rystra
b640ce457f Removed unused story files 2020-05-21 12:49:40 +02:00
Thaum Rystra
13a0d66433 Disabled various buttons when the user doesn't have edit permission 2020-05-21 12:47:02 +02:00
Thaum Rystra
47aad6d186 Added UI to unshare a view-only character with yourself 2020-05-20 16:52:05 +02:00
Thaum Rystra
32eb85a099 Refactored all forms to disable all fields without edit permission 2020-05-20 16:40:47 +02:00
Thaum Rystra
048b150c88 Significantly improved performance of interacting with large library trees 2020-05-19 01:28:29 +02:00
Thaum Rystra
65d367942e Got cards to behave themselves in columns and not overflow width 2020-05-19 01:03:18 +02:00
Thaum Rystra
e0fc5abe7b Improved invite landing page UI 2020-05-19 00:28:31 +02:00
Thaum Rystra
18c9474570 Separated attacks and actions into two separate cards 2020-05-19 00:19:23 +02:00
Thaum Rystra
4a039e769b Added viewers, tree node viewers and fixed forms for new damage schema 2020-05-19 00:15:05 +02:00
Thaum Rystra
93ab67a91b Fixed failure to recompute creature on tree reorganize 2020-05-18 23:53:34 +02:00
Thaum Rystra
4352ca5f0d Inventory items can now be equipped 2020-05-18 23:03:34 +02:00
Thaum Rystra
fe11c9ec23 Tree nodes are no longer lazy - smoother animations when expanding nodes 2020-05-18 20:22:48 +02:00
Thaum Rystra
37d6b32ea3 Action 'uses' now shows up as the computed value 2020-05-18 20:22:00 +02:00
Thaum Rystra
7592332637 Fixed setDocToLastOrder not working with new ordering design 2020-05-18 20:09:21 +02:00
Thaum Rystra
397ff82c43 Organizing the tree now causes a character recomputation where relevant 2020-05-18 19:58:28 +02:00
Thaum Rystra
7e3815a699 Fixed glitchy reordering of trees 2020-05-18 02:03:31 +02:00
Thaum Rystra
9214529284 rewrote entire ordering structure for ancestor trees 2020-05-18 02:03:14 +02:00
Thaum Rystra
60f5588e7d Prevented description viewers from keeping zombie text after description is deleted 2020-05-17 18:13:06 +02:00
Thaum Rystra
ad3bec3521 Began working on bringing forms and UI in line with data structure overhaul 2020-05-17 00:06:19 +02:00
Thaum Rystra
ca5ded7ded Class levels now recompute properly 2020-05-16 22:51:17 +02:00
Thaum Rystra
5c0a2a4d6c Overhauled computations to allow for toggles :'( that sucked 2020-05-16 22:03:21 +02:00
Thaum Rystra
7024adecaf created a general way to fetch the active properties of an ancestor 2020-05-16 17:08:57 +02:00
Thaum Rystra
9b6f259358 Library node edit form no longer uses stored variants 2020-05-16 14:27:54 +02:00
Thaum Rystra
3642d1d249 Workaround for Firefox not obeying break-inside: avoid; 2020-05-16 14:19:13 +02:00
Thaum Rystra
cd2727b61c Can now link google account 2020-05-16 14:07:22 +02:00
Thaum Rystra
acb9dc342a Improved handling of character avatars, added portraits 2020-05-16 13:40:54 +02:00
Thaum Rystra
d59d8cb54f Library insert forms no longer used stored variants 2020-05-16 12:51:59 +02:00
Thaum Rystra
2c988b8717 Fixed an error where incorrectly targeted effects would cause computation error 2020-05-15 17:27:34 +02:00
Thaum Rystra
3af48649f7 Added some guards against missing properties 2020-05-15 16:51:58 +02:00
Thaum Rystra
79e03e0e63 Separated tool, weapon, armor, and language proficiencies into separate cards 2020-05-15 16:43:37 +02:00
Thaum Rystra
2d788f0c07 ability scores now pass on their skill effects to checks and skills 2020-05-15 16:38:28 +02:00
Thaum Rystra
891fd00b5f Skills now correctly denormalise their passive bonus, conditional benefits, advantage, and fail effects 2020-05-15 16:23:57 +02:00
Thaum Rystra
41b05064c8 login redirects now carry over to the register page 2020-05-15 15:16:59 +02:00
Thaum Rystra
cf110db67d Effect stats input now uses chips 2020-05-15 15:09:46 +02:00
Thaum Rystra
db696574f5 Comboboxes now clear search text when selecting an option 2020-05-15 15:09:34 +02:00
Thaum Rystra
4478628200 Reworked how bare symbols are handled, which should fix simplification 2020-05-15 14:44:08 +02:00
Thaum Rystra
cd8a557120 Let effects autofill skills as well as attributes 2020-05-15 14:29:07 +02:00
Thaum Rystra
5f95471bb6 Added transitions to tree tab property viewer 2020-05-15 14:23:32 +02:00
Thaum Rystra
a62726ae3a Allowed long property viewers in tree tab to scroll 2020-05-15 13:57:09 +02:00
Thaum Rystra
59fa5bcd8c Improved spacing on cards 2020-05-15 13:54:43 +02:00
Thaum Rystra
4b3f068d87 Fixed character sheet tabs not taking up the full screen height 2020-05-15 13:38:31 +02:00
Thaum Rystra
a771d896a8 Prevented dialog titles from overflowing 2020-05-15 13:31:07 +02:00
Thaum Rystra
b439befd47 Stopped tree view overflowing horizontally 2020-05-15 13:23:34 +02:00
Thaum Rystra
b44a18c28c Made tree tab work on mobile. prevented overflow of long titles 2020-05-15 13:18:27 +02:00
Thaum Rystra
bae2f4181a removed old default docs 2020-05-15 11:10:30 +02:00
Thaum Rystra
9b46a91641 Fixed missing Creature imports 2020-05-15 11:10:21 +02:00
Thaum Rystra
fc9467177b hotfix to prevent character sheet going black if DamageMultipliers isn't present 2020-05-14 15:32:54 +02:00
Thaum Rystra
136dd5fd29 Removed stray logging 2020-05-14 15:25:58 +02:00
Thaum Rystra
cb34363a4e Damage multipliers now compute and show up on the character sheet 2020-05-14 15:22:23 +02:00
Thaum Rystra
a4d6adacff Fixed damage multiplier forms and viewers 2020-05-14 13:43:12 +02:00
Thaum Rystra
29588a87d0 Fixed dependency loops causing a stack overflow. Added level variable 2020-05-14 13:32:46 +02:00
Thaum Rystra
97fcb76454 Added if function 2020-05-13 10:42:20 +02:00
Thaum Rystra
9069ee8e35 Improved effect, skill, and attribute viewers 2020-05-13 10:28:39 +02:00
Thaum Rystra
170bac6934 Pruned unused components 2020-05-13 09:43:01 +02:00
Thaum Rystra
809426b183 Improved effect components 2020-05-13 09:42:24 +02:00
Thaum Rystra
e8728166a9 Moved effect components to components folder 2020-05-13 09:34:00 +02:00
Thaum Rystra
5046a847cf Quality pass over all publications, fixed public documents permission error 2020-05-13 09:29:29 +02:00
Thaum Rystra
b6c7ea8c4f Improved handling of tiers, made guest tier funcitonal, improved invites 2020-05-12 15:28:43 +02:00
Thaum Rystra
bbda0ea1b6 Invites can now be managed to some extent 2020-05-12 14:11:43 +02:00
Thaum Rystra
47206ccfc4 Continued implementing sharing 2020-05-12 12:27:24 +02:00
Thaum Rystra
dd213feb0a Added label to username edit field 2020-05-09 13:53:47 +02:00
Thaum Rystra
30ab216dc1 removed redundant publications 2020-05-09 13:45:11 +02:00
Thaum Rystra
ea88a9c41e Libraries can now be shared from the library edit dialog 2020-05-09 13:40:01 +02:00
Thaum Rystra
f0e22dc1ca Made feedback page go to discord 2020-05-09 13:33:33 +02:00
Thaum Rystra
8c608937bb Username can now be changed 2020-05-09 13:15:03 +02:00
Thaum Rystra
c3ed3d55ce Locked friends page since it's not implemented 2020-05-08 17:12:03 +02:00
Thaum Rystra
323cac9405 corrected not implemented page to 'beta' instead of 'alpha' 2020-05-08 16:58:01 +02:00
Thaum Rystra
62689174e2 removed stray console.log 2020-05-08 16:31:58 +02:00
549 changed files with 28716 additions and 11112 deletions

View File

@@ -66,5 +66,24 @@ You should see this:
=> App running at: http://localhost:3000/
```
Environmental Variables
-----------------------
```
MAIL_URL=smtp://<your smtp mail url>
METEOR_SETTINGS={ "public": { "environment": "production", "patreon": { "clientId": "<your patreon client ID>", "campaignId": "<your campaign id>" } }, "patreon": { "clientSecret": "<your client secret>", "creatorAccessToken": "<your creator access token>" } }
MONGO_OPLOG_URL=mongodb+srv://<your url for the oplog account of your mongo database>
MONGO_URL=mongodb+srv://<your url for the read/write account of your mongo database>
NPM_CONFIG_PRODUCTION=true
PROJECT_DIR=app
ROOT_URL=https://<url of your DiceCloud instance>
DEFAULT_LIBRARIES=<comma separated list of library ids that will be subscribed by default: "abc123,def456">
```
To disable Patreon features and unlock all paid restrictions for all users of your deployment, replace
`"patreon": { "clientId": ... }"` with `"disablePatreon": true` in the public key of the METEOR_SETTINGS environment variable.
Alternatively run `meteor run --settings exampleMeteorSettings.json` to start the app with the example settings that disable Patreon by default.
Now, visiting [](http://localhost:3000/) should show you an empty instance of
DiceCloud running.

View File

@@ -3,49 +3,51 @@
# 'meteor add' and 'meteor remove' will edit this file for you,
# but you can also edit it by hand.
accounts-password@1.6.0
accounts-ui@1.3.1
random@1.2.0
accounts-password
random
dburles:collection-helpers
reactive-var@1.0.11
underscore@1.0.10
reactive-var
underscore
momentjs:moment
dburles:mongo-collection-instances
percolate:migrations
accounts-google@1.3.3
email@1.2.3
accounts-google
email
meteorhacks:subs-manager
chuangbo:marked
meteor-base@1.4.0
mobile-experience@1.1.0
mongo@1.10.0
session@1.2.0
jquery@1.11.10
tracker@1.2.0
logging@1.1.20
reload@1.3.0
ejson@1.1.1
check@1.3.1
standard-minifier-js@2.6.0
shell-server@0.5.0
seba:minifiers-autoprefixer
meteor-base
mobile-experience
mongo
session
tracker
logging
reload
ejson
check
standard-minifier-js
shell-server
templates:array
ecmascript@0.14.3
es5-shim@4.8.0
reactive-dict@1.3.0
ecmascript
es5-shim
reactive-dict
percolate:synced-cron
ongoworks:speakingurl
service-configuration@1.0.11
google-config-ui@1.0.1
dynamic-import@0.5.2
ddp-rate-limiter@1.0.7
rate-limit@1.0.9
meteortesting:mocha
service-configuration
dynamic-import
ddp-rate-limiter
rate-limit
mdg:validated-method
akryum:vue-router2
static-html
aldeed:collection2@3.0.0
aldeed:collection2
aldeed:schema-index
akryum:vue-component
zer0th:meteor-vuetify-loader
accounts-patreon
bozhao:link-accounts
peerlibrary:reactive-publish
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.10.2
METEOR@2.2.1

View File

@@ -1,126 +1,129 @@
accounts-base@1.6.0
accounts-base@1.9.0
accounts-google@1.3.3
accounts-oauth@1.2.0
accounts-password@1.6.0
accounts-password@1.7.1
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.0.6
akryum:vue-sass@0.1.2
aldeed:collection2@3.4.1
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.2
babel-runtime@1.5.0
base64@1.0.12
binary-heap@1.0.11
blaze@2.3.4
blaze-tools@1.0.10
boilerplate-generator@1.7.0
bozhao:link-accounts@2.1.1
blaze-tools@1.1.2
boilerplate-generator@1.7.1
bozhao:link-accounts@2.4.0
caching-compiler@1.2.2
caching-html-compiler@1.1.3
caching-html-compiler@1.2.1
callback-hook@1.3.0
check@1.3.1
chuangbo:marked@0.3.5_1
coffeescript@2.4.1
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.1
ddp-common@1.4.0
ddp-rate-limiter@1.0.7
ddp-server@2.3.1
ddp-rate-limiter@1.0.9
ddp-server@2.3.3
deps@1.0.12
diff-sequence@1.1.1
dynamic-import@0.5.2
ecmascript@0.14.3
dynamic-import@0.6.0
ecmascript@0.15.1
ecmascript-runtime@0.7.0
ecmascript-runtime-client@0.10.0
ecmascript-runtime-server@0.9.0
ecmascript-runtime-client@0.11.1
ecmascript-runtime-server@0.10.1
ejson@1.1.1
email@1.2.3
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
id-map@1.1.0
html-tools@1.1.2
htmljs@1.1.1
http@1.4.4
id-map@1.1.1
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.3
meteortesting:mocha@1.1.5
meteortesting:mocha-core@7.0.1
minifier-css@1.5.0
minifier-js@2.6.0
minimongo@1.6.0
mikowals:batch-insert@1.2.0
minifier-css@1.5.4
minifier-js@2.6.1
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.24.0
mongo@1.10.0
mongo-decimal@0.1.1
momentjs:moment@2.29.1
mongo@1.11.1
mongo-decimal@0.1.2
mongo-dev-server@1.1.0
mongo-id@1.0.7
npm-bcrypt@0.9.3
npm-mongo@3.7.0
oauth@1.3.0
mongo-id@1.0.8
npm-bcrypt@0.9.4
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
percolate:migrations@0.9.8
peerlibrary:assert@0.3.0
peerlibrary:check-extension@0.7.0
peerlibrary:computed-field@0.10.0
peerlibrary:data-lookup@0.3.0
peerlibrary:extend-publish@0.6.0
peerlibrary:fiber-utils@0.10.0
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: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.1
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.1.2
seba:minifiers-autoprefixer@2.0.1
service-configuration@1.0.11
session@1.2.0
sha@1.0.9
shell-server@0.5.0
socket-stream-client@0.3.0
spacebars@1.0.15
spacebars-compiler@1.1.3
simple:json-routes@2.1.0
simple:rest@1.1.1
simple:rest-method-mixin@1.0.1
socket-stream-client@0.3.3
spacebars-compiler@1.3.0
srp@1.1.0
standard-minifier-js@2.6.0
static-html@1.2.2
standard-minifier-js@2.6.1
static-html@1.3.2
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.1
tmeasday:check-npm-versions@1.0.2
tracker@1.2.0
typescript@4.2.2
underscore@1.0.10
url@1.3.0
webapp@1.9.1
webapp-hashing@1.0.9
url@1.3.2
webapp@1.10.1
webapp-hashing@1.1.0
zer0th:meteor-vuetify-loader@0.1.30

View File

@@ -1,5 +1,6 @@
<head>
<link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/@mdi/font@5.x/css/materialdesignicons.min.css" rel="stylesheet">
<meta name="viewport" content="width=device-width initial-scale=1.0, user-scalable=no">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">

View File

@@ -1,2 +1,4 @@
import "/imports/ui/vueSetup.js";
import "/imports/ui/styles/stylesIndex.js";
import '/imports/ui/vueSetup.js';
import '/imports/ui/styles/stylesIndex.js';
import '/imports/client/config.js';
import '/imports/client/serviceWorker.js';

View File

@@ -0,0 +1,6 @@
{
"public": {
"environment": "production",
"disablePatreon": true
}
}

View File

@@ -1,11 +0,0 @@
import SimpleSchema from 'simpl-schema';
let Encounters = new Mongo.Collection("encounters");
let EncounterSchema = new SimpleSchema({
//an encounter is a single flow of time all parties in an encounter are in-sync time wise
});
Encounters.attachSchema(EncounterSchema);
export default Encounters;

View File

@@ -1,49 +0,0 @@
import SimpleSchema from 'simpl-schema';
let Parties = new Mongo.Collection("parties");
let partySchema = new SimpleSchema({
name: {
type: String,
defaultValue: "New Party",
trim: false,
optional: true,
},
characters: {
type: Array,
defaultValue: [],
},
characters: {
type: String,
regEx: SimpleSchema.RegEx.Id,
index: 1,
},
owner: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
});
Parties.attachSchema(partySchema);
Parties.allow({
insert: function(userId, doc) {
return userId && doc.owner === userId;
},
update: function(userId, doc, fields, modifier) {
return userId && doc.owner === userId;
},
remove: function(userId, doc) {
return userId && doc.owner === userId;
},
fetch: ["owner"],
});
Parties.deny({
update: function(userId, docs, fields, modifier) {
// can't change owners
return _.contains(fields, "owner");
}
});
export default Parties;

View File

@@ -1,297 +0,0 @@
import { Meteor } from 'meteor/meteor';
import { Mongo } from 'meteor/mongo';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import SimpleSchema from 'simpl-schema';
import ChildSchema, { RefSchema } from '/imports/api/parenting/ChildSchema.js';
import { recomputeCreature } from '/imports/api/creature/computation/recomputeCreature.js';
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
import Creatures from '/imports/api/creature/Creatures.js';
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 propertySchemasIndex from '/imports/api/properties/computedPropertySchemasIndex.js';
import {
setLineageOfDocs,
getAncestry,
renewDocIds
} from '/imports/api/parenting/parenting.js';
import {setDocToLastOrder} from '/imports/api/parenting/order.js';
let CreatureProperties = new Mongo.Collection('creatureProperties');
let CreaturePropertySchema = new SimpleSchema({
type: {
type: String,
allowedValues: Object.keys(propertySchemasIndex),
},
tags: {
type: Array,
defaultValue: [],
},
'tags.$': {
type: String,
},
disabled: {
type: Boolean,
optional: true,
},
});
for (let key in propertySchemasIndex){
let schema = new SimpleSchema({});
schema.extend(propertySchemasIndex[key]);
schema.extend(CreaturePropertySchema);
schema.extend(ChildSchema);
schema.extend(SoftRemovableSchema);
CreatureProperties.attachSchema(schema, {
selector: {type: key}
});
}
function getCreature(property){
if (!property) throw new Meteor.Error('No property provided');
let creature = Creatures.findOne(property.ancestors[0].id);
if (!creature) throw new Meteor.Error('Creature does not exist');
return creature;
}
function assertPropertyEditPermission(property, userId){
let creature = getCreature(property);
return assertEditPermission(creature, userId);
}
function recomputeCreatures(property){
for (let ref of property.ancestors){
if (ref.collection === 'creatures') {
recomputeCreature.call({charId: ref.id});
}
}
}
const insertProperty = new ValidatedMethod({
name: 'CreatureProperties.methods.insert',
validate: null,
run({creatureProperty}) {
assertPropertyEditPermission(creatureProperty, this.userId);
let _id = CreatureProperties.insert(creatureProperty);
let property = CreatureProperties.findOne(_id);
recomputeCreatures(property);
},
});
const insertPropertyFromLibraryNode = new ValidatedMethod({
name: 'CreatureProperties.methods.insertPropertyFromLibraryNode',
validate: new SimpleSchema({
nodeId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
parentRef: {
type: RefSchema,
},
}).validator(),
run({nodeId, parentRef}) {
// get the new ancestry for the properties
let {parentDoc, ancestors} = getAncestry({parentRef});
// Check permission to edit
if (parentRef.collection === 'creatures'){
assertEditPermission(parentDoc, this.userId);
} else if (parentRef.collection === 'creatureProperties'){
assertPropertyEditPermission(parentDoc, this.userId);
} else {
throw `${parentRef.collection} is not a valid parent collection`
}
// Fetch the library node and its decendents, provided they have not been
// removed
let node = LibraryNodes.findOne({
_id: nodeId,
removed: {$ne: true},
});
if (!node) throw `Node not found for nodeId: ${nodeId}`;
let oldParent = node.parent;
let nodes = LibraryNodes.find({
'ancestors.id': nodeId,
removed: {$ne: true},
}).fetch();
// The root node is last in the array of nodes
nodes.push(node);
// re-map all the ancestors
setLineageOfDocs({
docArray: nodes,
newAncestry: ancestors,
oldParent,
});
// Give the docs new IDs without breaking internal references
renewDocIds({
docArray: nodes,
collectionMap: {'libraryNodes': 'creatureProperties'}
});
// Order the root node
setDocToLastOrder({
collection: CreatureProperties,
doc: node,
});
// Insert the creature properties
let docId;
nodes.forEach(doc => {
docId = CreatureProperties.insert(doc);
});
// Recompute the creatures doc was attached to
let doc = CreatureProperties.findOne(docId);
recomputeCreatures(doc);
// Return the docId of the last property, the inserted root property
return docId;
},
})
const updateProperty = new ValidatedMethod({
name: 'CreatureProperties.methods.update',
validate({_id, path}){
if (!_id) return false;
// We cannot change these fields with a simple update
switch (path[0]){
case 'type':
case 'order':
case 'parent':
case 'ancestors':
case 'damage':
throw new Meteor.Error('Permission denied',
'This property can\'t be updated directly');
}
},
run({_id, path, value}) {
let property = CreatureProperties.findOne(_id);
assertPropertyEditPermission(property, this.userId);
let pathString = path.join('.');
let modifier;
// unset empty values
if (value === null || value === undefined){
modifier = {$unset: {[pathString]: 1}};
} else {
modifier = {$set: {[pathString]: value}};
}
CreatureProperties.update(_id, modifier, {
selector: {type: property.type},
});
recomputeCreatures(property);
},
});
const damageProperty = new ValidatedMethod({
name: 'CreatureProperties.methods.adjust',
validate: new SimpleSchema({
_id: SimpleSchema.RegEx.Id,
operation: {
type: String,
allowedValues: ['set', 'increment']
},
value: Number,
}).validator(),
run({_id, operation, value}) {
let currentProperty = CreatureProperties.findOne(_id);
// Check permissions
assertPropertyEditPermission(currentProperty, this.userId);
// Check if property can take damage
let schema = CreatureProperties.simpleSchema(currentProperty);
if (!schema.allowsKey('damage')){
throw new Meteor.Error(
'Damage property failed',
`Property of type "${currentProperty.type}" can't be damaged`
);
}
if (operation === 'set'){
let currentValue = currentProperty.value;
// Set represents what we want the value to be after damage
// So we need the actual damage to get to that value
let damage = currentValue - value;
// Damage can't exceed total value
if (damage > currentValue) damage = currentValue;
// Damage must be positive
if (damage < 0) damage = 0;
CreatureProperties.update(_id, {
$set: {damage}
}, {
selector: currentProperty
});
} else if (operation === 'increment'){
let currentValue = currentProperty.value - (currentProperty.damage || 0);
let currentDamage = currentProperty.damage;
let increment = value;
// Can't increase damage above the remaining value
if (increment > currentValue) increment = currentValue;
// Can't decrease damage below zero
if (-increment > currentDamage) increment = -currentDamage;
CreatureProperties.update(_id, {
$inc: {damage: increment}
}, {
selector: currentProperty
});
}
recomputeCreatures(currentProperty);
},
});
const pushToProperty = new ValidatedMethod({
name: 'CreatureProperties.methods.push',
validate: null,
run({_id, path, value}){
let property = CreatureProperties.findOne(_id);
assertPropertyEditPermission(property, this.userId);
CreatureProperties.update(_id, {
$push: {[path.join('.')]: value},
}, {
selector: {type: property.type},
});
recomputeCreatures(property);
}
});
const pullFromProperty = new ValidatedMethod({
name: 'CreatureProperties.methods.pull',
validate: null,
run({_id, path, itemId}){
let property = CreatureProperties.findOne(_id);
assertPropertyEditPermission(property, this.userId);
CreatureProperties.update(_id, {
$pull: {[path.join('.')]: {_id: itemId}},
}, {
selector: {type: property.type},
getAutoValues: false,
});
recomputeCreatures(property);
}
});
const softRemoveProperty = new ValidatedMethod({
name: 'CreatureProperties.methods.softRemove',
validate: new SimpleSchema({
_id: SimpleSchema.RegEx.Id
}).validator(),
run({_id}){
let property = CreatureProperties.findOne(_id);
assertPropertyEditPermission(property, this.userId);
softRemove({_id, collection: CreatureProperties});
recomputeCreatures(property);
}
});
export default CreatureProperties;
export {
CreaturePropertySchema,
insertProperty,
insertPropertyFromLibraryNode,
updateProperty,
damageProperty,
pushToProperty,
pullFromProperty,
softRemoveProperty,
};

View File

@@ -1,140 +0,0 @@
import SimpleSchema from 'simpl-schema';
import deathSaveSchema from '/imports/api/properties/subSchemas/DeathSavesSchema.js'
import ColorSchema from '/imports/api/properties/subSchemas/ColorSchema.js';
import SharingSchema from '/imports/api/sharing/SharingSchema.js';
import {assertEditPermission, assertOwnership} from '/imports/api/sharing/sharingPermissions.js';
import '/imports/api/creature/removeCreature.js';
//set up the collection for creatures
Creatures = new Mongo.Collection('creatures');
let CreatureSettingsSchema = new SimpleSchema({
//slowed down by carrying too much?
useVariantEncumbrance: {
type: Boolean,
optional: true,
},
//hide spellcasting tab
hideSpellcasting: {
type: Boolean,
optional: true,
},
// Swap around the modifier and stat
swapStatAndModifier: {
type: Boolean,
optional: true,
},
});
let CreatureSchema = new SimpleSchema({
// Strings
name: {
type: String,
defaultValue: '',
optional: true,
},
urlName: {
type: String,
optional: true,
autoValue: function() {
return getSlug(this.field('name').value, {maintainCase: true}) || '-';
},
},
alignment: {
type: String,
optional: true
},
gender: {
type: String,
optional: true
},
picture: {
type: String,
optional: true
},
// Mechanics
deathSave: {
type: deathSaveSchema,
defaultValue: {},
},
xp: {
type: SimpleSchema.Integer,
defaultValue: 0,
},
weightCarried: {
type: Number,
defaultValue: 0,
},
level: {
type: SimpleSchema.Integer,
defaultValue: 0,
},
type: {
type: String,
defaultValue: 'pc',
allowedValues: ['pc', 'npc', 'monster'],
},
variables: {
type: Object,
blackbox: true,
defaultValue: {}
},
// Settings
settings: {
type: CreatureSettingsSchema,
defaultValue: {},
},
});
CreatureSchema.extend(ColorSchema);
CreatureSchema.extend(SharingSchema);
Creatures.attachSchema(CreatureSchema);
const insertCreature = new ValidatedMethod({
name: 'Creatures.methods.insertCreature',
validate: null,
run() {
if (!this.userId) {
throw new Meteor.Error('Creatures.methods.insert.denied',
'You need to be logged in to insert a creature');
}
// Create the creature document
let charId = Creatures.insert({
owner: this.userId,
});
this.unblock();
return charId;
},
});
const updateCreature = new ValidatedMethod({
name: 'Creatures.methods.update',
validate({_id, path, value}){
if (!_id) return false;
// Allowed fields
let allowedFields = ['name', 'alignment', 'gender', 'picture', 'settings'];
if (!allowedFields.includes(path[0])){
throw new Meteor.Error('Creatures.methods.update.denied',
'This field can\'t be updated using this method');
}
},
run({_id, path, value}) {
let creature = Creatures.findOne(_id);
assertEditPermission(creature, this.userId);
Creatures.update(_id, {
$set: {[path.join('.')]: value},
});
},
});
export default Creatures;
export { CreatureSchema, insertCreature, removeCreature, updateCreature };

View File

@@ -1,146 +0,0 @@
// Don't just export a constant, because deep nested objects could be changed
// by code that requires it. Exporting a function that returns the newly created
// object is a little safer.
export default () => ({
"attributes": [
{"name": "Strength", "variableName": "strength", "baseValue": 10, "type": "ability"},
{"name": "Dexterity", "variableName": "dexterity", "baseValue": 10, "type": "ability"},
{"name": "Constitution", "variableName": "constitution", "baseValue": 10, "type": "ability"},
{"name": "Intelligence", "variableName": "intelligence", "baseValue": 10, "type": "ability"},
{"name": "Wisdom", "variableName": "wisdom", "baseValue": 10, "type": "ability"},
{"name": "Charisma", "variableName": "charisma", "baseValue": 10, "type": "ability"},
{"name": "Speed", "variableName": "speed", "baseValue": 30, "type": "stat"},
{"name": "Armor Class", "variableName": "armor", "baseValue": 10, "type": "stat"},
{"name": "d6 Hit Dice", "variableName": "d6HitDice", "type": "hitDice"},
{"name": "d8 Hit Dice", "variableName": "d8HitDice", "type": "hitDice"},
{"name": "d10 Hit Dice", "variableName": "d10HitDice", "type": "hitDice"},
{"name": "d12 Hit Dice", "variableName": "d12HitDice", "type": "hitDice"},
{"name": "Hit Points", "variableName": "hitPoints", "type": "healthBar"},
{"name": "Temporary Hit Points", "variableName": "tempHitPoints", "type": "healthBar"},
{"name": "Ki", "variableName": "ki", "type": "resource"},
{"name": "Rages", "variableName": "rages", "type": "resource"},
{"name": "Sourcery Points", "variableName": "sorceryPoints", "type": "resource"},
{"name": "Superiority Dice", "variableName": "superiorityDice", "type": "resource"},
{"name": "Expertise Dice", "variableName": "expertiseDice", "type": "resource"},
{"name": "Level 1 Spell Slots", "variableName": "level1SpellSlots", "type": "spellSlot"},
{"name": "Level 2 Spell Slots", "variableName": "level2SpellSlots", "type": "spellSlot"},
{"name": "Level 3 Spell Slots", "variableName": "level3SpellSlots", "type": "spellSlot"},
{"name": "Level 4 Spell Slots", "variableName": "level4SpellSlots", "type": "spellSlot"},
{"name": "Level 5 Spell Slots", "variableName": "level5SpellSlots", "type": "spellSlot"},
{"name": "Level 6 Spell Slots", "variableName": "level6SpellSlots", "type": "spellSlot"},
{"name": "Level 7 Spell Slots", "variableName": "level7SpellSlots", "type": "spellSlot"},
{"name": "Level 8 Spell Slots", "variableName": "level8SpellSlots", "type": "spellSlot"},
{"name": "Level 9 Spell Slots", "variableName": "level9SpellSlots", "type": "spellSlot"},
{"name": "Proficiency Bonus", "variableName": "proficiencyBonus", "type": "modifier"},
{"name": "Carry Capacity Multiplier", "variableName": "carryMultiplier", "type": "utility", "baseValue": 1},
{"name": "Rage Damage", "variableName": "rageDamage", "type": "utility"},
],
"skills": [
{"name": "Acrobatics", "variableName": "acrobatics", "ability": "dexterity", "type":"skill"},
{"name": "Animal Handling", "variableName": "animalHandling", "ability": "wisdom", "type":"skill"},
{"name": "Arcana", "variableName": "arcana", "ability": "intelligence", "type":"skill"},
{"name": "Athletics", "variableName": "athletics", "ability": "strength", "type":"skill"},
{"name": "Deception", "variableName": "deception", "ability": "charisma", "type":"skill"},
{"name": "History", "variableName": "history", "ability": "intelligence", "type":"skill"},
{"name": "Insight", "variableName": "insight", "ability": "wisdom", "type":"skill"},
{"name": "Intimidation", "variableName": "intimidation", "ability": "charisma", "type":"skill"},
{"name": "Investigation", "variableName": "investigation", "ability": "intelligence", "type":"skill"},
{"name": "Medicine", "variableName": "medicine", "ability": "wisdom", "type":"skill"},
{"name": "Nature", "variableName": "nature", "ability": "intelligence", "type":"skill"},
{"name": "Perception", "variableName": "perception", "ability": "wisdom", "type":"skill"},
{"name": "Performance", "variableName": "performance", "ability": "charisma", "type":"skill"},
{"name": "Persuasion", "variableName": "persuasion", "ability": "charisma", "type":"skill"},
{"name": "Religion", "variableName": "religion", "ability": "intelligence", "type":"skill"},
{"name": "Sleight of Hand", "variableName": "sleightOfHand", "ability": "dexterity", "type":"skill"},
{"name": "Stealth", "variableName": "stealth", "ability": "dexterity", "type":"skill"},
{"name": "Survival", "variableName": "survival", "ability": "wisdom", "type":"skill"},
{"name": "Strength Save", "variableName": "strengthSave", "ability": "strength", "type":"save"},
{"name": "Dexterity Save", "variableName": "dexteritySave", "ability": "dexterity", "type":"save"},
{"name": "Constitution Save", "variableName": "constitutionSave", "ability": "constitution", "type":"save"},
{"name": "Intelligence Save", "variableName": "intelligenceSave", "ability": "intelligence", "type":"save"},
{"name": "Wisdom Save", "variableName": "wisdomSave", "ability": "wisdom", "type":"save"},
{"name": "Charisma Save", "variableName": "charismaSave", "ability": "charisma", "type":"save"},
{"name": "Initiative", "variableName": "initiative", "ability": "dexterity", "type":"check"},
],
"damageMultipliers": [
{"name": "Acid Multiplier", "variableName":"acidMultiplier"},
{"name": "Bludgeoning Multiplier", "variableName":"bludgeoningMultiplier"},
{"name": "Cold Multiplier", "variableName":"coldMultiplier"},
{"name": "Fire Multiplier", "variableName":"fireMultiplier"},
{"name": "Force Multiplier", "variableName":"forceMultiplier"},
{"name": "Lightning Multiplier", "variableName":"lightningMultiplier"},
{"name": "Necrotic Multiplier", "variableName":"necroticMultiplier"},
{"name": "Piercing Multiplier", "variableName":"piercingMultiplier"},
{"name": "Poison Multiplier", "variableName":"poisonMultiplier"},
{"name": "Psychic Multiplier", "variableName":"psychicMultiplier"},
{"name": "Radiant Multiplier", "variableName":"radiantMultiplier"},
{"name": "Slashing Multiplier", "variableName":"slashingMultiplier"},
{"name": "Thunder Multiplier", "variableName":"thunderMultiplier"},
],
"effects": [
{
"name": "Proficiency bonus by level",
"stat": "proficiencyBonus",
"operation": "add",
"calculation": "floor(level / 4 + 1.75)",
"enabled": true,
},{
"name": "Constitution modifier per level",
"stat": "hitPoints",
"operation": "add",
"calculation": "constitutionMod * level",
"enabled": true,
},
],
"containers": [
{
"name": "Coin Pouch",
"isCarried": true,
"description": "A sturdy pouch for coins",
"items": [
{
"name": "Gold piece",
"plural": "Gold pieces",
"quantity": 0,
"weight": 0.02,
"value": 1,
"settings": {
"showIncrement": true,
},
}, {
"name": "Silver piece",
"plural": "Silver pieces",
"quantity": 0,
"weight": 0.02,
"value": 0.1,
"settings": {
"showIncrement": true,
},
}, {
"name": "Copper piece",
"plural": "Copper pieces",
"quantity": 0,
"weight": 0.02,
"value": 0.01,
"settings": {
"showIncrement": true,
},
},
],
},
],
});

View File

@@ -0,0 +1,13 @@
import spendResources from '/imports/api/creature/actions/spendResources.js'
import embedInlineCalculations from '/imports/api/creature/computation/afterComputation/embedInlineCalculations.js';
export default function applyAction({prop, log}){
let content = { name: prop.name };
if (prop.summary){
content.value = embedInlineCalculations(
prop.summary, prop.summaryCalculations
);
}
log.content.push(content);
spendResources({prop, log});
}

View File

@@ -0,0 +1,55 @@
import evaluateString from '/imports/api/creature/computation/afterComputation/evaluateString.js';
import damagePropertiesByName from '/imports/api/creature/creatureProperties/methods/damagePropertiesByName.js';
export default function applyAdjustment({
prop,
creature,
targets,
actionContext,
log
}){
let damageTargets = prop.target === 'self' ? [creature] : targets;
let scope = {
...creature.variables,
...actionContext,
};
var {result, context} = evaluateString({
string: prop.amount,
scope,
fn: 'reduce'
});
context.errors.forEach(e => {
log.content.push({
name: 'Attribute damage error',
value: e.message || e.toString(),
});
});
if (damageTargets) {
damageTargets.forEach(target => {
if (prop.target === 'each'){
({result} = evaluateString({
string: prop.amount,
scope,
fn: 'reduce'
}));
}
damagePropertiesByName.call({
creatureId: target._id,
variableName: prop.stat,
operation: prop.operation || 'increment',
value: result.value,
});
log.content.push({
name: 'Attribute damage',
value: `${prop.stat}${prop.operation === 'set' ? ' set to' : ''}` +
` ${result.isNumber ? -result.value : result.toString()}`,
});
});
} else {
log.content.push({
name: 'Attribute damage',
value: `${prop.stat}${prop.operation === 'set' ? ' set to' : ''}` +
` ${result.isNumber ? -result.value : result.toString()}`,
});
}
}

View File

@@ -0,0 +1,22 @@
import roll from '/imports/parser/roll.js';
export default function applyAttack({
prop,
log,
actionContext,
creature,
}){
let value = roll(1, 20)[0];
actionContext.attackRoll = {value};
let criticalHitTarget = creature.variables.criticalHitTarget &&
creature.variables.criticalHitTarget.currentValue || 20;
let criticalHit = value >= criticalHitTarget;
if (criticalHit) actionContext.criticalHit = {value: true};
let result = value + prop.rollBonusResult;
actionContext.toHit = {value: result};
log.content.push({
name: criticalHit ? 'Critical Hit!' : 'To Hit',
value: `1d20 [${value}] + ${prop.rollBonusResult} = ` + result,
});
}

View File

@@ -0,0 +1,61 @@
import {
setLineageOfDocs,
renewDocIds
} from '/imports/api/parenting/parenting.js';
import {setDocToLastOrder} from '/imports/api/parenting/order.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
export default function applyBuff({
prop,
children,
creature,
targets = [],
//actionContext,
}){
let buffTargets = prop.target === 'self' ? [creature] : targets;
//let scope = {
// ...creature.variables,
// ...actionContext,
//};
// TODO
// If the target is not self, walk through all decendants and replace
// variables in calculations with their values from the creature scope
// If the target is self, replace all the target.x references with just x
// Then copy the decendants of the buff to the targets
prop.applied = true;
let propList = [prop];
function addChildrenToPropList(children){
children.forEach(child => {
propList.push(child.node);
addChildrenToPropList(child.children);
});
}
addChildrenToPropList(children);
let oldParent = {
id: prop.parent.id,
collection: prop.parent.collection,
};
buffTargets.forEach(target => {
copyNodeListToTarget(propList, target, oldParent);
});
}
function copyNodeListToTarget(propList, target, oldParent){
let ancestry = [{collection: 'creatures', id: target._id}];
setLineageOfDocs({
docArray: propList,
newAncestry: ancestry,
oldParent,
});
renewDocIds({
docArray: propList,
});
setDocToLastOrder({
collection: CreatureProperties,
doc: propList[0],
});
CreatureProperties.batchInsert(propList);
}

View File

@@ -0,0 +1,115 @@
import evaluateString from '/imports/api/creature/computation/afterComputation/evaluateString.js';
import dealDamage from '/imports/api/creature/creatureProperties/methods/dealDamage.js';
import {insertCreatureLog} from '/imports/api/creature/log/CreatureLogs.js';
import { CompilationContext } from '/imports/parser/parser.js';
export default function applyDamage({
prop,
creature,
targets,
actionContext,
log,
}){
let damageTargets = prop.target === 'self' ? [creature] : targets;
let scope = {
...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,
});
// 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({
name: 'Damage error',
value: result.toString(),
});
return;
}
// Memoise the damage suffix for the log
let suffix = (criticalHit ? ' critical ' : ' ') +
prop.damageType +
(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, 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.value,
});
// Log the damage done
if (target._id === creature._id){
// Target is same as self, log damage as such
log.content.push({
name,
value: damageDealt + suffix + ' to self',
});
} else {
log.content.push({
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,
value: 'Recieved ' + damageDealt + suffix,
}],
}
});
}
});
} else {
// There are no targets, just log the result
log.content.push({
name: prop.damageType === 'healing' ? 'Healing' : 'Damage',
value: result.toString() + suffix,
});
}
}

View File

@@ -0,0 +1,82 @@
import applyAction from '/imports/api/creature/actions/applyAction.js';
import applyAdjustment from '/imports/api/creature/actions/applyAdjustment.js';
import applyAttack from '/imports/api/creature/actions/applyAttack.js';
import applyBuff from '/imports/api/creature/actions/applyBuff.js';
import applyDamage from '/imports/api/creature/actions/applyDamage.js';
import applyRoll from '/imports/api/creature/actions/applyRoll.js';
import applyToggle from '/imports/api/creature/actions/applyToggle.js';
import applySave from '/imports/api/creature/actions/applySave.js';
function applyProperty(options){
let prop = options.prop;
if (prop.type === 'buff'){
// ignore only applied buffs, don't apply them again
if (prop.applied === true){
return false;
}
// Only ignore toggles if they wont be computed
} else if (prop.type === 'toggle') {
if (prop.disabled) return false;
if (prop.enabled) return true;
if (!prop.condition) return false;
// Ignore inactive props of other types
} else if (prop.deactivatedBySelf === true){
return false;
}
switch (prop.type){
case 'action':
case 'spell':
applyAction(options);
break;
case 'attack':
applyAction(options);
applyAttack(options);
break;
case 'damage':
applyDamage(options);
break;
case 'adjustment':
applyAdjustment(options);
break;
case 'buff':
applyBuff(options);
return false;
case 'toggle':
return applyToggle(options);
case 'roll':
applyRoll(options);
break;
case 'savingThrow':
return applySave(options);
}
return true;
}
function applyPropertyAndWalkChildren({prop, children, targets, ...options}){
let shouldKeepWalking = applyProperty({ prop, children, targets, ...options });
if (shouldKeepWalking){
applyProperties({ forest: children, targets, ...options,});
}
}
export default function applyProperties({ forest, targets, ...options}){
forest.forEach(node => {
let prop = node.node;
options.actionContext[`#${prop.type}`] = prop;
let children = node.children;
if (shouldSplit(prop) && targets.length){
targets.forEach(target => {
let targets = [target]
applyPropertyAndWalkChildren({ targets, prop, children, ...options});
});
} else {
applyPropertyAndWalkChildren({prop, children, targets, ...options});
}
});
}
function shouldSplit(prop){
if (prop.target === 'each'){
return true;
}
}

View File

@@ -0,0 +1,25 @@
import evaluateString from '/imports/api/creature/computation/afterComputation/evaluateString.js';
export default function applyRoll({
prop,
creature,
actionContext,
log,
}){
let scope = {
...creature.variables,
...actionContext,
};
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

@@ -0,0 +1,76 @@
import evaluateString from '/imports/api/creature/computation/afterComputation/evaluateString.js';
import CreaturesProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import roll from '/imports/parser/roll.js';
export default function applySave({
prop,
creature,
actionContext,
log,
}){
let scope = {
...creature.variables,
...actionContext,
};
try {
// Calculate the DC
var {result} = evaluateString({
string: prop.dc,
scope,
fn: 'reduce'
});
let dc = result.value;
log.content.push({
name: prop.name,
value: ' DC ' + result.toString(),
});
if (prop.target === 'self'){
let save = CreaturesProperties.findOne({
'ancestors.id': creature._id,
type: 'skill',
skillType: 'save',
variableName: prop.stat,
removed: {$ne: true},
inactive: {$ne: true},
});
if (!save){
log.content.push({
name: 'Saving throw error',
value: 'No saving throw found: ' + prop.stat,
});
return;
}
let value, values, resultPrefix;
if (save.advantage === 1){
values = roll(2, 20).sort().reverse();
value = values[0];
resultPrefix = `Advantage: 1d20 [${values[0]},~~${values[1]}~~] + ${save.value} = `
} else if (save.advantage === -1){
values = roll(2, 20).sort();
value = values[0];
resultPrefix = `Disadvantage: 1d20 [${values[0]},~~${values[1]}~~] + ${save.value} = `
} else {
values = roll(1, 20);
value = values[0];
resultPrefix = `1d20 [${value}] + ${save.value} = `
}
actionContext.savingThrowRoll = {value};
let result = value + save.value;
actionContext.savingThrow = {value: result};
let saveSuccess = result >= dc;
log.content.push({
name: 'Save',
value: resultPrefix + result + (saveSuccess ? 'Passed' : 'Failed')
});
return !saveSuccess;
} else {
// TODO
return true;
}
} catch (e){
log.content.push({
name: 'Save error',
value: e.toString(),
});
}
}

View File

@@ -0,0 +1,33 @@
import evaluateString from '/imports/api/creature/computation/afterComputation/evaluateString.js';
export default function applyToggle({
prop,
creature,
actionContext,
log,
}){
let scope = {
...creature.variables,
...actionContext,
};
if (Number.isFinite(+prop.condition)){
return !!+prop.condition;
}
var {result} = evaluateString({
string: prop.condition,
scope,
fn: 'reduce'
});
if (result.constructor.name === 'ErrorNode') {
log.content.push({
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

@@ -0,0 +1,92 @@
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 Creatures from '/imports/api/creature/creatures/Creatures.js';
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js';
import { doActionWork } from '/imports/api/creature/actions/doAction.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import getAncestorContext from '/imports/api/creature/actions/getAncestorContext.js';
import recomputeInventory from '/imports/api/creature/denormalise/recomputeInventory';
import recomputeInactiveProperties from '/imports/api/creature/denormalise/recomputeInactiveProperties';
const castSpellWithSlot = new ValidatedMethod({
name: 'creatureProperties.castSpellWithSlot',
validate: new SimpleSchema({
spellId: SimpleSchema.RegEx.Id,
slotId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
optional: true,
},
targetId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
optional: true,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 10,
timeInterval: 5000,
},
run({spellId, slotId, targetId}) {
let spell = CreatureProperties.findOne(spellId);
// Check permissions
let creature = getRootCreatureAncestor(spell);
assertEditPermission(creature, this.userId);
let target = undefined;
if (targetId) {
target = Creatures.findOne(targetId);
assertEditPermission(target, this.userId);
}
let slotLevel = spell.level || 0;
if (slotLevel !== 0){
let slot = CreatureProperties.findOne(slotId);
if (!slot){
throw new Meteor.Error('No slot',
'Slot not found to cast spell');
}
if (!slot.currentValue){
throw new Meteor.Error('No slot',
'Slot depleted');
}
if (!(slot.spellSlotLevelValue >= spell.level)){
throw new Meteor.Error('Slot too small',
'Slot is not large enough to cast spell');
}
slotLevel = slot.spellSlotLevelValue;
damagePropertyWork({
property: slot,
operation: 'increment',
value: 1,
});
}
let actionContext = getAncestorContext(spell);
doActionWork({
action: spell,
actionContext: {slotLevel, ...actionContext},
creature,
targets: target ? [target] : [],
method: this,
});
// Note these lines only recompute the top-level creature, not the nearest one
// The acting creature might have a new item
recomputeInventory(creature._id);
// The spell might add properties which need to be activated
recomputeInactiveProperties(creature._id);
recomputeCreatureByDoc(creature);
if (target){
recomputeInventory(target._id);
recomputeInactiveProperties(target._id);
recomputeCreatureByDoc(target);
}
},
});
export default castSpellWithSlot;

View File

@@ -0,0 +1,99 @@
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 Creatures from '/imports/api/creature/creatures/Creatures.js';
import { CreatureLogSchema, insertCreatureLogWork } from '/imports/api/creature/log/CreatureLogs.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js';
import nodesToTree from '/imports/api/parenting/nodesToTree.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';
import getAncestorContext from '/imports/api/creature/actions/getAncestorContext.js';
const doAction = new ValidatedMethod({
name: 'creatureProperties.doAction',
validate: new SimpleSchema({
actionId: SimpleSchema.RegEx.Id,
targetIds: {
type: Array,
defaultValue: [],
maxCount: 10,
optional: true,
},
'targetIds.$': {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 10,
timeInterval: 5000,
},
run({actionId, targetIds = []}) {
let action = CreatureProperties.findOne(actionId);
// Check permissions
let creature = getRootCreatureAncestor(action);
// Build ancestor context
let actionContext = getAncestorContext(action);
assertEditPermission(creature, this.userId);
let targets = [];
targetIds.forEach(targetId => {
let target = Creatures.findOne(targetId);
assertEditPermission(target, this.userId);
targets.push(target);
});
doActionWork({action, creature, targets, actionContext, 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);
});
},
});
export function doActionWork({
action,
creature,
targets,
actionContext = {},
method
}){
// Create the log
let log = CreatureLogSchema.clean({
creatureId: creature._id,
creatureName: creature.name,
});
let decendantForest = nodesToTree({
collection: CreatureProperties,
ancestorId: action._id,
});
let startingForest = [{
node: action,
children: decendantForest,
}];
applyProperties({
forest: startingForest,
actionContext,
creature,
targets,
log,
});
insertCreatureLogWork({log, creature, method});
}
export default doAction;

View File

@@ -0,0 +1,56 @@
import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import roll from '/imports/parser/roll.js';
const doCheck = new ValidatedMethod({
name: 'creature.doCheck',
validate: new SimpleSchema({
creatureId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
optional: true,
},
attributeName: {
type: String,
optional: true,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 10,
timeInterval: 5000,
},
run({creatureId, attributeName}) {
let creature = Creatures.findOne(creatureId);
assertEditPermission(creature, this.userId);
let bonus = getAttributeValue({creature, attributeName})
return doCheckWork({bonus});
},
});
function getAttributeValue({creature, attributeName}){
let att = creature.variables[attributeName];
if (!att) throw new Meteor.Error('No such attribute',
`This creature does not have a ${attributeName} property`);
let bonus = att.attributeType === 'ability'? att.modifier : att.value;
return bonus || 0;
}
export function doCheckWork({bonus, advantage = 0}){
let rolls = roll(2,20);
let chosenRoll;
if (advantage === 1){
chosenRoll = Math.max.apply(rolls);
} else if (advantage === -1){
chosenRoll = Math.min.apply(rolls);
} else {
chosenRoll = rolls[0];
}
let result = chosenRoll + bonus;
return {rolls, bonus, chosenRoll, result};
}
export default doCheck;

View File

@@ -0,0 +1,15 @@
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
export default function getAncestorContext(prop){
// Build ancestor context
const actionContext = {};
let ancestorIds = prop.ancestors.map(ref => ref.id);
CreatureProperties.find({
_id: {$in: ancestorIds}
}, {
sort: {order: 1},
}).forEach(ancestor => {
actionContext[`#${ancestor.type}`] = ancestor;
});
return actionContext;
}

View File

@@ -0,0 +1,93 @@
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { adjustQuantityWork } from '/imports/api/creature/creatureProperties/methods/adjustQuantity.js';
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
export default function spendResources({prop, log}){
// Check Uses
if (prop.usesUsed >= prop.usesResult){
throw new Meteor.Error('Insufficient Uses',
'This prop has no uses left');
}
// Resources
if (prop.insufficientResources){
throw new Meteor.Error('Insufficient Resources',
'This creature doesn\'t have sufficient resources to perform this prop');
}
// Items
let itemQuantityAdjustments = [];
let spendLog = [];
let gainLog = [];
prop.resources.itemsConsumed.forEach(itemConsumed => {
if (!itemConsumed.itemId){
throw new Meteor.Error('Ammo not selected',
'No ammo was selected for this prop');
}
let item = CreatureProperties.findOne(itemConsumed.itemId);
if (!item || item.ancestors[0].id !== prop.ancestors[0].id){
throw new Meteor.Error('Ammo not found',
'The prop\'s ammo was not found on the creature');
}
if (!item.equipped){
throw new Meteor.Error('Ammo not equipped',
'The selected ammo is not equipped');
}
if (!itemConsumed.quantity) return;
itemQuantityAdjustments.push({
property: item,
operation: 'increment',
value: itemConsumed.quantity,
});
let logName = item.name;
if (itemConsumed.quantity > 1 || itemConsumed.quantity < -1){
logName = item.plural || logName;
}
if (itemConsumed.quantity > 0){
spendLog.push(logName + ': ' + itemConsumed.quantity);
} else if (itemConsumed.quantity < 0){
gainLog.push(logName + ': ' + -itemConsumed.quantity);
}
});
// No more errors should be thrown after this line
// Now that we have confirmed that there are no errors, do actual work
//Items
itemQuantityAdjustments.forEach(adjustQuantityWork);
// Use uses
if (prop.usesResult){
CreatureProperties.update(prop._id, {
$inc: {usesUsed: 1}
}, {
selector: prop
});
log.content.push({
name: 'Uses left',
value: prop.usesResult - (prop.usesUsed || 0) - 1,
});
}
// Damage stats
prop.resources.attributesConsumed.forEach(attConsumed => {
if (!attConsumed.quantity) return;
let stat = CreatureProperties.findOne(attConsumed.statId);
damagePropertyWork({
property: stat,
operation: 'increment',
value: attConsumed.quantity,
});
if (attConsumed.quantity > 0){
spendLog.push(stat.name + ': ' + attConsumed.quantity);
} else if (attConsumed.quantity < 0){
gainLog.push(stat.name + ': ' + -attConsumed.quantity);
}
});
// Log all the spending
if (gainLog.length) log.content.push({
name: 'Gained',
value: gainLog.join('\n'),
});
if (spendLog.length) log.content.push({
name: 'Spent',
value: spendLog.join('\n'),
});
}

View File

@@ -0,0 +1,57 @@
import SimpleSchema from 'simpl-schema';
// Archived creatures is an immutable collection of creatures that are no longer
// in use and can be safely archived by the mongoDB hosting service.
// It keeps the working datasets like creatureProperties much smaller
// than they would otherwise be.
let ArchivedCreatures = new Mongo.Collection('archivedCreatures');
// We use blackbox objects for everything:
// - saves time checking every object against a schema
// - doesn't accidentaly create indices defined in subschemas
// - The objects we are archiving have already been checked against their
// own schemas
let ArchivedCreatureSchema = new SimpleSchema({
owner: {
type: String,
regEx: SimpleSchema.RegEx.Id,
// The primary index on this collection
index: 1,
},
archiveDate: {
type: Date,
// Indexed so the archiving system can archive documents when they
// get to a certain age
index: 1,
},
creature: {
type: Object,
blackbox: true,
},
properties: {
type: Array,
},
'properties.$': {
type: Object,
blackbox: true,
},
experiences: {
type: Array,
},
'experiences.$': {
type: Object,
blackbox: true,
},
logs: {
type: Array,
},
'logs.$': {
type: Object,
blackbox: true,
},
});
ArchivedCreatures.attachSchema(ArchivedCreatureSchema);
import '/imports/api/creature/archive/methods/index.js';
export default ArchivedCreatures;

View File

@@ -0,0 +1,66 @@
import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import { assertOwnership } from '/imports/api/creature/creatures/creaturePermissions.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js';
import Experiences from '/imports/api/creature/experience/Experiences.js';
import { removeCreatureWork } from '/imports/api/creature/creatures/methods/removeCreature.js';
import ArchivedCreatures from '/imports/api/creature/archive/ArchivedCreatures.js';
function archiveCreature(creatureId){
// Build the archive document
const creature = Creatures.findOne(creatureId);
const properties = CreatureProperties.find({'ancestors.id': creatureId}).fetch();
const experiences = Experiences.find({creatureId}).fetch();
const logs = CreatureLogs.find({creatureId}).fetch();
let archiveCreature = {
owner: creature.owner,
archiveDate: new Date(),
creature,
properties,
experiences,
logs,
};
// Insert it
let id = ArchivedCreatures.insert(archiveCreature);
// Remove the original creature
removeCreatureWork(creatureId);
return id;
}
const archiveCreatures = new ValidatedMethod({
name: 'Creatures.methods.archiveCreatures',
validate: new SimpleSchema({
creatureIds: {
type: Array,
max: 10,
},
'creatureIds.$': {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 1,
timeInterval: 5000,
},
run({creatureIds}) {
for (let id of creatureIds){
assertOwnership(id, this.userId)
}
let archivedIds = [];
for (let id of creatureIds){
let archivedId = archiveCreature(id);
archivedIds.push(archivedId);
}
return archivedIds;
},
});
export default archiveCreatures;

View File

@@ -0,0 +1,2 @@
import '/imports/api/creature/archive/methods/archiveCreatures.js';
import '/imports/api/creature/archive/methods/restoreCreatures.js';

View File

@@ -0,0 +1,77 @@
import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import { assertOwnership } from '/imports/api/sharing/sharingPermissions.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js';
import Experiences from '/imports/api/creature/experience/Experiences.js';
import ArchivedCreatures from '/imports/api/creature/archive/ArchivedCreatures.js';
import { removeCreatureWork } from '/imports/api/creature/creatures/methods/removeCreature.js';
function restoreCreature(archiveId){
// Get the archive
const archivedCreature = ArchivedCreatures.findOne(archiveId);
// Insert the creature sub documents
// They still have their original _id's
Creatures.insert(archivedCreature.creature);
try {
// Add all the properties
if (archivedCreature.properties && archivedCreature.properties.length){
CreatureProperties.batchInsert(archivedCreature.properties);
}
if (archivedCreature.experiences && archivedCreature.experiences.length){
Experiences.batchInsert(archivedCreature.experiences);
}
if (archivedCreature.logs && archivedCreature.logs.length){
CreatureLogs.batchInsert(archivedCreature.logs);
}
// Remove the archived creature
ArchivedCreatures.remove(archiveId);
} catch (e) {
// If the above fails, delete the inserted creature
removeCreatureWork(archivedCreature.creature._id);
throw e;
}
// Do not recompute. The creature was in a computed and ordered state when
// we archived it, just restore everything as-is
return archivedCreature.creature._id;
}
const restoreCreatures = new ValidatedMethod({
name: 'Creatures.methods.restoreCreatures',
validate: new SimpleSchema({
archiveIds: {
type: Array,
max: 10,
},
'archiveIds.$': {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 1,
timeInterval: 5000,
},
run({archiveIds}) {
for (let id of archiveIds){
let archivedCreature = ArchivedCreatures.findOne(id, {
fields: {owner: 1}
});
assertOwnership(archivedCreature, this.userId)
}
let creatureIds = [];
for (let id of archiveIds){
let creatureId = restoreCreature(id);
creatureIds.push(creatureId);
}
return creatureIds;
},
});
export default restoreCreatures;

View File

@@ -1,187 +0,0 @@
import { includes, cloneDeep, has } from 'lodash';
export default class ComputationMemo {
constructor(props){
this.statsByVariableName = {};
this.originalPropsById = {};
this.propsById = {};
this.skillsByAbility = {};
this.unassignedEffects = [];
props.filter((prop) => {
// skip effects, proficiencies, and class levels for the next pass
if (
prop.type === 'effect' ||
prop.type === 'proficiency' ||
prop.type === 'classLevel'
) return true;
// Add all the stats
this.addStat(prop);
}).forEach((prop) => {
// Now add all effects and proficiencies
if (prop.type === 'effect'){
this.addEffect(prop);
} else if (prop.type === 'proficiency') {
this.addProficiency(prop);
} else if (prop.type === 'classLevel'){
this.addClassLevel(prop);
}
});
}
registerProperty(prop){
this.originalPropsById[prop._id] = cloneDeep(prop);
this.propsById[prop._id] = prop;
prop.computationDetails = propDetails(prop);
return prop;
}
storeHighestClassLevel(name, prop){
// Only store the highest level classLevel
let stat = this.statsByVariableName[name]
if (!stat){
this.statsByVariableName[name] = prop;
} else if (!has(stat, 'level')){
// Stat is overriden by an attribute
return;
} else if (stat.level < prop.level) {
this.statsByVariableName[name] = prop;
}
}
addClassLevel(prop){
prop = this.registerProperty(prop);
if (prop.variableName){
this.storeHighestClassLevel(prop.variableName, prop);
}
if (prop.baseClass){
this.storeHighestClassLevel(prop.baseClass, prop);
}
}
addStat(prop){
prop = this.registerProperty(prop);
let variableName = prop.variableName;
if (!variableName) return;
if (this.statsByVariableName[variableName]){
prop.value = NaN;
prop.computationDetails.error = 'variableNameCollision';
console.warn('variableNameCollision', prop);
return;
}
this.statsByVariableName[variableName] = prop;
if (
prop.type === 'skill' &&
isSkillCheck(prop) &&
prop.ability
){
this.addSkillToAbility(prop, prop.ability)
}
}
addSkillToAbility(prop, ability){
if (!this.skillsByAbility[ability]){
this.skillsByAbility[ability] = [];
}
this.skillsByAbility[ability].push(prop);
}
addEffect(prop){
prop = this.registerProperty(prop);
let targets = this.getEffectTargets(prop);
targets.forEach(target => {
target.computationDetails.effects.push(prop);
});
if (!targets.size){
this.unassignedEffects.push(prop);
}
}
getEffectTargets(prop){
let targets = new Set();
if (!prop.stats) return targets;
prop.stats.forEach((statName) => {
let target = this.statsByVariableName[statName];
if (!target) return;
targets.add(target);
if (isSkillOperation(prop) && isAbility(target)){
let extras = this.skillsByAbility[statName] || [];
targets.add(...extras)
}
});
return targets;
}
addProficiency(prop){
prop = this.registerProperty(prop);
let targets = this.getProficiencyTargets(prop);
targets.forEach(target => {
target.computationDetails.proficiencies.push(prop);
});
}
getProficiencyTargets(prop){
let targets = new Set();
if (!prop.stats) return targets;
prop.stats.forEach(statName => {
let target = this.statsByVariableName[statName];
if (!target) return;
targets.add(target);
if (isAbility(target) && isSkillCheck(prop)) {
let extras = this.skillsByAbility[statName] || [];
targets.add(...extras)
}
});
return targets;
}
}
const skillOperations = [
'advantage',
'disadvantage',
'passiveAdd',
'fail',
'conditional',
'rollBonus',
];
function isAbility(prop){
return prop.type === 'attribute' &&
prop.attributeType === 'ability'
}
function isSkillCheck(prop){
return includes(['skill', 'check', 'save', 'utility'], prop.skillType);
}
function isSkillOperation(prop){
return skillOperations.includes(prop.operation);
}
function propDetails(prop){
return propDetailsByType[prop.type]() || {};
}
const propDetailsByType = {
attribute(){
return {
computed: false,
busyComputing: false,
effects: [],
};
},
skill(){
return {
computed: false,
busyComputing: false,
effects: [],
proficiencies: [],
};
},
effect(){
return {
computed: false,
};
},
classLevel(){
return {
computed: true,
};
},
proficiency(){
return {};
},
damageMultiplier(){
return {};
},
}

View File

@@ -0,0 +1,11 @@
import INLINE_CALCULATION_REGEX from '/imports/constants/INLINE_CALCULTION_REGEX.js';
export default function embedInlineCalculations(string, calculations){
if (!string) return '';
if (!calculations) return string;
let index = 0;
return string.replace(INLINE_CALCULATION_REGEX, substring => {
let comp = calculations && calculations[index++];
return (comp && 'result' in comp) ? comp.result : substring;
});
}

View File

@@ -1,98 +1,67 @@
import * as math from 'mathjs';
import replaceBareSymbolsWithValueAccessor from '/imports/api/creature/computation/utility/replaceBareSymbolsWithValueAccessor.js';
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){
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 calc;
let node;
try {
calc = math.parse(string);
node = parse(string);
} catch (e) {
errors.push(e);
return {result: string, errors};
}
// Replace all bare symbols with symbol.value
let transformedCalc = calc.transform(replaceBareSymbolsWithValueAccessor);
// Evaluate the expression to a number or return with substitutions
try {
let result = transformedCalc.evaluate(scope);
return {result, errors};
} catch (e1){
errors.push(e1);
try {
let result = simplifyWithAccessors(calc, scope).toHTML();
return {result, errors};
} catch (e2){
errors.push(e2);
return {result: calc.toHTML(), errors};
}
context.storeError(e);
return {result: {value: string}, context};
}
node = replaceConstants({calc: node, context, scope});
let result = node[fn](scope, context);
return {result, context};
}
function simplifyWithAccessors(calc, scope){
let noAccessorCalc = calc.transform(substituteAccessors(scope));
return math.simplify(noAccessorCalc);
}
// returns a function to replace all accessors with either their resolved value
// or a symbol to simplify with
function substituteAccessors(scope){
return function(node){
if (node.isAccessorNode){
try {
return evaluateAccessor(node, scope);
} catch (e) {
return replaceAccessorWithSymbol(node);
// 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;
}
} else {
return node;
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;
}
// Throws error if symbol is undefined in scope
function evaluateAccessor(node, scope){
let value = node.evaluate(scope);
if (value === undefined){
throw 'Undefined symbol'
}
return new math.ConstantNode(value);
}
function replaceAccessorWithSymbol(node){
let symbolNode = new math.SymbolNode(node.toString());
return symbolNode;
}
/*
function overrideSymbolNodeHTML(symbolNode){
let safeName = escape(symbolNode.name);
symbolNode.toHTML = function(){
console.log('running custom tohtml function')
return `<span class="math-symbol math-substitution-failed">${safeName}</span>`
}
return symbolNode;
}
// Escape special HTML characters
// Copied directly from math.js source to help with overriding toHTML
function escape (value) {
let text = String(value)
text = text.replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
return text
}
*/

View File

@@ -1,11 +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){
if (!string) return string;
// Compute everything inside bracers
return string.replace(/\{([^{}]*)\}/g, function(match, p1){
let {result} = evaluateString(p1, scope);
return result;
});
}

View File

@@ -1,73 +0,0 @@
import computeStat from '/imports/api/creature/computation/computeStat.js';
export default function combineStat(stat, aggregator, memo){
if (stat.type === 'attribute'){
combineAttribute(stat, aggregator);
} else if (stat.type === 'skill'){
combineSkill(stat, aggregator, memo);
} else if (stat.type === 'damageMultiplier'){
combineDamageMultiplier(stat, memo);
}
}
function combineAttribute(stat, aggregator){
let result = (aggregator.base + aggregator.add) * aggregator.mul;
if (result < aggregator.min) result = aggregator.min;
if (result > aggregator.max) result = aggregator.max;
if (!stat.decimal) result = Math.floor(result);
stat.value = result;
stat.baseValue = aggregator.statBaseValue;
if (stat.attributeType === 'ability') {
stat.modifier = Math.floor((result - 10) / 2);
}
stat.currentValue = stat.value - (stat.damage || 0);
}
function combineSkill(stat, aggregator, memo){
// Skills are based on some ability Modifier
let ability = memo.statsByVariableName[stat.ability]
if (stat.ability && ability){
if (!ability.computationDetails.computed){
computeStat(ability, memo);
}
stat.abilityMod = ability.modifier;
}
// Combine all the child proficiencies
stat.proficiency = stat.baseProficiency || 0;
for (let i in stat.computationDetails.proficiencies){
let prof = stat.computationDetails.proficiencies[i];
if (prof.value > stat.proficiency) stat.proficiency = prof.value;
}
// Get the character's proficiency bonus to apply
let profBonusStat = memo.statsByVariableName['proficiencyBonus'];
let profBonus = profBonusStat && profBonusStat.value;
/** TODO level needs to be on the memo somewhere
if (typeof profBonus !== "number"){
profBonus = Math.floor(char.level / 4 + 1.75);
}
*/
// Multiply the proficiency bonus by the actual proficiency
profBonus *= stat.proficiency;
// Combine everything to get the final result
let result = (stat.abilityMod + profBonus + aggregator.add) * aggregator.mul;
if (result < aggregator.min) result = aggregator.min;
if (result > aggregator.max) result = aggregator.max;
result = Math.floor(result);
if (stat.base > result) result = stat.base;
stat.value = result;
}
function combineDamageMultiplier(stat){
if (stat.immunityCount) return 0;
let result;
if (stat.ressistanceCount && !stat.vulnerabilityCount){
result = 0.5;
} else if (!stat.ressistanceCount && stat.vulnerabilityCount){
result = 2;
} else {
result = 1;
}
stat.value = result;
}

View File

@@ -1,15 +0,0 @@
import evaluateCalculation from '/imports/api/creature/computation/evaluateCalculation.js';
export default function computeEffect(effect, memo){
if (effect.computationDetails.computed) return;
if (_.isFinite(effect.calculation)){
effect.result = +effect.calculation;
} else if(effect.operation === "conditional" || effect.operation === "rollBonus"){
effect.result = effect.calculation;
} else if(_.contains(["advantage", "disadvantage", "fail"], effect.operation)){
effect.result = 1;
} else {
effect.result = evaluateCalculation(effect.calculation, memo);
}
effect.computationDetails.computed = true;
}

View File

@@ -1,12 +0,0 @@
import { each, forOwn } from 'lodash';
import computeStat from '/imports/api/creature/computation/computeStat.js';
import computeEffect from '/imports/api/creature/computation/computeEffect.js';
export default function computeMemo(memo){
forOwn(memo.statsByVariableName, (stat) => {
computeStat (stat, memo);
});
each(memo.unassignedEffects, (effect) => {
computeEffect(effect, memo);
});
}

View File

@@ -1,30 +0,0 @@
import combineStat from '/imports/api/creature/computation/combineStat.js';
import computeEffect from '/imports/api/creature/computation/computeEffect.js';
import EffectAggregator from '/imports/api/creature/computation/EffectAggregator.js';
import { each } from 'lodash';
export default function computeStat(stat, memo){
// If the stat is already computed, skip it
if (stat.computationDetails.computed) return;
if (stat.computationDetails.busyComputing){
// Trying to compute this stat again while it is already computing.
// We must be in a dependency loop.
stat.computationDetails.computed = true;
stat.value = NaN;
stat.computationDetails.busyComputing = false;
stat.computationDetails.error = 'dependencyLoop';
console.warn('dependencyLoop', stat);
return;
}
// Compute and aggregate all the effects
let aggregator = new EffectAggregator(stat, memo)
each(stat.computationDetails.effects, (effect) => {
computeEffect(effect, memo);
aggregator.addEffect(effect);
});
// Conglomerate all the effects to compute the final stat values
combineStat(stat, aggregator, memo);
// Mark the attribute as computed
stat.computationDetails.computed = true;
stat.computationDetails.busyComputing = false;
}

View File

@@ -0,0 +1,289 @@
import { includes, cloneDeep } from 'lodash';
import findAncestorByType from '/imports/api/creature/computation/engine/findAncestorByType.js';
// The computation memo is an in-memory data structure used only during the
// computation process
export default class ComputationMemo {
constructor(props, creature){
this.statsByVariableName = {};
this.constantsByVariableName = {};
this.constantsById = {};
this.extraStatsByVariableName = {};
this.statsById = {};
this.originalPropsById = {};
this.propsById = {};
this.skillsByAbility = {};
this.unassignedEffects = [];
this.classLevelsById = {};
this.classes = {};
this.togglesById = {};
this.toggleIds = new Set();
// Equipped items that might be used as ammo
this.equipmentById = {};
// Properties that have calculations, but don't impact other properties
this.endStepPropsById = {};
// First note all the ids of all the toggles
props.forEach((prop) => {
if (
prop.type === 'toggle'
) {
this.toggleIds.add(prop._id);
}
});
props.filter((prop) => {
if (
prop.type === 'toggle'
) {
this.addToggle(prop);
} else {
return true;
}
}).filter((prop) => {
if (
prop.type === 'attribute' ||
prop.type === 'skill'
) {
// Add all the stats
this.addStat(prop);
} else if (
prop.type === 'item'
) {
this.addEquipment(prop);
} else {
return true;
}
}).forEach((prop) => {
// Now add everything else
if (prop.type === 'effect'){
this.addEffect(prop);
} else if (prop.type === 'proficiency') {
this.addProficiency(prop);
} else if (prop.type === 'classLevel'){
this.addClassLevel(prop);
} else if (prop.type === 'constant'){
this.addConstant(prop);
} else {
this.addEndStepProp(prop);
}
});
for (let name in creature.denormalizedStats){
if (!this.statsByVariableName[name]){
this.statsByVariableName[name] = {
variableName: name,
value: creature.denormalizedStats[name],
computationDetails: propDetailsByType.denormalizedStat(),
}
}
}
}
addConstant(prop){
prop = this.registerProperty(prop);
this.constantsById[prop._id] = prop;
}
registerProperty(prop){
this.originalPropsById[prop._id] = cloneDeep(prop);
this.propsById[prop._id] = prop;
prop.dependencies = [];
prop.computationDetails = propDetails(prop);
prop.ancestors.forEach(ancestor => {
if (this.toggleIds.has(ancestor.id)){
prop.computationDetails.toggleAncestors.push(ancestor.id);
}
});
return prop;
}
addToggle(prop){
prop = this.registerProperty(prop);
this.togglesById[prop._id] = prop;
}
addClassLevel(prop){
prop = this.registerProperty(prop);
this.classLevelsById[prop._id] = prop;
}
addStat(prop){
let variableName = prop.variableName;
if (!variableName) return;
let existingStat = this.statsByVariableName[variableName];
prop = this.registerProperty(prop);
if (existingStat){
existingStat.computationDetails.idsOfSameName.push(prop._id);
} else {
this.statsById[prop._id] = prop;
this.statsByVariableName[variableName] = prop;
if (
prop.type === 'skill' &&
isSkillCheck(prop) &&
prop.ability
){
this.addSkillToAbility(prop, prop.ability)
}
}
}
addSkillToAbility(prop, ability){
if (!this.skillsByAbility[ability]){
this.skillsByAbility[ability] = [];
}
this.skillsByAbility[ability].push(prop);
}
addEffect(prop){
prop = this.registerProperty(prop);
let targets = this.getEffectTargets(prop);
targets.forEach(target => {
if (target.computationDetails && target.computationDetails.effects){
target.computationDetails.effects.push(prop);
}
});
if (!targets.size){
this.unassignedEffects.push(prop);
}
}
getEffectTargets(prop){
let targets = new Set();
if (!prop.stats) return targets;
prop.stats.forEach((statName) => {
let target;
if (statName[0] === '#'){
target = findAncestorByType({
type: statName.slice(1),
prop,
memo: this
});
} else {
target = this.statsByVariableName[statName];
}
if (!target) return;
targets.add(target);
if (isSkillOperation(prop) && isAbility(target)){
let extras = this.skillsByAbility[statName] || [];
extras.forEach(ex =>{
// Only pass on ability effects to skills and checks
if (ex.skillType === 'skill' || ex.skillType === 'check'){
targets.add(ex)
}
});
}
});
return targets;
}
addProficiency(prop){
prop = this.registerProperty(prop);
let targets = this.getProficiencyTargets(prop);
targets.forEach(target => {
if(target.computationDetails.proficiencies){
target.computationDetails.proficiencies.push(prop);
}
});
}
getProficiencyTargets(prop){
let targets = new Set();
if (!prop.stats) return targets;
prop.stats.forEach(statName => {
let target = this.statsByVariableName[statName];
if (!target) return;
targets.add(target);
if (isAbility(target)) {
let extras = this.skillsByAbility[statName] || [];
extras.forEach(ex =>{
// Only pass on ability proficiencies to skills and checks
if (ex.skillType === 'skill' || ex.skillType === 'check'){
targets.add(ex)
}
});
}
});
return targets;
}
addEquipment(prop){
prop = this.registerProperty(prop);
this.equipmentById[prop._id] = prop;
}
addEndStepProp(prop){
prop = this.registerProperty(prop);
this.endStepPropsById[prop._id] = prop;
}
}
function isAbility(prop){
return prop.type === 'attribute' &&
prop.attributeType === 'ability'
}
function isSkillCheck(prop){
return includes(['skill', 'check', 'save', 'utility'], prop.skillType);
}
const skillOperations = [
'advantage',
'disadvantage',
'passiveAdd',
'fail',
'conditional',
'rollBonus',
];
function isSkillOperation(prop){
return skillOperations.includes(prop.operation);
}
function propDetails(prop){
return propDetailsByType[prop.type] && propDetailsByType[prop.type]() ||
propDetailsByType.default();
}
const propDetailsByType = {
default(){
return {
toggleAncestors: [],
};
},
toggle(){
return {
computed: false,
busyComputing: false,
toggleAncestors: [],
};
},
attribute(){
return {
computed: false,
busyComputing: false,
effects: [],
proficiencies: [],
toggleAncestors: [],
idsOfSameName: [],
};
},
skill(){
return {
computed: false,
busyComputing: false,
effects: [],
proficiencies: [],
toggleAncestors: [],
idsOfSameName: [],
};
},
effect(){
return {
computed: false,
busyComputing: false,
toggleAncestors: [],
};
},
classLevel(){
return {
computed: true,
toggleAncestors: [],
};
},
proficiency(){
return {
toggleAncestors: [],
};
},
denormalizedStat(){
return {
toggleAncestors: [],
};
}
}

View File

@@ -1,30 +1,32 @@
import evaluateCalculation from '/imports/api/creature/computation/evaluateCalculation.js';
export default class EffectAggregator{
constructor(stat, memo){
if (stat.baseValueCalculation){
this.statBaseValue = evaluateCalculation(stat.baseValueCalculation, memo);
this.base = +this.statBaseValue;
} else {
this.base = 0;
}
constructor(){
this.base = undefined;
this.add = 0;
this.mul = 1;
this.min = Number.NEGATIVE_INFINITY;
this.max = Number.POSITIVE_INFINITY;
this.advantage = 0;
this.disadvantage = 0;
this.passiveAdd = 0;
this.passiveAdd = undefined;
this.fail = 0;
this.set = undefined;
this.conditional = [];
this.rollBonus = [];
this.hasNoEffects = true;
}
addEffect(effect){
let result = effect.result;
if (this.hasNoEffects) this.hasNoEffects = false;
switch(effect.operation){
case 'base':
// Take the largest base value
this.base = result > this.base ? result : this.base;
if (Number.isFinite(result)){
if(Number.isFinite(this.base)){
this.base = Math.max(this.base, result);
} else {
this.base = result;
}
}
break;
case 'add':
// Add all adds together
@@ -42,6 +44,10 @@ export default class EffectAggregator{
// Take the smallest max value
this.max = result < this.max ? result : this.max;
break;
case 'set':
// Take the highest set value
this.set = this.set === undefined || result > this.set ? result : this.set;
break;
case 'advantage':
// Sum number of advantages
this.advantage++;
@@ -52,6 +58,7 @@ export default class EffectAggregator{
break;
case 'passiveAdd':
// Add all passive adds together
if (this.passiveAdd === undefined) this.passiveAdd = 0;
this.passiveAdd += result;
break;
case 'fail':

View File

@@ -0,0 +1,25 @@
import computeToggle from '/imports/api/creature/computation/engine/computeToggle.js';
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);
prop.dependencies = union(
prop.dependencies,
[toggle._id],
toggle.dependencies,
);
// Deactivate if the toggle is false
if (!toggle.toggleResult){
prop.inactive = true;
prop.deactivatedByAncestor = true;
prop.deactivatedByToggle = true;
}
});
}

View File

@@ -0,0 +1,197 @@
import computeStat from '/imports/api/creature/computation/engine/computeStat.js';
import computeProficiency from '/imports/api/creature/computation/engine/computeProficiency.js';
import evaluateCalculation from '/imports/api/creature/computation/engine/evaluateCalculation.js';
import stripFloatingPointOddities from '/imports/ui/utility/stripFloatingPointOddities.js';
import { union } from 'lodash';
export default function combineStat(stat, aggregator, memo){
if (stat.type === 'attribute'){
combineAttribute(stat, aggregator, memo);
} else if (stat.type === 'skill'){
combineSkill(stat, aggregator, memo);
} else if (stat.type === 'damageMultiplier'){
combineDamageMultiplier(stat, memo);
}
}
function getAggregatorResult(stat, aggregator){
let base;
if (!Number.isFinite(aggregator.base)){
base = stat.baseValue || 0;
} else if (!Number.isFinite(stat.baseValue)){
base = aggregator.base || 0;
} else {
base = Math.max(aggregator.base, stat.baseValue);
}
let result = (base + aggregator.add) * aggregator.mul;
if (result < aggregator.min) {
result = aggregator.min;
}
if (result > aggregator.max) {
result = aggregator.max;
}
if (aggregator.set !== undefined) {
result = aggregator.set;
}
if (!stat.decimal && Number.isFinite(result)){
result = Math.floor(result);
} else if (Number.isFinite(result)){
result = stripFloatingPointOddities(result);
}
return result;
}
function combineAttribute(stat, aggregator, memo){
stat.value = getAggregatorResult(stat, aggregator);
if (stat.attributeType === 'spellSlot'){
let {
result,
context,
dependencies
} = evaluateCalculation({
string: stat.spellSlotLevelCalculation,
memo,
prop: stat,
});
stat.spellSlotLevelValue = result.value;
stat.spellSlotLevelErrors = context.errors;
stat.dependencies = union(stat.dependencies, dependencies);
}
stat.currentValue = stat.value - (stat.damage || 0);
// Ability scores get modifiers
if (stat.attributeType === 'ability') {
stat.modifier = Math.floor((stat.currentValue - 10) / 2);
} else {
stat.modifier = undefined;
}
// Hit dice get constitution modifiers
stat.constitutionMod = undefined;
if (stat.attributeType === 'hitDice') {
let conStat = memo.statsByVariableName['constitution'];
if (conStat && 'modifier' in conStat){
stat.constitutionMod = conStat.modifier;
stat.dependencies = union(
stat.dependencies,
[conStat._id],
conStat.dependencies,
);
}
}
// Stats that have no effects can be hidden based on a sheet setting
stat.hide = aggregator.hasNoEffects &&
stat.baseValue === undefined ||
undefined
}
function combineSkill(stat, aggregator, memo){
// Skills are based on some ability Modifier
let ability = stat.ability && memo.statsByVariableName[stat.ability]
if (stat.ability && ability){
computeStat(ability, memo);
stat.abilityMod = ability.modifier;
stat.dependencies = union(
stat.dependencies,
[ability._id],
ability.dependencies,
);
} else {
stat.abilityMod = 0;
}
// Combine all the child proficiencies
stat.proficiency = 0;
for (let i in stat.computationDetails.proficiencies){
let prof = stat.computationDetails.proficiencies[i];
computeProficiency(prof, memo);
if (
!prof.deactivatedByToggle &&
prof.value > stat.proficiency
){
stat.proficiency = prof.value;
stat.dependencies = union(
stat.dependencies,
[prof._id],
prof.dependencies,
);
}
}
// Get the character's proficiency bonus to apply
let profBonusStat = memo.statsByVariableName['proficiencyBonus'];
let profBonus = profBonusStat && profBonusStat.value;
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
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 base = aggregator.base || 0;
let result = (base + stat.abilityMod + profBonus + aggregator.add) * aggregator.mul;
if (result < aggregator.min) result = aggregator.min;
if (result > aggregator.max) result = aggregator.max;
if (aggregator.set !== undefined) {
result = aggregator.set;
}
if (Number.isFinite(result)){
result = Math.floor(result);
}
stat.value = result;
// Advantage/disadvantage
if (aggregator.advantage && !aggregator.disadvantage){
stat.advantage = 1;
} else if (aggregator.disadvantage && !aggregator.advantage){
stat.advantage = -1;
} else {
stat.advantage = 0;
}
// Passive bonus
stat.passiveBonus = aggregator.passiveAdd;
// conditional benefits
stat.conditionalBenefits = aggregator.conditional;
// Roll bonuses
stat.rollBonus = aggregator.rollBonus;
// Forced to fail
stat.fail = aggregator.fail;
// Rollbonus
stat.rollBonuses = aggregator.rollBonus;
// Hide
stat.hide = aggregator.hasNoEffects &&
stat.baseValue === undefined &&
stat.proficiency == 0 ||
undefined;
}
function combineDamageMultiplier(stat){
if (stat.immunityCount) return 0;
let result;
if (stat.ressistanceCount && !stat.vulnerabilityCount){
result = 0.5;
} else if (!stat.ressistanceCount && stat.vulnerabilityCount){
result = 2;
} else {
result = 1;
}
stat.value = result;
}

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

@@ -0,0 +1,55 @@
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 computeEffect(effect, memo){
if (effect.computationDetails.computed) return;
if (effect.computationDetails.busyComputing){
// Trying to compute this effect again while it is already computing.
// We must be in a dependency loop.
effect.computationDetails.computed = true;
effect.result = NaN;
effect.computationDetails.busyComputing = false;
effect.computationDetails.error = 'dependencyLoop';
if (Meteor.isClient) console.warn('dependencyLoop', effect);
return;
}
// Before doing any work, mark this effect as busy
effect.computationDetails.busyComputing = true;
// Apply any toggles
applyToggles(effect, memo);
// Determine result of effect calculation
delete effect.errors;
if (!effect.calculation){
if(effect.operation === 'add' || effect.operation === 'base'){
effect.result = 0;
} else {
delete effect.result
}
} else if (Number.isFinite(+effect.calculation)){
effect.result = +effect.calculation;
} else if(effect.operation === 'conditional' || effect.operation === 'rollBonus'){
effect.result = effect.calculation;
} else if(_.contains(['advantage', 'disadvantage', 'fail'], effect.operation)){
effect.result = 1;
} else {
let {
result,
context,
dependencies,
} = evaluateCalculation({
string: effect.calculation,
prop: effect,
memo
});
effect.result = result.value;
effect.dependencies = union(effect.dependencies, dependencies);
if (context.errors.length){
effect.errors = context.errors;
}
}
effect.computationDetails.computed = true;
effect.computationDetails.busyComputing = false;
}

View File

@@ -0,0 +1,129 @@
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':
computeAction(prop, memo);
break;
case 'adjustment':
case 'damage':
computePropertyField(prop, memo, 'amount', 'compile');
break;
case 'attack':
computeAction(prop, memo);
computePropertyField(prop, memo, 'rollBonus');
break;
case 'savingThrow':
computePropertyField(prop, memo, 'dc');
break;
case 'spellList':
computePropertyField(prop, memo, 'maxPrepared');
computePropertyField(prop, memo, 'attackRollBonus');
computePropertyField(prop, memo, 'dc');
break;
case 'propertySlot':
computePropertyField(prop, memo, 'quantityExpected');
computePropertyField(prop, memo, 'slotCondition');
break;
case 'roll':
computePropertyField(prop, memo, 'roll', 'compile');
break;
}
}
function computeAction(prop, memo){
// Uses
let {
result,
context,
dependencies,
} = evaluateCalculation({ string: prop.uses, prop, memo});
prop.usesResult = result.value;
prop.dependencies = union(prop.dependencies, dependencies);
if (context.errors.length){
prop.usesErrors = context.errors;
} else {
delete prop.usesErrors;
}
prop.insufficientResources = undefined;
if (prop.usesUsed >= prop.usesResult){
prop.insufficientResources = true;
}
if (!prop.resources) return;
// Attributes consumed
prop.resources.attributesConsumed.forEach((attConsumed, i) => {
if (attConsumed.variableName){
let stat = memo.statsByVariableName[attConsumed.variableName];
prop.resources.attributesConsumed[i].statId = stat && stat._id;
prop.resources.attributesConsumed[i].statName = stat && stat.name;
let available = stat && stat.currentValue || 0;
prop.resources.attributesConsumed[i].available = available;
if (available < attConsumed.quantity){
prop.insufficientResources = true;
}
if (stat){
prop.dependencies = union(
prop.dependencies,
[stat._id],
stat.dependencies
);
}
}
});
// Items consumed
prop.resources.itemsConsumed.forEach((itemConsumed, i) => {
let item = itemConsumed.itemId ?
memo.equipmentById[itemConsumed.itemId] :
undefined;
let available = item ? item.quantity : 0;
prop.resources.itemsConsumed[i].available = available;
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;
}
});
}
function computePropertyField(prop, memo, fieldName, fn){
let {
result,
context,
dependencies,
} = evaluateCalculation({string: prop[fieldName], prop, memo, fn});
if (result instanceof ConstantNode){
prop[`${fieldName}Result`] = result.value;
} else {
prop[`${fieldName}Result`] = result.toString();
}
prop.dependencies = union(prop.dependencies, dependencies);
if (context.errors.length){
prop[`${fieldName}Errors`] = context.errors;
} else {
delete prop[`${fieldName}Errors`];
}
}

View File

@@ -0,0 +1,40 @@
import evaluateCalculation from '/imports/api/creature/computation/engine/evaluateCalculation.js';
import INLINE_CALCULATION_REGEX from '/imports/constants/INLINE_CALCULTION_REGEX.js';
import ErrorNode from '/imports/parser/parseTree/ErrorNode.js';
import { union } from 'lodash';
export default function computeInlineCalculations(prop, memo){
if (prop.summary){
computeInlineCalcsForField(prop, memo, 'summary');
}
if (prop.description){
computeInlineCalcsForField(prop, memo, 'description');
}
}
function computeInlineCalcsForField(prop, memo, field){
let string = prop[field];
let inlineComputations = [];
let matches = string.matchAll(INLINE_CALCULATION_REGEX);
for (let match of matches){
let calculation = match[1];
let {
result,
context,
dependencies,
} = evaluateCalculation({string: calculation, prop, memo, fn: 'compile'});
if (result instanceof ErrorNode){
result = '`Calculation Error`';
}
let computation = {
calculation,
result: result && result.toString(),
};
if (context.errors.length){
computation.errors = context.errors;
}
inlineComputations.push(computation);
prop.dependencies = union(prop.dependencies, dependencies);
}
prop[`${field}Calculations`] = inlineComputations;
}

View File

@@ -0,0 +1,66 @@
import { forOwn, has, union } from 'lodash';
import applyToggles from '/imports/api/creature/computation/engine/applyToggles.js';
export default function computeLevels(memo){
computeClassLevels(memo);
computeTotalLevel(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){
memo.statsByVariableName[name] = classLevel;
memo.classes[name] = classLevel;
} else if (!has(stat, 'level')){
// Stat is overriden by an attribute
return;
} else if (stat.level < classLevel.level) {
memo.statsByVariableName[name] = classLevel;
memo.classes[name] = classLevel;
}
});
}
function computeTotalLevel(memo){
let currentLevel = memo.statsByVariableName['level'];
if (!currentLevel || currentLevel.deactivatedByToggle){
currentLevel = {
value: 0,
dependencies: [],
computationDetails: {
builtIn: true,
computed: true,
}
};
memo.statsByVariableName['level'] = currentLevel;
}
// bail out if overriden by an attribute
if (!currentLevel.computationDetails.builtIn) return;
let level = 0;
for (let name in memo.classes){
let cls = memo.classes[name];
level += cls.level || 0;
if (cls._id){
currentLevel.dependencies = union(
currentLevel.dependencies,
[cls._id]
)
}
if (cls.dependencies){
currentLevel.dependencies = union(
currentLevel.dependencies,
cls.dependencies,
)
}
}
currentLevel.value = level;
}

View File

@@ -0,0 +1,37 @@
import { each, forOwn } from 'lodash';
import computeLevels from '/imports/api/creature/computation/engine/computeLevels.js';
import computeStat from '/imports/api/creature/computation/engine/computeStat.js';
import computeEffect from '/imports/api/creature/computation/engine/computeEffect.js';
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);
});
// Compute effects which didn't end up targeting a stat
each(memo.unassignedEffects, effect => {
computeEffect(effect, memo);
});
// Compute toggles which didn't already get computed by dependencies
forOwn(memo.togglesById, toggle => {
computeToggle(toggle, memo);
});
// Compute end step properties
forOwn(memo.endStepPropsById, prop => {
computeEndStepProperty(prop, memo);
});
// Compute inline calculations
forOwn(memo.propsById, prop => {
computeInlineCalculations(prop, 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

@@ -0,0 +1,162 @@
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, without } from 'lodash';
export default function computeStat(stat, memo){
// If the stat is already computed, skip it
if (stat.computationDetails.computed) return;
if (stat.computationDetails.busyComputing){
// Trying to compute this stat again while it is already computing.
// We must be in a dependency loop.
stat.computationDetails.computed = true;
stat.value = NaN;
stat.computationDetails.busyComputing = false;
stat.computationDetails.error = 'dependencyLoop';
if (Meteor.isClient) console.warn('dependencyLoop', stat);
return;
}
// Before doing any work, mark this stat as busy
stat.computationDetails.busyComputing = true;
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 = [];
}
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) {
delete statInstance.baseValueErrors;
let {
result,
context,
dependencies
} = evaluateCalculation({
string: statInstance.baseValueCalculation,
prop: statInstance,
memo
});
result.value = +result.value;
if (!isNaN(result.value)){
statInstance.baseValue = result.value;
} else {
statInstance.baseValue = undefined;
}
statInstance.dependencies = union(statInstance.dependencies, dependencies);
if (context.errors.length){
statInstance.baseValueErrors = context.errors;
}
// Apply all the base values
if (Number.isFinite(statInstance.baseValue)){
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

@@ -0,0 +1,55 @@
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){
if (toggle.computationDetails.computed) return;
if (toggle.computationDetails.busyComputing){
// Trying to compute this effect again while it is already computing.
// We must be in a dependency loop.
toggle.computationDetails.computed = true;
toggle.result = false;
toggle.computationDetails.busyComputing = false;
toggle.computationDetails.error = 'dependencyLoop';
if (Meteor.isClient) console.warn('dependencyLoop', toggle);
return;
}
// 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){
toggle.toggleResult = true;
} else if (toggle.disabled){
toggle.toggleResult = false;
} else if (!toggle.condition){
toggle.toggleResult = false;
} else if (Number.isFinite(+toggle.condition)){
toggle.toggleResult = !!+toggle.condition;
} else {
let {
result,
context,
dependencies,
} = evaluateCalculation({string: toggle.condition, prop: toggle, memo});
toggle.toggleResult = !!result.value;
toggle.dependencies = union(
toggle.dependencies,
dependencies,
);
if (context.errors.length){
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

@@ -0,0 +1,137 @@
import computeStat from '/imports/api/creature/computation/engine/computeStat.js';
import { prettifyParseError, parse, CompilationContext } from '/imports/parser/parser.js';
import SymbolNode from '/imports/parser/parseTree/SymbolNode.js';
import AccessorNode from '/imports/parser/parseTree/AccessorNode.js';
import ConstantNode from '/imports/parser/parseTree/ConstantNode.js';
import ErrorNode from '/imports/parser/parseTree/ErrorNode.js';
import findAncestorByType from '/imports/api/creature/computation/engine/findAncestorByType.js';
import { union } from 'lodash';
/* Convert a calculation into a constant output and errors*/
export default function evaluateCalculation({
string,
prop,
memo,
fn = 'reduce',
}){
let dependencies = [];
let context = new CompilationContext();
if (!string) return {
result: new ConstantNode({value: string, type: 'string'}),
context,
dependencies,
};
if (typeof string !== 'string'){
string = string.toString();
}
// Parse the string
let calc;
try {
calc = parse(string);
} catch (e) {
let error = prettifyParseError(e);
return {
result: new ErrorNode({context, error}),
context,
dependencies,
};
}
// Replace constants with their parsed constant
let replaceResults = replaceConstants({
calc, memo, prop, dependencies, context
});
dependencies = replaceResults.dependencies;
calc = replaceResults.calc;
if (replaceResults.failed){
return {
result: new ConstantNode({value: string, type: 'string'}),
context,
dependencies,
};
}
// Ensure all symbol nodes are defined and computed
dependencies = computeSymbols({calc, memo, prop, dependencies})
// Evaluate
let result = calc[fn](memo.statsByVariableName, context);
return {result, context, dependencies};
}
// Replace constants in the calc with the right ParseNodes
function replaceConstants({calc, memo, prop, dependencies, context}){
let constFailed = [];
calc = calc.replaceNodes(node => {
if (!(node instanceof SymbolNode)) return;
let stat, constant;
if (node.name[0] !== '#'){
stat = memo.statsByVariableName[node.name]
constant = memo.constantsByVariableName[node.name];
} else if (node.name === '#constant'){
constant = findAncestorByType({type: 'constant', prop, memo});
}
// 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
]);
// 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 { failed, dependencies, calc };
}
// Ensure all symbol nodes are defined and computed
function computeSymbols({calc, memo, prop, dependencies}){
calc.traverse(node => {
if (node instanceof SymbolNode || node instanceof AccessorNode){
let stat;
// References up the tree start with #
if (node.name[0] === '#'){
stat = findAncestorByType({type: node.name.slice(1), prop, memo});
memo.statsByVariableName[node.name] = stat;
} else {
stat = memo.statsByVariableName[node.name];
}
if (stat && stat.computationDetails && !stat.computationDetails.computed){
computeStat(stat, memo);
}
if (stat){
if (stat.dependencies){
dependencies = union(dependencies, [
stat._id || node.name,
...stat.dependencies
]);
} else {
dependencies = union(dependencies, [stat._id || node.name]);
}
}
}
});
return dependencies;
}

View File

@@ -0,0 +1,10 @@
export default function findAncestorByType({type, prop, memo}){
if (!prop || !prop.ancestors) return;
let ancestor;
for (let i = prop.ancestors.length - 1; i >= 0; i--){
ancestor = memo.propsById[prop.ancestors[i].id];
if (ancestor && ancestor.type === type){
return ancestor;
}
}
}

View File

@@ -0,0 +1,24 @@
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
export default function getComputationProperties(creatureId){
// Find all the relevant properties
return CreatureProperties.find({
'ancestors.id': creatureId,
removed: {$ne: true},
$or: [
// All active properties
{inactive: {$ne: true}},
// 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,
}
}).fetch();
}

View File

@@ -0,0 +1,51 @@
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { union } from 'lodash';
export default function getDependentProperties({
creatureId,
propertyIds,
propertiesDependedAponIds,
}){
// find ids of all dependant toggles that have conditions, even if inactive
let toggleIds = CreatureProperties.find({
'ancestors.id': creatureId,
type: 'toggle',
removed: {$ne: true},
condition: { $exists: true },
dependencies: {$in: propertyIds},
}, {
fields: {_id: 1},
}).map(t => t._id);
// Find all the dependant properties
let props = CreatureProperties.find({
'ancestors.id': creatureId,
removed: {$ne: true},
dependencies: {$in: propertyIds},
$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}},
]
}, { fields: {_id: 1, dependencies: 1} }).fetch();
// Add all the properties that changing props depend on, but haven't yet been
// included to make an array of every property we need
let allConnectedPropIds = [...propertyIds, ...propertiesDependedAponIds];
props.forEach(prop => {
allConnectedPropIds = union(
allConnectedPropIds,
prop.dependencies,
[prop._id]);
});
// Add on all the properties and the objects they depend apon
return CreatureProperties.find({
_id: {$in: allConnectedPropIds}
}, {
// Ignore fields not used in computations
fields: {icon: 0},
sort: {order: 1},
}).fetch();
}

View File

@@ -0,0 +1,118 @@
import { Meteor } from 'meteor/meteor'
import { isEqual, forOwn } from 'lodash';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import propertySchemasIndex from '/imports/api/properties/computedOnlyPropertySchemasIndex.js';
export default function writeAlteredProperties(memo){
let bulkWriteOperations = [];
// Loop through all properties on the memo
forOwn(memo.propsById, changed => {
let schema = propertySchemasIndex[changed.type];
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);
}
});
writePropertiesSequentially(bulkWriteOperations);
}
function addChangedKeysToOp(op, keys, original, changed) {
// Loop through all keys that can be changed by computation
// and compile an operation that sets all those keys
for (let key of keys){
if (!isEqual(original[key], changed[key])){
if (!op) op = newOperation(original._id, changed.type);
let value = changed[key];
if (value === undefined){
// Unset values that become undefined
addUnsetOp(op, key);
} else {
// Set values that changed to something else
addSetOp(op, key, value);
}
}
}
return op;
}
function newOperation(_id, type){
let newOp = {
updateOne: {
filter: {_id},
update: {},
}
};
if (Meteor.isClient){
newOp.type = type;
}
return newOp;
}
function addSetOp(op, key, value){
if (op.updateOne.update.$set){
op.updateOne.update.$set[key] = value;
} else {
op.updateOne.update.$set = {[key]: value};
}
}
function addUnsetOp(op, key){
if (op.updateOne.update.$unset){
op.updateOne.update.$unset[key] = 1;
} else {
op.updateOne.update.$unset = {[key]: 1};
}
}
// We use this instead of bulkWriteProperties because it functions with latency
// compensation without needing to roll back changes, which causes multiple
// expensive redraws of the character sheet
function writePropertiesSequentially(bulkWriteOps){
bulkWriteOps.forEach(op => {
let updateOneOrMany = op.updateOne || op.updateMany;
CreatureProperties.update(updateOneOrMany.filter, updateOneOrMany.update, {
// The bulk code is bypassing validation, so do the same here
// selector: {type: op.type} // include this if bypass is off
bypassCollection2: true,
});
});
}
// This is more efficient on the database, but significantly less efficient
// in the UI because of incompatibility with latency compensation. If the
// duplicate redraws can be fixed, this is a strictly better way of processing
// writes
function bulkWriteProperties(bulkWriteOps){
if (!bulkWriteOps.length) return;
// bulkWrite is only available on the server
if (Meteor.isServer){
CreatureProperties.rawCollection().bulkWrite(
bulkWriteOps,
{ordered : false},
function(e){
if (e) {
console.error('Bulk write failed: ');
console.error(e);
}
}
);
} else {
writePropertiesSequentially(bulkWriteOps);
}
}

View File

@@ -0,0 +1,59 @@
import { pick, forOwn } from 'lodash';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import VERSION from '/imports/constants/VERSION.js';
export default function writeCreatureVariables(memo, creatureId, fullRecompute = true) {
const fields = [
'ability',
'abilityMod',
'advantage',
'attributeType',
'baseProficiency',
'baseValue',
'calculation',
'conditionalBenefits',
'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,
}});
} else {
let $set = {};
forOwn(memo.statsByVariableName, (stat, variableName) => {
let condensedStat = pick(stat, fields);
$set[`variables.${variableName}`] = condensedStat;
});
Creatures.update(creatureId, {$set});
}
}

View File

@@ -1,32 +0,0 @@
import replaceBareSymbolsWithValueAccessor from '/imports/api/creature/computation/utility/replaceBareSymbolsWithValueAccessor.js';
import computeStat from '/imports/api/creature/computation/computeStat.js';
import * as math from 'mathjs';
export default function evaluateCalculation(string, memo){
if (!string) return string;
// Parse the string using mathjs
let calc;
try {
calc = math.parse(string);
} catch (e) {
console.error(e);
return string;
}
// Ensure all symbol nodes are defined and coputed
calc.traverse(node => {
if (node.isSymbolNode){
let stat = memo.statsByVariableName[node.name];
if (stat && !stat.computationDetails.computed){
computeStat(stat, memo);
}
}
});
// Ensure any bare symbols are value accessors instead
let substitutedCalc = calc.transform(replaceBareSymbolsWithValueAccessor);
// Evaluate the expression to a number or return with substitutions
try {
return substitutedCalc.evaluate(memo.statsByVariableName);
} catch (e){
return substitutedCalc.toString();
}
}

View File

@@ -1,42 +0,0 @@
import Creatures from '/imports/api/creature/Creatures.js';
import CreatureProperties from '/imports/api/creature/CreatureProperties.js';
export default function getCalculationProperties(creatureId){
// First get ids of disabled properties and unequiped items
let disabledAncestorIds = CreatureProperties.find({
'ancestors.id': creatureId,
$or: [
{disabled: true},
{equipped: false},
],
}, {
fields: {_id: 1},
}).map(prop => prop._id);
// Then get the ids of creatures that are children of this creature
// to isolate their decendent properties from this calculation
Creatures.find({
'ancestors.id': creatureId,
}, {
fields: {_id: 1},
}).forEach(prop => {
disabledAncestorIds.push(prop._id);
});
// Get all the properties that aren't from the excluded decendents
return CreatureProperties.find({
'ancestors.id': {
$eq: creatureId,
$nin: disabledAncestorIds,
},
removed: {$ne: true},
type: {$in: [
'attribute',
'skill',
'damageMultiplier',
'effect',
'proficiency',
'classLevel',
]},
}).fetch();
}

View File

@@ -1,24 +1,37 @@
import { Meteor } from 'meteor/meteor'
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import SimpleSchema from 'simpl-schema';
import { assertEditPermission } from '/imports/api/creature/creaturePermissions.js';
import ComputationMemo from '/imports/api/creature/computation/ComputationMemo.js';
import computeMemo from '/imports/api/creature/computation/computeMemo.js';
import getCalculationProperties from '/imports/api/creature/computation/getCalculationProperties.js';
import writeAlteredProperties from '/imports/api/creature/computation/writeAlteredProperties.js';
import writeCreatureVariables from '/imports/api/creature/computation/writeCreatureVariables.js';
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import ComputationMemo from '/imports/api/creature/computation/engine/ComputationMemo.js';
import getComputationProperties from '/imports/api/creature/computation/engine/getComputationProperties.js';
import computeMemo from '/imports/api/creature/computation/engine/computeMemo.js';
import writeAlteredProperties from '/imports/api/creature/computation/engine/writeAlteredProperties.js';
import writeCreatureVariables from '/imports/api/creature/computation/engine/writeCreatureVariables.js';
import { recomputeDamageMultipliersById } from '/imports/api/creature/denormalise/recomputeDamageMultipliers.js';
import recomputeSlotFullness from '/imports/api/creature/denormalise/recomputeSlotFullness.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import getDependentProperties from '/imports/api/creature/computation/engine/getDependentProperties.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import recomputeInactiveProperties from '/imports/api/creature/denormalise/recomputeInactiveProperties.js';
export const recomputeCreature = new ValidatedMethod({
name: 'Creatures.methods.recomputeCreature',
name: 'creatures.recomputeCreature',
validate: new SimpleSchema({
charId: { type: String }
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({charId}) {
let creature = Creatures.findOne(charId);
// Permission
assertEditPermission(charId, this.userId);
assertEditPermission(creature, this.userId);
// Work, call this direcly if you are already in a method that has checked
// for permission to edit a given character
recomputeCreatureById(charId);
@@ -26,6 +39,11 @@ export const recomputeCreature = new ValidatedMethod({
});
export function recomputeCreatureById(creatureId){
let creature = Creatures.findOne(creatureId);
recomputeCreatureByDoc(creature);
}
/**
* This function is the heart of DiceCloud. It recomputes a creature's stats,
* distilling down effects and proficiencies into the final stats that make up
@@ -62,12 +80,41 @@ export const recomputeCreature = new ValidatedMethod({
* - Mark the stat as computed
* - Write the computed results back to the database
*/
export function recomputeCreatureById(creatureId){
let props = getCalculationProperties(creatureId);
let computationMemo = new ComputationMemo(props);
export function recomputeCreatureByDoc(creature){
const creatureId = creature._id;
let props = getComputationProperties(creatureId);
let computationMemo = new ComputationMemo(props, creature);
computeMemo(computationMemo);
writeAlteredProperties(computationMemo);
writeCreatureVariables(computationMemo, creatureId);
// if(Meteor.isClient) console.log(computationMemo);
recomputeDamageMultipliersById(creatureId);
recomputeSlotFullness(creatureId);
return computationMemo;
}
export function recomputePropertyDependencies(property){
let creature = getRootCreatureAncestor(property);
recomputeCreatureByDependencies({
creature,
propertyIds: [property._id],
propertiesDependedAponIds: property.dependencies,
});
}
export function recomputeCreatureByDependencies({
creature,
propertyIds,
propertiesDependedAponIds
}){
let props = getDependentProperties({
creatureId: creature._id,
propertyIds,
propertiesDependedAponIds,
});
let computationMemo = new ComputationMemo(props, creature);
computeMemo(computationMemo);
writeAlteredProperties(computationMemo);
writeCreatureVariables(computationMemo, creature._id, false)
recomputeInactiveProperties(creature._id);
return computationMemo;
}

View File

@@ -1,22 +0,0 @@
import * as math from 'mathjs';
export default function replaceBareSymbolsWithValueAccessor(node, path) {
if (node.isFunctionNode){
let fn = node.fn;
if (fn && fn.isSymbolNode){
fn.skipReplacement = true;
}
return node;
} else if (
node.isSymbolNode &&
path !== 'object' &&
node.skipReplacement !== true
) {
const object = new math.SymbolNode(node.name);
const address = new math.ConstantNode('value');
const index = new math.IndexNode([address]);
return new math.AccessorNode(object, index);
} else {
return node;
}
}

View File

@@ -1,101 +0,0 @@
import { Meteor } from 'meteor/meteor'
import { isEqual, forOwn } from 'lodash';
import { ComputedOnlySkillSchema } from '/imports/api/properties/Skills.js';
import { ComputedOnlyAttributeSchema } from '/imports/api/properties/Attributes.js';
import { ComputedOnlyEffectSchema } from '/imports/api/properties/Effects.js';
import CreatureProperties from '/imports/api/creature/CreatureProperties.js';
export default function writeAlteredProperties(memo){
let bulkWriteOperations = [];
// Loop through all properties on the memo
forOwn(memo.originalPropsById, (original, _id) => {
let changed = memo.propsById[_id];
let schema;
switch (changed.type){
case 'skill':
schema = ComputedOnlySkillSchema;
break;
case 'attribute':
schema = ComputedOnlyAttributeSchema;
break;
case 'effect':
schema = ComputedOnlyEffectSchema;
break;
default:
return;
}
let op = undefined;
// Loop through all keys that can be changed by computation
// and compile an operation that sets all those keys
for (let key of schema.objectKeys()){
if (!isEqual(original[key], changed[key])){
if (!op) op = newOperation(_id, changed.type);
let value = changed[key];
if (value === undefined){
// Unset values that become undefined
addUnsetOp(op, key);
} else {
// Set values that changed to something else
addSetOp(op, key, value);
}
}
}
if (op){
bulkWriteOperations.push(op);
}
});
bulkWriteProperties(bulkWriteOperations);
}
function newOperation(_id, type){
let newOp = {
updateOne: {
filter: {_id},
update: {},
}
};
if (Meteor.isClient){
newOp.type = type;
}
return newOp;
}
function addSetOp(op, key, value){
if (op.updateOne.update.$set){
op.updateOne.update.$set[key] = value;
} else {
op.updateOne.update.$set = {[key]: value};
}
}
function addUnsetOp(op, key){
if (op.updateOne.update.$unset){
op.updateOne.update.$unset[key] = 1;
} else {
op.updateOne.update.$unset = {[key]: 1};
}
}
function bulkWriteProperties(bulkWriteOps){
if (!bulkWriteOps.length) return;
if (Meteor.isServer){
CreatureProperties.rawCollection().bulkWrite(
bulkWriteOps,
{ordered : false},
function(e){
if (e) console.error(e);
}
);
} else {
bulkWriteOps.forEach(op => {
CreatureProperties.update(op.updateOne.filter, op.updateOne.update, {
// The server code is bypassing collection 2 validation, so do the same
// on the client
// include this if bypass is off:
// selector: {type: op.type}
bypassCollection2: true,
});
});
}
}

View File

@@ -1,35 +0,0 @@
import { pick, forOwn } from 'lodash';
import Creatures from '/imports/api/creature/Creatures.js';
export default function writeCreatureVariables(memo, creatureId) {
const fields = [
'name',
'attributeType',
'baseValue',
'damage',
'decimal',
'reset',
'resetMultiplier',
'value',
'currentValue',
'modifier',
'ability',
'skillType',
'baseProficiency',
'abilityMod',
'advantage',
'passiveBonus',
'proficiency',
'conditionalBenefits',
'rollBonuses',
'fail',
'level',
];
memo.creatureVariables = {};
forOwn(memo.statsByVariableName, (stat, variableName) => {
let condensedStat = pick(stat, fields);
memo.creatureVariables[variableName] = condensedStat;
});
Creatures.update(creatureId, {$set: {variables: memo.creatureVariables}});
}

View File

@@ -0,0 +1,39 @@
import SimpleSchema from 'simpl-schema';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
let CreatureFolders = new Mongo.Collection('creatureFolders');
let creatureFolderSchema = new SimpleSchema({
name: {
type: String,
trim: false,
optional: true,
max: STORAGE_LIMITS.name,
},
creatures: {
type: Array,
defaultValue: [],
},
'creatures.$': {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
owner: {
type: String,
regEx: SimpleSchema.RegEx.Id,
index: 1,
},
archived: {
type: Boolean,
optional: true,
},
order: {
type: Number,
defaultValue: 0,
},
});
CreatureFolders.attachSchema(creatureFolderSchema);
import '/imports/api/creature/creatureFolders/methods.js/index.js';
export default CreatureFolders;

View File

@@ -0,0 +1,4 @@
import '/imports/api/creature/creatureFolders/methods.js/insertCreatureFolder.js';
import '/imports/api/creature/creatureFolders/methods.js/updateCreatureFolderName.js';
import '/imports/api/creature/creatureFolders/methods.js/removeCreatureFolder.js';
import '/imports/api/creature/creatureFolders/methods.js/moveCreatureToFolder.js';

View File

@@ -0,0 +1,46 @@
import CreatureFolders from '/imports/api/creature/creatureFolders/CreatureFolders.js';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
const insertCreatureFolder = new ValidatedMethod({
name: 'creatureFolders.methods.insert',
validate: null,
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run() {
// Ensure logged in
let userId = this.userId;
if (!userId) {
throw new Meteor.Error('creatureFolders.methods.insert.denied',
'You need to be logged in to insert a folder');
}
// Limit folders to 50 per user
let existingFolders = CreatureFolders.find({
owner: userId
}, {
fields: {order: 1},
sort: {order :-1}
});
if (existingFolders.count() >= 50){
throw new Meteor.Error('creatureFolders.methods.insert.denied',
'You can not have more than 50 folders');
}
// Make the new folder the last in the order
let order = 0;
let lastFolder = existingFolders.fetch()[0];
if (lastFolder){
order = (lastFolder.order || 0) + 1;
}
// Insert
return CreatureFolders.insert({
name: 'Folder',
owner: userId,
order,
});
},
});
export default insertCreatureFolder;

View File

@@ -0,0 +1,45 @@
import CreatureFolders from '/imports/api/creature/creatureFolders/CreatureFolders.js';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
const moveCreatureToFolder = new ValidatedMethod({
name: 'creatureFolders.methods.moveCreatureToFolder',
validate: null,
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({creatureId, folderId}) {
// Ensure logged in
let userId = this.userId;
if (!userId) {
throw new Meteor.Error('creatureFolders.methods.updateName.denied',
'You need to be logged in to remove a folder');
}
// Check that this folder is owned by the user
if (folderId){
let existingFolder = CreatureFolders.findOne(folderId);
if (existingFolder.owner !== userId){
throw new Meteor.Error('creatureFolders.methods.updateName.denied',
'This folder does not belong to you');
}
}
// Remove from other folders
CreatureFolders.update({
owner: userId
}, {
$pull: {creatures: creatureId},
}, {
multi: true,
});
if (folderId){
// Add to this folder
CreatureFolders.update(folderId, {
$addToSet: {creatures: creatureId},
});
}
},
});
export default moveCreatureToFolder;

View File

@@ -0,0 +1,31 @@
import CreatureFolders from '/imports/api/creature/creatureFolders/CreatureFolders.js';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
const removeCreatureFolder = new ValidatedMethod({
name: 'creatureFolders.methods.remove',
validate: null,
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({_id}) {
// Ensure logged in
let userId = this.userId;
if (!userId) {
throw new Meteor.Error('creatureFolders.methods.updateName.denied',
'You need to be logged in to remove a folder');
}
// Check that this folder is owned by the user
let existingFolder = CreatureFolders.findOne(_id);
if (existingFolder.owner !== userId){
throw new Meteor.Error('creatureFolders.methods.updateName.denied',
'This folder does not belong to you');
}
// Remove
return CreatureFolders.remove(_id);
},
});
export default removeCreatureFolder;

View File

@@ -0,0 +1,43 @@
import CreatureFolders from '/imports/api/creature/creatureFolders/CreatureFolders.js';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
const reorderCreatureFolder = new ValidatedMethod({
name: 'creatureFolders.methods.reorder',
validate: null,
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({_id, order}) {
// Ensure logged in
let userId = this.userId;
if (!userId) {
throw new Meteor.Error('creatureFolders.methods.reorder.denied',
'You need to be logged in to reorder a folder');
}
// Check that this folder is owned by the user
let existingFolder = CreatureFolders.findOne(_id);
if (existingFolder.owner !== userId){
throw new Meteor.Error('creatureFolders.methods.reorder.denied',
'This folder does not belong to you');
}
// First give it the new order, it should end in 0.5 putting it between two other docs
CreatureFolders.update(_id, {$set: {order}});
this.unblock();
// Reorder all the folders with integer numbers in this new order
CreatureFolders.find({
owner: userId
}, {
fields: {order: 1,},
sort: {order: -1}
}).forEach((folder, index) => {
if (folder.order !== index){
CreatureFolders.update(_id, {$set: {order: index}})
}
});
},
});
export default reorderCreatureFolder;

View File

@@ -0,0 +1,31 @@
import CreatureFolders from '/imports/api/creature/creatureFolders/CreatureFolders.js';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
const updateCreatureFolderName = new ValidatedMethod({
name: 'creatureFolders.methods.updateName',
validate: null,
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({_id, name}) {
// Ensure logged in
let userId = this.userId;
if (!userId) {
throw new Meteor.Error('creatureFolders.methods.updateName.denied',
'You need to be logged in to update a folder');
}
// Check that this folder is owned by the user
let existingFolder = CreatureFolders.findOne(_id);
if (existingFolder.owner !== userId){
throw new Meteor.Error('creatureFolders.methods.updateName.denied',
'This folder does not belong to you');
}
// Update
return CreatureFolders.update(_id, {$set: {name}});
},
});
export default updateCreatureFolderName;

View File

@@ -0,0 +1,100 @@
import { Mongo } from 'meteor/mongo';
import SimpleSchema from 'simpl-schema';
import ColorSchema from '/imports/api/properties/subSchemas/ColorSchema.js';
import ChildSchema from '/imports/api/parenting/ChildSchema.js';
import SoftRemovableSchema from '/imports/api/parenting/SoftRemovableSchema.js';
import propertySchemasIndex from '/imports/api/properties/computedPropertySchemasIndex.js';
import { storedIconsSchema } from '/imports/api/icons/Icons.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
let CreatureProperties = new Mongo.Collection('creatureProperties');
let CreaturePropertySchema = new SimpleSchema({
type: {
type: String,
allowedValues: Object.keys(propertySchemasIndex),
},
tags: {
type: Array,
defaultValue: [],
maxCount: STORAGE_LIMITS.tagCount,
},
'tags.$': {
type: String,
max: STORAGE_LIMITS.tagLength,
},
disabled: {
type: Boolean,
optional: true,
},
icon: {
type: storedIconsSchema,
optional: true,
max: STORAGE_LIMITS.icon,
},
// Reference to the library node that this property was copied from
libraryNodeId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
optional: true,
},
// Denormalised flag if this property is inactive on the sheet for any reason
// Including being disabled, or a decendent of a disabled property
inactive: {
type: Boolean,
optional: true,
index: 1,
},
// Denormalised flag if this property was made inactive by an inactive
// ancestor. True if this property has an inactive ancestor even if this
// property is itself inactive
deactivatedByAncestor: {
type: Boolean,
optional: true,
index: 1,
},
// Denormalised flag if this property was made inactive because of its own
// state
deactivatedBySelf: {
type: Boolean,
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,
defaultValue: [],
index: 1,
},
'dependencies.$': {
type: String,
},
});
for (let key in propertySchemasIndex){
let schema = new SimpleSchema({});
schema.extend(propertySchemasIndex[key]);
schema.extend(CreaturePropertySchema);
schema.extend(ColorSchema);
schema.extend(ChildSchema);
schema.extend(SoftRemovableSchema);
CreatureProperties.attachSchema(schema, {
selector: {type: key}
});
}
import '/imports/api/creature/creatureProperties/methods/index.js';
import '/imports/api/creature/actions/doAction.js';
import '/imports/api/creature/actions/castSpellWithSlot.js';
export default CreatureProperties;
export {
CreaturePropertySchema,
};

View File

@@ -0,0 +1,5 @@
import Creatures from '/imports/api/creature/creatures/Creatures.js';
export default function getRootCreatureAncestor(property){
return Creatures.findOne(property.ancestors[0].id);
}

View File

@@ -0,0 +1,69 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import SimpleSchema from 'simpl-schema';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import 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',
validate: new SimpleSchema({
_id: SimpleSchema.RegEx.Id,
operation: {
type: String,
allowedValues: ['set', 'increment']
},
value: Number,
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({_id, operation, value}) {
// Permissions
let property = CreatureProperties.findOne(_id);
let rootCreature = getRootCreatureAncestor(property);
assertEditPermission(rootCreature, this.userId);
// Do work
adjustQuantityWork({property, operation, value});
// Changing quantity does not change dependencies, but recomputing the
// inventory changes many deps at once, so recompute fully
recomputeCreatureByDoc(rootCreature);
recomputeInventory(rootCreature._id);
},
});
export function adjustQuantityWork({property, operation, value}){
// Check if property has quantity
let schema = CreatureProperties.simpleSchema(property);
if (!schema.allowsKey('quantity')){
throw new Meteor.Error(
'Adjust quantity failed',
`Property of type "${property.type}" doesn't have a quantity`
);
}
if (operation === 'set'){
CreatureProperties.update(property._id, {
$set: {quantity: value}
}, {
selector: property
});
} else if (operation === 'increment'){
// value here is 'damage'
value = -value;
let currentQuantity = property.quantity;
if (currentQuantity + value < 0) value = -currentQuantity;
CreatureProperties.update(property._id, {
$inc: {quantity: value}
}, {
selector: property
});
}
}
export default adjustQuantity;

View File

@@ -0,0 +1,57 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import SimpleSchema from 'simpl-schema';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
import { recomputePropertyDependencies } from '/imports/api/creature/computation/methods/recomputeCreature.js';
const damagePropertiesByName = new ValidatedMethod({
name: 'CreatureProperties.damagePropertiesByName',
validate: new SimpleSchema({
creatureId: SimpleSchema.RegEx.Id,
variableName: {
type: String,
},
operation: {
type: String,
allowedValues: ['set', 'increment']
},
value: Number,
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 20,
timeInterval: 5000,
},
run({creatureId, variableName, operation, value}) {
// Check permissions
let creature = Creatures.findOne(creatureId, {
fields: {
damageMultipliers: 1,
owner: 1,
readers: 1,
writers: 1,
},
});
assertEditPermission(creature, this.userId);
let lastProperty;
CreatureProperties.find({
'ancestors.id': creatureId,
variableName,
removed: {$ne: false},
inactive: {$ne: true},
}).forEach(property => {
// Check if property can take damage
let schema = CreatureProperties.simpleSchema(property);
if (!schema.allowsKey('damage')) return;
// Damage the property
damagePropertyWork({property, operation, value});
lastProperty = property;
});
if (lastProperty) recomputePropertyDependencies(lastProperty);
}
});
export default damagePropertiesByName;

View File

@@ -0,0 +1,80 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import SimpleSchema from 'simpl-schema';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import { recomputePropertyDependencies } from '/imports/api/creature/computation/methods/recomputeCreature.js';
const damageProperty = new ValidatedMethod({
name: 'creatureProperties.damage',
validate: new SimpleSchema({
_id: SimpleSchema.RegEx.Id,
operation: {
type: String,
allowedValues: ['set', 'increment']
},
value: Number,
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 20,
timeInterval: 5000,
},
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
let schema = CreatureProperties.simpleSchema(property);
if (!schema.allowsKey('damage')){
throw new Meteor.Error(
'Damage property failed',
`Property of type "${property.type}" can't be damaged`
);
}
let result = damagePropertyWork({property, operation, value});
// Dependencies can't be changed through damage, only recompute deps
recomputePropertyDependencies(property);
return result;
},
});
export function damagePropertyWork({property, operation, value}){
if (operation === 'set'){
let currentValue = property.value;
// Set represents what we want the value to be after damage
// So we need the actual damage to get to that value
let damage = currentValue - value;
// Damage can't exceed total value
if (damage > currentValue) damage = currentValue;
// Damage must be positive
if (damage < 0) damage = 0;
CreatureProperties.update(property._id, {
$set: {damage}
}, {
selector: property
});
return currentValue - damage;
} else if (operation === 'increment'){
let currentValue = property.value - (property.damage || 0);
let currentDamage = property.damage;
let increment = value;
// Can't increase damage above the remaining value
if (increment > currentValue) increment = currentValue;
// Can't decrease damage below zero
if (-increment > currentDamage) increment = -currentDamage;
CreatureProperties.update(property._id, {
$inc: {damage: increment}
}, {
selector: property
});
return increment;
}
}
export default damageProperty;

View File

@@ -0,0 +1,73 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import SimpleSchema from 'simpl-schema';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
import { recomputeCreatureByDependencies } from '/imports/api/creature/computation/methods/recomputeCreature.js';
const dealDamage = new ValidatedMethod({
name: 'creatureProperties.dealDamage',
validate: new SimpleSchema({
creatureId: SimpleSchema.RegEx.Id,
damageType: {
type: String,
},
amount: Number,
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 20,
timeInterval: 5000,
},
run({creatureId, damageType, amount}) {
// permissions
let creature = Creatures.findOne(creatureId, {
fields: {
damageMultipliers: 1,
owner: 1,
readers: 1,
writers: 1,
},
});
assertEditPermission(creature, this.userId);
// Get all the health bars and do damage to them
let healthBars = CreatureProperties.find({
'ancestors.id': creatureId,
type: 'attribute',
attributeType:'healthBar',
removed: {$ne: true},
inactive: {$ne: true},
}, {
sort: {order: -1},
});
let multiplier = creature.damageMultipliers[damageType];
if (multiplier === undefined) multiplier = 1;
let totalDamage = Math.floor(amount * multiplier);
let damageLeft = totalDamage;
if (damageType === 'healing') damageLeft = -totalDamage;
let propertyIds = [];
let propertiesDependedAponIds = [];
healthBars.forEach(healthBar => {
if (damageLeft === 0) return;
let damageAdded = damagePropertyWork({
property: healthBar,
operation: 'increment',
value: damageLeft,
});
damageLeft -= damageAdded;
propertyIds.push(healthBar._id);
propertiesDependedAponIds.push(...healthBar.dependencies);
});
recomputeCreatureByDependencies({
creature,
propertyIds,
propertiesDependedAponIds,
});
return totalDamage;
},
});
export default dealDamage;

View File

@@ -0,0 +1,105 @@
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 { assertEditPermission } from '/imports/api/sharing/sharingPermissions.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',
validate: new SimpleSchema({
_id: {
type: String,
regEx: SimpleSchema.RegEx.Id,
}
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({_id}) {
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;
},
});
export default duplicateProperty;

View File

@@ -0,0 +1,58 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
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 { 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({
name: 'creatureProperties.equip',
validate({_id, equipped}){
if (!_id) throw new Meteor.Error('No _id', '_id is required');
if (equipped !== true && equipped !== false) {
throw new Meteor.Error('No equipped', 'equipped is required to be true or false');
}
},
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({_id, equipped}) {
let item = CreatureProperties.findOne(_id);
if (item.type !== 'item') throw new Meteor.Error('wrong type',
'Equip and unequip can only be performed on items');
let creature = getRootCreatureAncestor(item);
assertEditPermission(creature, this.userId);
CreatureProperties.update(_id, {
$set: {equipped},
}, {
selector: {type: 'item'},
});
let tag = equipped ? BUILT_IN_TAGS.equipment : BUILT_IN_TAGS.carried;
let parentRef = getParentRefByTag(creature._id, tag);
if (!parentRef) parentRef = {id: creature._id, collection: 'creatures'};
organizeDoc.call({
docRef: {
id: _id,
collection: 'creatureProperties',
},
parentRef,
order: Number.MAX_SAFE_INTEGER,
skipRecompute: true,
});
recomputeInactiveProperties(creature._id);
recomputeInventory(creature._id);
recomputeCreatureByDoc(creature);
},
});
export default equipItem;

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

@@ -0,0 +1,44 @@
export default function getSlotFillFilter({slot, libraryIds}){
let filter = {
removed: {$ne: true},
$and: []
};
if (libraryIds){
filter['ancestors.id'] = {$in: libraryIds};
}
if (slot.slotType){
filter.$and.push({
$or: [{
type: slot.slotType
},{
type: 'slotFiller',
slotFillerType: slot.slotType,
}]
});
}
let tagsOr = [];
let tagsNor = [];
if (slot.slotTags && slot.slotTags.length){
tagsOr.push({tags: {$all: slot.slotTags}});
}
if (slot.extraTags && slot.extraTags.length){
slot.extraTags.forEach(extra => {
if (!extra.tags || !extra.tags.length) return;
if (extra.operation === 'OR'){
tagsOr.push({tags: {$all: extra.tags}});
} else if (extra.operation === 'NOT'){
tagsNor.push({tags: {$all: extra.tags}});
}
});
}
if (tagsOr.length){
filter.$and.push({$or: tagsOr});
}
if (tagsNor.length){
filter.$and.push({$nor: tagsNor});
}
if (!filter.$and.length){
delete filter.$and;
}
return filter;
}

View File

@@ -0,0 +1,14 @@
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';
import '/imports/api/creature/creatureProperties/methods/duplicateProperty.js';
import '/imports/api/creature/creatureProperties/methods/equipItem.js';
import '/imports/api/creature/creatureProperties/methods/insertProperty.js';
import '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js';
import '/imports/api/creature/creatureProperties/methods/pullFromProperty.js';
import '/imports/api/creature/creatureProperties/methods/pushToProperty.js';
import '/imports/api/creature/creatureProperties/methods/restoreProperty.js';
import '/imports/api/creature/creatureProperties/methods/selectAmmoItem.js';
import '/imports/api/creature/creatureProperties/methods/softRemoveProperty.js';
import '/imports/api/creature/creatureProperties/methods/updateCreatureProperty.js';

View File

@@ -0,0 +1,156 @@
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: new SimpleSchema({
creatureProperty: {
type: Object,
blackbox: true,
},
parentRef: RefSchema,
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
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,
});
},
});
export function insertPropertyWork({property, creature}){
delete property._id;
let _id = CreatureProperties.insert(property);
// Tree structure changed by insert, reorder the tree
reorderDocs({
collection: CreatureProperties,
ancestorId: creature._id,
});
// 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

@@ -0,0 +1,250 @@
import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
import { RefSchema } from '/imports/api/parenting/ChildSchema.js';
import 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 { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import {
setLineageOfDocs,
getAncestry,
renewDocIds
} from '/imports/api/parenting/parenting.js';
import { reorderDocs } from '/imports/api/parenting/order.js';
import { setDocToLastOrder } from '/imports/api/parenting/order.js';
import recomputeInventory from '/imports/api/creature/denormalise/recomputeInventory.js';
import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
const insertPropertyFromLibraryNode = new ValidatedMethod({
name: 'creatureProperties.insertPropertyFromLibraryNode',
validate: new SimpleSchema({
nodeIds: {
type: Array,
max: 20,
},
'nodeIds.$': {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
parentRef: {
type: RefSchema,
},
order: {
type: Number,
optional: true,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({nodeIds, parentRef, order}) {
// 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);
// {libraryId: hasViewPermission}
//let libraryPermissionMemoir = {};
let node;
nodeIds.forEach(nodeId => {
// TODO: Check library view permission for each node before starting
node = insertPropertyFromNode(nodeId, ancestors, order);
});
// get one of the root inserted docs
let rootId = node._id;
// Tree structure changed by inserts, reorder the tree
reorderDocs({
collection: CreatureProperties,
ancestorId: rootCreature._id,
});
// The library properties need to denormalise which of them are inactive
recomputeInactiveProperties(rootCreature._id);
// 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
return rootId;
},
});
function insertPropertyFromNode(nodeId, ancestors, order){
// Fetch the library node and its decendents, provided they have not been
// removed
// TODO: Check permission to read the library this node is in
let node = LibraryNodes.findOne({
_id: nodeId,
removed: {$ne: true},
});
if (!node) {
if (Meteor.isClient) return;
else {
throw new Meteor.Error(
'Insert property from library failed',
`No library document with id '${nodeId}' was found`
);
}
}
let oldParent = node.parent;
let nodes = LibraryNodes.find({
'ancestors.id': nodeId,
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];
// set libraryNodeIds
storeLibraryNodeReferences(nodes, nodeId);
// re-map all the ancestors
setLineageOfDocs({
docArray: nodes,
newAncestry: ancestors,
oldParent,
});
// Give the docs new IDs without breaking internal references
renewDocIds({
docArray: nodes,
collectionMap: {'libraryNodes': 'creatureProperties'}
});
// Order the root node
if (order === undefined){
setDocToLastOrder({
collection: CreatureProperties,
doc: node,
});
} else {
node.order = order;
}
// Insert the creature properties
CreatureProperties.batchInsert(nodes);
return node;
}
function storeLibraryNodeReferences(nodes){
nodes.forEach(node => {
node.libraryNodeId = node._id;
});
}
// 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;
});
// TODO: Force the referencedNode to take the old id of the reference
// such that the reference's children can be kept
// 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

@@ -0,0 +1,36 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js';
const pullFromProperty = new ValidatedMethod({
name: 'creatureProperties.pull',
validate: null,
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({_id, path, itemId}){
// Permissions
let property = CreatureProperties.findOne(_id);
let rootCreature = getRootCreatureAncestor(property);
assertEditPermission(rootCreature, this.userId);
// Do work
CreatureProperties.update(_id, {
$pull: {[path.join('.')]: {_id: itemId}},
}, {
selector: {type: property.type},
getAutoValues: false,
});
// TODO figure out if this method can change deps or not
recomputeCreatureByDoc(rootCreature);
// recomputePropertyDependencies(property);
}
});
export default pullFromProperty;

View File

@@ -0,0 +1,53 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js';
import { get } from 'lodash';
const pushToProperty = new ValidatedMethod({
name: 'creatureProperties.push',
validate: null,
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({_id, path, value}){
// Permissions
let property = CreatureProperties.findOne(_id);
let rootCreature = getRootCreatureAncestor(property);
assertEditPermission(rootCreature, this.userId);
let joinedPath = path.join('.');
// Respect maxCount
let schema = CreatureProperties.simpleSchema(property);
let maxCount = schema.get(joinedPath, 'maxCount');
if (Number.isFinite(maxCount)){
let array = get(property, path);
let currentCount = array ? array.length : 0;
if (currentCount >= maxCount){
throw new Meteor.Error(
'Array is full',
`Cannot have more than ${maxCount} values`
);
}
}
// Do work
CreatureProperties.update(_id, {
$push: {[joinedPath]: value},
}, {
selector: {type: property.type},
});
// TODO figure out if this method can change deps or not
recomputeCreatureByDoc(rootCreature);
// recomputePropertyDependencies(property);
}
});
export default pushToProperty;

View File

@@ -0,0 +1,40 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import SimpleSchema from 'simpl-schema';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
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',
validate: new SimpleSchema({
_id: SimpleSchema.RegEx.Id
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({_id}){
// Permissions
let property = CreatureProperties.findOne(_id);
let rootCreature = getRootCreatureAncestor(property);
assertEditPermission(rootCreature, this.userId);
// 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
recomputeCreatureByDoc(rootCreature);
}
});
export default restoreProperty;

View File

@@ -0,0 +1,52 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import SimpleSchema from 'simpl-schema';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js';
const selectAmmoItem = new ValidatedMethod({
name: 'creatureProperties.selectAmmoItem',
validate: new SimpleSchema({
actionId: SimpleSchema.RegEx.Id,
itemId: SimpleSchema.RegEx.Id,
itemConsumedIndex: Number,
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({actionId, itemId, itemConsumedIndex}) {
// Permissions
let action = CreatureProperties.findOne(actionId);
let rootCreature = getRootCreatureAncestor(action);
assertEditPermission(rootCreature, this.userId);
// Check that this index has a document to edit
let itemConsumed = action.resources.itemsConsumed[itemConsumedIndex];
if (!itemConsumed){
throw new Meteor.Error('Resouce not found',
'Could not set ammo, because the ammo document was not found');
}
let itemToLink = CreatureProperties.findOne(itemId);
if (!itemToLink){
throw new Meteor.Error('Item not found',
'Could not set ammo: the item was not found');
}
let path = `resources.itemsConsumed.${itemConsumedIndex}.itemId`;
CreatureProperties.update(actionId, {
$set: {[path]: itemId}
}, {
selector: action,
});
// Changing the linked item does change the dependency tree
// TODO: We can predict exactly which deps will be affected instead of
// recomputing the entire creature
recomputeCreatureByDoc(rootCreature);
},
});
export default selectAmmoItem;

View File

@@ -0,0 +1,37 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import SimpleSchema from 'simpl-schema';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { 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',
validate: new SimpleSchema({
_id: SimpleSchema.RegEx.Id
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({_id}){
// Permissions
let property = CreatureProperties.findOne(_id);
let rootCreature = getRootCreatureAncestor(property);
assertEditPermission(rootCreature, this.userId);
// Do work
softRemove({_id, collection: CreatureProperties});
// Potentially changes items and containers
recomputeInventory(rootCreature._id);
// Changes dependency tree by removing children
recomputeCreatureByDoc(rootCreature);
}
});
export default softRemoveProperty;

View File

@@ -0,0 +1,67 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.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',
validate({_id, path}){
if (!_id) throw new Meteor.Error('No _id', '_id is required');
// We cannot change these fields with a simple update
switch (path[0]){
case 'type':
case 'order':
case 'parent':
case 'ancestors':
case 'damage':
throw new Meteor.Error('Permission denied',
'This property can\'t be updated directly');
}
},
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({_id, path, value}) {
// Permission
let property = CreatureProperties.findOne(_id, {
fields: {type: 1, ancestors: 1}
});
let rootCreature = getRootCreatureAncestor(property);
assertEditPermission(rootCreature, this.userId);
let pathString = path.join('.');
let modifier;
// unset empty values
if (value === null || value === undefined){
modifier = {$unset: {[pathString]: 1}};
} else {
modifier = {$set: {[pathString]: value}};
}
CreatureProperties.update(_id, modifier, {
selector: {type: property.type},
});
// Some updates might cause other properties to become inactive
if ([
'applied', 'equipped', 'prepared', 'alwaysPrepared', 'disabled'
].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
// denormalised stats might change, so fetch the creature again
recomputeCreatureById(rootCreature._id);
},
});
export default updateCreatureProperty;

View File

@@ -0,0 +1,12 @@
import { recomputeCreatureById } from '/imports/api/creature/computation/methods/recomputeCreature.js';
/**
* Recomputes all ancestor creatures of this property
*/
export default function recomputeCreaturesByProperty(property){
for (let ref of property.ancestors){
if (ref.collection === 'creatures') {
recomputeCreatureById.call(ref.id);
}
}
}

View File

@@ -0,0 +1,180 @@
import SimpleSchema from 'simpl-schema';
import deathSaveSchema from '/imports/api/properties/subSchemas/DeathSavesSchema.js'
import ColorSchema from '/imports/api/properties/subSchemas/ColorSchema.js';
import SharingSchema from '/imports/api/sharing/SharingSchema.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
//set up the collection for creatures
let Creatures = new Mongo.Collection('creatures');
let CreatureSettingsSchema = new SimpleSchema({
//slowed down by carrying too much?
useVariantEncumbrance: {
type: Boolean,
optional: true,
},
//hide spellcasting tab
hideSpellcasting: {
type: Boolean,
optional: true,
},
// Swap around the modifier and stat
swapStatAndModifier: {
type: Boolean,
optional: true,
},
// Hide all the unused stats
hideUnusedStats: {
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,
optional: true,
min: 0,
max: 1,
},
discordWebhook: {
type: String,
optional: true,
max: STORAGE_LIMITS.url,
},
});
let CreatureSchema = new SimpleSchema({
// Strings
name: {
type: String,
defaultValue: '',
optional: true,
max: STORAGE_LIMITS.name,
},
alignment: {
type: String,
optional: true,
max: STORAGE_LIMITS.name,
},
gender: {
type: String,
optional: true,
max: STORAGE_LIMITS.name,
},
picture: {
type: String,
optional: true,
max: STORAGE_LIMITS.url,
},
avatarPicture: {
type: String,
optional: true,
max: STORAGE_LIMITS.url,
},
// Mechanics
deathSave: {
type: deathSaveSchema,
defaultValue: {},
},
// Stats that are computed and denormalised outside of recomputation
denormalizedStats: {
type: Object,
defaultValue: {},
},
// Sum of all XP gained by this character
'denormalizedStats.xp': {
type: SimpleSchema.Integer,
defaultValue: 0,
},
// Sum of all levels granted by milestone XP
'denormalizedStats.milestoneLevels': {
type: SimpleSchema.Integer,
defaultValue: 0,
},
// 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,
},
// Version of computation engine that was last used to compute this creature
computeVersion: {
type: String,
optional: true,
},
type: {
type: String,
defaultValue: 'pc',
allowedValues: ['pc', 'npc', 'monster'],
},
damageMultipliers: {
type: Object,
blackbox: true,
defaultValue: {}
},
variables: {
type: Object,
blackbox: true,
defaultValue: {}
},
// Tabletop
tabletop: {
type: String,
regEx: SimpleSchema.RegEx.id,
optional: true,
},
initiativeRoll: {
type: SimpleSchema.Integer,
optional: true,
},
// Settings
settings: {
type: CreatureSettingsSchema,
defaultValue: {},
},
});
CreatureSchema.extend(ColorSchema);
CreatureSchema.extend(SharingSchema);
Creatures.attachSchema(CreatureSchema);
import '/imports/api/creature/creatures/methods/index.js';
export default Creatures;
export { CreatureSchema };

View File

@@ -1,3 +1,4 @@
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import {
assertEditPermission as editPermission,
assertViewPermission as viewPermission,
@@ -23,6 +24,6 @@ export function assertEditPermission(creature, userId) {
}
export function assertViewPermission(creature, userId) {
creature = getCreature(creature, {owner: 1, writers: 1, public: 1});
creature = getCreature(creature, {owner: 1, readers:1, writers: 1, public: 1});
viewPermission(creature, userId);
}

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

@@ -0,0 +1,5 @@
import getSlug from 'speakingurl';
export default function getCreatureUrlName({name}){
return getSlug(name, {maintainCase: true}) || '-';
}

View File

@@ -0,0 +1,4 @@
import '/imports/api/creature/creatures/methods/insertCreature.js';
import '/imports/api/creature/creatures/methods/removeCreature.js';
import '/imports/api/creature/creatures/methods/restCreature.js';
import '/imports/api/creature/creatures/methods/updateCreature.js';

View File

@@ -0,0 +1,70 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { getUserTier } from '/imports/api/users/patreon/tiers.js';
import defaultCharacterProperties from '/imports/api/creature/creatures/defaultCharacterProperties.js';
import insertPropertyFromLibraryNode from '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js';
const insertCreature = new ValidatedMethod({
name: 'creatures.insertCreature',
validate: null,
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run() {
if (!this.userId) {
throw new Meteor.Error('Creatures.methods.insert.denied',
'You need to be logged in to insert a creature');
}
let tier = getUserTier(this.userId);
let currentCharacterCount = Creatures.find({
owner: this.userId,
}, {
fields: {_id: 1},
}).count();
if (
tier.characterSlots !== -1 &&
currentCharacterCount >= tier.characterSlots
){
throw new Meteor.Error('Creatures.methods.insert.denied',
`You are already at your limit of ${tier.characterSlots} characters`)
}
// Create the creature document
let creatureId = Creatures.insert({
owner: this.userId,
});
// 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({
nodeIds: ['iHbhfcg3AL5isSWbw'],
parentRef: {id: baseId, collection: 'creatureProperties'},
order: 0.5,
});
}
return creatureId;
},
});
export default insertCreature;

View File

@@ -0,0 +1,41 @@
import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import { assertOwnership } from '/imports/api/creature/creatures/creaturePermissions.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js';
import Experiences from '/imports/api/creature/experience/Experiences.js';
function removeRelatedDocuments(creatureId){
CreatureProperties.remove({'ancestors.id': creatureId});
CreatureLogs.remove({creatureId});
Experiences.remove({creatureId});
}
const removeCreature = new ValidatedMethod({
name: 'Creatures.methods.removeCreature', // DDP method name
validate: new SimpleSchema({
charId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({charId}) {
assertOwnership(charId, this.userId)
this.unblock();
removeCreatureWork(charId)
},
});
export function removeCreatureWork(creatureId){
Creatures.remove(creatureId);
removeRelatedDocuments(creatureId);
}
export default removeCreature;

View File

@@ -0,0 +1,116 @@
import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import { recomputeCreatureById } from '/imports/api/creature/computation/methods/recomputeCreature.js';
const restCreature = new ValidatedMethod({
name: 'creature.methods.longRest',
validate: new SimpleSchema({
creatureId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
restType: {
type: String,
allowedValues: ['shortRest', 'longRest'],
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({creatureId, restType}) {
let creature = Creatures.findOne(creatureId, {
fields: {
owner: 1,
writers: 1,
settings: 1,
}
}) ;
// Need edit permissions
assertEditPermission(creature, this.userId);
// Long rests reset short rest properties as well
let resetFilter;
if (restType === 'shortRest'){
resetFilter = 'shortRest'
} else {
resetFilter = {$in: ['shortRest', 'longRest']}
}
// Only apply to active properties
let filter = {
'ancestors.id': creatureId,
reset: resetFilter,
removed: {$ne: true},
inactive: {$ne: true},
};
// update all attribute's damage
filter.type = 'attribute';
CreatureProperties.update(filter, {
$set: {damage: 0}
}, {
selector: {type: 'attribute'},
multi: true,
});
// Update all action-like properties' usesUsed
filter.type = {$in: [
'action',
'attack',
'spell'
]};
CreatureProperties.update(filter, {
$set: {usesUsed: 0}
}, {
selector: {type: 'action'},
multi: true,
});
// Reset half hit dice on a long rest, starting with the highest dice
if (restType === 'longRest'){
let hitDice = CreatureProperties.find({
'ancestors.id': creatureId,
type: 'attribute',
attributeType: 'hitDice',
removed: {$ne: true},
inactive: {$ne: true},
}, {
fields: {
hitDiceSize: 1,
damage: 1,
value: 1,
}
}).fetch();
// Use a collator to do sorting in natural order
let collator = new Intl.Collator('en', {
numeric: true, sensitivity: 'base'
});
// Get the hit dice in decending order of hitDiceSize
let compare = (a, b) => collator.compare(b.hitDiceSize, a.hitDiceSize)
hitDice.sort(compare);
// Get the total number of hit dice that can be recovered this rest
let totalHd = hitDice.reduce((sum, hd) => sum + (hd.value || 0), 0);
let resetMultiplier = creature.settings.hitDiceResetMultiplier || 0.5;
let recoverableHd = Math.max(Math.floor(totalHd*resetMultiplier), 1);
// recover each hit dice in turn until the recoverable amount is used up
let amountToRecover, resultingDamage;
hitDice.forEach(hd => {
if (!recoverableHd) return;
amountToRecover = Math.min(recoverableHd, hd.damage || 0);
if (!amountToRecover) return;
recoverableHd -= amountToRecover;
resultingDamage = hd.damage - amountToRecover;
CreatureProperties.update(hd._id, {
$set: {damage: resultingDamage}
}, {
selector: {type: 'attribute'},
});
});
}
recomputeCreatureById(creatureId);
},
});
export default restCreature;

View File

@@ -0,0 +1,45 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import {assertEditPermission} from '/imports/api/sharing/sharingPermissions.js';
const updateCreature = new ValidatedMethod({
name: 'creatures.update',
validate({_id, path}){
if (!_id) return false;
// Allowed fields
let allowedFields = [
'name',
'alignment',
'gender',
'picture',
'avatarPicture',
'color',
'settings',
];
if (!allowedFields.includes(path[0])){
throw new Meteor.Error('Creatures.methods.update.denied',
'This field can\'t be updated using this method');
}
},
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({_id, path, value}) {
let creature = Creatures.findOne(_id);
assertEditPermission(creature, this.userId);
if (value === undefined || value === null){
Creatures.update(_id, {
$unset: {[path.join('.')]: 1},
});
} else {
Creatures.update(_id, {
$set: {[path.join('.')]: value},
});
}
},
});
export default updateCreature;

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