Compare commits

...

440 Commits

Author SHA1 Message Date
Stefan Zermatten
b876c2801d Greyed out inactive props in the tree 2022-03-04 12:51:48 +02:00
Stefan Zermatten
698c9c7bbf Fixed adjustment error when trying to adjust a property that isn't set 2022-03-04 12:51:28 +02:00
Stefan Zermatten
7544243640 Fixed buffs not crystalising variables correctly 2022-03-04 12:51:06 +02:00
Stefan Zermatten
4b4e3a8928 Improve hover highlight UI effects for cards in dark mode
In light mode a change in elevation changes the drop shadow, but this is 
all but invisible in dark mode, so I added a highlight to the cards when 
hovering to show that the card can be expanded with a click
2022-03-03 17:21:59 +02:00
Stefan Zermatten
92a588bfcc Added slotFillerCondition field to class levels, same as in slot filler 2022-03-03 16:19:54 +02:00
Stefan Zermatten
43e956eb6a Fixed skills not obeying baseValue correctly 2022-03-03 15:55:07 +02:00
Stefan Zermatten
c4429f5dd7 Item quantity adjustment button now shows loading while in progress 2022-03-03 15:54:44 +02:00
Stefan Zermatten
4edfe1bcb9 Fixed inventory calculation to include item quantities 2022-03-03 15:53:58 +02:00
Stefan Zermatten
473a9f0253 Inlined a bunch of discord webhook text to help format messages better 2022-02-28 16:55:15 +02:00
Stefan Zermatten
94cdca4f31 Fixed uses left not logging correctly in actions 2022-02-28 16:25:42 +02:00
Stefan Zermatten
10d0a3f763 Added attack roll back to spell form 2022-02-28 16:13:52 +02:00
Stefan Zermatten
afe6c044cd Fixed dependency graph not building correctly for resources consumed 2022-02-28 00:02:55 +02:00
Stefan Zermatten
e6c7d79d7d Overhauled spell casting UX 2022-02-27 22:14:32 +02:00
Stefan Zermatten
49fa9cc470 Fixed parser to allow $ and x.0.thing in variable references 2022-02-26 19:36:56 +02:00
Stefan Zermatten
3646c13355 Merge branch 'version-2-dev' into version-2 2022-02-26 17:35:58 +02:00
Stefan Zermatten
27665e0bdc Finished roll check and roll attack buttons from stats page 2022-02-26 17:35:26 +02:00
Stefan Zermatten
fea29e60b7 Fixed inactive effects showing up on skill detail view 2022-02-26 15:21:08 +02:00
Stefan Zermatten
653f05012a Reversed the order of the creature compute dependency graph traversal
By doing this the traversal happens mostly in tree order, which is a 
better assumption of starting point in cases where there are dependency 
loops
2022-02-26 14:58:38 +02:00
Stefan Zermatten
7ee4a22d77 Fixed error where dependency loops including classLevels break the sheet 2022-02-26 13:06:00 +02:00
Stefan Zermatten
59c69a46a8 Attacks can now be rolled with advantage from the stats tab
TODO the action viewer as well still
2022-02-25 13:44:09 +02:00
Stefan Zermatten
f79a6d98ec Updated meteor 2022-02-25 12:27:59 +02:00
Stefan Zermatten
0ffa736143 Fixed dbv1 migration to match applied data patches 2022-02-25 12:27:52 +02:00
Stefan Zermatten
f1b4071c46 Inline calculation fields now reduce 2022-02-25 12:27:26 +02:00
Stefan Zermatten
249ece352c Fixed missing slot filler description 2022-02-25 10:28:09 +02:00
Stefan Zermatten
4fe3f30090 Merge branch 'version-2' of https://github.com/ThaumRystra/DiceCloud into version-2 2022-02-24 10:59:16 +02:00
Stefan Zermatten
44d3fbc065 Fixed slot filler viewer not having markdown for the description 2022-02-24 10:59:02 +02:00
Stefan Zermatten
b1feb126df Fixed inventory weight and value fields 2022-02-24 02:39:39 +02:00
Stefan Zermatten
69f9636688 Fixed spell lists and class levels not computing inline calculations 2022-02-23 17:01:12 +02:00
Stefan Zermatten
5383804af7 Fixed error with damage failing to apply if existing damage was undefined 2022-02-23 16:17:34 +02:00
Stefan Zermatten
0b8c88daef Began work on buttons to make rolls from the sheet 2022-02-23 16:08:04 +02:00
Stefan Zermatten
5b6bff91a4 Added resolve function to allow users to force a calculation to reduce 2022-02-23 12:58:12 +02:00
Stefan Zermatten
52453b46e9 Fixed experience not appearing as a variable after computation 2022-02-23 11:44:59 +02:00
Stefan Zermatten
78c67a4fd6 Fixed incorrect use of parser toString in places 2022-02-23 11:07:02 +02:00
Stefan Zermatten
90b277e181 Fixed not operator !working 2022-02-22 19:16:03 +02:00
Stefan Zermatten
dc4d0416a2 Fixed spells disabled by toggles still appearing in spell lists 2022-02-22 19:07:40 +02:00
Stefan Zermatten
12a0dff43f Hacked over ddp error that was not updating removed field correctly 2022-02-22 18:31:06 +02:00
Stefan Zermatten
b9f79f1c51 Fixed buffs missing from stats page 2022-02-22 18:10:04 +02:00
Stefan Zermatten
92d32e7cf8 Fixed tag layout in effect viewer for many tags overlapping one another 2022-02-22 18:07:18 +02:00
Stefan Zermatten
80460ceaed Fixed not found calculation warnings showing [object Object]
They were using the wrong "toString" method
2022-02-22 18:02:57 +02:00
Stefan Zermatten
8f30c1419c Fixed slots and slot fillers not calculating their conditions correctly
Also fixes slot fullness calculation
2022-02-22 17:59:12 +02:00
Stefan Zermatten
4c6d70b084 Fixed . in effect stat targets breaking entire sheet 2022-02-22 17:30:45 +02:00
Stefan Zermatten
ee2b400ee6 Fixed spell list card not showing maxPrepared spells correctly 2022-02-22 16:38:50 +02:00
Stefan Zermatten
ef8aafc1a1 Fixed storagepath for production 2022-02-22 13:03:37 +02:00
Stefan Zermatten
b68637e525 Updated node version 2022-02-22 12:31:47 +02:00
Stefan Zermatten
537eb310a8 Merge branch 'version-2-dev' into version-2 2022-02-22 12:07:10 +02:00
Stefan Zermatten
352c4d516d Fixed effect form not showing targetField value when set 2022-02-15 16:19:00 +02:00
Stefan Zermatten
378da71f5d Effects targeting calculations by tag now work in the engine and actions 2022-02-15 15:59:41 +02:00
Stefan Zermatten
e0f621cc44 Added data and UI for effects targeting calculations by tag
Still need to:
- update engine to compute calculations with effects.
- Add UI for effects applied to each calculation
2022-02-14 16:26:49 +02:00
Stefan Zermatten
359f18988c Account functionality extended, API authentication implemented
- Can now add a second email address to your account and delete one of 
your email addresses
- Reset password now works
- Resetting the password of an account without a password set will set 
one
- Email templates overhauled
- Login tokens limited to close previously devastating ($800 database 
bill) security hole
- Login with REST API now works
- Once logged in, authentication of API calls with token works
- Creatures can now be fetched using the API
2022-02-10 19:02:18 +02:00
Stefan Zermatten
3948d20f46 Allowed cross origin requests to REST API 2022-02-10 10:37:23 +02:00
Stefan Zermatten
76982e234c Merge pull request #285 from WeslleyNasRocha/patch-1
tableLookup function was returning a string
2022-02-10 09:22:29 +02:00
Stefan Zermatten
db222362bb Merge pull request #283 from ThaumRystra/dependabot/npm_and_yarn/app/tar-6.1.11
Bump tar from 6.1.6 to 6.1.11 in /app
2022-02-10 09:20:42 +02:00
Stefan Zermatten
7b02899824 Merge pull request #289 from ThaumRystra/dependabot/npm_and_yarn/app/marked-4.0.10
Bump marked from 0.8.2 to 4.0.10 in /app
2022-02-10 09:20:18 +02:00
Stefan Zermatten
15ead403a5 Added UI for action branches 2022-02-09 16:47:38 +02:00
Stefan Zermatten
2bdd60b5e8 Fixed issue where migrating attributes lost their base value calculation 2022-02-09 12:37:16 +02:00
Stefan Zermatten
78c313e3d1 Archives and restore now works to S3 and file system
If a file is stored on the file system and s3 settings later become 
available it is still correctly fetched from the file system.
2022-02-03 11:48:03 +02:00
Stefan Zermatten
2abaa86795 Began work on moving file storage to s3, not working yet 2022-01-19 16:01:07 +02:00
Stefan Zermatten
90820452af Updated packages 2022-01-16 20:27:14 +02:00
dependabot[bot]
3b438c8ba4 Bump marked from 0.8.2 to 4.0.10 in /app
Bumps [marked](https://github.com/markedjs/marked) from 0.8.2 to 4.0.10.
- [Release notes](https://github.com/markedjs/marked/releases)
- [Changelog](https://github.com/markedjs/marked/blob/master/.releaserc.json)
- [Commits](https://github.com/markedjs/marked/compare/v0.8.2...v4.0.10)

---
updated-dependencies:
- dependency-name: marked
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-15 02:25:34 +00:00
Stefan Zermatten
d845a8f17e Added big scary message to future me to backup before migrating data 2021-12-29 14:42:36 +02:00
Stefan Zermatten
ed1873babe App can now go into maintenance mode locking out routing 2021-12-29 14:25:01 +02:00
Stefan Zermatten
2cf19d1ee5 Fixed typo breaking archive restore 2021-12-27 18:04:22 +02:00
Stefan Zermatten
2cd784c92b Updated Meteor to 2.5.1 2021-12-27 17:48:34 +02:00
Stefan Zermatten
dca55d1d00 Reduced bundle size using dynamic imports for Vue components 2021-12-27 16:29:43 +02:00
Stefan Zermatten
1dc03c8a28 Fixed failing test 2021-12-20 12:22:13 +02:00
Stefan Zermatten
510136e07f When restoring archived files, migration happens automatically 2021-12-20 12:20:50 +02:00
Stefan Zermatten
a5f5baf1cc DB archive characters are migrated and moved to file archive on migrate 2021-12-20 11:53:02 +02:00
Stefan Zermatten
1e10d8751b Archive now uses file system instead of collection 2021-12-19 12:20:09 +02:00
Stefan Zermatten
211659f759 Fixed styling of app name on mobile installed versions 2021-12-14 09:48:39 +02:00
Stefan Zermatten
86f5da3ca5 Fixed rolling straight to the log to use new parser interface 2021-12-08 10:24:06 +02:00
Stefan Zermatten
a58fd8860d Fixed descriptions having borders when not appropriate 2021-12-08 09:31:02 +02:00
Stefan Zermatten
6e22e4286f Fixed referencing variable.description in a calculation
Whether the description's inline calculations are calculated or not is 
not defined.
2021-12-08 09:23:29 +02:00
Stefan Zermatten
e34f29f952 Computations now occupy their own nodes on the dependency graph
This mitigates most issues with properties having self-loops, 
particularly in cases like Strength where the value `strength` is used 
in the description of Strength
2021-12-07 21:05:24 +02:00
Stefan Zermatten
6698d2fd74 Added migration to fix caclulation changes
Migrate `.currentValue` to `.value` and `.value` to `.total`
2021-11-18 16:28:30 +02:00
Stefan Zermatten
e3a1eff751 Progress all over the place with viewer, forms, small engine tweaks 2021-10-21 22:18:01 +02:00
Stefan Zermatten
1b5bb981e9 Updated viewers
Action, classlevel, constant, container, damage multiplier, damage, 
effect, feature, folder, item
2021-10-19 17:19:35 +02:00
Stefan Zermatten
d6be0ae9f4 Continued work on UI viewers 2021-10-18 13:46:51 +02:00
Stefan Zermatten
417ff6e210 Buffs no longer have the "applied" field, it was redundant
Because children of actions are always inactive in the new engine, buffs 
that are children of actions are inactive while buffs elsewhere on the 
character sheet are active, making it redundant to keep the extra field
2021-10-18 13:46:38 +02:00
Stefan Zermatten
7a11a4aa22 Improved action viewer, fixed bugs found along the way 2021-10-18 12:48:26 +02:00
Stefan Zermatten
f2369277f1 Fixed bug from misspelling .overridden as .overriden 2021-10-18 11:27:43 +02:00
Stefan Zermatten
bc6c857b6b UI work to improve look and feel of Viewers 2021-10-17 23:28:39 +02:00
Stefan Zermatten
247353f0ed Small progress on tabletop 2021-10-16 19:05:35 +02:00
Stefan Zermatten
ea68cdf86f Migrating UI for new data structures 2021-10-15 11:12:40 +02:00
Stefan Zermatten
f3c52999e8 Fixed action rewrite build errors 2021-10-10 20:08:29 +02:00
Stefan Zermatten
eebfbfd636 Refactored tabletop methods 2021-10-10 19:58:41 +02:00
Stefan Zermatten
51d3fbbcb7 Completed first pass at action system re-write. Untested 2021-10-10 19:44:02 +02:00
Stefan Zermatten
0cdec4a429 Start of action system re-write 2021-10-09 12:36:06 +02:00
Stefan Zermatten
0097696cc8 Began merging attacks into actions 2021-10-06 14:57:13 +02:00
Weslley Rocha
b9008314a2 converting string to number 2021-10-05 10:14:40 -03:00
Stefan Zermatten
1a14393031 Parsed calculations are now cached between calculations
Parsing is one of the more expensive computations done to characters, so 
the parser results are now stored on the DB and only updated if they are 
dirty. A hash is used to determine if the calculation has changed since 
the last computation
2021-10-03 20:59:04 +02:00
Stefan Zermatten
c2d430ad23 Fixed tableLookup returning string instead of number 2021-10-03 19:21:35 +02:00
Stefan Zermatten
b78517b61f Finished migrating parser to be object orientation free. All tests pass 2021-10-03 13:54:17 +02:00
Stefan Zermatten
d30184434c Merge branch 'version-2-dev' of https://github.com/ThaumRystra/DiceCloud into version-2-dev 2021-10-01 13:41:41 +02:00
Stefan Zermatten
feffa45cf7 Began work on rewriting parser without object orientation
Parsing is expensive, if the parse tree can be stored on the DB it can 
save a lot of compute time, but mongo can't store Classes, so we 
re-write without classes
2021-10-01 13:41:22 +02:00
Stefan Zermatten
7dac0cf3f3 Removed color max length, using regex instead 2021-09-29 18:32:12 +02:00
Stefan Zermatten
cb1fd38df3 Optimized some slow parts of the engine.
Last low hanging fruit: parsing is slow, cache parsed calculations
2021-09-29 15:54:14 +02:00
Stefan Zermatten
cb10b53a10 Updated forms and some UI for new data structure 2021-09-28 14:25:11 +02:00
Stefan Zermatten
b471d0c5cf improved calculation fields 2021-09-27 19:25:11 +02:00
Stefan Zermatten
6dc7e12582 Fixed computation bug for compile level calcs 2021-09-27 18:14:04 +02:00
Stefan Zermatten
b0ee5cd304 Continued iterating on calculations, added failing test for bugs found 2021-09-27 17:26:52 +02:00
Stefan Zermatten
85e8756d1d Fixed parse errors not showing up on calculations 2021-09-27 16:39:44 +02:00
Stefan Zermatten
111040e789 Began migrating the rest of the codebase to the new computation engine 2021-09-27 15:39:18 +02:00
Stefan Zermatten
fdea748441 Tore out the old engine, left some wounds 2021-09-27 14:28:32 +02:00
Stefan Zermatten
2228802dd3 moved v1 migrations to the migrations folder 2021-09-27 13:44:43 +02:00
Stefan Zermatten
fe83b5cbc5 Fixed migration errors when tested against a copy of the live dataset 2021-09-27 13:42:37 +02:00
Stefan Zermatten
5e83a88132 Added migrations for all properties 2021-09-27 11:21:10 +02:00
Stefan Zermatten
091e367d27 Fixed suggested parent of class levels to be class 2021-09-27 11:20:40 +02:00
Stefan Zermatten
706303862f Fixed class level not having computed description 2021-09-27 11:06:48 +02:00
Weslley Nascimento Rocha
875b2f7c04 tableLookup function was returning a string
tableLookup function was returning a string instead of number
2021-09-25 16:36:59 -03:00
Stefan Zermatten
2cb14146d4 Tested and fixed skill compuations 2021-09-23 15:44:50 +02:00
Stefan Zermatten
8ea04fc786 Implemented effect and proficiency inheritance from abilities to skills 2021-09-23 15:22:28 +02:00
Stefan Zermatten
c0a9a1251d Tested and fixed effect computations 2021-09-23 15:22:01 +02:00
Stefan Zermatten
2f893710e2 Tested and fixed damage multiplier computations 2021-09-23 13:40:11 +02:00
Stefan Zermatten
734df59fd1 Tested and fixed inventory computation 2021-09-23 12:41:03 +02:00
Stefan Zermatten
e63dd2560a tested and fixed constant node computations 2021-09-23 12:03:25 +02:00
Stefan Zermatten
347bd8e476 Tested and fixed class level computations 2021-09-23 11:41:15 +02:00
Stefan Zermatten
1270e320ce Basic testing for attribute calculations 2021-09-17 15:11:29 +02:00
Stefan Zermatten
a660ccc458 Lots of progress testing and fixing computation engine 2021-09-16 14:31:28 +02:00
Stefan Zermatten
dfd7ad4af5 Got tests running on single property character 2021-09-15 15:15:18 +02:00
Stefan Zermatten
856fc41429 Began the long road of testing computations 2021-09-14 22:48:30 +02:00
Stefan Zermatten
8f93179187 Moved inventory computation to after toggles, added class levels computation 2021-09-14 16:18:36 +02:00
Stefan Zermatten
5c84836238 More engine rewrite progress, starting to get messy again 2021-09-13 16:12:04 +02:00
Stefan Zermatten
b877a8b45f Computation engine rewrite continues 2021-09-10 19:51:03 +02:00
Stefan Zermatten
28ec7082ee Fixed typo in slot form 2021-09-10 17:14:39 +02:00
Stefan Zermatten
55bca633fc Substantial progress on rebuilding computation engine 2021-09-09 13:47:41 +02:00
Stefan Zermatten
23e99565dc Started inventory calculation 2021-09-09 01:31:20 +02:00
Stefan Zermatten
06da15c44a Began rebuilding computation engine to be dependency graph centric 2021-09-08 17:23:00 +02:00
Stefan Zermatten
43f056ae95 more small progress on migration 2021-09-07 15:48:51 +02:00
Stefan Zermatten
b0980d26ac Iterated on migration :( 2021-09-06 23:59:52 +02:00
Stefan Zermatten
e79b8fda3b Improved migration code substantially, wrote migrations for more properties 2021-09-06 17:40:57 +02:00
Stefan Zermatten
235560eb44 Started on DBv1 migration 2021-09-06 11:36:42 +02:00
Stefan Zermatten
fc0cc6e689 References now merge children, fixed infinite reference loops 2021-09-05 18:51:29 +02:00
dependabot[bot]
c9d4d874aa Bump tar from 6.1.6 to 6.1.11 in /app
Bumps [tar](https://github.com/npm/node-tar) from 6.1.6 to 6.1.11.
- [Release notes](https://github.com/npm/node-tar/releases)
- [Changelog](https://github.com/npm/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/npm/node-tar/compare/v6.1.6...v6.1.11)

---
updated-dependencies:
- dependency-name: tar
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-31 18:45:06 +00:00
Stefan Zermatten
7f95680559 Updated thanks page to reflect that Sam is no longer my fiancee, since we're married now <3 2021-08-30 16:32:31 +02:00
Stefan Zermatten
6e51df363b Added increment buttons to all attributes with a value 2021-08-27 13:21:08 +02:00
Stefan Zermatten
608ab4e588 Added undo delete and recycle bin to library 2021-08-27 13:00:01 +02:00
Stefan Zermatten
518880fa5c Fixed error where searching for properties to insert while having other properties selected could prevent any insert from happening at all 2021-08-27 12:24:01 +02:00
Stefan Zermatten
f043c41e12 Fixed fab being confused by hiding the spells tab 2021-08-14 10:44:17 +02:00
Stefan Zermatten
0277de76c4 Removed stray console log 2021-08-12 19:56:01 +02:00
Stefan Zermatten
ffc3211ff9 Fixed some issues with slot filler searching 2021-08-12 18:28:53 +02:00
Stefan Zermatten
8162c76185 Added basic ownership transfer for shared documents 2021-08-10 19:01:31 +02:00
Stefan Zermatten
e21586e9ce Added reasonable storage limits to most string and array schemas 2021-08-10 18:12:55 +02:00
Stefan Zermatten
4c2155d8ff Buffs applied property can now be set in both library and character 2021-08-10 17:02:27 +02:00
Stefan Zermatten
44cc46ed22 Added the ability to add buffs as already applied straight from a library 2021-08-10 16:47:05 +02:00
Stefan Zermatten
d2b5d5f01d Fixed swiping between tabs when spells tab is hidden 2021-08-10 15:20:04 +02:00
Stefan Zermatten
4492c47b00 Fixed item tree node view only using quantity display if quantity is a number 2021-08-10 15:03:30 +02:00
Stefan Zermatten
6600cea9fa Fixed references display in tree views 2021-08-10 15:02:24 +02:00
Stefan Zermatten
741a9b080a Finished implementing new slot fill dialog, including ability to test slots 2021-08-10 14:35:27 +02:00
Stefan Zermatten
b041db22e4 Merge branch 'version-2' into version-2-dev 2021-08-10 13:31:32 +02:00
Stefan Zermatten
a465e2ce87 Continued implementing new slot dialog 2021-08-10 13:29:11 +02:00
Stefan Zermatten
8ecefb03ad Started re-implementing slot fill dialog with different design pattern 2021-08-10 10:32:56 +02:00
Stefan Zermatten
9f62a78eb0 Began work on implementing string and array storage limits 2021-08-09 22:34:04 +02:00
Stefan Zermatten
16e2b1249f Increased slot filler publication load quantity to 50 2021-08-09 20:54:29 +02:00
Stefan Zermatten
a35f9221a2 Fixed granting XP being accidentally locked behind patreon paid benefits 2021-08-09 18:06:04 +02:00
Stefan Zermatten
6364549d50 Added pretty url's from v1 2021-08-09 18:00:55 +02:00
Stefan Zermatten
d999fb46a7 Fixed a warning about source map failure when trying to define a scss variable 2021-08-09 17:57:35 +02:00
Stefan Zermatten
ec01a2adb5 Merge pull request #274 from GuillaumeDerval/version-2
Fix items/buffs/... refresh after casting a spell
2021-08-09 17:11:41 +02:00
Guillaume Derval
1f64558100 Fixed items/buffs/... refresh after casting a spell 2021-08-07 23:20:26 +02:00
Stefan Zermatten
19a2798bf7 Fixed tree item search highlighting in dark mode 2021-08-02 01:13:15 +02:00
Stefan Zermatten
a5f2c2e0d2 Removed duplicate property button on tree tab 2021-08-02 00:54:59 +02:00
Stefan Zermatten
ee174210fd Added search to library tree views 2021-08-02 00:29:56 +02:00
Stefan Zermatten
1e38295164 All properties added to the sheet now use the type/library/create UX 2021-08-01 23:28:04 +02:00
Stefan Zermatten
758cb2f8bc Fixed search icons 2021-07-31 21:53:18 +02:00
Stefan Zermatten
36bb3c3181 New UX for inserting properties from libraries including text search and multi-add 2021-07-31 21:49:15 +02:00
Stefan Zermatten
02434de34c Drastically improved tree tab search UX for locating parts of the sheet 2021-07-31 15:19:54 +02:00
Stefan Zermatten
0dc0bea53e fixes #262, emails from DiceCloud should now be from no-reply@dicecloud.com 2021-07-27 16:59:09 +02:00
Stefan Zermatten
c392119430 Added suggested parents to PROPERTIES for use later with user guidance 2021-07-27 16:36:19 +02:00
Stefan Zermatten
4e2e0ca364 Improved display of referenced properties 2021-07-27 16:21:55 +02:00
Stefan Zermatten
4a8b72f163 Made sure the unsubscribe button always shows on libraries you are subscribed to 2021-07-27 15:51:09 +02:00
Stefan Zermatten
d916dc2b78 Removed add library node buttons from libraries the user deosn't have edit permissions for 2021-07-27 15:44:26 +02:00
Stefan Zermatten
56860ba96d Fixed proficiency Tree node view not showing the name if it is defined 2021-07-27 15:40:16 +02:00
Stefan Zermatten
b607755f9f Fixed attribute base value calculation errors not being cleared if no new errors were made 2021-07-27 15:37:57 +02:00
Stefan Zermatten
86d8fa4325 Fixed items not animating correctly on insert 2021-07-27 15:33:41 +02:00
Stefan Zermatten
2b08249e5e removed lazy from v-menu which caused a console warning 2021-07-27 15:29:05 +02:00
Stefan Zermatten
3133e664d5 Reduced stats computation precision to round off small decimal floating point errors 2021-07-27 15:28:44 +02:00
Stefan Zermatten
48f32e0a8d Removed floating point small decimal oddities in parts of inventory tab 2021-07-27 15:21:35 +02:00
Stefan Zermatten
c72785c9e7 Added components to spell viewer 2021-07-27 15:00:10 +02:00
Stefan Zermatten
421ff2aa7d Fixed DISABLE_PATREON not working, it's now a Meteor setting instead of an ENV variable 2021-07-27 14:31:54 +02:00
Stefan Zermatten
9a9e6491b9 Improved usability with better hints in property forms and property type selection 2021-07-26 18:19:29 +02:00
Stefan Zermatten
332258705c Added service worker 2021-07-21 13:44:55 +02:00
Stefan Zermatten
73ef109d4d Added calculation errors that were missing 2021-07-20 10:37:32 +02:00
Stefan Zermatten
fc240a34c4 Changed tiers to their open beta configuration 2021-07-17 12:50:38 +02:00
Stefan Zermatten
8ac4028f38 Removed limit from guest tiers for closed beta 2021-07-14 00:59:32 +02:00
Stefan Zermatten
2849df1974 Merge branch 'version-2-dev' into version-2 2021-07-13 12:45:59 +02:00
Stefan Zermatten
3fa2cca7ae Locked dark mode to paid accounts only 2021-07-13 12:39:56 +02:00
Stefan Zermatten
a0b53af6d7 Fixed awkward padding in character limit alert 2021-07-13 12:25:15 +02:00
Stefan Zermatten
51c709f7a5 Changed tier limitations for closed beta 2021-07-13 12:23:56 +02:00
Stefan Zermatten
28d67409aa Changed some patreon nudging 2021-07-13 12:23:28 +02:00
Stefan Zermatten
7e97bcb6d8 Updated packages 2021-07-13 12:04:36 +02:00
Stefan Zermatten
4e87737a3e Fixed add-property button not re-appearing when adding a reference property 2021-07-12 18:11:48 +02:00
Stefan Zermatten
822fa4619f Fixed missing icons in calculation errors 2021-07-12 18:00:21 +02:00
Stefan Zermatten
80ba44a28f Added ancestor references to action context so that #spellList references work, closes #270 2021-07-12 17:56:35 +02:00
Stefan Zermatten
c3c079731e Fixed equipment creating ghost items on drag if the equipment folder is deleted from the character 2021-07-12 17:13:18 +02:00
Stefan Zermatten
039b7046b2 Fixed attributes not hiding when redundant 2021-07-12 17:03:32 +02:00
Stefan Zermatten
8eaad3600f Fixed computation error with base values 2021-07-12 14:56:12 +02:00
Stefan Zermatten
5cf78932e6 removed stray debug text 2021-07-12 14:47:41 +02:00
Stefan Zermatten
d43d364175 Removed stray console log 2021-07-12 14:46:37 +02:00
Stefan Zermatten
0ad4c71189 Fixed some missing icons 2021-07-12 14:45:24 +02:00
Stefan Zermatten
e8c6f26a0b fix skills UI bugs and icon consistency for skills 2021-07-12 14:39:05 +02:00
Stefan Zermatten
8804c80a56 Fixed skills not showing their base value in the effects list 2021-07-12 14:15:17 +02:00
Stefan Zermatten
0d21ab758e Fixed negative base values being ignored 2021-07-12 14:12:20 +02:00
Stefan Zermatten
2ecb0e2671 Fix: DC missing from spell list viewer in library 2021-07-12 13:39:08 +02:00
Stefan Zermatten
f3b9b62486 Fixed styling of inventory for denser item list tiles 2021-07-12 12:50:56 +02:00
Stefan Zermatten
29a4575760 Fixed a stray empty list item action taking space on creatures in lists 2021-06-22 15:19:46 +02:00
Stefan Zermatten
a6fbf71b36 Added folders to the sidebar character list 2021-06-22 15:13:59 +02:00
Stefan Zermatten
86d9383af0 Archive UI 2021-06-22 14:59:18 +02:00
Stefan Zermatten
3db589f775 Fixed button property for new Vuetify version 2021-06-22 14:59:02 +02:00
Stefan Zermatten
e96755927f Added archive dialog, empty for now 2021-06-21 16:42:47 +02:00
Stefan Zermatten
66dc0ee34f Added icon to indicate sharing status of characters 2021-06-21 16:37:02 +02:00
Stefan Zermatten
54bf21c57c Edit permission is no longer patreon-only 2021-06-21 16:32:24 +02:00
Stefan Zermatten
5f5fe801f6 Added method to transfer character ownership 2021-06-21 16:06:29 +02:00
Stefan Zermatten
a451afcbaf Prevented new characters being added if you are at your character limit 2021-06-21 16:06:10 +02:00
Stefan Zermatten
848e961e3b Refactored creature methods to their own folders 2021-06-21 15:20:04 +02:00
Stefan Zermatten
c5aca81131 Removed stray console logs 2021-06-20 13:35:23 +02:00
Stefan Zermatten
814e371148 Can now move creatures between folders using drag and drop 2021-06-20 13:32:28 +02:00
Stefan Zermatten
69f4bbf360 Added CRUD API and UI for creature folders 2021-06-20 12:41:08 +02:00
Stefan Zermatten
6b2d74a165 Fixed parties -> creatureFolders publications and ui 2021-06-18 12:59:39 +02:00
Stefan Zermatten
cf05aea80a Fixed Parties references 2021-06-18 10:49:01 +02:00
Stefan Zermatten
1a2d4b22bb Renamed parties to creatureFolders 2021-06-18 10:48:12 +02:00
Stefan Zermatten
81d52a1847 Enabled editing of attribute damage in library forms 2021-06-18 10:43:27 +02:00
Stefan Zermatten
e3fc56a844 Added backend architecture to archive and restore creatures. 2021-06-11 12:03:31 +02:00
Stefan Zermatten
64fceb9c38 Added environmental variables to readme for self-hosting 2021-06-10 15:19:14 +02:00
Stefan Zermatten
9994c1f32a New users now get subscribed to the default libraries as defined by env 2021-06-10 15:18:54 +02:00
Stefan Zermatten
7056c5b37b Added character slot limitations to tiers; added no-patreon tier for self hosting 2021-06-10 12:25:17 +02:00
Stefan Zermatten
1ad1d1f23d Migrated from Google material design icons to vuetify default MDI 2021-06-01 12:34:51 +02:00
Stefan Zermatten
c65c8f3299 Added note to attempt to keep children of reference nodes 2021-04-29 16:13:22 +02:00
Stefan Zermatten
4faea42371 Merge branch 'version-2-dev' into version-2 2021-04-29 15:53:24 +02:00
Stefan Zermatten
9825872576 Implemented Reference properties 2021-04-29 15:52:24 +02:00
Stefan Zermatten
85b536bc46 Added default array for stat proficiencies as well 2021-04-29 11:52:47 +02:00
Stefan Zermatten
9aa8203dcc Fixed bug where effects in stat computation could be undefined 2021-04-29 11:50:16 +02:00
Stefan Zermatten
217133137b Added note to improve query performance with root ancestor targeting 2021-04-29 11:34:58 +02:00
Stefan Zermatten
aef7dbcbb3 Fixed bug in stat computation dependency tracking 2021-04-29 11:22:13 +02:00
Stefan Zermatten
6ff750417f Fixed error in stat computation 2021-04-24 23:25:58 +02:00
Stefan Zermatten
a9eacfab03 Unprepared spells without lists now correctly show up when unprepared 2021-04-22 16:06:31 +02:00
Stefan Zermatten
1f633621b7 Fixed a bug with functions accepting rolled arguments 2021-04-22 15:59:12 +02:00
Stefan Zermatten
9f3c8bef34 Removed stray console log 2021-04-22 15:54:41 +02:00
Stefan Zermatten
8a83e7d8a1 Fixed back button appearing in embedded dialogs 2021-04-22 15:42:44 +02:00
Stefan Zermatten
a28182f3e9 Added missing half rounded down icon for skills in stats tab 2021-04-22 15:40:26 +02:00
Stefan Zermatten
3d122e062f Added the distinction between half rounded up or down for proficiencies 2021-04-22 15:39:14 +02:00
Stefan Zermatten
e9a273244a Improved Effect and Proficiency UI in attribute and skill viewers 2021-04-22 15:12:49 +02:00
Stefan Zermatten
1de3122254 Updated UI to hide extra attributes and skills with same variable name 2021-04-22 15:12:21 +02:00
Stefan Zermatten
298db01e5b Updated computation engine to handle multiple attributes and skills with the same variable name 2021-04-22 15:11:49 +02:00
Stefan Zermatten
727101cd63 Updated Meteor 2021-04-22 15:10:47 +02:00
Stefan Zermatten
d4d002cf31 Fixed an error when targeting an ability score with a proficiency 2021-04-15 12:00:11 +02:00
Stefan Zermatten
2150bd6da4 Added breadcrumbs to creature properties 2021-04-13 14:17:31 +02:00
Stefan Zermatten
e1df145675 Add property button now in creature property dialogs 2021-04-13 11:53:50 +02:00
Stefan Zermatten
1eb78756ac Fixed console error if creature is deleted while sheet is still showing 2021-04-13 11:53:18 +02:00
Stefan Zermatten
ce9b9199ec Fixed dialog stacking animation 2021-04-13 11:41:14 +02:00
Stefan Zermatten
cfb1414494 Start character sheet on the character details dialog instead of build 2021-04-13 11:06:46 +02:00
Stefan Zermatten
4abd689c9f Use DiceCloud 5e base as default for new characters 2021-04-13 11:06:25 +02:00
Stefan Zermatten
f0e443fba2 When hiding the spells tab or tree tab, only change tabs if on one of those 2021-04-13 11:05:25 +02:00
Stefan Zermatten
52e7deedc6 Leave character page before deleting to prevent UI errors 2021-04-13 10:50:35 +02:00
Stefan Zermatten
15d593db79 Properties quick-inserted from the sheet now go into folders in the tree 2021-04-12 16:04:04 +02:00
Stefan Zermatten
e30754ef26 Added method to insert property to a tagged parent 2021-04-12 15:35:25 +02:00
Stefan Zermatten
255ac529b3 Added more default properties to creatures 2021-04-12 15:35:12 +02:00
Stefan Zermatten
c8b5ada5b9 Changed all form input fields to outlined style instead of filled 2021-04-12 14:21:50 +02:00
Stefan Zermatten
0c24238069 Fixed not found page for Vuetify 2 2021-04-11 18:15:56 +02:00
Stefan Zermatten
66847430ad Fixed sign in and register pages not being built with Vuetify 2 components 2021-04-11 18:05:36 +02:00
Stefan Zermatten
bfb860605f Creature properties now duplicate with up to 50 children 2021-04-11 14:47:41 +02:00
Stefan Zermatten
d87524418a Fixed bug where character sheet fab could be permanently hidden by closing the insert from library dialog 2021-04-11 13:52:25 +02:00
Stefan Zermatten
5f97592ed3 Updated package-lock for last commit 2021-04-11 13:33:38 +02:00
Stefan Zermatten
562991216f Moved ignore-styles dep from devDependencies to dependencies 2021-04-11 13:23:47 +02:00
Stefan Zermatten
109b89022e removed id from comment 2021-04-11 13:09:16 +02:00
Stefan Zermatten
d4ca07ce9c Began working on character migration code 2021-04-11 13:08:41 +02:00
Stefan Zermatten
885607f685 Moved the tree fab to the toolbar with smart parenting 2021-04-11 12:36:14 +02:00
Stefan Zermatten
81460f8835 Added file missed in last commit 2021-04-11 12:15:51 +02:00
Stefan Zermatten
cce5f9b926 Merge branch 'version-2-dev' of https://github.com/ThaumRystra/DiceCloud into version-2-dev 2021-04-11 12:15:33 +02:00
Stefan Zermatten
7d3a51de9d Moved ancestry setting responsibility to trusted code 2021-04-11 12:15:30 +02:00
Stefan Zermatten
fc774fcc5e Moved ancestry setting responsibility to trusted code 2021-04-11 12:14:39 +02:00
Stefan Zermatten
0f37a49b95 Fixed bug where wrong fab would show on character tab if spell tab was hidden 2021-04-11 11:01:31 +02:00
Stefan Zermatten
9814e20091 Added the ability to hide spells and tree tab. Tree tab hidden by default 2021-04-11 10:43:33 +02:00
Stefan Zermatten
d89cb77040 Fixed no-experiences Icon 2021-04-11 10:29:09 +02:00
Stefan Zermatten
8590d29abf Improved animation feel of character sheet fab 2021-04-11 10:21:14 +02:00
Stefan Zermatten
9298754dc9 Fixed character sheet title not updating correctly 2021-04-09 12:44:54 +02:00
Stefan Zermatten
e2d6d40bb3 Duplicating library nodes now duplicates up to 50 descendants 2021-04-09 12:36:44 +02:00
Stefan Zermatten
152677b023 Library nodes are now smarter about where in the tree they are inserted based on the currently selected node 2021-04-09 12:36:14 +02:00
Stefan Zermatten
838e2ed35f Fixed toolbar colors for vuetify 2 2021-03-28 14:29:56 +02:00
Stefan Zermatten
60ae1ef604 Fixed bug where editing a field and immediately changing selected property would apply the change to the new property rather than the old one 2021-03-28 13:56:45 +02:00
Stefan Zermatten
ecfe5f1360 Fixed spell casting buttons having the wrong color in dark mode 2021-03-28 13:14:38 +02:00
Stefan Zermatten
292d3c3f37 Tree titles now have hover. Fixed primary color theme switching 2021-03-28 13:09:47 +02:00
Stefan Zermatten
3c26bb2fc6 Reworked log data format, overhauled snackbar 2021-03-28 12:31:39 +02:00
Stefan Zermatten
ada1355c29 Added UI for filtered out slot fillers allowing loading more 2021-03-27 14:39:00 +02:00
Stefan Zermatten
2662af8ea2 Fixed misalignment on ability score tiles 2021-03-27 13:47:02 +02:00
Stefan Zermatten
26b68dccef Fixed style of attribute cards 2021-03-27 13:45:10 +02:00
Stefan Zermatten
0717f8e8d7 Fixed broken and insecure packages 2021-03-27 13:34:33 +02:00
Stefan Zermatten
5cf0330e03 Added library node insert button to library page, no automatic parenting 2021-03-26 12:49:08 +02:00
Stefan Zermatten
1978a2e4c7 Fixed Error when referencing slotLevel in a spell 2021-03-26 11:18:14 +02:00
Stefan Zermatten
5a2e500348 Fixed constants under toggles triggering calculation of those toggles before class levels are defined 2021-03-26 11:11:15 +02:00
Stefan Zermatten
7d4356592a Iterated on card color scheme 2021-03-26 11:06:52 +02:00
Stefan Zermatten
2c448d1748 Fixed some pages background colors 2021-03-26 10:00:37 +02:00
Stefan Zermatten
4f96d817d5 Change card and background color scheme 2021-03-26 09:48:29 +02:00
Stefan Zermatten
f9998eabc4 Fixed tabs using the wrong primary color 2021-03-26 09:35:19 +02:00
Stefan Zermatten
623cff584c Fixed form section expansion 2021-03-26 09:26:31 +02:00
Stefan Zermatten
aa34508cb3 Fixed hovering on toolbar cards 2021-03-26 09:26:16 +02:00
Stefan Zermatten
6678bc1cea Fixed library subscriptions, again 2021-03-25 14:12:02 +02:00
Stefan Zermatten
0324b9f7c3 Fixed some tooltips to vuetify 2 2021-03-25 13:14:40 +02:00
Stefan Zermatten
ccac142ec6 Fixed some cards not animating elevation change 2021-03-25 13:08:48 +02:00
Stefan Zermatten
fe3fa56541 Continued migrating UI to vuetify 2 2021-03-25 12:54:44 +02:00
Stefan Zermatten
480da6fc7d ES Lint fix migration to vuetify 2 2021-03-25 10:20:13 +02:00
Stefan Zermatten
6ffb48b7b6 Began migration to Vuetify 2.x expect a lot to be broken 2021-03-24 16:23:39 +02:00
Stefan Zermatten
82150df5e0 Updated packages and dependencies 2021-03-24 14:38:47 +02:00
Stefan Zermatten
9a120a6e9a Added description to class level viewer 2021-03-23 15:07:47 +02:00
Stefan Zermatten
f385c2857e Fixed library forward arrow being disabled if you can't edit the library 2021-03-12 09:34:20 +02:00
Stefan Zermatten
11a2851ac4 Fixed slots with computed expected quantity not hiding when full 2021-03-10 14:51:38 +02:00
Stefan Zermatten
313382fb82 Fixed library subscription issues 2021-03-10 14:40:14 +02:00
Stefan Zermatten
b9ae337a64 Merge branch 'version-2-dev' of https://github.com/ThaumRystra/DiceCloud into version-2-dev 2021-03-02 14:32:08 +02:00
Stefan Zermatten
4dc0a6159b Animated log entries 2021-03-02 14:32:05 +02:00
Stefan Zermatten
e00dfe1532 Changed the color of the log background 2021-03-02 14:31:35 +02:00
Stefan Zermatten
28e1fcabd5 Fixed damage properties by name failing if no properties were found 2021-03-02 14:10:14 +02:00
Stefan Zermatten
2c0496b44b Fixed properties not being made inactive by toggles 2021-03-02 13:56:53 +02:00
Stefan Zermatten
89adda60ec Reworked single page libraries to be more in line with the library view 2021-03-02 13:05:38 +02:00
Stefan Zermatten
8c3710cda3 Started work on single page libraries 2021-03-02 00:24:54 +02:00
Stefan Zermatten
b501b9d830 Fixed crash in skill calculation when level is overridden by an attribute 2021-03-01 18:40:55 +02:00
Stefan Zermatten
574f8373e7 Fixed crash when indexing a non-array node, added more array node errors 2021-03-01 14:47:46 +02:00
Stefan Zermatten
a7ecdecec1 Prevented contextual variables #type from being written to creature variable list 2021-03-01 14:22:03 +02:00
Stefan Zermatten
0aa59a4bfc Fixed creature not recomputing correctly when weight carried changes 2021-03-01 14:15:21 +02:00
Stefan Zermatten
8f0ff3245e Fixed containers still carrying their own weight if their contents are weightless and they aren't carried 2021-03-01 14:15:01 +02:00
Stefan Zermatten
9a2d10b7ed Fixed new library button hiding and not coming back 2021-03-01 14:08:12 +02:00
Stefan Zermatten
a8aa1923a8 Fixed spells having a stray deativatedBySelf flag 2021-03-01 14:01:34 +02:00
Stefan Zermatten
57fa162c89 Fixed stray errors from unepexted types 2021-03-01 13:37:19 +02:00
Stefan Zermatten
4d548c901c Ensured property exists before attempting to damage it 2021-03-01 13:32:46 +02:00
Stefan Zermatten
a97be2f93a Made constants work in calculations performed after recomputation 2021-03-01 13:27:48 +02:00
Stefan Zermatten
1276f872a0 Removed unused function 2021-03-01 12:11:22 +02:00
Stefan Zermatten
7daab97297 Made toggles function properly when nested under inactive properties and each other 2021-03-01 11:55:43 +02:00
Stefan Zermatten
2e3704d096 Prevented resources from writing unchanged data to the database 2021-03-01 11:42:50 +02:00
Stefan Zermatten
7283a27727 Constants should now respect toggles 2021-03-01 11:42:23 +02:00
Stefan Zermatten
3517636b8b Reworked toggles, again, to try and catch more edge cases. Made toggles set the inactive status of their property children in the compute step instead of the inactive denormalisation step 2021-03-01 11:41:59 +02:00
Stefan Zermatten
e617ef9b75 Merge branch 'version-2' into version-2-dev 2021-03-01 10:18:55 +02:00
Stefan Zermatten
cd45ae1442 Fixed buffs not recomputing correctly because of inactive properties not being activated 2021-03-01 10:07:24 +02:00
Stefan Zermatten
bcedd548c7 Fixed: If usesUsed was undefined, usesLeft of an action was NaN 2021-03-01 10:06:31 +02:00
Stefan Zermatten
dc53e38efe Libraries only fetch their data whene expanded 2021-02-27 10:49:10 +02:00
Stefan Zermatten
e381b3b24d Merge branch 'version-2' of https://github.com/ThaumRystra/DiceCloud into version-2 2021-02-26 09:48:22 +02:00
Stefan Zermatten
111d971bc2 Added attacks and actions to stats tab quick insert 2021-02-26 09:48:18 +02:00
Stefan Zermatten
bf4ce4f9f7 Hotfix: Adding properties to the tree, type selection fixed 2021-02-25 19:43:17 +02:00
Stefan Zermatten
2a983b0a94 User accounts can now be deleted with some UI to prevent accidental deletion 2021-02-25 14:28:51 +02:00
Stefan Zermatten
a5460bba0b Added floating action button to add properties directly to the sheet 2021-02-25 12:37:32 +02:00
Stefan Zermatten
df361236f5 Hotfix: Containers total weight now showing correctly on inventory tab 2021-02-24 15:18:32 +02:00
Stefan Zermatten
e1d670fe9f Fixed: buffs 2021-02-24 15:05:53 +02:00
Stefan Zermatten
1e9f0515e5 Contents that are weightless are now summed and stored on the container 2021-02-24 14:22:52 +02:00
Stefan Zermatten
0404020335 Added weights and content weight to containers UI 2021-02-24 14:07:20 +02:00
Stefan Zermatten
c248d8f4a0 Weight carried, Net worth, and Attunement implemented and exposed in UI 2021-02-24 13:41:30 +02:00
Stefan Zermatten
8d95da8b7a Fixed a bug where certain base values would be strings instead of numbers in effect aggregators 2021-02-24 11:58:04 +02:00
Stefan Zermatten
e11ab39864 Added tableLookup function 2021-02-24 11:57:40 +02:00
Stefan Zermatten
331fcef9ad Fixed: Error message when focus grabbing element is missing on form 2021-02-24 10:06:25 +02:00
Stefan Zermatten
7e3bff9677 Show creature milestone level and xp if creature has both 2021-02-24 10:05:11 +02:00
Stefan Zermatten
1b650b26b6 Fixed: using creature stats like XP in calculations 2021-02-24 10:01:02 +02:00
Stefan Zermatten
5925605962 Fixed property edit buttons no longer get pushed by long property name 2021-02-24 09:52:51 +02:00
Stefan Zermatten
dee1265b69 Fixed: Inline calculations in libarries now display as expected 2021-02-24 09:46:52 +02:00
Stefan Zermatten
3d3ec3bcf2 Increaed number of slot fillers loaded by the slot fill dialog to 20 2021-02-24 09:23:55 +02:00
Stefan Zermatten
dce2c92516 Added attack roll bonus and dc to spell list. Use them in spells with #spellList.dcResult and #spellList.attackRollBonusResult 2021-02-23 15:21:20 +02:00
Stefan Zermatten
0fe2780983 Added property viewer for Toggle properties 2021-02-23 15:07:07 +02:00
Stefan Zermatten
e126cdd3cb Added property viewer for slot filler 2021-02-23 14:59:53 +02:00
Stefan Zermatten
d69ada0db4 Slot quantity is now a computed value, added property viewer for slots 2021-02-23 14:53:47 +02:00
Stefan Zermatten
858915b25b Added viewer for Saving Throw properties 2021-02-23 14:38:20 +02:00
Stefan Zermatten
d10a7eca14 Added viewer for Roll properties 2021-02-23 14:29:48 +02:00
Stefan Zermatten
671d17018c Added a viewer for Constant properties 2021-02-23 14:23:00 +02:00
Stefan Zermatten
f2883d320f Improved Attribute damage viewer 2021-02-23 13:59:26 +02:00
Stefan Zermatten
aad0c7249e Removed stray log to console 2021-02-23 12:47:34 +02:00
Stefan Zermatten
612fcca68c Only split properties accross targets if there are targets 2021-02-22 14:30:50 +02:00
Stefan Zermatten
12939c46de made saves walk children when not targeted at self 2021-02-22 14:28:38 +02:00
Stefan Zermatten
3801b17fde Attacks can now critical hit. criticalHitTarget overrides the roll required 2021-02-22 14:07:12 +02:00
Stefan Zermatten
88133a2fa3 Saving throws now work in actions 2021-02-22 12:38:21 +02:00
Stefan Zermatten
d00eedac19 Rolls now work in actions 2021-02-22 11:55:08 +02:00
Stefan Zermatten
6571fb860a Toggles now work in actions to make choices based on action context 2021-02-22 11:36:30 +02:00
Stefan Zermatten
8148f4d701 Fixed: Library nodes are published in order to prevent disordered building of the tree 2021-02-21 17:05:09 +02:00
Stefan Zermatten
523c34b719 Fixed: Slots that use conditions now only hide on falsey value (false, 0, '') 2021-02-21 17:01:24 +02:00
Stefan Zermatten
e833fba870 Fixed: bug that stopped buffs being deleted 2021-02-21 16:35:35 +02:00
Stefan Zermatten
f3e191c12e Fixed: Inserting properties to the tree now animate correctly to the inserted property 2021-02-20 16:00:40 +02:00
Stefan Zermatten
33415275a3 Item tiles are now smaller in the inventory view 2021-02-20 15:53:58 +02:00
Stefan Zermatten
3b1151d987 Fixed notes without summaries are no longer oversized 2021-02-20 15:52:15 +02:00
Stefan Zermatten
4288f98f7c Fixed: stats with no ability selected have an ability modifier of 0 instead of NaN 2021-02-20 15:50:25 +02:00
Stefan Zermatten
1a2ef8a4a2 Fixed: markdown images no longer overflow their container width 2021-02-20 15:45:45 +02:00
Stefan Zermatten
10e9a5faa8 Notes now show both summary and description in viewer 2021-02-20 15:41:30 +02:00
Stefan Zermatten
53594c0004 Fixed: items in containers not following tree order 2021-02-20 15:38:51 +02:00
Stefan Zermatten
e068675b46 Fixed and improved: Discord webhooks are working again with a new format 2021-02-20 15:27:20 +02:00
Stefan Zermatten
067f5df36e Fixed: Emptying the search bar in slot filler dialog now correctly clears the search 2021-02-20 10:17:35 +02:00
Stefan Zermatten
6113d86059 Fixed typo in calling a function in Constants autovalue 2021-02-16 11:19:50 +02:00
Stefan Zermatten
e3862bcdd9 Fixed constants autovalue 2021-02-16 10:49:18 +02:00
Stefan Zermatten
299f5a06dd Moved inline cacultion regex to a constant to ensure constistency 2021-02-16 10:14:26 +02:00
Stefan Zermatten
2776850311 Fixed missing import 2021-02-12 11:45:32 +02:00
Stefan Zermatten
3e6221309e Notes now have a summary and a description, some data migration my be needed 2021-02-12 11:44:03 +02:00
Stefan Zermatten
a078ce3d5d Description fields should now show calculation errors 2021-02-12 11:43:13 +02:00
Stefan Zermatten
b9e0475d07 Markdown now follow Github Flavor Markdown, including single line breaks 2021-02-12 11:11:12 +02:00
Stefan Zermatten
2b345c1f77 Improved error handling for most calculations 2021-02-12 11:00:44 +02:00
Stefan Zermatten
fed87f0a84 Fixed: items and spells should no longer be draggable when you don't have edit permission 2021-02-12 09:41:24 +02:00
Stefan Zermatten
b116be1238 Fixed flickering when inserting properties from library by ensuring consistent ID generation 2021-02-12 00:43:56 +02:00
Stefan Zermatten
ae373330ab Improved slot fill dialog UI, abandoned column layout in favour of wrapping flex rows 2021-02-12 00:18:29 +02:00
Stefan Zermatten
dcb535c84e Moved slot filler search to server side, limited docs in subscription 2021-02-11 22:09:43 +02:00
Stefan Zermatten
8c477ad4b1 Fixed constants not being found when used as the only thing in a calculation 2021-02-11 18:01:30 +02:00
Stefan Zermatten
eb2dd3bba1 Fixed computation dependency aggregation broken by refactoring 2021-02-11 16:27:22 +02:00
Stefan Zermatten
dc4808c70a fixed snackbars being blank 2021-02-11 16:20:33 +02:00
Stefan Zermatten
16c8c1db81 Improved descriptions in log entries 2021-02-11 16:16:22 +02:00
Stefan Zermatten
92a5c1e6c3 Improved log entries for actions 2021-02-11 16:08:31 +02:00
Stefan Zermatten
439eadf079 Condensed logs to a single card per action 2021-02-11 15:48:23 +02:00
Stefan Zermatten
d7083cf242 Hid unused cards on the stats tab 2021-02-11 13:48:10 +02:00
Stefan Zermatten
3af5e820ca Stopped inactive effects showing up in attributes 2021-02-11 13:38:34 +02:00
Stefan Zermatten
20aaab4dea Hid inactive notes 2021-02-11 13:14:31 +02:00
Stefan Zermatten
81cdf282ea Stopped inactive properties from showing computed inline fields, since they are not recomputed while inactive 2021-02-11 13:12:35 +02:00
Stefan Zermatten
3313ed0297 Added constants to the UI and Computation Engine 2021-02-11 13:03:31 +02:00
Stefan Zermatten
25fd5c18e8 Fixed casting broken by refactoring 2021-02-11 10:13:35 +02:00
Stefan Zermatten
5b9bb6e4bc Fixed spending resources for actions broken by refactoring 2021-02-11 10:13:23 +02:00
Stefan Zermatten
74370f6fec Performance optimization: Removed creature document from injection to prevent uneccessary Vue re-rendering 2021-02-11 10:04:28 +02:00
Stefan Zermatten
565ddccba6 fixed broken import 2021-02-04 16:55:50 +02:00
Stefan Zermatten
4ea4348a02 Now writing partial recalculations to creature variables 2021-02-04 16:52:26 +02:00
Stefan Zermatten
280f30dab5 Improved dependencies-only recalculations and fixed many calculation bugs 2021-02-04 16:16:51 +02:00
Stefan Zermatten
6d1e3f078c Optimised when certain recompute functions are called to prevent unccessary work 2021-02-04 13:59:08 +02:00
Stefan Zermatten
326d1bd165 Refactored computation into folders 2021-02-04 13:33:20 +02:00
Stefan Zermatten
87fa941f63 Merge branch 'version-2' of https://github.com/ThaumRystra/DiceCloud into version-2 2021-02-04 11:38:55 +02:00
Stefan Zermatten
449a4fba7d Refactored creature property methods into separate documents, might have broken a lot of things 2021-02-04 11:38:29 +02:00
Stefan Zermatten
9ff096ec0f Added modulo operator 2021-02-03 09:33:00 +02:00
Stefan Zermatten
f9f0186d95 Fixed error where parser was not creating accessor nodes correctly 2021-02-03 00:39:11 +02:00
Stefan Zermatten
60ea545ee9 Started implementing constant property 2021-02-02 16:36:23 +02:00
Stefan Zermatten
aaa5d0b63b Allowed effects and calculations to target nearest ancestors of #type 2021-02-02 16:11:59 +02:00
Stefan Zermatten
69c72e0987 Fixed parenthesis being discarded from compiled calculations in cases where they should not be 2021-02-02 15:13:49 +02:00
Stefan Zermatten
a6df4df534 Fixed some fields not storing strings when compiling calculations 2021-02-02 15:07:31 +02:00
Stefan Zermatten
8b8f9bea6f Removed character variables from creatures injected into character sheet as context 2021-01-31 20:13:45 +02:00
Stefan Zermatten
e7a27e4b83 Fixed hit dice breaking long rests 2021-01-31 19:45:14 +02:00
Stefan Zermatten
e14e875c42 Fixed conMod not being signed in hit dice tiles 2021-01-31 19:43:09 +02:00
Stefan Zermatten
a7898bfe4b Fixed props not having a default array for embedded calculations 2021-01-31 19:42:49 +02:00
Stefan Zermatten
aee899e181 Removed all UI computations from viewers and components 2021-01-31 18:42:17 +02:00
Stefan Zermatten
a5284bf6e8 Made 'always prepared' spells show up in casting list 2021-01-30 11:22:29 +02:00
Stefan Zermatten
0ea3f7a975 Migrated fields to embedded property calculations 2021-01-29 13:04:53 +02:00
Stefan Zermatten
1167538977 Denormalised inline calculations to property documents, needs to be referenced by UI still 2021-01-29 12:29:01 +02:00
Stefan Zermatten
9c799e3dc9 Added search to slot fill dialog 2021-01-28 17:09:37 +02:00
Stefan Zermatten
2bf749c4f1 Fixed: Layout of slot fill dialog is no longer broken by tall images, background is now the correct color 2021-01-28 16:48:33 +02:00
Stefan Zermatten
db83d5f82a Fix: Tabs and tab headers no longer lose sync when changing characters 2021-01-28 16:17:03 +02:00
Stefan Zermatten
d596061fa8 Fixed: Attributes that aren't ability scores now have their modifiers removed correctly 2021-01-28 16:05:47 +02:00
Stefan Zermatten
78efe639ed Fixed: Zero effect passive bonus now causes passive skill value to show. 2021-01-28 16:01:58 +02:00
Stefan Zermatten
e5bde38745 Removed console timers from recompute function: it's not the primary slowdown 2021-01-28 15:00:36 +02:00
Stefan Zermatten
83f2047dbe Replaced expensive getActiveProperties with cheaper filter by inactive field 2021-01-28 14:29:10 +02:00
Stefan Zermatten
fc03097ed8 Patreon access is now given by tier instead of by price paid 2021-01-28 12:15:45 +02:00
Stefan Zermatten
252ac23391 Merge branch 'version-2' of https://github.com/ThaumRystra/DiceCloud into version-2 2021-01-27 22:25:19 +02:00
Stefan Zermatten
531ddce6a0 Added dependency tracking to computations for future optimization effort 2021-01-27 22:24:28 +02:00
Stefan Zermatten
3cdeb73c30 Stopped removed items from showing up in inventory containers 2021-01-25 12:38:46 +02:00
Stefan Zermatten
c780c11e3f Improved spell slot casting dialog with search, filters, spell details 2021-01-22 14:09:23 +02:00
Stefan Zermatten
100c93b5ae Stopped dialog animation mocking transparent elements' background color 2021-01-22 12:20:33 +02:00
Stefan Zermatten
a4e6dd1d66 Added spellcasting to the stats page, click the icon next to a spell slot to cast 2021-01-19 16:10:34 +02:00
Stefan Zermatten
1b3b6362f7 Exposed spellSlotLevelValue to creature variables store 2021-01-19 12:07:40 +02:00
Stefan Zermatten
bdf4074e3c Added first pass at denormalizing inventory data, needs testing and integration 2021-01-15 17:41:06 +02:00
Stefan Zermatten
9492b2d8b8 Characters now start with a slot for a base. New characters start on the character tab with the build dialog open. 2021-01-14 12:28:51 +02:00
620 changed files with 25595 additions and 10937 deletions

View File

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

1
app/.gitignore vendored
View File

@@ -2,6 +2,7 @@
.meteor/meteorite
.demeteorized
.cache
.vscode
settings.json
public/components
public/_imports.html

View File

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

View File

@@ -1 +1 @@
METEOR@1.11.1
METEOR@2.6.1

View File

@@ -1,138 +1,126 @@
accounts-base@1.7.0
accounts-google@1.3.3
accounts-oauth@1.2.0
accounts-password@1.6.2
accounts-base@2.2.1
accounts-google@1.4.0
accounts-oauth@1.4.0
accounts-password@2.2.0
accounts-patreon@0.1.0
accounts-ui@1.3.1
accounts-ui-unstyled@1.4.2
akryum:npm-check@0.1.2
akryum:vue-component@0.15.2
akryum:vue-component-dev-client@0.4.7
akryum:vue-component-dev-server@0.1.4
akryum:vue-router2@0.2.3
aldeed:collection2@3.2.1
aldeed:collection2@3.5.0
aldeed:schema-index@3.0.0
allow-deny@1.1.0
autoupdate@1.6.0
babel-compiler@7.5.3
allow-deny@1.1.1
autoupdate@1.8.0
babel-compiler@7.8.1
babel-runtime@1.5.0
base64@1.0.12
binary-heap@1.0.11
blaze@2.3.4
blaze-tools@1.0.10
blaze-tools@1.1.2
boilerplate-generator@1.7.1
bozhao:link-accounts@2.2.1
bozhao:link-accounts@2.4.0
caching-compiler@1.2.2
caching-html-compiler@1.1.3
callback-hook@1.3.0
caching-html-compiler@1.2.1
callback-hook@1.4.0
check@1.3.1
chuangbo:marked@0.3.5_1
coffeescript@2.4.1
coffeescript-compiler@2.4.1
dburles:collection-helpers@1.1.0
dburles:mongo-collection-instances@0.3.5
dburles:mongo-collection-instances@0.3.6
ddp@1.4.0
ddp-client@2.3.3
ddp-client@2.5.0
ddp-common@1.4.0
ddp-rate-limiter@1.0.9
ddp-server@2.3.2
deps@1.0.12
ddp-rate-limiter@1.1.0
ddp-server@2.5.0
diff-sequence@1.1.1
dynamic-import@0.5.3
ecmascript@0.14.3
ecmascript-runtime@0.7.0
ecmascript-runtime-client@0.11.0
ecmascript-runtime-server@0.10.0
dynamic-import@0.7.2
ecmascript@0.16.1
ecmascript-runtime@0.8.0
ecmascript-runtime-client@0.12.1
ecmascript-runtime-server@0.11.0
ejson@1.1.1
email@2.0.0
email@2.2.0
es5-shim@4.8.0
fetch@0.1.1
geojson-utils@1.0.10
google-config-ui@1.0.1
google-oauth@1.3.0
google-oauth@1.4.1
hot-code-push@1.0.4
html-tools@1.0.11
htmljs@1.0.11
http@1.4.2
id-map@1.1.0
html-tools@1.1.2
htmljs@1.1.1
http@2.0.0
id-map@1.1.1
inter-process-messaging@0.1.1
jquery@1.11.11
lai:collection-extensions@0.2.1_1
launch-screen@1.2.0
less@2.8.0
livedata@1.0.18
lmieulet:meteor-coverage@1.1.4
lai:collection-extensions@0.3.0
launch-screen@1.3.0
localstorage@1.2.0
logging@1.1.20
logging@1.3.1
mdg:validated-method@1.2.0
meteor@1.9.3
meteor-base@1.4.0
meteorhacks:picker@1.0.3
meteorhacks:subs-manager@1.6.4
meteortesting:browser-tests@1.3.4
meteortesting:mocha@1.1.5
meteortesting:mocha-core@7.0.1
mikowals:batch-insert@1.2.0
minifier-css@1.5.3
minifier-js@2.6.0
minimongo@1.6.0
meteor@1.10.0
meteor-base@1.5.1
meteortesting:browser-tests@1.3.5
meteortesting:mocha@2.0.3
meteortesting:mocha-core@8.1.2
mikowals:batch-insert@1.3.0
minifier-css@1.6.0
minifier-js@2.7.3
minimongo@1.8.0
mobile-experience@1.1.0
mobile-status-bar@1.1.0
modern-browsers@0.1.5
modules@0.15.0
modern-browsers@0.1.7
modules@0.18.0
modules-runtime@0.12.0
momentjs:moment@2.29.1
mongo@1.10.0
mongo-decimal@0.1.1
mongo@1.14.6
mongo-decimal@0.1.2
mongo-dev-server@1.1.0
mongo-id@1.0.7
npm-bcrypt@0.9.3
npm-mongo@3.8.1
oauth@1.3.2
oauth2@1.3.0
observe-sequence@1.0.16
ongoworks:speakingurl@9.0.0
mongo-id@1.0.8
npm-mongo@4.3.1
oauth@2.1.1
oauth2@1.3.1
ordered-dict@1.1.0
ostrio:cookies@2.7.0
ostrio:files@2.0.1
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-mongo@0.4.1
peerlibrary:reactive-publish@0.10.0
peerlibrary:server-autorun@0.8.0
percolate:migrations@0.9.8
peerlibrary:subscription-data@0.8.0
percolate:migrations@1.0.3
percolate:synced-cron@1.3.2
promise@0.11.2
raix:eventemitter@0.1.3
promise@0.12.0
raix:eventemitter@1.0.0
random@1.2.0
rate-limit@1.0.9
react-fast-refresh@0.2.2
reactive-dict@1.3.0
reactive-var@1.0.11
reload@1.3.0
reload@1.3.1
retry@1.1.0
routepolicy@1.1.0
seba:minifiers-autoprefixer@1.2.1
service-configuration@1.0.11
routepolicy@1.1.1
seba:minifiers-autoprefixer@2.0.1
service-configuration@1.3.0
session@1.2.0
sha@1.0.9
shell-server@0.5.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
standard-minifier-js@2.6.0
static-html@1.2.2
templates:array@1.0.3
templating@1.3.2
templating-compiler@1.3.3
templating-runtime@1.3.2
templating-tools@1.1.2
tmeasday:check-npm-versions@0.3.2
simple:json-routes@2.3.1
simple:rest@1.2.1
simple:rest-bearer-token-parser@1.1.1
simple:rest-json-error-handler@1.1.1
simple:rest-method-mixin@1.1.0
socket-stream-client@0.4.0
spacebars-compiler@1.3.0
standard-minifier-js@2.8.0
static-html@1.3.2
templating-tools@1.2.1
tmeasday:check-npm-versions@1.0.2
tracker@1.2.0
typescript@4.4.1
underscore@1.0.10
url@1.3.1
webapp@1.9.1
webapp-hashing@1.0.9
url@1.3.2
webapp@1.13.1
webapp-hashing@1.1.0
zer0th:meteor-vuetify-loader@0.1.41

View File

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

View File

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

View File

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

View File

@@ -1,598 +0,0 @@
import { Meteor } from 'meteor/meteor';
import { Mongo } from 'meteor/mongo';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
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, restore } 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';
import { reorderDocs } from '/imports/api/parenting/order.js';
import '/imports/api/creature/actions/doAction.js';
import '/imports/api/creature/creatureProperties/manageEquipment.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,
},
});
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}
});
}
export 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') {
reorderDocs({collection: CreatureProperties, ancestorId: ref.id});
recomputeCreature.call({charId: ref.id});
}
}
}
const insertProperty = new ValidatedMethod({
name: 'creatureProperties.insert',
validate: null,
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
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.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);
assertPropertyEditPermission(creatureProperty, this.userId);
delete creatureProperty._id;
CreatureProperties.insert(creatureProperty);
recomputeCreatures(creatureProperty);
},
});
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
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 insertedDocIds = CreatureProperties.batchInsert(nodes);
// get the root inserted doc
let rootId = insertedDocIds[insertedDocIds.length - 1];
// Recompute the creatures doc was attached to
recomputeCreatures(node);
// Return the docId of the last property, the inserted root property
return rootId;
},
})
const updateProperty = 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}) {
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);
},
});
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;
}
}
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);
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: property, operation, value})
});
recomputeCreature.call({charId: creatureId});
}
})
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}) {
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`
);
}
damagePropertyWork({property: currentProperty, operation, value})
recomputeCreatures(currentProperty);
},
});
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}) {
let creature = Creatures.findOne(creatureId, {
fields: {
damageMultipliers: 1,
owner: 1,
readers: 1,
writers: 1,
},
});
// Check permissions
assertEditPermission(creature, this.userId);
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;
healthBars.forEach(healthBar => {
if (damageLeft === 0) return;
let damageAdded = damagePropertyWork({
property: healthBar,
operation: 'increment',
value: damageLeft,
});
damageLeft -= damageAdded;
});
recomputeCreature.call({charId: creatureId});
return totalDamage;
},
});
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
});
}
}
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}) {
let currentProperty = CreatureProperties.findOne(_id);
// Check permissions
assertPropertyEditPermission(currentProperty, this.userId);
adjustQuantityWork({property: currentProperty, operation, value});
recomputeCreatures(currentProperty);
},
});
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}) {
let action = CreatureProperties.findOne(actionId);
// Check permissions
assertPropertyEditPermission(action, 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,
});
recomputeCreatures(action);
},
});
const pushToProperty = new ValidatedMethod({
name: 'creatureProperties.push',
validate: null,
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
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.pull',
validate: null,
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
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.softRemove',
validate: new SimpleSchema({
_id: SimpleSchema.RegEx.Id
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({_id}){
let property = CreatureProperties.findOne(_id);
assertPropertyEditPermission(property, this.userId);
softRemove({_id, collection: CreatureProperties});
recomputeCreatures(property);
}
});
const restoreProperty = new ValidatedMethod({
name: 'creatureProperties.restore',
validate: new SimpleSchema({
_id: SimpleSchema.RegEx.Id
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({_id}){
let property = CreatureProperties.findOne(_id);
assertPropertyEditPermission(property, this.userId);
restore({_id, collection: CreatureProperties});
recomputeCreatures(property);
}
});
export default CreatureProperties;
export {
CreaturePropertySchema,
insertProperty,
duplicateProperty,
insertPropertyFromLibraryNode,
updateProperty,
dealDamage,
damagePropertiesByName,
damageProperty,
adjustQuantity,
selectAmmoItem,
pushToProperty,
pullFromProperty,
softRemoveProperty,
restoreProperty,
};

View File

@@ -1,28 +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,
},
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

@@ -1,11 +0,0 @@
import spendResources from '/imports/api/creature/actions/spendResources.js'
import {insertCreatureLog} from '/imports/api/creature/log/CreatureLogs.js';
export default function applyAction({prop, creature}){
spendResources(prop);
insertCreatureLog.call({
log: {
text: prop.name,
creatureId: creature._id},
});
}

View File

@@ -1,56 +0,0 @@
import evaluateString from '/imports/api/creature/computation/afterComputation/evaluateString.js';
import {insertCreatureLog} from '/imports/api/creature/log/CreatureLogs.js';
import { damagePropertiesByName } from '/imports/api/creature/CreatureProperties.js';
export default function applyAdjustment({
prop,
creature,
targets,
actionContext
}){
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') {
return insertCreatureLog.call({ log: {
text: errors.join(', ') || 'Something went wrong',
creatureId: creature._id,
}});
}
} catch (e){
return insertCreatureLog.call({ log: {
text: e.toString(),
creatureId: creature._id,
}});
}
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
});
insertCreatureLog.call({
log: {
text: `${prop.stat} ${prop.operation === 'set' ? 'set to' : ''} ${-result}`,
creatureId: target._id,
}
});
});
} else {
insertCreatureLog.call({
log: {
text: `${prop.stat} ${prop.operation === 'set' ? 'set to' : ''} ${-result}`,
creatureId: creature._id,
}
});
}
}

View File

@@ -1,18 +0,0 @@
import roll from '/imports/parser/roll.js';
import {insertCreatureLog} from '/imports/api/creature/log/CreatureLogs.js';
export default function applyAttack({
prop,
//children,
creature,
//targets,
//actionContext
}){
let result = roll(1, 20)[0] + prop.rollBonusResult;
insertCreatureLog.call({
log: {
text: `${prop.name} attack. ${result} to hit`,
creatureId: creature._id,
}
});
}

View File

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

View File

@@ -1,63 +0,0 @@
import evaluateString from '/imports/api/creature/computation/afterComputation/evaluateString.js';
import {insertCreatureLog} from '/imports/api/creature/log/CreatureLogs.js';
import { dealDamage } from '/imports/api/creature/CreatureProperties.js';
export default function applyDamage({
prop,
creature,
targets,
actionContext
}){
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') {
return insertCreatureLog.call({ log: {
text: errors.join(', '),
creatureId: creature._id,
}});
}
} catch (e){
return insertCreatureLog.call({ log: {
text: e.toString(),
creatureId: creature._id,
}});
}
if (damageTargets) {
damageTargets.forEach(target => {
if (prop.target === 'each'){
result = evaluateString(prop.amount, scope, 'reduce');
}
let damageDealt = dealDamage.call({
creatureId: target._id,
damageType: prop.damageType,
amount: result,
});
insertCreatureLog.call({
log: {
text: `Recieved ${damageDealt} ${prop.damageType}${prop.damageType !== 'healing'? ' damage': ''}`,
creatureId: target._id,
}
});
if (target._id !== creature._id){
insertCreatureLog.call({
log: {
text: `Dealt ${damageDealt} ${prop.damageType}${prop.damageType !== 'healing'? ' damage': ''}`,
creatureId: creature._id,
}
});
}
});
} else {
insertCreatureLog.call({
log: {
text: `${result} ${prop.damageType}${prop.damageType !== 'healing'? ' damage': ''}`,
creatureId: creature._id,
}
});
}
}

View File

@@ -1,67 +0,0 @@
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 applyDamage from '/imports/api/creature/actions/applyDamage.js';
import applyBuff from '/imports/api/creature/actions/applyBuff.js';
function applyProperty(options){
let prop = options.prop;
if (
prop.disabled === true || // ignore disabled props
prop.equipped === false || // ignore unequipped items
prop.toggleResult === false || // ignore untoggled toggles
prop.applied === true // ignore buffs that are already applied
){
return false;
}
switch (prop.type){
case 'action':
case 'spell':
applyAction(options);
return true;
case 'attack':
applyAction(options);
applyAttack(options);
return true;
case 'damage':
applyDamage(options);
return true;
case 'adjustment':
applyAdjustment(options);
return true;
case 'buff':
applyBuff(options);
return false;
case 'roll':
// applyRoll(options);
return true;
case 'savingThrow':
// applySavingThrow(options);
return false;
}
}
export default function applyProperties({
forest,
creature,
targets,
actionContext
}){
forest.forEach(child => {
let walkChildren = applyProperty({
prop: child.node,
children: child.children,
creature,
targets,
actionContext
});
if (walkChildren){
applyProperties({
forest: child.children,
creature,
targets,
actionContext
});
}
});
}

View File

@@ -1,62 +0,0 @@
import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import CreatureProperties, { getCreature } from '/imports/api/creature/CreatureProperties.js';
import { assertEditPermission } from '/imports/api/creature/creaturePermissions.js';
import { recomputeCreatureByDoc } from '/imports/api/creature/computation/recomputeCreature.js';
import { nodesToTree } from '/imports/api/parenting/parenting.js';
import applyProperties from '/imports/api/creature/actions/applyProperties.js';
const doAction = new ValidatedMethod({
name: 'creatureProperties.doAction',
validate: new SimpleSchema({
actionId: SimpleSchema.RegEx.Id,
targetId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
optional: true,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 10,
timeInterval: 5000,
},
run({actionId, targetId}) {
let action = CreatureProperties.findOne(actionId);
// Check permissions
let creature = getCreature(action);
assertEditPermission(creature, this.userId);
let target = undefined;
if (targetId) {
target = getCreature(targetId);
assertEditPermission(target, this.userId);
}
doActionWork({action, creature, target});
// Note this only recomputes the top-level creature, not the nearest one
recomputeCreatureByDoc(creature);
if (target){
recomputeCreatureByDoc(target);
}
},
});
function doActionWork({action, creature, target}){
let actionContext = {};
let decendantForest = nodesToTree({
collection: CreatureProperties,
ancestorId: action._id,
});
let startingForest = [{
node: action,
children: decendantForest,
}];
applyProperties({
forest: startingForest,
creature,
target,
actionContext
});
}
export default doAction;

View File

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

@@ -1,57 +0,0 @@
import CreatureProperties, { damagePropertyWork, adjustQuantityWork } from '/imports/api/creature/CreatureProperties.js';
export default function spendResources(action){
// Check Uses
if (action.usesUsed >= action.usesResult){
throw new Meteor.Error('Insufficient Uses',
'This action has no uses left');
}
// Resources
if (action.insufficientResources){
throw new Meteor.Error('Insufficient Resources',
'This creature doesn\'t have sufficient resources to perform this action');
}
// Items
let itemQuantityAdjustments = [];
action.resources.itemsConsumed.forEach(itemConsumed => {
if (!itemConsumed.itemId){
throw new Meteor.Error('Ammo not selected',
'No ammo was selected for this action');
}
let item = CreatureProperties.findOne(itemConsumed.itemId);
if (!item || item.ancestors[0].id !== action.ancestors[0].id){
throw new Meteor.Error('Ammo not found',
'The action\'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,
});
});
// 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
CreatureProperties.update(action._id, {
$inc: {usesUsed: 1}
}, {
selector: action
});
// Damage stats
action.resources.attributesConsumed.forEach(attConsumed => {
if (!attConsumed.quantity) return;
let stat = CreatureProperties.findOne(attConsumed.statId);
damagePropertyWork({
property: stat,
operation: 'increment',
value: attConsumed.quantity,
});
});
}

View File

@@ -0,0 +1,17 @@
import { createS3FilesCollection } from '/imports/api/files/s3FileStorage.js';
const ArchiveCreatureFiles = createS3FilesCollection({
collectionName: 'archiveCreatureFiles',
storagePath: Meteor.isDevelopment ? '/DiceCloud/archiveCreatures/' : 'assets/app/archiveCreatures',
onBeforeUpload(file) {
// Allow upload files under 10MB, and only in json format
if (file.size > 10485760) {
return 'Please upload with size equal or less than 10MB';
}
if (!/json/i.test(file.extension)){
return 'Please upload only a JSON file';
}
}
});
export default ArchiveCreatureFiles;

View File

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

View File

@@ -0,0 +1,78 @@
import SCHEMA_VERSION from '/imports/constants/SCHEMA_VERSION.js';
import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import { assertOwnership } from '/imports/api/creature/creatures/creaturePermissions.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js';
import Experiences from '/imports/api/creature/experience/Experiences.js';
import { removeCreatureWork } from '/imports/api/creature/creatures/methods/removeCreature.js';
import ArchiveCreatureFiles from '/imports/api/creature/archive/ArchiveCreatureFiles.js';
export function getArchiveObj(creatureId){
// Build the archive document
const creature = Creatures.findOne(creatureId);
const properties = CreatureProperties.find({'ancestors.id': creatureId}).fetch();
const experiences = Experiences.find({creatureId}).fetch();
const logs = CreatureLogs.find({creatureId}).fetch();
let archiveCreature = {
meta: {
type: 'DiceCloud V2 Creature Archive',
schemaVersion: SCHEMA_VERSION,
archiveDate: new Date(),
},
creature,
properties,
experiences,
logs,
};
return archiveCreature;
}
export function archiveCreature(creatureId){
const archive = getArchiveObj(creatureId);
const buffer = Buffer.from(JSON.stringify(archive, null, 2));
ArchiveCreatureFiles.write(buffer, {
fileName: `${archive.creature.name || archive.creature._id}.json`,
type: 'application/json',
userId: archive.creature.owner,
meta: {
schemaVersion: SCHEMA_VERSION,
creatureId: archive.creature._id,
creatureName: archive.creature.name,
},
}, (error) => {
if (error){
throw error;
} else {
removeCreatureWork(creatureId);
}
}, true);
}
const archiveCreatureToFile = new ValidatedMethod({
name: 'Creatures.methods.archiveCreatureToFile',
validate: new SimpleSchema({
'creatureId': {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 10,
timeInterval: 5000,
},
async run({creatureId}) {
assertOwnership(creatureId, this.userId);
if (Meteor.isServer){
archiveCreature(creatureId);
} else {
removeCreatureWork(creatureId);
}
},
});
export default archiveCreatureToFile;

View File

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

View File

@@ -0,0 +1,82 @@
import SCHEMA_VERSION from '/imports/constants/SCHEMA_VERSION.js';
import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js';
import Experiences from '/imports/api/creature/experience/Experiences.js';
import { removeCreatureWork } from '/imports/api/creature/creatures/methods/removeCreature.js';
import ArchiveCreatureFiles from '/imports/api/creature/archive/ArchiveCreatureFiles.js';
let migrateArchive;
if (Meteor.isServer){
migrateArchive = require('/imports/migrations/server/migrateArchive.js').default;
}
function restoreCreature(archive){
if (SCHEMA_VERSION < archive.meta.schemaVersion){
throw new Meteor.Error('Incompatible',
'The archive file is from a newer version. Update required to read.')
}
// Migrate and verify the archive meets the current schema
migrateArchive(archive);
// Insert the creature sub documents
// They still have their original _id's
Creatures.insert(archive.creature);
try {
// Add all the properties
if (archive.properties && archive.properties.length){
CreatureProperties.batchInsert(archive.properties);
}
if (archive.experiences && archive.experiences.length){
Experiences.batchInsert(archive.experiences);
}
if (archive.logs && archive.logs.length){
CreatureLogs.batchInsert(archive.logs);
}
} catch (e) {
// If the above fails, delete the inserted creature
removeCreatureWork(archive.creature._id);
throw e;
}
}
const restoreCreaturefromFile = new ValidatedMethod({
name: 'Creatures.methods.restoreCreaturefromFile',
validate: new SimpleSchema({
'fileId': {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 10,
timeInterval: 5000,
},
async run({fileId}) {
// fetch the file
const file = ArchiveCreatureFiles.findOne({_id: fileId}).get();
if (!file){
throw new Meteor.Error('File not found',
'The requested creature archive does not exist');
}
// Assert ownership
const userId = file?.userId;
if (!userId || userId !== this.userId){
throw new Meteor.Error('Permission denied',
'You can only restore creatures you own');
}
if (Meteor.isServer){
// Read the file data
const archive = await ArchiveCreatureFiles.readJSONFile(file);
restoreCreature(archive);
}
//Remove the archive once the restore succeeded
ArchiveCreatureFiles.remove({_id: fileId});
},
});
export default restoreCreaturefromFile;

View File

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

View File

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

View File

@@ -1,88 +0,0 @@
import evaluateCalculation from '/imports/api/creature/computation/evaluateCalculation.js';
export default class EffectAggregator{
constructor(stat, memo){
delete this.baseValueErrors;
if (stat.baseValueCalculation){
let {result, context} = evaluateCalculation(stat.baseValueCalculation, memo);
this.statBaseValue = result.value;
if (context.errors.length){
this.baseValueErrors = context.errors;
}
this.base = this.statBaseValue;
} else {
this.base = 0;
}
this.add = 0;
this.mul = 1;
this.min = Number.NEGATIVE_INFINITY;
this.max = Number.POSITIVE_INFINITY;
this.advantage = 0;
this.disadvantage = 0;
this.passiveAdd = 0;
this.fail = 0;
this.set = undefined;
this.conditional = [];
this.rollBonus = [];
this.hasNoEffects = true;
}
addEffect(effect){
let result = effect.result;
if (this.hasNoEffects) this.hasNoEffects = false;
switch(effect.operation){
case 'base':
// Take the largest base value
this.base = result > this.base ? result : this.base;
if (effect.statBase){
if (this.statBaseValue === undefined || result > this.statBaseValue){
this.statBaseValue = result;
}
}
break;
case 'add':
// Add all adds together
this.add += result;
break;
case 'mul':
// Multiply the muls together
this.mul *= result;
break;
case 'min':
// Take the largest min value
this.min = result > this.min ? result : this.min;
break;
case 'max':
// Take the smallest max value
this.max = result < this.max ? result : this.max;
break;
case 'set':
// Take the highest set value
this.set = this.set === undefined || result > this.set ? result : this.set;
break;
case 'advantage':
// Sum number of advantages
this.advantage++;
break;
case 'disadvantage':
// Sum number of disadvantages
this.disadvantage++;
break;
case 'passiveAdd':
// Add all passive adds together
this.passiveAdd += result;
break;
case 'fail':
// Sum number of fails
this.fail++;
break;
case 'conditional':
// Store array of conditionals
this.conditional.push(result);
break;
case 'rollBonus':
// Store array of roll bonuses
this.rollBonus.push(result);
break;
}
}
}

View File

@@ -1,29 +0,0 @@
import { parse, CompilationContext } from '/imports/parser/parser.js';
import ConstantNode from '/imports/parser/parseTree/ConstantNode.js';
export default function evaluateString(string, scope, fn = 'compile'){
let errors = [];
if (!string){
errors.push('No string provided');
return {result: string, errors};
}
if (!scope) errors.push('No scope provided');
// Parse the string using mathjs
let node;
try {
node = parse(string);
} catch (e) {
errors.push(e);
return {result: string, errors};
}
let 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};
}
}

View File

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

View File

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

View File

@@ -1,131 +0,0 @@
import computeStat from '/imports/api/creature/computation/computeStat.js';
import applyToggles from '/imports/api/creature/computation/applyToggles.js';
import evaluateCalculation from '/imports/api/creature/computation/evaluateCalculation.js';
export default function combineStat(stat, aggregator, memo){
if (stat.type === 'attribute'){
combineAttribute(stat, aggregator, memo);
} else if (stat.type === 'skill'){
combineSkill(stat, aggregator, memo);
} else if (stat.type === 'damageMultiplier'){
combineDamageMultiplier(stat, memo);
}
}
function getAggregatorResult(stat, aggregator){
let result = (aggregator.base + aggregator.add) * aggregator.mul;
if (result < aggregator.min) {
result = aggregator.min;
}
if (result > aggregator.max) {
result = aggregator.max;
}
if (aggregator.set !== undefined) {
result = aggregator.set;
}
if (!stat.decimal && Number.isFinite(result)){
result = Math.floor(result);
}
return result;
}
function combineAttribute(stat, aggregator, memo){
stat.value = getAggregatorResult(stat, aggregator);
stat.baseValue = aggregator.statBaseValue;
stat.baseValueErrors = aggregator.baseValueErrors;
if (stat.attributeType === 'spellSlot'){
let {result, context} = evaluateCalculation(stat.spellSlotLevelCalculation, memo);
stat.spellSlotLevelValue = result.value;
stat.spellSlotLevelErrors = context.errors;
}
stat.currentValue = stat.value - (stat.damage || 0);
if (stat.attributeType === 'ability') {
stat.modifier = Math.floor((stat.currentValue - 10) / 2);
}
stat.hide = aggregator.hasNoEffects &&
stat.baseValue === undefined ||
undefined
}
function combineSkill(stat, aggregator, memo){
// Skills are based on some ability Modifier
let ability = memo.statsByVariableName[stat.ability]
if (stat.ability && ability){
if (!ability.computationDetails.computed){
computeStat(ability, memo);
}
stat.abilityMod = ability.modifier;
}
// Combine all the child proficiencies
stat.proficiency = stat.baseProficiency || 0;
for (let i in stat.computationDetails.proficiencies){
let prof = stat.computationDetails.proficiencies[i];
applyToggles(prof, memo);
if (
!prof.computationDetails.disabledByToggle &&
prof.value > stat.proficiency
){
stat.proficiency = prof.value;
}
}
// Get the character's proficiency bonus to apply
let profBonusStat = memo.statsByVariableName['proficiencyBonus'];
let profBonus = profBonusStat && profBonusStat.value;
if (typeof profBonus !== 'number' && memo.statsByVariableName['level']){
let level = memo.statsByVariableName['level'].value;
profBonus = Math.ceil(level / 4) + 1;
}
// Multiply the proficiency bonus by the actual proficiency
profBonus *= stat.proficiency;
// Base value
stat.baseValue = aggregator.statBaseValue;
stat.baseValueErrors = aggregator.baseValueErrors;
// Combine everything to get the final result
let result = (aggregator.base + stat.abilityMod + profBonus + aggregator.add) * aggregator.mul;
if (result < aggregator.min) result = aggregator.min;
if (result > aggregator.max) result = aggregator.max;
if (aggregator.set !== undefined) {
result = aggregator.set;
}
if (Number.isFinite(result)){
result = Math.floor(result);
}
stat.value = result;
// Advantage/disadvantage
if (aggregator.advantage && !aggregator.disadvantage){
stat.advantage = 1;
} else if (aggregator.disadvantage && !aggregator.advantage){
stat.advantage = -1;
} else {
stat.advantage = 0;
}
// Passive bonus
stat.passiveBonus = aggregator.passiveAdd;
// conditional benefits
stat.conditionalBenefits = aggregator.conditional;
// Roll bonuses
stat.rollBonus = aggregator.rollBonus;
// Forced to fail
stat.fail = aggregator.fail;
// Rollbonus
stat.rollBonuses = aggregator.rollBonus;
// Hide
stat.hide = aggregator.hasNoEffects &&
stat.baseValue === undefined &&
stat.proficiency == 0 ||
undefined;
}
function combineDamageMultiplier(stat){
if (stat.immunityCount) return 0;
let result;
if (stat.ressistanceCount && !stat.vulnerabilityCount){
result = 0.5;
} else if (!stat.ressistanceCount && stat.vulnerabilityCount){
result = 2;
} else {
result = 1;
}
stat.value = result;
}

View File

@@ -1,45 +0,0 @@
import evaluateCalculation from '/imports/api/creature/computation/evaluateCalculation.js';
import applyToggles from '/imports/api/creature/computation/applyToggles.js';
export default function computeEffect(effect, memo){
if (effect.computationDetails.computed) return;
if (effect.computationDetails.busyComputing){
// Trying to compute this effect again while it is already computing.
// We must be in a dependency loop.
effect.computationDetails.computed = true;
effect.result = NaN;
effect.computationDetails.busyComputing = false;
effect.computationDetails.error = 'dependencyLoop';
if (Meteor.isClient) console.warn('dependencyLoop', effect);
return;
}
// Before doing any work, mark this effect as busy
effect.computationDetails.busyComputing = true;
// Apply any toggles
applyToggles(effect, memo);
// Determine result of effect calculation
delete effect.errors;
if (!effect.calculation){
if(effect.operation === 'add' || effect.operation === 'base'){
effect.result = 0;
} else {
delete effect.result
}
} else if (Number.isFinite(+effect.calculation)){
effect.result = +effect.calculation;
} else if(effect.operation === 'conditional' || effect.operation === 'rollBonus'){
effect.result = effect.calculation;
} else if(_.contains(['advantage', 'disadvantage', 'fail'], effect.operation)){
effect.result = 1;
} else {
let {result, context} = evaluateCalculation(effect.calculation, memo);
effect.result = result.value;
if (context.errors.length){
effect.errors = context.errors;
}
}
effect.computationDetails.computed = true;
effect.computationDetails.busyComputing = false;
}

View File

@@ -1,94 +0,0 @@
import evaluateCalculation from '/imports/api/creature/computation/evaluateCalculation.js';
export default function computeEndStepProperty(prop, memo){
switch (prop.type){
case 'action':
case 'spell':
computeAction(prop, memo);
break;
case 'attack':
computeAction(prop, memo);
computeAttack(prop, memo);
break;
case 'savingThrow':
computeSavingThrow(prop, memo);
break;
case 'spellList':
computeSpellList(prop, memo);
break;
case 'propertySlot':
computeSlot(prop, memo);
}
}
function computeAction(prop, memo){
// Uses
let {result, context} = evaluateCalculation(prop.uses, memo);
prop.usesResult = result.value;
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;
}
}
});
// 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;
}
});
}
function computePropertyField(prop, memo, fieldName, fn){
let {result, context} = evaluateCalculation(prop[fieldName], memo, fn);
prop[`${fieldName}Result`] = result.value;
if (context.errors.length){
prop[`${fieldName}Errors`] = context.errors;
} else {
delete prop[`${fieldName}Errors`];
}
}
function computeAttack(prop, memo){
computePropertyField(prop, memo, 'rollBonus');
}
function computeSavingThrow(prop, memo){
computePropertyField(prop, memo, 'dc');
}
function computeSpellList(prop, memo){
computePropertyField(prop, memo, 'maxPrepared');
}
function computeSlot(prop, memo){
computePropertyField(prop, memo, 'slotCondition');
}

View File

@@ -1,44 +0,0 @@
import { forOwn, has } from 'lodash';
export default function computeLevels(memo){
computeClassLevels(memo);
computeTotalLevel(memo);
}
function computeClassLevels(memo){
forOwn(memo.classLevelsById, classLevel => {
let name = classLevel.variableName;
let stat = memo.statsByVariableName[name];
if (!stat){
memo.statsByVariableName[name] = classLevel;
memo.classes[name] = classLevel;
} else if (!has(stat, 'level')){
// Stat is overriden by an attribute
return;
} else if (stat.level < classLevel.level) {
memo.statsByVariableName[name] = classLevel;
memo.classes[name] = classLevel;
}
});
}
function computeTotalLevel(memo){
let currentLevel = memo.statsByVariableName['level'];
if (!currentLevel){
currentLevel = {
value: 0,
computationDetails: {
builtIn: true,
computed: true,
}
};
memo.statsByVariableName['level'] = currentLevel;
}
// bail out if overriden by an attribute
if (!currentLevel.computationDetails.builtIn) return;
let level = 0;
for (let name in memo.classes){
level += memo.classes[name].level || 0;
}
memo.statsByVariableName['level'].value = level;
}

View File

@@ -1,27 +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';
import computeEndStepProperty from '/imports/api/creature/computation/computeEndStepProperty.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);
});
}

View File

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

View File

@@ -1,37 +0,0 @@
import evaluateCalculation from '/imports/api/creature/computation/evaluateCalculation.js';
export default function computeToggle(toggle, memo){
if (toggle.computationDetails.computed) return;
if (toggle.computationDetails.busyComputing){
// Trying to compute this effect again while it is already computing.
// We must be in a dependency loop.
toggle.computationDetails.computed = true;
toggle.result = false;
toggle.computationDetails.busyComputing = false;
toggle.computationDetails.error = 'dependencyLoop';
if (Meteor.isClient) console.warn('dependencyLoop', toggle);
return;
}
// Before doing any work, mark this toggle as busy
toggle.computationDetails.busyComputing = true;
// Do work
delete toggle.errors;
if (toggle.enabled){
toggle.toggleResult = true;
} else if (toggle.disabled){
toggle.toggleResult = false;
} else if (!toggle.condition){
toggle.toggleResult = false;
} else if (Number.isFinite(+toggle.condition)){
toggle.toggleResult = !!+toggle.condition;
} else {
let {result, context} = evaluateCalculation(toggle.condition, memo);
toggle.toggleResult = !!result.value;
if (context.errors.length){
toggle.errors = context.errors;
}
}
toggle.computationDetails.computed = true;
toggle.computationDetails.busyComputing = false;
}

View File

@@ -1,41 +0,0 @@
import computeStat from '/imports/api/creature/computation/computeStat.js';
import { 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';
/* Convert a calculation into a constant output and errors*/
export default function evaluateCalculation(string, memo, fn = 'reduce'){
if (!string) return {
context: {errors: []},
result: new ConstantNode({value: string, type: 'string'}),
};
let errors = [];
// Parse the string
let calc;
try {
calc = parse(string);
} catch (e) {
errors.push({
type: 'parsing',
message: e.message || e
});
return {
context: {errors},
result: new ConstantNode({value: string, type: 'string'}),
};
}
// Ensure all symbol nodes are defined and coputed
calc.traverse(node => {
if (node instanceof SymbolNode || node instanceof AccessorNode){
let stat = memo.statsByVariableName[node.name];
if (stat && !stat.computationDetails.computed){
computeStat(stat, memo);
}
}
});
// Evaluate
let context = new CompilationContext();
let result = calc[fn](memo.statsByVariableName, context);
return {result, context};
}

View File

@@ -1,138 +0,0 @@
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 CreatureProperties from '/imports/api/creature/CreatureProperties.js';
import computeMemo from '/imports/api/creature/computation/computeMemo.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/denormalise/recomputeDamageMultipliers.js';
import recomputeInactiveProperties from '/imports/api/creature/denormalise/recomputeInactiveProperties.js';
import recomputeSlotFullness from '/imports/api/creature/denormalise/recomputeSlotFullness.js';
import Creatures from '/imports/api/creature/Creatures.js';
export const recomputeCreature = new ValidatedMethod({
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(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);
},
});
const calculationPropertyTypes = [
'attribute',
'skill',
'effect',
'proficiency',
'classLevel',
'toggle',
'item',
// End step types
'action',
'attack',
'savingThrow',
'spellList',
'spell',
'propertySlot',
];
export function recomputeCreatureById(creatureId){
let creature = Creatures.findOne(creatureId);
recomputeCreatureByDoc(creature);
}
/**
* This function is the heart of DiceCloud. It recomputes a creature's stats,
* distilling down effects and proficiencies into the final stats that make up
* a creature.
*
* Essentially this is a depth first tree traversal algorithm that computes
* stats' dependencies before computing stats themselves, while detecting
* dependency loops.
*
* At the moment it makes no effort to limit recomputation to just what was
* changed.
*
* Attempting to implement dependency management to limit recomputation to just
* change affected stats should only happen as a last resort, when this function
* can no longer be performed more efficiently, and server resources can not be
* expanded to meet demand.
*
* A brief overview:
* - Fetch the stats of the creature and add them to
* an object for quick lookup
* - Fetch the effects and proficiencies which apply to each stat and store them with the stat
* - Fetch the class levels and store them as well
* - Mark each stat and effect as uncomputed
* - Iterate over each stat in order and compute it
* - If the stat is already computed, skip it
* - If the stat is busy being computed, we are in a dependency loop, make it NaN and mark computed
* - Mark the stat as busy computing
* - Iterate over each effect which applies to the attribute
* - If the effect is not computed compute it
* - If the effect relies on another attribute, get its computed value
* - Recurse if that attribute is uncomputed
* - apply the effect to the attribute
* - Conglomerate all the effects to compute the final stat values
* - Mark the stat as computed
* - Write the computed results back to the database
*/
export function recomputeCreatureByDoc(creature){
const creatureId = creature._id;
// find 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 active properties
let props = CreatureProperties.find({
'ancestors.id': creatureId,
removed: {$ne: true},
type: {$in: calculationPropertyTypes},
$or: [
{inactive: {$ne: true}},
// But also the inactive computed toggles and their decendants
{'ancestors.id': {$in: toggleIds}},
{_id: {$in: toggleIds}},
]
}, {
fields: { // Filter out potentially large fields
icon: 0,
summary: 0,
description: 0,
},
sort: {
order: 1,
}
}).fetch();
let computationMemo = new ComputationMemo(props, creature);
recomputeInactiveProperties(creatureId);
computeMemo(computationMemo);
writeAlteredProperties(computationMemo);
writeCreatureVariables(computationMemo, creatureId);
recomputeDamageMultipliersById(creatureId);
recomputeSlotFullness(creatureId);
return computationMemo;
}

View File

@@ -1,129 +0,0 @@
import { Meteor } from 'meteor/meteor'
import { isEqual, forOwn } from 'lodash';
import CreatureProperties from '/imports/api/creature/CreatureProperties.js';
// Schemas
// Calculated props
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';
// End step props
import { ComputedOnlyActionSchema } from '/imports/api/properties/Actions.js';
import { ComputedOnlyAttackSchema } from '/imports/api/properties/Attacks.js';
import { ComputedOnlySavingThrowSchema } from '/imports/api/properties/SavingThrows.js';
import { ComputedOnlySpellListSchema } from '/imports/api/properties/SpellLists.js';
import { ComputedOnlySpellSchema } from '/imports/api/properties/Spells.js';
import { ComputedOnlySlotSchema } from '/imports/api/properties/Slots.js';
const schemasByType = {
'skill': ComputedOnlySkillSchema,
'attribute': ComputedOnlyAttributeSchema,
'effect': ComputedOnlyEffectSchema,
'toggle': ComputedOnlyToggleSchema,
'action': ComputedOnlyActionSchema,
'attack': ComputedOnlyAttackSchema,
'savingThrow': ComputedOnlySavingThrowSchema,
'spellList': ComputedOnlySpellListSchema,
'spell': ComputedOnlySpellSchema,
'propertySlot': ComputedOnlySlotSchema,
};
export default function writeAlteredProperties(memo){
let bulkWriteOperations = [];
// Loop through all properties on the memo
forOwn(memo.propsById, changed => {
let schema = schemasByType[changed.type];
if (!schema) return;
let extraIds = changed.computationDetails.idsOfSameName;
let ids;
if (extraIds && extraIds.length){
ids = [changed._id, ...extraIds];
} else {
ids = [changed._id];
}
ids.forEach(id => {
let op = undefined;
let original = memo.originalPropsById[id];
op = addChangedKeysToOp(op, schema.objectKeys(), original, changed);
if (op){
bulkWriteOperations.push(op);
}
});
});
bulkWriteProperties(bulkWriteOperations);
}
function addChangedKeysToOp(op, keys, original, changed) {
// Loop through all keys that can be changed by computation
// and compile an operation that sets all those keys
for (let key of keys){
if (!isEqual(original[key], changed[key])){
if (!op) op = newOperation(original._id, changed.type);
let value = changed[key];
if (value === undefined){
// Unset values that become undefined
addUnsetOp(op, key);
} else {
// Set values that changed to something else
addSetOp(op, key, value);
}
}
}
return op;
}
function newOperation(_id, type){
let newOp = {
updateOne: {
filter: {_id},
update: {},
}
};
if (Meteor.isClient){
newOp.type = type;
}
return newOp;
}
function addSetOp(op, key, value){
if (op.updateOne.update.$set){
op.updateOne.update.$set[key] = value;
} else {
op.updateOne.update.$set = {[key]: value};
}
}
function addUnsetOp(op, key){
if (op.updateOne.update.$unset){
op.updateOne.update.$unset[key] = 1;
} else {
op.updateOne.update.$unset = {[key]: 1};
}
}
function bulkWriteProperties(bulkWriteOps){
if (!bulkWriteOps.length) return;
if (Meteor.isServer){
CreatureProperties.rawCollection().bulkWrite(
bulkWriteOps,
{ordered : false},
function(e){
if (e) {
console.error('Bulk write failed: ');
console.error(e);
}
}
);
} 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,
});
});
}
}

View File

@@ -1,39 +0,0 @@
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) {
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,
computeVersion: VERSION,
}});
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,109 @@
import { Mongo } from 'meteor/mongo';
import SimpleSchema from 'simpl-schema';
import ColorSchema from '/imports/api/properties/subSchemas/ColorSchema.js';
import ChildSchema from '/imports/api/parenting/ChildSchema.js';
import SoftRemovableSchema from '/imports/api/parenting/SoftRemovableSchema.js';
import propertySchemasIndex from '/imports/api/properties/computedPropertySchemasIndex.js';
import { storedIconsSchema } from '/imports/api/icons/Icons.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
let CreatureProperties = new Mongo.Collection('creatureProperties');
let CreaturePropertySchema = new SimpleSchema({
_id: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
_migrationError: {
type: String,
optional: true,
},
type: {
type: String,
allowedValues: Object.keys(propertySchemasIndex),
},
tags: {
type: Array,
defaultValue: [],
maxCount: STORAGE_LIMITS.tagCount,
},
'tags.$': {
type: String,
max: STORAGE_LIMITS.tagLength,
},
disabled: {
type: Boolean,
optional: true,
},
icon: {
type: storedIconsSchema,
optional: true,
max: STORAGE_LIMITS.icon,
},
// Reference to the library node that this property was copied from
libraryNodeId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
optional: true,
},
});
const DenormalisedOnlyCreaturePropertySchema = new SimpleSchema({
// 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,
removeBeforeCompute: true,
},
// 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,
removeBeforeCompute: true,
},
// Denormalised flag if this property was made inactive because of its own
// state
deactivatedBySelf: {
type: Boolean,
optional: true,
index: 1,
removeBeforeCompute: true,
},
// Denormalised flag if this property was made inactive because of a toggle
// calculation. Either an ancestor toggle calculation or its own.
deactivatedByToggle: {
type: Boolean,
optional: true,
index: 1,
removeBeforeCompute: true,
},
});
CreaturePropertySchema.extend(DenormalisedOnlyCreaturePropertySchema);
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 {
DenormalisedOnlyCreaturePropertySchema,
CreaturePropertySchema,
};

View File

@@ -1,7 +0,0 @@
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import getClosestPropertyAncestorCreature from '/imports/api/creature/creatureProperties/getClosestPropertyAncestorCreature.js';
export default function assertPropertyEditPermission(prop, userId){
let creature = getClosestPropertyAncestorCreature(prop);
assertEditPermission(creature, userId);
}

View File

@@ -1,7 +0,0 @@
import Creatures from '/imports/api/creature/Creatures.js';
import getClosestPropertyAncestorCreatureId from '/imports/api/creature/creatureProperties/getClosestPropertyAncestorCreatureId.js';
export default function getClosestPropertyAncestorCreature(prop){
let creatureId = getClosestPropertyAncestorCreatureId(prop);
return Creatures.findOne(creatureId);
}

View File

@@ -1,13 +0,0 @@
export default function getClosestPropertyAncestorCreatureId(prop){
if (!prop.ancestors) throw 'Property has no ancestors';
let creatureId;
// Find the last ancestor in the creature collection
for (let i = prop.ancestors.length - 1; i >= 0; i--){
if (prop.ancestors[i].collection === 'creatures'){
creatureId = prop.ancestors[i].id;
break;
}
}
if (!creatureId) throw 'This property has no creature ancestors';
return creatureId;
}

View File

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

View File

@@ -0,0 +1,67 @@
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 computeCreature from '/imports/api/engine/computeCreature.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
computeCreature(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,53 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import SimpleSchema from 'simpl-schema';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
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: {
variables: 1,
owner: 1,
readers: 1,
writers: 1,
},
});
assertEditPermission(creature, this.userId);
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});
});
}
});
export default damagePropertiesByName;

View File

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

View File

@@ -0,0 +1,72 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import SimpleSchema from 'simpl-schema';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
import computeCreature from '/imports/api/engine/computeCreature.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: {
owner: 1,
readers: 1,
writers: 1,
},
});
assertEditPermission(creature, this.userId);
const totalDamage = dealDamageWork({creature, damageType, amount})
computeCreature(creatureId);
return totalDamage;
},
});
export function dealDamageWork({creature, damageType, amount}){
// Get all the health bars and do damage to them
let healthBars = CreatureProperties.find({
'ancestors.id': creature._id,
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);
const totalDamage = amount;
let damageLeft = totalDamage;
if (damageType === 'healing') damageLeft = -totalDamage;
let propertyIds = [];
healthBars.forEach(healthBar => {
if (damageLeft === 0) return;
let damageAdded = damagePropertyWork({
property: healthBar,
operation: 'increment',
value: damageLeft,
});
damageLeft -= damageAdded;
propertyIds.push(healthBar._id);
});
return totalDamage;
}
export default dealDamage;

View File

@@ -0,0 +1,97 @@
import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import {
setLineageOfDocs,
renewDocIds
} from '/imports/api/parenting/parenting.js';
import { reorderDocs } from '/imports/api/parenting/order.js';
import computeCreature from '/imports/api/engine/computeCreature.js';
var snackbar;
if (Meteor.isClient){
snackbar = require(
'/imports/ui/components/snackbars/SnackbarQueue.js'
).snackbar
}
const DUPLICATE_CHILDREN_LIMIT = 50;
const duplicateProperty = new ValidatedMethod({
name: 'creatureProperties.duplicate',
validate: new SimpleSchema({
_id: {
type: String,
regEx: SimpleSchema.RegEx.Id,
}
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({_id}) {
let property = CreatureProperties.findOne(_id);
let creature = getRootCreatureAncestor(property);
assertEditPermission(creature, this.userId);
// Renew the doc ID
let randomSrc = DDP.randomStream('duplicateProperty');
let propertyId = randomSrc.id();
property._id = propertyId;
// Get all the descendants
let nodes = CreatureProperties.find({
'ancestors.id': _id,
removed: {$ne: true},
}, {
limit: DUPLICATE_CHILDREN_LIMIT + 1,
sort: {order: 1},
}).fetch();
// Alert the user if the limit was hit
if (nodes.length > DUPLICATE_CHILDREN_LIMIT){
nodes.pop();
if (Meteor.isClient){
snackbar({
text: `Only the first ${DUPLICATE_CHILDREN_LIMIT} children were duplicated`,
});
}
}
// re-map all the ancestors
setLineageOfDocs({
docArray: nodes,
newAncestry : [
...property.ancestors,
{id: propertyId, collection: 'creatureProperties'}
],
oldParent : {id: _id, collection: 'creatureProperties'},
});
// Give the docs new IDs without breaking internal references
renewDocIds({docArray: nodes});
// Order the root node
property.order += 0.5;
// Insert the properties
CreatureProperties.batchInsert([property, ...nodes]);
// Tree structure changed by inserts, reorder the tree
reorderDocs({
collection: CreatureProperties,
ancestorId: property.ancestors[0].id,
});
// Inserting a creature property invalidates dependencies: full recompute
computeCreature(creature._id);
return propertyId;
},
});
export default duplicateProperty;

View File

@@ -1,26 +1,12 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import CreatureProperties from '/imports/api/creature/CreatureProperties.js';
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 getClosestPropertyAncestorCreature from '/imports/api/creature/creatureProperties/getClosestPropertyAncestorCreature.js';
import INVENTORY_TAGS from '/imports/constants/INVENTORY_TAGS.js';
function getParentRefByTag(creatureId, tag){
let prop = CreatureProperties.findOne({
'ancestors.id': creatureId,
removed: {$ne: true},
inactive: {$ne: true},
tags: tag,
}, {
sort: {order: 1},
});
if (prop){
return {id: prop._id, collection: 'creatureProperties'};
} else {
return {id: creatureId, collection: 'creatures'};
}
}
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import computeCreature from '/imports/api/engine/computeCreature.js';
import BUILT_IN_TAGS from '/imports/constants/BUILT_IN_TAGS.js';
import getParentRefByTag from '/imports/api/creature/creatureProperties/methods/getParentRefByTag.js';
// Equipping or unequipping an item will also change its parent
const equipItem = new ValidatedMethod({
@@ -40,16 +26,17 @@ const equipItem = new ValidatedMethod({
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 = getClosestPropertyAncestorCreature(item);
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 tag = equipped ? BUILT_IN_TAGS.equipment : BUILT_IN_TAGS.carried;
let parentRef = getParentRefByTag(creature._id, tag);
// organizeDoc handles recompuation
if (!parentRef) parentRef = {id: creature._id, collection: 'creatures'};
organizeDoc.call({
docRef: {
id: _id,
@@ -57,8 +44,11 @@ const equipItem = new ValidatedMethod({
},
parentRef,
order: Number.MAX_SAFE_INTEGER,
skipRecompute: true,
});
computeCreature(creature._id);
},
});
export { equipItem, getParentRefByTag }
export default equipItem;

View File

@@ -0,0 +1,48 @@
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 computeCreature from '/imports/api/engine/computeCreature.js';
const flipToggle = new ValidatedMethod({
name: 'creatureProperties.flipToggle',
validate({_id}){
if (!_id) throw new Meteor.Error('No _id', '_id is required');
},
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({_id}) {
// Permission
let property = CreatureProperties.findOne(_id, {
fields: {type: 1, ancestors: 1, enabled: 1, disabled: 1}
});
if (property.type !== 'toggle'){
throw new Meteor.Error('wrong property',
'This method can only be applied to toggles');
}
if (!property.enabled && !property.disabled){
throw new Meteor.Error('Computed toggle',
'Can\'t flip a toggle that is computed')
}
let rootCreature = getRootCreatureAncestor(property);
assertEditPermission(rootCreature, this.userId);
// Invert the current value, disabled is the canonical store of value
const currentValue = !property.disabled;
CreatureProperties.update(_id, {$set: {
enabled: !currentValue,
disabled: currentValue,
}}, {
selector: {type: 'toggle'},
});
// Updating a toggle is likely to change the whole tree, do a full recompute
computeCreature(rootCreature._id);
},
});
export default flipToggle;

View File

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

View File

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

View File

@@ -0,0 +1,15 @@
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';
import '/imports/api/creature/creatureProperties/methods/flipToggle.js';

View File

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

View File

@@ -0,0 +1,241 @@
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 computeCreature from '/imports/api/engine/computeCreature.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 fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
const insertPropertyFromLibraryNode = new ValidatedMethod({
name: 'creatureProperties.insertPropertyFromLibraryNode',
validate: new SimpleSchema({
nodeIds: {
type: Array,
max: 20,
},
'nodeIds.$': {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
parentRef: {
type: RefSchema,
},
order: {
type: Number,
optional: true,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({nodeIds, parentRef, order}) {
// get the new ancestry for the properties
let {parentDoc, ancestors} = getAncestry({parentRef});
// Check permission to edit
let rootCreature;
if (parentRef.collection === 'creatures'){
rootCreature = parentDoc;
} else if (parentRef.collection === 'creatureProperties'){
rootCreature = getRootCreatureAncestor(parentDoc);
} else {
throw `${parentRef.collection} is not a valid parent collection`
}
assertEditPermission(rootCreature, this.userId);
// {libraryId: hasViewPermission}
//let libraryPermissionMemoir = {};
let node;
nodeIds.forEach(nodeId => {
// TODO: Check library view permission for each node before starting
node = insertPropertyFromNode(nodeId, ancestors, order);
});
// get one of the root inserted docs
let rootId = node._id;
// Tree structure changed by inserts, reorder the tree
reorderDocs({
collection: CreatureProperties,
ancestorId: rootCreature._id,
});
// Inserting a creature property invalidates dependencies: full recompute
computeCreature(rootCreature._id);
// Return the docId of the last property, the inserted root property
return rootId;
},
});
function insertPropertyFromNode(nodeId, ancestors, order){
// Fetch the library node and its decendents, provided they have not been
// removed
// TODO: Check permission to read the library this node is in
let node = LibraryNodes.findOne({
_id: nodeId,
removed: {$ne: true},
});
if (!node) {
if (Meteor.isClient) return;
else {
throw new Meteor.Error(
'Insert property from library failed',
`No library document with id '${nodeId}' was found`
);
}
}
let oldParent = node.parent;
let nodes = LibraryNodes.find({
'ancestors.id': nodeId,
removed: {$ne: true},
}).fetch();
// Convert all references into actual nodes
nodes = reifyNodeReferences(nodes);
// The root node is first in the array of nodes
// It must get the first generated ID to prevent flickering
nodes = [node, ...nodes];
// set libraryNodeIds
storeLibraryNodeReferences(nodes);
// re-map all the ancestors
setLineageOfDocs({
docArray: nodes,
newAncestry: ancestors,
oldParent,
});
// Give the docs new IDs without breaking internal references
renewDocIds({
docArray: nodes,
collectionMap: {'libraryNodes': 'creatureProperties'}
});
// Order the root node
if (order === undefined){
setDocToLastOrder({
collection: CreatureProperties,
doc: node,
});
} else {
node.order = order;
}
// Insert the creature properties
CreatureProperties.batchInsert(nodes);
return node;
}
function storeLibraryNodeReferences(nodes){
nodes.forEach(node => {
if (node.libraryNodeId) return;
node.libraryNodeId = node._id;
});
}
// Covert node references into actual nodes
// TODO: check permissions for each library a reference node references
function reifyNodeReferences(nodes, visitedRefs = new Set(), depth = 0){
depth += 1;
// New nodes added this function
let newNodes = [];
// Filter out the reference nodes we replace
let resultingNodes = nodes.filter(node => {
// This isn't a reference node, continue as normal
if (node.type !== 'reference') return true;
// We have gone too deep, keep the reference node as an error
if (depth >= 10){
if (Meteor.isClient) console.warn('Reference depth limit exceeded');
node.cache = {error: 'Reference depth limit exceeded'};
return true;
}
let referencedNode
try {
referencedNode = fetchDocByRef(node.ref);
referencedNode.order = node.order;
// We are definitely replacing this node, so add it to the list
visitedRefs.add(node._id);
} catch (e){
node.cache = {error: e.reason || e.message || e.toString()};
return true;
}
// Get all the descendants of the referenced node
let descendents = LibraryNodes.find({
'ancestors.id': referencedNode._id,
removed: {$ne: true},
}, {
sort: {order: 1},
}).fetch();
// We are adding the referenced node and its descendants
let addedNodes = [referencedNode, ...descendents];
// re-map all the ancestors to parent the new sub-tree into our existing
// node tree
setLineageOfDocs({
docArray: addedNodes,
newAncestry: node.ancestors,
oldParent: referencedNode.parent,
});
// Filter all the looped references
addedNodes = addedNodes.filter(addedNode => {
// Add all non-reference nodes
if (addedNode.type !== 'reference'){
return true;
}
// If this exact reference has already been resolved before, filter it out
if (visitedRefs.has(addedNode._id)){
return false;
} else {
// Otherwise mark it as visited, and keep it
visitedRefs.add(addedNode._id);
return true;
}
});
// Before renewing Ids make sure the library node reference is stored
storeLibraryNodeReferences(addedNodes);
// Give the new referenced sub-tree new ids
// The referenced node must get the id of the ref node so that the
// descendants of the ref node keep their ancestry intact
renewDocIds({
docArray: addedNodes,
idMap: { [referencedNode._id]: node._id },
});
// Reify the subtree as well with recursion
addedNodes = reifyNodeReferences(addedNodes, visitedRefs, depth);
// Store the new nodes from this inner loop without altering the array
// we are looping over
newNodes.push(...addedNodes);
});
// We are done filtering the array, we can add the new nodes to it
resultingNodes.push(...newNodes);
return resultingNodes;
}
export default insertPropertyFromLibraryNode;

View File

@@ -0,0 +1,36 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import computeCreature from '/imports/api/engine/computeCreature.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
computeCreature(rootCreature._id);
// recomputePropertyDependencies(property);
}
});
export default pullFromProperty;

View File

@@ -0,0 +1,52 @@
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 computeCreature from '/imports/api/engine/computeCreature.js';
import { get } from 'lodash';
const pushToProperty = new ValidatedMethod({
name: 'creatureProperties.push',
validate: null,
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({_id, path, value}){
// Permissions
let property = CreatureProperties.findOne(_id);
let rootCreature = getRootCreatureAncestor(property);
assertEditPermission(rootCreature, this.userId);
let joinedPath = path.join('.');
// Respect maxCount
let schema = CreatureProperties.simpleSchema(property);
let maxCount = schema.get(joinedPath, 'maxCount');
if (Number.isFinite(maxCount)){
let array = get(property, path);
let currentCount = array ? array.length : 0;
if (currentCount >= maxCount){
throw new Meteor.Error(
'Array is full',
`Cannot have more than ${maxCount} values`
);
}
}
// Do work
CreatureProperties.update(_id, {
$push: {[joinedPath]: value},
}, {
selector: {type: property.type},
});
// TODO figure out if this method can change deps or not
computeCreature(rootCreature._id);
}
});
export default pushToProperty;

View File

@@ -0,0 +1,34 @@
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 computeCreature from '/imports/api/engine/computeCreature.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});
// Changes dependency tree by restoring children
computeCreature(rootCreature._id);
}
});
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 computeCreature from '/imports/api/engine/computeCreature.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
computeCreature(rootCreature._id);
},
});
export default selectAmmoItem;

View File

@@ -0,0 +1,34 @@
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 computeCreature from '/imports/api/engine/computeCreature.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});
// Changes dependency tree by removing children
computeCreature(rootCreature._id);
}
});
export default softRemoveProperty;

View File

@@ -0,0 +1,54 @@
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 computeCreature from '/imports/api/engine/computeCreature.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},
});
// Updating a property is likely to change dependencies, do a full recompute
// denormalised stats might change, so fetch the creature again
computeCreature(rootCreature._id);
},
});
export default updateCreatureProperty;

View File

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

View File

@@ -1,14 +1,8 @@
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 {assertEditPermission} from '/imports/api/sharing/sharingPermissions.js';
import { assertUserHasPaidBenefits } from '/imports/api/users/patreon/tiers.js';
import '/imports/api/creature/removeCreature.js';
import '/imports/api/creature/restCreature.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
//set up the collection for creatures
let Creatures = new Mongo.Collection('creatures');
@@ -34,6 +28,16 @@ let CreatureSettingsSchema = new SimpleSchema({
type: Boolean,
optional: true,
},
// Show the tree tab
showTreeTab: {
type: Boolean,
optional: true,
},
// Hide the spells tab
hideSpellsTab: {
type: Boolean,
optional: true,
},
// How much each hitDice resets on a long rest
hitDiceResetMultiplier: {
type: Number,
@@ -44,7 +48,7 @@ let CreatureSettingsSchema = new SimpleSchema({
discordWebhook: {
type: String,
optional: true,
max: 200,
max: STORAGE_LIMITS.url,
},
});
@@ -54,22 +58,27 @@ let CreatureSchema = new SimpleSchema({
type: String,
defaultValue: '',
optional: true,
max: STORAGE_LIMITS.name,
},
alignment: {
type: String,
optional: true
optional: true,
max: STORAGE_LIMITS.name,
},
gender: {
type: String,
optional: true
optional: true,
max: STORAGE_LIMITS.name,
},
picture: {
type: String,
optional: true
optional: true,
max: STORAGE_LIMITS.url,
},
avatarPicture: {
type: String,
optional: true,
max: STORAGE_LIMITS.url,
},
// Mechanics
deathSave: {
@@ -91,11 +100,6 @@ let CreatureSchema = new SimpleSchema({
type: SimpleSchema.Integer,
defaultValue: 0,
},
// Sum of all weights of items and containers that are carried
'denormalizedStats.weightCarried': {
type: Number,
defaultValue: 0,
},
// Version of computation engine that was last used to compute this creature
computeVersion: {
type: String,
@@ -116,6 +120,20 @@ let CreatureSchema = new SimpleSchema({
blackbox: true,
defaultValue: {}
},
computeErrors: {
type: Array,
optional: true,
},
'computeErrors.$': {
type: Object,
},
'computeErrors.$.type': {
type: String,
},
'computeErrors.$.details' : {
type: Object,
blackbox: true,
},
// Tabletop
tabletop: {
@@ -140,73 +158,9 @@ CreatureSchema.extend(SharingSchema);
Creatures.attachSchema(CreatureSchema);
const insertCreature = new ValidatedMethod({
name: 'creatures.insertCreature',
validate: null,
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run() {
if (!this.userId) {
throw new Meteor.Error('Creatures.methods.insert.denied',
'You need to be logged in to insert a creature');
}
assertUserHasPaidBenefits(this.userId);
// Create the creature document
let charId = Creatures.insert({
owner: this.userId,
});
this.unblock();
return charId;
},
});
const updateCreature = new ValidatedMethod({
name: 'creatures.update',
validate({_id, path}){
if (!_id) return false;
// Allowed fields
let allowedFields = [
'name',
'alignment',
'gender',
'picture',
'avatarPicture',
'color',
'settings',
];
if (!allowedFields.includes(path[0])){
throw new Meteor.Error('Creatures.methods.update.denied',
'This field can\'t be updated using this method');
}
},
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({_id, path, value}) {
let creature = Creatures.findOne(_id);
assertEditPermission(creature, this.userId);
if (value === undefined || value === null){
Creatures.update(_id, {
$unset: {[path.join('.')]: 1},
});
} else {
Creatures.update(_id, {
$set: {[path.join('.')]: value},
});
}
},
});
import '/imports/api/creature/creatures/methods/index.js';
import '/imports/api/engine/actions/doAction.js';
export default Creatures;
export { CreatureSchema, insertCreature, updateCreature };
export { CreatureSchema };

View File

@@ -1,4 +1,4 @@
import Creatures from '/imports/api/creature/Creatures.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import {
assertEditPermission as editPermission,
assertViewPermission as viewPermission,

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,9 @@
import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import { assertOwnership } from '/imports/api/creature/creaturePermissions.js';
import Creatures from '/imports/api/creature/Creatures.js';
import CreatureProperties from '/imports/api/creature/CreatureProperties.js';
import { assertOwnership } from '/imports/api/creature/creatures/creaturePermissions.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js';
import Experiences from '/imports/api/creature/experience/Experiences.js';
@@ -28,10 +28,14 @@ const removeCreature = new ValidatedMethod({
},
run({charId}) {
assertOwnership(charId, this.userId)
Creatures.remove(charId);
this.unblock();
removeRelatedDocuments(charId);
this.unblock();
removeCreatureWork(charId)
},
});
export function removeCreatureWork(creatureId){
Creatures.remove(creatureId);
removeRelatedDocuments(creatureId);
}
export default removeCreature;

View File

@@ -1,11 +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 { assertEditPermission } from '/imports/api/creature/creaturePermissions.js';
import { recomputeCreatureById } from '/imports/api/creature/computation/recomputeCreature.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import computeCreature from '/imports/api/engine/computeCreature.js';
const restCreature = new ValidatedMethod({
name: 'creature.methods.longRest',
@@ -43,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, {
@@ -70,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'
@@ -105,7 +109,7 @@ const restCreature = new ValidatedMethod({
});
});
}
recomputeCreatureById(creatureId);
computeCreature(creatureId);
},
});

View File

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

View File

@@ -1,74 +0,0 @@
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';
export const recomputeDamageMultipliers = new ValidatedMethod({
name: 'creatures.recomputeDamageMultipliers',
validate: new SimpleSchema({
creatureId: { type: String }
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({creatureId}) {
// Permission
assertEditPermission(creatureId, this.userId);
// Work, call this direcly if you are already in a method that has checked
// for permission to edit a given character
recomputeDamageMultipliersById(creatureId);
},
});
export function recomputeDamageMultipliersById(creatureId){
if (!creatureId) throw 'Creature ID is required';
let props = getActiveProperties({
ancestorId: creatureId,
filter: {type: 'damageMultiplier'},
});
// Count of how many weakness, resistances and immunities each damage type has
let multipliersByName = {};
props.forEach(dm => {
dm.damageTypes.forEach(damageType => {
if (!multipliersByName[damageType]){
multipliersByName[damageType] = {
weaknesses: 0,
resistances: 0,
immunities: 0,
};
}
if (dm.value === 0){
multipliersByName[damageType].immunities++;
} else if (dm.value === 0.5){
multipliersByName[damageType].resistances++;
} else if (dm.value === 2){
multipliersByName[damageType].weaknesses++;
}
});
});
// Make an Object with keys of all the damage types that have a resulting
// immunity, weakness, or resistance
let damageMultipliers = {};
for (let damageType in multipliersByName){
let multiplier = multipliersByName[damageType];
if (multiplier.immunities){
damageMultipliers[damageType] = 0;
} else if (multiplier.resistances && !multiplier.weaknesses){
damageMultipliers[damageType] = 0.5;
} else if (multiplier.weaknesses && !multiplier.resistances){
damageMultipliers[damageType] = 2;
}
}
// Store the Object on the creature document
Creatures.update(creatureId, {$set: {damageMultipliers}});
}

View File

@@ -1,59 +0,0 @@
import CreatureProperties from '/imports/api/creature/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}}, {deactivatedByAncestor: true}],
}, {
$set: {inactive: 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

@@ -1,39 +0,0 @@
import CreatureProperties from '/imports/api/creature/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;
if (slot.quantityExpected === 0){
spaceLeft = null;
} else {
spaceLeft = slot.quantityExpected - totalFilled;
}
if (slot.totalFilled !== totalFilled || slot.spaceLeft !== spaceLeft){
CreatureProperties.update(slot._id, {
$set: {totalFilled, spaceLeft},
}, {
selector: {type: 'propertySlot'}
});
}
});
}

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 { 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/recomputeCreature.js';
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import computeCreature from '/imports/api/engine/computeCreature.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
let Experiences = new Mongo.Collection('experiences');
@@ -12,6 +12,7 @@ let ExperienceSchema = new SimpleSchema({
name: {
type: String,
optional: true,
max: STORAGE_LIMITS.name,
},
// The amount of XP this experience gives
xp: {
@@ -60,7 +61,7 @@ const insertExperienceForCreature = function({experience, creatureId, userId}){
}
experience.creatureId = creatureId;
let id = Experiences.insert(experience);
recomputeCreatureById(creatureId);
computeCreature(creatureId);
return id;
};
@@ -90,11 +91,6 @@ const insertExperience = new ValidatedMethod({
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});
@@ -123,11 +119,6 @@ const removeExperience = new ValidatedMethod({
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
@@ -144,7 +135,7 @@ const removeExperience = new ValidatedMethod({
}
experience.creatureId = creatureId;
let numRemoved = Experiences.remove(experienceId);
recomputeCreatureById(creatureId);
computeCreature(creatureId);
return numRemoved;
},
});
@@ -168,11 +159,6 @@ const recomputeExperiences = new ValidatedMethod({
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;
@@ -189,7 +175,7 @@ const recomputeExperiences = new ValidatedMethod({
'denormalizedStats.xp': xp,
'denormalizedStats.milestoneLevels': milestoneLevels
}});
recomputeCreatureById(creatureId);
computeCreature(creatureId);
},
});

View File

@@ -1,86 +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 = {sort: {order: 1}},
includeUntoggled = false,
includeUnprepared = false,
includeUnequipped = false,
excludeAncestors,
}){
filter = getActivePropertyFilter({
ancestorId,
filter,
includeUntoggled,
includeUnprepared,
includeUnequipped,
excludeAncestors,
});
return CreatureProperties.find(filter, options).fetch();
}
export function getActivePropertyFilter({
ancestorId,
filter = {},
includeUntoggled = false,
includeUnprepared = false,
includeUnequipped = false,
excludeAncestors = [],
}){
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
{applied: false}, // Buffs can be applied
],
};
if (!includeUnequipped){
disabledAncestorsFilter.$or.push({type: 'item', equipped: {$ne: true}});
}
if (!includeUntoggled){
disabledAncestorsFilter.$or.push({toggleResult: false});
}
if (!includeUnprepared){
disabledAncestorsFilter.$or.push({
type: 'spell',
prepared: {$ne: true},
alwaysPrepared: {$ne: true}
});
}
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
if (filter['ancestors.id'] && Meteor.isClient){
console.warn('Filtering on ancestor id is ignored')
}
filter['ancestors.id'] = {
$eq: ancestorId,
$nin: disabledAncestorIds.concat(excludeAncestors),
};
// 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,14 +1,17 @@
import SimpleSchema from 'simpl-schema';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
let ExperienceSchema = new SimpleSchema({
title: {
type: String,
optional: true,
max: STORAGE_LIMITS.name,
},
// Potentially long description of the event
description: {
type: String,
optional: true,
max: STORAGE_LIMITS.description,
},
// The real-world date that it occured
date: {
@@ -24,14 +27,17 @@ let ExperienceSchema = new SimpleSchema({
worldDate: {
type: String,
optional: true,
max: STORAGE_LIMITS.name,
},
// Tags to better find this entry later
tags: {
type: Array,
defaultValue: [],
maxCount: STORAGE_LIMITS.tagCount,
},
'tags.$': {
type: String,
max: STORAGE_LIMITS.tagLength,
},
// ID of the journal this entry belongs to
journalId: {

View File

@@ -1,10 +1,13 @@
import SimpleSchema from 'simpl-schema';
import Creatures from '/imports/api/creature/Creatures.js';
import Creatures from '/imports/api/creature/creatures/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 } from '/imports/parser/parser.js';
import {assertEditPermission} from '/imports/api/creature/creatures/creaturePermissions.js';
import {parse, prettifyParseError} from '/imports/parser/parser.js';
import resolve, { toString } from '/imports/parser/resolve.js';
const PER_CREATURE_LOG_LIMIT = 100;
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
if (Meteor.isServer){
var sendWebhookAsCreature = require('/imports/server/discord/sendWebhook.js').sendWebhookAsCreature;
@@ -13,13 +16,13 @@ if (Meteor.isServer){
let CreatureLogs = new Mongo.Collection('creatureLogs');
let CreatureLogSchema = new SimpleSchema({
text: {
type: String,
},
type: {
type: String,
allowedValues: ['roll', 'change', 'damage', 'info'],
defaultValue: 'info',
content: {
type: Array,
defaultValue: [],
maxCount: STORAGE_LIMITS.logContentCount,
},
'content.$': {
type: LogContentSchema,
},
// The real-world date that it occured, usually sorted by date
date: {
@@ -37,6 +40,11 @@ let CreatureLogSchema = new SimpleSchema({
regEx: SimpleSchema.RegEx.Id,
index: 1,
},
creatureName: {
type: String,
optional: true,
max: STORAGE_LIMITS.name,
},
});
CreatureLogs.attachSchema(CreatureLogSchema);
@@ -49,6 +57,7 @@ function removeOldLogs(creatureId){
sort: {date: -1},
skip: PER_CREATURE_LOG_LIMIT,
});
if (!firstExpiredLog) return;
// Remove all logs older than the one over the limit
CreatureLogs.remove({
creatureId,
@@ -56,24 +65,36 @@ function removeOldLogs(creatureId){
});
}
function logToMessageData(log){
let embed = {
fields: [],
};
log.content.forEach(field => {
if (!field.name) field.name = '\u200b';
if (!field.value) field.value = '\u200b';
embed.fields.push(field);
});
return { embeds: [embed] };
}
function logWebhook({log, creature}){
if (Meteor.isServer){
sendWebhookAsCreature({
creature,
content: log.text,
data: logToMessageData(log),
});
}
}
const insertCreatureLog = new ValidatedMethod({
name: 'creatureLogs.methods.insertCreatureLog',
name: 'creatureLogs.methods.insert',
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
validate: new SimpleSchema({
log: CreatureLogSchema.omit('type', 'date'),
log: CreatureLogSchema.omit('date'),
}).validator(),
run({log}){
const creatureId = log.creatureId;
@@ -87,21 +108,28 @@ const insertCreatureLog = new ValidatedMethod({
}});
assertEditPermission(creature, this.userId);
// 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){
this.unblock();
removeOldLogs(creatureId);
logWebhook({log, creature});
}
let id = insertCreatureLogWork({log, creature, method: this})
return id;
},
});
export function insertCreatureLogWork({log, creature, method}){
// Build the new log
if (typeof log === 'string'){
log = {content: [{value: log}]};
}
if (!log.content?.length) return;
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;
@@ -135,39 +163,48 @@ const logRoll = new ValidatedMethod({
avatarPicture: 1,
}});
assertEditPermission(creature, this.userId);
let parsedResult = parse(roll);
let logText;
if (parsedResult === null) {
logText = 'Unexpected end of input';
}
else try {
logText = [];
let rollContext = new CompilationContext();
let compiled = parsedResult.compile(creature.variables, rollContext);
let compiledString = compiled.toString();
if (!equalIgnoringWhitespace(compiledString, roll)) logText.push(roll);
logText.push(compiledString);
let rolled = compiled.roll(creature.variables, rollContext);
let rolledString = rolled.toString();
if (rolledString !== compiledString) logText.push(rolled.toString());
let result = rolled.reduce(creature.variables, rollContext);
let resultString = result.toString();
if (resultString !== rolledString) logText.push(resultString);
logText = logText.join('\n\n');
let logContent = []
let parsedResult = undefined;
try {
parsedResult = parse(roll);
} catch (e){
logText = 'Calculation error';
let error = prettifyParseError(e);
logContent.push({name: 'Parse Error', value: error});
}
if (parsedResult) try {
let {
result: compiled,
context
} = resolve('compile', parsedResult, creature.variables);
const compiledString = toString(compiled);
if (!equalIgnoringWhitespace(compiledString, roll)) logContent.push({
value: roll
});
logContent.push({
value: compiledString
});
let {result: rolled} = resolve('roll', compiled, creature.variables, context);
let rolledString = toString(rolled);
if (rolledString !== compiledString) logContent.push({
value: rolledString
});
let {result} = resolve('reduce', rolled, creature.variables, context);
let resultString = toString(result);
if (resultString !== rolledString) logContent.push({
value: resultString
});
} catch (e){
console.error(e);
logContent = [{name: 'Calculation error'}];
}
const log = {
text: logText,
content: logContent,
creatureId,
date: new Date(),
};
let id = CreatureLogs.insert(log);
if (Meteor.isServer){
this.unblock();
removeOldLogs(creatureId);
logWebhook({log, creature});
}
let id = insertCreatureLogWork({log, creature, method: this});
return id;
},
});

View File

@@ -0,0 +1,51 @@
import SimpleSchema from 'simpl-schema';
import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js';
import RollDetailsSchema from '/imports/api/properties/subSchemas/RollDetailsSchema.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
let LogContentSchema = new SimpleSchema({
// The name of the field, included in discord webhook message
name: {
type: String,
optional: true,
max: STORAGE_LIMITS.name,
},
// The details of the field, included in discord webhook message
// Markdown support
value: {
type: String,
optional: true,
max: STORAGE_LIMITS.summary,
},
// Inline with other content fields
inline: {
type: Boolean,
optional: true,
},
context: {
type: Object,
optional: true,
},
'context.errors':{
type: Array,
defaultValue: [],
maxCount: STORAGE_LIMITS.errorCount,
},
'context.errors.$': {
type: ErrorSchema,
},
'context.rolls': {
type: Array,
defaultValue: [],
maxCount: STORAGE_LIMITS.rollCount,
},
'context.rolls.$': {
type: RollDetailsSchema,
},
'context.doubleRolls': {
type: Boolean,
optional: true,
},
});
export default LogContentSchema;

View File

@@ -2,7 +2,7 @@ import {
assertEditPermission,
assertViewPermission,
assertOwnership,
} from '/imports/api/creature/creaturePermissions.js';
} from '/imports/api/creature/creatures/creaturePermissions.js';
// Checks if the method has permission to run on the document. If the document
// has a charId, that creature is checked, otherwise if it has an _id and the

View File

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

View File

@@ -0,0 +1,27 @@
import action from './applyPropertyByType/applyAction.js';
import adjustment from './applyPropertyByType/applyAdjustment.js';
import branch from './applyPropertyByType/applyBranch.js';
import buff from './applyPropertyByType/applyBuff.js';
import damage from './applyPropertyByType/applyDamage.js';
import note from './applyPropertyByType/applyNote.js';
import roll from './applyPropertyByType/applyRoll.js';
import savingThrow from './applyPropertyByType/applySavingThrow.js';
import toggle from './applyPropertyByType/applyToggle.js';
const applyPropertyByType = {
action,
adjustment,
branch,
buff,
damage,
note,
roll,
savingThrow,
spell: action,
toggle,
};
export default function applyProperty(node, opts, ...rest){
opts.scope[`#${node.node.type}`] = node.node;
return applyPropertyByType[node.node.type]?.(node, opts, ...rest);
}

View File

@@ -0,0 +1,294 @@
import recalculateInlineCalculations from './shared/recalculateInlineCalculations.js';
import recalculateCalculation from './shared/recalculateCalculation.js';
import rollDice from '/imports/parser/rollDice.js';
import applyProperty from '../applyProperty.js';
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';
import numberToSignedString from '/imports/ui/utility/numberToSignedString.js';
export default function applyAction(node, {creature, targets, scope, log}){
const prop = node.node;
if (prop.target === 'self') targets = [creature];
// Log the name and description
let content = { name: prop.name };
if (prop.description?.text){
recalculateInlineCalculations(prop.description, scope, log);
content.value = prop.description.value;
}
if (content.name || content.value){
log.content.push(content);
}
// Spend the resources
const failed = spendResources({prop, log, scope});
if (failed) return;
const attack = prop.attackRoll || prop.attackRollBonus;
// Attack if there is an attack roll
if (attack && attack.calculation){
if (targets.length){
targets.forEach(target => {
applyAttackToTarget({attack, target, scope, log});
// Apply the children, but only to the current target
applyChildren(node, {targets: [target], scope, log});
});
} else {
applyAttackWithoutTarget({attack, scope, log});
applyChildren(node, {creature, targets, scope, log});
}
} else {
applyChildren(node, {creature, targets, scope, log});
}
}
function applyAttackWithoutTarget({attack, scope, log}){
delete scope['$attackHit'];
delete scope['$attackMiss'];
delete scope['$criticalHit'];
delete scope['$criticalMiss'];
delete scope['$attackRoll'];
recalculateCalculation(attack, scope, log);
let {
resultPrefix,
result,
criticalHit,
criticalMiss,
} = rollAttack(attack, scope);
let name = criticalHit ? 'Critical Hit!' : criticalMiss ? 'Critical Miss!' : 'To Hit';
if (scope['$attackAdvantage'] === 1){
name += ' (Advantage)';
} else if(scope['$attackAdvantage'] === -1){
name += ' (Disadvantage)';
}
log.content.push({
name,
value: `${resultPrefix}\n**${result}**`,
inline: true,
});
}
function applyAttackToTarget({attack, target, scope, log}){
delete scope['$attackHit'];
delete scope['$attackMiss'];
delete scope['$criticalHit'];
delete scope['$criticalMiss'];
delete scope['$attackDiceRoll'];
delete scope['$attackRoll'];
recalculateCalculation(attack, scope, log);
let {
resultPrefix,
result,
criticalHit,
criticalMiss,
} = rollAttack(attack, scope);
if (target.variables.armor){
const armor = target.variables.armor.value;
let name = criticalHit ? 'Critical Hit!' :
criticalMiss ? 'Critical Miss!' :
result > armor ? 'Hit!' : 'Miss!';
if (scope['$attackAdvantage'] === 1){
name += ' (Advantage)';
} else if(scope['$attackAdvantage'] === -1){
name += ' (Disadvantage)';
}
log.content.push({
name,
value: `${resultPrefix}\n**${result}**`,
inline: true,
});
if ((result > armor) || (criticalHit)){
scope['$attackHit'] = true;
} else {
scope['$attackMiss'] = true;
}
} else {
log.content.push({
name: 'Error',
value:'Target has no `armor`',
});
log.content.push({
name: criticalHit ? 'Critical Hit!' : criticalMiss ? 'Critical Miss!' : 'To Hit',
value: `${resultPrefix}\n**${result}**`,
inline: true,
});
}
}
function rollAttack(attack, scope){
const rollModifierText = numberToSignedString(attack.value, true);
let value, resultPrefix;
if (attack.advantage === 1 || scope['$attackAdvantage']){
const [a, b] = rollDice(2, 20);
if (a >= b) {
value = a;
resultPrefix = `1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText}`;
} else {
value = b;
resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`;
}
} else if (attack.advantage === -1 || scope['$attackDisadvantage']){
const [a, b] = rollDice(2, 20);
if (a <= b) {
value = a;
resultPrefix = `1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText}`;
} else {
value = b;
resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`;
}
} else {
value = rollDice(1, 20)[0];
resultPrefix = `1d20 [${value}] ${rollModifierText}`
}
scope['$attackRoll'] = {value};
const result = value + attack.value;
const {criticalHit, criticalMiss} = applyCrits(value, scope);
return {resultPrefix, result, value, criticalHit, criticalMiss};
}
function applyCrits(value, scope){
let criticalHitTarget = scope.criticalHitTarget?.value || 20;
let criticalHit = value >= criticalHitTarget;
let criticalMiss;
if (criticalHit){
scope['$criticalHit'] = {value: true};
scope['$attackHit'] = {value: true};
} else {
criticalMiss = value === 1;
if (criticalMiss){
scope['$criticalMiss'] = 1;
scope['$attackMiss'] = {value: true};
} else {
// Untargeted attacks hit by default
scope['$attackHit'] = {value: true}
}
}
return {criticalHit, criticalMiss};
}
function applyChildren(node, args){
node.children.forEach(child => applyProperty(child, args));
}
function spendResources({prop, log, scope}){
// Check Uses
if (prop.usesLeft <= 0){
log.content.push({
name: 'Error',
value: `${prop.name || 'action'} does not have enough uses left`,
});
return true;
}
// Resources
if (prop.insufficientResources){
log.content.push({
name: 'Error',
value: 'This creature doesn\'t have sufficient resources to perform this action',
});
return true;
}
// Items
let itemQuantityAdjustments = [];
let spendLog = [];
let gainLog = [];
try {
prop.resources.itemsConsumed.forEach(itemConsumed => {
recalculateCalculation(itemConsumed.quantity, scope, log);
if (!itemConsumed.itemId){
throw 'No ammo was selected for this prop';
}
let item = CreatureProperties.findOne(itemConsumed.itemId);
if (!item || item.ancestors[0].id !== prop.ancestors[0].id){
throw 'The prop\'s ammo was not found on the creature';
}
if (!item.equipped){
throw 'The selected ammo is not equipped';
}
if (
!itemConsumed.quantity.value ||
!isFinite(itemConsumed.quantity.value)
) return;
itemQuantityAdjustments.push({
property: item,
operation: 'increment',
value: itemConsumed.quantity.value,
});
let logName = item.name;
if (itemConsumed.quantity.value > 1 || itemConsumed.quantity.value < -1){
logName = item.plural || logName;
}
if (itemConsumed.quantity.value > 0){
spendLog.push(logName + ': ' + itemConsumed.quantity.value);
} else if (itemConsumed.quantity.value < 0){
gainLog.push(logName + ': ' + -itemConsumed.quantity.value);
}
});
} catch (e){
log.content.push({
name: 'Error',
value: e,
});
return true;
}
// 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.usesLeft){
CreatureProperties.update(prop._id, {
$inc: {usesUsed: 1}
}, {
selector: prop
});
log.content.push({
name: 'Uses left',
value: prop.usesLeft - 1,
inline: true,
});
}
// Damage stats
prop.resources.attributesConsumed.forEach(attConsumed => {
recalculateCalculation(attConsumed.quantity, scope, log);
if (!attConsumed.quantity?.value) return;
let stat = scope[attConsumed.variableName];
if (!stat){
spendLog.push(stat.name + ': ' + ' not found');
return;
}
damagePropertyWork({
property: stat,
operation: 'increment',
value: attConsumed.quantity.value,
});
if (attConsumed.quantity.value > 0){
spendLog.push(stat.name + ': ' + attConsumed.quantity.value);
} else if (attConsumed.quantity.value < 0){
gainLog.push(stat.name + ': ' + -attConsumed.quantity.value);
}
});
// Log all the spending
if (gainLog.length) log.content.push({
name: 'Gained',
value: gainLog.join('\n'),
inline: true,
});
if (spendLog.length) log.content.push({
name: 'Spent',
value: spendLog.join('\n'),
inline: true,
});
}

View File

@@ -0,0 +1,59 @@
import applyProperty from '../applyProperty.js';
import recalculateCalculation from './shared/recalculateCalculation.js';
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
export default function applyAdjustment(node, {
creature, targets, scope, log
}){
const prop = node.node;
const damageTargets = prop.target === 'self' ? [creature] : targets;
if (!prop.amount) {
return applyChildren(node, {creature, targets, scope, log});
}
// Evaluate the amount
recalculateCalculation(prop.amount, scope, log);
const value = +prop.amount.value;
if (!isFinite(value)) {
return applyChildren(node, {creature, targets, scope, log});
}
if (damageTargets?.length) {
damageTargets.forEach(target => {
let stat = target.variables[prop.stat];
if (!stat?.type) {
log.content.push({
name: 'Error',
value: `Could not apply attribute damage, creature does not have \`${prop.stat}\` set`
});
return applyChildren(node, {creature, targets, scope, log});
}
damagePropertyWork({
property: stat,
operation: prop.operation,
value: value,
});
log.content.push({
name: 'Attribute damage',
value: `${prop.stat}${prop.operation === 'set' ? ' set to' : ''}` +
` ${value}`,
inline: true,
});
});
} else {
log.content.push({
name: 'Attribute damage',
value: `${prop.stat}${prop.operation === 'set' ? ' set to' : ''}` +
` ${value}`,
inline: true,
});
}
return applyChildren(node, {creature, targets, scope, log});
}
function applyChildren(node, args){
node.children.forEach(child => applyProperty(child, args));
}

View File

@@ -0,0 +1,51 @@
import applyProperty from '../applyProperty.js';
import recalculateCalculation from './shared/recalculateCalculation.js';
import rollDice from '/imports/parser/rollDice.js';
export default function applyBranch(node, {
creature, targets, scope, log
}){
const applyChildren = function(){
node.children.forEach(child => applyProperty(child, {
creature, targets, scope, log
}));
};
const prop = node.node;
switch(prop.branchType){
case 'if':
recalculateCalculation(prop.condition, scope, log);
if (prop.condition?.value) applyChildren();
break;
case 'hit':
if (scope['$attackHit']?.value) applyChildren();
break;
case 'miss':
if (scope['$attackMiss']?.value) applyChildren();
break;
case 'failedSave':
if (scope['$saveFailed']?.value) applyChildren();
break;
case 'successfulSave':
if (scope['$saveSucceeded']?.value) applyChildren();
break;
case 'random':
if (node.children.length){
let index = rollDice(1, node.children.length)[0] - 1;
applyProperty(node.children[index], {
creature, targets, scope, log
});
}
break;
case 'eachTarget':
if (targets.length){
targets.forEach(target => {
node.children.forEach(child => applyProperty(child, {
creature, targets: [target], scope, log
}));
});
} else {
applyChildren();
}
break;
}
}

View File

@@ -0,0 +1,101 @@
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 computedSchemas from '/imports/api/properties/computedPropertySchemasIndex.js';
import applyFnToKey from '/imports/api/engine/computation/utility/applyFnToKey.js';
import { get } from 'lodash';
import resolve, { map, toString } from '/imports/parser/resolve.js';
import symbol from '/imports/parser/parseTree/symbol.js';
import logErrors from './shared/logErrors.js';
import cyrb53 from '/imports/api/engine/computation/utility/cyrb53.js';
export default function applyBuff(node, {creature, targets, scope, log}){
const prop = node.node;
let buffTargets = prop.target === 'self' ? [creature] : targets;
// Then copy the decendants of the buff to the targets
let propList = [prop];
function addChildrenToPropList(children){
children.forEach(child => {
propList.push(child.node);
addChildrenToPropList(child.children);
});
}
addChildrenToPropList(node.children);
crystalizeVariables({propList, scope, log});
let oldParent = {
id: prop.parent.id,
collection: prop.parent.collection,
};
buffTargets.forEach(target => {
copyNodeListToTarget(propList, target, oldParent);
});
// Don't apply the children of the buff, they get copied to the target instead
}
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);
}
/**
* Replaces all variables with their resolved values
* except variables of the form `$target.thing.total` become `thing.total`
*/
function crystalizeVariables({propList, scope, log}){
propList.forEach(prop => {
computedSchemas[prop.type].computedFields().forEach( calcKey => {
applyFnToKey(prop, calcKey, (prop, key) => {
const calcObj = get(prop, key);
if (!calcObj?.parseNode) return;
calcObj.parseNode = map(calcObj.parseNode, node => {
// Skip nodes that aren't symbols or accessors
if (
node.parseType !== 'accessor' && node.parseType !== 'symbol'
) return node;
// Handle variables
if (node.name === '$target'){
// strip $target
if (node.parseType === 'accessor'){
node.name = node.path.shift();
if (!node.path.length){
return symbol.create({name: node.name})
}
} else {
// Can't strip symbols
log.content.push({
name: 'Error',
value: 'Variable `$target` should not be used without a property: $target.property',
});
}
return node;
} else {
// Resolve all other variables
const {result, context} = resolve('reduce', node, scope);
logErrors(context.errors, log);
return result;
}
});
calcObj.calculation = toString(calcObj.parseNode);
calcObj.hash = cyrb53(calcObj.calculation);
});
});
});
}

View File

@@ -0,0 +1,116 @@
import applyProperty from '../applyProperty.js';
import { dealDamageWork } from '/imports/api/creature/creatureProperties/methods/dealDamage.js';
import {insertCreatureLog} from '/imports/api/creature/log/CreatureLogs.js';
import resolve, { Context, toString } from '/imports/parser/resolve.js';
import logErrors from './shared/logErrors.js';
import applyEffectsToCalculationParseNode from '/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js';
export default function applyDamage(node, {
creature, targets, scope, log
}){
const applyChildren = function(){
node.children.forEach(child => applyProperty(child, {
creature, targets, scope, log
}));
};
const prop = node.node;
// Skip if there is no parse node to work with
if (!prop.amount.parseNode) return;
// Choose target
let damageTargets = prop.target === 'self' ? [creature] : targets;
// Determine if the hit is critical
let criticalHit = scope['$criticalHit']?.value &&
prop.damageType !== 'healing' // Can't critically heal
;
// Double the damage rolls if the hit is critical
let context = new Context({
options: {doubleRolls: criticalHit},
});
// Gather all the lines we need to log into an array
const logValue = [];
const logName = prop.damageType === 'healing' ? 'Healing' : 'Damage';
// Compile the dice roll and store that string first
// const {result: compiled} = resolve('compiled', prop.amount.parseNode, scope, context);
// logValue.push(toString(compiled));
// logErrors(context.errors, log);
// roll the dice only and store that string
applyEffectsToCalculationParseNode(prop.amount, log);
const {result: rolled} = resolve('roll', prop.amount.parseNode, scope, context);
logValue.push(toString(rolled));
logErrors(context.errors, log);
// Reset the errors so we don't log the same errors twice
context.errors = [];
// Resolve the roll to a final value
const {result: reduced} = resolve('reduce', rolled, scope, context);
logErrors(context.errors, log);
// Store the result
if (reduced.parseType === 'constant'){
prop.amount.value = reduced.value;
} else if (reduced.parseType === 'error'){
prop.amount.value = null;
} else {
prop.amount.value = toString(reduced);
}
const damage = +reduced.value;
// If we didn't end up with a constant of finite amount, give up
if (reduced?.parseType !== 'constant' && !isFinite(reduced.value)){
return applyChildren();
}
// Memoise the damage suffix for the log
let suffix = (criticalHit ? ' critical ' : ' ') +
prop.damageType +
(prop.damageType !== 'healing' ? ' damage ': '');
if (damageTargets && damageTargets.length) {
// Iterate through all the targets
damageTargets.forEach(target => {
// Deal the damage to the target
let damageDealt = dealDamageWork({
creature: target,
damageType: prop.damageType,
amount: damage,
});
// Log the damage done
if (target._id === creature._id){
// Target is same as self, log damage as such
logValue.push(`**${damageDealt}** ${suffix} to self`);
} else {
logValue.push(`Dealt **${damageDealt}** ${suffix} ${target.name && ' to '}${target.name}`);
// Log the damage received on that creature's log as well
insertCreatureLog.call({
log: {
creatureId: target._id,
content: [{
name,
value: `Recieved **${damageDealt}** ${suffix}`,
}],
}
});
}
});
} else {
// There are no targets, just log the result
logValue.push(`**${damage}** ${suffix}`);
}
log.content.push({
name: logName,
value: logValue.join('\n'),
inline: true,
});
return applyChildren();
}

View File

@@ -0,0 +1,25 @@
import recalculateInlineCalculations from './shared/recalculateInlineCalculations.js';
import applyProperty from '../applyProperty.js';
export default function applyNote(node, {creature, targets, scope, log}){
const prop = node.node;
// Log Name, summary
let content = { name: prop.name };
if (prop.summary?.text){
recalculateInlineCalculations(prop.summary, scope, log);
content.value = prop.summary.value;
}
if (content.name || content.value){
log.content.push(content);
}
// Log description
if (prop.description?.text){
recalculateInlineCalculations(prop.description, scope, log);
log.content.push({value: prop.description.value});
}
// Apply children
node.children.forEach(child => applyProperty(child, {
creature, targets, scope, log
}));
}

View File

@@ -0,0 +1,22 @@
import applyProperty from '../applyProperty.js';
import recalculateCalculation from './shared/recalculateCalculation.js';
export default function applyRoll(node, {creature, targets, scope, log}){
const prop = node.node;
if (prop.roll?.calculation){
recalculateCalculation(prop.roll, scope, log);
if (isFinite(prop.roll.value)){
scope[prop.variableName] = prop.roll.value;
}
log.content.push({
name: prop.name,
value: prop.variableName + ' = ' + prop.roll.calculation + ' = ' + prop.roll.value,
inline: true,
});
}
return node.children.forEach(child => applyProperty(child, {
creature, targets, scope, log
}));
}

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