Compare commits

...

269 Commits

Author SHA1 Message Date
Stefan Zermatten
df361236f5 Hotfix: Containers total weight now showing correctly on inventory tab 2021-02-24 15:18:32 +02:00
Stefan Zermatten
e1d670fe9f Fixed: buffs 2021-02-24 15:05:53 +02:00
Stefan Zermatten
1e9f0515e5 Contents that are weightless are now summed and stored on the container 2021-02-24 14:22:52 +02:00
Stefan Zermatten
0404020335 Added weights and content weight to containers UI 2021-02-24 14:07:20 +02:00
Stefan Zermatten
c248d8f4a0 Weight carried, Net worth, and Attunement implemented and exposed in UI 2021-02-24 13:41:30 +02:00
Stefan Zermatten
8d95da8b7a Fixed a bug where certain base values would be strings instead of numbers in effect aggregators 2021-02-24 11:58:04 +02:00
Stefan Zermatten
e11ab39864 Added tableLookup function 2021-02-24 11:57:40 +02:00
Stefan Zermatten
331fcef9ad Fixed: Error message when focus grabbing element is missing on form 2021-02-24 10:06:25 +02:00
Stefan Zermatten
7e3bff9677 Show creature milestone level and xp if creature has both 2021-02-24 10:05:11 +02:00
Stefan Zermatten
1b650b26b6 Fixed: using creature stats like XP in calculations 2021-02-24 10:01:02 +02:00
Stefan Zermatten
5925605962 Fixed property edit buttons no longer get pushed by long property name 2021-02-24 09:52:51 +02:00
Stefan Zermatten
dee1265b69 Fixed: Inline calculations in libarries now display as expected 2021-02-24 09:46:52 +02:00
Stefan Zermatten
3d3ec3bcf2 Increaed number of slot fillers loaded by the slot fill dialog to 20 2021-02-24 09:23:55 +02:00
Stefan Zermatten
dce2c92516 Added attack roll bonus and dc to spell list. Use them in spells with #spellList.dcResult and #spellList.attackRollBonusResult 2021-02-23 15:21:20 +02:00
Stefan Zermatten
0fe2780983 Added property viewer for Toggle properties 2021-02-23 15:07:07 +02:00
Stefan Zermatten
e126cdd3cb Added property viewer for slot filler 2021-02-23 14:59:53 +02:00
Stefan Zermatten
d69ada0db4 Slot quantity is now a computed value, added property viewer for slots 2021-02-23 14:53:47 +02:00
Stefan Zermatten
858915b25b Added viewer for Saving Throw properties 2021-02-23 14:38:20 +02:00
Stefan Zermatten
d10a7eca14 Added viewer for Roll properties 2021-02-23 14:29:48 +02:00
Stefan Zermatten
671d17018c Added a viewer for Constant properties 2021-02-23 14:23:00 +02:00
Stefan Zermatten
f2883d320f Improved Attribute damage viewer 2021-02-23 13:59:26 +02:00
Stefan Zermatten
aad0c7249e Removed stray log to console 2021-02-23 12:47:34 +02:00
Stefan Zermatten
612fcca68c Only split properties accross targets if there are targets 2021-02-22 14:30:50 +02:00
Stefan Zermatten
12939c46de made saves walk children when not targeted at self 2021-02-22 14:28:38 +02:00
Stefan Zermatten
3801b17fde Attacks can now critical hit. criticalHitTarget overrides the roll required 2021-02-22 14:07:12 +02:00
Stefan Zermatten
88133a2fa3 Saving throws now work in actions 2021-02-22 12:38:21 +02:00
Stefan Zermatten
d00eedac19 Rolls now work in actions 2021-02-22 11:55:08 +02:00
Stefan Zermatten
6571fb860a Toggles now work in actions to make choices based on action context 2021-02-22 11:36:30 +02:00
Stefan Zermatten
8148f4d701 Fixed: Library nodes are published in order to prevent disordered building of the tree 2021-02-21 17:05:09 +02:00
Stefan Zermatten
523c34b719 Fixed: Slots that use conditions now only hide on falsey value (false, 0, '') 2021-02-21 17:01:24 +02:00
Stefan Zermatten
e833fba870 Fixed: bug that stopped buffs being deleted 2021-02-21 16:35:35 +02:00
Stefan Zermatten
f3e191c12e Fixed: Inserting properties to the tree now animate correctly to the inserted property 2021-02-20 16:00:40 +02:00
Stefan Zermatten
33415275a3 Item tiles are now smaller in the inventory view 2021-02-20 15:53:58 +02:00
Stefan Zermatten
3b1151d987 Fixed notes without summaries are no longer oversized 2021-02-20 15:52:15 +02:00
Stefan Zermatten
4288f98f7c Fixed: stats with no ability selected have an ability modifier of 0 instead of NaN 2021-02-20 15:50:25 +02:00
Stefan Zermatten
1a2ef8a4a2 Fixed: markdown images no longer overflow their container width 2021-02-20 15:45:45 +02:00
Stefan Zermatten
10e9a5faa8 Notes now show both summary and description in viewer 2021-02-20 15:41:30 +02:00
Stefan Zermatten
53594c0004 Fixed: items in containers not following tree order 2021-02-20 15:38:51 +02:00
Stefan Zermatten
e068675b46 Fixed and improved: Discord webhooks are working again with a new format 2021-02-20 15:27:20 +02:00
Stefan Zermatten
067f5df36e Fixed: Emptying the search bar in slot filler dialog now correctly clears the search 2021-02-20 10:17:35 +02:00
Stefan Zermatten
6113d86059 Fixed typo in calling a function in Constants autovalue 2021-02-16 11:19:50 +02:00
Stefan Zermatten
e3862bcdd9 Fixed constants autovalue 2021-02-16 10:49:18 +02:00
Stefan Zermatten
299f5a06dd Moved inline cacultion regex to a constant to ensure constistency 2021-02-16 10:14:26 +02:00
Stefan Zermatten
2776850311 Fixed missing import 2021-02-12 11:45:32 +02:00
Stefan Zermatten
3e6221309e Notes now have a summary and a description, some data migration my be needed 2021-02-12 11:44:03 +02:00
Stefan Zermatten
a078ce3d5d Description fields should now show calculation errors 2021-02-12 11:43:13 +02:00
Stefan Zermatten
b9e0475d07 Markdown now follow Github Flavor Markdown, including single line breaks 2021-02-12 11:11:12 +02:00
Stefan Zermatten
2b345c1f77 Improved error handling for most calculations 2021-02-12 11:00:44 +02:00
Stefan Zermatten
fed87f0a84 Fixed: items and spells should no longer be draggable when you don't have edit permission 2021-02-12 09:41:24 +02:00
Stefan Zermatten
b116be1238 Fixed flickering when inserting properties from library by ensuring consistent ID generation 2021-02-12 00:43:56 +02:00
Stefan Zermatten
ae373330ab Improved slot fill dialog UI, abandoned column layout in favour of wrapping flex rows 2021-02-12 00:18:29 +02:00
Stefan Zermatten
dcb535c84e Moved slot filler search to server side, limited docs in subscription 2021-02-11 22:09:43 +02:00
Stefan Zermatten
8c477ad4b1 Fixed constants not being found when used as the only thing in a calculation 2021-02-11 18:01:30 +02:00
Stefan Zermatten
eb2dd3bba1 Fixed computation dependency aggregation broken by refactoring 2021-02-11 16:27:22 +02:00
Stefan Zermatten
dc4808c70a fixed snackbars being blank 2021-02-11 16:20:33 +02:00
Stefan Zermatten
16c8c1db81 Improved descriptions in log entries 2021-02-11 16:16:22 +02:00
Stefan Zermatten
92a5c1e6c3 Improved log entries for actions 2021-02-11 16:08:31 +02:00
Stefan Zermatten
439eadf079 Condensed logs to a single card per action 2021-02-11 15:48:23 +02:00
Stefan Zermatten
d7083cf242 Hid unused cards on the stats tab 2021-02-11 13:48:10 +02:00
Stefan Zermatten
3af5e820ca Stopped inactive effects showing up in attributes 2021-02-11 13:38:34 +02:00
Stefan Zermatten
20aaab4dea Hid inactive notes 2021-02-11 13:14:31 +02:00
Stefan Zermatten
81cdf282ea Stopped inactive properties from showing computed inline fields, since they are not recomputed while inactive 2021-02-11 13:12:35 +02:00
Stefan Zermatten
3313ed0297 Added constants to the UI and Computation Engine 2021-02-11 13:03:31 +02:00
Stefan Zermatten
25fd5c18e8 Fixed casting broken by refactoring 2021-02-11 10:13:35 +02:00
Stefan Zermatten
5b9bb6e4bc Fixed spending resources for actions broken by refactoring 2021-02-11 10:13:23 +02:00
Stefan Zermatten
74370f6fec Performance optimization: Removed creature document from injection to prevent uneccessary Vue re-rendering 2021-02-11 10:04:28 +02:00
Stefan Zermatten
565ddccba6 fixed broken import 2021-02-04 16:55:50 +02:00
Stefan Zermatten
4ea4348a02 Now writing partial recalculations to creature variables 2021-02-04 16:52:26 +02:00
Stefan Zermatten
280f30dab5 Improved dependencies-only recalculations and fixed many calculation bugs 2021-02-04 16:16:51 +02:00
Stefan Zermatten
6d1e3f078c Optimised when certain recompute functions are called to prevent unccessary work 2021-02-04 13:59:08 +02:00
Stefan Zermatten
326d1bd165 Refactored computation into folders 2021-02-04 13:33:20 +02:00
Stefan Zermatten
87fa941f63 Merge branch 'version-2' of https://github.com/ThaumRystra/DiceCloud into version-2 2021-02-04 11:38:55 +02:00
Stefan Zermatten
449a4fba7d Refactored creature property methods into separate documents, might have broken a lot of things 2021-02-04 11:38:29 +02:00
Stefan Zermatten
9ff096ec0f Added modulo operator 2021-02-03 09:33:00 +02:00
Stefan Zermatten
f9f0186d95 Fixed error where parser was not creating accessor nodes correctly 2021-02-03 00:39:11 +02:00
Stefan Zermatten
60ea545ee9 Started implementing constant property 2021-02-02 16:36:23 +02:00
Stefan Zermatten
aaa5d0b63b Allowed effects and calculations to target nearest ancestors of #type 2021-02-02 16:11:59 +02:00
Stefan Zermatten
69c72e0987 Fixed parenthesis being discarded from compiled calculations in cases where they should not be 2021-02-02 15:13:49 +02:00
Stefan Zermatten
a6df4df534 Fixed some fields not storing strings when compiling calculations 2021-02-02 15:07:31 +02:00
Stefan Zermatten
8b8f9bea6f Removed character variables from creatures injected into character sheet as context 2021-01-31 20:13:45 +02:00
Stefan Zermatten
e7a27e4b83 Fixed hit dice breaking long rests 2021-01-31 19:45:14 +02:00
Stefan Zermatten
e14e875c42 Fixed conMod not being signed in hit dice tiles 2021-01-31 19:43:09 +02:00
Stefan Zermatten
a7898bfe4b Fixed props not having a default array for embedded calculations 2021-01-31 19:42:49 +02:00
Stefan Zermatten
aee899e181 Removed all UI computations from viewers and components 2021-01-31 18:42:17 +02:00
Stefan Zermatten
a5284bf6e8 Made 'always prepared' spells show up in casting list 2021-01-30 11:22:29 +02:00
Stefan Zermatten
0ea3f7a975 Migrated fields to embedded property calculations 2021-01-29 13:04:53 +02:00
Stefan Zermatten
1167538977 Denormalised inline calculations to property documents, needs to be referenced by UI still 2021-01-29 12:29:01 +02:00
Stefan Zermatten
9c799e3dc9 Added search to slot fill dialog 2021-01-28 17:09:37 +02:00
Stefan Zermatten
2bf749c4f1 Fixed: Layout of slot fill dialog is no longer broken by tall images, background is now the correct color 2021-01-28 16:48:33 +02:00
Stefan Zermatten
db83d5f82a Fix: Tabs and tab headers no longer lose sync when changing characters 2021-01-28 16:17:03 +02:00
Stefan Zermatten
d596061fa8 Fixed: Attributes that aren't ability scores now have their modifiers removed correctly 2021-01-28 16:05:47 +02:00
Stefan Zermatten
78efe639ed Fixed: Zero effect passive bonus now causes passive skill value to show. 2021-01-28 16:01:58 +02:00
Stefan Zermatten
e5bde38745 Removed console timers from recompute function: it's not the primary slowdown 2021-01-28 15:00:36 +02:00
Stefan Zermatten
83f2047dbe Replaced expensive getActiveProperties with cheaper filter by inactive field 2021-01-28 14:29:10 +02:00
Stefan Zermatten
fc03097ed8 Patreon access is now given by tier instead of by price paid 2021-01-28 12:15:45 +02:00
Stefan Zermatten
252ac23391 Merge branch 'version-2' of https://github.com/ThaumRystra/DiceCloud into version-2 2021-01-27 22:25:19 +02:00
Stefan Zermatten
531ddce6a0 Added dependency tracking to computations for future optimization effort 2021-01-27 22:24:28 +02:00
Stefan Zermatten
3cdeb73c30 Stopped removed items from showing up in inventory containers 2021-01-25 12:38:46 +02:00
Stefan Zermatten
c780c11e3f Improved spell slot casting dialog with search, filters, spell details 2021-01-22 14:09:23 +02:00
Stefan Zermatten
100c93b5ae Stopped dialog animation mocking transparent elements' background color 2021-01-22 12:20:33 +02:00
Stefan Zermatten
a4e6dd1d66 Added spellcasting to the stats page, click the icon next to a spell slot to cast 2021-01-19 16:10:34 +02:00
Stefan Zermatten
1b3b6362f7 Exposed spellSlotLevelValue to creature variables store 2021-01-19 12:07:40 +02:00
Stefan Zermatten
bdf4074e3c Added first pass at denormalizing inventory data, needs testing and integration 2021-01-15 17:41:06 +02:00
Stefan Zermatten
9492b2d8b8 Characters now start with a slot for a base. New characters start on the character tab with the build dialog open. 2021-01-14 12:28:51 +02:00
Stefan Zermatten
27f1f4e720 Fixed a bug with dialog animations hiding buttons by failing to clean up 2021-01-12 13:51:36 +02:00
Stefan Zermatten
d63b8c835d Fixed bug in last release where unlimited slots were always hidden on hide when full 2021-01-12 13:51:11 +02:00
Stefan Zermatten
85f3881935 Removed alert about data loss, most data structures are stable and it's being alarmist 2021-01-12 13:40:02 +02:00
Stefan Zermatten
5e4299e6db removed stray console logging 2021-01-12 13:24:08 +02:00
Stefan Zermatten
0e663f36db Slot fillers that count as more than one slot now update the space left correctly.
Slot fillers removed from a library can no longer be added to a slot.
If a slot has limited space left, this will be reflected on the error 
finding slots message
2021-01-12 13:22:48 +02:00
Stefan Zermatten
de9ea5922c Fixed bugs when a spell list does not have limit on prepared spells 2021-01-12 13:10:36 +02:00
Stefan Zermatten
a2cfe43e74 Disabled the direct damage input in the attribute form: it can't be edited anyway 2021-01-12 13:04:25 +02:00
Stefan Zermatten
026c11c13b Made sure attributes show their currentValue instead of Value 2021-01-12 13:03:33 +02:00
Stefan Zermatten
403f2663c2 Fixed bugs with item display, equipment will now automatically move to the first property with the 'equipment' tag, carried items will move to the first property with the 'carried' tag 2021-01-12 12:54:02 +02:00
Stefan Zermatten
28c042343e All children of infinite slots will now hide when "hide when full" is active 2021-01-12 10:36:07 +02:00
Stefan Zermatten
3f116875a1 Children of slots now display in the correct order 2021-01-12 10:35:37 +02:00
Stefan Zermatten
ae5b4b7d5c Made inactive toggle decendants specifically included when recomputing active properties 2021-01-11 22:03:54 +02:00
Stefan Zermatten
75835d74f6 Merge pull request #259 from Ganonsmasher/version-2
Found a fix for toggles.
2020-12-17 17:19:54 +02:00
Ganonsmasher
583b652fc4 Found a fix for toggles.
It doesn't fix the problem of their property being absent on the stats page cards, but they can compute properly and disable their children with this.
2020-12-15 18:08:29 -05:00
Stefan Zermatten
df317a8942 Merge branch 'version-2' of https://github.com/ThaumRystra/DiceCloud into version-2 2020-11-20 14:21:52 +02:00
Stefan Zermatten
ff0e9b1ff9 Added Lord of Junk to Patreon Paragons 2020-11-20 14:13:47 +02:00
Stefan Zermatten
fa24430a7f Fixed parsing of variable names with numbers and stacked dice rolls like dd8-> 1d(1d8) 2020-11-13 10:04:37 +02:00
Stefan Zermatten
fde2f821e7 Fixed parser not handling whitespace 2020-11-12 21:44:08 +02:00
Stefan Zermatten
827430c987 Fixed edit permission errors for some creature prop methods 2020-11-12 21:25:48 +02:00
Stefan Zermatten
2a1aa02e97 Added true and false keywords, fixed grammar ambiguity in if statements 2020-11-12 15:11:24 +02:00
Stefan Zermatten
aeb347084f Fixed ambiguitiy in grammar caused by previous fixes 2020-11-12 13:47:10 +02:00
Stefan Zermatten
005bc162cb Fixed != not being matched because ! was matched first. Fixed presedence for & | and relational operators 2020-11-12 13:45:14 +02:00
Stefan Zermatten
9941d91bb8 Fixed != operator, separated == and ===, != and !== for strictness control 2020-11-12 13:44:01 +02:00
Stefan Zermatten
23c77690a1 Healing damage type now heals instead of damaging 2020-11-12 13:01:09 +02:00
Stefan Zermatten
525b528d9a Added attribute damage and self damage results to actions and log. 2020-11-12 12:57:48 +02:00
Stefan Zermatten
3917f63d5e Increased subscription rate limit to prevent infinitely spinning characters 2020-11-12 11:04:04 +02:00
Stefan Zermatten
bd056ab042 Improved subscription permissions, should now work as expected for public documents 2020-11-12 10:48:46 +02:00
Stefan Zermatten
cd84b2562a Fixed an error with finding deployed version SHA 2020-11-10 14:48:09 +02:00
Stefan Zermatten
c7436ffb1e Caught error where git couldn't be used to get version 2020-11-10 14:37:32 +02:00
Stefan Zermatten
be7d7f898f disabled short and log rest buttons if user has no edit permission 2020-11-10 14:22:09 +02:00
Stefan Zermatten
3024168e95 Replaced old parser with new parser 2020-11-10 14:07:22 +02:00
Stefan Zermatten
1f0678b50b Added not operator to the parser 2020-11-05 15:32:01 +02:00
Stefan Zermatten
4dad2c41e5 Updated parser to accept underscores in variable names 2020-11-05 14:50:44 +02:00
Stefan Zermatten
46385dd9b2 Build details with no slots hidden moved to a dialog 2020-11-05 14:27:01 +02:00
Stefan Zermatten
7cb65954b5 Added the ability to hide slots when full 2020-11-05 14:05:17 +02:00
Stefan Zermatten
749799d869 Denormalised slot fill total to database 2020-11-05 13:35:55 +02:00
Stefan Zermatten
3293dad671 Limited what fields are included when fetching recompute documents to improve performance 2020-11-05 12:59:48 +02:00
Stefan Zermatten
88df942c59 Fixed an error with missing identity details in patreon request 2020-11-05 12:59:26 +02:00
Stefan Zermatten
9722bbc667 Characters now recompute on subscribe if they haven't been computed in the current version 2020-11-04 14:27:31 +02:00
Stefan Zermatten
2fb0ba79c6 began work to get inactive state of properties denormalised 2020-11-03 15:57:14 +02:00
Stefan Zermatten
3f7ddd62fc Merge branch 'version-2' of https://github.com/ThaumRystra/DiceCloud1 into version-2 2020-10-27 10:41:06 +02:00
Stefan Zermatten
227d6c5aae Markdown and calculations now supported in slot filler descriptions 2020-10-27 10:39:20 +02:00
Stefan Zermatten
147ef97576 Markdown now supported in slot filler descriptions 2020-10-27 10:34:01 +02:00
Stefan Zermatten
54806b0f3c Merge branch 'version-2' of https://github.com/ThaumRystra/DiceCloud1 into version-2 2020-10-19 11:39:24 +02:00
Stefan Zermatten
1165158d46 Forced creatures to reorder their docs before recomputing 2020-10-19 11:39:21 +02:00
Stefan Zermatten
32de70cd45 Class levels now have a description field 2020-10-17 19:33:56 +02:00
Stefan Zermatten
68499e4de5 You can now click on properties filling slots to view their details dialog 2020-10-17 19:28:54 +02:00
Stefan Zermatten
3f4cb8e26b Added undo buttons for deleting properties off a creature 2020-10-17 19:10:37 +02:00
Stefan Zermatten
ebab41838c Used tree node views in slot fill selection 2020-10-17 16:56:23 +02:00
Stefan Zermatten
e8da7a6c17 Moved snackbars to their own store and component 2020-10-17 16:06:27 +02:00
Stefan Zermatten
46189c68df All property forms now allow tags 2020-10-17 13:42:24 +02:00
Stefan Zermatten
9fa997ed24 Started moving snackbars to vue store, still needs to be separated into its own module 2020-10-16 13:38:58 +02:00
Stefan Zermatten
e3bf6557ec fixed bug in dialog store 2020-10-16 10:35:51 +02:00
Stefan Zermatten
7aa3e5a217 Stringified errors from scheduled deletion 2020-10-16 10:23:34 +02:00
Stefan Zermatten
dc1b025090 Increased delete job frequency to 10 minutes 2020-10-16 10:00:07 +02:00
Stefan Zermatten
2e370a9884 Fixed removed slots not being hidden 2020-10-15 16:42:57 +02:00
Stefan Zermatten
384fa076f1 hotfix tags not filtering correctly 2020-10-15 16:12:32 +02:00
Stefan Zermatten
7922e30ddc Added tags to some properties. Added condition to class levels 2020-10-15 16:00:32 +02:00
Stefan Zermatten
1ba4f76763 Class levels can now have conditions 2020-10-15 15:57:19 +02:00
Stefan Zermatten
839f91c3b2 Fixed icons not going to dark mode when slot filling cards are selected 2020-10-15 15:30:11 +02:00
Stefan Zermatten
26567ce840 slot fill cards with pictures no longer get icons 2020-10-15 15:25:10 +02:00
Stefan Zermatten
2a729a4eca Made slot fill dialog a list of cards to leverage pretty slotFillers 2020-10-15 15:24:14 +02:00
Stefan Zermatten
ed17d9e2d2 Added slotfiller property type to increase control over slot filling 2020-10-15 14:54:58 +02:00
Stefan Zermatten
8e9405b5ad Allowed slots with unlimited children, improved slot ui text 2020-10-15 13:50:46 +02:00
Stefan Zermatten
7fc783dcad Removed recompute button 2020-10-15 13:50:14 +02:00
Stefan Zermatten
b15ad7e51a Merge branch 'version-2' of https://github.com/ThaumRystra/DiceCloud1 into version-2 2020-10-15 13:49:49 +02:00
Stefan Zermatten
8a3d2474fc Merge pull request #254 from JoeZwet/version-2
fix: prevent discord mention exploit
2020-10-15 13:48:30 +02:00
Joe van der Zwet
09371e7d54 add requested changes 2020-10-16 00:47:32 +13:00
Joe van der Zwet
0776d33909 fix: prevent discord mention exploit 2020-10-16 00:36:32 +13:00
Stefan Zermatten
6e98d71c3c Improved slot UI look and feel 2020-10-15 13:00:29 +02:00
Stefan Zermatten
8f89f4b63f Ensured all subscriptions return empty arrays instead of errors or ready 2020-10-15 12:34:46 +02:00
Stefan Zermatten
c0070d017e Removed debugging code 2020-10-14 15:54:16 +02:00
Stefan Zermatten
51569592ab First implementation on Slots UI 2020-10-14 14:45:26 +02:00
Stefan Zermatten
d2cb86ac27 Fixed broken logging for actions 2020-10-14 11:33:25 +02:00
Stefan Zermatten
bde9183158 Log optimistic UI now fixed, rolls are now instant 2020-10-14 11:25:05 +02:00
Stefan Zermatten
0cc9e01754 Renamed, moved LogTab to CharacterLog 2020-10-14 09:37:00 +02:00
Stefan Zermatten
9856471202 Stopped log making toast if it's visible 2020-10-14 09:33:17 +02:00
Stefan Zermatten
4f77782a7a log messages are now aligned right 2020-10-13 13:53:30 +02:00
Stefan Zermatten
5f13aaa031 Fixed empty strings in log input 2020-10-13 13:44:06 +02:00
Stefan Zermatten
1321cf6a96 Moved log tab to right drawer 2020-10-13 13:42:18 +02:00
Stefan Zermatten
dee8249f61 Creature logs are now removed with creatures 2020-10-13 12:43:55 +02:00
Stefan Zermatten
0af0afc0d0 Discord webhooks now mirror character log 2020-10-13 12:42:02 +02:00
Stefan Zermatten
a104fc8a87 Fixing broken casing on file pt.2 2020-10-06 10:55:12 +02:00
Stefan Zermatten
46f452987f Fixing broken casing on file 2020-10-06 10:54:52 +02:00
Stefan Zermatten
a87cb1286a Improved custom rolls on log tab 2020-10-06 09:53:08 +02:00
Stefan Zermatten
844588cdbf Started adding text input to log tab 2020-09-30 16:24:33 +02:00
Stefan Zermatten
a6a96fc19f Started work on character log for rolls to be stored 2020-09-29 22:34:30 +02:00
Stefan Zermatten
75ab43da00 Started work on UI for rolling checks 2020-09-29 16:37:28 +02:00
Stefan Zermatten
df7000889b fixed security deps 2020-09-29 10:54:37 +02:00
Stefan Zermatten
65754dea80 removed damage multipliers from health bar card, it has its own card 2020-09-29 10:54:27 +02:00
Stefan Zermatten
30cca39e7c Merge branch 'version-2' of https://github.com/ThaumRystra/DiceCloud1 into version-2 2020-09-28 13:58:03 +02:00
Stefan Zermatten
5ad5c914fb Added gitignore file for renders 2020-09-24 19:00:28 +02:00
Stefan Zermatten
f27550362a Fixed various parser bugs, implemented unary operators 2020-09-18 22:13:12 +02:00
Stefan Zermatten
50f7977a60 Fixed patreon update write location 2020-09-18 20:37:16 +02:00
Stefan Zermatten
bc5c465a32 Started work on checks 2020-09-18 14:00:29 +02:00
Stefan Zermatten
c8ddf9d547 Added the ability to double all number of dice to roll using context 2020-09-18 12:24:08 +02:00
Stefan Zermatten
6570665c1e Added functions and ensured the context was being passed around correctly 2020-09-18 11:52:44 +02:00
Stefan Zermatten
06f17a6d33 Parser now uses context to store details of the computation 2020-09-18 10:14:53 +02:00
Stefan Zermatten
b69ad6c306 Removed unused parser code 2020-09-10 11:39:27 +02:00
Stefan Zermatten
5dec760452 Parser now works with variables passed into scope 2020-09-10 11:38:28 +02:00
Stefan Zermatten
ede4e1367d Continued work on parser, now calling functions and rolling correctly 2020-09-10 00:14:24 +02:00
Stefan Zermatten
81645df2a6 Lots of work on the parser including testing interface 2020-09-09 17:09:50 +02:00
Stefan Zermatten
445171ce80 Added preferences subheader to accounts page 2020-09-09 13:58:48 +02:00
Stefan Zermatten
dedab7b046 Added patreon tier refresh button, autorefresh tier on login daily 2020-09-09 13:55:21 +02:00
Stefan Zermatten
a5c16ba83a Overhauled inventory tab again. Closer in functionality to V1 2020-08-22 00:36:17 +02:00
Stefan Zermatten
46501f2759 Spells that aren't prepared no longer count as active properties 2020-08-21 16:34:52 +02:00
Stefan Zermatten
a6ed1004be Added Resistance, Vulnerability, and Immunity to the health bar card 2020-08-21 16:27:01 +02:00
Stefan Zermatten
8539356b9e Added UI to prepare spells 2020-08-21 16:04:31 +02:00
Stefan Zermatten
93db5e9288 Made library link readonly instead of disabled so it can be copied 2020-08-10 04:23:55 +02:00
Stefan Zermatten
b4cb91a892 Multiple libraries can now be opened, allowing library items to be moved 2020-08-10 04:19:54 +02:00
Stefan Zermatten
9c93747845 Fixed bug where array accessors were attempting to use the substitution engine prematurely 2020-08-10 04:14:53 +02:00
Stefan Zermatten
a51154e434 Prevented test webhooks being sent in production 2020-07-26 19:56:08 +02:00
Stefan Zermatten
1bde0db0ba Fixed features showing up when disabled by an ancestor 2020-07-26 19:53:07 +02:00
Stefan Zermatten
bc001202ec Fixed spell list tiles not being opaque 2020-07-26 19:47:44 +02:00
Stefan Zermatten
c7985af83b Continued work on tabletops, hidden from main app and all methods disallowed for non-admins 2020-07-26 19:45:07 +02:00
Stefan Zermatten
0f20cd4bd9 Implemented drag and drop on spells page 2020-07-26 19:39:50 +02:00
Stefan Zermatten
1ac01941c7 Fixed bug where multiple classes woudn't show up in persona tab 2020-07-26 16:43:26 +02:00
Stefan Zermatten
95d8d2cb9a Started work on tabletop view 2020-07-17 23:31:12 +02:00
Stefan Zermatten
47345b3694 Experimenting with webhooks. 2020-07-13 16:38:24 +02:00
Stefan Zermatten
308168791b Made dX rolls work as 1dX 2020-06-30 15:15:55 +02:00
Stefan Zermatten
7be4280508 Began implementing dice rolls in the maths parser 2020-06-30 14:40:20 +02:00
Stefan Zermatten
56f9e82326 Improved spells tab to be more in line with the v1 implementation 2020-06-29 14:52:47 +02:00
Stefan Zermatten
6ddea8a8ab Improved slot schema, added ui for slots 2020-06-29 14:15:49 +02:00
Stefan Zermatten
e1ddfb2cab Fixed a bug where registering a property without disabledByToggle breaks recompuation 2020-06-24 18:20:56 +02:00
Stefan Zermatten
d36d5b15d0 hide unused spell card 2020-06-24 18:17:35 +02:00
Stefan Zermatten
2af687361e Improved spell appearance in spell tab 2020-06-23 01:36:48 +02:00
Stefan Zermatten
e572807082 Attributes of type spell slot now store their slot level 2020-06-23 01:36:21 +02:00
Stefan Zermatten
c44aeac198 Added 'prepared' field to spells 2020-06-22 13:45:47 +02:00
Stefan Zermatten
757cf5c34b Fixed spells not being able to be inserted or editing in characters 2020-06-22 13:45:35 +02:00
Stefan Zermatten
8cbfec25b3 Added the setting to swap ability scores and modifiers to the account page 2020-06-22 13:22:53 +02:00
Stefan Zermatten
c4dc5895aa Relaxed rate limiting on icon search, improved error messaging 2020-06-22 00:20:40 +02:00
Stefan Zermatten
cffe0ee574 Added minimal UI to display applied buffs 2020-06-22 00:14:07 +02:00
Stefan Zermatten
ce51be7b8e moved proficiencies after actions on the stats tab 2020-06-21 23:57:19 +02:00
Stefan Zermatten
315073bd8e Refactored actions and let actions apply buffs to self 2020-06-21 23:54:51 +02:00
Stefan Zermatten
50b99ef54f Improved performance of adding library properties with many decendants 2020-06-21 23:24:07 +02:00
Stefan Zermatten
9b01f5fb45 Improved actions UI, Actions (including spells) can now have icons 2020-06-17 13:23:13 +02:00
Stefan Zermatten
389785f5db Fixed bug where library large screen view won't scroll 2020-06-17 13:22:48 +02:00
Stefan Zermatten
e1bfb173ab Overhauled action detail view 2020-06-16 13:51:58 +02:00
Stefan Zermatten
ecba587253 Fixed a bug with proficiency forms not editing proficiency correctly 2020-06-16 12:35:50 +02:00
Stefan Zermatten
3f540d0f14 Overhaul of character action components, actions now consume resources 2020-06-15 22:30:27 +02:00
Stefan Zermatten
dc18734d1f Backend work to support actions consuming their resources on use 2020-06-13 23:11:49 +02:00
Stefan Zermatten
1535e00093 Denormalized some calculations into recomputation step 2020-06-07 21:08:53 +02:00
Stefan Zermatten
5198c655e9 Added subscription rate limiting 2020-06-06 14:30:15 +02:00
Stefan Zermatten
8d41643136 Increased damage property rate limit to 4/s 2020-06-06 14:25:23 +02:00
Stefan Zermatten
ea8d036c72 Added rate limiting to all methods 2020-06-06 14:23:13 +02:00
Stefan Zermatten
93d566e263 Exposed methods and publications to http requests, changed method names 2020-06-06 12:31:07 +02:00
Stefan Zermatten
b4da32f9ab Fixed soft removed documents never getting permanently removed 2020-06-05 23:08:31 +02:00
Stefan Zermatten
986fe8fd93 Added an autofocus field to most forms 2020-06-05 22:39:21 +02:00
Stefan Zermatten
dd4596851e Improved class level viewer and tree node view 2020-06-05 22:25:22 +02:00
Stefan Zermatten
bc3fc9574a Added loading and empty state to experience list 2020-06-05 22:20:40 +02:00
Stefan Zermatten
db1ae5db3d Iterated on XP system 2020-06-05 21:48:28 +02:00
Stefan Zermatten
d1e7eb2fa0 Added basic XP system 2020-06-05 16:14:26 +02:00
Stefan Zermatten
efb8b87a2d Alphabetized properties by displayed name 2020-05-31 22:39:15 +02:00
Stefan Zermatten
b04b915c7b Removed stray logging 2020-05-31 22:36:27 +02:00
Stefan Zermatten
21b823f85c Dark mode now with 20% more dark 2020-05-31 22:25:04 +02:00
Stefan Zermatten
4631579181 Character toolbar now correctly uses dark and light text where appropriate 2020-05-31 22:22:42 +02:00
Stefan Zermatten
edf3920e84 Character sheet toolbars now match the color of the character 2020-05-31 22:16:38 +02:00
Stefan Zermatten
fb91fd12df Set up custom icons for most properties 2020-05-31 21:03:45 +02:00
Stefan Zermatten
19f4735412 Icon search field now focuses when the menu is opened 2020-05-31 19:18:49 +02:00
Stefan Zermatten
fb2f1efa72 Property insert forms now have color selectors 2020-05-31 19:00:32 +02:00
Stefan Zermatten
f7ee09470e Improved container and item forms and viewers 2020-05-31 18:50:00 +02:00
Stefan Zermatten
a5c42fea19 Made custom svg icons work anywhere a regular icon would work 2020-05-31 18:49:46 +02:00
Stefan Zermatten
8f81614294 Weight and value are no longer required on containers 2020-05-31 15:59:37 +02:00
Stefan Zermatten
c56cebc652 Fixed dark/light font color swapping not working in dark mode 2020-05-31 15:58:21 +02:00
Stefan Zermatten
d24fb5661d re-enabled computation on client side for optimistic UI 2020-05-30 23:56:55 +02:00
349 changed files with 13425 additions and 3275 deletions

View File

@@ -3,7 +3,7 @@
# '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-password@1.6.2
accounts-ui@1.3.1
random@1.2.0
dburles:collection-helpers
@@ -13,7 +13,7 @@ momentjs:moment
dburles:mongo-collection-instances
percolate:migrations
accounts-google@1.3.3
email@1.2.3
email@2.0.0
meteorhacks:subs-manager
chuangbo:marked
meteor-base@1.4.0
@@ -38,7 +38,7 @@ ongoworks:speakingurl
service-configuration@1.0.11
google-config-ui@1.0.1
dynamic-import@0.5.2
ddp-rate-limiter@1.0.7
ddp-rate-limiter@1.0.9
rate-limit@1.0.9
meteortesting:mocha
mdg:validated-method
@@ -50,3 +50,7 @@ akryum:vue-component
accounts-patreon
bozhao:link-accounts
peerlibrary:reactive-publish
simple:rest
simple:rest-method-mixin
mikowals:batch-insert
peerlibrary:subscription-data

View File

@@ -1 +1 @@
METEOR@1.10.2
METEOR@1.11.1

View File

@@ -1,7 +1,7 @@
accounts-base@1.6.0
accounts-base@1.7.0
accounts-google@1.3.3
accounts-oauth@1.2.0
accounts-password@1.6.0
accounts-password@1.6.2
accounts-patreon@0.1.0
accounts-ui@1.3.1
accounts-ui-unstyled@1.4.2
@@ -10,7 +10,7 @@ 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:collection2@3.2.1
aldeed:schema-index@3.0.0
allow-deny@1.1.0
autoupdate@1.6.0
@@ -20,8 +20,8 @@ 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
boilerplate-generator@1.7.1
bozhao:link-accounts@2.2.1
caching-compiler@1.2.2
caching-html-compiler@1.1.3
callback-hook@1.3.0
@@ -34,17 +34,17 @@ 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
ddp-rate-limiter@1.0.9
ddp-server@2.3.2
deps@1.0.12
diff-sequence@1.1.1
dynamic-import@0.5.2
dynamic-import@0.5.3
ecmascript@0.14.3
ecmascript-runtime@0.7.0
ecmascript-runtime-client@0.10.0
ecmascript-runtime-server@0.9.0
ecmascript-runtime-client@0.11.0
ecmascript-runtime-server@0.10.0
ejson@1.1.1
email@1.2.3
email@2.0.0
es5-shim@4.8.0
fetch@0.1.1
geojson-utils@1.0.10
@@ -69,10 +69,11 @@ 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:browser-tests@1.3.4
meteortesting:mocha@1.1.5
meteortesting:mocha-core@7.0.1
minifier-css@1.5.0
mikowals:batch-insert@1.2.0
minifier-css@1.5.3
minifier-js@2.6.0
minimongo@1.6.0
mobile-experience@1.1.0
@@ -80,25 +81,29 @@ 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
momentjs:moment@2.29.1
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
npm-mongo@3.8.1
oauth@1.3.2
oauth2@1.3.0
observe-sequence@1.0.16
ongoworks:speakingurl@9.0.0
ordered-dict@1.1.0
patreon-oauth@0.1.0
peerlibrary:assert@0.3.0
peerlibrary:check-extension@0.7.0
peerlibrary:computed-field@0.10.0
peerlibrary:data-lookup@0.3.0
peerlibrary:extend-publish@0.6.0
peerlibrary:fiber-utils@0.10.0
peerlibrary:reactive-mongo@0.4.0
peerlibrary:reactive-publish@0.10.0
peerlibrary:server-autorun@0.8.0
peerlibrary:subscription-data@0.8.0
percolate:migrations@0.9.8
percolate:synced-cron@1.3.2
promise@0.11.2
@@ -110,12 +115,15 @@ reactive-var@1.0.11
reload@1.3.0
retry@1.1.0
routepolicy@1.1.0
seba:minifiers-autoprefixer@1.1.2
seba:minifiers-autoprefixer@1.2.1
service-configuration@1.0.11
session@1.2.0
sha@1.0.9
shell-server@0.5.0
socket-stream-client@0.3.0
simple:json-routes@2.1.0
simple:rest@1.1.1
simple:rest-method-mixin@1.0.1
socket-stream-client@0.3.1
spacebars@1.0.15
spacebars-compiler@1.1.3
srp@1.1.0
@@ -129,6 +137,6 @@ templating-tools@1.1.2
tmeasday:check-npm-versions@0.3.2
tracker@1.2.0
underscore@1.0.10
url@1.3.0
url@1.3.1
webapp@1.9.1
webapp-hashing@1.0.9

View File

@@ -1,3 +1,3 @@
import '/imports/ui/vueSetup.js';
import '/imports/ui/styles/stylesIndex.js';
import '/imports/ui/styles/stylesIndex.js';
import '/imports/client/config.js';

0
app/grammar.js Normal file
View File

View File

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

View File

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

View File

@@ -1,366 +0,0 @@
import { Meteor } from 'meteor/meteor';
import { Mongo } from 'meteor/mongo';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import SimpleSchema from 'simpl-schema';
import ColorSchema from '/imports/api/properties/subSchemas/ColorSchema.js';
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';
import { storedIconsSchema } from '/imports/api/icons/Icons.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,
},
icon: {
type: storedIconsSchema,
optional: true,
}
});
for (let key in propertySchemasIndex){
let schema = new SimpleSchema({});
schema.extend(propertySchemasIndex[key]);
schema.extend(CreaturePropertySchema);
schema.extend(ColorSchema);
schema.extend(ChildSchema);
schema.extend(SoftRemovableSchema);
CreatureProperties.attachSchema(schema, {
selector: {type: key}
});
}
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}) {
delete creatureProperty._id;
assertPropertyEditPermission(creatureProperty, this.userId);
let _id = CreatureProperties.insert(creatureProperty);
let property = CreatureProperties.findOne(_id);
recomputeCreatures(property);
},
});
const duplicateProperty = new ValidatedMethod({
name: 'CreatureProperties.methods.duplicate',
validate: new SimpleSchema({
_id: {
type: String,
regEx: SimpleSchema.RegEx.Id,
}
}).validator(),
run({_id}) {
let creatureProperty = CreatureProperties.findOne(_id);
assertPropertyEditPermission(creatureProperty, this.userId);
delete creatureProperty._id;
CreatureProperties.insert(creatureProperty);
recomputeCreatures(creatureProperty);
},
});
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 adjustQuantity = new ValidatedMethod({
name: 'CreatureProperties.methods.adjustQuantity',
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('quantity')){
throw new Meteor.Error(
'Adjust quantity failed',
`Property of type "${currentProperty.type}" doesn't have a quantity`
);
}
if (operation === 'set'){
CreatureProperties.update(_id, {
$set: {quantity: value}
}, {
selector: currentProperty
});
} else if (operation === 'increment'){
// value here is 'damage'
value = -value;
let currentQuantity = currentProperty.quantity;
if (currentQuantity + value < 0) value = -currentQuantity;
CreatureProperties.update(_id, {
$inc: {quantity: value}
}, {
selector: currentProperty
});
}
},
});
const pushToProperty = new ValidatedMethod({
name: 'CreatureProperties.methods.push',
validate: null,
run({_id, path, value}){
let property = CreatureProperties.findOne(_id);
assertPropertyEditPermission(property, this.userId);
CreatureProperties.update(_id, {
$push: {[path.join('.')]: value},
}, {
selector: {type: property.type},
});
recomputeCreatures(property);
}
});
const pullFromProperty = new ValidatedMethod({
name: 'CreatureProperties.methods.pull',
validate: null,
run({_id, path, itemId}){
let property = CreatureProperties.findOne(_id);
assertPropertyEditPermission(property, this.userId);
CreatureProperties.update(_id, {
$pull: {[path.join('.')]: {_id: itemId}},
}, {
selector: {type: property.type},
getAutoValues: false,
});
recomputeCreatures(property);
}
});
const softRemoveProperty = new ValidatedMethod({
name: 'CreatureProperties.methods.softRemove',
validate: new SimpleSchema({
_id: SimpleSchema.RegEx.Id
}).validator(),
run({_id}){
let property = CreatureProperties.findOne(_id);
assertPropertyEditPermission(property, this.userId);
softRemove({_id, collection: CreatureProperties});
recomputeCreatures(property);
}
});
export default CreatureProperties;
export {
CreaturePropertySchema,
insertProperty,
duplicateProperty,
insertPropertyFromLibraryNode,
updateProperty,
damageProperty,
adjustQuantity,
pushToProperty,
pullFromProperty,
softRemoveProperty,
};

View File

@@ -1,10 +1,12 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
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 CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import {assertEditPermission} from '/imports/api/sharing/sharingPermissions.js';
import { getUserTier } from '/imports/api/users/patreon/tiers.js';
import { assertUserHasPaidBenefits } from '/imports/api/users/patreon/tiers.js';
import '/imports/api/creature/removeCreature.js';
import '/imports/api/creature/restCreature.js';
@@ -39,7 +41,12 @@ let CreatureSettingsSchema = new SimpleSchema({
optional: true,
min: 0,
max: 1,
}
},
discordWebhook: {
type: String,
optional: true,
max: 200,
},
});
let CreatureSchema = new SimpleSchema({
@@ -65,24 +72,60 @@ let CreatureSchema = new SimpleSchema({
type: String,
optional: true,
},
// Mechanics
deathSave: {
type: deathSaveSchema,
defaultValue: {},
},
xp: {
// Stats that are computed and denormalised outside of recomputation
denormalizedStats: {
type: Object,
defaultValue: {},
},
// Sum of all XP gained by this character
'denormalizedStats.xp': {
type: SimpleSchema.Integer,
defaultValue: 0,
},
weightCarried: {
// Sum of all levels granted by milestone XP
'denormalizedStats.milestoneLevels': {
type: SimpleSchema.Integer,
defaultValue: 0,
},
// Inventory
'denormalizedStats.weightTotal': {
type: Number,
defaultValue: 0,
},
level: {
type: SimpleSchema.Integer,
'denormalizedStats.weightEquipment': {
type: Number,
defaultValue: 0,
},
'denormalizedStats.weightCarried': {
type: Number,
defaultValue: 0,
},
'denormalizedStats.valueTotal': {
type: Number,
defaultValue: 0,
},
'denormalizedStats.valueEquipment': {
type: Number,
defaultValue: 0,
},
'denormalizedStats.valueCarried': {
type: Number,
defaultValue: 0,
},
'denormalizedStats.itemsAttuned': {
type: Number,
defaultValue: 0,
},
// Version of computation engine that was last used to compute this creature
computeVersion: {
type: String,
optional: true,
},
type: {
type: String,
defaultValue: 'pc',
@@ -99,6 +142,17 @@ let CreatureSchema = new SimpleSchema({
defaultValue: {}
},
// Tabletop
tabletop: {
type: String,
regEx: SimpleSchema.RegEx.id,
optional: true,
},
initiativeRoll: {
type: SimpleSchema.Integer,
optional: true,
},
// Settings
settings: {
type: CreatureSettingsSchema,
@@ -113,48 +167,83 @@ Creatures.attachSchema(CreatureSchema);
const insertCreature = new ValidatedMethod({
name: 'Creatures.methods.insertCreature',
name: 'creatures.insertCreature',
validate: null,
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run() {
if (!this.userId) {
throw new Meteor.Error('Creatures.methods.insert.denied',
'You need to be logged in to insert a creature');
}
let tier = getUserTier(this.userId);
if (!tier.paidBenefits){
throw new Meteor.Error('Creatures.methods.insert.denied',
`The ${tier.name} tier does not allow you to insert a creature`);
}
assertUserHasPaidBenefits(this.userId);
// Create the creature document
let charId = Creatures.insert({
let creatureId = Creatures.insert({
owner: this.userId,
});
CreatureProperties.insert({
slotTags: ['base'],
quantityExpected: 1,
type: 'propertySlot',
name: 'Base',
description: 'Choose a starting point for your character, this will define the basic setup of your character sheet. Without a base, your sheet will be empty.',
hideWhenFull: true,
parent: {collection: 'creatures', id: creatureId},
ancestors: [{collection: 'creatures', id: creatureId}],
order: 0,
tags: [],
spaceLeft: 1,
totalFilled: 0,
});
this.unblock();
return charId;
return creatureId;
},
});
const updateCreature = new ValidatedMethod({
name: 'Creatures.methods.update',
name: 'creatures.update',
validate({_id, path}){
if (!_id) return false;
// Allowed fields
let allowedFields = ['name', 'alignment', 'gender', 'picture', 'avatarPicture', 'settings'];
let allowedFields = [
'name',
'alignment',
'gender',
'picture',
'avatarPicture',
'color',
'settings',
];
if (!allowedFields.includes(path[0])){
throw new Meteor.Error('Creatures.methods.update.denied',
'This field can\'t be updated using this method');
}
},
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({_id, path, value}) {
let creature = Creatures.findOne(_id);
assertEditPermission(creature, this.userId);
Creatures.update(_id, {
$set: {[path.join('.')]: value},
});
if (value === undefined || value === null){
Creatures.update(_id, {
$unset: {[path.join('.')]: 1},
});
} else {
Creatures.update(_id, {
$set: {[path.join('.')]: value},
});
}
},
});

View File

@@ -0,0 +1,28 @@
import SimpleSchema from 'simpl-schema';
let Parties = new Mongo.Collection('parties');
let partySchema = new SimpleSchema({
name: {
type: String,
defaultValue: 'New Party',
trim: false,
optional: true,
},
creatures: {
type: Array,
defaultValue: [],
},
'creatures.$': {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
owner: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
});
Parties.attachSchema(partySchema);
export default Parties;

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,89 @@
import evaluateString from '/imports/api/creature/computation/afterComputation/evaluateString.js';
import dealDamage from '/imports/api/creature/creatureProperties/methods/dealDamage.js';
import {insertCreatureLog} from '/imports/api/creature/log/CreatureLogs.js';
import { CompilationContext } from '/imports/parser/parser.js';
export default function applyDamage({
prop,
creature,
targets,
actionContext,
log,
}){
let damageTargets = prop.target === 'self' ? [creature] : targets;
let scope = {
...creature.variables,
...actionContext,
};
if (targets.length === 1){
scope.target = targets[0].variables;
}
let criticalHit = !!(
actionContext.criticalHit &&
actionContext.criticalHit.value &&
prop.damageType !== 'healing' // Can't critically heal
);
let context = new CompilationContext({
doubleRolls: criticalHit,
});
try {
var {result, errors} = evaluateString(prop.amount, scope, 'reduce', context);
if (typeof result !== 'number') {
log.content.push({
error: errors.join(', '),
});
}
} catch (e){
log.content.push({
error: e.toString(),
});
}
let suffix = (criticalHit ? ' critical ' : '') +
prop.damageType +
(prop.damageType !== 'healing' ? ' damage': '');
if (damageTargets && damageTargets.length) {
damageTargets.forEach(target => {
let name = prop.damageType === 'healing' ? 'Healing' : 'Damage';
if (prop.target === 'each'){
result = evaluateString(prop.amount, scope, 'reduce');
}
let damageDealt = dealDamage.call({
creatureId: target._id,
damageType: prop.damageType,
amount: result,
});
if (target._id === creature._id){
log.content.push({
name,
result: damageDealt,
details: suffix + 'to self',
});
} else {
log.content.push({
name,
resultPrefix: 'Dealt ',
result: damageDealt,
details: suffix + `${target.name && ' to '}${target.name}`,
});
insertCreatureLog.call({
log: {
content: [{
name,
resultPrefix: 'Recieved ',
result: damageDealt,
details: suffix,
}],
creatureId: target._id,
}
});
}
});
} else {
log.content.push({
name: prop.damageType === 'healing' ? 'Healing' : 'Damage',
result,
details: suffix,
});
}
}

View File

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

View File

@@ -0,0 +1,32 @@
import evaluateString from '/imports/api/creature/computation/afterComputation/evaluateString.js';
export default function applyRoll({
prop,
creature,
actionContext,
log,
}){
let scope = {
...creature.variables,
...actionContext,
};
try {
var {result, errors} = evaluateString(prop.roll, scope, 'reduce');
actionContext[prop.variableName] = result;
log.content.push({
name: prop.name,
resultPrefix: prop.variableName + ' = ' + prop.roll + ' = ',
result,
});
if (errors.length) {
log.content.push({
error: errors.join(', '),
});
}
} catch (e){
log.content.push({
error: e.toString(),
});
}
}

View File

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

View File

@@ -0,0 +1,35 @@
import evaluateString from '/imports/api/creature/computation/afterComputation/evaluateString.js';
export default function applyToggle({
prop,
creature,
actionContext,
log,
}){
let scope = {
...creature.variables,
...actionContext,
};
if (Number.isFinite(+prop.condition)){
return !!+prop.condition;
}
try {
var {result, errors} = evaluateString(prop.condition, scope, 'reduce');
if (typeof result !== 'number' && typeof result !== 'boolean') {
log.content.push({
error: errors.join(', '),
});
return false;
}
log.content.push({
name: prop.name,
resultPrefix: prop.condition + ' = ',
result,
});
return !!result;
} catch (e){
log.content.push({
error: e.toString(),
});
}
}

View File

@@ -0,0 +1,79 @@
import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import Creatures from '/imports/api/creature/Creatures.js';
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
import { assertEditPermission } from '/imports/api/creature/creaturePermissions.js';
import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js';
import { doActionWork } from '/imports/api/creature/actions/doAction.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
const castSpellWithSlot = new ValidatedMethod({
name: 'creatureProperties.castSpellWithSlot',
validate: new SimpleSchema({
spellId: SimpleSchema.RegEx.Id,
slotId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
optional: true,
},
targetId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
optional: true,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 10,
timeInterval: 5000,
},
run({spellId, slotId, targetId}) {
let spell = CreatureProperties.findOne(spellId);
// Check permissions
let creature = getRootCreatureAncestor(spell);
assertEditPermission(creature, this.userId);
let target = undefined;
if (targetId) {
target = Creatures.findOne(targetId);
assertEditPermission(target, this.userId);
}
let slotLevel = spell.level || 0;
if (slotLevel !== 0){
let slot = CreatureProperties.findOne(slotId);
if (!slot){
throw new Meteor.Error('No slot',
'Slot not found to cast spell');
}
if (!slot.currentValue){
throw new Meteor.Error('No slot',
'Slot depleted');
}
if (!(slot.spellSlotLevelValue >= spell.level)){
throw new Meteor.Error('Slot too small',
'Slot is not large enough to cast spell');
}
slotLevel = slot.spellSlotLevelValue;
damagePropertyWork({
property: slot,
operation: 'increment',
value: 1,
});
}
doActionWork({
action: spell,
context: {slotLevel},
creature,
target,
method: this,
});
// Note this only recomputes the top-level creature, not the nearest one
recomputeCreatureByDoc(creature);
if (target){
recomputeCreatureByDoc(target);
}
},
});
export default castSpellWithSlot;

View File

@@ -0,0 +1,88 @@
import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import Creatures from '/imports/api/creature/Creatures.js';
import { CreatureLogSchema, insertCreatureLogWork } from '/imports/api/creature/log/CreatureLogs.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import { assertEditPermission } from '/imports/api/creature/creaturePermissions.js';
import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js';
import { nodesToTree } from '/imports/api/parenting/parenting.js';
import applyProperties from '/imports/api/creature/actions/applyProperties.js';
import recomputeInventory from '/imports/api/creature/denormalise/recomputeInventory.js';
const doAction = new ValidatedMethod({
name: 'creatureProperties.doAction',
validate: new SimpleSchema({
actionId: SimpleSchema.RegEx.Id,
targetIds: {
type: Array,
defaultValue: [],
maxCount: 10,
optional: true,
},
'targetIds.$': {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 10,
timeInterval: 5000,
},
run({actionId, targetIds = []}) {
let action = CreatureProperties.findOne(actionId);
// Check permissions
let creature = getRootCreatureAncestor(action);
assertEditPermission(creature, this.userId);
let targets = [];
targetIds.forEach(targetId => {
let target = Creatures.findOne(targetId);
assertEditPermission(target, this.userId);
targets.push(target);
});
doActionWork({action, creature, targets, method: this});
// The acting creature might have used ammo
recomputeInventory(creature._id);
// recompute creatures
recomputeCreatureByDoc(creature);
targets.forEach(target => {
recomputeCreatureByDoc(target);
});
},
});
export function doActionWork({
action,
creature,
targets,
context = {},
method
}){
// Create the log
let log = CreatureLogSchema.clean({
creatureId: creature._id,
creatureName: creature.name,
});
let decendantForest = nodesToTree({
collection: CreatureProperties,
ancestorId: action._id,
});
let startingForest = [{
node: action,
children: decendantForest,
}];
applyProperties({
forest: startingForest,
actionContext: context,
creature,
targets,
log,
});
insertCreatureLogWork({log, creature, method});
}
export default doAction;

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
import math from '/imports/math.js';
import bareSymbolSubtitutor from '/imports/api/creature/computation/utility/bareSymbolSubtitutor.js';
import { parse, CompilationContext } from '/imports/parser/parser.js';
import ConstantNode from '/imports/parser/parseTree/ConstantNode.js';
export default function evaluateString(string, scope){
export default function evaluateString(string, scope, fn = 'compile', context){
let errors = [];
if (!string){
errors.push('No string provided');
@@ -11,88 +11,20 @@ export default function evaluateString(string, scope){
if (!scope) errors.push('No scope provided');
// Parse the string using mathjs
let calc;
let node;
try {
calc = math.parse(string);
node = parse(string);
} catch (e) {
errors.push(e);
return {result: string, errors};
}
// Replace all bare symbols with symbol.value
let transformedCalc = calc.transform(bareSymbolSubtitutor(scope));
// 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(transformedCalc, scope).toHTML();
return {result, errors};
} catch (e2){
errors.push(e2);
return {result: transformedCalc.toHTML(), errors};
}
if (!context){
context = new CompilationContext({});
}
let result = node[fn](scope, context);
if (result instanceof ConstantNode){
return {result: result.value, errors: context.errors}
} else {
return {result: result.toString(), errors: context.errors};
}
}
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

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

View File

@@ -1,22 +0,0 @@
import { each, forOwn } from 'lodash';
import computeLevels from '/imports/api/creature/computation/computeLevels.js';
import computeStat from '/imports/api/creature/computation/computeStat.js';
import computeEffect from '/imports/api/creature/computation/computeEffect.js';
import computeToggle from '/imports/api/creature/computation/computeToggle.js';
export default function computeMemo(memo){
// Compute level
computeLevels(memo);
// Compute all stats, even if they are overriden
forOwn(memo.statsById, stat => {
computeStat (stat, memo);
});
// Compute effects which didn't end up targeting a stat
each(memo.unassignedEffects, effect => {
computeEffect(effect, memo);
});
forOwn(memo.togglesById, toggle => {
computeToggle(toggle, memo);
});
// Compute class levels
}

View File

@@ -1,10 +1,12 @@
import { includes, cloneDeep } from 'lodash';
import findAncestorByType from '/imports/api/creature/computation/engine/findAncestorByType.js';
// The computation memo is an in-memory data structure used only during the
// computation process
export default class ComputationMemo {
constructor(props){
constructor(props, creature){
this.statsByVariableName = {};
this.constantsByVariableName = {};
this.extraStatsByVariableName = {};
this.statsById = {};
this.originalPropsById = {};
@@ -15,6 +17,10 @@ export default class ComputationMemo {
this.classes = {};
this.togglesById = {};
this.toggleIds = new Set();
// Equipped items that might be used as ammo
this.equipmentById = {};
// Properties that have calculations, but don't impact other properties
this.endStepPropsById = {};
// First note all the ids of all the toggles
props.forEach((prop) => {
if (
@@ -38,23 +44,49 @@ export default class ComputationMemo {
) {
// Add all the stats
this.addStat(prop);
} else if (
prop.type === 'item'
) {
this.addEquipment(prop);
} else {
return true;
}
}).forEach((prop) => {
// Now add all effects and proficiencies
// Now add everything else
if (prop.type === 'effect'){
this.addEffect(prop);
} else if (prop.type === 'proficiency') {
this.addProficiency(prop);
} else if (prop.type === 'classLevel'){
this.addClassLevel(prop);
} else if (prop.type === 'constant'){
this.addConstant(prop);
} else {
this.addEndStepProp(prop);
}
});
for (let name in creature.denormalizedStats){
if (!this.statsByVariableName[name]){
this.statsByVariableName[name] = {
variableName: name,
value: creature.denormalizedStats[name],
computationDetails: propDetailsByType.denormalizedStat(),
}
}
}
}
addConstant(prop){
prop = this.registerProperty(prop);
if (
!this.constantsByVariableName[prop.variableName]
){
this.constantsByVariableName[prop.variableName] = prop
}
}
registerProperty(prop){
this.originalPropsById[prop._id] = cloneDeep(prop);
this.propsById[prop._id] = prop;
prop.dependencies = [];
prop.computationDetails = propDetails(prop);
prop.ancestors.forEach(ancestor => {
if (this.toggleIds.has(ancestor.id)){
@@ -85,6 +117,7 @@ export default class ComputationMemo {
stats: [variableName],
computationDetails: propDetailsByType.effect(),
statBase: true,
dependencies: [],
});
}
if (prop.baseProficiency){
@@ -94,6 +127,7 @@ export default class ComputationMemo {
computationDetails: propDetailsByType.proficiency(),
type: 'proficiency',
statBase: true,
dependencies: [],
});
}
} else {
@@ -119,7 +153,7 @@ export default class ComputationMemo {
prop = this.registerProperty(prop);
let targets = this.getEffectTargets(prop);
targets.forEach(target => {
if (target.computationDetails.effects){
if (target.computationDetails && target.computationDetails.effects){
target.computationDetails.effects.push(prop);
}
});
@@ -131,7 +165,16 @@ export default class ComputationMemo {
let targets = new Set();
if (!prop.stats) return targets;
prop.stats.forEach((statName) => {
let target = this.statsByVariableName[statName];
let target;
if (statName[0] === '#'){
target = findAncestorByType({
type: statName.slice(1),
prop,
memo: this
});
} else {
target = this.statsByVariableName[statName];
}
if (!target) return;
targets.add(target);
if (isSkillOperation(prop) && isAbility(target)){
@@ -172,6 +215,14 @@ export default class ComputationMemo {
});
return targets;
}
addEquipment(prop){
prop = this.registerProperty(prop);
this.equipmentById[prop._id] = prop;
}
addEndStepProp(prop){
prop = this.registerProperty(prop);
this.endStepPropsById[prop._id] = prop;
}
}
function isAbility(prop){
@@ -197,10 +248,17 @@ function isSkillOperation(prop){
}
function propDetails(prop){
return propDetailsByType[prop.type]() || {};
return propDetailsByType[prop.type] && propDetailsByType[prop.type]() ||
propDetailsByType.default();
}
const propDetailsByType = {
default(){
return {
toggleAncestors: [],
disabledByToggle: false,
};
},
toggle(){
return {
computed: false,
@@ -251,4 +309,10 @@ const propDetailsByType = {
disabledByToggle: false,
};
},
denormalizedStat(){
return {
toggleAncestors: [],
disabledByToggle: false,
};
}
}

View File

@@ -1,13 +1,26 @@
import evaluateCalculation from '/imports/api/creature/computation/evaluateCalculation.js';
import evaluateCalculation from '/imports/api/creature/computation/engine/evaluateCalculation.js';
import { union } from 'lodash';
export default class EffectAggregator{
constructor(stat, memo){
delete this.baseValueErrors;
if (stat.baseValueCalculation){
let {value, errors} = evaluateCalculation(stat.baseValueCalculation, memo);
this.statBaseValue = value;
if (errors.length){
this.baseValueErrors = errors;
let {
result,
context,
dependencies
} = evaluateCalculation({
string: stat.baseValueCalculation,
prop: stat,
memo
});
this.statBaseValue = +result.value;
stat.dependencies = union(
stat.dependencies,
dependencies,
);
if (context.errors.length){
this.baseValueErrors = context.errors;
}
this.base = this.statBaseValue;
} else {
@@ -19,7 +32,7 @@ export default class EffectAggregator{
this.max = Number.POSITIVE_INFINITY;
this.advantage = 0;
this.disadvantage = 0;
this.passiveAdd = 0;
this.passiveAdd = undefined;
this.fail = 0;
this.set = undefined;
this.conditional = [];
@@ -69,6 +82,7 @@ export default class EffectAggregator{
break;
case 'passiveAdd':
// Add all passive adds together
if (this.passiveAdd === undefined) this.passiveAdd = 0;
this.passiveAdd += result;
break;
case 'fail':

View File

@@ -1,9 +1,15 @@
import computeToggle from '/imports/api/creature/computation/computeToggle.js';
import computeToggle from '/imports/api/creature/computation/engine/computeToggle.js';
import { union } from 'lodash';
export default function applyToggles(prop, memo){
prop.computationDetails.toggleAncestors.forEach(toggleId => {
let toggle = memo.togglesById[toggleId];
computeToggle(toggle, memo);
prop.dependencies = union(
prop.dependencies,
[toggle._id],
toggle.dependencies,
);
if (!toggle.toggleResult){
prop.computationDetails.disabledByToggle = true;
}

View File

@@ -1,9 +1,11 @@
import computeStat from '/imports/api/creature/computation/computeStat.js';
import applyToggles from '/imports/api/creature/computation/applyToggles.js';
import computeStat from '/imports/api/creature/computation/engine/computeStat.js';
import applyToggles from '/imports/api/creature/computation/engine/applyToggles.js';
import evaluateCalculation from '/imports/api/creature/computation/engine/evaluateCalculation.js';
import { union } from 'lodash';
export default function combineStat(stat, aggregator, memo){
if (stat.type === 'attribute'){
combineAttribute(stat, aggregator);
combineAttribute(stat, aggregator, memo);
} else if (stat.type === 'skill'){
combineSkill(stat, aggregator, memo);
} else if (stat.type === 'damageMultiplier'){
@@ -28,14 +30,45 @@ function getAggregatorResult(stat, aggregator){
return result;
}
function combineAttribute(stat, aggregator){
function combineAttribute(stat, aggregator, memo){
stat.value = getAggregatorResult(stat, aggregator);
stat.baseValue = aggregator.statBaseValue;
stat.baseValueErrors = aggregator.baseValueErrors;
if (stat.attributeType === 'ability') {
stat.modifier = Math.floor((stat.value - 10) / 2);
if (stat.attributeType === 'spellSlot'){
let {
result,
context,
dependencies
} = evaluateCalculation({
string: stat.spellSlotLevelCalculation,
memo,
prop: stat,
});
stat.spellSlotLevelValue = result.value;
stat.spellSlotLevelErrors = context.errors;
stat.dependencies = union(stat.dependencies, dependencies);
}
stat.currentValue = stat.value - (stat.damage || 0);
// Ability scores get modifiers
if (stat.attributeType === 'ability') {
stat.modifier = Math.floor((stat.currentValue - 10) / 2);
} else {
stat.modifier = undefined;
}
// Hit dice get constitution modifiers
stat.constitutionMod = undefined;
if (stat.attributeType === 'hitDice') {
let conStat = memo.statsByVariableName['constitution'];
if (conStat && 'modifier' in conStat){
stat.constitutionMod = conStat.modifier;
stat.dependencies = union(
stat.dependencies,
[conStat._id],
conStat.dependencies,
);
}
}
// Stats that have no effects can be hidden based on a sheet setting
stat.hide = aggregator.hasNoEffects &&
stat.baseValue === undefined ||
undefined
@@ -43,12 +76,19 @@ function combineAttribute(stat, aggregator){
function combineSkill(stat, aggregator, memo){
// Skills are based on some ability Modifier
let ability = memo.statsByVariableName[stat.ability]
let ability = stat.ability && memo.statsByVariableName[stat.ability]
if (stat.ability && ability){
if (!ability.computationDetails.computed){
computeStat(ability, memo);
}
stat.abilityMod = ability.modifier;
stat.dependencies = union(
stat.dependencies,
[ability._id],
ability.dependencies,
);
} else {
stat.abilityMod = 0;
}
// Combine all the child proficiencies
stat.proficiency = stat.baseProficiency || 0;
@@ -60,6 +100,11 @@ function combineSkill(stat, aggregator, memo){
prof.value > stat.proficiency
){
stat.proficiency = prof.value;
stat.dependencies = union(
stat.dependencies,
[prof._id],
prof.dependencies,
);
}
}
// Get the character's proficiency bonus to apply
@@ -69,6 +114,18 @@ function combineSkill(stat, aggregator, memo){
if (typeof profBonus !== 'number' && memo.statsByVariableName['level']){
let level = memo.statsByVariableName['level'].value;
profBonus = Math.ceil(level / 4) + 1;
if (level._id){
stat.dependencies = union(stat.dependencies, [level._id]);
}
if (level.dependencies){
stat.dependencies = union(stat.dependencies, level.dependencies);
}
} else {
stat.dependencies = union(
stat.dependencies,
[profBonusStat._id],
profBonusStat.dependencies,
);
}
// Multiply the proficiency bonus by the actual proficiency
profBonus *= stat.proficiency;

View File

@@ -1,5 +1,6 @@
import evaluateCalculation from '/imports/api/creature/computation/evaluateCalculation.js';
import applyToggles from '/imports/api/creature/computation/applyToggles.js';
import evaluateCalculation from '/imports/api/creature/computation/engine/evaluateCalculation.js';
import applyToggles from '/imports/api/creature/computation/engine/applyToggles.js';
import { union } from 'lodash';
export default function computeEffect(effect, memo){
if (effect.computationDetails.computed) return;
@@ -34,10 +35,19 @@ export default function computeEffect(effect, memo){
} else if(_.contains(['advantage', 'disadvantage', 'fail'], effect.operation)){
effect.result = 1;
} else {
let {value, errors} = evaluateCalculation(effect.calculation, memo);
effect.result = value;
if (errors.length){
effect.errors = errors;
let {
result,
context,
dependencies,
} = evaluateCalculation({
string: effect.calculation,
prop: effect,
memo
});
effect.result = result.value;
effect.dependencies = union(effect.dependencies, dependencies);
if (context.errors.length){
effect.errors = context.errors;
}
}
effect.computationDetails.computed = true;

View File

@@ -0,0 +1,119 @@
import evaluateCalculation from '/imports/api/creature/computation/engine/evaluateCalculation.js';
import ConstantNode from '/imports/parser/parseTree/ConstantNode.js';
import { union } from 'lodash';
export default function computeEndStepProperty(prop, memo){
switch (prop.type){
case 'action':
case 'spell':
computeAction(prop, memo);
break;
case 'adjustment':
case 'damage':
computePropertyField(prop, memo, 'amount', 'compile');
break;
case 'attack':
computeAction(prop, memo);
computePropertyField(prop, memo, 'rollBonus');
break;
case 'savingThrow':
computePropertyField(prop, memo, 'dc');
break;
case 'spellList':
computePropertyField(prop, memo, 'maxPrepared');
computePropertyField(prop, memo, 'attackRollBonus');
computePropertyField(prop, memo, 'dc');
break;
case 'propertySlot':
computePropertyField(prop, memo, 'quantityExpected');
computePropertyField(prop, memo, 'slotCondition');
break;
case 'roll':
computePropertyField(prop, memo, 'roll', 'compile');
break;
}
}
function computeAction(prop, memo){
// Uses
let {
result,
context,
dependencies,
} = evaluateCalculation({ string: prop.uses, prop, memo});
prop.usesResult = result.value;
prop.dependencies = union(prop.dependencies, dependencies);
if (context.errors.length){
prop.usesErrors = context.errors;
} else {
delete prop.usesErrors;
}
prop.insufficientResources = undefined;
if (prop.usesUsed >= prop.usesResult){
prop.insufficientResources = true;
}
if (!prop.resources) return;
// Attributes consumed
prop.resources.attributesConsumed.forEach((attConsumed, i) => {
if (attConsumed.variableName){
let stat = memo.statsByVariableName[attConsumed.variableName];
prop.resources.attributesConsumed[i].statId = stat && stat._id;
prop.resources.attributesConsumed[i].statName = stat && stat.name;
let available = stat && stat.currentValue || 0;
prop.resources.attributesConsumed[i].available = available;
if (available < attConsumed.quantity){
prop.insufficientResources = true;
}
if (stat){
prop.dependencies = union(
prop.dependencies,
[stat._id],
stat.dependencies
);
}
}
});
// Items consumed
prop.resources.itemsConsumed.forEach((itemConsumed, i) => {
let item = itemConsumed.itemId && memo.equipmentById[itemConsumed.itemId];
prop.resources.itemsConsumed[i].itemId = item && item._id;
let available = item && item.quantity || 0;
prop.resources.itemsConsumed[i].available = available;
let name = item && item.name;
if (item && item.quantity !== 1 && item.plural){
name = item.plural;
}
prop.resources.itemsConsumed[i].itemName = name;
prop.resources.itemsConsumed[i].itemIcon = item && item.icon;
prop.resources.itemsConsumed[i].itemColor = item && item.color;
if (!item || available < itemConsumed.quantity){
prop.insufficientResources = true;
}
if (item){
prop.dependencies = union(
prop.dependencies,
[item._id],
item.dependencies
);
}
});
}
function computePropertyField(prop, memo, fieldName, fn){
let {
result,
context,
dependencies,
} = evaluateCalculation({string: prop[fieldName], prop, memo, fn});
if (result instanceof ConstantNode){
prop[`${fieldName}Result`] = result.value;
} else {
prop[`${fieldName}Result`] = result.toString();
}
prop.dependencies = union(prop.dependencies, dependencies);
if (context.errors.length){
prop[`${fieldName}Errors`] = context.errors;
} else {
delete prop[`${fieldName}Errors`];
}
}

View File

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

View File

@@ -1,4 +1,4 @@
import { forOwn, has } from 'lodash';
import { forOwn, has, union } from 'lodash';
export default function computeLevels(memo){
computeClassLevels(memo);
@@ -7,6 +7,11 @@ export default function computeLevels(memo){
function computeClassLevels(memo){
forOwn(memo.classLevelsById, classLevel => {
// class levels are mutually dependent
classLevel.dependencies = union(
classLevel.dependencies,
Object.keys(memo.classLevelsById)
);
let name = classLevel.variableName;
let stat = memo.statsByVariableName[name];
if (!stat){
@@ -27,6 +32,7 @@ function computeTotalLevel(memo){
if (!currentLevel){
currentLevel = {
value: 0,
dependencies: [],
computationDetails: {
builtIn: true,
computed: true,
@@ -38,7 +44,20 @@ function computeTotalLevel(memo){
if (!currentLevel.computationDetails.builtIn) return;
let level = 0;
for (let name in memo.classes){
level += memo.classes[name].level || 0;
let cls = memo.classes[name];
level += cls.level || 0;
if (cls._id){
currentLevel.dependencies = union(
currentLevel.dependencies,
[cls._id]
)
}
if (cls.dependencies){
currentLevel.dependencies = union(
currentLevel.dependencies,
cls.dependencies,
)
}
}
memo.statsByVariableName['level'].value = level;
currentLevel.value = level;
}

View File

@@ -0,0 +1,32 @@
import { each, forOwn } from 'lodash';
import computeLevels from '/imports/api/creature/computation/engine/computeLevels.js';
import computeStat from '/imports/api/creature/computation/engine/computeStat.js';
import computeEffect from '/imports/api/creature/computation/engine/computeEffect.js';
import computeToggle from '/imports/api/creature/computation/engine/computeToggle.js';
import computeEndStepProperty from '/imports/api/creature/computation/engine/computeEndStepProperty.js';
import computeInlineCalculations from '/imports/api/creature/computation/engine/computeInlineCalculations.js';
export default function computeMemo(memo){
// Compute level
computeLevels(memo);
// Compute all stats, even if they are overriden
forOwn(memo.statsById, stat => {
computeStat (stat, memo);
});
// Compute effects which didn't end up targeting a stat
each(memo.unassignedEffects, effect => {
computeEffect(effect, memo);
});
// Compute toggles which didn't already get computed by dependencies
forOwn(memo.togglesById, toggle => {
computeToggle(toggle, memo);
});
// Compute end step properties
forOwn(memo.endStepPropsById, prop => {
computeEndStepProperty(prop, memo);
});
// Compute inline calculations
forOwn(memo.propsById, prop => {
computeInlineCalculations(prop, memo);
});
}

View File

@@ -1,8 +1,8 @@
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 applyToggles from '/imports/api/creature/computation/applyToggles.js';
import { each } from 'lodash';
import combineStat from '/imports/api/creature/computation/engine/combineStat.js';
import computeEffect from '/imports/api/creature/computation/engine/computeEffect.js';
import EffectAggregator from '/imports/api/creature/computation/engine/EffectAggregator.js';
import applyToggles from '/imports/api/creature/computation/engine/applyToggles.js';
import { each, union } from 'lodash';
export default function computeStat(stat, memo){
// If the stat is already computed, skip it
@@ -27,6 +27,16 @@ export default function computeStat(stat, memo){
let aggregator = new EffectAggregator(stat, memo)
each(stat.computationDetails.effects, (effect) => {
computeEffect(effect, memo);
if (effect._id){
stat.dependencies = union(
stat.dependencies,
[effect._id]
);
}
stat.dependencies = union(
stat.dependencies,
effect.dependencies
)
if (!effect.computationDetails.disabledByToggle){
aggregator.addEffect(effect);
}

View File

@@ -1,4 +1,5 @@
import evaluateCalculation from '/imports/api/creature/computation/evaluateCalculation.js';
import evaluateCalculation from '/imports/api/creature/computation/engine/evaluateCalculation.js';
import { union } from 'lodash';
export default function computeToggle(toggle, memo){
if (toggle.computationDetails.computed) return;
@@ -26,10 +27,18 @@ export default function computeToggle(toggle, memo){
} else if (Number.isFinite(+toggle.condition)){
toggle.toggleResult = !!+toggle.condition;
} else {
let {value, errors} = evaluateCalculation(toggle.condition, memo);
toggle.toggleResult = value;
if (errors.length){
toggle.errors = errors;
let {
result,
context,
dependencies,
} = evaluateCalculation({string: toggle.condition, prop: toggle, memo});
toggle.toggleResult = !!result.value;
toggle.dependencies = union(
toggle.dependencies,
dependencies,
);
if (context.errors.length){
toggle.errors = context.errors;
}
}
toggle.computationDetails.computed = true;

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,23 +1,13 @@
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 { ComputedOnlyToggleSchema } from '/imports/api/properties/Toggles.js';
import CreatureProperties from '/imports/api/creature/CreatureProperties.js';
const schemasByType = {
'skill': ComputedOnlySkillSchema,
'attribute': ComputedOnlyAttributeSchema,
'effect': ComputedOnlyEffectSchema,
'toggle': ComputedOnlyToggleSchema,
};
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import propertySchemasIndex from '/imports/api/properties/computedOnlyPropertySchemasIndex.js';
export default function writeAlteredProperties(memo){
let bulkWriteOperations = [];
// Loop through all properties on the memo
forOwn(memo.propsById, changed => {
let schema = schemasByType[changed.type];
let schema = propertySchemasIndex[changed.type];
if (!schema) return;
let extraIds = changed.computationDetails.idsOfSameName;
let ids;
@@ -29,13 +19,14 @@ export default function writeAlteredProperties(memo){
ids.forEach(id => {
let op = undefined;
let original = memo.originalPropsById[id];
op = addChangedKeysToOp(op, schema.objectKeys(), original, changed);
let keys = ['dependencies', ...schema.objectKeys()];
op = addChangedKeysToOp(op, keys, original, changed);
if (op){
bulkWriteOperations.push(op);
}
});
});
bulkWriteProperties(bulkWriteOperations);
writePropertiesSequentially(bulkWriteOperations);
}
function addChangedKeysToOp(op, keys, original, changed) {
@@ -86,8 +77,27 @@ function addUnsetOp(op, key){
}
}
// We use this instead of bulkWriteProperties because it functions with latency
// compensation without needing to roll back changes, which causes multiple
// expensive redraws of the character sheet
function writePropertiesSequentially(bulkWriteOps){
bulkWriteOps.forEach(op => {
let updateOneOrMany = op.updateOne || op.updateMany;
CreatureProperties.update(updateOneOrMany.filter, updateOneOrMany.update, {
// The bulk code is bypassing validation, so do the same here
// selector: {type: op.type} // include this if bypass is off
bypassCollection2: true,
});
});
}
// This is more efficient on the database, but significantly less efficient
// in the UI because of incompatibility with latency compensation. If the
// duplicate redraws can be fixed, this is a strictly better way of processing
// writes
function bulkWriteProperties(bulkWriteOps){
if (!bulkWriteOps.length) return;
// bulkWrite is only available on the server
if (Meteor.isServer){
CreatureProperties.rawCollection().bulkWrite(
bulkWriteOps,
@@ -100,15 +110,6 @@ function bulkWriteProperties(bulkWriteOps){
}
);
} else {
bulkWriteOps.forEach(op => {
let updateOneOrMany = op.updateOne || op.updateMany;
CreatureProperties.update(updateOneOrMany.filter, updateOneOrMany.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,
});
});
writePropertiesSequentially(bulkWriteOps);
}
}

View File

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

View File

@@ -1,98 +0,0 @@
import computeStat from '/imports/api/creature/computation/computeStat.js';
import math from '/imports/math.js';
/* Convert a calculation into a constant output and errors*/
export default function evaluateCalculation(string, memo){
if (!string) return string;
let errors = [];
// Parse the string using mathjs
let calc;
try {
calc = math.parse(string);
} catch (e) {
errors.push({
type: 'parsing',
message: e.message || e
});
return {errors, value: 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);
}
}
});
// Replace all symbols with their subtitution
let substitutedCalc = calc.transform(
symbolSubtitutor(memo.statsByVariableName, errors)
);
// Evaluate the expression to a number or return with substitutions
try {
let value = substitutedCalc.evaluate(memo.statsByVariableName);
if (typeof value === 'object') value = value.toString();
return {errors, value};
} catch (e){
errors.push({
type: 'evaluation',
message: e.message || e
});
let value = substitutedCalc.toString();
return {errors, value};
}
}
// returns a function to replace all symbols with either their resolved value
// or zero, keeping the errors
function symbolSubtitutor(scope, errors){
return function(node){
// mark symbol nodes that are children of function nodes to be skipped
if (node.isFunctionNode){
let fn = node.fn;
if (fn && fn.isSymbolNode){
fn.skipReplacement = true;
}
return node;
} else if (node.isSymbolNode && node.skipReplacement !== true){
//bare symbols of name "stat", should search for stat.value
let stat = scope[node.name];
if (stat){
if (stat.value === undefined){
errors.push({
type: 'subsitution',
message: `${node.name} does not have a value, set to 0`
});
return new math.ConstantNode(0);
} else {
return new math.ConstantNode(stat.value);
}
} else {
try {
return new math.ConstantNode(node.evaluate(scope));
} catch (e) {
errors.push({
type: 'subsitution',
message: `${node.name} not found, set to 0`
});
return new math.ConstantNode(0);
}
}
} else if (node.isAccessorNode){
try {
let value = node.evaluate(scope);
if (value === undefined) throw 'Not found';
return new math.ConstantNode(value);
} catch (e) {
errors.push({
type: 'subsitution',
message: `${node.toString()} not found, set to 0`
});
return new math.ConstantNode(0);
}
} else {
return node;
}
}
}

View File

@@ -1,24 +1,37 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import SimpleSchema from 'simpl-schema';
import { assertEditPermission } from '/imports/api/creature/creaturePermissions.js';
import ComputationMemo from '/imports/api/creature/computation/ComputationMemo.js';
import computeMemo from '/imports/api/creature/computation/computeMemo.js';
import getActiveProperties from '/imports/api/creature/getActiveProperties.js';
import writeAlteredProperties from '/imports/api/creature/computation/writeAlteredProperties.js';
import writeCreatureVariables from '/imports/api/creature/computation/writeCreatureVariables.js';
import { recomputeDamageMultipliersById } from '/imports/api/creature/damageMultiplierDenormalise/recomputeDamageMultipliers.js'
import ComputationMemo from '/imports/api/creature/computation/engine/ComputationMemo.js';
import getComputationProperties from '/imports/api/creature/computation/engine/getComputationProperties.js';
import computeMemo from '/imports/api/creature/computation/engine/computeMemo.js';
import writeAlteredProperties from '/imports/api/creature/computation/engine/writeAlteredProperties.js';
import writeCreatureVariables from '/imports/api/creature/computation/engine/writeCreatureVariables.js';
import { recomputeDamageMultipliersById } from '/imports/api/creature/denormalise/recomputeDamageMultipliers.js';
import recomputeSlotFullness from '/imports/api/creature/denormalise/recomputeSlotFullness.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import getDependentProperties from '/imports/api/creature/computation/engine/getDependentProperties.js';
import Creatures from '/imports/api/creature/Creatures.js';
import recomputeInactiveProperties from '/imports/api/creature/denormalise/recomputeInactiveProperties.js';
export const recomputeCreature = new ValidatedMethod({
name: 'Creatures.methods.recomputeCreature',
name: 'creatures.recomputeCreature',
validate: new SimpleSchema({
charId: { type: String }
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({charId}) {
let creature = Creatures.findOne(charId);
// Permission
assertEditPermission(charId, this.userId);
assertEditPermission(creature, this.userId);
// Work, call this direcly if you are already in a method that has checked
// for permission to edit a given character
recomputeCreatureById(charId);
@@ -26,14 +39,10 @@ export const recomputeCreature = new ValidatedMethod({
});
const calculationPropertyTypes = [
'attribute',
'skill',
'effect',
'proficiency',
'classLevel',
'toggle',
];
export function recomputeCreatureById(creatureId){
let creature = Creatures.findOne(creatureId);
recomputeCreatureByDoc(creature);
}
/**
* This function is the heart of DiceCloud. It recomputes a creature's stats,
@@ -71,19 +80,42 @@ const calculationPropertyTypes = [
* - Mark the stat as computed
* - Write the computed results back to the database
*/
export function recomputeCreatureById(creatureId){
// Skipping computation on the client can result in the server being made to
// do work that isn't possible, in exchange for dramatic performance gains
if (Meteor.isClient) return;
let props = getActiveProperties({
ancestorId: creatureId,
filter: {type: {$in: calculationPropertyTypes}},
includeUntoggled: true,
});
let computationMemo = new ComputationMemo(props);
export function recomputeCreatureByDoc(creature){
const creatureId = creature._id;
let props = getComputationProperties(creatureId);
let computationMemo = new ComputationMemo(props, creature);
computeMemo(computationMemo);
writeAlteredProperties(computationMemo);
writeCreatureVariables(computationMemo, creatureId);
recomputeDamageMultipliersById(creatureId);
recomputeSlotFullness(creatureId);
recomputeInactiveProperties(creatureId);
return computationMemo;
}
export function recomputePropertyDependencies(property){
let creature = getRootCreatureAncestor(property);
recomputeCreatureByDependencies({
creature,
propertyIds: [property._id],
propertiesDependedAponIds: property.dependencies,
});
}
export function recomputeCreatureByDependencies({
creature,
propertyIds,
propertiesDependedAponIds
}){
let props = getDependentProperties({
creatureId: creature._id,
propertyIds,
propertiesDependedAponIds,
});
let computationMemo = new ComputationMemo(props, creature);
computeMemo(computationMemo);
writeAlteredProperties(computationMemo);
writeCreatureVariables(computationMemo, creature._id, false)
recomputeInactiveProperties(creature._id);
return computationMemo;
}

View File

@@ -1,24 +0,0 @@
import math from '/imports/math.js';
export default function bareSymbolSubtitutor(scope){
return function(node, path){
if (!scope) return node;
if (node.isFunctionNode){
let fn = node.fn;
if (fn && fn.isSymbolNode){
fn.skipReplacement = true;
}
return node;
} else if (
node.isSymbolNode &&
path !== 'object' &&
node.skipReplacement !== true
) {
let stat = scope[node.name];
if (!stat) return node;
return new math.ConstantNode(stat.value);
} else {
return node;
}
}
}

View File

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

View File

@@ -24,6 +24,6 @@ export function assertEditPermission(creature, userId) {
}
export function assertViewPermission(creature, userId) {
creature = getCreature(creature, {owner: 1, writers: 1, public: 1});
creature = getCreature(creature, {owner: 1, readers:1, writers: 1, public: 1});
viewPermission(creature, userId);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,33 @@
import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import { insertPropertyWork } from '/imports/api/creature/creatureProperties/methods/insertProperty.js';
const duplicateProperty = new ValidatedMethod({
name: 'creatureProperties.duplicate',
validate: new SimpleSchema({
_id: {
type: String,
regEx: SimpleSchema.RegEx.Id,
}
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({_id}) {
let creatureProperty = CreatureProperties.findOne(_id);
let rootCreature = getRootCreatureAncestor(creatureProperty);
assertEditPermission(rootCreature, this.userId);
insertPropertyWork({
property: creatureProperty,
creature: rootCreature,
});
},
});
export default duplicateProperty;

View File

@@ -0,0 +1,70 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import { organizeDoc } from '/imports/api/parenting/organizeMethods.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js';
import recomputeInventory from '/imports/api/creature/denormalise/recomputeInventory.js';
import INVENTORY_TAGS from '/imports/constants/INVENTORY_TAGS.js';
export function getParentRefByTag(creatureId, tag){
let prop = CreatureProperties.findOne({
'ancestors.id': creatureId,
removed: {$ne: true},
inactive: {$ne: true},
tags: tag,
}, {
sort: {order: 1},
});
if (prop){
return {id: prop._id, collection: 'creatureProperties'};
} else {
return {id: creatureId, collection: 'creatures'};
}
}
// Equipping or unequipping an item will also change its parent
const equipItem = new ValidatedMethod({
name: 'creatureProperties.equip',
validate({_id, equipped}){
if (!_id) throw new Meteor.Error('No _id', '_id is required');
if (equipped !== true && equipped !== false) {
throw new Meteor.Error('No equipped', 'equipped is required to be true or false');
}
},
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({_id, equipped}) {
let item = CreatureProperties.findOne(_id);
if (item.type !== 'item') throw new Meteor.Error('wrong type',
'Equip and unequip can only be performed on items');
let creature = getRootCreatureAncestor(item);
assertEditPermission(creature, this.userId);
CreatureProperties.update(_id, {
$set: {equipped},
}, {
selector: {type: 'item'},
});
let tag = equipped ? INVENTORY_TAGS.equipment : INVENTORY_TAGS.carried;
let parentRef = getParentRefByTag(creature._id, tag);
organizeDoc.call({
docRef: {
id: _id,
collection: 'creatureProperties',
},
parentRef,
order: Number.MAX_SAFE_INTEGER,
skipRecompute: true,
});
recomputeInventory(creature._id);
recomputeCreatureByDoc(creature);
},
});
export default equipItem;

View File

@@ -0,0 +1,14 @@
import '//imports/api/creature/creatureProperties/methods/adjustQuantity.js';
import '/imports/api/creature/creatureProperties/methods/damagePropertiesByName.js';
import '/imports/api/creature/creatureProperties/methods/damageProperty.js';
import '/imports/api/creature/creatureProperties/methods/dealDamage.js';
import '/imports/api/creature/creatureProperties/methods/duplicateProperty.js';
import '/imports/api/creature/creatureProperties/methods/equipItem.js';
import '/imports/api/creature/creatureProperties/methods/insertProperty.js';
import '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js';
import '/imports/api/creature/creatureProperties/methods/pullFromProperty.js';
import '/imports/api/creature/creatureProperties/methods/pushToProperty.js';
import '/imports/api/creature/creatureProperties/methods/restoreProperty.js';
import '/imports/api/creature/creatureProperties/methods/selectAmmoItem.js';
import '/imports/api/creature/creatureProperties/methods/softRemoveProperty.js';
import '/imports/api/creature/creatureProperties/methods/updateCreatureProperty.js';

View File

@@ -0,0 +1,49 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import { reorderDocs } from '/imports/api/parenting/order.js';
import recomputeInactiveProperties from '/imports/api/creature/denormalise/recomputeInactiveProperties.js';
import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js';
import recomputeInventory from '/imports/api/creature/denormalise/recomputeInventory.js';
const insertProperty = new ValidatedMethod({
name: 'creatureProperties.insert',
validate: null,
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({creatureProperty}) {
let rootCreature = getRootCreatureAncestor(creatureProperty);
assertEditPermission(rootCreature, this.userId);
return insertPropertyWork({
property: creatureProperty,
creature: rootCreature,
});
},
});
export function insertPropertyWork({property, creature}){
delete property._id;
let _id = CreatureProperties.insert(property);
// Tree structure changed by insert, reorder the tree
reorderDocs({
collection: CreatureProperties,
ancestorId: creature._id,
});
// Inserting the active status of the property needs to be denormalised
recomputeInactiveProperties(creature._id);
// Recompute the inventory if it has changed
if (property.type === 'item' || property.type === 'container'){
recomputeInventory(creature._id);
}
// Inserting a creature property invalidates dependencies: full recompute
recomputeCreatureByDoc(creature);
return _id;
}
export default insertProperty;

View File

@@ -0,0 +1,110 @@
import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
import { RefSchema } from '/imports/api/parenting/ChildSchema.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import recomputeInactiveProperties from '/imports/api/creature/denormalise/recomputeInactiveProperties.js';
import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import {
setLineageOfDocs,
getAncestry,
renewDocIds
} from '/imports/api/parenting/parenting.js';
import { reorderDocs } from '/imports/api/parenting/order.js';
import { setDocToLastOrder } from '/imports/api/parenting/order.js';
import recomputeInventory from '/imports/api/creature/denormalise/recomputeInventory.js';
const insertPropertyFromLibraryNode = new ValidatedMethod({
name: 'creatureProperties.insertPropertyFromLibraryNode',
validate: new SimpleSchema({
nodeId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
parentRef: {
type: RefSchema,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({nodeId, parentRef}) {
// get the new ancestry for the properties
let {parentDoc, ancestors} = getAncestry({parentRef});
// Check permission to edit
let rootCreature;
if (parentRef.collection === 'creatures'){
rootCreature = parentDoc;
} else if (parentRef.collection === 'creatureProperties'){
rootCreature = getRootCreatureAncestor(parentDoc);
} else {
throw `${parentRef.collection} is not a valid parent collection`
}
assertEditPermission(rootCreature, this.userId);
// 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 first in the array of nodes
// It must get the first generated ID to prevent flickering
nodes = [node, ...nodes];
// 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
CreatureProperties.batchInsert(nodes);
// get the root inserted doc
let rootId = node._id;
// Tree structure changed by inserts, reorder the tree
reorderDocs({
collection: CreatureProperties,
ancestorId: rootCreature._id,
});
// The library properties need to denormalise which of them are inactive
recomputeInactiveProperties(rootId);
// Some of the library properties may be items or containers
recomputeInventory(rootCreature._id);
// Inserting a creature property invalidates dependencies: full recompute
recomputeCreatureByDoc(rootCreature);
// Return the docId of the last property, the inserted root property
return rootId;
},
});
export default insertPropertyFromLibraryNode;

View File

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

View File

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

View File

@@ -0,0 +1,40 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import SimpleSchema from 'simpl-schema';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import { restore } from '/imports/api/parenting/softRemove.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import recomputeInactiveProperties from '/imports/api/creature/denormalise/recomputeInactiveProperties.js';
import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js';
import recomputeInventory from '/imports/api/creature/denormalise/recomputeInventory.js';
const restoreProperty = new ValidatedMethod({
name: 'creatureProperties.restore',
validate: new SimpleSchema({
_id: SimpleSchema.RegEx.Id
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({_id}){
// Permissions
let property = CreatureProperties.findOne(_id);
let rootCreature = getRootCreatureAncestor(property);
assertEditPermission(rootCreature, this.userId);
// Do work
restore({_id, collection: CreatureProperties});
// Items and containers might be restored
recomputeInventory(rootCreature._id);
// Parents active status may have changed while it was deleted
recomputeInactiveProperties(rootCreature._id);
// Changes dependency tree by restoring children
recomputeCreatureByDoc(rootCreature);
}
});
export default restoreProperty;

View File

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

View File

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

View File

@@ -0,0 +1,66 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js';
import recomputeInactiveProperties from '/imports/api/creature/denormalise/recomputeInactiveProperties.js';
import recomputeInventory from '/imports/api/creature/denormalise/recomputeInventory.js';
const updateCreatureProperty = new ValidatedMethod({
name: 'creatureProperties.update',
validate({_id, path}){
if (!_id) throw new Meteor.Error('No _id', '_id is required');
// We cannot change these fields with a simple update
switch (path[0]){
case 'type':
case 'order':
case 'parent':
case 'ancestors':
case 'damage':
throw new Meteor.Error('Permission denied',
'This property can\'t be updated directly');
}
},
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({_id, path, value}) {
// Permission
let property = CreatureProperties.findOne(_id, {
fields: {type: 1, ancestors: 1}
});
let rootCreature = getRootCreatureAncestor(property);
assertEditPermission(rootCreature, this.userId);
let pathString = path.join('.');
let modifier;
// unset empty values
if (value === null || value === undefined){
modifier = {$unset: {[pathString]: 1}};
} else {
modifier = {$set: {[pathString]: value}};
}
CreatureProperties.update(_id, modifier, {
selector: {type: property.type},
});
// Some updates might cause other properties to become inactive
if ([
'applied', 'equipped', 'prepared', 'alwaysPrepared', 'disabled'
].includes(path[0])){
recomputeInactiveProperties(rootCreature._id);
}
if (property.type === 'item' || property.type === 'container'){
// Potentially changes items and containers
recomputeInventory(rootCreature._id);
}
// Updating a property is likely to change dependencies, do a full recompute
recomputeCreatureByDoc(rootCreature);
},
});
export default updateCreatureProperty;

View File

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

View File

@@ -1,17 +1,24 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import SimpleSchema from 'simpl-schema';
import { assertEditPermission } from '/imports/api/creature/creaturePermissions.js';
import Creatures from '/imports/api/creature/Creatures.js';
import getActiveProperties from '/imports/api/creature/getActiveProperties.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
export const recomputeDamageMultipliers = new ValidatedMethod({
name: 'Creatures.methods.recomputeDamageMultipliers',
name: 'creatures.recomputeDamageMultipliers',
validate: new SimpleSchema({
creatureId: { type: String }
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({creatureId}) {
// Permission
assertEditPermission(creatureId, this.userId);
@@ -24,9 +31,13 @@ export const recomputeDamageMultipliers = new ValidatedMethod({
export function recomputeDamageMultipliersById(creatureId){
if (!creatureId) throw 'Creature ID is required';
let props = getActiveProperties({
ancestorId: creatureId,
filter: {type: 'damageMultiplier'},
let props = CreatureProperties.find({
'ancestors.id': creatureId,
type: 'damageMultiplier',
removed: {$ne: true},
inactive: {$ne: true},
}, {
sort: {order: 1}
});
// Count of how many weakness, resistances and immunities each damage type has

View File

@@ -0,0 +1,72 @@
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
export default function recomputeInactiveProperties(ancestorId){
let disabledFilter = {
'ancestors.id': ancestorId,
$or: [
{disabled: true}, // Everything can be disabled
{type: 'buff', applied: false}, // Buffs can be applied
{type: 'item', equipped: {$ne: true}},
{type: 'toggle', toggleResult: false},
{type: 'spell', prepared: {$ne: true}, alwaysPrepared: {$ne: true}},
],
};
let disabledIds = CreatureProperties.find(disabledFilter, {
fields: {_id: 1},
}).map(prop => prop._id);
// Deactivate relevant properties
// Inactive properties
CreatureProperties.update({
'ancestors.id': ancestorId,
'_id': {$in: disabledIds},
$or: [
{inactive: {$ne: true}},
{deactivatedBySelf: {$ne: true}},
{deactivatedByAncestor: true},
],
}, {
$set: {
inactive: true,
deactivatedBySelf: true,
},
$unset: {deactivatedByAncestor: 1},
}, {
multi: true,
selector: {type: 'any'},
});
// Decendants of inactive properties
CreatureProperties.update({
'ancestors.id': {$eq: ancestorId, $in: disabledIds},
$or: [
{inactive: {$ne: true}},
{deactivatedByAncestor: {$ne: true}},
],
}, {
$set: {
inactive: true,
deactivatedByAncestor: true,
},
}, {
multi: true,
selector: {type: 'any'},
});
// Remove inactive from all the properties that are inactive but shouldn't be
CreatureProperties.update({
'ancestors.id': {$eq: ancestorId, $nin: disabledIds},
'_id': {$nin: disabledIds},
$or: [
{inactive: true},
{deactivatedByAncestor: true},
],
}, {
$unset: {
inactive: 1,
deactivatedByAncestor: 1,
},
}, {
multi: true,
selector: {type: 'any'},
});
}

View File

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

View File

@@ -0,0 +1,43 @@
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
// n + 1 database queries + n potential updates for n slots. Could be sped up.
export default function recomputeSlotFullness(ancestorId){
CreatureProperties.find({
'ancestors.id': ancestorId,
type: 'propertySlot',
}).forEach(slot => {
let children = CreatureProperties.find({
'parent.id': slot._id,
removed: {$ne: true},
}, {
fields: {
slotQuantityFilled: 1,
type: 1
}
}).fetch();
let totalFilled = 0;
children.forEach(child => {
if (child.type === 'slotFiller'){
totalFilled += child.slotQuantityFilled;
} else {
totalFilled++;
}
});
let spaceLeft;
let expected = slot.quantityExpectedResult;
if (typeof expected !== 'number'){
expected = 1;
}
if (expected === 0){
spaceLeft = null;
} else {
spaceLeft = expected - totalFilled;
}
if (slot.totalFilled !== totalFilled || slot.spaceLeft !== spaceLeft){
CreatureProperties.update(slot._id, {
$set: {totalFilled, spaceLeft},
}, {
selector: {type: 'propertySlot'}
});
}
});
}

View File

@@ -0,0 +1,197 @@
import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import { getUserTier } from '/imports/api/users/patreon/tiers.js';
import { assertEditPermission } from '/imports/api/creature/creaturePermissions.js';
import Creatures from '/imports/api/creature/Creatures.js';
import { recomputeCreatureById } from '/imports/api/creature/computation/methods/recomputeCreature.js';
let Experiences = new Mongo.Collection('experiences');
let ExperienceSchema = new SimpleSchema({
name: {
type: String,
optional: true,
},
// The amount of XP this experience gives
xp: {
type: SimpleSchema.Integer,
optional: true,
min: 0,
},
// Setting levels instead of value grants whole levels
levels: {
type: SimpleSchema.Integer,
optional: true,
min: 0,
index: 1,
},
// The real-world date that it occured, usually sorted by date
date: {
type: Date,
autoValue: function() {
// If the date isn't set, set it to now
if (!this.isSet) {
return new Date();
}
},
index: 1,
},
creatureId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
index: 1,
},
});
Experiences.attachSchema(ExperienceSchema);
const insertExperienceForCreature = function({experience, creatureId, userId}){
assertEditPermission(creatureId, userId);
if (experience.xp){
Creatures.update(creatureId, {$inc: {
'denormalizedStats.xp': experience.xp
}});
}
if (experience.levels) {
Creatures.update(creatureId, {$inc: {
'denormalizedStats.milestoneLevels': experience.levels
}});
}
experience.creatureId = creatureId;
let id = Experiences.insert(experience);
recomputeCreatureById(creatureId);
return id;
};
const insertExperience = new ValidatedMethod({
name: 'experiences.insert',
validate: new SimpleSchema({
experience: {
type: ExperienceSchema.omit('creatureId'),
},
creatureIds: {
type: Array,
max: 12,
},
'creatureIds.$': {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({experience, creatureIds}) {
let userId = this.userId;
if (!userId) {
throw new Meteor.Error('Experiences.methods.insert.denied',
'You need to be logged in to insert an experience');
}
let tier = getUserTier(this.userId);
if (!tier.paidBenefits){
throw new Meteor.Error('Experiences.methods.insert.denied',
`The ${tier.name} tier does not allow you to grant experience`);
}
let insertedIds = [];
creatureIds.forEach(creatureId => {
let id = insertExperienceForCreature({experience, creatureId, userId});
insertedIds.push(id);
});
return insertedIds;
},
});
const removeExperience = new ValidatedMethod({
name: 'experiences.remove',
validate: new SimpleSchema({
experienceId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({experienceId}) {
let userId = this.userId;
if (!userId) {
throw new Meteor.Error('Experiences.methods.remove.denied',
'You need to be logged in to remove an experience');
}
let tier = getUserTier(this.userId);
if (!tier.paidBenefits){
throw new Meteor.Error('Experiences.methods.remove.denied',
`The ${tier.name} tier does not allow you to remove an experience`);
}
let experience = Experiences.findOne(experienceId);
if (!experience) return;
let creatureId = experience.creatureId
assertEditPermission(creatureId, userId);
if (experience.xp){
Creatures.update(creatureId, {$inc: {
'denormalizedStats.xp': -experience.xp
}});
}
if (experience.levels) {
Creatures.update(creatureId, {$inc: {
'denormalizedStats.milestoneLevels': -experience.levels
}});
}
experience.creatureId = creatureId;
let numRemoved = Experiences.remove(experienceId);
recomputeCreatureById(creatureId);
return numRemoved;
},
});
const recomputeExperiences = new ValidatedMethod({
name: 'experiences.recompute',
validate: new SimpleSchema({
creatureId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({creatureId}) {
let userId = this.userId;
if (!userId) {
throw new Meteor.Error('Experiences.methods.recompute.denied',
'You need to be logged in to recompute a creature\'s experiences');
}
let tier = getUserTier(this.userId);
if (!tier.paidBenefits){
throw new Meteor.Error('Experiences.methods.recompute.denied',
`The ${tier.name} tier does not allow you to recompute a creature's experiences`);
}
assertEditPermission(creatureId, userId);
let xp = 0;
let milestoneLevels = 0;
Experiences.find({
creatureId
}, {
fields: {xp: 1, levels: 1}
}).forEach(experience => {
xp += experience.xp || 0;
milestoneLevels += experience.levels || 0;
});
Creatures.update(creatureId, {$set: {
'denormalizedStats.xp': xp,
'denormalizedStats.milestoneLevels': milestoneLevels
}});
recomputeCreatureById(creatureId);
},
});
export default Experiences;
export { ExperienceSchema, insertExperience, removeExperience, recomputeExperiences };

View File

@@ -1,61 +0,0 @@
import Creatures from '/imports/api/creature/Creatures.js';
import CreatureProperties from '/imports/api/creature/CreatureProperties.js';
export default function getActiveProperties({
ancestorId,
filter = {},
options,
includeUntoggled = false
}){
filter = getActivePropertyFilter({ancestorId, filter, includeUntoggled});
return CreatureProperties.find(filter, options).fetch();
}
export function getActivePropertyFilter({
ancestorId,
filter = {},
includeUntoggled = false
}){
if (!ancestorId){
throw 'Ancestor Id is required to get active properties'
}
// First get ids of disabled properties, unequiped items, unapplied buffs
let disabledAncestorsFilter = {
'ancestors.id': ancestorId,
$or: [
{disabled: true}, // Everything can be disabled
{equipped: false}, // Items can be equipped
{applied: false}, // Buffs can be applied
],
};
if (!includeUntoggled){
disabledAncestorsFilter.$or.push({toggleResult: false});
}
let disabledAncestorIds = CreatureProperties.find(disabledAncestorsFilter, {
fields: {_id: 1},
}).map(prop => prop._id);
// Then get the ids of creatures that are children of this creature
// to isolate their decendent properties
Creatures.find({
'ancestors.id': ancestorId,
}, {
fields: {_id: 1},
}).forEach(subCreature => {
disabledAncestorIds.push(subCreature._id);
});
// Get all the properties that are decendents of the ancestor of interest but
// aren't from the excluded decendents
filter['ancestors.id'] = {
$eq: ancestorId,
$nin: disabledAncestorIds,
};
// Get properties that aren't removed
filter.removed = {$ne: true};
// Don't include the disabled ancestors themselves either
filter._id = {
$nin: disabledAncestorIds,
}
return filter;
}

View File

@@ -1,7 +1,7 @@
import SimpleSchema from 'simpl-schema';
let ExperienceSchema = new SimpleSchema({
name: {
title: {
type: String,
optional: true,
},
@@ -10,11 +10,6 @@ let ExperienceSchema = new SimpleSchema({
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,
@@ -30,6 +25,20 @@ let ExperienceSchema = new SimpleSchema({
type: String,
optional: true,
},
// Tags to better find this entry later
tags: {
type: Array,
defaultValue: [],
},
'tags.$': {
type: String,
},
// ID of the journal this entry belongs to
journalId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
index: 1,
}
});
export { ExperienceSchema };

View File

@@ -0,0 +1,231 @@
import SimpleSchema from 'simpl-schema';
import Creatures from '/imports/api/creature/Creatures.js';
import LogContentSchema from '/imports/api/creature/log/LogContentSchema.js';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import {assertEditPermission} from '/imports/api/creature/creaturePermissions.js';
import {
parse,
CompilationContext,
prettifyParseError
} from '/imports/parser/parser.js';
const PER_CREATURE_LOG_LIMIT = 100;
if (Meteor.isServer){
var sendWebhookAsCreature = require('/imports/server/discord/sendWebhook.js').sendWebhookAsCreature;
}
let CreatureLogs = new Mongo.Collection('creatureLogs');
let CreatureLogSchema = new SimpleSchema({
content: {
type: Array,
defaultValue: [],
maxCount: 25,
},
'content.$': {
type: LogContentSchema,
},
// The real-world date that it occured, usually sorted by date
date: {
type: Date,
autoValue: function() {
// If the date isn't set, set it to now
if (!this.isSet) {
return new Date();
}
},
index: 1,
},
creatureId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
index: 1,
},
creatureName: {
type: String,
optional: true,
},
});
CreatureLogs.attachSchema(CreatureLogSchema);
function removeOldLogs(creatureId){
// Find the first log that is over the limit
let firstExpiredLog = CreatureLogs.find({
creatureId
}, {
sort: {date: -1},
skip: PER_CREATURE_LOG_LIMIT,
});
// Remove all logs older than the one over the limit
CreatureLogs.remove({
creatureId,
date: {$lte: firstExpiredLog.date},
});
}
function logToMessageData(log){
let embed = {
fields: [],
};
log.content.forEach(c => {
let field = {};
let descriptionField = {};
if (c.name) field.name = c.name;
let valueArray = [];
if (c.error) valueArray.push(`*${c.error}*`);
if (c.resultPrefix) valueArray.push(`${c.resultPrefix}`);
if (c.result) valueArray.push(`\`${c.result}\``);
if (c.details) valueArray.push(c.details);
if (valueArray.length) field.value = valueArray.join(' ');
if (c.description){
if (!field.value){
field.value = c.description;
} else {
descriptionField.value = c.description;
}
}
if (field.name || field.value){
if (!field.name) field.name = '\u200b';
if (!field.value) field.value = '\u200b';
embed.fields.push(field);
}
if (descriptionField.value){
descriptionField.name = '\u200b';
embed.fields.push(descriptionField);
}
});
return { embeds: [embed] };
}
function logWebhook({log, creature}){
if (Meteor.isServer){
sendWebhookAsCreature({
creature,
data: logToMessageData(log),
});
}
}
const insertCreatureLog = new ValidatedMethod({
name: 'creatureLogs.methods.insertCreatureLog',
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
validate: new SimpleSchema({
log: CreatureLogSchema.omit('date'),
}).validator(),
run({log}){
const creatureId = log.creatureId;
const creature = Creatures.findOne(creatureId, {fields: {
readers: 1,
writers: 1,
owner: 1,
'settings.discordWebhook': 1,
name: 1,
avatarPicture: 1,
}});
assertEditPermission(creature, this.userId);
// Build the new log
let id = insertCreatureLogWork({log, creature, method: this})
return id;
},
});
export function insertCreatureLogWork({log, creature, method}){
// Build the new log
if (typeof log === 'string'){
log = {text: log};
}
log.date = new Date();
// Insert it
let id = CreatureLogs.insert(log);
if (Meteor.isServer){
method.unblock();
removeOldLogs(creature._id);
logWebhook({log, creature});
}
return id;
}
function equalIgnoringWhitespace(a, b){
if (typeof a !== 'string' || typeof b !== 'string') return a === b;
return a.replace(/\s/g,'') === b.replace(/\s/g, '');
}
const logRoll = new ValidatedMethod({
name: 'creatureLogs.methods.logForCreature',
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
validate: new SimpleSchema({
roll: {
type: String,
},
creatureId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
}).validator(),
run({roll, creatureId}){
const creature = Creatures.findOne(creatureId, {fields: {
variables: 1,
readers: 1,
writers: 1,
owner: 1,
'settings.discordWebhook': 1,
name: 1,
avatarPicture: 1,
}});
assertEditPermission(creature, this.userId);
let logContent = []
let parsedResult = undefined;
try {
parsedResult = parse(roll);
} catch (e){
let error = prettifyParseError(e);
logContent.push({error});
}
if (parsedResult) try {
let rollContext = new CompilationContext();
let compiled = parsedResult.compile(creature.variables, rollContext);
let compiledString = compiled.toString();
if (!equalIgnoringWhitespace(compiledString, roll)) logContent.push({
details: roll
});
logContent.push({
details: compiledString
});
let rolled = compiled.roll(creature.variables, rollContext);
let rolledString = rolled.toString();
if (rolledString !== compiledString) logContent.push({
result: rolled.toString()
});
let result = rolled.reduce(creature.variables, rollContext);
let resultString = result.toString();
if (resultString !== rolledString) logContent.push({
result: resultString
});
} catch (e){
logContent = [{error: 'Calculation error'}];
}
const log = {
content: logContent,
creatureId,
date: new Date(),
};
let id = insertCreatureLogWork({log, creature, method: this});
return id;
},
});
export default CreatureLogs;
export { CreatureLogSchema, insertCreatureLog, logRoll};

View File

@@ -0,0 +1,58 @@
import SimpleSchema from 'simpl-schema';
import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js';
import RollDetailsSchema from '/imports/api/properties/subSchemas/RollDetailsSchema.js';
let LogContentSchema = new SimpleSchema({
name: {
type: String,
optional: true,
},
error: {
type: String,
optional: true,
},
resultPrefix: {
type: String,
optional: true,
},
result: {
type: String,
optional: true,
},
expandedResult: {
type: String,
optional: true,
},
details: {
type: String,
optional: true,
},
description: {
type: String,
optional: true,
},
context: {
type: Object,
optional: true,
},
'context.errors':{
type: Array,
defaultValue: [],
},
'context.errors.$': {
type: ErrorSchema,
},
'context.rolls': {
type: Array,
defaultValue: [],
},
'context.rolls.$': {
type: RollDetailsSchema,
},
'context.doubleRolls': {
type: Boolean,
optional: true,
},
});
export default LogContentSchema;

View File

@@ -1,4 +1,4 @@
import { recomputeCreatureById } from '/imports/api/creature/computation/recomputeCreature.js';
import { recomputeCreatureById } from '/imports/api/creature/computation/methods/recomputeCreature.js';
export default function recomputeCreatureMixin(methodOptions){
let runFunc = methodOptions.run;

View File

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

View File

@@ -1,10 +1,10 @@
import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import Creatures from '/imports/api/creature/Creatures.js';
import CreatureProperties from '/imports/api/creature/CreatureProperties.js';
import getActiveProperties, { getActivePropertyFilter } from '/imports/api/creature/getActiveProperties.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { assertEditPermission } from '/imports/api/creature/creaturePermissions.js';
import { recomputeCreatureById } from '/imports/api/creature/computation/recomputeCreature.js';
import { recomputeCreatureById } from '/imports/api/creature/computation/methods/recomputeCreature.js';
const restCreature = new ValidatedMethod({
name: 'creature.methods.longRest',
@@ -18,6 +18,11 @@ const restCreature = new ValidatedMethod({
allowedValues: ['shortRest', 'longRest'],
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({creatureId, restType}) {
let creature = Creatures.findOne(creatureId, {
fields: {
@@ -37,11 +42,12 @@ const restCreature = new ValidatedMethod({
resetFilter = {$in: ['shortRest', 'longRest']}
}
// Only apply to active properties
let filter = getActivePropertyFilter({
filter: {reset: resetFilter},
ancestorId: creatureId,
includeUntoggled: true,
});
let filter = {
'ancestors.id': creatureId,
reset: resetFilter,
removed: {$ne: true},
inactive: {$ne: true},
};
// update all attribute's damage
filter.type = 'attribute';
CreatureProperties.update(filter, {
@@ -64,15 +70,19 @@ const restCreature = new ValidatedMethod({
});
// Reset half hit dice on a long rest, starting with the highest dice
if (restType === 'longRest'){
let hitDice = getActiveProperties({
ancestorId: creatureId,
filter: {type: 'attribute', attributeType: 'hitDice'},
options: {fields: {
let hitDice = CreatureProperties.find({
'ancestors.id': creatureId,
type: 'attribute',
attributeType: 'hitDice',
removed: {$ne: true},
inactive: {$ne: true},
}, {
fields: {
hitDiceSize: 1,
damage: 1,
value: 1,
}},
});
}
}).fetch();
// Use a collator to do sorting in natural order
let collator = new Intl.Collator('en', {
numeric: true, sensitivity: 'base'

View File

@@ -1,5 +1,6 @@
import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import { assertAdmin } from '/imports/api/sharing/sharingPermissions.js';
let Icons = new Mongo.Collection('icons');
@@ -48,7 +49,7 @@ Icons.attachSchema(iconsSchema);
// This method does not validate icons against the schema, use wisely;
const writeIcons = new ValidatedMethod({
name: 'icons.methods.write',
name: 'icons.write',
validate: null,
run(icons){
assertAdmin(this.userId);
@@ -60,7 +61,7 @@ const writeIcons = new ValidatedMethod({
});
const findIcons = new ValidatedMethod({
name: 'icons.methods.find',
name: 'icons.find',
validate: new SimpleSchema({
search: {
type: String,
@@ -68,6 +69,11 @@ const findIcons = new ValidatedMethod({
optional: true,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 20,
timeInterval: 10000,
},
run({search}){
if (!search) return [];
if (!Meteor.isServer) return;

View File

@@ -1,4 +1,5 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import SimpleSchema from 'simpl-schema';
import SharingSchema from '/imports/api/sharing/SharingSchema.js';
import simpleSchemaMixin from '/imports/api/creature/mixins/simpleSchemaMixin.js';
@@ -34,7 +35,7 @@ Libraries.attachSchema(LibrarySchema);
export default Libraries;
const insertLibrary = new ValidatedMethod({
name: 'Libraries.methods.insert',
name: 'libraries.insert',
mixins: [
simpleSchemaMixin,
],
@@ -55,7 +56,7 @@ const insertLibrary = new ValidatedMethod({
});
const updateLibraryName = new ValidatedMethod({
name: 'Libraries.methods.updateName',
name: 'libraries.updateName',
validate: new SimpleSchema({
_id: {
type: String,
@@ -65,6 +66,11 @@ const updateLibraryName = new ValidatedMethod({
type: String,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({_id, name}){
let library = Libraries.findOne(_id);
assertEditPermission(library, this.userId);
@@ -73,7 +79,7 @@ const updateLibraryName = new ValidatedMethod({
});
const setLibraryDefault = new ValidatedMethod({
name: 'Libraries.methods.makeLibraryDefault',
name: 'libraries.makeLibraryDefault',
validate: new SimpleSchema({
_id: {
type: String,
@@ -83,6 +89,11 @@ const setLibraryDefault = new ValidatedMethod({
type: Boolean,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({_id, isDefault}) {
if (!Meteor.users.isAdmin()){
throw new Meteor.Error('Permission denied', 'User must be admin to set libraries as default');
@@ -92,13 +103,18 @@ const setLibraryDefault = new ValidatedMethod({
});
const removeLibrary = new ValidatedMethod({
name: 'Libraries.methods.remove',
name: 'libraries.remove',
validate: new SimpleSchema({
_id: {
type: String,
regEx: SimpleSchema.RegEx.id
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({_id}){
let library = Libraries.findOne(_id);
assertOwnership(library, this.userId);

View File

@@ -1,6 +1,7 @@
import { Meteor } from 'meteor/meteor';
import { Mongo } from 'meteor/mongo';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import SimpleSchema from 'simpl-schema';
import ColorSchema from '/imports/api/properties/subSchemas/ColorSchema.js';
import ChildSchema from '/imports/api/parenting/ChildSchema.js';
@@ -31,6 +32,14 @@ let LibraryNodeSchema = new SimpleSchema({
}
});
// Set up server side search index
if (Meteor.isServer) {
LibraryNodes._ensureIndex({
'name': 'text',
'tags': 'text',
});
}
for (let key in propertySchemasIndex){
let schema = new SimpleSchema({});
schema.extend(LibraryNodeSchema);
@@ -56,8 +65,13 @@ function assertNodeEditPermission(node, userId){
}
const insertNode = new ValidatedMethod({
name: 'LibraryNodes.methods.insert',
name: 'libraryNodes.insert',
validate: null,
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run(libraryNode) {
delete libraryNode._id;
assertNodeEditPermission(libraryNode, this.userId);
@@ -66,13 +80,18 @@ const insertNode = new ValidatedMethod({
});
const duplicateNode = new ValidatedMethod({
name: 'LibraryNodes.methods.duplicate',
name: 'libraryNodes.duplicate',
validate: new SimpleSchema({
_id: {
type: String,
regEx: SimpleSchema.RegEx.Id,
}
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({_id}) {
let libraryNode = LibraryNodes.findOne(_id);
assertNodeEditPermission(libraryNode, this.userId);
@@ -82,7 +101,7 @@ const duplicateNode = new ValidatedMethod({
})
const updateLibraryNode = new ValidatedMethod({
name: 'LibraryNodes.methods.update',
name: 'libraryNodes.update',
validate({_id, path}){
if (!_id) return false;
// We cannot change these fields with a simple update
@@ -94,6 +113,11 @@ const updateLibraryNode = new ValidatedMethod({
return false;
}
},
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({_id, path, value}) {
let node = LibraryNodes.findOne(_id);
assertNodeEditPermission(node, this.userId);
@@ -112,8 +136,13 @@ const updateLibraryNode = new ValidatedMethod({
});
const pushToLibraryNode = new ValidatedMethod({
name: 'LibraryNodes.methods.push',
name: 'libraryNodes.push',
validate: null,
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({_id, path, value}){
let node = LibraryNodes.findOne(_id);
assertNodeEditPermission(node, this.userId);
@@ -126,8 +155,13 @@ const pushToLibraryNode = new ValidatedMethod({
});
const pullFromLibraryNode = new ValidatedMethod({
name: 'LibraryNodes.methods.pull',
name: 'libraryNodes.pull',
validate: null,
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({_id, path, itemId}){
let node = LibraryNodes.findOne(_id);
assertNodeEditPermission(node, this.userId);
@@ -141,10 +175,15 @@ const pullFromLibraryNode = new ValidatedMethod({
});
const softRemoveLibraryNode = new ValidatedMethod({
name: 'LibraryNodes.methods.softRemove',
name: 'libraryNodes.softRemove',
validate: new SimpleSchema({
_id: SimpleSchema.RegEx.Id
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({_id}){
let node = LibraryNodes.findOne(_id);
assertNodeEditPermission(node, this.userId);

View File

@@ -1,16 +1,18 @@
import SimpleSchema from 'simpl-schema';
import { union } from 'lodash';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import { updateParent } from '/imports/api/parenting/parenting.js';
import { reorderDocs, 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';
import { recomputeCreatureById } from '/imports/api/creature/computation/recomputeCreature.js';
import { recomputeCreatureById } from '/imports/api/creature/computation/methods/recomputeCreature.js';
import recomputeInactiveProperties from '/imports/api/creature/denormalise/recomputeInactiveProperties.js';
import recomputeInventory from '/imports/api/creature/denormalise/recomputeInventory.js';
const organizeDoc = new ValidatedMethod({
name: 'organize.methods.organizeDoc',
name: 'organize.organizeDoc',
validate: new SimpleSchema({
docRef: RefSchema,
parentRef: RefSchema,
@@ -18,8 +20,17 @@ const organizeDoc = new ValidatedMethod({
type: Number,
// Should end in 0.5 to place it reliably between two existing documents
},
skipRecompute: {
type: Boolean,
optional: true,
},
}).validator(),
run({docRef, parentRef, order}) {
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({docRef, parentRef, order, skipRecompute}) {
let doc = fetchDocByRef(docRef);
let collection = getCollectionByName(docRef.collection);
// The user must be able to edit both the doc and its parent to move it
@@ -45,16 +56,25 @@ const organizeDoc = new ValidatedMethod({
// Figure out which creatures need to be recalculated after this move
let docCreatures = getCreatureAncestors(doc);
let parentCreatures = getCreatureAncestors(parent);
let creaturesToRecompute = union(docCreatures, parentCreatures);
// Recompute the creatures
creaturesToRecompute.forEach(id => {
recomputeCreatureById(id);
});
if (!skipRecompute){
let creaturesToRecompute = union(docCreatures, parentCreatures);
// Recompute the creatures
creaturesToRecompute.forEach(id => {
// The active status of some properties might change due to a change in
// ancestry
recomputeInactiveProperties(id);
if (doc.type === 'container' || doc.type === 'item'){
recomputeInventory(id);
}
// Some Dependencies depend on ancestry, so a full recompute is needed
recomputeCreatureById(id);
});
}
},
});
const reorderDoc = new ValidatedMethod({
name: 'organize.methods.reorderDoc',
name: 'organize.reorderDoc',
validate: new SimpleSchema({
docRef: RefSchema,
order: {
@@ -62,6 +82,11 @@ const reorderDoc = new ValidatedMethod({
// Should end in 0.5 to place it reliably between two existing documents
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({docRef, order}) {
let doc = fetchDocByRef(docRef);
assertDocEditPermission(doc, this.userId);

View File

@@ -120,17 +120,23 @@ export function setLineageOfDocs({docArray, oldParent, newAncestry}){
export function renewDocIds({docArray, collectionMap}){
// map of {oldId: newId}
let idMap = {};
// Give new ids and map the changes
// Get a random generator that's consistent on client and server
let randomSrc = DDP.randomStream('renewDocIds');
// Give new ids and map the changes as {oldId: newId}
docArray.forEach(doc => {
let oldId = doc._id;
let newId = Random.id();
let newId = randomSrc.id();
doc._id = newId;
idMap[oldId] = newId;
});
// Remap all references using the new IDs
const remapReference = ref => {
if (idMap[ref.id]){
ref.id = idMap[ref.id];
ref.collection = collectionMap[ref.collection] || ref.collection;
ref.collection = collectionMap && collectionMap[ref.collection] || ref.collection;
}
}
docArray.forEach(doc => {
@@ -154,6 +160,14 @@ export function updateParent({docRef, parentRef}){
// Get the parent and its ancestry
let {parentDoc, parent, ancestors} = getAncestry({parentRef});
// Check that the doc isn't its own ancestor
ancestors.forEach(ancestor => {
if (docRef.id === ancestor.id){
throw new Meteor.Error('invalid parenting',
'A doc can\'t be its own ancestor')
}
});
// If the doc and its parent are in the same collection, apply the allowed
// parent rules based on type
if (docRef.collection === parentRef.collection){
@@ -204,17 +218,11 @@ export function getName(doc){
}
}
export function nodesToTree({collection, ancestorId, filter, options}){
// Store a dict of all the nodes
export function nodeArrayToTree(nodes){
// Store a dict and list of all the nodes
let nodeIndex = {};
let nodeList = [];
if (!options) options = {};
options.sort = {order: 1};
collection.find({
'ancestors.id': ancestorId,
removed: {$ne: true},
...filter,
}, options).forEach( node => {
nodes.forEach( node => {
let treeNode = {
node: node,
children: [],
@@ -238,3 +246,12 @@ export function nodesToTree({collection, ancestorId, filter, options}){
});
return forest;
}
export function nodesToTree({collection, ancestorId, filter = {}, options = {}}){
if (!('ancestors.id' in filter)) filter['ancestors.id'] = ancestorId;
if (!('removed' in filter)) filter['removed'] = {$ne: true};
if (!options.sort) options.sort = {order: 1};
if (!('order' in options.sort)) options.sort.order = 1;
let nodes = collection.find(filter, options);
return nodeArrayToTree(nodes);
}

View File

@@ -41,16 +41,21 @@ const restoreError = function(){
};
export function restore({_id, collection}){
collection = getCollectionByName(collection);
if (typeof collection === 'string') {
collection = getCollectionByName(collection);
}
let numUpdated = collection.update({
_id,
removedWith: {$exists: false}
}, { $unset: {
removed: 1,
removedAt: 1,
}});
}}, {
selector: {type: 'any'},
},);
if (numUpdated === 0) restoreError();
updateDescendants({
collection,
ancestorId: _id,
filter: {
removedWith: _id,

View File

@@ -1,6 +1,7 @@
import SimpleSchema from 'simpl-schema';
import ResourcesSchema from '/imports/api/properties/subSchemas/ResourcesSchema.js'
import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js';
import InlineComputationSchema from '/imports/api/properties/subSchemas/InlineComputationSchema.js';
import { storedIconsSchema } from '/imports/api/icons/Icons.js'
/*
* Actions are things a character can do
* Any rolls that are children of actions will be rolled when taking the action
@@ -12,6 +13,10 @@ let ActionSchema = new SimpleSchema({
type: String,
optional: true,
},
summary: {
type: String,
optional: true,
},
description: {
type: String,
optional: true,
@@ -40,13 +45,60 @@ let ActionSchema = new SimpleSchema({
'tags.$': {
type: String,
},
// Duplicate the ResourceSchema here so we can extend it elegantly.
resources: {
type: ResourcesSchema,
type: Object,
defaultValue: {},
},
'resources.itemsConsumed': {
type: Array,
defaultValue: [],
},
'resources.itemsConsumed.$': {
type: Object,
},
'resources.itemsConsumed.$._id': {
type: String,
regEx: SimpleSchema.RegEx.Id,
autoValue(){
if (!this.isSet) return Random.id();
}
},
'resources.itemsConsumed.$.tag': {
type: String,
optional: true,
},
'resources.itemsConsumed.$.quantity': {
type: Number,
defaultValue: 1,
},
'resources.itemsConsumed.$.itemId': {
type: String,
optional: true,
},
'resources.attributesConsumed': {
type: Array,
defaultValue: [],
},
'resources.attributesConsumed.$': {
type: Object,
},
'resources.attributesConsumed.$._id': {
type: String,
regEx: SimpleSchema.RegEx.Id,
autoValue(){
if (!this.isSet) return Random.id();
}
},
'resources.attributesConsumed.$.variableName': {
type: String,
optional: true,
},
'resources.attributesConsumed.$.quantity': {
type: Number,
defaultValue: 1,
},
// 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,
@@ -64,4 +116,83 @@ let ActionSchema = new SimpleSchema({
},
});
export { ActionSchema };
const ComputedOnlyActionSchema = new SimpleSchema({
summaryCalculations: {
type: Array,
defaultValue: [],
maxCount: 32,
},
'summaryCalculations.$': InlineComputationSchema,
descriptionCalculations: {
type: Array,
defaultValue: [],
maxCount: 32,
},
'descriptionCalculations.$': InlineComputationSchema,
usesResult: {
type: SimpleSchema.Integer,
optional: true,
},
usesErrors: {
type: Array,
optional: true,
},
'usesErrors.$':{
type: ErrorSchema,
},
resources: Object,
'resources.itemsConsumed': Array,
'resources.itemsConsumed.$': Object,
'resources.itemsConsumed.$.available': {
type: Number,
optional: true,
},
// This appears both in the computed and uncomputed schema because it can be
// set by both a computation or a form
'resources.itemsConsumed.$.itemId': {
type: String,
regEx: SimpleSchema.RegEx.Id,
optional: true,
},
'resources.itemsConsumed.$.itemName': {
type: String,
optional: true,
},
'resources.itemsConsumed.$.itemIcon': {
type: storedIconsSchema,
optional: true,
},
'resources.itemsConsumed.$.itemColor': {
type: String,
optional: true,
},
'resources.attributesConsumed': Array,
'resources.attributesConsumed.$': Object,
'resources.attributesConsumed.$.available': {
type: Number,
optional: true,
},
'resources.attributesConsumed.$.statId': {
type: String,
regEx: SimpleSchema.RegEx.Id,
optional: true,
},
'resources.attributesConsumed.$.statName': {
type: String,
optional: true,
},
// True if the uses left is zero, or any item or attribute consumed is
// insufficient
insufficientResources: {
type: Boolean,
optional: true,
},
});
const ComputedActionSchema = new SimpleSchema()
.extend(ActionSchema)
.extend(ComputedOnlyActionSchema);
export { ActionSchema, ComputedOnlyActionSchema, ComputedActionSchema};

View File

@@ -1,7 +1,9 @@
import SimpleSchema from 'simpl-schema';
import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js';
const AdjustmentSchema = new SimpleSchema({
// The roll that determines how much to change the attribute
// This can be simplified, but should only compute when activated
amount: {
type: String,
optional: true,
@@ -12,7 +14,7 @@ const AdjustmentSchema = new SimpleSchema({
type: String,
defaultValue: 'every',
allowedValues: [
'self', // the character who took the action
'self', // the character who took the Adjustment
'each', // rolled once for `each` target
'every', // rolled once and applied to `every` target
],
@@ -22,6 +24,29 @@ const AdjustmentSchema = new SimpleSchema({
type: String,
optional: true,
},
operation: {
type: String,
allowedValues: ['set', 'increment'],
defaultValue: 'increment',
},
});
export { AdjustmentSchema };
const ComputedOnlyAdjustmentSchema = new SimpleSchema({
amountResult: {
type: SimpleSchema.Integer,
optional: true,
},
amountErrors: {
type: Array,
optional: true,
},
'amountErrors.$':{
type: ErrorSchema,
},
});
const ComputedAdjustmentSchema = new SimpleSchema()
.extend(AdjustmentSchema)
.extend(ComputedOnlyAdjustmentSchema);
export { AdjustmentSchema, ComputedAdjustmentSchema, ComputedOnlyAdjustmentSchema };

View File

@@ -1,5 +1,6 @@
import SimpleSchema from 'simpl-schema';
import { ActionSchema } from '/imports/api/properties/Actions.js';
import { ActionSchema, ComputedOnlyActionSchema } from '/imports/api/properties/Actions.js';
import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js';
// Attacks are special instances of actions
let AttackSchema = new SimpleSchema()
@@ -25,4 +26,24 @@ let AttackSchema = new SimpleSchema()
},
});
export { AttackSchema };
const ComputedOnlyAttackSchema = new SimpleSchema()
.extend(ComputedOnlyActionSchema)
.extend({
rollBonusResult: {
type: Number,
optional: true,
},
rollBonusErrors: {
type: Array,
optional: true,
},
'rollBonusErrors.$':{
type: ErrorSchema,
},
});
const ComputedAttackSchema = new SimpleSchema()
.extend(AttackSchema)
.extend(ComputedOnlyAttackSchema);
export { AttackSchema, ComputedOnlyAttackSchema, ComputedAttackSchema };

View File

@@ -1,6 +1,7 @@
import SimpleSchema from 'simpl-schema';
import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js';
import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js';
import InlineComputationSchema from '/imports/api/properties/subSchemas/InlineComputationSchema.js';
/*
* Attributes are numbered stats of a character
@@ -39,6 +40,11 @@ let AttributeSchema = new SimpleSchema({
type: String,
allowedValues: ['d4', 'd6', 'd8', 'd10', 'd12', 'd20'],
optional: true,
},
// For type spellSlot, the level needs to be stored separately
spellSlotLevelCalculation: {
type: String,
optional: true,
},
// The starting value, before effects
baseValueCalculation: {
@@ -70,6 +76,12 @@ let AttributeSchema = new SimpleSchema({
});
let ComputedOnlyAttributeSchema = new SimpleSchema({
descriptionCalculations: {
type: Array,
defaultValue: [],
maxCount: 32,
},
'descriptionCalculations.$': InlineComputationSchema,
// The result of baseValueCalculation
baseValue: {
type: SimpleSchema.oneOf(Number, String, Boolean),
@@ -81,6 +93,18 @@ let ComputedOnlyAttributeSchema = new SimpleSchema({
},
'baseValueErrors.$': {
type: ErrorSchema,
},
// The result of spellSlotLevelCalculation
spellSlotLevelValue: {
type: SimpleSchema.oneOf(Number, String, Boolean),
optional: true,
},
spellSlotLevelErrors: {
type: Array,
optional: true,
},
'spellSlotLevelErrors.$': {
type: ErrorSchema,
},
// The computed value of the attribute
value: {
@@ -99,6 +123,11 @@ let ComputedOnlyAttributeSchema = new SimpleSchema({
type: SimpleSchema.Integer,
optional: true,
},
// The computed creature constitution modifier
constitutionMod: {
type: Number,
optional: true,
},
// Should this attribute hide
hide: {
type: Boolean,

View File

@@ -1,4 +1,5 @@
import SimpleSchema from 'simpl-schema';
import InlineComputationSchema from '/imports/api/properties/subSchemas/InlineComputationSchema.js';
let BuffSchema = new SimpleSchema({
name: {
@@ -29,12 +30,13 @@ let BuffSchema = new SimpleSchema({
},
});
let AppliedBuffSchema = new SimpleSchema({
applied: {
type: Boolean,
defaultValue: true,
index: 1,
let ComputedOnlyBuffSchema = new SimpleSchema({
descriptionCalculations: {
type: Array,
defaultValue: [],
maxCount: 32,
},
'descriptionCalculations.$': InlineComputationSchema,
durationSpent: {
type: Number,
optional: true,
@@ -54,6 +56,10 @@ let AppliedBuffSchema = new SimpleSchema({
'appliedBy.collection': {
type: String,
},
}).extend(BuffSchema);
})
export { AppliedBuffSchema, BuffSchema };
const ComputedBuffSchema = new SimpleSchema()
.extend(BuffSchema)
.extend(ComputedOnlyBuffSchema);
export { BuffSchema, ComputedOnlyBuffSchema, ComputedBuffSchema };

View File

@@ -6,6 +6,11 @@ let ClassLevelSchema = new SimpleSchema({
type: String,
optional: true,
},
// Only used by slot filling dialog, not computed
description: {
type: String,
optional: true,
},
// The name of this class level's variable
variableName: {
type: String,
@@ -23,6 +28,11 @@ let ClassLevelSchema = new SimpleSchema({
'nextLevelTags.$': {
type: String,
},
// Same as in SlotFillers.js
slotFillerCondition: {
type: String,
optional: true,
},
});
export { ClassLevelSchema };

View File

@@ -0,0 +1,82 @@
import SimpleSchema from 'simpl-schema';
import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js';
import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js';
import {
parse,
CompilationContext,
prettifyParseError,
} from '/imports/parser/parser.js';
import AccessorNode from '/imports/parser/parseTree/AccessorNode.js';
import SymbolNode from '/imports/parser/parseTree/SymbolNode.js';
/*
* Constants are primitive values that can be used elsewhere in computations
*/
let ConstantSchema = new SimpleSchema({
name: {
type: String,
optional: true,
},
// The technical, lowercase, single-word name used in formulae
variableName: {
type: String,
regEx: VARIABLE_NAME_REGEX,
min: 2,
defaultValue: 'newConstant',
},
// The input value to be parsed, must return a constant node or an array
// of constant nodes to be valid
calculation: {
type: String,
optional: true,
},
errors: {
type: Array,
autoValue(){
let calc = this.field('calculation');
if (!calc.isSet && this.isModifier) {
this.unset()
return;
}
let string = calc.value;
if (!string) return [];
// Evaluate the calculation with no scope
let {result, context} = parseString(string);
// Any existing errors will result in an early failure
if (context && context.errors.length) return context.errors;
// Ban variables in constants if necessary
result && result.traverse(node => {
if (node instanceof SymbolNode || node instanceof AccessorNode){
context.storeError({
type: 'error',
message: 'Variables can\'t be used to define a constant'
});
}
});
return context && context.errors || [];
}
},
'errors.$':{
type: ErrorSchema,
},
});
function parseString(string, fn = 'compile'){
let context = new CompilationContext();
if (!string){
return {result: string, context};
}
// Parse the string using mathjs
let node;
try {
node = parse(string);
} catch (e) {
let message = prettifyParseError(e);
context.storeError({type: 'error', message});
return {context};
}
let result = node[fn]({/*empty scope*/}, context);
return {result, context}
}
export { ConstantSchema };

View File

@@ -1,4 +1,5 @@
import SimpleSchema from 'simpl-schema';
import InlineComputationSchema from '/imports/api/properties/subSchemas/InlineComputationSchema.js';
let ContainerSchema = new SimpleSchema({
name: {
@@ -18,12 +19,12 @@ let ContainerSchema = new SimpleSchema({
weight: {
type: Number,
min: 0,
defaultValue: 0
optional: true,
},
value: {
type: Number,
min: 0,
defaultValue: 0
optional: true,
},
description: {
type: String,
@@ -32,4 +33,26 @@ let ContainerSchema = new SimpleSchema({
},
});
export { ContainerSchema };
const ComputedOnlyContainerSchema = new SimpleSchema({
descriptionCalculations: {
type: Array,
defaultValue: [],
maxCount: 32,
},
'descriptionCalculations.$': InlineComputationSchema,
// Weight of all the contents, zero if `contentsWeightless` is true
contentsWeight:{
type: Number,
optional: true,
},
contentsValue:{
type: Number,
optional: true,
},
});
const ComputedContainerSchema = new SimpleSchema()
.extend(ComputedOnlyContainerSchema)
.extend(ContainerSchema);
export { ContainerSchema, ComputedOnlyContainerSchema, ComputedContainerSchema };

View File

@@ -1,8 +1,10 @@
import SimpleSchema from 'simpl-schema';
import DAMAGE_TYPES from '/imports/constants/DAMAGE_TYPES.js';
import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js';
const DamageSchema = new SimpleSchema({
// The roll that determines how much to damage the attribute
// This can be simplified, but only computed when applied
amount: {
type: String,
optional: true,
@@ -25,4 +27,22 @@ const DamageSchema = new SimpleSchema({
},
});
export { DamageSchema };
const ComputedOnlyDamageSchema = new SimpleSchema({
amountResult: {
type: SimpleSchema.oneOf(String, SimpleSchema.Integer),
optional: true,
},
amountErrors: {
type: Array,
optional: true,
},
'amountErrors.$':{
type: ErrorSchema,
},
});
const ComputedDamageSchema = new SimpleSchema()
.extend(DamageSchema)
.extend(ComputedOnlyDamageSchema);
export { DamageSchema, ComputedDamageSchema, ComputedOnlyDamageSchema };

View File

@@ -38,7 +38,6 @@ let EffectSchema = new SimpleSchema({
},
'stats.$': {
type: String,
optional: true,
},
});

View File

@@ -1,4 +1,5 @@
import SimpleSchema from 'simpl-schema';
import InlineComputationSchema from '/imports/api/properties/subSchemas/InlineComputationSchema.js';
let FeatureSchema = new SimpleSchema({
name: {
@@ -14,4 +15,26 @@ let FeatureSchema = new SimpleSchema({
},
});
export { FeatureSchema }
let ComputedOnlyFeatureSchema = new SimpleSchema({
summaryCalculations: {
type: Array,
defaultValue: [],
maxCount: 32,
},
'summaryCalculations.$': InlineComputationSchema,
descriptionCalculations: {
type: Array,
defaultValue: [],
maxCount: 32,
},
'descriptionCalculations.$': InlineComputationSchema,
});
const ComputedFeatureSchema = new SimpleSchema()
.extend(FeatureSchema)
.extend(ComputedOnlyFeatureSchema);
export { FeatureSchema, ComputedFeatureSchema, ComputedOnlyFeatureSchema }

View File

@@ -1,4 +1,5 @@
import SimpleSchema from 'simpl-schema';
import InlineComputationSchema from '/imports/api/properties/subSchemas/InlineComputationSchema.js';
const ItemSchema = new SimpleSchema({
name: {
@@ -33,7 +34,6 @@ const ItemSchema = new SimpleSchema({
optional: true,
},
// If this item is equipped, it requires attunement
// Being equipped is `enabled === true`
requiresAttunement: {
type: Boolean,
optional: true,
@@ -54,4 +54,17 @@ const ItemSchema = new SimpleSchema({
},
});
export { ItemSchema };
let ComputedOnlyItemSchema = new SimpleSchema({
descriptionCalculations: {
type: Array,
defaultValue: [],
maxCount: 32,
},
'descriptionCalculations.$': InlineComputationSchema,
});
const ComputedItemSchema = new SimpleSchema()
.extend(ItemSchema)
.extend(ComputedOnlyItemSchema);
export { ItemSchema, ComputedItemSchema, ComputedOnlyItemSchema };

View File

@@ -1,14 +1,41 @@
import SimpleSchema from 'simpl-schema';
import InlineComputationSchema from '/imports/api/properties/subSchemas/InlineComputationSchema.js';
let NoteSchema = new SimpleSchema({
name: {
type: String,
optional: true,
},
summary: {
type: String,
optional: true,
},
description: {
type: String,
optional: true,
},
});
export { NoteSchema };
let ComputedOnlyNoteSchema = new SimpleSchema({
summaryCalculations: {
type: Array,
defaultValue: [],
maxCount: 32,
},
'summaryCalculations.$': InlineComputationSchema,
descriptionCalculations: {
type: Array,
defaultValue: [],
maxCount: 32,
},
'descriptionCalculations.$': InlineComputationSchema,
});
const ComputedNoteSchema = new SimpleSchema()
.extend(NoteSchema)
.extend(ComputedOnlyNoteSchema);
export { NoteSchema, ComputedNoteSchema, ComputedOnlyNoteSchema, };

View File

@@ -12,7 +12,6 @@ let ProficiencySchema = new SimpleSchema({
},
'stats.$': {
type: String,
optional: true,
},
// A number representing how proficient the character is
value: {

View File

@@ -1,4 +1,6 @@
import SimpleSchema from 'simpl-schema';
import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js';
import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js';
/**
* Rolls are children to actions or other rolls, they are triggered with 0 or
@@ -19,7 +21,18 @@ import SimpleSchema from 'simpl-schema';
* child rolls are applied
*/
let RollSchema = new SimpleSchema({
// The roll
name: {
type: String,
defaultValue: 'New Roll',
},
// The technical, lowercase, single-word name used in formulae
variableName: {
type: String,
regEx: VARIABLE_NAME_REGEX,
min: 2,
defaultValue: 'newRoll',
},
// The roll, can be simplified, but only computed in context
roll: {
type: String,
optional: true,
@@ -35,4 +48,22 @@ let RollSchema = new SimpleSchema({
},
});
export { RollSchema };
let ComputedOnlyRollSchema = new SimpleSchema({
rollResult: {
type: SimpleSchema.Integer,
optional: true,
},
rollErrors: {
type: Array,
optional: true,
},
'rollErrors.$':{
type: ErrorSchema,
},
});
const ComputedRollSchema = new SimpleSchema()
.extend(RollSchema)
.extend(ComputedOnlyRollSchema);
export { RollSchema, ComputedRollSchema, ComputedOnlyRollSchema };

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