Compare commits

...

500 Commits

Author SHA1 Message Date
Thaum Rystra
3fa9077124 fixed typo, added tier 2020-05-07 16:14:50 +02:00
Thaum Rystra
cb8d311fdc Merge branch 'version-2' of https://github.com/ThaumRystra/DiceCloud into version-2 2020-05-07 15:53:52 +02:00
Thaum Rystra
eb3f8c9105 fixed countdown page for not-logged in users 2020-05-07 15:53:24 +02:00
Thaum Rystra
82a2241e4d fixed countdown page for not-logged in users 2020-05-07 15:50:44 +02:00
Thaum Rystra
3804323322 Added countdown page 2020-05-07 15:42:42 +02:00
Thaum Rystra
31185a4b12 Invite system created, mostly done, but timeboxed for now 2020-05-07 14:52:18 +02:00
Thaum Rystra
4ca47d3a62 Improved account page 2020-05-02 18:22:04 +02:00
Thaum Rystra
26662b939c Improved data risk warning 2020-05-02 18:21:55 +02:00
Thaum Rystra
48e54c42b4 Improved usability and UI for Features. Fixed embedded computations 2020-05-02 17:54:22 +02:00
Thaum Rystra
d649fb9d54 Greatly improved look and feel of site navigation 2020-05-02 17:09:56 +02:00
Thaum Rystra
073578b90d Made all login Patreon only, limited some functionality to $5 patrons 2020-04-30 22:38:27 +02:00
Thaum Rystra
1ca6bc834a Removed variable names from items, now only use tags. Items can also be attuned 2020-04-30 14:52:41 +02:00
Thaum Rystra
4180e153dd Computation now handles class levels 2020-04-30 14:26:41 +02:00
Thaum Rystra
15db76a2fe Added proficiencies for languages tools and weapons to the stats tab 2020-04-30 14:26:28 +02:00
Thaum Rystra
1763358642 Ui tweaks 2020-04-29 14:00:58 +02:00
Thaum Rystra
f63faa867b Fixed deleted health bars still showing on character sheet 2020-04-29 12:48:21 +02:00
Thaum Rystra
a2fe8d950a Fixed computations throwing errors when not provided with context 2020-04-29 12:44:09 +02:00
Thaum Rystra
d63565633e Zero value stats now appear in character sheet 2020-04-29 11:10:23 +02:00
Thaum Rystra
4fd62d17bd Updated markdown santizing because the old implementation was deprecated 2020-04-29 11:03:47 +02:00
Thaum Rystra
ed9cfee9f9 Added autocomplete for fields that expect variable names 2020-04-29 10:53:06 +02:00
Thaum Rystra
966fcc449d Hit dice now explicitly set their size 2020-04-28 17:12:22 +02:00
Thaum Rystra
eae35062d0 Allowed attributes to take calculations as their base value 2020-04-28 16:23:52 +02:00
Thaum Rystra
ee0fb72d56 Ensured that properties that are removed don't show up on the character sheet 2020-04-28 14:51:53 +02:00
Thaum Rystra
528e53fc9b Fixed proficiency calculations 2020-04-27 13:56:04 +02:00
Thaum Rystra
17c9d270e6 Fixed modifiers not being computed and displayed 2020-04-27 13:19:23 +02:00
Thaum Rystra
a2bae8d12a Fixed an issue with clearing forms not correctly unsetting a value 2020-04-26 11:37:51 +02:00
Thaum Rystra
13cb9253c3 Added resource forms to actions 2020-04-26 10:28:40 +02:00
Thaum Rystra
88ed912d18 in mathjs node.eval is deprecated, replaced with .evaluate 2020-04-26 10:28:23 +02:00
Thaum Rystra
6746f5f989 Added results viewer 2020-04-26 09:42:32 +02:00
Thaum Rystra
b1328e4cf5 Action and attack components show up correctly on character sheet 2020-04-24 15:10:58 +02:00
Thaum Rystra
7bf0e959d7 reinstalled all npm packages 2020-04-24 14:07:42 +02:00
Thaum Rystra
ed35d2e984 Began work on viewers for attacks and actions 2020-04-23 19:32:48 +02:00
Thaum Rystra
a6bdfe247c Removed multipliers from reset field 2020-04-23 18:55:13 +02:00
Thaum Rystra
a86176a20d fixed action reset menu clearing throwing an error 2020-04-23 15:37:30 +02:00
Thaum Rystra
4e57bd4a73 Unified the add buttons for results as a single menu button 2020-04-23 15:37:05 +02:00
Thaum Rystra
95bfcd79c9 Added UI backend that can do computations with context 2020-04-23 14:26:05 +02:00
Thaum Rystra
7416101a34 Computation writes variables available/computed to the creature document 2020-04-16 17:57:18 +02:00
Thaum Rystra
2164174218 Fixed all effects getting added to unassigned effects even when they were assigned 2020-04-16 15:48:07 +02:00
Thaum Rystra
1717ee4bc7 Made spells into a special kind of action 2020-04-16 15:21:31 +02:00
Thaum Rystra
e06196a54c Fixed buffs not being able to be added to actions after creation 2020-04-04 19:09:00 +02:00
Thaum Rystra
6008d8b47a Made parent target of forms optional again 2020-04-04 18:44:57 +02:00
Thaum Rystra
e77513110b Refactored all forms for updated code style 2020-04-04 18:40:08 +02:00
Thaum Rystra
1856e90d12 Changed data structure around attacks and their consumed resources 2020-04-04 18:18:38 +02:00
Thaum Rystra
97111741bf updated dependencies 2020-04-04 14:30:22 +02:00
Thaum Rystra
fc5576397a Added quote rules to eslint 2020-04-04 11:45:08 +02:00
Thaum Rystra
53b1f16d2f replaced jscsrc and jshint with eslint 2020-04-04 11:31:07 +02:00
Stefan Zermatten
2981813751 Creature computations working again 2020-03-23 11:59:04 +02:00
Stefan Zermatten
74fef2bd39 Refactored computations again, split into multiple files, lots still to do 2020-03-17 16:13:18 +02:00
Stefan Zermatten
1a0c2bca78 Began refactoring character computations 2020-03-16 17:28:53 +02:00
Stefan Zermatten
5ed8e08993 Fixed effects not being able to be added to stored buffs 2020-03-16 14:16:50 +02:00
Stefan Zermatten
e3c6949491 Added search to character tree tab 2020-03-13 16:09:43 +02:00
Stefan Zermatten
e0e7693fff Improved app layout a bit 2020-03-13 14:56:59 +02:00
Stefan Zermatten
7fe2292c2a Added persona page 2020-03-13 14:56:43 +02:00
Stefan Zermatten
9290c9570c Moved organize buttons for inventory and spells tab to the right 2020-03-13 12:23:23 +02:00
Stefan Zermatten
24725381d7 Added spells tab 2020-03-13 12:02:57 +02:00
Stefan Zermatten
c1aacb9ebe Containers that have ancestor containers no longer show up in top level inventory 2020-03-13 11:42:19 +02:00
Stefan Zermatten
0e71d1c719 Moved spell lists to advanced section of spells form 2020-03-13 11:39:30 +02:00
Stefan Zermatten
2381769ea2 Inventory now uses filtered tree views to display items in containers 2020-03-13 11:23:19 +02:00
Stefan Zermatten
adfe1dc613 Markdown now works in property descriptions for viewers 2020-03-13 10:12:01 +02:00
Stefan Zermatten
27300190d3 Clicking container headers now works as expected 2020-03-12 16:51:50 +02:00
Stefan Zermatten
d90894d7c6 Fixed items not showing up in containers 2020-03-12 16:43:06 +02:00
Stefan Zermatten
9e7c1ce405 Get vue-meteor-tracker to freeze meteor data to stop a infinite flush loops 2020-03-12 16:40:37 +02:00
Stefan Zermatten
d00ff000ce Added inventory tab 2020-03-12 15:51:49 +02:00
Stefan Zermatten
e26031368d fixed inserting library node not finding the node to animate dialog to 2020-03-09 15:07:08 +02:00
Stefan Zermatten
9ac6b510e4 Added tree of sub-properties to property dialog 2020-03-09 14:29:11 +02:00
Stefan Zermatten
e67b4c72e3 Removed stray console log 2020-03-09 12:58:14 +02:00
Stefan Zermatten
1c700121ca removed accounts-meld 2020-03-09 12:56:40 +02:00
Stefan Zermatten
8e4694f63e Removed enabled/disabled property of features 2020-03-09 12:44:30 +02:00
Stefan Zermatten
827d567ac2 Fixed stats being displayed for all subscribed creatures instead of the current creature 2020-03-09 12:34:11 +02:00
Stefan Zermatten
43a08eb034 Health bar refactored to not use keycodes, which are deprecated 2020-03-09 12:08:39 +02:00
Stefan Zermatten
ea5ac42ec0 fixed padding on dark mode button 2020-03-09 10:47:39 +02:00
Stefan Zermatten
1be09e48ef Clearable selects now unset the property when cleared 2020-03-09 10:44:40 +02:00
Stefan Zermatten
625455da09 Re-organized ui/properties folder 2020-03-06 10:15:38 +02:00
Stefan Zermatten
4a25c22b64 Fixed spell and resource card adjustments 2020-03-06 10:02:39 +02:00
Stefan Zermatten
53e9be1407 Removed fibres from dependencies, meteor should come with it 2020-03-06 09:15:04 +02:00
Stefan Zermatten
7055f4d990 Added node versions to the package file to help the host build the app 2020-03-06 08:57:13 +02:00
Stefan Zermatten
797b6a0d88 Updated Meteor and npm packages 2020-03-05 15:31:13 +02:00
Stefan Zermatten
7afbfa1816 Fixed no-op bulk writes 2020-03-05 14:28:32 +02:00
Stefan Zermatten
d6877905c9 removed empty console error 2020-03-05 14:09:21 +02:00
Stefan Zermatten
69e8a307fc Removed a bunch of console noise 2020-03-05 14:05:32 +02:00
Stefan Zermatten
66e70c8c94 Libraries can now be deleted 2020-03-05 14:00:16 +02:00
Thaum Rystra
dfa3b057b0 Fixed property adjustments on the stats page 2020-03-04 09:56:06 +02:00
Thaum Rystra
f3d86ef274 Characters can now be deleted 2020-03-03 17:49:35 +02:00
Thaum Rystra
46a0e92402 Improved sharing dialog, setting a sheet as public now working 2020-03-03 17:00:05 +02:00
Stefan Zermatten
d0c8131d5f Fixed syntax errors breaking the build 2020-03-02 16:35:16 +02:00
Stefan Zermatten
5578dca6e9 began implementing sharing dialog 2020-03-02 16:31:57 +02:00
Stefan Zermatten
724f9574a2 Added basic creature document editing UI 2020-03-02 15:45:55 +02:00
Stefan Zermatten
2bd7c2908f Added remove creature method 2020-03-02 10:03:58 +02:00
Thaum Rystra
692a7983f8 Fixed inserting new characters. No wizard though 2020-02-29 17:31:21 +02:00
Stefan Zermatten
acb9369100 Removed floating action button from features tab 2020-02-18 15:44:32 +02:00
Stefan Zermatten
21dbab1877 Improved effect form and view 2020-02-18 14:05:15 +02:00
Stefan Zermatten
86926e593c Got creature recomputation to do things in sheet 2020-02-18 13:27:31 +02:00
Stefan Zermatten
81cec77c9e Added write methods to creature properties 2020-02-18 12:14:38 +02:00
Stefan Zermatten
acb8a073de Updated packes 2020-02-18 12:14:24 +02:00
Stefan Zermatten
6996d2a99e updated node packages 2020-02-10 09:53:59 +02:00
Stefan Zermatten
83b810ce3d Specified that variable names must be used in ammunition references 2020-01-27 13:50:27 +02:00
Stefan Zermatten
328480d2c5 Sidebar no longer refers to "alpha" build in preparation for beta release 2020-01-27 13:50:11 +02:00
Stefan Zermatten
8e4c6252cd Added a universal dialog for creature properties 2020-01-27 13:49:50 +02:00
Stefan Zermatten
c3cc4c881d Migrated creature computations to use the new data structure for creature properties 2020-01-27 11:21:52 +02:00
Stefan Zermatten
4ee7307e34 Migrating character sheet to new data format 2020-01-16 08:55:53 +02:00
Stefan Zermatten
eabc0aa32e Groundwork for default libraries and slots 2019-11-13 11:54:27 +02:00
Stefan Zermatten
ae0b060f01 Added inserting library subtrees as character property subtrees 2019-11-05 10:56:04 +02:00
Stefan Zermatten
79a4488a28 Fixed a layout issue with library and node view 2019-11-04 13:38:42 +02:00
Stefan Zermatten
dfa13ef2c3 Disabled character list 2019-11-04 13:30:46 +02:00
Stefan Zermatten
f6d80f6ae4 Made big improvements to library page in preparation of adding library nodes to character sheets 2019-11-04 13:27:31 +02:00
Stefan Zermatten
f4d613a20b Started working on getting creature property insertion working 2019-09-27 11:06:33 +02:00
Stefan Zermatten
73f193460d Fixed Roll form 2019-09-26 11:29:46 +02:00
Stefan Zermatten
ba4cc1a496 Fixed more odering bugs, still flashes before being sorted though :( 2019-09-25 16:08:01 +02:00
Stefan Zermatten
21030d186a Added saving throws, messed with data structure a bunch, and updated forms to suit 2019-09-25 14:38:20 +02:00
Stefan Zermatten
4fcae9b3f9 Fixed a bug where moving documents around deleted docs breaks the ordering 2019-09-25 14:37:54 +02:00
Stefan Zermatten
66d11d58f3 Data changes to make attacks top level objects 2019-09-19 16:03:46 +02:00
Stefan Zermatten
63a20b2bef Moved subschema folder 2019-09-16 10:24:13 +02:00
Stefan Zermatten
88a2a506b0 Renamed RollResult to RollResults 2019-09-16 09:39:04 +02:00
Stefan Zermatten
710c578119 Fiddled with rolls and saves 2019-09-02 13:49:01 +02:00
Stefan Zermatten
a8b3fc3f2f Started restructuring the library with attacks, saves, and limited parenting 2019-08-12 16:42:30 +02:00
Stefan Zermatten
6f4710bee3 Roll modifiers are effects that target rolls based on their tags 2019-08-06 16:37:52 +02:00
Stefan Zermatten
50621ca269 Damage multipliers can now target or exclude tags 2019-08-06 16:37:38 +02:00
Stefan Zermatten
16d61eb708 Effects can now impact multiple stats 2019-08-06 16:36:37 +02:00
Stefan Zermatten
b110eadb7c Rerouted pages that shouldn't work to an info alert making that clear 2019-08-06 13:17:04 +02:00
Stefan Zermatten
45c84e28a3 Improved property viewers, added some new ones 2019-08-06 13:16:46 +02:00
Stefan Zermatten
b70634f5de removed old property components 2019-08-06 10:37:37 +02:00
Stefan Zermatten
e3f18fab69 Moved sidebar component to layout folder 2019-08-06 10:37:25 +02:00
Stefan Zermatten
56f3af3c4b Client can't read node environment... changed storybook flag to public setting 2019-08-06 10:13:55 +02:00
Stefan Zermatten
a58def26d1 Added storybook to production if SHOW_STORYBOOK env variable is true 2019-08-06 10:03:27 +02:00
Stefan Zermatten
0014c691d2 Added alternative text for missing property viewers 2019-08-05 12:10:50 +02:00
Stefan Zermatten
5436b12108 Added more property viewers 2019-08-05 12:04:26 +02:00
Stefan Zermatten
d45b184170 moved effect viewer 2019-08-02 13:00:16 +02:00
Stefan Zermatten
0b184c4e85 Library node editing now includes editing sub-documents and soft removal 2019-08-02 12:35:59 +02:00
Stefan Zermatten
745f4fd353 Library nodes can now be edited :D 2019-08-02 10:47:29 +02:00
Stefan Zermatten
3c4f3e26f8 Added edit button to library node view 2019-08-01 16:06:43 +02:00
Stefan Zermatten
67ea67148f Added a description field to attributes 2019-08-01 15:45:15 +02:00
Stefan Zermatten
f37ff919fb Fixed library node creation dialog 2019-08-01 15:25:52 +02:00
Stefan Zermatten
76b6501b31 Improved library view layout 2019-08-01 14:39:15 +02:00
Stefan Zermatten
229a5dddcf Added attribute property viewer, incomplete 2019-08-01 14:03:51 +02:00
Stefan Zermatten
e29c77dc67 Changed experience title to name for consistency 2019-08-01 14:03:29 +02:00
Stefan Zermatten
c87a3a3f60 Moved ui/forms to ui/properties/forms 2019-08-01 12:07:57 +02:00
Stefan Zermatten
9f7d6b8ae7 began work on generalized property viewer 2019-08-01 12:03:40 +02:00
Stefan Zermatten
4ccf999fc7 Trees can now do selection 2019-08-01 12:03:15 +02:00
Stefan Zermatten
549418b395 Iteration on library UI 2019-07-31 15:04:52 +02:00
Stefan Zermatten
14fe48efb3 Moved imports/ui/creature/properties to /imports/ui/properties 2019-07-31 12:02:26 +02:00
Stefan Zermatten
18eeaf4884 Added back button to library page 2019-07-31 12:00:49 +02:00
Stefan Zermatten
4f93ad3e9b Trees can now be freely re-arranged :D 2019-07-31 11:52:11 +02:00
Stefan Zermatten
d0304da4fd Began making generic tree re-arranging methods, still buggy 2019-07-30 16:47:21 +02:00
Stefan Zermatten
4b7ff2146f Purged all references to the schema factory, use SCHEMA_OPTIONS constant instead 2019-07-30 15:21:30 +02:00
Stefan Zermatten
2385b69720 Removed all separate property collections to be replaced with a single "creature property" collection 2019-07-30 15:13:39 +02:00
Stefan Zermatten
31bc3663a7 Moved properties out of creature folder, since they apply to library nodes as well now 2019-07-30 14:50:08 +02:00
Stefan Zermatten
cbdd72e09b Some changes to how parenting and ordering of docs interface 2019-07-30 14:48:49 +02:00
Stefan Zermatten
438f128641 Moved parenting folder from /api/creatures/ to /api/ 2019-07-30 13:07:31 +02:00
Stefan Zermatten
3460cbf5a0 Significantly improved tree view of libraries 2019-07-30 12:49:20 +02:00
Stefan Zermatten
da561bfc83 Added night mode to account settings 2019-07-30 10:09:01 +02:00
Stefan Zermatten
eb63a7a7f3 made XP optional in experiences, so that experiences can just be journal entries 2019-07-30 10:01:23 +02:00
Stefan Zermatten
b5ceb7fad0 updated npm packages 2019-07-30 09:59:18 +02:00
Stefan Zermatten
fb5b2b8ada Removed creature from property selector, creatures aren't properties 2019-07-30 09:59:10 +02:00
Stefan Zermatten
ea628ed379 Fixed some broken switches 2019-07-29 13:25:58 +02:00
Stefan Zermatten
d35fa447a3 Added item and container forms 2019-07-29 13:25:47 +02:00
Stefan Zermatten
73b43574ee Added spell and spell list forms 2019-07-29 12:49:19 +02:00
Stefan Zermatten
02cb690325 Spells now reference their spell lists directly 2019-07-25 15:44:45 +02:00
Stefan Zermatten
5662ce3666 Added tags to actions 2019-07-25 14:04:36 +02:00
Stefan Zermatten
45e36ac3d4 Added skill form, abstracted proficiency selection 2019-07-23 12:37:30 +02:00
Stefan Zermatten
812a6679d5 Added roll form 2019-07-23 12:02:19 +02:00
Stefan Zermatten
1bfb48c672 Added forms for Class level, damage multiplier, experience, folder, note, proficiency 2019-07-23 11:28:26 +02:00
Stefan Zermatten
946b47ea61 Gave buff targets a default value 2019-07-22 13:51:32 +02:00
Stefan Zermatten
bd3f676919 Fixed some import errors 2019-07-22 13:45:30 +02:00
Stefan Zermatten
4062d79e90 Reorganized forms into their own folder 2019-07-22 13:05:11 +02:00
Stefan Zermatten
dfa302a4a9 Completed Action form, effects form, reworked form event api 2019-07-22 12:43:57 +02:00
Stefan Zermatten
baa1c0967c Update meteor and npm packages to fix security vulnerabilities 2019-07-19 14:27:34 +02:00
Stefan Zermatten
a0dc36557c Added action form, but without buffs 2019-07-19 14:07:22 +02:00
Stefan Zermatten
0c002ae5cd Moved some fields to advanced section of attribute form 2019-07-18 16:25:52 +02:00
Stefan Zermatten
11d3b0fa8d Improved style of attribute form 2019-07-18 14:36:39 +02:00
Stefan Zermatten
d28d6de684 Inserting library nodes into libraries now works as expected 2019-07-03 14:19:16 +02:00
Stefan Zermatten
93d8a8d33e Library attribute insert form complete 2019-07-02 17:28:07 +02:00
Stefan Zermatten
4abb5edbf3 Got library attribute form onto the screen :D 2019-06-28 14:48:02 +02:00
Stefan Zermatten
9757da2cae Started work on library node insert forms 2019-06-27 16:52:28 +02:00
Stefan Zermatten
bd4fb58935 updated packages 2019-06-24 10:28:44 +02:00
Stefan Zermatten
1add44f0e7 renamed SmartInput to SmartInputMixin 2019-06-24 10:28:33 +02:00
Stefan Zermatten
99db45e522 Added function to convert libraryNode lists into forest of object trees 2019-06-20 16:43:18 +02:00
Stefan Zermatten
7a59b4542e added library publication 2019-06-20 16:42:05 +02:00
Stefan Zermatten
335608f6a9 Fixed library subscription 2019-06-14 13:07:26 +02:00
Stefan Zermatten
4d47584f4f Routed library page and cleaned up errors 2019-06-13 17:07:31 +02:00
Stefan Zermatten
48fe0d3608 Started on library page UI 2019-05-31 15:36:19 +02:00
Stefan Zermatten
9a194a20cb Added library list UI 2019-05-10 13:05:21 +02:00
Stefan Zermatten
de183297fc Updated some NPM packages to fix vulnerabilities 2019-05-10 11:32:43 +02:00
Stefan Zermatten
d921ad46d8 Merge pull request #215 from pspeter3/docker-development
Add Docker Compose
2019-04-29 09:37:34 +02:00
Phips Peter
00d02a3bb5 Add Docker Compose
In order for a developer to run DiceCloud locally, all they need to do is run `docker-compose up --build`. This does take a very long time to execute but prevents a developer from needing to deal with configuration.
2019-04-25 16:48:27 -07:00
Stefan Zermatten
b0caffae1a Added better data to the Tree node list story 2019-04-24 14:05:16 +02:00
Stefan Zermatten
e71bfb2691 Improved tree view 2019-04-24 13:22:31 +02:00
Stefan Zermatten
fc24cf4a5b Improved tree view with drag and drop 2019-04-15 15:40:42 +02:00
Stefan Zermatten
062e554629 Added comments to libraries 2019-04-15 12:39:42 +02:00
Stefan Zermatten
a1d77cdaab added update method to library nodes 2019-04-15 12:27:14 +02:00
Stefan Zermatten
f4011abf7b Moved all creature related API to the creature folder 2019-04-15 12:09:37 +02:00
Stefan Zermatten
aa9802b34e Iterated on library nodes 2019-04-15 12:04:17 +02:00
Stefan Zermatten
dabb54b0a3 Refactored UI folder structure 2019-04-15 11:44:27 +02:00
Stefan Zermatten
05867c61dd Started on tree view for real 2019-04-12 13:37:47 +02:00
Thaum Rystra
ea968ad955 Merge branch 'version-2' of https://github.com/ThaumRystra/DiceCloud into version-2 2019-04-06 10:56:57 +02:00
Thaum Rystra
b7b0ac9c00 Separated parser class nodes and began writing compile methods 2019-04-06 10:56:53 +02:00
Stefan Zermatten
ab2ea36541 Added markdown support to features 2019-04-05 13:05:45 +02:00
Stefan Zermatten
b35e08b9b4 Iterated on features UI 2019-04-03 16:50:22 +02:00
Stefan Zermatten
021a53ef36 Moved properties schema, added 'removeProperty' method 2019-04-03 16:50:12 +02:00
Stefan Zermatten
681e669e76 Refactored schemas to make properties all implicitely children 2019-04-03 12:38:01 +02:00
Stefan Zermatten
28ffcc87b4 updated package name 2019-04-03 10:28:04 +02:00
Stefan Zermatten
888a7ea592 Removed Bowerrc, nothing uses bower anymore 2019-04-03 10:27:11 +02:00
Stefan Zermatten
053a7a36a6 Began generalizing insert forms for character properties to reduce duplicated code 2019-04-01 16:58:15 +02:00
Stefan Zermatten
6d68796a11 Fixed nasty bug where mixins were bashing the schemas passed to them 2019-04-01 16:57:29 +02:00
Stefan Zermatten
18493afbbf Collated update methods into an index, fixed typo 2019-04-01 15:51:11 +02:00
Stefan Zermatten
a94f437ba8 methods now use correct mixins 2019-04-01 13:48:39 +02:00
Stefan Zermatten
d21827106c Moved parse tree classes out of grammar.js started working on compilation. Broke the build 2019-03-29 14:08:09 +02:00
Stefan Zermatten
caf7f3efb9 Moved parser into main source folders 2019-03-27 12:23:27 +02:00
Stefan Zermatten
484b73d836 Moved parser javascript into single block 2019-03-27 11:25:34 +02:00
Stefan Zermatten
ee788c952a Improved grammar 2019-03-27 11:06:43 +02:00
Stefan Zermatten
ffa98d76fc Began writing a custom parser for calculations 2019-03-26 16:32:24 +02:00
Stefan Zermatten
5bb5f047f4 Moved a lot of functionality to mixins, improved parenting 2019-03-19 15:57:21 +02:00
Stefan Zermatten
1146d56324 Fixed jshint warnings 2019-03-19 09:31:14 +02:00
Stefan Zermatten
ef83d54fd9 Reorganized folder structure, removed legacy v1 code 2019-03-14 10:10:58 +02:00
Stefan Zermatten
18e3f653f3 Added insert and update methods for all properties 2019-03-13 14:03:03 +02:00
Stefan Zermatten
572aca5906 Moved name to properties schema 2019-03-13 09:40:20 +02:00
Stefan Zermatten
f3d19d3b38 Changed instances to encounters 2019-03-13 09:31:46 +02:00
Stefan Zermatten
94f6631a7d Overhauled data models to make actions and libraries more universal 2019-03-12 16:47:20 +02:00
Stefan Zermatten
febb65a513 Re-wrote parenting, should be significantly faster, more maintainable 2019-03-08 13:57:24 +02:00
Stefan Zermatten
15c11e16ab Began adding generic child lists of effects, proficiencies, etc. 2019-03-06 11:58:40 +02:00
Stefan Zermatten
8772e539da Made initiative a skill of type "check" instead of a modifier 2019-02-27 16:08:20 +02:00
Stefan Zermatten
4e7c0c5a90 Removed skill type limit from proficiencies that are included in computation 2019-02-27 15:51:44 +02:00
Stefan Zermatten
0ae2cc5545 Changed creature computations to leverage the MathJS parser (big deal) 2019-02-27 15:49:28 +02:00
Stefan Zermatten
687b517164 Added spell slot display, improved resource card 2019-02-26 16:37:19 +02:00
Stefan Zermatten
8305596373 Added resource cards 2019-02-26 14:11:59 +02:00
Stefan Zermatten
8f624b24a9 Organised components into folders 2019-02-26 11:27:48 +02:00
Stefan Zermatten
a2432c7161 Moved attribute related components into their own folder 2019-02-26 11:20:01 +02:00
Stefan Zermatten
b7a4a3d3fa Added simple feature UI components and insertion dialog 2019-02-25 15:38:57 +02:00
Stefan Zermatten
e5ff116208 Made the stats FAB fixed instead of absolute 2019-02-21 10:57:28 +02:00
Stefan Zermatten
24f3431900 Removed all spaces around v-icon's text, because it causes alignment issues 2019-02-20 15:48:35 +02:00
Stefan Zermatten
4f402830d8 Added attribute insertion UI and API 2019-02-20 14:30:04 +02:00
Stefan Zermatten
9e208ad3b3 Dialog stack callbacks can now return a return element ID to animate to 2019-02-20 14:29:33 +02:00
Stefan Zermatten
9d027aeabf Allowed smart inputs to accept errors as props 2019-02-20 14:27:20 +02:00
Stefan Zermatten
97ec5d4b5c Return element IDs can now be set when popping the dialog stack 2019-02-20 10:30:41 +02:00
Stefan Zermatten
6c421c3a98 Smart inputs now run all their debounced functions before they get destroyed 2019-02-20 10:30:07 +02:00
Stefan Zermatten
665a9d716a Unset modifiers of attributes that aren't abilities 2019-02-20 09:27:37 +02:00
Stefan Zermatten
aa7a426e9f Fixed some issues with computation 2019-02-19 11:12:46 +02:00
Stefan Zermatten
da6909a997 Added skill dialog 2019-02-19 11:12:37 +02:00
Stefan Zermatten
3129b86ef0 Improved action and attack schemas 2019-02-18 15:52:17 +02:00
Stefan Zermatten
e63ae96cb5 Changed dialog stack from using element ids to data-ids to allow duplicate ids to work 2019-02-18 15:17:31 +02:00
Stefan Zermatten
a31a70f435 Made health bar editing compatible with dark mode 2019-02-18 14:26:46 +02:00
Stefan Zermatten
c5899e0816 Improved dialog stack handling of scrolling while the stack is open 2019-02-18 13:41:36 +02:00
Stefan Zermatten
d30ee06e33 Added edit tab to base dialogs, added edit screen to attribute dialog 2019-02-18 13:41:21 +02:00
Stefan Zermatten
5d45788521 Fixed a bug with smart inputs getting stuck loading if you set them to their original value and tabbed away 2019-02-15 14:50:08 +02:00
Stefan Zermatten
23cd5d6a12 Added effect edit expansion panel 2019-02-15 14:25:11 +02:00
Stefan Zermatten
38da95256b Made icon search compatible with dark mode 2019-02-15 12:48:24 +02:00
Stefan Zermatten
f773a20a59 Made hit dice list tiles compatible with dark mode 2019-02-15 12:24:52 +02:00
Stefan Zermatten
23654cd09c Made effect edit compatible with dark mode 2019-02-15 12:21:02 +02:00
Stefan Zermatten
09b2c38b43 Made color picker compatible with dark mode 2019-02-15 12:17:12 +02:00
Stefan Zermatten
1103248574 Abstracted effect list tile into its own component and made it dark mode compatible 2019-02-15 12:03:57 +02:00
Stefan Zermatten
96bb072dad Made health bar compatible with dark mode 2019-02-15 11:46:25 +02:00
Stefan Zermatten
3f6b3fe4c1 Made ability list tiles compatible with dark mode 2019-02-13 15:47:25 +02:00
Stefan Zermatten
ad7a485778 Added dark mode 2019-02-13 14:44:25 +02:00
Stefan Zermatten
58949e14fe Added a tree view of the character, fixed the issue it revealed 2019-02-11 16:15:57 +02:00
Stefan Zermatten
8bcb4407e6 Added method to persist hit dice arrow buttons to database 2019-02-11 13:55:43 +02:00
Stefan Zermatten
fd15e8a6c7 Removed duplicate hit dice effects 2019-02-11 13:53:11 +02:00
Stefan Zermatten
938573a4b4 Improved attribute dialogs 2019-02-07 15:36:47 +02:00
Stefan Zermatten
465f6645b7 Fixed colors of character sheet and base dialog toolbars 2019-02-07 13:56:48 +02:00
Stefan Zermatten
4b25373c7c Abstracted text fields into smart input components 2019-02-07 13:53:44 +02:00
Stefan Zermatten
5142b8e1a0 Merge branch 'version-2' of https://github.com/ThaumRystra/DiceCloud1 into version-2 2019-02-07 10:02:50 +02:00
Thaum Rystra
f8e83ebe7e Added text-field component to edit database text without it getting bashed by data cleaning 2019-02-06 19:38:27 +02:00
Stefan Zermatten
b67926e0fc Added schema defaults to all schemas to prevent strings from being trimmed 2019-02-06 17:32:08 +02:00
Stefan Zermatten
bf2e9439cf Added debouncing for ui update functions 2019-02-06 17:26:56 +02:00
Stefan Zermatten
de4509ab6a made reset options clearable 2019-02-06 15:28:23 +02:00
Stefan Zermatten
f42f9590d2 Updated packages 2019-02-06 15:28:02 +02:00
Stefan Zermatten
29c5ed2194 Improved select menus 2019-02-06 14:46:21 +02:00
Stefan Zermatten
cf0440a8db Added color picker 2019-02-06 13:43:11 +02:00
Stefan Zermatten
4917729f29 Added basic attribute edit form 2019-02-06 11:16:32 +02:00
Stefan Zermatten
fe8e72d225 Changed primary color to red, since form elements default to it, secondary color is now grey 2019-02-06 10:57:47 +02:00
Stefan Zermatten
0e913b1f63 Items now have a number of uses which can be used up. 2019-02-06 10:08:22 +02:00
Stefan Zermatten
58f26cf849 Added sensible defaults to character creation dialog 2019-02-04 14:49:28 +02:00
Stefan Zermatten
836b10d499 Fixed some bugs in creature computations, removed debugging logs 2019-02-04 14:49:14 +02:00
Stefan Zermatten
8d4146f242 Added constitution bonus to hitpoints as a default effect 2019-02-04 14:48:25 +02:00
Stefan Zermatten
7742e5deef Gave defaults to adjustments and effects for attributes in case they have not been set yet 2019-02-04 14:30:41 +02:00
Stefan Zermatten
41c69ae040 Allowed character creation dialog to be cancelled 2019-02-04 14:30:17 +02:00
Stefan Zermatten
c6e7f8eeb6 Added effect editing component, abstracted out operation icons 2019-02-04 14:30:04 +02:00
Stefan Zermatten
e3e7b76f02 Results of effects are now stored on the effect, fixed defaults to suit 2019-02-04 14:29:03 +02:00
Stefan Zermatten
2dcbc91ccd Removed value from effects, calculation now stores the effect value even if it's a number 2019-02-04 14:28:21 +02:00
Stefan Zermatten
2fcf3047ee made sure healthBar elements that will be animated to have solid backgrounds 2019-01-31 10:34:50 +02:00
Stefan Zermatten
a95ae88a31 Fixed dialog animations to elements with no box shadow 2019-01-31 10:34:17 +02:00
Stefan Zermatten
f6aca08f63 Added missing import 2019-01-31 10:33:59 +02:00
Stefan Zermatten
87ee39b5bd Updated Readme 2019-01-31 10:08:01 +02:00
Stefan Zermatten
d466bfe428 Allowed skills to come with a base proficiency 2019-01-31 09:27:27 +02:00
Stefan Zermatten
e357b29145 Added labels to healthBars and improved attribute dialog opening 2019-01-30 16:38:47 +02:00
Stefan Zermatten
a80d070533 Refactored ability dialogs as generalised attribute dialogs 2019-01-30 14:10:46 +02:00
Stefan Zermatten
80d369f0d4 Got healthbars persisting data to the database 2019-01-30 13:34:45 +02:00
Stefan Zermatten
f6b0c746cc Improved ability dialogs 2019-01-29 16:36:13 +02:00
Stefan Zermatten
4584499019 Added A way to get Game-icons.net icons into the database (it's not secure yet), plus some icon ui 2019-01-28 16:26:39 +02:00
Stefan Zermatten
77d2f87373 Fixed an issue caused by storing components on the store, added ability dialog 2019-01-24 16:45:02 +02:00
Stefan Zermatten
8c0edfaa93 Added separate return element ids to dialogs 2019-01-24 15:11:42 +02:00
Stefan Zermatten
2e6ef52594 Dialog stack animations complete 2019-01-24 14:40:38 +02:00
Stefan Zermatten
00e8cbc1c8 Added dialog animations, still working on box shadows 2019-01-23 16:49:58 +02:00
Stefan Zermatten
e8a0e86548 Added creature view and edit permissions 2019-01-23 16:49:47 +02:00
Stefan Zermatten
60dfba3b46 Completed the stats tab, conditions not added yet 2019-01-21 16:03:05 +02:00
Stefan Zermatten
e43718f034 Removed outdated comment 2019-01-18 13:27:56 +02:00
Stefan Zermatten
36022e4bc4 Added hit dice tiles 2019-01-18 13:00:44 +02:00
Stefan Zermatten
0497223804 Added head request for roboto so that all font weights work 2019-01-18 12:54:40 +02:00
Stefan Zermatten
ef9512f0ec Moved storybook to route of UI 2019-01-18 12:18:16 +02:00
Stefan Zermatten
369dae17ee Storybook now has the option not to wrap a story in a card 2019-01-18 11:34:16 +02:00
Stefan Zermatten
4728d06c0b Added attribute cards 2019-01-18 10:38:28 +02:00
Stefan Zermatten
2d3cb367da Added ability score list tiles 2019-01-17 16:37:37 +02:00
Stefan Zermatten
f2137e26b2 Removed the space between a skill name and the conditional benefit astrisk 2019-01-17 15:35:16 +02:00
Stefan Zermatten
0bd654e557 Gave health bars a background fade so that you can click away from them to close the edit box 2019-01-17 15:26:07 +02:00
Stefan Zermatten
f06c4adb32 Corrrected the full proficiency icon 2019-01-17 15:07:12 +02:00
Stefan Zermatten
fabf377f72 Added Skill list tiles 2019-01-17 15:01:45 +02:00
Stefan Zermatten
03c244e7c9 Added health bar story and improved health bar functionality 2019-01-17 15:01:34 +02:00
Stefan Zermatten
ea7eabf27d Updated packages and meteor version, now need NO_HMR=1 when running in dev mode 2019-01-17 15:01:16 +02:00
Stefan Zermatten
bbbed216eb Added A dev-only storybook 2019-01-17 15:00:44 +02:00
Stefan Zermatten
c5cb98dc9f Added basic health bar with editing popup 2019-01-09 14:40:44 +02:00
Stefan Zermatten
aa6d973ae1 Character sheet toolbars now take on their character's color 2019-01-09 14:40:31 +02:00
Stefan Zermatten
76da2c8393 Added Character sheet 2018-12-21 12:17:49 +02:00
Stefan Zermatten
e0195499e5 Fixed broken sidebar links 2018-12-21 10:51:33 +02:00
Stefan Zermatten
798cf3edd7 Characters now insert with intelligent defaults based on the character wizard 2018-12-19 14:15:56 +02:00
Stefan Zermatten
13669fdc91 Refactored character insertion and continued work on creation dialog 2018-12-18 14:20:17 +02:00
Stefan Zermatten
dcc460d9e6 Migrations 2018-11-28 11:48:49 +02:00
Stefan Zermatten
c7fcb4de0c More migrations... 2018-11-15 15:22:20 +02:00
Stefan Zermatten
a5e7bd95c7 Indexed publications and imported them when the server starts 2018-11-15 11:23:57 +02:00
Stefan Zermatten
51919297df moved publications to imports folder 2018-11-15 11:20:49 +02:00
Stefan Zermatten
5b80032fa1 Migrated publications to import format 2018-11-15 11:20:25 +02:00
Stefan Zermatten
ade614dd5f Finished migrating creatures to simpl-schema 2018-11-15 11:11:26 +02:00
Stefan Zermatten
c4984c07bf More progress on migrating schema 2018-11-12 16:23:43 +02:00
Stefan Zermatten
b49ab67390 Removed .idea folder 2018-10-29 12:28:59 +02:00
Stefan Zermatten
c7f0c0a4c4 Removed old polymer components 2018-10-29 12:28:33 +02:00
Stefan Zermatten
7cda854d22 Continued migrating to Simpl-Schema and imports 2018-10-19 14:01:23 +02:00
Stefan Zermatten
989706483a updated CustomBuffs 2018-10-12 13:44:36 +02:00
Stefan Zermatten
53a1137848 Updated simpl-schema and collection2, started untangling the mess that made 2018-10-12 13:38:51 +02:00
Stefan Zermatten
e3065f089f Moved effects to default character stats 2018-10-12 12:38:24 +02:00
Stefan Zermatten
6aaa4ebe00 Added base values to skills to make implementing non player creatures easier 2018-10-12 12:35:15 +02:00
Stefan Zermatten
158615c25c moved exports to the function definitions, rather than bottom of file 2018-10-12 12:24:56 +02:00
Stefan Zermatten
4cd46fe209 Refactored creature computation with more comments 2018-10-12 12:14:15 +02:00
Stefan Zermatten
04059709eb Got creature computation working again after moving to imports dir 2018-10-12 11:59:29 +02:00
Stefan Zermatten
d117570165 renamed model to api 2018-10-12 09:28:16 +02:00
Stefan Zermatten
4b900d5664 lowercased all model directories 2018-10-12 09:21:03 +02:00
Stefan Zermatten
189a1d0a16 Started work on migrating model to lazy evaluation 2018-10-12 09:18:18 +02:00
Stefan Zermatten
ca8223ccad Moved the model into the imports directory 2018-10-12 09:01:23 +02:00
Stefan Zermatten
b2eed9a672 Renamed Characters to Creatures 2018-10-12 08:52:14 +02:00
Stefan Zermatten
d330e15dce Added character creation dialog 2018-10-11 16:39:55 +02:00
Stefan Zermatten
4ac56a31de Successful registration pushes the user to the character list 2018-10-11 11:50:19 +02:00
Stefan Zermatten
41a25151fc Rolled back 1.8 update because it broke the build 2018-10-11 11:39:27 +02:00
Stefan Zermatten
e1c1b727ed updated Meteor to 1.8 2018-10-11 10:44:07 +02:00
Stefan Zermatten
dc8185df98 Got basic dialog working, no morph animation yet 2018-10-09 16:24:43 +02:00
Stefan Zermatten
89820780b5 Added accounts page 2018-10-05 12:55:21 +02:00
Stefan Zermatten
d0ce162315 Made sure some links use vue router 2018-10-03 16:37:12 +02:00
Stefan Zermatten
6835f5f4f9 Removed blaze, old client side code 2018-10-03 11:14:23 +02:00
Stefan Zermatten
e9d5e85e75 Started on dialog stack 2018-10-02 08:51:04 +02:00
Stefan Zermatten
c46f8c5171 Speed dial fixed 2018-10-02 08:50:45 +02:00
Thaum Rystra
f41ff1c52f Started work on vue sign-in 2018-10-01 09:38:03 +02:00
Stefan Zermatten
7f418c26da Started work on character list page 2018-09-28 13:07:32 +02:00
Stefan Zermatten
acdc084905 Home and sidebar implemented 2018-09-28 13:07:12 +02:00
Stefan Zermatten
6a73d3576a Added Vue, looks like this branch is now going to be DiceCloud V2 2018-09-27 14:55:27 +02:00
Stefan Zermatten
d82031ab0d Added indices, fixed bugs 2018-09-27 09:39:21 +02:00
Stefan Zermatten
cf33bfcce1 Fixed compile errors 2018-09-25 16:04:30 +02:00
Stefan Zermatten
005433268d Added migration code 2018-09-25 15:23:52 +02:00
Stefan Zermatten
ae470642c1 Computed characters are now written back to the database 2018-09-25 11:51:06 +02:00
Stefan Zermatten
dd89556b7f Moved recompute character XP and Weight Carried around 2018-09-25 11:10:47 +02:00
Stefan Zermatten
b3c2176de8 Added weight carried recomputation 2018-09-25 09:07:43 +02:00
Stefan Zermatten
9fccc3be61 Renamed addDefaultAttributes to addDefaultStats 2018-08-28 09:57:09 +02:00
Stefan Zermatten
bc26a77f92 Merge branch 'feature-server-computation' of https://github.com/ThaumRystra/DiceCloud1 into feature-server-computation 2018-08-28 09:55:26 +02:00
Stefan Zermatten
fa3cca7193 Added baseValue to attributes 2018-08-28 09:55:22 +02:00
Thaum Rystra
662fc91e17 New characters get the appropriate attributes/skills/damageMultipliers 2018-08-25 20:19:17 +02:00
Stefan Zermatten
b4506fd939 Began abstracting default abilities, damage multipliers and skills into rulesets. 2018-08-24 13:30:31 +02:00
Stefan Zermatten
5c40a84660 Changed Characters to creatures, as the new attributes system is generalisable 2018-08-24 12:14:03 +02:00
Stefan Zermatten
7c9687955d Added Models for attributes, skills and damage multipliers 2018-08-24 12:12:38 +02:00
Stefan Zermatten
b21a91b0aa Added very basic testing, got attribute calculations working 2018-08-24 10:58:59 +02:00
Stefan Zermatten
f4b1da0c80 Comments and some refactoring 2018-08-23 16:17:42 +02:00
Stefan Zermatten
755e7fba30 Started the big move to server-side computation. 2018-08-23 15:55:21 +02:00
Stefan Zermatten
41c90bb69f Updated Meteor 2018-08-23 11:45:38 +02:00
Stefan Zermatten
68432541db Merge pull request #168 from Frogvall/master
Adding advantage/disadvantage arrows to initiative stat card
2018-08-23 11:34:02 +02:00
Frogvall
c417c45db1 Cleaned up a bit 2018-06-20 14:33:34 +02:00
Frogvall
3f3caf63e4 Add css and js implementation of previously naïve approach
Bonus: Learned some meteor :)
2018-06-20 14:25:18 +02:00
Jonas
49c5a1fcb1 Added adv class to skills in stat cards
Added arrows to initiative, for example
2018-06-18 00:51:20 +02:00
Stefan Zermatten
9fb37148fa Merge pull request #167 from mommothazaz123/master
Add raw character JSON output, improve ratelimiter rules of API
2018-06-08 09:26:52 +02:00
Andrew Zhu
a67d7fb4ea Merge branch 'master' into master 2018-06-07 02:06:34 -07:00
Andrew Zhu
8e0f19742b fix some branch conflicts 2018-06-07 02:03:12 -07:00
Andrew Zhu
216e502c8a add permission check to JSONcharacter 2018-06-07 01:38:29 -07:00
Andrew Zhu
1a18d1f816 update README to reflect dir name change 2018-06-07 01:08:41 -07:00
Andrew Zhu
c099e3173b rename /rpg-docs to /app 2018-06-07 01:07:49 -07:00
Andrew Zhu
de93636c7c Revert "add mixmax:smart-disconnect to reduce server load"
This reverts commit 465a61f80f.
2018-06-07 01:03:16 -07:00
Stefan Zermatten
5e263443b3 Updated dependencies 2018-06-04 15:42:23 +02:00
Stefan Zermatten
8c3a891254 Merge branch 'bugfix-163' 2018-06-04 15:29:14 +02:00
Stefan Zermatten
e737067990 Made sure empty party names take up some space so they can be edited 2018-06-04 15:28:08 +02:00
Andrew Zhu
465a61f80f add mixmax:smart-disconnect to reduce server load 2018-05-27 22:58:02 -07:00
Andrew Zhu
bbf42aaf97 make ratelimiter actually match, return time to reset on hitting ratelimit 2018-05-25 01:39:35 -07:00
Andrew Zhu
b20d086a24 add character JSON endpoint, improve ratelimit rules 2018-05-25 01:34:39 -07:00
Stefan Zermatten
52baf297ca Changed rpg-docs folder to app 2018-05-21 14:21:07 +02:00
Thaum Rystra
45e9f491ff Merge branch 'feature-blacklist' 2018-04-02 14:27:24 +02:00
Thaum Rystra
742b26b0de Added rate limiting logging and an error page for hitting the rate limit on opening characters 2018-04-02 14:26:55 +02:00
Stefan Zermatten
164ba78c81 Added blacklist checks and rate limit logging
Needs testing
2018-03-12 09:22:04 +02:00
Thaum Rystra
e27211b24d Merge branch 'enhancement-154' 2018-03-03 20:13:03 +02:00
Thaum Rystra
30987752cc Hotfix - Remove localhost from image link on printed character sheet 2018-03-03 17:28:42 +02:00
Thaum Rystra
058ee2691f Merge branch 'feature-print'
# Conflicts:
#	rpg-docs/package-lock.json
2018-03-03 17:18:16 +02:00
Thaum Rystra
f0cf7f4956 Added QR code and finished page 1 2018-03-03 17:13:36 +02:00
Thaum Rystra
75c8720b04 Moved printed character sheets to their own page
This makes sure the entire printed sheet is rendered before the browser  attempts to print it, solving all manner of errors
2018-03-03 11:13:16 +02:00
Thaum Rystra
f73f2f670f Formatted all existing printed character sheet fields nicely 2018-03-02 21:40:21 +02:00
Thaum Rystra
c6e62e1cfa Added borders to ability scores and AC 2018-03-02 07:25:37 +02:00
Jacob
4e574c0f51 Added "clear" (reset to 0) button to resource cards 2018-02-24 14:49:42 +00:00
Jacob
80b195b7f7 Added reset button to resource cards 2018-02-24 14:42:41 +00:00
Thaum Rystra
67956d9a42 Fixed dropdown boxes being clipped in dialogs, updated Meteor 2018-01-26 18:18:49 +02:00
Stefan Zermatten
64b3ca6066 Added proficiencies to printed sheet; some fixes 2017-12-12 15:54:21 +02:00
Stefan Zermatten
8349f7da9b Merge branch 'master' into feature-print 2017-12-06 09:22:57 +02:00
Stefan Zermatten
0636042878 Added core-js because an error message told me to 2017-11-22 14:34:54 +02:00
Stefan Zermatten
6207ffa516 Updated meteor 2017-11-21 10:53:07 +02:00
Stefan Zermatten
9d33612054 Merge branch 'feature-rate-limiting-api'
closes #141
2017-11-21 08:50:03 +02:00
Stefan Zermatten
face6387a0 Fixed attacks not being added to spells added with multi-add
Fixes #145
2017-11-01 15:55:07 +02:00
Stefan Zermatten
b308595dac Implement both tokens and rate limiting to API
Closes #141, but still needs better UI on failure
2017-10-12 16:24:12 +02:00
Stefan Zermatten
1d2de197a4 Replaced trello with github on home page 2017-09-28 14:04:42 +02:00
Stefan Zermatten
a3d790b47d Merge branch 'feature-manage-user' 2017-09-28 13:47:46 +02:00
Stefan Zermatten
efe6dd87db Added link to change password
closes #78
2017-09-28 13:47:30 +02:00
Stefan Zermatten
5b33a6e783 Fixed username dialog title 2017-09-28 13:22:21 +02:00
Stefan Zermatten
8730fab40b Moved username dialog to own folder 2017-09-28 13:21:14 +02:00
Stefan Zermatten
992776bb40 Merge branch 'feature-onboarding' 2017-09-28 13:18:52 +02:00
Stefan Zermatten
bc9ec4421c Polished onboarding, removed stray logs
closes #102
2017-09-28 13:17:33 +02:00
Stefan Zermatten
4c31ab601c Improved new user experience and fixed errors on character delete 2017-09-28 13:03:54 +02:00
Stefan Zermatten
c4e77c7eae Replaced all paper-tooltip with custom, working version 2017-09-28 11:47:03 +02:00
Stefan Zermatten
2cd6e27f70 Finished basic new user experience 2017-09-28 10:26:45 +02:00
Stefan Zermatten
f6b2dde479 Added basic onboarding steps 2017-09-27 16:19:00 +02:00
Stefan Zermatten
44da62a962 Lowered subscription caching to improve performance 2017-09-26 15:11:03 +02:00
Stefan Zermatten
4e96047e90 Added rate limiting to heavy subscriptions 2017-09-26 14:59:41 +02:00
Stefan Zermatten
212986ac37 Fixed missing charId 2017-09-26 13:54:33 +02:00
Stefan Zermatten
877f516565 Added efficient computation of characters to replace heavy export functionality 2017-09-26 13:36:01 +02:00
Thaum Rystra
750022f0f1 Added missing indices 2017-09-24 04:02:15 +02:00
Stefan Zermatten
614284c73d Updated Meteor and some packages 2017-09-22 12:52:21 +02:00
Stefan Zermatten
6528fc8bab Improved vMix export
closes #138
2017-09-22 11:41:32 +02:00
Stefan Zermatten
020930b2e4 Checked if old hitpoint slider exists before resetting it
closes #135
2017-09-22 11:03:00 +02:00
Stefan Zermatten
dcd76e06e1 Merge branch 'fixbug-137-multiadd-spells-delete-themselves' 2017-09-22 10:43:04 +02:00
Stefan Zermatten
8a58002415 Fixed spells in the library using their library ID's in the spells collection
closes #137
2017-09-22 10:42:34 +02:00
Stefan Zermatten
535fcd77cf Added missing attributes to vmix export 2017-09-13 16:01:05 +02:00
Stefan Zermatten
7c2aed26a4 Fixed vMix export, included vMix parties 2017-09-13 14:01:07 +02:00
Stefan Zermatten
fab052050a Merge branch 'feature-vmix' 2017-09-13 10:01:46 +02:00
Stefan Zermatten
b7bdb141c8 Added basic vMix server side api 2017-09-13 10:01:32 +02:00
Stefan Zermatten
00a050d337 Added basic printing functionality 2017-09-11 09:27:01 +02:00
Stefan Zermatten
0b8fabde14 Merge branch 'enhancement-106' 2017-09-08 10:48:44 +02:00
Stefan Zermatten
3336e177d9 Merge branch 'enhancement-spell-library-style'
closes #106
2017-09-08 10:46:41 +02:00
Stefan Zermatten
2e9440e325 Merge branch 'enhancement-spell-library-style' 2017-09-08 09:47:53 +02:00
Stefan Zermatten
e4ac400cbd Replaced spell library spell selection highlighting with checkboxes
Makes it more intuitive that multiple items can be selected
2017-09-08 09:47:43 +02:00
Stefan Zermatten
f600999c5f Guarded against error when hiding feature description portion of card 2017-09-08 09:46:58 +02:00
Stefan Zermatten
0af905699a Uncommented buffs from spell library 2017-09-08 09:46:00 +02:00
Stefan Zermatten
6e900cfaae Merge pull request #123 from Dumbgenius/spells-improvements
Spells improvements
2017-09-08 08:30:56 +02:00
Stefan Zermatten
1f42d3c622 Merge pull request #128 from Dumbgenius/fix-127
[#127] Arrow library items should give 20 arrows rather than one "Arrows (20)"
2017-09-08 08:29:56 +02:00
Stefan Zermatten
ee453d968f Merge pull request #134 from Dumbgenius/fixbug-attackview-damage-type
attackView now correctly shows damage type
2017-09-07 16:31:26 +02:00
Stefan Zermatten
0850e59b30 Merge pull request #122 from Dumbgenius/feature-22-temp-hp, requires migration
Feature - TempHP as a character attribute,
2017-09-07 16:29:55 +02:00
Stefan Zermatten
f3e44cf033 Moved polymer styling to app-theme 2017-09-07 16:26:09 +02:00
Stefan Zermatten
907f9d15d4 Merge pull request #131 from Dumbgenius/feature-custom-buffs
Feature - custom buffs
2017-09-07 14:42:05 +02:00
Stefan Zermatten
4c3d5d40dd Merge branch 'master' into feature-custom-buffs 2017-09-07 14:41:33 +02:00
Stefan Zermatten
ef0deb20aa Swapped dropdown box for full character list 2017-09-07 14:36:45 +02:00
Stefan Zermatten
b43ee08d75 Fixed some editing bugs on new buffs, improved style 2017-09-07 14:33:59 +02:00
Stefan Zermatten
7e7f1ec997 Merge pull request #132 from Dumbgenius/enhancement-blank-descriptions
Removed blank bit of card on features with no short description
2017-09-07 12:03:32 +02:00
Stefan Zermatten
0f652f5c74 Merge pull request #126 from Dumbgenius/feature-unsharing-characters
Added ability to "unshare" a character.
2017-09-07 12:01:14 +02:00
Stefan Zermatten
3e02875eaf Merge pull request #130 from Dumbgenius/enhancement-85
Added "carried" checkbox to container edit screen (resolves #85)
2017-09-07 11:54:11 +02:00
Stefan Zermatten
b9a5230344 Merge pull request #129 from Dumbgenius/enhancement-84
Added attacks to features (resolves #84)
2017-09-07 11:49:03 +02:00
Stefan Zermatten
28780b96c3 Merge pull request #120 from Dumbgenius/feature-drag-drop-spells
Feature - drag and drop spells
2017-09-07 11:43:34 +02:00
Jacob
bec0b33805 attackView now correctly shows damage type 2017-09-06 18:45:43 +01:00
Jacob
ad9ccbe7ef Removed blank bit of card on features with no short description 2017-09-05 14:31:26 +01:00
Jacob
e2933c2df5 Fixed a couple of places where "Buff" should've been "Condition" 2017-09-05 13:56:15 +01:00
Jacob
87583fdac6 Added buff details to apply buff dialog 2017-09-05 13:48:26 +01:00
Jacob
68e1382aed Buffs/conditions now have a delete button on the list on Stats 2017-09-04 20:36:13 +01:00
Jacob
7b62c82e32 Buff and condition view dialogs now only have the delete button 2017-09-04 20:16:56 +01:00
Jacob
6dd92586a4 Conditions are now separate from buffs, like in #109 2017-09-04 20:16:38 +01:00
Jacob
b3d0db1f02 Merge branch 'master' into feature-custom-buffs 2017-09-04 18:22:04 +01:00
Jacob
5f35c71c9d Added "carried" checkbox to container edit screen
Resolves #85
2017-09-04 15:50:57 +01:00
Jacob
85baf4e5d1 Added attacks to features (resolves #84) 2017-09-04 15:35:52 +01:00
Jacob
15d797131e Fixes #127. Also fixes it for other ammo as well as iron spikes 2017-09-04 15:22:45 +01:00
Stefan Zermatten
06ac9f70c8 Merge pull request #119 from Dumbgenius/srd-fixes
SRD fixes,

Merging this pull request doesn't change the deployed version of DiceCloud, but indicates that the database has been updated using the affected JSON files
2017-09-04 10:50:42 +02:00
Jacob
471a3e274e Refactored code which applies buffs 2017-08-10 02:49:29 +01:00
Jacob
d4031dc4a7 Removed an unnecessary console log 2017-08-09 16:11:09 +01:00
Jacob
18e5ab3f21 Can now set target of buffs
...to "Self only", "Others only", or "Both".
2017-08-09 16:08:19 +01:00
Jacob
c9fe2f17a0 If buff's target is "self", dialog is no longer shown 2017-08-09 15:48:12 +01:00
Jacob
818cb3905f Can now access the name of public characters for buffs
the buffDialog can now access the name of a publicly-available character
so it can display their name as the caster
2017-08-09 15:30:33 +01:00
Jacob
64ef426035 Applying character no longer duplicated in buff apply list 2017-08-09 15:30:25 +01:00
Jacob
2b188f1a8d Merge branch 'master' into feature-custom-buffs
# Conflicts:
#	rpg-docs/client/views/character/inventory/itemDialog/itemDialog.html
#	rpg-docs/client/views/character/spells/spellDialog/spellDialog.html
2017-08-09 15:03:54 +01:00
Jacob
08735ea4f7 Added ability to add/edit buffs 2017-08-09 15:01:20 +01:00
Jacob
bce1b85600 Relocated the buff list to be inline in stats.html 2017-08-09 11:39:18 +01:00
Jacob
0d023e2ba3 Adding conditions now works again 2017-08-09 11:36:45 +01:00
Jacob
dad575de64 Buffs are now split into custom buffs and conditions 2017-08-09 11:20:49 +01:00
Jacob
cb648b4a28 Buffs now display who and what they were applied by. 2017-08-09 11:13:24 +01:00
Jacob
1279137362 Buffs now display attacks and proficiencies in buffDialog 2017-08-09 10:42:34 +01:00
Jacob
3c06529906 buffViewList is now split into "Conditions" and "Buffs" 2017-08-09 10:16:50 +01:00
Jacob
a3b0c6cafd Added a schema/collection pair for CustomBuffs 2017-08-09 10:06:11 +01:00
Jacob
a1d9f7f5bb Added ability to remove self from readers.
Closes #125.
2017-08-09 02:18:46 +01:00
Jacob
3b03e9c71c Spell library now allows you to select multiple spells 2017-07-30 23:27:29 +01:00
Jacob
0eea6f2386 Cantrips now display as "<school> cantrip" rather than "Level 0 <school>"
Also fixed spacing if "(ritual)" tag was not present
2017-07-30 21:20:27 +01:00
Jacob
8f8714d3e5 Altered spacing of headers within baseDialog 2017-07-30 21:14:02 +01:00
Jacob
7c38e8d70a Added newline before At Higher Levels on Healing Word. 2017-07-30 20:22:59 +01:00
Jacob
826859ca3f Added migration to add TempHP to all characters 2017-07-29 22:41:37 +01:00
Jacob
2ca13fbb56 Renamed tempHitPointSlider to extraHitPointSlider in the css 2017-07-29 21:45:02 +01:00
Jacob
6d801e0178 Renamed old "Temporary HP" to "Extra HP" (THP is now the actual attribute) 2017-07-29 21:42:48 +01:00
Jacob
9ddac7d5cd Temporary HP (the attribute) now appears on Stats page 2017-07-29 21:25:05 +01:00
Jacob
7a6f751e30 THP added as a character attribute 2017-07-29 20:09:48 +01:00
Jacob
2389768234 statOrder is now set programatically from an array
This makes it far easier to add new stats, as we don't have to update
every single number manually.
2017-07-29 19:20:26 +01:00
Jacob
c76fe99148 Can now move and copy spells between characters 2017-07-26 21:02:36 +01:00
Jacob
53afaa4f37 CTRL-dragging now copies spells 2017-07-26 19:54:18 +01:00
Jacob
3599b5fbc4 Added basic drag-and-drop functionality between spell lists
Closes #105
2017-07-26 19:23:41 +01:00
Jacob
275fb1ca65 Scrying's tables are now actual Markdown tables 2017-07-21 15:46:33 +01:00
Jacob
d5d937b04a Reincarnate's race table is now an actual Markdown table 2017-07-21 15:38:17 +01:00
Jacob
aa554adbce Teleport's familiarity table is now a table 2017-07-21 15:30:39 +01:00
Jacob
cb739eb207 Removed indents after double-newlines in spells SRD.
They don't actually render in Markdown, and if there was an indent of >=4
spaces it caused the entire next line to be rendered as code (for example
in the Animate Objects spell).
2017-07-21 14:58:29 +01:00
Jacob
66df2ea4aa Animate Objects' stats are now in a table 2017-07-21 14:43:37 +01:00
Jacob
bb95fe0d9a Merge branch 'master' into feature-conditions 2017-07-18 11:10:04 +01:00
Jacob
17a390a8aa Changed Model of Buffs for forwards compatibility
Added a parent attribute and made Buffs a child, so that buffs can be
added to items, spells and features.
2017-07-17 02:41:51 +01:00
Jacob
b3ef43eb70 Added ability to add/remove basic conditions
Conditions are those listed in ./lib/methods/conditions.js
2017-07-16 04:27:32 +01:00
Jacob
be92ef224c Preliminary additions 2017-07-15 19:54:23 +01:00
657 changed files with 22388 additions and 16715 deletions

View File

@@ -1,13 +0,0 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="HtmlUnknownAttribute" enabled="false" level="WARNING" enabled_by_default="false">
<option name="myValues">
<value>
<list size="0" />
</value>
</option>
<option name="myCustomValuesEnabled" value="true" />
</inspection_tool>
</profile>
</component>

View File

@@ -1,7 +0,0 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="PROJECT_PROFILE" value="Project Default" />
<option name="USE_PROJECT_PROFILE" value="true" />
<version value="1.0" />
</settings>
</component>

56
.jscsrc
View File

@@ -1,56 +0,0 @@
{
"requireOperatorBeforeLineBreak": true,
"requireCamelCaseOrUpperCaseIdentifiers": true,
"maximumLineLength": {
"value": 80,
"allowComments": true,
"allowRegex": true
},
"validateIndentation": "\t",
"validateQuoteMarks": "\"",
"disallowMultipleLineStrings": true,
"disallowMixedSpacesAndTabs": "smart",
"disallowTrailingWhitespace": true,
"disallowSpaceAfterPrefixUnaryOperators": true,
"disallowMultipleVarDecl": false,
"disallowNewlineBeforeBlockStatements": true,
"disallowKeywordsOnNewLine": ["else"],
"requireSpaceAfterKeywords": [
"if",
"else",
"for",
"while",
"do",
"switch",
"return",
"try",
"catch"
],
"requireSpaceBeforeBinaryOperators": [
"=", "+=", "-=", "*=", "/=", "%=", "<<=", ">>=", ">>>=",
"&=", "|=", "^=", "+=",
"+", "-", "*", "/", "%", "<<", ">>", ">>>", "&",
"|", "^", "&&", "||", "===", "==", ">=",
"<=", "<", ">", "!=", "!=="
],
"requireSpaceAfterBinaryOperators": true,
"requireSpacesInConditionalExpression": true,
"requireSpacesInForStatement": true,
"requireTrailingComma": {
"ignoreSingleValue": true,
"ignoreSingleLine": true
},
"requireLineFeedAtFileEnd": true,
"disallowSpacesInAnonymousFunctionExpression": {
"beforeOpeningRoundBrace": true
},
"disallowSpacesInsideObjectBrackets": "all",
"disallowSpacesInsideArrayBrackets": "all",
"disallowSpacesInsideParentheses": true,
"disallowMultipleLineBreaks": true,
"disallowNewlineBeforeBlockStatements": true
}

View File

@@ -1,4 +0,0 @@
{
"undef": false,
"esversion": 6
}

14
Dockerfile Normal file
View File

@@ -0,0 +1,14 @@
FROM ubuntu:latest
RUN apt-get update --quiet \
&& apt-get install --quiet --yes \
bsdtar \
curl \
git
RUN ln --symbolic --force $(which bsdtar) $(which tar)
RUN useradd --create-home --shell /bin/bash dicecloud
USER dicecloud
WORKDIR /home/dicecloud
RUN curl https://install.meteor.com/?release=1.8.0.2 | sh
ENV PATH="${PATH}:/home/dicecloud/.meteor"
COPY dev.sh ./dev.sh
ENTRYPOINT ./dev.sh

View File

@@ -1,13 +1,70 @@
RPG Docs
DiceCloud
========
This is the repo for [DiceCloud](dicecloud.com).
DiceCloud is a free, auditable, real-time character sheet for D&D 5e.
Philosophy
----------
Setting up your character on DiceCloud takes a little longer than
just filling it in on a paper character sheet would. The goal of using an
online sheet is to make actually playing the game more streamlined, and
ultimately more fun. So putting a little extra effort into setting up a
character now pays off over and over again once you're playing.
The idea is to track where each number comes from, and allow you to easily make
changes on the fly. Let's look at a hypothetical example.
> You need to swim through a sunken section of dungeon to fetch the quest's Thing.
> You'll need to take off your magical Plate Armor of +1 Constitution to swim
> without sinking, of course.
>
> Taking it off will take away that disadvantage on
> stealth checks, change your armor class, your speed and your constitution, and
> which in turn changes your hit points and your constitution saving throw.
> Working out all those changes in the middle of a game will drag the game to a
> halt.
>
> Fortunately you have DiceCloud, so it's a matter of dragging
> your Plate Armor +1 Con from your "equipment" box to your "backpack" box and
> you're done. Your hitpoints change correctly, your saving throws are up to date,
> your armor class goes back to reflecting the fact that you have natural armor
> from being a dragonborn. Your character sheet keeps up and you
> ultimately get more time to play the game. Huzzah!
Getting started
---------------
`git clone https://github.com/ThaumRystra/DiceCloud1 dicecloud`
Running DiceCloud locally, either to host it yourself away from an internet
connection, or to contribute to developing it further, is fairly
straightforward and it should work on Linux, Windows, and Mac.
You'll need to have installed:
- [git](https://www.atlassian.com/git/tutorials/install-git)
- [Meteor](https://www.meteor.com/install)
Then, it's just a matter of cloning this repository into a folder, and running
`meteor` in the app directory.
`git clone https://github.com/ThaumRystra/DiceCloud dicecloud`
`cd dicecloud`
`cd rpg-docs`
`bower install`
`cd app`
`meteor npm install`
`meteor`
You should see this:
```
=> Started proxy.
=> [HMR] Dev server listening on port 3003.
=> Started MongoDB.
=> Started your app.
=> App running at: http://localhost:3000/
```
Now, visiting [](http://localhost:3000/) should show you an empty instance of
DiceCloud running.

View File

@@ -1,9 +1,11 @@
.meteor/local
.meteor/meteorite
.demeteorized
.cache
settings.json
public/components
public/_imports.html
private/oldClient
nohup.out
node_modules
dump

View File

@@ -13,3 +13,7 @@ notices-for-facebook-graph-api-2
1.3.0-split-minifiers-package
1.4.0-remove-old-dev-bundle-link
1.4.1-add-shell-server-package
1.4.3-split-account-service-packages
1.5-add-dynamic-import-package
1.7-split-underscore-from-meteor-base
1.8.3-split-jquery-from-blaze

51
app/.meteor/packages Normal file
View File

@@ -0,0 +1,51 @@
# Meteor packages used by this project, one per line.
#
# '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
dburles:collection-helpers
reactive-var@1.0.11
underscore@1.0.10
momentjs:moment
dburles:mongo-collection-instances
percolate:migrations
accounts-google@1.3.3
email@1.2.3
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
templates:array
ecmascript@0.14.3
es5-shim@4.8.0
reactive-dict@1.3.0
percolate:synced-cron
ongoworks:speakingurl
service-configuration@1.0.11
google-config-ui@1.0.1
dynamic-import@0.5.2
ddp-rate-limiter@1.0.7
rate-limit@1.0.9
meteortesting:mocha
mdg:validated-method
akryum:vue-router2
static-html
aldeed:collection2@3.0.0
aldeed:schema-index
akryum:vue-component
accounts-patreon
bozhao:link-accounts

1
app/.meteor/release Normal file
View File

@@ -0,0 +1 @@
METEOR@1.10.2

126
app/.meteor/versions Normal file
View File

@@ -0,0 +1,126 @@
accounts-base@1.6.0
accounts-google@1.3.3
accounts-oauth@1.2.0
accounts-password@1.6.0
accounts-patreon@0.1.0
accounts-ui@1.3.1
accounts-ui-unstyled@1.4.2
akryum:npm-check@0.1.2
akryum:vue-component@0.15.2
akryum:vue-component-dev-client@0.4.7
akryum:vue-component-dev-server@0.1.4
akryum:vue-router2@0.2.3
aldeed:collection2@3.0.6
aldeed:schema-index@3.0.0
allow-deny@1.1.0
autoupdate@1.6.0
babel-compiler@7.5.3
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
caching-compiler@1.2.2
caching-html-compiler@1.1.3
callback-hook@1.3.0
check@1.3.1
chuangbo:marked@0.3.5_1
dburles:collection-helpers@1.1.0
dburles:mongo-collection-instances@0.3.5
ddp@1.4.0
ddp-client@2.3.3
ddp-common@1.4.0
ddp-rate-limiter@1.0.7
ddp-server@2.3.1
deps@1.0.12
diff-sequence@1.1.1
dynamic-import@0.5.2
ecmascript@0.14.3
ecmascript-runtime@0.7.0
ecmascript-runtime-client@0.10.0
ecmascript-runtime-server@0.9.0
ejson@1.1.1
email@1.2.3
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
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
livedata@1.0.18
lmieulet:meteor-coverage@1.1.4
localstorage@1.2.0
logging@1.1.20
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
mobile-experience@1.1.0
mobile-status-bar@1.1.0
modern-browsers@0.1.5
modules@0.15.0
modules-runtime@0.12.0
momentjs:moment@2.24.0
mongo@1.10.0
mongo-decimal@0.1.1
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
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
percolate:synced-cron@1.3.2
promise@0.11.2
raix:eventemitter@0.1.3
random@1.2.0
rate-limit@1.0.9
reactive-dict@1.3.0
reactive-var@1.0.11
reload@1.3.0
retry@1.1.0
routepolicy@1.1.0
seba:minifiers-autoprefixer@1.1.2
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
srp@1.1.0
standard-minifier-js@2.6.0
static-html@1.2.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
tracker@1.2.0
underscore@1.0.10
url@1.3.0
webapp@1.9.1
webapp-hashing@1.0.9

View File

@@ -1,9 +1,8 @@
<head>
<meta name="viewport" content="width=device-width initial-scale=1.0, user-scalable=no">
<link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons" rel="stylesheet">
<meta name="viewport" content="width=device-width initial-scale=1.0, user-scalable=no">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<link href='https://fonts.googleapis.com/css?family=Roboto:400,300,300italic,400italic,500,500italic,700,700italic,900,900italic,100italic,100&subset=latin,latin-ext' rel='stylesheet' type='text/css'>
<link rel="apple-touch-icon" sizes="57x57" href="/apple-touch-icon-57x57.png?v=lk6WXp6Pmj">
<link rel="apple-touch-icon" sizes="60x60" href="/apple-touch-icon-60x60.png?v=lk6WXp6Pmj">
<link rel="apple-touch-icon" sizes="72x72" href="/apple-touch-icon-72x72.png?v=lk6WXp6Pmj">
@@ -23,4 +22,17 @@
<meta name="msapplication-TileColor" content="#b91d1d">
<meta name="msapplication-TileImage" content="/mstile-144x144.png?v=lk6WXp6Pmj">
<meta name="theme-color" content="#d12929">
<style type="text/css" media="print">
@page {
margin: 0mm;
}
html {
margin: 0px;
}
* {
-webkit-transition: none !important;
transition: none !important;
}
</style>
</head>

3
app/client/index.html Normal file
View File

@@ -0,0 +1,3 @@
<body>
<div id="app"></div>
</body>

2
app/client/main.js Normal file
View File

@@ -0,0 +1,2 @@
import "/imports/ui/vueSetup.js";
import "/imports/ui/styles/stylesIndex.js";

View File

@@ -0,0 +1,11 @@
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,6 +1,8 @@
Parties = new Mongo.Collection("parties");
import SimpleSchema from 'simpl-schema';
Schemas.Party = new SimpleSchema({
let Parties = new Mongo.Collection("parties");
let partySchema = new SimpleSchema({
name: {
type: String,
defaultValue: "New Party",
@@ -8,10 +10,13 @@ Schemas.Party = new SimpleSchema({
optional: true,
},
characters: {
type: [String],
type: Array,
defaultValue: [],
},
characters: {
type: String,
regEx: SimpleSchema.RegEx.Id,
index: 1,
defaultValue: [],
},
owner: {
type: String,
@@ -19,7 +24,7 @@ Schemas.Party = new SimpleSchema({
},
});
Parties.attachSchema(Schemas.Party);
Parties.attachSchema(partySchema);
Parties.allow({
insert: function(userId, doc) {
@@ -40,3 +45,5 @@ Parties.deny({
return _.contains(fields, "owner");
}
});
export default Parties;

View File

@@ -0,0 +1,296 @@
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});
}
});
export default CreatureProperties;
export {
CreaturePropertySchema,
insertProperty,
insertPropertyFromLibraryNode,
updateProperty,
damageProperty,
pushToProperty,
pullFromProperty,
softRemoveProperty,
};

View File

@@ -0,0 +1,140 @@
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

@@ -0,0 +1,146 @@
// 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,187 @@
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,71 @@
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;
}
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.fail = 0;
this.conditional = [];
this.rollBonus = [];
}
addEffect(effect){
let result = effect.result;
switch(effect.operation){
case 'base':
// Take the largest base value
this.base = result > this.base ? result : this.base;
break;
case 'add':
// Add all adds together
this.add += result;
break;
case 'mul':
// Multiply the muls together
this.mul *= result;
break;
case 'min':
// Take the largest min value
this.min = result > this.min ? result : this.min;
break;
case 'max':
// Take the smallest max value
this.max = result < this.max ? result : this.max;
break;
case 'advantage':
// Sum number of advantages
this.advantage++;
break;
case 'disadvantage':
// Sum number of disadvantages
this.disadvantage++;
break;
case 'passiveAdd':
// Add all passive adds together
this.passiveAdd += result;
break;
case 'fail':
// Sum number of fails
this.fail++;
break;
case 'conditional':
// Store array of conditionals
this.conditional.push(result);
break;
case 'rollBonus':
// Store array of roll bonuses
this.rollBonus.push(result);
break;
}
}
}

View File

@@ -0,0 +1,98 @@
import * as math from 'mathjs';
import replaceBareSymbolsWithValueAccessor from '/imports/api/creature/computation/utility/replaceBareSymbolsWithValueAccessor.js';
export default function evaluateString(string, scope){
let errors = [];
if (!string){
errors.push('No string provided');
return {result: string, errors};
}
if (!scope) errors.push('No scope provided');
// Parse the string using mathjs
let calc;
try {
calc = math.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};
}
}
}
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);
}
} else {
return node;
}
}
}
// 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

@@ -0,0 +1,11 @@
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

@@ -0,0 +1,73 @@
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

@@ -0,0 +1,15 @@
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

@@ -0,0 +1,12 @@
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

@@ -0,0 +1,30 @@
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,32 @@
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

@@ -0,0 +1,42 @@
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

@@ -0,0 +1,73 @@
import { Meteor } from 'meteor/meteor'
import { ValidatedMethod } from 'meteor/mdg:validated-method';
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';
export const recomputeCreature = new ValidatedMethod({
name: 'Creatures.methods.recomputeCreature',
validate: new SimpleSchema({
charId: { type: String }
}).validator(),
run({charId}) {
// Permission
assertEditPermission(charId, 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);
},
});
/**
* 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
* a creature.
*
* Essentially this is a depth first tree traversal algorithm that computes
* stats' dependencies before computing stats themselves, while detecting
* dependency loops.
*
* At the moment it makes no effort to limit recomputation to just what was
* changed.
*
* Attempting to implement dependency management to limit recomputation to just
* change affected stats should only happen as a last resort, when this function
* can no longer be performed more efficiently, and server resources can not be
* expanded to meet demand.
*
* A brief overview:
* - Fetch the stats of the creature and add them to
* an object for quick lookup
* - Fetch the effects and proficiencies which apply to each stat and store them with the stat
* - Fetch the class levels and store them as well
* - Mark each stat and effect as uncomputed
* - Iterate over each stat in order and compute it
* - If the stat is already computed, skip it
* - If the stat is busy being computed, we are in a dependency loop, make it NaN and mark computed
* - Mark the stat as busy computing
* - Iterate over each effect which applies to the attribute
* - If the effect is not computed compute it
* - If the effect relies on another attribute, get its computed value
* - Recurse if that attribute is uncomputed
* - apply the effect to the attribute
* - Conglomerate all the effects to compute the final stat values
* - 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);
computeMemo(computationMemo);
writeAlteredProperties(computationMemo);
writeCreatureVariables(computationMemo, creatureId);
// if(Meteor.isClient) console.log(computationMemo);
return computationMemo;
}

View File

@@ -0,0 +1,12 @@
import * as math from 'mathjs';
export default function replaceBareSymbolsWithValueAccessor(node, path) {
if (node.isSymbolNode && path !== 'object') {
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

@@ -0,0 +1,101 @@
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

@@ -0,0 +1,35 @@
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,97 @@
import {computeCreature} from "./recomputeCreature.js";
import assert from "assert";
const makeEffect = function(operation, value){
let effect = {computed: false, result: 0, operation}
if (_.isFinite(value)){
effect.value = +value;
} else {
effect.calculation = value;
}
return effect;
}
describe('computeCreature', function () {
it('computes an aritrary creature', function () {
let char = {
atts: {
attribute1: {
computed: false,
busyComputing: false,
type: "attribute",
attributeType: "ability",
result: 0,
mod: 0, // The resulting modifier if this is an ability
base: 0,
add: 0,
mul: 1,
min: Number.NEGATIVE_INFINITY,
max: Number.POSITIVE_INFINITY,
effects: [
makeEffect("base", 10),
makeEffect("add", 5),
makeEffect("mul", 2),
],
},
attribute2: {
computed: false,
busyComputing: false,
type: "attribute",
result: 0,
mod: 0, // The resulting modifier if this is an ability
base: 0,
add: 0,
mul: 1,
min: Number.NEGATIVE_INFINITY,
max: Number.POSITIVE_INFINITY,
effects: [
makeEffect("base", "attribute1"),
makeEffect("max", 2),
],
},
},
skills: {
skill1: {
computed: false,
busyComputing: false,
type: "skill",
ability: "attribute1",
result: 0,
proficiency: 0,
add: 0,
mul: 1,
min: Number.NEGATIVE_INFINITY,
max: Number.POSITIVE_INFINITY,
advantage: 0,
disadvantage: 0,
passiveAdd: 0,
fail: 0,
conditional: 0,
effects: [],
proficiencies: [],
},
},
dms: {
dm1: {
computed: false,
busyComputing: false,
type: "damageMultiplier",
result: 0,
immunityCount: 0,
ressistanceCount: 0,
vulnerabilityCount: 0,
effects: [],
}
},
classes: {
Barbarian: {
level: 5,
},
},
level: 5,
};
char = computeCreature(char);
console.log(char);
assert(true);
});
});

View File

@@ -0,0 +1,28 @@
import {
assertEditPermission as editPermission,
assertViewPermission as viewPermission,
assertOwnership as ownership
} from '/imports/api/sharing/sharingPermissions.js';
function getCreature(creature, fields){
if (typeof creature === 'string'){
return Creatures.findOne(creature, {fields});
} else {
return creature;
}
}
export function assertOwnership(creature, userId){
creature = getCreature(creature, {owner: 1});
ownership(creature, userId);
}
export function assertEditPermission(creature, userId) {
creature = getCreature(creature, {owner: 1, writers: 1});
editPermission(creature, userId);
}
export function assertViewPermission(creature, userId) {
creature = getCreature(creature, {owner: 1, writers: 1, public: 1});
viewPermission(creature, userId);
}

View File

@@ -0,0 +1,181 @@
import DEFAULT_CHARACTER_DOCS from '/imports/api/creature/DEFAULT_CHARACTER_DOCS.js';
import { Random } from 'meteor/random';
const setParent = function(charId){
let parent = {
collection: "Creatures",
id: charId,
group: "default",
};
return doc => {
doc.parent = doc.parent || parent;
doc.charId = charId;
};
};
const getRacialBonusEffect = function(charId, attribute, bonus){
return {
name: "Race Bonus",
stat: attribute,
operation: "add",
calculation: bonus.toString(),
enabled: true,
parent: {
collection: "Creatures",
id: charId,
group: "race",
},
charId: charId,
};
};
const giveDocsOrderAndIds = function(docArray){
for (i in docArray){
docArray[i].order = +i;
if (!docArray[i]._id){
docArray[i]._id = Random.id();
}
}
};
const getDefaultCharacterDocs = function(charId, {
// Character form data
baseStrength = 10,
baseDexterity = 10,
baseConstitution = 10,
baseIntelligence = 10,
baseWisdom = 10,
baseCharisma = 10,
strengthBonus = 0,
dexterityBonus = 0,
constitutionBonus = 0,
intelligenceBonus = 0,
wisdomBonus = 0,
charismaBonus = 0,
hitDice = "d8",
cls = "Class",
level = 1,
}){
let docs = DEFAULT_CHARACTER_DOCS();
// Setup the base ability scores
docs.attributes[0].baseValue = baseStrength;
docs.attributes[1].baseValue = baseDexterity;
docs.attributes[2].baseValue = baseConstitution;
docs.attributes[3].baseValue = baseIntelligence;
docs.attributes[4].baseValue = baseWisdom;
docs.attributes[5].baseValue = baseCharisma;
// Set up racial bonuses
if (strengthBonus) {
docs.effects.push(
getRacialBonusEffect(charId, 'strength', strengthBonus)
);
}
if (dexterityBonus) {
docs.effects.push(
getRacialBonusEffect(charId, 'dexterity', dexterityBonus)
);
}
if (constitutionBonus) {
docs.effects.push(
getRacialBonusEffect(charId, 'constitution', constitutionBonus)
);
}
if (intelligenceBonus) {
docs.effects.push(
getRacialBonusEffect(charId, 'intelligence', intelligenceBonus)
);
}
if (wisdomBonus) {
docs.effects.push(
getRacialBonusEffect(charId, 'wisdom', wisdomBonus)
);
}
if (charismaBonus) {
docs.effects.push(
getRacialBonusEffect(charId, 'charisma', charismaBonus)
);
}
// Set up Class
const strippedCls = cls.replace(/\s+/g, '')
const classId = Random.id();
docs.classes = [{
_id: classId,
charId,
level,
name: cls,
}];
// Setup hit dice
docs.effects.push({
name: cls,
stat: `${hitDice}HitDice`,
operation: "add",
calculation: `${strippedCls}Level`,
enabled: true,
parent: {
collection: "Classes",
id: classId,
},
charId: charId,
});
// Setup health for all class levels
let healthPerLevel = 4;
if (hitDice == "d6"){
healthPerLevel = 4;
} else if (hitDice == "d8"){
healthPerLevel = 5;
} else if (hitDice == "d10"){
healthPerLevel = 6;
} else if (hitDice == "d12"){
healthPerLevel = 7;
}
docs.effects.push({
name: cls,
stat: `hitPoints`,
operation: "add",
calculation: `${healthPerLevel - 2} + ${healthPerLevel} * ${strippedCls}Level`,
enabled: true,
parent: {
collection: "Classes",
id: classId,
},
charId: charId,
});
// Set the parents for base items
docs.attributes.forEach(setParent(charId));
docs.skills.forEach(setParent(charId));
docs.damageMultipliers.forEach(setParent(charId));
docs.effects.forEach(setParent(charId));
docs.containers.forEach(setParent(charId));
// Set up parenting on items and move them to the top level items object
docs.items = [];
docs.containers.forEach(container => {
container._id = Random.id();
const parent = {
collection: "Containers",
id: container._id,
};
container.items.forEach(item => {
item.parent = parent;
item.charId = charId;
});
// Move the items to the top level array
docs.items.push(...container.items);
delete container.items;
});
// Order the docs
for (collection in docs){
giveDocsOrderAndIds(docs[collection]);
}
return docs
};
export default getDefaultCharacterDocs;

View File

@@ -0,0 +1,52 @@
import {
assertEditPermission,
assertViewPermission,
assertOwnership,
} from '/imports/api/creature/creaturePermissions.js';
// Checks if the method has permission to run on the document. If the document
// has a charId, that creature is checked, otherwise if it has an _id and the
// collection is defined in the method options, that document is fetched to
// determine its charId, otherwise a getCharId method can be defined to perform
// a special search for the required creature
//
// Because this mixin injects the charId into argument objects that don't
// already contain it, it should always come last in the mixin list, so that it
// the outermost wrapper of the run function
export default function creaturePermissionMixin(methodOptions){
let assertPermission;
if (methodOptions.permission === 'owner'){
assertPermission = assertOwnership;
} else if (methodOptions.permission === 'edit'){
assertPermission = assertEditPermission;
} else if (methodOptions.permission === 'view'){
assertPermission = assertViewPermission;
} else {
throw "`permission` missing in method options";
}
let getCharId;
if (methodOptions.getCharId){
getCharId = methodOptions.getCharId;
} else if (methodOptions.collection) {
getCharId = function({_id}){
return methodOptions.collection.findOne(_id, {
fields: {charId: 1}
}).charId;
};
} else {
getCharId = function(){
throw "`getCharId` or `collection` missing in method options," +
" or {charId} missing in call";
};
}
let runFunc = methodOptions.run;
methodOptions.run = function(doc, ...rest){
// Store the charId on the doc for other mixins if it had to be fetched
doc.charId = doc.charId || getCharId.apply(this, arguments);
assertPermission(doc.charId, this.userId);
return runFunc.call(this, doc, ...rest);
};
return methodOptions;
}

View File

@@ -0,0 +1,59 @@
import {
updateChildren,
updateDescendants,
} from '/imports/api/parenting/parenting.js';
import { inheritedFields } from '/imports/api/parenting/ChildSchema.js';
import MONGO_OPERATORS from '/imports/constants/MONGO_OPERATORS.js';
// This mixin can be safely applied to all update methods which are validated
// with the updateSchemaMixin. It will propagate updates to fields which
// are inherited and normalised on the parent or ancestor docs
// It should have neglible performance impact for updates that aren't inherited
function propagateInheritanceUpdate({_id, update}){
let childModifier = {};
let descendantModifier = {};
// For each operator
for (let operator of MONGO_OPERATORS){
// If the operator is in the update, for each field
if (update[operator]) for (let field in update[operator]){
// Get the top level field that was actually modified
const indexOfDot = field.indexOf('.');
let modifiedField;
if (indexOfDot !== -1) {
modifiedField = field.substring(0, indexOfDot);
} else {
modifiedField = field;
}
// If that field is updated and inherited
if (inheritedFields.has(modifiedField)){
// Perform the same update on the descendants
if (!childModifier[operator]) childModifier[operator] = {};
if (!descendantModifier[operator]) descendantModifier[operator] = {};
childModifier[operator][`parent.${field}`] = update[operator][field];
descendantModifier[operator][`ancestors.$.${field}`] = update[operator][field];
}
}
}
// Update the parent object of its children
updateChildren({
parentId: _id,
modifier: childModifier,
});
// Update the ancestors object of its descendants
updateDescendants({
ancestorId: _id,
modifier: descendantModifier,
});
}
export default function propagateInheritanceUpdateMixin(methodOptions){
let runFunc = methodOptions.run;
methodOptions.run = function({_id, update}){
const result = runFunc.apply(this, arguments);
propagateInheritanceUpdate({_id, update});
return result;
};
return methodOptions;
}

View File

@@ -0,0 +1,17 @@
import { recomputeCreatureById } from '/imports/api/creature/computation/recomputeCreature.js';
export default function recomputeCreatureMixin(methodOptions){
let runFunc = methodOptions.run;
methodOptions.run = function({charId}){
const result = runFunc.apply(this, arguments);
if (
methodOptions.skipRecompute &&
methodOptions.skipRecompute.apply(this, arguments)
) {
return result;
}
recomputeCreatureById(charId);
return result;
};
return methodOptions;
}

View File

@@ -0,0 +1,27 @@
import SimpleSchema from 'simpl-schema';
import { setDocToLastOrder } from '/imports/api/parenting/order.js';
export function setDocToLastMixin(methodOptions){
// Make sure the doc has a charId
// This mixin should come before simpleSchemaMixin so that it can extend the
// schema before it is turned into a validate function
if (methodOptions.validate){
throw new Meteor.Error(`setDocToLastMixin should come before simpleSchemaMixin`);
}
methodOptions.schema = new SimpleSchema({
charId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
}).extend(methodOptions.schema);
let collection = methodOptions.collection;
if (!collection){
throw new Meteor.Error("`collection` required in method options for setDocToLastMixin");
}
let runFunc = methodOptions.run;
methodOptions.run = function(doc){
setDocToLastOrder({collection, doc});
return runFunc.apply(this, arguments);
};
return methodOptions;
}

View File

@@ -0,0 +1,51 @@
// Copied from https://github.com/sethjgore/meteor-simple-schema-mixin
// and updated to simpl-schema npm package
import SimpleSchema from 'simpl-schema';
export default function simpleSchemaMixin(methodOptions) {
// If the user didn't give us a schema and they did give us a validate, assume
// that they are choosing to use the validate way of doing things in this call.
// If they've built a wrapper around ValidateMethod that includes this mixin
// all the time, this could happen semi-"intentionally". There may be times they
// just don't want to use a schema and have specified a "validate" option. So
// returning the unchanged options instead of an error seems proper.
if ((typeof methodOptions.schema === 'undefined'
&& typeof methodOptions.validate !== 'undefined')
|| (typeof methodOptions.schema !== 'undefined' && methodOptions.schema === null
&& typeof methodOptions.validate !== 'undefined' && methodOptions.validate !== null)) {
return methodOptions;
}
// If they truly gave us both... that just doesn't seem proper.
if (methodOptions.validate && methodOptions.validate !== null) {
throw new Meteor.Error(
'simpleSchemaMixin.options',
'"schema" and "validate" options cannot be used together');
}
// Note that setting them both null will make it through, defaulting to the
// schema = null behavior (enforce no args) instead of the validate = null
// behavior (do no validation).
const newOptions = methodOptions;
newOptions.schemaValidatorOptions =
newOptions.schemaValidatorOptions ||
{ clean: true, filter: false };
let simpleSchema;
if (!newOptions.schema || newOptions.schema === null) {
// Allow simply leaving off both the schema and validate specifications
// or setting them to "null" as a shorthand. In this case, unlike
// the straight default validate or typical coder's call to validator,
// we will ENFORCE the Method be called without parameters because of
// the "filter: false" above.
simpleSchema = new SimpleSchema({});
} else if (newOptions.schema instanceof SimpleSchema) {
// In this one case, we can save ourselves the time to make a schema out
// of the schema.
simpleSchema = newOptions.schema;
} else {
simpleSchema = new SimpleSchema(newOptions.schema);
}
newOptions.validate = simpleSchema.validator(newOptions.schemaValidatorOptions);
return newOptions;
};

View File

@@ -0,0 +1,67 @@
const argumentSchema = new SimpleSchema({
_id: SimpleSchema.RegEx.Id,
update: {
type: Object,
blackbox: true,
},
});
// Modified simpleSchemaMixin
import SimpleSchema from 'simpl-schema';
export default function updateSchemaMixin(methodOptions) {
// If the user didn't give us a schema and they did give us a validate, assume
// that they are choosing to use the validate way of doing things in this call.
if ((
typeof methodOptions.schema === 'undefined' &&
typeof methodOptions.validate !== 'undefined'
) || (
typeof methodOptions.schema !== 'undefined' &&
methodOptions.schema === null &&
typeof methodOptions.validate !== 'undefined' &&
methodOptions.validate !== null
)) {
return methodOptions;
}
// If they truly gave us both... that just doesn't seem proper.
if (methodOptions.validate && methodOptions.validate !== null) {
throw new Meteor.Error(
'simpleSchemaMixin.options',
'"schema" and "validate" options cannot be used together');
}
// Note that setting them both null will make it through, defaulting to the
// schema = null behavior (enforce no args) instead of the validate = null
// behavior (do no validation).
// Apply default validator options if none are provided
methodOptions.schemaValidatorOptions =
methodOptions.schemaValidatorOptions ||
{ clean: true, modifier: true };
// Make the update schema a SimpleSchema, if it isn't already
let updateSchema;
if (methodOptions.schema instanceof SimpleSchema) {
updateSchema = methodOptions.schema;
} else {
updateSchema = new SimpleSchema(methodOptions.schema);
}
// Set up the new validation
methodOptions.validate = function(args){
argumentSchema.validate(args);
updateSchema.validate(
{$set: args.update},
methodOptions.schemaValidatorOptions
);
};
// Give a default run function if one isn't supplied
if (!methodOptions.run){
methodOptions.run = function({_id, update}){
return methodOptions.collection.update(_id, {$set: update});
};
}
return methodOptions;
}

View File

@@ -0,0 +1,26 @@
import SimpleSchema from 'simpl-schema';
import Creatures from '/imports/api/creature/Creatures.js';
import CreatureProperties from '/imports/api/creature/CreatureProperties.js'
import { assertOwnership } from '/imports/api/creature/creaturePermissions.js';
function removeRelatedDocuments(charId){
CreatureProperties.remove({'ancestors.id': charId});
};
const removeCreature = new ValidatedMethod({
name: "Creatures.methods.removeCreature", // DDP method name
validate: new SimpleSchema({
charId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
}).validator(),
run({charId}) {
assertOwnership(charId, this.userId)
Creatures.remove(charId);
this.unblock();
removeRelatedDocuments(charId);
},
});
export default removeCreature;

View File

@@ -0,0 +1,28 @@
import MONGO_OPERATORS from '/imports/constants/MONGO_OPERATORS.js';
const hasAny = function(values){
for (let value of values){
if (this.has(value)){
return true;
}
}
return false;
};
// Returns a Set of fields the modifier changes
// The set has been extended with the "hasAny" function
export default function getModifierFields (modifier) {
let fields = new Set();
for (let operator of MONGO_OPERATORS){
if (modifier[operator]) for (let field in modifier[operator]){
const indexOfDot = field.indexOf('.');
if (indexOfDot !== -1) {
field = field.substring(0, indexOfDot);
}
fields.add(field);
}
}
fields.hasAny = hasAny;
return fields;
}

View File

@@ -0,0 +1,53 @@
import SimpleSchema from 'simpl-schema';
let Icons = new Mongo.Collection('icons');
iconsSchema = new SimpleSchema({
name: {
type: String,
unique: true,
index: 1,
},
description: {
type: String,
optional: true,
},
tags: {
type: Array,
optional: true,
index: 1,
},
'tags.$': {
type: String,
},
shape: {
type: String,
},
});
if (Meteor.isServer) {
Icons._ensureIndex({
'name': 'text',
'description': 'text',
'tags': 'text',
});
}
Icons.attachSchema(iconsSchema);
/*
console.warn("Write Icons is not secure, disable before deployment")
const writeIcons = new ValidatedMethod({
name: 'writeIcons',
validate: null,
run(icons){
if (Meteor.isServer){
this.unblock();
Icons.rawCollection().insert(icons, {ordered: false});
}
}
});
*/
export { writeIcons };
export default Icons;

View File

@@ -0,0 +1,100 @@
import SimpleSchema from 'simpl-schema';
import SharingSchema from '/imports/api/sharing/SharingSchema.js';
import simpleSchemaMixin from '/imports/api/creature/mixins/simpleSchemaMixin.js';
import { assertEditPermission, assertOwnership } from '/imports/api/sharing/sharingPermissions.js';
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
/**
* Libraries are trees of library nodes where each node represents a character
* property.
*
* Libraries can be shared, have multiple readers and writers, and can be
* subscribed to.
*
* Permissions to library nodes are controlled by the libraries they belong to.
*/
let Libraries = new Mongo.Collection('libraries');
let LibrarySchema = new SimpleSchema({
name: {
type: String,
},
isDefault: {
type: Boolean,
optional: true,
},
});
LibrarySchema.extend(SharingSchema);
Libraries.attachSchema(LibrarySchema);
export default Libraries;
const insertLibrary = new ValidatedMethod({
name: 'Libraries.methods.insert',
mixins: [
simpleSchemaMixin,
],
schema: LibrarySchema.omit('owner', 'isDefault'),
run(library) {
library.owner = this.userId;
return Libraries.insert(library);
},
});
const updateLibraryName = new ValidatedMethod({
name: 'Libraries.methods.updateName',
validate: new SimpleSchema({
_id: {
type: String,
regEx: SimpleSchema.RegEx.id
},
name: {
type: String,
},
}).validator(),
run({_id, name}){
let library = Libraries.findOne(_id);
assertEditPermission(library, this.userId);
Libraries.update(_id, {$set: {name}});
},
});
const setLibraryDefault = new ValidatedMethod({
name: 'Libraries.methods.makeLibraryDefault',
validate: new SimpleSchema({
_id: {
type: String,
regEx: SimpleSchema.RegEx.id
},
isDefault: {
type: Boolean,
},
}).validator(),
run({_id, isDefault}) {
if (!Meteor.users.isAdmin()){
throw new Meteor.Error('Permission denied', 'User must be admin to set libraries as default');
}
return Libraries.update(_id, {$set: {isDefault}});
},
});
const removeLibrary = new ValidatedMethod({
name: 'Libraries.methods.remove',
validate: new SimpleSchema({
_id: {
type: String,
regEx: SimpleSchema.RegEx.id
},
}).validator(),
run({_id}){
let library = Libraries.findOne(_id);
assertOwnership(library, this.userId);
Libraries.remove(_id);
this.unblock();
LibraryNodes.remove({'ancestors.id': _id});
}
})
export { LibrarySchema, insertLibrary, setLibraryDefault, updateLibraryName, removeLibrary };

View File

@@ -0,0 +1,139 @@
import { Meteor } from 'meteor/meteor';
import { Mongo } from 'meteor/mongo';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import SimpleSchema from 'simpl-schema';
import ChildSchema from '/imports/api/parenting/ChildSchema.js';
import propertySchemasIndex from '/imports/api/properties/propertySchemasIndex.js';
import Libraries from '/imports/api/library/Libraries.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';
let LibraryNodes = new Mongo.Collection('libraryNodes');
let LibraryNodeSchema = new SimpleSchema({
type: {
type: String,
allowedValues: Object.keys(propertySchemasIndex),
},
tags: {
type: Array,
defaultValue: [],
},
'tags.$': {
type: String,
},
});
for (let key in propertySchemasIndex){
let schema = new SimpleSchema({});
schema.extend(LibraryNodeSchema);
schema.extend(propertySchemasIndex[key]);
schema.extend(ChildSchema);
schema.extend(SoftRemovableSchema);
LibraryNodes.attachSchema(schema, {
selector: {type: key}
});
}
function getLibrary(node){
if (!node) throw new Meteor.Error('No node provided');
let library = Libraries.findOne(node.ancestors[0].id);
if (!library) throw new Meteor.Error('Library does not exist');
return library;
}
function assertNodeEditPermission(node, userId){
let lib = getLibrary(node);
return assertEditPermission(lib, userId);
}
const insertNode = new ValidatedMethod({
name: 'LibraryNodes.methods.insert',
validate: null,
run(libraryNode) {
assertNodeEditPermission(libraryNode, this.userId);
return LibraryNodes.insert(libraryNode);
},
});
const updateLibraryNode = new ValidatedMethod({
name: 'LibraryNodes.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':
return false;
}
},
run({_id, path, value}) {
let node = LibraryNodes.findOne(_id);
assertNodeEditPermission(node, 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}};
}
return LibraryNodes.update(_id, modifier, {
selector: {type: node.type},
});
},
});
const pushToLibraryNode = new ValidatedMethod({
name: 'LibraryNodes.methods.push',
validate: null,
run({_id, path, value}){
let node = LibraryNodes.findOne(_id);
assertNodeEditPermission(node, this.userId);
return LibraryNodes.update(_id, {
$push: {[path.join('.')]: value},
}, {
selector: {type: node.type},
});
}
});
const pullFromLibraryNode = new ValidatedMethod({
name: 'LibraryNodes.methods.pull',
validate: null,
run({_id, path, itemId}){
let node = LibraryNodes.findOne(_id);
assertNodeEditPermission(node, this.userId);
return LibraryNodes.update(_id, {
$pull: {[path.join('.')]: {_id: itemId}},
}, {
selector: {type: node.type},
getAutoValues: false,
});
}
});
const softRemoveLibraryNode = new ValidatedMethod({
name: 'LibraryNodes.methods.softRemove',
validate: new SimpleSchema({
_id: SimpleSchema.RegEx.Id
}).validator(),
run({_id}){
let node = LibraryNodes.findOne(_id);
assertNodeEditPermission(node, this.userId);
softRemove({_id, collection: LibraryNodes});
}
});
export default LibraryNodes;
export {
LibraryNodeSchema,
insertNode,
updateLibraryNode,
pullFromLibraryNode,
pushToLibraryNode,
softRemoveLibraryNode,
};

View File

@@ -0,0 +1,11 @@
import SimpleSchema from 'simpl-schema';
Blacklist = new Mongo.Collection("blacklist");
Schemas.Blacklist = new SimpleSchema({
userId: {
type: String,
},
});
Blacklist.attachSchema(Schemas.Blacklist);

View File

@@ -1,3 +1,5 @@
import SimpleSchema from 'simpl-schema';
ChangeLogs = new Mongo.Collection("changeLogs");
Schemas.ChangeLog = new SimpleSchema({

View File

@@ -1,3 +1,5 @@
import SimpleSchema from 'simpl-schema';
Reports = new Mongo.Collection("reports");
Schemas.Report = new SimpleSchema({
@@ -22,7 +24,7 @@ Schemas.Report = new SimpleSchema({
},
//the immediate impact of doing this action (eg. -1 rages)
severity: {
type: Number,
type: SimpleSchema.Integer,
defaultValue: 5,
min: 1,
max: 10,

View File

@@ -0,0 +1,33 @@
import SimpleSchema from 'simpl-schema';
const RefSchema = new SimpleSchema({
id: {
type: String,
regEx: SimpleSchema.RegEx.Id,
index: 1
},
collection: {
type: String
},
});
let ChildSchema = new SimpleSchema({
order: {
type: Number,
min: 0,
},
parent: {
type: RefSchema,
optional: true,
},
ancestors: {
type: Array,
defaultValue: [],
},
'ancestors.$': {
type: RefSchema,
},
});
export default ChildSchema;
export { RefSchema };

View File

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

View File

@@ -1,26 +1,24 @@
Meteor.startup(() => {
const collections = [
Attacks, Buffs, Classes, Effects, Experiences,
Features, Notes, Proficiencies, SpellLists, Spells,
Containers, Items,
];
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
let collections = [LibraryNodes];
if (Meteor.isServer) Meteor.startup(() => {
/**
* Deletes all soft removed documents that were removed more than 30 minutes ago
* and were not restored
* @return {Number} Number of documents removed
*/
const deleteOldSoftRemovedDocs = function(){
let numRemoved = 0;
const now = new Date();
const thirtyMinutesAgo = new Date(now.getTime() - 30*60000);
_.each(collections, (collection) => {
numRemoved += collection.remove({
collections.forEach(collection => {
collection.remove({
removed: true,
removedAt: {$lt: thirtyMinutesAgo} // dates *before* 30 minutes ago
}, error => {
if (error) console.error(error);
});
});
return numRemoved;
return;
};
SyncedCron.add({

View File

@@ -0,0 +1,12 @@
import getCollectionByName from '/imports/api/parenting/getCollectionByName.js';
const docNotFoundError = function({id, collection}){
throw new Meteor.Error('document-not-found',
`No document could be found with id: ${id} in ${collection}`
);
};
export default function fetchDocByRef({id, collection}, options){
return getCollectionByName(collection).findOne(id, options) ||
docNotFoundError({id, collection});
}

View File

@@ -0,0 +1,11 @@
const collectionDoesntExistError = function(collectionName){
throw new Meteor.Error('bad-collection-reference',
`Parent references collection ${collectionName}, which does not exist`
);
};
const getCollectionByName = function(name){
return Mongo.Collection.get(name) || collectionDoesntExistError(name);
};
export default getCollectionByName;

View File

@@ -0,0 +1,198 @@
import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
import getCollectionByName from '/imports/api/parenting/getCollectionByName.js';
// Docs keep track of their order amongst their siblings and keep a copy of the
// order of their ancestors. Order is first compared between oldest non-shared
// ancestors, then by ancestors before children, then between order of siblings.
export function compareOrder(docA, docB){
// < 0 if A comes before B
// = 0 if A and B are the same order
// > 0 if B comes before A
// Documents are equal order to themselves
if (docA._id && docB._id && docA._id === docB._id){
return 0;
}
// If they are siblings, just compare order
if (docA.parent.id === docB.parent.id){
return docA.order - docB.order;
}
// They must share a root ancestor to be meaningfully sorted
if (docA.ancestors[0].id !== docB.ancestors[0].id){
return 0;
}
// Go through their ancestors after the root, and find the first order
// difference
// TODO ancestors don't store order yet
let i, difference;
const length = Math.min(docA.ancestors.length, docB.ancestors.length);
for (i = 1; i < length; i++){
difference = docA.ancestors[i].order - docB.ancestors[i].order;
if (difference){
return difference;
} else if (docA.ancestors[i].id !== docB.ancestors[i].id) {
throw new Meteor.Error('Sibling order clash',
'Sibling docs share the same order, sort failed');
}
}
// We haven't returned yet, all ancestors up to this point are shared and one
// doc has no more ancestors implying one is an ancestor of the other,
// return the difference in their ancestor list lengths, shorter comes first
return docA.ancestors.length - docB.ancestors.length
}
export function getHighestOrder({collection, parentId}){
const highestOrderedDoc = collection.findOne({
'parent.id': parentId,
}, {
fields: {order: 1},
sort: {order: -1},
});
return highestOrderedDoc ? highestOrderedDoc.order : -1;
}
export function setDocToLastOrder({collection, doc}){
doc.order = getHighestOrder({
collection,
parentId: doc.parent.id,
}) + 1;
}
// update the order of a doc, and shift the siblings around to suit the new
// order
export function updateDocOrder({docRef, order}){
let doc = fetchDocByRef(docRef, {fields: {
order: 1,
parent: 1,
}});
let collection = getCollectionByName(docRef.collection);
const currentOrder = doc.order;
if (currentOrder === order){
return;
} else {
// First move the documents that are in the way
let inBetweenSelector, increment;
if (order > currentOrder){
// Move in-between docs backward
inBetweenSelector = {
$gt: currentOrder,
$lte: order
};
increment = -1;
} else if (order < currentOrder){
// Move in-between docs forward
inBetweenSelector = {
$lt: currentOrder,
$gte: order
};
increment = 1;
}
collection.update({
'parent.id': doc.parent.id,
order: inBetweenSelector,
}, {
$inc: {order: increment},
}, {
multi: true,
selector: {type: 'any'},
});
// Then move the document itself
collection.update(doc._id, {$set: {order}}, {selector: {type: 'any'}});
}
}
export function removedDocAtOrder({collection, doc}){
// Decrement the order of all docs after the removed doc
collection.update({
'parent.id': doc.parent.id,
order: {$gt: doc.order},
}, {
$inc: {order: -1},
}, {
multi: true,
selector: {type: 'any'},
});
}
export function insertedDocAtOrder({collection, parentId, order}){
// Increment the order of all docs after the inserted doc
collection.update({
'parent.id': parentId,
order: {$gte: order},
}, {
$inc: {order: 1},
}, {
multi: true,
selector: {type: 'any'},
});
}
// Update the order a single doc and re-order the entire sibling list
// with the change
export function safeUpdateDocOrder({docRef, order}){
let collection = getCollectionByName(docRef.collection);
let movedDoc = fetchDocByRef(docRef, {fields: {
parent: 1, name: 1
}});
let parentId = movedDoc.parent.id;
let bulkWrite = [];
let docs = collection.find({
'parent.id': parentId,
'_id': {$ne: movedDoc._id},
}, {
fields: {order: 1, name: 1},
sort: {order: 1}
}).fetch();
docs.splice(order, 0, movedDoc);
docs.forEach((doc, index) => {
if (doc.order !== index){
bulkWrite.push({
updateOne: {
filter: {_id: doc._id},
update: {$set: {order: index}},
},
});
}
});
if (Meteor.isServer){
collection.rawCollection().bulkWrite(bulkWrite);
} else {
bulkWrite.forEach(op => {
collection.update(
op.updateOne.filter,
op.updateOne.update,
{selector: {type: 'any'}}
);
});
}
};
export function reorderDocs({collection, parentId}){
let bulkWrite = [];
collection.find({
'parent.id': parentId,
}, {
fields: {order: 1},
sort: {order: 1}
}).forEach((doc, index) => {
if (doc.order !== index){
bulkWrite.push({
updateOne : {
filter: {_id: doc._id},
update: {$set: {order: index}},
},
});
}
});
if (Meteor.isServer){
collection.rawCollection().bulkWrite(bulkWrite);
} else {
bulkWrite.forEach(op => {
collection.update(op.updateOne.filter, op.updateOne.update);
});
}
}

View File

@@ -0,0 +1,55 @@
import SimpleSchema from 'simpl-schema';
import { updateParent } from '/imports/api/parenting/parenting.js';
import { insertedDocAtOrder, removedDocAtOrder, safeUpdateDocOrder } from '/imports/api/parenting/order.js';
import { RefSchema } from '/imports/api/parenting/ChildSchema.js';
import { assertDocEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
import getCollectionByName from '/imports/api/parenting/getCollectionByName.js';
const organizeDoc = new ValidatedMethod({
name: 'organize.methods.organizeDoc',
validate: new SimpleSchema({
docRef: RefSchema,
parentRef: RefSchema,
order: {
type: Number,
min: 0,
},
}).validator(),
run({docRef, parentRef, order}) {
let doc = fetchDocByRef(docRef);
let collection = getCollectionByName(docRef.collection);
// The user must be able to edit both the doc and its parent to move it
// successfully
assertDocEditPermission(doc, this.userId);
let parent = fetchDocByRef(parentRef);
assertDocEditPermission(parent, this.userId);
// Reorder the documents in the doc's old parent
removedDocAtOrder({collection, doc});
// Reorder the docs in the destination parent
insertedDocAtOrder({collection, parentId: parentRef.id, order});
// Change the doc's parent
updateParent({docRef, parentRef});
// Change the doc's order
collection.update(doc._id, {$set: {order}}, {selector: {type: 'any'}});
},
});
const reorderDoc = new ValidatedMethod({
name: 'organize.methods.reorderDoc',
validate: new SimpleSchema({
docRef: RefSchema,
order: {
type: Number,
min: 0,
},
}).validator(),
run({docRef, order}) {
let doc = fetchDocByRef(docRef);
assertDocEditPermission(doc, this.userId);
safeUpdateDocOrder({docRef, order})
},
});
export { organizeDoc, reorderDoc };

View File

@@ -0,0 +1,240 @@
import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
import getCollectionByName from '/imports/api/parenting/getCollectionByName.js';
import { flatten, findLast } from 'lodash';
const generalParents = [
'attribute',
'buff',
'classLevel',
'feature',
'folder',
'root',
'item',
'spell',
];
// Which types are allowed as parents for other types
const allowedParenting = {
folder: [...generalParents, 'container'],
rollResult: ['roll', 'rollResult'],
container: ['root', 'folder'],
item: ['root', 'container', 'folder'],
};
const allParentTypes = new Set(flatten(Object.values(allowedParenting)));
export function canBeParent(type){
return true;
//TODO until there is a good reason to disallow certain parenting options,
// this should just let the user do whatever
return type && allParentTypes.has(type);
}
export function getAllowedParents({childType}){
return allowedParenting[childType] || generalParents;
}
export function isParentAllowed({parentType = 'root', childType}){
return true;
//TODO until there is a good reason to disallow certain parenting options,
// this should just let the user do whatever
if (!childType) throw 'childType is required';
let allowedParents = getAllowedParents({childType});
return allowedParents.includes(parentType);
}
export function fetchParent({id, collection}){
return fetchDocByRef({id, collection});
}
export function fetchChildren({ collection, parentId, filter = {}, options = {sort: {order: 1}} }){
filter["parent.id"] = parentId;
let children = [];
children.push(
...collection.find({
"parent.id": parentId
}, options).fetch()
);
return children;
}
export function updateChildren({collection, parentId, filter = {}, modifier, options={}}){
filter["parent.id"] = parentId;
options.multi = true;
collection.update(filter, modifier, options);
}
export function fetchDescendants({ collection, ancestorId, filter = {}, options}){
filter["ancestors.id"] = ancestorId;
let descendants = [];
descendants.push(...collection.find(filter, options).fetch());
return descendants;
}
export function updateDescendants({collection, ancestorId, filter = {}, modifier, options={}}){
filter["ancestors.id"] = ancestorId;
options.multi = true;
options.selector = {type: 'any'};
collection.update(filter, modifier, options);
}
export function forEachDescendant({collection, ancestorId, filter = {}, options}, callback){
filter["ancestors.id"] = ancestorId;
collection.find(filter, options).forEach(callback);
}
// 1 database read
export function getAncestry({parentRef, inheritedFields = {}}){
let parentDoc = fetchDocByRef(parentRef, {fields: inheritedFields});
let parent = { ...parentRef};
for (let field in inheritedFields){
if (inheritedFields[field]){
parent[field] = parentDoc[field];
}
}
// Ancestors is [...parent's ancestors, parent ref]
let ancestors = parentDoc.ancestors || [];
ancestors.push(parent);
return {parentDoc, parent, ancestors};
}
export function setLineageOfDocs({docArray, oldParent, newAncestry}){
//const oldParent = oldAncestry[oldAncestry.length - 1];
const newParent = newAncestry[newAncestry.length - 1];
docArray.forEach(doc => {
if(doc.parent.id === oldParent.id){
doc.parent = newParent;
}
let oldAncestors = doc.ancestors;
let oldParentIndex = oldAncestors.findIndex(a => a.id === oldParent.id);
doc.ancestors = [...newAncestry, ...oldAncestors.slice(oldParentIndex + 1)];
});
}
/**
* Give documents new random ids and transform their references.
* Transform collections of re-IDed docs according to the collection map
*/
export function renewDocIds({docArray, collectionMap}){
// map of {oldId: newId}
let idMap = {};
// Give new ids and map the changes
docArray.forEach(doc => {
let oldId = doc._id;
let newId = Random.id();
doc._id = newId;
idMap[oldId] = newId;
});
const remapReference = ref => {
if (idMap[ref.id]){
ref.id = idMap[ref.id];
ref.collection = collectionMap[ref.collection] || ref.collection;
}
}
docArray.forEach(doc => {
remapReference(doc.parent);
doc.ancestors.forEach(remapReference);
});
}
export function updateParent({docRef, parentRef}){
let collection = getCollectionByName(docRef.collection);
let oldDoc = fetchDocByRef(docRef, {fields: {
parent: 1,
ancestors: 1,
type: 1,
}});
let updateOptions = { selector: {type: 'any'} };
// Skip if we aren't changing the parent id
if (oldDoc.parent.id === parentRef.id) return;
// Get the parent and its ancestry
let {parentDoc, parent, ancestors} = getAncestry({parentRef});
// If the doc and its parent are in the same collection, apply the allowed
// parent rules based on type
if (docRef.collection === parentRef.collection){
let parentAllowed = isParentAllowed({
parentType: parentDoc.type,
childType: oldDoc.type
});
if (!parentAllowed){
throw new Meteor.Error('invalid parenting',
`Can't make ${oldDoc.type} a child of ${parentDoc.type}`)
}
}
// update the document's parenting
collection.update(docRef.id, {
$set: {parent, ancestors}
}, updateOptions);
// Remove the old ancestors from the descendants
updateDescendants({
collection,
ancestorId: docRef.id,
modifier: {$pullAll: {
ancestors: oldDoc.ancestors,
}},
options: updateOptions,
});
// Add the new ancestors to the descendants
updateDescendants({
collection,
ancestorId: docRef.id,
modifier: {$push: {
ancestors: {
$each: ancestors,
$position: 0,
},
}},
options: updateOptions,
});
}
export function getName(doc){
if (doc.name) return name;
var i = doc.ancestors.length;
while(i--) {
if (ancestors[i].name) return ancestors[i].name;
}
}
export function nodesToTree({collection, ancestorId, filter}){
// Store a dict of all the nodes
let nodeIndex = {};
let nodeList = [];
collection.find({
'ancestors.id': ancestorId,
removed: {$ne: true},
...filter,
}, {
sort: {order: 1}
}).forEach( node => {
let treeNode = {
node: node,
children: [],
};
nodeIndex[node._id] = treeNode;
nodeList.push(treeNode);
});
// Create a forest of trees
let forest = [];
// Either the node is a child of its nearest found ancestor, or in the forest as a root
nodeList.forEach(treeNode => {
let ancestorInForest = findLast(
treeNode.node.ancestors,
ancestor => !!nodeIndex[ancestor.id]
);
if (ancestorInForest){
nodeIndex[ancestorInForest.id].children.push(treeNode);
} else {
forest.push(treeNode);
}
});
return forest;
}

View File

@@ -0,0 +1,64 @@
import getCollectionByName from '/imports/api/parenting/getCollectionByName.js';
import { updateDescendants } from '/imports/api/parenting/parenting.js';
export function softRemove({_id, collection}){
let removalDate = new Date();
if (typeof collection === 'string') {
collection = getCollectionByName(collection);
}
// Remove this document
collection.update(
_id, {
$set: {
removed: true,
removedAt: removalDate,
},
$unset: {
removedWith: 1,
}
}, {
selector: {type: 'any'},
},
);
// Remove all the descendants that have not yet been removed, and set them to be
// removed with this document
updateDescendants({
collection,
ancestorId: _id,
filter: {removed: {$ne: true}},
modifier: {$set: {
removed: true,
removedAt: removalDate,
removedWith: _id,
}},
});
}
const restoreError = function(){
throw new Meteor.Error('restore-failed',
'Could not restore this document, maybe it was removed by a parent?'
);
};
export function restore({_id, collection}){
collection = getCollectionByName(collection);
let numUpdated = collection.update({
_id,
removedWith: {$exists: false}
}, { $unset: {
removed: 1,
removedAt: 1,
}});
if (numUpdated === 0) restoreError();
updateDescendants({
ancestorId: _id,
filter: {
removedWith: _id,
},
modifier: { $unset: {
removed: 1,
removedAt: 1,
removedWith: 1,
}},
});
}

View File

@@ -0,0 +1,72 @@
import SimpleSchema from 'simpl-schema';
import ResourcesSchema from '/imports/api/properties/subSchemas/ResourcesSchema.js'
import ResultsSchema from '/imports/api/properties/subSchemas/ResultsSchema.js';
/*
* Actions are things a character can do
* Any rolls that are children of actions will be rolled when taking the action
* Any actions that are children of this action will be considered alternatives
* to this action
*/
let ActionSchema = new SimpleSchema({
name: {
type: String,
optional: true,
},
description: {
type: String,
optional: true,
},
// What time-resource is used to take the action in combat
// long actions take longer than 1 round to cast
actionType: {
type: String,
allowedValues: ['action', 'bonus', 'attack', 'reaction', 'free', 'long'],
defaultValue: 'action',
},
// Who is the action directed at
target: {
type: String,
defaultValue: 'singleTarget',
allowedValues: [
'self',
'singleTarget',
'multipleTargets',
],
},
tags: {
type: Array,
defaultValue: [],
},
'tags.$': {
type: String,
},
results: {
type: ResultsSchema,
defaultValue: {},
},
resources: {
type: ResourcesSchema,
defaultValue: {},
},
// Calculation of how many times this action can be used
// Only set if this action tracks its own uses, rather than adjusting
// resources
uses: {
type: String,
optional: true,
},
// Integer of how many times it has already been used
usesUsed: {
type: SimpleSchema.Integer,
optional: true,
},
// How this action's uses are reset automatically
reset: {
type: String,
allowedValues: ['longRest', 'shortRest'],
optional: true,
},
});
export { ActionSchema };

View File

@@ -0,0 +1,28 @@
import SimpleSchema from 'simpl-schema';
import { ActionSchema } from '/imports/api/properties/Actions.js';
// Attacks are special instances of actions
let AttackSchema = new SimpleSchema()
.extend(ActionSchema)
.extend({
// What gets added to the d20 roll
rollBonus: {
type: String,
defaultValue: 'strength.modifier + proficiencyBonus',
optional: true,
},
// Set better defaults for the action
actionType: {
type: String,
defaultValue: 'attack',
},
tags: {
type: Array,
defaultValue: ['attack'],
},
'tags.$': {
type: String,
},
});
export { AttackSchema };

View File

@@ -0,0 +1,94 @@
import SimpleSchema from 'simpl-schema';
import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js';
/*
* Attributes are numbered stats of a character
*/
let AttributeSchema = new SimpleSchema({
name: {
type: String,
defaultValue: 'New Attribute',
},
// The technical, lowercase, single-word name used in formulae
variableName: {
type: String,
regEx: VARIABLE_NAME_REGEX,
min: 3,
defaultValue: 'newAttribute',
},
// How it is displayed and computed is determined by type
attributeType: {
type: String,
allowedValues: [
'ability', //Strength, Dex, Con, etc.
'stat', // Speed, Armor Class
'modifier', // Proficiency Bonus, Initiative
'hitDice', // d12 hit dice
'healthBar', // Hitpoints, Temporary Hitpoints, can take damage
'bar', // Displayed as a health bar, can't take damage
'resource', // Rages, sorcery points
'spellSlot', // Level 1, 2, 3... spell slots
'utility', // Aren't displayed, Jump height, Carry capacity
],
defaultValue: 'stat',
index: 1,
},
// For type hitDice, the size needs to be stored separately
hitDiceSize: {
type: String,
allowedValues: ['d4', 'd6', 'd8', 'd10', 'd12', 'd20'],
optional: true,
},
// The starting value, before effects
baseValueCalculation: {
type: String,
optional: true,
},
// Description of what the attribute is used for
description: {
type: String,
optional: true,
},
// The damage done to the attribute, always positive
damage: {
type: SimpleSchema.Integer,
optional: true,
min: 0,
},
// Can the value be decimal?
decimal: {
type: Boolean,
optional: true,
},
// Automatically zero the adjustment on these conditions
reset: {
type: String,
optional: true,
allowedValues: ['shortRest', 'longRest'],
},
});
let ComputedOnlyAttributeSchema = new SimpleSchema({
// The result of baseValueCalculation
baseValue: {
type: SimpleSchema.oneOf(Number, String, Boolean),
optional: true,
},
// The computed value of the attribute
value: {
type: SimpleSchema.oneOf(Number, String, Boolean),
defaultValue: 0,
optional: true,
},
// The computed modifier, provided the attribute type is `ability`
modifier: {
type: SimpleSchema.Integer,
optional: true,
},
});
const ComputedAttributeSchema = new SimpleSchema()
.extend(ComputedOnlyAttributeSchema)
.extend(AttributeSchema);
export { AttributeSchema, ComputedOnlyAttributeSchema, ComputedAttributeSchema };

View File

@@ -0,0 +1,75 @@
import SimpleSchema from 'simpl-schema';
import { Random } from 'meteor/random';
import { StoredEffectSchema } from '/imports/api/properties/Effects.js';
let BuffSchema = new SimpleSchema({
name: {
type: String,
optional: true,
},
description: {
type: String,
optional: true,
},
duration: {
type: String,
optional: true,
},
});
// The effects in the stored buff need to be resolved to a number before being
// placed on other characters, if they are applied to self, they can remain as
// calculations, provided they don't contain any rolls
let StoredBuffSchema = new SimpleSchema({
effects: {
type: Array,
defaultValue: [],
},
'effects.$': {
type: StoredEffectSchema,
},
target: {
type: String,
allowedValues: [
'self', // the character who took the buff
'each', // rolled once for `each` target
'every', // rolled once and applied to `every` target
],
defaultValue: 'every',
},
}).extend(BuffSchema);
let StoredBuffWithIdSchema = new SimpleSchema({
_id: {
type: String,
regEx: SimpleSchema.RegEx.Id,
autoValue(){
if (!this.isSet){
return Random.id();
}
},
},
}).extend(StoredBuffSchema);
let AppliedBuffSchema = new SimpleSchema({
durationSpent: {
type: Number,
optional: true,
min: 0,
},
appliedBy: {
type: Object,
},
'appliedBy.name': {
type: String,
},
'appliedBy.id': {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
'appliedBy.collection': {
type: String,
},
}).extend(BuffSchema);
export { AppliedBuffSchema, StoredBuffSchema, StoredBuffWithIdSchema };

View File

@@ -0,0 +1,33 @@
import SimpleSchema from 'simpl-schema';
import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js';
let ClassLevelSchema = new SimpleSchema({
name: {
type: String,
optional: true,
},
// The name of this class level's variable
variableName: {
type: String,
regEx: VARIABLE_NAME_REGEX,
},
// The variable name of the base class this level is attached to
baseClass: {
type: String,
regEx: VARIABLE_NAME_REGEX,
optional: true,
},
level: {
type: SimpleSchema.Integer,
defaultValue: 1,
},
nextLevelTags: {
type: Array,
defaultValue: [],
},
'nextLevelTags.$': {
type: String,
},
});
export { ClassLevelSchema };

View File

@@ -0,0 +1,35 @@
import SimpleSchema from 'simpl-schema';
let ContainerSchema = new SimpleSchema({
name: {
type: String,
optional: true,
trim: false
},
carried: {
type: Boolean,
defaultValue: true,
optional: true,
},
contentsWeightless: {
type: Boolean,
optional: true,
},
weight: {
type: Number,
min: 0,
defaultValue: 0
},
value: {
type: Number,
min: 0,
defaultValue: 0
},
description: {
type: String,
optional: true,
trim: false
},
});
export { ContainerSchema };

View File

@@ -0,0 +1,46 @@
import SimpleSchema from 'simpl-schema';
import DAMAGE_TYPES from '/imports/constants/DAMAGE_TYPES.js';
/*
* DamageMultipliers are multipliers that affect how much damage is taken from
* a given damage type
*/
let DamageMultiplierSchema = new SimpleSchema({
name: {
type: String,
optional: true,
},
damageTypes: {
type: Array,
defaultValue: [],
},
// The technical, lowercase, single-word name used in formulae
'damageTypes.$': {
type: String,
allowedValues: DAMAGE_TYPES,
},
// The value of the damage multiplier
value: {
type: Number,
defaultValue: 0.5,
allowedValues: [0, 0.5, 2],
},
// Tags which bypass this multiplier (OR)
excludeTags: {
type: Array,
defaultValue: [],
},
'excludeTags.$': {
type: String,
},
// Tags which must be present to be affected by this multiplier (AND)
targetTags: {
type: Array,
defaultValue: [],
},
'targetTags.$': {
type: String,
},
});
export { DamageMultiplierSchema };

View File

@@ -0,0 +1,67 @@
import SimpleSchema from 'simpl-schema';
import { Random } from 'meteor/random';
/*
* Effects are reason-value attached to skills and abilities
* that modify their final value or presentation in some way
*/
let EffectSchema = new SimpleSchema({
name: {
type: String,
optional: true,
},
operation: {
type: String,
defaultValue: 'add',
allowedValues: [
'base',
'add',
'mul',
'min',
'max',
'advantage',
'disadvantage',
'passiveAdd',
'fail',
'conditional',
'rollBonus',
],
},
calculation: {
type: String,
optional: true,
},
//which stats the effect is applied to
stats: {
type: Array,
defaultValue: [],
},
'stats.$': {
type: String,
optional: true,
},
});
const StoredEffectSchema = new SimpleSchema({
_id: {
type: String,
regEx: SimpleSchema.RegEx.Id,
autoValue(){
if (!this.isSet) return Random.id();
}
},
}).extend(EffectSchema);
const ComputedOnlyEffectSchema = new SimpleSchema({
// The computed result of the effect
result: {
type: SimpleSchema.oneOf(Number, String, Boolean),
optional: true,
},
})
const ComputedEffectSchema = new SimpleSchema()
.extend(ComputedOnlyEffectSchema)
.extend(EffectSchema);
export { EffectSchema, StoredEffectSchema, ComputedEffectSchema, ComputedOnlyEffectSchema };

View File

@@ -0,0 +1,35 @@
import SimpleSchema from 'simpl-schema';
let ExperienceSchema = new SimpleSchema({
name: {
type: String,
optional: true,
},
// Potentially long description of the event
description: {
type: String,
optional: true,
},
// The amount of XP this experience gives
value: {
type: SimpleSchema.Integer,
optional: true,
},
// The real-world date that it occured
date: {
type: Date,
autoValue: function() {
// If the date isn't set, set it to now
if (!this.isSet) {
return new Date();
}
},
},
// The date in-world of this event
worldDate: {
type: String,
optional: true,
},
});
export { ExperienceSchema };

View File

@@ -0,0 +1,17 @@
import SimpleSchema from 'simpl-schema';
let FeatureSchema = new SimpleSchema({
name: {
type: String,
},
summary: {
type: String,
optional: true,
},
description: {
type: String,
optional: true,
},
});
export { FeatureSchema }

View File

@@ -0,0 +1,11 @@
import SimpleSchema from 'simpl-schema';
// Folders organize a character sheet into a tree, particularly to group things
// like 'race' and 'background'
let FolderSchema = new SimpleSchema({
name: {
type: String,
},
});
export { FolderSchema };

View File

@@ -0,0 +1,57 @@
import SimpleSchema from 'simpl-schema';
const ItemSchema = new SimpleSchema({
name: {
type: String,
optional: true,
},
// Plural name of the item, if there is more than one
plural: {
type: String,
optional: true,
},
description: {
type: String,
optional: true,
},
// Number currently held
quantity: {
type: SimpleSchema.Integer,
min: 0,
defaultValue: 1
},
// Weight per item in the stack
weight: {
type: Number,
min: 0,
defaultValue: 0,
},
// Value per item in the stack, in gold pieces
value: {
type: Number,
min: 0,
defaultValue: 0,
},
// If this item is equipped, it requires attunement
// Being equipped is `enabled === true`
requiresAttunement: {
type: Boolean,
optional: true,
},
attuned: {
type: Boolean,
optional: true,
},
// Show increment/decrement buttons in item lists
showIncrement: {
type: Boolean,
optional: true,
},
// Unequipped items shouldn't affect creature stats
equipped: {
type: Boolean,
defaultValue: false,
},
});
export { ItemSchema };

View File

@@ -0,0 +1,14 @@
import SimpleSchema from 'simpl-schema';
let NoteSchema = new SimpleSchema({
name: {
type: String,
optional: true,
},
description: {
type: String,
optional: true,
},
});
export { NoteSchema };

View File

@@ -0,0 +1,25 @@
import SimpleSchema from 'simpl-schema';
let ProficiencySchema = new SimpleSchema({
name: {
type: String,
optional: true,
},
// The variableNames of the skills, tags, or attributes to apply proficiency to
stats: {
type: Array,
defaultValue: [],
},
'stats.$': {
type: String,
optional: true,
},
// A number representing how proficient the character is
value: {
type: Number,
allowedValues: [0.5, 1, 2],
defaultValue: 1,
},
});
export { ProficiencySchema };

View File

@@ -0,0 +1,46 @@
import SimpleSchema from 'simpl-schema';
import RollResultsSchema from '/imports/api/properties/subSchemas/RollResultsSchema.js'
/**
* Rolls are children to actions or other rolls, they are triggered with 0 or
* more targets and then apply their results to the character taking the action
* or the target of the action.
*
* # Rolls are resolved in one of two ways:
* Regular rolls:
* The target number is computed in the target's context
* The roll is computed in the action taker's context
* If the roll meets or exceeds the target number, the adjustments and buffs
* are applied
*
* Saving throws:
* The target number is computed in the action taker's context
* The roll is computed in the target's context
* If the roll fails to meet or exceed the target number, the adjustments and
* child rolls are applied
*/
let RollSchema = new SimpleSchema({
// The roll
roll: {
type: String,
optional: true,
},
// Effects can apply to this tag specifically
// Ranged spell attack, Ranged weapon attack, etc.
tags: {
type: Array,
defaultValue: [],
},
'tags.$': {
type: String,
},
rollResults: {
type: Array,
defaultValue: [],
},
'rollResults.$': {
type: RollResultsSchema,
},
});
export { RollSchema };

View File

@@ -0,0 +1,26 @@
import SimpleSchema from 'simpl-schema';
import ResultsSchema from '/imports/api/properties/subSchemas/ResultsSchema.js';
// These are the rolls made when saves are called for
// For the saving throw bonus or proficiency, see ./Skills.js
let SavingThrowSchema = new SimpleSchema ({
dc: {
type: String,
optional: true,
},
// The variable name of ability the saving throw relies on
ability: {
type: String,
optional: true,
},
passResults: {
type: ResultsSchema,
defaultValue: {},
},
failResults: {
type: ResultsSchema,
defaultValue: {},
},
});
export { SavingThrowSchema };

View File

@@ -0,0 +1,109 @@
import SimpleSchema from 'simpl-schema';
/*
* Skills are anything that results in a modifier to be added to a D20
* Skills have an ability score modifier that they use as their basis
*/
let SkillSchema = new SimpleSchema({
name: {
type: String,
optional: true,
},
// The technical, lowercase, single-word name used in formulae
// Ignored for skilltype = save
variableName: {
type: String,
regEx: /^\w*[a-z]\w*$/i,
},
// The variable name of the ability this skill relies on
ability: {
type: String,
optional: true,
},
// What type of skill is this
skillType: {
type: String,
allowedValues: [
"skill",
"save",
"check",
"tool",
"weapon",
"language",
"utility", //not displayed anywhere
],
defaultValue: 'skill',
},
// If the baseValue is higher than the computed value, it will be used as `value`
baseValue: {
type: Number,
optional: true,
},
// The base proficiency of this skill
baseProficiency: {
type: Number,
optional: true,
},
// Description of what the skill is used for
description: {
type: String,
optional: true,
},
});
let ComputedOnlySkillSchema = new SimpleSchema({
// Computed value of skill to be added to skill rolls
value: {
type: Number,
defaultValue: 0,
},
// Computed value added by the ability
abilityMod: {
type: SimpleSchema.Integer,
optional: true,
},
// Computed advantage/disadvantage
advantage: {
type: SimpleSchema.Integer,
optional: true,
allowedValues: [-1, 0, 1],
},
// Computed bonus to passive checks
passiveBonus: {
type: Number,
optional: true,
},
// Computed proficiency multiplier
proficiency: {
type: Number,
allowedValues: [0, 0.5, 1, 2],
defaultValue: 0,
},
// Compiled text of all conditional benefits
conditionalBenefits: {
type: Array,
optional: true,
},
'conditionalBenefits.$': {
type: String,
},
// Compiled text of all roll bonuses
rollBonuses: {
type: Array,
optional: true,
},
'rollBonuses.$': {
type: String,
},
// Computed number of things forcing this skill to fail
fail: {
type: SimpleSchema.Integer,
optional: true,
},
})
const ComputedSkillSchema = new SimpleSchema()
.extend(ComputedOnlySkillSchema)
.extend(SkillSchema);
export { SkillSchema, ComputedSkillSchema, ComputedOnlySkillSchema };

View File

@@ -0,0 +1,13 @@
import SimpleSchema from 'simpl-schema';
let SlotSchema = new SimpleSchema({
slotTags: {
type: Array,
defaultValue: [],
},
'slotTags.$': {
type: String,
},
});
export { SlotSchema };

View File

@@ -0,0 +1,19 @@
import SimpleSchema from 'simpl-schema';
let SpellListSchema = new SimpleSchema({
name: {
type: String,
optional: true,
},
description: {
type: String,
optional: true,
},
// Calculation of how many spells in this list can be prepared
maxPrepared: {
type: String,
optional: true,
},
});
export { SpellListSchema }

View File

@@ -0,0 +1,96 @@
import { ActionSchema } from '/imports/api/properties/Actions.js';
import SimpleSchema from 'simpl-schema';
const magicSchools = [
'abjuration',
'conjuration',
'divination',
'enchantment',
'evocation',
'illusion',
'necromancy',
'transmutation',
];
let SpellSchema = new SimpleSchema({})
.extend(ActionSchema)
.extend({
name: {
type: String,
optional: true,
},
// If it's always prepared, it doesn't count against the number of spells
// prepared in a spell list, and enabled should be true
alwaysPrepared: {
type: Boolean,
optional: true,
},
// This spell ignores spell slot rules
castWithoutSpellSlots: {
type: Boolean,
optional: true,
},
hasAttackRoll: {
type: Boolean,
optional: true,
},
// Spell lists that this spell appears on
spellLists: {
type: Array,
defaultValue: [],
},
'spellLists.$': {
type: String,
},
description: {
type: String,
optional: true,
},
castingTime: {
type: String,
optional: true,
defaultValue: 'action',
},
range: {
type: String,
optional: true,
},
duration: {
type: String,
optional: true,
defaultValue: 'Instantaneous',
},
verbal: {
type: Boolean,
optional: true,
},
somatic: {
type: Boolean,
optional: true,
},
concentration: {
type: Boolean,
optional: true,
},
material: {
type: String,
optional: true,
},
ritual: {
type: Boolean,
optional: true,
},
level: {
type: SimpleSchema.Integer,
defaultValue: 1,
max: 9,
min: 0,
},
school: {
type: String,
defaultValue: 'abjuration',
allowedValues: magicSchools,
},
});
export { SpellSchema };

View File

@@ -0,0 +1,47 @@
import SimpleSchema from 'simpl-schema';
import { ActionSchema } from '/imports/api/properties/Actions.js';
import { AttackSchema } from '/imports/api/properties/Attacks.js';
import { ComputedAttributeSchema } from '/imports/api/properties/Attributes.js';
import { AppliedBuffSchema } from '/imports/api/properties/Buffs.js';
import { ClassLevelSchema } from '/imports/api/properties/ClassLevels.js';
import { DamageMultiplierSchema } from '/imports/api/properties/DamageMultipliers.js';
import { ComputedEffectSchema } from '/imports/api/properties/Effects.js';
import { ExperienceSchema } from '/imports/api/properties/Experiences.js';
import { FeatureSchema } from '/imports/api/properties/Features.js';
import { FolderSchema } from '/imports/api/properties/Folders.js';
import { NoteSchema } from '/imports/api/properties/Notes.js';
import { ProficiencySchema } from '/imports/api/properties/Proficiencies.js';
import { RollSchema } from '/imports/api/properties/Rolls.js';
import { SavingThrowSchema } from '/imports/api/properties/SavingThrows.js';
import { ComputedSkillSchema } from '/imports/api/properties/Skills.js';
import { SlotSchema } from '/imports/api/properties/Slots.js';
import { SpellListSchema } from '/imports/api/properties/SpellLists.js';
import { SpellSchema } from '/imports/api/properties/Spells.js';
import { ContainerSchema } from '/imports/api/properties/Containers.js';
import { ItemSchema } from '/imports/api/properties/Items.js';
const propertySchemasIndex = {
action: ActionSchema,
attack: AttackSchema,
attribute: ComputedAttributeSchema,
buff: AppliedBuffSchema,
classLevel: ClassLevelSchema,
damageMultiplier: DamageMultiplierSchema,
effect: ComputedEffectSchema,
experience: ExperienceSchema,
feature: FeatureSchema,
folder: FolderSchema,
note: NoteSchema,
proficiency: ProficiencySchema,
roll: RollSchema,
savingThrow: SavingThrowSchema,
skill: ComputedSkillSchema,
slot: SlotSchema,
spellList: SpellListSchema,
spell: SpellSchema,
container: ContainerSchema,
item: ItemSchema,
any: new SimpleSchema({}),
};
export default propertySchemasIndex;

View File

@@ -0,0 +1,47 @@
import SimpleSchema from 'simpl-schema';
import { ActionSchema } from '/imports/api/properties/Actions.js';
import { AttackSchema } from '/imports/api/properties/Attacks.js';
import { AttributeSchema } from '/imports/api/properties/Attributes.js';
import { StoredBuffSchema } from '/imports/api/properties/Buffs.js';
import { ClassLevelSchema } from '/imports/api/properties/ClassLevels.js';
import { DamageMultiplierSchema } from '/imports/api/properties/DamageMultipliers.js';
import { EffectSchema } from '/imports/api/properties/Effects.js';
import { ExperienceSchema } from '/imports/api/properties/Experiences.js';
import { FeatureSchema } from '/imports/api/properties/Features.js';
import { FolderSchema } from '/imports/api/properties/Folders.js';
import { NoteSchema } from '/imports/api/properties/Notes.js';
import { ProficiencySchema } from '/imports/api/properties/Proficiencies.js';
import { RollSchema } from '/imports/api/properties/Rolls.js';
import { SavingThrowSchema } from '/imports/api/properties/SavingThrows.js';
import { SkillSchema } from '/imports/api/properties/Skills.js';
import { SlotSchema } from '/imports/api/properties/Slots.js';
import { SpellListSchema } from '/imports/api/properties/SpellLists.js';
import { SpellSchema } from '/imports/api/properties/Spells.js';
import { ContainerSchema } from '/imports/api/properties/Containers.js';
import { ItemSchema } from '/imports/api/properties/Items.js';
const propertySchemasIndex = {
action: ActionSchema,
attack: AttackSchema,
attribute: AttributeSchema,
buff: StoredBuffSchema,
classLevel: ClassLevelSchema,
damageMultiplier: DamageMultiplierSchema,
effect: EffectSchema,
experience: ExperienceSchema,
feature: FeatureSchema,
folder: FolderSchema,
note: NoteSchema,
proficiency: ProficiencySchema,
roll: RollSchema,
savingThrow: SavingThrowSchema,
skill: SkillSchema,
slot: SlotSchema,
spellList: SpellListSchema,
spell: SpellSchema,
container: ContainerSchema,
item: ItemSchema,
any: new SimpleSchema({}),
};
export default propertySchemasIndex;

View File

@@ -0,0 +1,35 @@
import SimpleSchema from 'simpl-schema';
import { Random } from 'meteor/random';
const AdjustmentSchema = new SimpleSchema({
_id: {
type: String,
regEx: SimpleSchema.RegEx.Id,
autoValue(){
if (!this.isSet) return Random.id();
}
},
// The roll that determines how much to change the attribute
adjustment: {
type: String,
optional: true,
defaultValue: '1',
},
// Who this adjustment applies to
target: {
type: String,
defaultValue: 'every',
allowedValues: [
'self', // the character who took the action
'each', // rolled once for `each` target
'every', // rolled once and applied to `every` target
],
},
// The stat this rolls applies to, if damage type is set, this is ignored
stat: {
type: String,
optional: true,
},
});
export default AdjustmentSchema;

View File

@@ -0,0 +1,22 @@
import SimpleSchema from 'simpl-schema';
import { Random } from 'meteor/random';
const AttributeConsumedSchema = new SimpleSchema({
_id: {
type: String,
regEx: SimpleSchema.RegEx.Id,
autoValue(){
if (!this.isSet) return Random.id();
}
},
variableName: {
type: String,
optional: true,
},
quantity: {
type: Number,
defaultValue: 1,
},
});
export default AttributeConsumedSchema;

View File

@@ -0,0 +1,12 @@
import SimpleSchema from 'simpl-schema';
const ColorSchema = new SimpleSchema({
color: {
type: String,
// match hex colors of the form #A23 or #A23f56
regEx: /^#([a-f0-9]{3}){1,2}\b$/i,
optional: true,
},
});
export default ColorSchema;

View File

@@ -0,0 +1,36 @@
import SimpleSchema from 'simpl-schema';
import { Random } from 'meteor/random';
import DAMAGE_TYPES from '/imports/constants/DAMAGE_TYPES.js';
const DamageSchema = new SimpleSchema({
_id: {
type: String,
regEx: SimpleSchema.RegEx.Id,
autoValue(){
if (!this.isSet) return Random.id();
}
},
// The roll that determines how much to damage the attribute
damage: {
type: String,
optional: true,
defaultValue: '1d8 + strength.modifier',
},
// Who this adjustment applies to
target: {
type: String,
defaultValue: 'every',
allowedValues: [
'self', // the character who took the action
'each', // rolled once for `each` target
'every', // rolled once and applied to `every` target
],
},
damageType: {
type: String,
allowedValues: DAMAGE_TYPES,
defaultValue: 'slashing',
},
});
export default DamageSchema;

View File

@@ -1,12 +1,14 @@
Schemas.DeathSave = new SimpleSchema({
import SimpleSchema from 'simpl-schema';
const DeathSavesSchema = new SimpleSchema({
pass: {
type: Number,
type: SimpleSchema.Integer,
min: 0,
max: 3,
defaultValue: 0,
},
fail: {
type: Number,
type: SimpleSchema.Integer,
min: 0,
max: 3,
defaultValue: 0,
@@ -20,3 +22,5 @@ Schemas.DeathSave = new SimpleSchema({
defaultValue: false,
},
});
export default DeathSavesSchema;

View File

@@ -0,0 +1,22 @@
import SimpleSchema from 'simpl-schema';
import { Random } from 'meteor/random';
const ItemConsumedSchema = new SimpleSchema({
_id: {
type: String,
regEx: SimpleSchema.RegEx.Id,
autoValue(){
if (!this.isSet) return Random.id();
}
},
tag: {
type: String,
optional: true,
},
quantity: {
type: Number,
defaultValue: 1,
},
});
export default ItemConsumedSchema;

View File

@@ -0,0 +1,22 @@
import SimpleSchema from 'simpl-schema';
import ItemConsumedSchema from '/imports/api/properties/subSchemas/ItemConsumedSchema.js';
import AttributeConsumedSchema from '/imports/api/properties/subSchemas/AttributeConsumedSchema.js';
const ResourcesSchema = new SimpleSchema({
itemsConsumed: {
type: Array,
defaultValue: [],
},
'itemsConsumed.$': {
type: ItemConsumedSchema,
},
attributesConsumed: {
type: Array,
defaultValue: [],
},
'attributesConsumed.$': {
type: AttributeConsumedSchema,
},
});
export default ResourcesSchema;

View File

@@ -0,0 +1,36 @@
import SimpleSchema from 'simpl-schema';
import AdjustmentSchema from '/imports/api/properties/subSchemas/AdjustmentSchema.js';
import DamageSchema from '/imports/api/properties/subSchemas/DamageSchema.js';
import { StoredBuffWithIdSchema } from '/imports/api/properties/Buffs.js';
let ResultsSchema = new SimpleSchema({
// Adjustments applied when taking this action
// Ideally, if these adjustments can't be made, the action should be unusable
adjustments: {
type: Array,
defaultValue: [],
},
'adjustments.$': {
type: AdjustmentSchema,
},
// Damage is done to hitpoints or hitpoint-like stats
// has a damage type, can be mitigated by resistances, etc.
damages: {
type: Array,
defaultValue: [],
},
'damages.$': {
type: DamageSchema,
},
// Buffs applied when taking this action
buffs: {
type: Array,
defaultValue: [],
},
'buffs.$': {
type: StoredBuffWithIdSchema,
},
});
export default ResultsSchema;

View File

@@ -0,0 +1,25 @@
import SimpleSchema from 'simpl-schema';
import ResultsSchema from '/imports/api/properties/subSchemas/ResultsSchema.js';
let RollResultsSchema = new SimpleSchema ({
_id: {
type: String,
regEx: SimpleSchema.RegEx.Id,
autoValue(){
if (!this.isSet) return Random.id();
}
},
// Expression of whether or not to apply the roll
// Evaluates to an expression which gets compared to the roll
// or a number which the roll must equal
comparison: {
type: String,
optional: true,
},
results: {
type: ResultsSchema,
defaultValue: {},
},
});
export default RollResultsSchema ;

View File

@@ -0,0 +1,37 @@
import SimpleSchema from 'simpl-schema';
import '/imports/api/sharing/sharing.js';
let SharingSchema = new SimpleSchema({
owner: {
type: String,
regEx: SimpleSchema.RegEx.Id,
index: 1
},
readers: {
type: Array,
defaultValue: [],
index: 1,
max: 50,
},
"readers.$": {
type: String,
regEx: SimpleSchema.RegEx.Id
},
writers: {
type: Array,
defaultValue: [],
index: 1,
max: 20,
},
"writers.$": {
type: String,
regEx: SimpleSchema.RegEx.Id
},
public: {
type: Boolean,
defaultValue: false,
index: 1,
},
});
export default SharingSchema;

View File

@@ -0,0 +1,20 @@
import SimpleSchema from 'simpl-schema';
import { assertOwnership } from '/imports/api/sharing/sharingPermissions.js';
import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
import getCollectionByName from '/imports/api/parenting/getCollectionByName.js';
import { RefSchema } from '/imports/api/parenting/ChildSchema.js';
const setPublic = new ValidatedMethod({
name: 'sharing.methods.setPublic',
validate: new SimpleSchema({
docRef: RefSchema,
public: { type: Boolean },
}).validator(),
run({docRef, public}){
let doc = fetchDocByRef(docRef);
assertOwnership(doc, this.userId);
getCollectionByName(docRef.collection).update(docRef.id, {$set: {public}});
},
});
export { setPublic };

View File

@@ -0,0 +1,91 @@
import { _ } from 'meteor/underscore';
import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
function assertIdValid(userId){
if (!userId || typeof userId !== 'string'){
throw new Meteor.Error("Permission denied",
"No user ID given for edit permission check");
}
}
function assertdocExists(doc){
if (!doc){
throw new Meteor.Error("Permission denied",
`No such document exists`);
}
}
export function assertOwnership(doc, userId){
assertIdValid(userId);
assertdocExists(doc);
if (doc.owner === userId ){
return true;
} else {
throw new Meteor.Error("Permission denied",
`You are not the owner of this document`);
}
}
/**
* Assert that the user can edit the root document which manages its own sharing
* permissions.
*
* Warning: the doc and userId must be set by a trusted source
*/
export function assertEditPermission(doc, userId) {
assertIdValid(userId);
assertdocExists(doc);
if (doc.owner === userId || _.contains(doc.writers, userId)){
return true;
} else {
throw new Meteor.Error("Edit permission denied",
`You do not have permission to edit this document`);
}
}
function getRoot(doc){
assertdocExists(doc);
if (doc.ancestors && doc.ancestors.length && doc.ancestors[0]){
return fetchDocByRef(doc.ancestors[0]);
} else {
return doc;
}
}
/**
* Assert that the user can edit a descendant document whose root ancestor
* implements sharing permissions.
*
* Warning: the doc and userId must be set by a trusted source
*/
export function assertDocEditPermission(doc, userId){
let root = getRoot(doc);
assertEditPermission(root, userId);
}
export function assertViewPermission(doc, userId) {
assertIdValid(userId);
assertdocExists(doc);
if (
doc.owner === userId ||
doc.public ||
_.contains(doc.readers, userId) ||
_.contains(doc.writers, userId)
){
return true;
} else {
throw new Meteor.Error("View permission denied",
`You do not have permission to view this character`);
}
}
/**
* Assert that the user can view a descendant document whose root ancestor
* implements sharing permissions.
*
* Warning: the doc and userId must be set by a trusted source
*/
export function assertDocViewPermission(doc, userId){
let root = getRoot(doc);
assertViewPermission(root, userId);
}

View File

@@ -0,0 +1,153 @@
import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { getUserTier } from '/imports/api/users/patreon/tiers.js';
let Invites= new Mongo.Collection('invites');
let InviteSchema = new SimpleSchema({
inviter: {
type: String,
regEx: SimpleSchema.RegEx.Id,
index: 1,
},
invitee: {
type: String,
regEx: SimpleSchema.RegEx.Id,
optional: true,
index: 1,
},
inviteToken: {
type: String,
optional: true,
index: 1,
unique: 1,
},
isFunded: {
type: Boolean,
},
isRedundant: {
type: Boolean,
optional: true,
},
// The timestamp of when the invitee was confirmed
// Older invites have priority over newer ones
dateConfirmed: {
type: Date,
optional: true,
},
});
if (Meteor.isServer){
Accounts.onLogin(function({user}){
alignInvitesWithPatreonTier(user);
});
}
function alignInvitesWithPatreonTier(user){
const tier = getUserTier(user);
let availableInvites = tier.invites;
let currentlyFundedInvites = [];
let currenltyUnfundedInvites = [];
Invites.find({
inviter: user._id
}).forEach(invite => {
if (invite.isFunded){
currentlyFundedInvites.push(invite);
} else {
currenltyUnfundedInvites.push(invite);
}
});
// Return early if no work needs doing to skip sorting
if (currentlyFundedInvites.length === availableInvites) return;
// Sort the invites by date forwards and backwards
currentlyFundedInvites.sort((a, b) => a.dateConfirmed - b.dateConfirmed);
currenltyUnfundedInvites.sort((a, b) => b.dateConfirmed - a.dateConfirmed);
// Defund or delete excess invites
while (currentlyFundedInvites.length > availableInvites){
let inviteToDefund = currentlyFundedInvites.pop();
if (inviteToDefund.invitee){
Invites.update(inviteToDefund._id, {$set: {isFunded: false}});
} else {
Invites.remove(inviteToDefund._id);
}
}
// Fund unfunded invites or insert new ones
while (currentlyFundedInvites.length < availableInvites){
if (currenltyUnfundedInvites.length){
let inviteToFund = currenltyUnfundedInvites.pop();
currentlyFundedInvites.push(inviteToFund);
Invites.update(inviteToFund._id, {$set: {isFunded: true}});
} else {
let inviteId = Invites.insert({inviter: user._id, isFunded: true});
currentlyFundedInvites.push({_id: inviteId});
}
}
}
const getInviteToken = new ValidatedMethod({
name: 'Invites.methods.getToken',
validate: new SimpleSchema({
inviteId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
}).validator(),
run({inviteId}) {
let invite = Invites.findOne(inviteId);
if (this.userId !== invite.inviter) {
throw new Meteor.Error('Invites.methods.getToken.denied',
'You need to be the inviter of the invite to create a token');
}
if (invite.inviteToken){
return invite.inviteToken;
} else {
let inviteToken = Random.id(5);
Invites.update(inviteId, {$set: {inviteToken}})
return inviteToken;
}
},
});
const acceptInviteToken = new ValidatedMethod({
name: 'Invites.methods.acceptToken',
validate: new SimpleSchema({
inviteToken: {
type: String,
},
}).validator(),
run({inviteToken}) {
if (this.userId) {
throw new Meteor.Error('Invites.methods.acceptToken.denied',
'You need to be the logged in to accept a token');
}
let invite = Invites.findOne({inviteToken});
if (!invite){
throw new Meteor.Error('Invites.methods.acceptToken.notFound',
'No invite could be found for this token');
}
// If the invitee is already filled, fix unexpected case by deleting the token
if (invite.invitee){
Invites.update(invite._id, {
$unset: {inviteToken: 1}
});
throw new Meteor.Error('Invites.methods.acceptToken.alreadyAccepted',
'This invite already has an invitee, and shouldn\'t have a token');
}
if (this.userId === invite.inviter){
throw new Meteor.Error('Invites.methods.acceptToken.ownToken',
'You can\'t accept your own invite');
}
Invites.update(invite._id, {
$set: {invitee: this.userId},
$unset: {inviteToken: 1},
});
},
});
Invites.attachSchema(InviteSchema);
export default Invites;
export { alignInvitesWithPatreonTier, getInviteToken, acceptInviteToken };

View File

@@ -0,0 +1,143 @@
import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
const userSchema = new SimpleSchema({
username: {
type: String,
optional: true,
max: 30,
min: 4,
},
emails: {
type: Array,
optional: true,
},
'emails.$': {
type: Object,
},
'emails.$.address': {
type: String,
regEx: SimpleSchema.RegEx.Email,
},
'emails.$.verified': {
type: Boolean,
},
registered_emails: {
type: Array,
optional: true,
},
'registered_emails.$': {
type: Object,
blackbox: true,
},
createdAt: {
type: Date
},
services: {
type: Object,
optional: true,
blackbox: true,
},
roles: {
type: Array,
optional: true,
},
'roles.$': {
type: String
},
// In order to avoid an 'Exception in setInterval callback' from Meteor
heartbeat: {
type: Date,
optional: true,
},
apiKey: {
type: String,
index: 1,
optional: true,
},
darkMode: {
type: Boolean,
optional: true,
},
subscribedLibraries: {
type: Array,
defaultValue: [],
max: 100,
},
'subscribedLibraries.$': {
type: String,
},
});
Meteor.users.attachSchema(userSchema);
Meteor.users.generateApiKey = new ValidatedMethod({
name: 'Users.methods.generateApiKey',
validate: null,
run(){
if(Meteor.isClient) return;
var user = Meteor.users.findOne(this.userId);
if (!user) return;
if (user && user.apiKey) return;
var apiKey = Random.id(30);
Meteor.users.update(this.userId, {$set: {apiKey}});
},
});
Meteor.users.setDarkMode = new ValidatedMethod({
name: 'Users.methods.setDarkMode',
validate: new SimpleSchema({
darkMode: { type: Boolean },
}).validator(),
run({darkMode}){
if (!this.userId) return;
Meteor.users.update(this.userId, {$set: {darkMode}});
},
});
Meteor.users.sendVerificationEmail = new ValidatedMethod({
name: 'Users.methods.sendVerificationEmail',
validate: new SimpleSchema({
userId:{
type: String,
optional: true,
},
address: {
type: String,
},
}).validator(),
run({userId, address}){
userId = this.userId || userId;
let user = Meteor.users.findOne(userId);
if (!user) {
throw new Meteor.Error('User not found',
'Can\'t send a validation email to a user that does not exist');
}
if (!_.some(user.emails, email => email.address === address)) {
throw new Meteor.Error('Email address not found',
'The specified email address wasn\'t found on this user account');
}
Accounts.sendVerificationEmail(userId, address);
}
});
Meteor.users.isAdmin = function(userId){
userId = this.userId || userId;
let user = Meteor.users.findOne(userId);
return user && user.roles.includes('admin');
}
Meteor.users.findUserByUsernameOrEmail = new ValidatedMethod({
name: 'Users.methods.findUserByUsernameOrEmail',
validate: new SimpleSchema({
usernameOrEmail:{
type: String,
},
}).validator(),
run({usernameOrEmail}){
if (Meteor.isClient) return;
let user = Accounts.findUserByUsername(usernameOrEmail) ||
Accounts.findUserByEmail(usernameOrEmail);
return user && user._id;
}
});

View File

@@ -0,0 +1,20 @@
// Adds accounts-patreon support to bozhao:link-accounts
import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';
export default function linkWithPatreon(options, callback) {
if (!Meteor.userId()) {
throw new Meteor.Error(402, 'Please login to an existing account before link.');
}
if (!Package['patreon-oauth']) {
throw new Meteor.Error(403, 'Please include patreon-oauth package');
}
if (!callback && typeof options === 'function') {
callback = options;
options = null;
}
const credentialRequestCompleteCallback = Accounts.oauth.linkCredentialRequestCompleteHandler(callback);
Package['patreon-oauth'].Patreon.requestCredential(options, credentialRequestCompleteCallback);
}

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