Compare commits

...

241 Commits

Author SHA1 Message Date
Stefan Zermatten
4c617332f2 Bumped version 2022-12-05 11:17:47 +02:00
Stefan Zermatten
03b623d898 Merge branch 'develop' 2022-12-05 11:17:13 +02:00
Stefan Zermatten
a0744e5af3 Improved printing on some browsers 2022-12-05 11:14:22 +02:00
Stefan Zermatten
b92d2ecf05 fixes #307 Github link on new home page dead 2022-12-05 10:27:51 +02:00
Stefan Zermatten
aabcdac242 Re-added column layout hacks to stop chrome crashing 2022-12-05 10:18:05 +02:00
Stefan Zermatten
9fbeb0c06f Fixed type in character delete dialog 2022-12-05 10:07:32 +02:00
Stefan Zermatten
c058f3eab4 Stopped spell list cards animating on prepare 2022-12-03 12:17:20 +02:00
Stefan Zermatten
0a2d4cf97b Fixed hiding rest btn w/out events breaks statsTab 2022-12-03 12:16:26 +02:00
Stefan Zermatten
7151e1bb4e Merge remote-tracking branch 'origin/master' into version-2 2022-12-02 14:50:53 +02:00
Stefan Zermatten
b088a2d433 Bumped version 2022-12-02 09:44:44 +02:00
Stefan Zermatten
8aa5ee81d5 Merge branch 'version-2-dev' into version-2 2022-12-02 09:43:10 +02:00
Stefan Zermatten
ef26153bb2 Improved [redacted], added routes and navigation 2022-12-01 13:28:33 +02:00
Stefan Zermatten
77597e8056 Updated static pages, home, about, sign-in 2022-11-30 15:37:28 +02:00
Stefan Zermatten
ee1b876259 Bumped version 2022-11-29 14:53:23 +02:00
Stefan Zermatten
12fbca5c78 Merge branch 'version-2-dev' into version-2 2022-11-29 14:53:01 +02:00
Stefan Zermatten
da6fb55ca0 Fixed automated tab navs. going to the wrong tab 2022-11-29 14:52:22 +02:00
Stefan Zermatten
8551e318c2 Demoted features tab back in tab order 2022-11-29 14:35:27 +02:00
Stefan Zermatten
f175cffab8 Bumped version 2022-11-29 14:27:53 +02:00
Stefan Zermatten
2bca582af6 Merge branch 'version-2-dev' into version-2 2022-11-29 14:26:59 +02:00
Stefan Zermatten
5815c7ca34 Padded character list to reveal add folder button
It was hiding behind FAB
2022-11-29 14:10:28 +02:00
Stefan Zermatten
c237162475 Fixed sidebar party closing on route change 2022-11-29 14:05:24 +02:00
Stefan Zermatten
e87772c2a3 Fixed folder groupStats behaviour when !groupStats 2022-11-29 12:06:27 +02:00
Stefan Zermatten
704314a7eb Udpated npm packages 2022-11-29 11:48:28 +02:00
Stefan Zermatten
7ffd0bf61d Fixed menus in dialogs in firefox
Also improved look of scrollbars incl. dark mode
2022-11-29 11:48:20 +02:00
Stefan Zermatten
69b3ba781d Disabled tabletop routing for now 2022-11-28 23:41:10 +02:00
Stefan Zermatten
bf8eb52a96 bumped number of writers limit from 20 to 32 2022-11-28 16:54:21 +02:00
Stefan Zermatten
684d672028 Removed column layout hacks
Fixes drag fallbacks not being in front of cards
Might fix flashy shit on ios
2022-11-28 15:40:47 +02:00
Stefan Zermatten
fb98544ae1 Fixed drag and drop on Firefox 2022-11-28 15:39:47 +02:00
Stefan Zermatten
ec8b9c209c fixed rests on actions with undefined usesUsed 2022-11-28 14:50:41 +02:00
Stefan Zermatten
bee90a7a80 Fixed rests on attributes with undefined damage 2022-11-28 14:49:38 +02:00
Stefan Zermatten
5ad0de9eb7 Bumped version 2022-11-25 16:36:44 +02:00
Stefan Zermatten
0b377fcb71 Attributes show their children in stats cards 2022-11-25 16:27:18 +02:00
Stefan Zermatten
1f26fbf00e Iterated on stat grouping cards
adde slots, spell lists, children of slot fillers
hid properties in most places
spell slots in correct order
2022-11-25 13:25:38 +02:00
Stefan Zermatten
bb1e9624ad Fixed hit dice missing on stats tab 2022-11-25 12:14:47 +02:00
Stefan Zermatten
bda446858e Fixed spell tab btn not hiding correctly on mobile 2022-11-24 15:04:35 +02:00
Stefan Zermatten
e19e91f7e0 Fixed broken $attackRoll always returning 0 2022-11-24 14:51:05 +02:00
Stefan Zermatten
bac9fc98dd Fixed order of stats tab, unhid sneaky folders 2022-11-24 14:48:58 +02:00
Stefan Zermatten
420663c149 Hotfix 2 crashy boogaloo 2022-11-24 14:10:22 +02:00
Stefan Zermatten
23d44e54e3 Hotfixed S3 not loading 2022-11-24 13:39:28 +02:00
Stefan Zermatten
881496e9c1 Removed all md docs 2022-11-24 13:09:03 +02:00
Stefan Zermatten
002c767d1a Bumped version 2022-11-24 12:57:53 +02:00
Stefan Zermatten
9aaf31d5cf Merge branch 'version-2-dev' into version-2 2022-11-24 12:56:30 +02:00
Stefan Zermatten
d05cd2fa19 Iterated on docs, fixed links 2022-11-24 12:29:00 +02:00
Stefan Zermatten
f13774df11 Removed old mardown docs 2022-11-24 11:56:20 +02:00
Stefan Zermatten
cc78ba948e Merge branch 'version-2-dev' of https://github.com/ThaumRystra/DiceCloud into version-2-dev 2022-11-24 11:45:17 +02:00
Stefan Zermatten
c6bfb84bb0 Updated default docs 2022-11-24 11:45:13 +02:00
Stefan Zermatten
7e49100d14 Fixed dragging on mobile 2022-11-23 15:52:45 +02:00
Stefan Zermatten
c3ac49a8c4 Fixed default doc importing 2022-11-23 15:51:09 +02:00
Stefan Zermatten
fd9d525ba9 fixed: Show only the last event with a var name 2022-11-22 21:59:44 +02:00
Stefan Zermatten
d947b62ba4 Fixed events not being selectable for reset in libraries 2022-11-22 21:29:08 +02:00
Stefan Zermatten
046509224e Fixed library page janky loading 2022-11-22 21:21:53 +02:00
Stefan Zermatten
63bcf023ee Big UI overhaul
Moved tabs to bottom nav on mobile
Added actions tab
Conditional benefits on skills/saves show on stats tab
2022-11-22 20:51:21 +02:00
Stefan Zermatten
9741d1d56c Got in-dialog help working with new docs 2022-11-22 03:01:59 +02:00
Stefan Zermatten
0f12c98408 Merge branch 'version-2-dev' of https://github.com/ThaumRystra/DiceCloud into version-2-dev 2022-11-22 02:34:38 +02:00
Stefan Zermatten
254390e1c1 Added default doc seeding for new servers 2022-11-22 02:34:34 +02:00
Stefan Zermatten
ad2f43712d Merge pull request #300 from Jonpot/patch-1
Fix skill.md typo
2022-11-22 02:33:13 +02:00
Stefan Zermatten
55f8dac0db Merge pull request #302 from Jonpot/patch-3
Fix Action scope variables
2022-11-22 02:32:17 +02:00
Jonpot
9f8c3f0f3d Update applyAction.js
As per the docs, $attackDiceRoll should be the value of the d20 before modifiers, and $attackRoll should the the total value, after modifiers. Pre-patch, the former variable is never defined, and the latter variable has the wrong value.
2022-11-21 16:20:48 -08:00
Jonpot
56bd41f435 Fix skill.md typo 2022-11-21 15:49:01 -08:00
Stefan Zermatten
063d4288ef Point buys can now guess cost while dragging slide 2022-11-22 01:45:25 +02:00
Stefan Zermatten
a3355dd988 stat grouping is now everywhere
This lead to a complete refactor of the stats page
Some things might break
2022-11-22 00:56:10 +02:00
Stefan Zermatten
d2649fd66e Overhauled how documentation works 2022-11-21 18:22:49 +02:00
Stefan Zermatten
e619734ee1 Fixed checks not having access to #skill 2022-11-21 13:16:54 +02:00
Stefan Zermatten
5108b32624 Added line breaks in parser strings
Actual line breaks and \n both work
2022-11-20 00:19:41 +02:00
Stefan Zermatten
a9b389023e added todo 2022-11-19 23:44:02 +02:00
Stefan Zermatten
e06d2befc4 Fixed damage multipliers not using implicit tags 2022-11-19 23:24:11 +02:00
Stefan Zermatten
04c9c4cfc2 Fixed bug where updates on sliders weren't debounced 2022-11-19 22:39:35 +02:00
Stefan Zermatten
cc7dc257fb Fixed attributes not showing base value in lib 2022-11-19 19:35:46 +02:00
Stefan Zermatten
f3deadb3f1 Fixed buff descriptions not calced before logged 2022-11-19 19:25:51 +02:00
Stefan Zermatten
dcfb380e57 Fixed saving throw tag targeted effects
They will now roll dice effects before saving
2022-11-19 19:15:53 +02:00
Stefan Zermatten
a568cdfb1e Fixed NaNing of skills that have rolled effects 2022-11-19 18:57:26 +02:00
Stefan Zermatten
cea63e6a8e Moved dev file storage to inside meteor app folder
Prevented verbose logging of file storage
2022-11-19 18:34:52 +02:00
Stefan Zermatten
b6b0cfbb9b Fixed triggers on attribute reset on rest 2022-11-19 18:12:51 +02:00
Stefan Zermatten
428aeef635 Removed HMR test text 2022-11-19 17:56:36 +02:00
Stefan Zermatten
e3644eb9e8 Moved UI to client folder to fix HMR 2022-11-19 17:51:50 +02:00
Stefan Zermatten
060b5f93ca Reduced bundle size and updates packages 2022-11-19 17:19:07 +02:00
Stefan Zermatten
0f3a96da17 Spell list ability modifier can take non-abilities
defaults to .value if .modifier is undefined
is now undefined for no .modifier or .value
2022-11-18 14:21:22 +02:00
Stefan Zermatten
c55d572134 Bumped version 2022-11-16 23:52:08 +02:00
Stefan Zermatten
0a2b60990e Merge branch 'version-2-dev' into version-2 2022-11-16 23:51:37 +02:00
Stefan Zermatten
a437ff5aef Fixed log of recovering HD not having names 2022-11-09 15:02:41 +02:00
Stefan Zermatten
3d31d62860 Completed folder stat grouping UI 2022-11-09 14:58:52 +02:00
Stefan Zermatten
8377231254 Began work on stat grouping by folder 2022-11-09 00:00:54 +02:00
Stefan Zermatten
1ec29365cb Added custom sheet events
Made rest buttons optional
2022-11-08 23:01:09 +02:00
Stefan Zermatten
60b21c1901 Fixed bugs with effects
they were not providing advantage or conditional benefits
2022-11-08 18:29:27 +02:00
Stefan Zermatten
03f87b0afa Added spellcasting ability to spell lists 2022-11-08 18:09:00 +02:00
Stefan Zermatten
48291d2c8f Added help to property creation forms 2022-11-08 17:17:26 +02:00
Stefan Zermatten
1cedf55fbf Merge branch 'version-2-print' into version-2-dev 2022-11-08 17:00:52 +02:00
Stefan Zermatten
bed4d4b162 Fixed logo not showing 2022-11-08 16:59:52 +02:00
Stefan Zermatten
a1d992ec8d Fixed blank multipliers box 2022-11-07 16:38:54 +02:00
Stefan Zermatten
008ef62517 Printing implemented, needs print button on sheet 2022-11-07 16:18:35 +02:00
Stefan Zermatten
c436309ba8 Work on column-based print layout 2022-11-07 00:07:42 +02:00
Stefan Zermatten
0bfdb73b47 Added a quick exit for migrations if the database is new 2022-11-04 12:34:37 +02:00
Stefan Zermatten
a462cc5ca2 Updated packages 2022-11-04 12:34:22 +02:00
Stefan Zermatten
5d57a74667 Merge branch 'version-2-dev' into version-2 2022-11-03 20:52:26 +02:00
Stefan Zermatten
21b0029df7 bumped version 2022-11-03 20:51:58 +02:00
Stefan Zermatten
c0ccafa787 Added overflow stops to health bars 2022-11-03 20:50:10 +02:00
Stefan Zermatten
d63ad9ea8f Added hide when total/value zero to attributes 2022-11-03 20:39:02 +02:00
Stefan Zermatten
8f56a60fb1 Added copy-to and related sharing permissions 2022-11-03 20:18:59 +02:00
Stefan Zermatten
358ae46627 Began work on copy to for library nodes 2022-11-03 19:08:44 +02:00
Stefan Zermatten
cc915410da Fixed query hitting mongo with too much regex 2022-10-25 19:00:02 +02:00
Stefan Zermatten
0b1db3c40c Updated meteor 2022-10-18 15:40:41 +02:00
Stefan Zermatten
0ad7e659d2 updated docs to include create a class guide 2022-10-18 15:40:17 +02:00
Stefan Zermatten
58c3875dc7 Hotifix: Casting cantrips without a spell slot 2022-10-12 07:36:42 +02:00
Stefan Zermatten
84f506f1fe Added $checkDiceRoll $checkRoll $checkModifier variables 2022-10-12 07:32:39 +02:00
Stefan Zermatten
a5b4b20324 Added link to V2 2022-10-11 14:08:33 +02:00
Stefan Zermatten
d0a3ccc76a bumped version 2022-10-10 16:54:57 +02:00
Stefan Zermatten
93ac9215c2 Merge branch 'version-2-dev' into version-2 2022-10-10 16:53:10 +02:00
Stefan Zermatten
a6b501a62c Fixed error on missing group prop in tree node 2022-10-10 16:51:02 +02:00
Stefan Zermatten
e956bacf07 Added actionType to effective tags 2022-10-10 16:49:10 +02:00
Stefan Zermatten
60b6b283b1 Folders now get their children applied by actions 2022-10-10 16:45:53 +02:00
Stefan Zermatten
1c9b390551 Added ritual casting 2022-10-09 23:11:06 +02:00
Stefan Zermatten
21a487635d Removed unused code from action cards 2022-10-09 21:56:42 +02:00
Stefan Zermatten
c92a26d5e6 Action cards no longer display folder or the descendants of buffs 2022-10-09 21:56:01 +02:00
Stefan Zermatten
49b514b8f3 Load common dialogs more aggressively 2022-10-09 20:55:50 +02:00
Stefan Zermatten
5cb835c536 Got basic typescript tools working 2022-10-09 17:33:43 +02:00
Stefan Zermatten
aa8f2d230d Hunted the last of the \t's to extinction 2022-10-09 16:56:28 +02:00
Stefan Zermatten
2fa913b09a Applied style rules to genocide all \t characters 2022-10-09 16:01:36 +02:00
Stefan Zermatten
de598c70a7 Fixed rolled effects not applying to checks 2022-10-09 11:10:50 +02:00
Stefan Zermatten
baecdeff24 Fixed bug where items with zero quantity have active children 2022-10-09 10:10:21 +02:00
Stefan Zermatten
d4b7d22b5f Fixed toggled off spells showing in spell list 2022-09-26 09:43:00 +02:00
Stefan Zermatten
87f79737e8 Fixed empty calculated inline fields showing calc 2022-09-25 12:39:49 +02:00
Stefan Zermatten
9f0ffe13f8 Updated meteor to fix observer bugs 2022-09-13 17:34:46 +02:00
Stefan Zermatten
adaa31d76c damage tags to ignore multipliers 2022-09-13 17:34:30 +02:00
Stefan Zermatten
b051d764f8 Slot cards have slot color as outline 2022-09-13 15:47:31 +02:00
Stefan Zermatten
ffb5b4a4f3 Libraries show name in page title 2022-09-13 15:44:37 +02:00
Stefan Zermatten
fd87b7fb75 Added advantage popup to spell cast 2022-09-09 13:20:54 +02:00
Stefan Zermatten
f035902842 Removed unused file 2022-09-08 14:47:12 +02:00
Stefan Zermatten
dbc5f7253f Finished basic docs 2022-09-05 14:36:39 +02:00
Stefan Zermatten
f0e7253374 Updated docs 2022-09-01 13:33:28 +02:00
Stefan Zermatten
ffe37bf907 Added more property help docs 2022-09-01 12:18:29 +02:00
Stefan Zermatten
a63e2099d3 Added documentation UI and began documenting props 2022-08-31 14:43:38 +02:00
Stefan Zermatten
0308e4e7a7 Merge branch 'version-2' into version-2-dev 2022-08-29 11:30:55 +02:00
Stefan Zermatten
43f8df09f0 Fixed client crash when effects target calcs 2022-08-26 09:42:34 +02:00
Stefan Zermatten
b6ed9ffb74 Merge branch 'version-2-dev' into version-2 2022-08-25 15:24:25 +02:00
Stefan Zermatten
a84da7d8a5 Buffs can skip var freezing, freeze inline calcs 2022-08-25 15:10:56 +02:00
Stefan Zermatten
249aebea0f Allowed some properties to return damaged action values
When a prop is damaged during an action, it now tries
to show its new value during the rest of that action
2022-08-25 15:10:36 +02:00
Stefan Zermatten
11a527481e Show Point buy on Build tab 2022-08-25 13:18:24 +02:00
Stefan Zermatten
8d729216b5 Properties now have their variable name as a default tag 2022-08-25 12:15:12 +02:00
Stefan Zermatten
1677e8c424 Fixed silence missing from trigger form 2022-08-25 12:14:32 +02:00
Stefan Zermatten
987aacbb67 Silence for triggers also 2022-08-25 12:12:07 +02:00
Stefan Zermatten
2714d0b9d5 Added the ability to silence most action props 2022-08-25 12:10:51 +02:00
Stefan Zermatten
1d98c41168 Fixed slotLevel not having the right value in spell scope 2022-08-25 11:40:30 +02:00
Stefan Zermatten
e42ec4b862 Continued work on point buy UI 2022-08-23 14:44:35 +02:00
Stefan Zermatten
59fc5ab851 Continued work on point buy UI 2022-08-22 15:07:40 +02:00
Stefan Zermatten
5d14c392e8 Added creature new variables to API 2022-08-22 11:58:48 +02:00
Stefan Zermatten
c6ca8c1fa4 Added point buy to computation engine 2022-08-19 14:03:12 +02:00
Stefan Zermatten
28307e26c3 Fixed some issues with skill display 2022-08-19 14:03:03 +02:00
Stefan Zermatten
6d42eb62f0 Merge branch 'version-2' into version-2-dev 2022-08-19 09:18:55 +02:00
Stefan Zermatten
877c9ca099 Fixed cache bashing in checks
Cache should only return clones of data,
not references to the cached data
2022-08-17 17:21:18 +02:00
Stefan Zermatten
9b652fc133 Added point buy form 2022-08-17 13:42:47 +02:00
Stefan Zermatten
c48cc20fb9 Added limits to character strings, migration to remove image data urls from character pictures 2021-08-10 10:44:24 +02:00
Stefan Zermatten
c0b031f2b5 Updated packages 2021-07-17 14:41:51 +02:00
Stefan Zermatten
c6b633613c Removed all patreon based restrictions 2021-07-17 14:10:06 +02:00
Stefan Zermatten
03b7f1037e Patreon access is now granted by tier instead of price paid 2021-01-28 12:20:01 +02:00
Stefan Zermatten
d99c44fdeb Added disconnection notice and smart disconnect 2020-10-06 13:16:04 +02:00
Stefan Zermatten
1e8549323b Merge branch 'master' of https://github.com/ThaumRystra/DiceCloud 2020-10-06 09:57:50 +02:00
Stefan Zermatten
49a83b487a Drastically increased frequency of old document cleanup to prevent large spikes in oplog 2020-10-06 09:57:24 +02:00
Stefan Zermatten
a072d23097 Added bcrypt back 2020-09-28 11:13:03 +02:00
Stefan Zermatten
ffb78c59b3 Merge branch 'master' of https://github.com/ThaumRystra/DiceCloud1 2020-09-28 11:06:05 +02:00
Stefan Zermatten
1f663bf4b4 Updated packages and meteor version 2020-09-28 11:02:35 +02:00
Stefan Zermatten
f58a035209 Fixed github link 2020-09-28 10:38:08 +02:00
Stefan Zermatten
3fdb9f79bd Fixed routes forcing a page refresh on navigation 2020-09-28 10:37:53 +02:00
Thaum Rystra
311bbfa58c Fixed tier required to view libraries: now $3 2020-05-18 12:31:39 +02:00
Thaum Rystra
d1126596c4 Added specific node and npm versions for the hosting provider 2020-04-25 11:49:15 +02:00
Thaum Rystra
bb6125f84c removed fibres from package.json because it breaks the build 2020-04-25 11:44:16 +02:00
Thaum Rystra
2d489f119d Updated dependencies 2020-04-25 11:41:55 +02:00
Thaum Rystra
3ab73f62bf Fix: drag and drop 2020-04-25 11:17:42 +02:00
Stefan Zermatten
8b30c7b6d0 Updated packages and dependencies 2020-02-18 10:44:17 +02:00
Stefan Zermatten
4a6fa304b3 Add user management methods for admins 2019-09-02 13:55:37 +02:00
Stefan Zermatten
fabb884831 Fixed issue with Link Generation to connect patreon account 2019-05-10 10:03:01 +02:00
Stefan Zermatten
2a703900ee Hotfix subscribed libraries not showing up in add item from library dialog 2019-05-09 09:19:45 +02:00
Stefan Zermatten
0789e4d759 Merge branch 'hotfix-item-libraries' 2019-05-07 09:26:12 +02:00
Stefan Zermatten
39c91f58e4 Swapped weight and value in library item dialog to be consistent with the inventory 2019-05-07 09:25:06 +02:00
Stefan Zermatten
c84342b21a Fixed styling of item library dialog on small screens 2019-05-07 09:19:12 +02:00
Stefan Zermatten
0373feb2ea Fixed issue where effects in libraries would appear editable to subscribers 2019-05-07 09:18:53 +02:00
Stefan Zermatten
0b11595657 Fixed an issue where adding items from libraries didn't get all their properties 2019-05-06 15:55:45 +02:00
Stefan Zermatten
e7f3f669dd Fixed an issue where users without user profiles would fail to load their libraries 2019-05-06 15:12:54 +02:00
Stefan Zermatten
8d969bd447 Merge branch 'feature-library-ui' 2019-05-06 14:55:15 +02:00
Stefan Zermatten
b3aeaf06ea Fixed an error when deleting categories from library items without any settings 2019-05-06 14:54:30 +02:00
Stefan Zermatten
85e3b0724a Added a skip button to the new user experience 2019-05-06 14:51:58 +02:00
Stefan Zermatten
81a3ede86e Substantially improved item libraries UI, locked behind Patreon tier 5 2019-05-06 14:51:48 +02:00
Stefan Zermatten
d4864dda5f Fixed error on no meteor.settings file, updated meteor 2019-05-03 13:41:49 +02:00
Stefan Zermatten
5ce1b6aff8 closes #210
closes #211
2019-04-03 10:16:24 +02:00
Stefan Zermatten
41731212ef Added application performance monitoring 2019-03-07 14:53:52 +02:00
Stefan Zermatten
ef9867d409 Merge branch 'feature-patreon-accounts' 2019-03-07 13:47:32 +02:00
Stefan Zermatten
721300700e Fixed capitalization error 2019-03-07 13:46:56 +02:00
Stefan Zermatten
bc6dfbe498 Fixed stray quotation mark 2019-03-07 13:45:38 +02:00
Stefan Zermatten
0a22073d67 Added library link for $5 patrons 2019-03-07 13:44:35 +02:00
Stefan Zermatten
857213f157 Improved Patreon linking 2019-03-07 13:35:31 +02:00
Stefan Zermatten
b3371fca53 Added fetching User data from patreon and writing it to the DiceCloud user database 2019-03-06 17:05:44 +02:00
Stefan Zermatten
3fbb006783 Added Patreon notification badge for new patreon posts 2019-02-21 11:51:46 +02:00
Stefan Zermatten
2253672f43 Merge pull request #202 from mommothazaz123/master
Add multiple new API endpoints
2019-02-21 10:58:19 +02:00
Andrew Zhu
ed6d557f8a Merge branch 'master' into master 2019-02-12 13:51:20 -08:00
Andrew Zhu
4d642b56bb use direct insert, add schema check 2019-02-12 13:51:39 -08:00
Stefan Zermatten
436c5bb785 Fixed bug in view permission causing 500 errors for Avrae 2019-02-12 09:53:29 +02:00
Stefan Zermatten
8489ef5ec0 Fixed restoring characters not working correctly for sub documents 2019-02-11 13:04:14 +02:00
Stefan Zermatten
c9710bdb09 Fixed restored characters not belonging to the user restoring them 2019-02-11 12:10:04 +02:00
Stefan Zermatten
26784f11b6 Merge branch 'feature-backup-restore' 2019-02-11 12:01:52 +02:00
Stefan Zermatten
23d43f7d43 Added character restore functionality 2019-02-11 11:58:45 +02:00
Stefan Zermatten
1ebb0d2527 Got character copying working 2019-02-11 11:11:51 +02:00
Stefan Zermatten
9d86cb8bee Added the copy character method 2019-02-11 10:21:11 +02:00
Stefan Zermatten
3343f8a813 Allowed canViewCharacter to take in a character instead of a charId to save a database read 2019-02-11 10:17:43 +02:00
Stefan Zermatten
0260824c2f Made gave backup and restore the ability to change ids for all docs 2019-02-11 10:09:18 +02:00
Stefan Zermatten
66ee3ff808 Merge remote-tracking branch 'origin/master' into feature-backup-restore 2019-02-11 09:45:05 +02:00
Andrew Zhu
cb71f6d380 move everything to Meteor methods 2019-02-07 22:05:24 -08:00
Andrew Zhu
2f04d9ec1c remove server check overrides 2019-02-07 15:45:45 -08:00
Andrew Zhu
40c54524a7 add delete character endpoint 2019-02-05 15:46:06 -08:00
Andrew Zhu
b890a3b11e add feature, effect, prof, class insert 2019-02-05 15:21:32 -08:00
Andrew Zhu
c9242a95f3 add createCharacter, transferCharacter endpoints 2019-02-05 15:14:11 -08:00
Andrew Zhu
fedda62c7c add endpoint to add spells 2019-02-05 13:59:55 -08:00
Andrew Zhu
612575d0e6 add skeletons, ratelimits for endpoints 2019-02-05 13:14:09 -08:00
Andrew Zhu
d1d22c0d89 formatting, add helper func for POST endpoints 2019-02-05 13:09:56 -08:00
Andrew Zhu
b94f5ebb4b add getUserId API endpoint 2019-02-05 13:08:28 -08:00
Stefan Zermatten
3f32535666 fixed link to dicecloud repo 2019-01-31 10:11:32 +02:00
Stefan Zermatten
4ea02c4fbb Fixed hidden link to localhost 2019-01-31 10:10:56 +02:00
Stefan Zermatten
b052e8dd19 Update README.md 2019-01-31 10:09:19 +02:00
Stefan Zermatten
e2822b9f22 added naive backup restore 2019-01-28 11:35:56 +02:00
Stefan Zermatten
c46b836985 Merge pull request #192 from Frogvall/master
Updated heat metal according to SRD/PHB
2018-11-14 10:14:19 +02:00
Frogvall
65d1bac0dc Updated heat metal according to SRD/PHB 2018-11-14 06:40:22 +01:00
Stefan Zermatten
fcae3056de Merge branch 'bugfix-67' 2018-11-13 12:57:28 +02:00
Stefan Zermatten
7d364c80c0 Added hotfix to fix password button until the relevant package can be updated, fixes #67 2018-11-13 12:57:02 +02:00
Stefan Zermatten
0ff6c08abd Merge branch 'bugfix-150' 2018-11-13 12:01:52 +02:00
Stefan Zermatten
1c95336843 Spell slot bubbles are limited to 10, overflow is shown numerically
fixes #150
2018-11-13 12:01:40 +02:00
Stefan Zermatten
b36720511b Merge branch 'bugfix-155' 2018-11-13 10:39:26 +02:00
Stefan Zermatten
261220fdd5 Fixes #155, buffs can now only be applied if you have write access
refactored applying buffs to be a method, not a client side operation. You can now only applied if you have write access to the receiving character.
2018-11-13 10:39:14 +02:00
Stefan Zermatten
64edc52cca Merge branch 'bugfix-170' 2018-11-13 10:08:08 +02:00
Stefan Zermatten
56f1bd2829 Fixes #170 and maybe some other more subtle problems regarding soft removes not cascading properly, orphaning objects 2018-11-13 10:07:55 +02:00
Stefan Zermatten
3b669fd2f9 Merge branch 'bugfix-177' 2018-11-13 09:32:24 +02:00
Stefan Zermatten
933878e158 fixes #177 2018-11-13 09:31:57 +02:00
Stefan Zermatten
0e6ca56316 Merge branch 'bugfix-191' 2018-11-13 09:28:06 +02:00
Stefan Zermatten
6599fe1ef8 fixes #191 2018-11-13 09:26:36 +02:00
Stefan Zermatten
f39baf43a1 Merge branch 'bugfix-187' 2018-11-13 08:59:34 +02:00
Stefan Zermatten
96f4e35e25 fixes #187 2018-11-13 08:58:42 +02:00
Stefan Zermatten
e17dbf6601 Upgrade to meteor 1.8 2018-10-10 14:28:48 +02:00
Stefan Zermatten
3f81d419f7 updated meteor 2018-10-10 14:00:46 +02:00
Stefan Zermatten
1c00f5aa04 Hotfix iOS sign in issues (maybe) 2018-10-04 10:05:02 +02:00
Stefan Zermatten
f5a32cb50a Cobbled together some semblance of an item library UI 2018-10-02 15:43:10 +02:00
Stefan Zermatten
f4d3368fb4 Updated cron job to clean database of soft removed documents 2018-09-25 10:58:01 +02:00
Stefan Zermatten
1de9fb558a Merge pull request #182 from mommothazaz123/patch-1
fix error in Characters.deny()
2018-09-14 10:06:43 +02:00
Andrew Zhu
06ffc94b4c fix error in Characters.deny()
Renamed `docs` to `doc`, since `doc.owner` was undefined.
2018-09-07 14:07:55 -07:00
Stefan Zermatten
74c6a423ee Allowed owners to give their characters to new owners, no UI yet. 2018-08-28 09:59:10 +02:00
574 changed files with 19202 additions and 10886 deletions

1
app/.gitignore vendored
View File

@@ -3,6 +3,7 @@
.demeteorized
.cache
.vscode
fileStorage
settings.json
public/components
public/_imports.html

View File

@@ -4,27 +4,27 @@
# but you can also edit it by hand.
accounts-password@2.3.1
random@1.2.0
underscore@1.0.10
random@1.2.1
underscore@1.0.11
dburles:mongo-collection-instances
accounts-google@1.4.0
email@2.2.1
email@2.2.2
meteor-base@1.5.1
mobile-experience@1.1.0
mongo@1.15.0
session@1.2.0
tracker@1.2.0
mongo@1.16.1
session@1.2.1
tracker@1.2.1
logging@1.3.1
reload@1.3.1
ejson@1.1.2
check@1.3.1
standard-minifier-js@2.8.0
ejson@1.1.3
check@1.3.2
standard-minifier-js@2.8.1
shell-server@0.5.0
ecmascript@0.16.2
ecmascript@0.16.3
es5-shim@4.8.0
service-configuration@1.3.0
service-configuration@1.3.1
dynamic-import@0.7.2
ddp-rate-limiter@1.1.0
ddp-rate-limiter@1.1.1
rate-limit@1.0.9
mdg:validated-method
static-html@1.3.2
@@ -37,7 +37,6 @@ 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
@@ -48,3 +47,5 @@ simple:rest-bearer-token-parser
simple:rest-json-error-handler
littledata:synced-cron
mdg:meteor-apm-agent
typescript@4.5.4
seba:minifiers-autoprefixer

View File

@@ -1 +1 @@
METEOR@2.7.3
METEOR@2.8.1

View File

@@ -1,4 +1,4 @@
accounts-base@2.2.3
accounts-base@2.2.5
accounts-google@1.4.0
accounts-oauth@1.4.1
accounts-password@2.3.1
@@ -12,7 +12,7 @@ aldeed:collection2@3.5.0
aldeed:schema-index@3.0.0
allow-deny@1.1.1
autoupdate@1.8.0
babel-compiler@7.9.0
babel-compiler@7.9.2
babel-runtime@1.5.1
base64@1.0.12
binary-heap@1.0.11
@@ -22,26 +22,26 @@ bozhao:link-accounts@2.6.1
caching-compiler@1.2.2
caching-html-compiler@1.2.1
callback-hook@1.4.0
check@1.3.1
check@1.3.2
coffeescript@2.4.1
coffeescript-compiler@2.4.1
dburles:mongo-collection-instances@0.3.6
ddp@1.4.0
ddp-client@2.5.0
ddp@1.4.1
ddp-client@2.6.1
ddp-common@1.4.0
ddp-rate-limiter@1.1.0
ddp-server@2.5.0
diff-sequence@1.1.1
ddp-rate-limiter@1.1.1
ddp-server@2.6.0
diff-sequence@1.1.2
dynamic-import@0.7.2
ecmascript@0.16.2
ecmascript@0.16.3
ecmascript-runtime@0.8.0
ecmascript-runtime-client@0.12.1
ecmascript-runtime-server@0.11.0
ejson@1.1.2
email@2.2.1
ejson@1.1.3
email@2.2.2
es5-shim@4.8.0
fetch@0.1.1
geojson-utils@1.0.10
fetch@0.1.2
geojson-utils@1.0.11
google-oauth@1.4.2
hot-code-push@1.0.4
html-tools@1.1.3
@@ -55,33 +55,33 @@ littledata:synced-cron@1.5.1
livedata@1.0.18
localstorage@1.2.0
logging@1.3.1
mdg:meteor-apm-agent@3.5.0
mdg:meteor-apm-agent@3.5.1
mdg:validated-method@1.2.0
meteor@1.10.0
meteor@1.10.2
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.4
minimongo@1.8.0
minifier-css@1.6.1
minifier-js@2.7.5
minimongo@1.9.0
mobile-experience@1.1.0
mobile-status-bar@1.1.0
modern-browsers@0.1.8
modules@0.18.0
modules-runtime@0.13.0
mongo@1.15.0
modern-browsers@0.1.9
modules@0.19.0
modules-runtime@0.13.1
mongo@1.16.1
mongo-decimal@0.1.3
mongo-dev-server@1.1.0
mongo-id@1.0.8
mongo-livedata@1.0.12
npm-mongo@4.3.1
npm-mongo@4.11.0
oauth@2.1.2
oauth2@1.3.1
ordered-dict@1.1.0
ostrio:cookies@2.7.2
ostrio:files@2.0.1
ostrio:files@2.3.2
patreon-oauth@0.1.0
peerlibrary:assert@0.3.0
peerlibrary:check-extension@0.7.0
@@ -93,20 +93,20 @@ peerlibrary:reactive-mongo@0.4.1
peerlibrary:reactive-publish@0.10.0
peerlibrary:server-autorun@0.8.0
peerlibrary:subscription-data@0.8.0
percolate:migrations@1.0.3
promise@0.12.0
percolate:migrations@1.1.0
promise@0.12.1
raix:eventemitter@1.0.0
random@1.2.0
random@1.2.1
rate-limit@1.0.9
react-fast-refresh@0.2.3
reactive-dict@1.3.0
reactive-var@1.0.11
reactive-dict@1.3.1
reactive-var@1.0.12
reload@1.3.1
retry@1.1.0
routepolicy@1.1.1
seba:minifiers-autoprefixer@2.0.1
service-configuration@1.3.0
session@1.2.0
service-configuration@1.3.1
session@1.2.1
sha@1.0.9
shell-server@0.5.0
simple:json-routes@2.3.1
@@ -116,14 +116,14 @@ simple:rest-json-error-handler@1.1.1
simple:rest-method-mixin@1.1.0
socket-stream-client@0.5.0
spacebars-compiler@1.3.1
standard-minifier-js@2.8.0
standard-minifier-js@2.8.1
static-html@1.3.2
templating-tools@1.2.2
tmeasday:check-npm-versions@1.0.2
tracker@1.2.0
tracker@1.2.1
typescript@4.5.4
underscore@1.0.10
underscore@1.0.11
url@1.3.2
webapp@1.13.1
webapp-hashing@1.1.0
webapp@1.13.2
webapp-hashing@1.1.1
zer0th:meteor-vuetify-loader@0.1.41

View File

@@ -2,38 +2,38 @@
<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">
<link rel="apple-touch-icon" sizes="57x57" href="/apple-touch-icon-57x57.png?v=lk6WXp6Pmj">
<link rel="apple-touch-icon" sizes="60x60" href="/apple-touch-icon-60x60.png?v=lk6WXp6Pmj">
<link rel="apple-touch-icon" sizes="72x72" href="/apple-touch-icon-72x72.png?v=lk6WXp6Pmj">
<link rel="apple-touch-icon" sizes="76x76" href="/apple-touch-icon-76x76.png?v=lk6WXp6Pmj">
<link rel="apple-touch-icon" sizes="114x114" href="/apple-touch-icon-114x114.png?v=lk6WXp6Pmj">
<link rel="apple-touch-icon" sizes="120x120" href="/apple-touch-icon-120x120.png?v=lk6WXp6Pmj">
<link rel="apple-touch-icon" sizes="144x144" href="/apple-touch-icon-144x144.png?v=lk6WXp6Pmj">
<link rel="apple-touch-icon" sizes="152x152" href="/apple-touch-icon-152x152.png?v=lk6WXp6Pmj">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon-180x180.png?v=lk6WXp6Pmj">
<link rel="icon" type="image/png" href="/favicon-32x32.png?v=lk6WXp6Pmj" sizes="32x32">
<link rel="icon" type="image/png" href="/favicon-194x194.png?v=lk6WXp6Pmj" sizes="194x194">
<link rel="icon" type="image/png" href="/favicon-96x96.png?v=lk6WXp6Pmj" sizes="96x96">
<link rel="icon" type="image/png" href="/android-chrome-192x192.png?v=lk6WXp6Pmj" sizes="192x192">
<link rel="icon" type="image/png" href="/favicon-16x16.png?v=lk6WXp6Pmj" sizes="16x16">
<link rel="manifest" href="/manifest.json?v=lk6WXp6Pmj">
<link rel="shortcut icon" href="/favicon.ico?v=lk6WXp6Pmj">
<meta name="msapplication-TileColor" content="#b91d1d">
<meta name="msapplication-TileImage" content="/mstile-144x144.png?v=lk6WXp6Pmj">
<meta name="theme-color" content="#d12929">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<link rel="apple-touch-icon" sizes="57x57" href="/apple-touch-icon-57x57.png?v=lk6WXp6Pmj">
<link rel="apple-touch-icon" sizes="60x60" href="/apple-touch-icon-60x60.png?v=lk6WXp6Pmj">
<link rel="apple-touch-icon" sizes="72x72" href="/apple-touch-icon-72x72.png?v=lk6WXp6Pmj">
<link rel="apple-touch-icon" sizes="76x76" href="/apple-touch-icon-76x76.png?v=lk6WXp6Pmj">
<link rel="apple-touch-icon" sizes="114x114" href="/apple-touch-icon-114x114.png?v=lk6WXp6Pmj">
<link rel="apple-touch-icon" sizes="120x120" href="/apple-touch-icon-120x120.png?v=lk6WXp6Pmj">
<link rel="apple-touch-icon" sizes="144x144" href="/apple-touch-icon-144x144.png?v=lk6WXp6Pmj">
<link rel="apple-touch-icon" sizes="152x152" href="/apple-touch-icon-152x152.png?v=lk6WXp6Pmj">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon-180x180.png?v=lk6WXp6Pmj">
<link rel="icon" type="image/png" href="/favicon-32x32.png?v=lk6WXp6Pmj" sizes="32x32">
<link rel="icon" type="image/png" href="/favicon-194x194.png?v=lk6WXp6Pmj" sizes="194x194">
<link rel="icon" type="image/png" href="/favicon-96x96.png?v=lk6WXp6Pmj" sizes="96x96">
<link rel="icon" type="image/png" href="/android-chrome-192x192.png?v=lk6WXp6Pmj" sizes="192x192">
<link rel="icon" type="image/png" href="/favicon-16x16.png?v=lk6WXp6Pmj" sizes="16x16">
<link rel="manifest" href="/manifest.json?v=lk6WXp6Pmj">
<link rel="shortcut icon" href="/favicon.ico?v=lk6WXp6Pmj">
<meta name="msapplication-TileColor" content="#b91d1d">
<meta name="msapplication-TileImage" content="/mstile-144x144.png?v=lk6WXp6Pmj">
<meta name="theme-color" content="#d12929">
<style type="text/css" media="print">
@page {
margin: 0mm;
}
html {
margin: 0px;
}
* {
-webkit-transition: none !important;
transition: none !important;
}
</style>
<style type="text/css" media="print">
@page {
margin: 0mm;
}
html {
margin: 0px;
}
* {
-webkit-transition: none !important;
transition: none !important;
}
</style>
</head>

View File

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

View File

@@ -1,18 +1,24 @@
import { createS3FilesCollection } from '/imports/api/files/s3FileStorage.js';
import SimpleSchema from 'simpl-schema';
import { incrementFileStorageUsed } from '/imports/api/users/methods/updateFileStorageUsed.js';
import { CreaturePropertySchema } from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { CreatureSchema } from '/imports/api/creature/creatures/Creatures.js';
let createS3FilesCollection;
if (Meteor.isServer) {
createS3FilesCollection = require('/imports/api/files/server/s3FileStorage.js').createS3FilesCollection
} else {
createS3FilesCollection = require('/imports/api/files/client/s3FileStorage.js').createS3FilesCollection
}
const ArchiveCreatureFiles = createS3FilesCollection({
collectionName: 'archiveCreatureFiles',
storagePath: Meteor.isDevelopment ? '/DiceCloud/archiveCreatures/' : 'assets/app/archiveCreatures',
storagePath: Meteor.isDevelopment ? '../../../../../fileStorage/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)){
if (!/json/i.test(file.extension)) {
return 'Please upload only a JSON file';
}
return true;

View File

@@ -4,25 +4,25 @@ 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,
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,
},
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,

View File

@@ -18,23 +18,23 @@ let CreaturePropertySchema = new SimpleSchema({
type: String,
optional: true,
},
type: {
type: {
type: String,
allowedValues: Object.keys(propertySchemasIndex),
},
tags: {
type: Array,
defaultValue: [],
tags: {
type: Array,
defaultValue: [],
maxCount: STORAGE_LIMITS.tagCount,
},
'tags.$': {
type: String,
},
'tags.$': {
type: String,
max: STORAGE_LIMITS.tagLength,
},
disabled: {
type: Boolean,
optional: true,
},
},
disabled: {
type: Boolean,
optional: true,
},
icon: {
type: storedIconsSchema,
optional: true,
@@ -93,20 +93,20 @@ const DenormalisedOnlyCreaturePropertySchema = new SimpleSchema({
CreaturePropertySchema.extend(DenormalisedOnlyCreaturePropertySchema);
for (let key in propertySchemasIndex){
let schema = new SimpleSchema({});
schema.extend(propertySchemasIndex[key]);
schema.extend(CreaturePropertySchema);
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}
});
schema.extend(ChildSchema);
schema.extend(SoftRemovableSchema);
CreatureProperties.attachSchema(schema, {
selector: { type: key }
});
}
export default CreatureProperties;
export {
DenormalisedOnlyCreaturePropertySchema,
CreaturePropertySchema,
CreaturePropertySchema,
};

View File

@@ -20,33 +20,33 @@ const adjustQuantity = new ValidatedMethod({
numRequests: 5,
timeInterval: 5000,
},
run({_id, operation, value}) {
run({ _id, operation, value }) {
// Permissions
let property = CreatureProperties.findOne(_id);
let property = CreatureProperties.findOne(_id);
let rootCreature = getRootCreatureAncestor(property);
assertEditPermission(rootCreature, this.userId);
assertEditPermission(rootCreature, this.userId);
// Do work
adjustQuantityWork({property, operation, value});
adjustQuantityWork({ property, operation, value });
},
});
export function adjustQuantityWork({property, operation, value}){
export function adjustQuantityWork({ property, operation, value }) {
// Check if property has quantity
let schema = CreatureProperties.simpleSchema(property);
if (!schema.allowsKey('quantity')){
if (!schema.allowsKey('quantity')) {
throw new Meteor.Error(
'Adjust quantity failed',
`Property of type "${property.type}" doesn't have a quantity`
);
}
if (operation === 'set'){
if (operation === 'set') {
CreatureProperties.update(property._id, {
$set: {quantity: value, dirty: true}
$set: { quantity: value, dirty: true }
}, {
selector: property
});
} else if (operation === 'increment'){
} else if (operation === 'increment') {
// value here is 'damage'
value = -value;
let currentQuantity = property.quantity;

View File

@@ -22,36 +22,44 @@ const damageProperty = new ValidatedMethod({
timeInterval: 5000,
},
run({ _id, operation, value }) {
// Get action context
const prop = CreatureProperties.findOne(_id);
let prop = CreatureProperties.findOne(_id);
if (!prop) throw new Meteor.Error(
'Damage property failed', 'Property doesn\'t exist'
);
const creatureId = prop.ancestors[0].id;
const actionContext = new ActionContext(creatureId, [creatureId], this);
// Check permissions
// Check permissions
assertEditPermission(actionContext.creature, this.userId);
// Check if property can take damage
let schema = CreatureProperties.simpleSchema(prop);
if (!schema.allowsKey('damage')){
throw new Meteor.Error(
'Damage property failed',
`Property of type "${prop.type}" can't be damaged`
);
// Check if property can take damage
let schema = CreatureProperties.simpleSchema(prop);
if (!schema.allowsKey('damage')) {
throw new Meteor.Error(
'Damage property failed',
`Property of type "${prop.type}" can't be damaged`
);
}
// Replace the prop by its actionContext counterpart if possible
if (prop.variableName) {
const actionContextProp = actionContext.scope[prop.variableName];
if (actionContextProp?._id === prop._id) {
prop = actionContextProp;
}
}
const result = damagePropertyWork({ prop, operation, value, actionContext });
// Insert the log
actionContext.writeLog();
return result;
},
});
export function damagePropertyWork({ prop, operation, value, actionContext }) {
export function damagePropertyWork({ prop, operation, value, actionContext, logFunction }) {
// Save the value to the scope before applying the before triggers
if (operation === 'increment') {
@@ -78,7 +86,7 @@ export function damagePropertyWork({ prop, operation, value, actionContext }) {
}
let damage, newValue, increment;
if (operation === 'set'){
if (operation === 'set') {
const total = prop.total || 0;
// Set represents what we want the value to be after damage
// So we need the actual damage to get to that value
@@ -94,7 +102,11 @@ export function damagePropertyWork({ prop, operation, value, actionContext }) {
}, {
selector: prop
});
} else if (operation === 'increment'){
// Also write it straight to the prop so that it is updated in the actionContext
prop.damage = damage;
prop.value = newValue;
logFunction?.(newValue);
} else if (operation === 'increment') {
let currentValue = prop.value || 0;
let currentDamage = prop.damage || 0;
increment = value;
@@ -111,6 +123,10 @@ export function damagePropertyWork({ prop, operation, value, actionContext }) {
}, {
selector: prop
});
// Also write it straight to the prop so that it is updated in the actionContext
prop.damage += increment;
prop.value -= increment;
logFunction?.(increment);
}
applyTriggers(actionContext.triggers?.damageProperty?.after, prop, actionContext);

View File

@@ -5,14 +5,14 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import {
setLineageOfDocs,
renewDocIds
setLineageOfDocs,
renewDocIds
} from '/imports/api/parenting/parenting.js';
import { reorderDocs } from '/imports/api/parenting/order.js';
var snackbar;
if (Meteor.isClient){
if (Meteor.isClient) {
snackbar = require(
'/imports/ui/components/snackbars/SnackbarQueue.js'
'/imports/client/ui/components/snackbars/SnackbarQueue.js'
).snackbar
}
@@ -31,7 +31,7 @@ const duplicateProperty = new ValidatedMethod({
numRequests: 5,
timeInterval: 5000,
},
run({_id}) {
run({ _id }) {
let property = CreatureProperties.findOne(_id);
let creature = getRootCreatureAncestor(property);
@@ -44,17 +44,17 @@ const duplicateProperty = new ValidatedMethod({
// Get all the descendants
let nodes = CreatureProperties.find({
'ancestors.id': _id,
removed: {$ne: true},
}, {
'ancestors.id': _id,
removed: { $ne: true },
}, {
limit: DUPLICATE_CHILDREN_LIMIT + 1,
sort: {order: 1},
sort: { order: 1 },
}).fetch();
// Alert the user if the limit was hit
if (nodes.length > DUPLICATE_CHILDREN_LIMIT){
if (nodes.length > DUPLICATE_CHILDREN_LIMIT) {
nodes.pop();
if (Meteor.isClient){
if (Meteor.isClient) {
snackbar({
text: `Only the first ${DUPLICATE_CHILDREN_LIMIT} children were duplicated`,
});
@@ -63,25 +63,25 @@ const duplicateProperty = new ValidatedMethod({
// re-map all the ancestors
setLineageOfDocs({
docArray: nodes,
newAncestry : [
docArray: nodes,
newAncestry: [
...property.ancestors,
{id: propertyId, collection: 'creatureProperties'}
{ id: propertyId, collection: 'creatureProperties' }
],
oldParent : {id: _id, collection: 'creatureProperties'},
});
oldParent: { id: _id, collection: 'creatureProperties' },
});
// Give the docs new IDs without breaking internal references
renewDocIds({docArray: nodes});
renewDocIds({ docArray: nodes });
// Order the root node
property.order += 0.5;
// Mark the sheet as needing recompute
property.dirty = true;
// Insert the properties
CreatureProperties.batchInsert([property, ...nodes]);
CreatureProperties.batchInsert([property, ...nodes]);
// Tree structure changed by inserts, reorder the tree
reorderDocs({

View File

@@ -10,8 +10,8 @@ import getParentRefByTag from '/imports/api/creature/creatureProperties/methods/
// Equipping or unequipping an item will also change its parent
const equipItem = new ValidatedMethod({
name: 'creatureProperties.equip',
validate({_id, equipped}){
if (!_id) throw new Meteor.Error('No _id', '_id is required');
validate({ _id, equipped }) {
if (!_id) throw new Meteor.Error('No _id', '_id is required');
if (equipped !== true && equipped !== false) {
throw new Meteor.Error('No equipped', 'equipped is required to be true or false');
}
@@ -21,20 +21,20 @@ const equipItem = new ValidatedMethod({
numRequests: 5,
timeInterval: 5000,
},
run({_id, equipped}) {
run({ _id, equipped }) {
let item = CreatureProperties.findOne(_id);
if (item.type !== 'item') throw new Meteor.Error('wrong type',
'Equip and unequip can only be performed on items');
'Equip and unequip can only be performed on items');
let creature = getRootCreatureAncestor(item);
assertEditPermission(creature, this.userId);
CreatureProperties.update(_id, {
$set: { equipped, dirty: true },
}, {
selector: {type: 'item'},
});
selector: { type: 'item' },
});
let tag = equipped ? BUILT_IN_TAGS.equipment : BUILT_IN_TAGS.carried;
let parentRef = getParentRefByTag(creature._id, tag);
if (!parentRef) parentRef = {id: creature._id, collection: 'creatures'};
if (!parentRef) parentRef = { id: creature._id, collection: 'creatures' };
organizeDoc.call({
docRef: {

View File

@@ -6,24 +6,24 @@ import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/ge
const flipToggle = new ValidatedMethod({
name: 'creatureProperties.flipToggle',
validate({_id}){
if (!_id) throw new Meteor.Error('No _id', '_id is required');
validate({ _id }) {
if (!_id) throw new Meteor.Error('No _id', '_id is required');
},
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({_id}) {
run({ _id }) {
// Permission
let property = CreatureProperties.findOne(_id, {
fields: {type: 1, ancestors: 1, enabled: 1, disabled: 1}
fields: { type: 1, ancestors: 1, enabled: 1, disabled: 1 }
});
if (property.type !== 'toggle'){
if (property.type !== 'toggle') {
throw new Meteor.Error('wrong property',
'This method can only be applied to toggles');
}
if (!property.enabled && !property.disabled){
if (!property.enabled && !property.disabled) {
throw new Meteor.Error('Computed toggle',
'Can\'t flip a toggle that is computed')
}
@@ -32,13 +32,15 @@ const flipToggle = new ValidatedMethod({
// Invert the current value, disabled is the canonical store of value
const currentValue = !property.disabled;
CreatureProperties.update(_id, {$set: {
enabled: !currentValue,
disabled: currentValue,
dirty: true,
}}, {
selector: {type: 'toggle'},
});
CreatureProperties.update(_id, {
$set: {
enabled: !currentValue,
disabled: currentValue,
dirty: true,
}
}, {
selector: { type: 'toggle' },
});
},
});

View File

@@ -12,7 +12,7 @@ import { getHighestOrder } from '/imports/api/parenting/order.js';
const insertProperty = new ValidatedMethod({
name: 'creatureProperties.insert',
validate: new SimpleSchema({
validate: new SimpleSchema({
creatureProperty: {
type: Object,
blackbox: true,
@@ -24,25 +24,25 @@ const insertProperty = new ValidatedMethod({
numRequests: 5,
timeInterval: 5000,
},
run({creatureProperty, parentRef}) {
run({ creatureProperty, parentRef }) {
// get the new ancestry for the properties
let {parentDoc, ancestors} = getAncestry({parentRef});
let { parentDoc, ancestors } = getAncestry({ parentRef });
// Check permission to edit
// Check permission to edit
let rootCreature;
if (parentRef.collection === 'creatures'){
if (parentRef.collection === 'creatures') {
rootCreature = parentDoc;
} else if (parentRef.collection === 'creatureProperties'){
} else if (parentRef.collection === 'creatureProperties') {
rootCreature = getRootCreatureAncestor(parentDoc);
} else {
throw `${parentRef.collection} is not a valid parent collection`
}
} else {
throw `${parentRef.collection} is not a valid parent collection`
}
assertEditPermission(rootCreature, this.userId);
creatureProperty.parent = parentRef;
creatureProperty.ancestors = ancestors;
return insertPropertyWork({
return insertPropertyWork({
property: creatureProperty,
creature: rootCreature,
});
@@ -75,31 +75,31 @@ const insertPropertyAsChildOfTag = new ValidatedMethod({
numRequests: 5,
timeInterval: 5000,
},
run({creatureProperty, creatureId, tag, tagDefaultName}) {
run({ creatureProperty, creatureId, tag, tagDefaultName }) {
let parentRef = getParentRefByTag(creatureId, tag);
if (!parentRef){
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'};
parentRef = { id: creatureId, collection: 'creatures' };
}
// get the new ancestry for the properties
let {parentDoc, ancestors} = getAncestry({parentRef});
let { parentDoc, ancestors } = getAncestry({ parentRef });
// Check permission to edit
let rootCreature;
if (parentRef.collection === 'creatures'){
if (parentRef.collection === 'creatures') {
rootCreature = parentDoc;
} else if (parentRef.collection === 'creatureProperties'){
} else if (parentRef.collection === 'creatureProperties') {
rootCreature = getRootCreatureAncestor(parentDoc);
} else {
throw `${parentRef.collection} is not a valid parent collection`
}
} else {
throw `${parentRef.collection} is not a valid parent collection`
}
assertEditPermission(rootCreature, this.userId);
// Add the folder first if we need to
if (insertFolderFirst){
if (insertFolderFirst) {
let order = getHighestOrder({
collection: CreatureProperties,
ancestorId: parentRef.id,
@@ -113,7 +113,7 @@ const insertPropertyAsChildOfTag = new ValidatedMethod({
order,
});
// Make the folder our new parent
let newParentRef = {id, collection: 'creatureProperties'};
let newParentRef = { id, collection: 'creatureProperties' };
ancestors = [parentRef, newParentRef];
parentRef = newParentRef;
creatureProperty.order = order + 1;
@@ -122,14 +122,14 @@ const insertPropertyAsChildOfTag = new ValidatedMethod({
creatureProperty.parent = parentRef;
creatureProperty.ancestors = ancestors;
return insertPropertyWork({
return insertPropertyWork({
property: creatureProperty,
creature: rootCreature,
});
},
});
export function insertPropertyWork({property, creature}){
export function insertPropertyWork({ property, creature }) {
delete property._id;
property.dirty = true;
let _id = CreatureProperties.insert(property);

View File

@@ -7,57 +7,57 @@ import { RefSchema } from '/imports/api/parenting/ChildSchema.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import {
setLineageOfDocs,
getAncestry,
renewDocIds
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({
name: 'creatureProperties.insertPropertyFromLibraryNode',
validate: new SimpleSchema({
nodeIds: {
type: Array,
max: 20,
},
'nodeIds.$': {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
parentRef: {
type: RefSchema,
},
'nodeIds.$': {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
parentRef: {
type: RefSchema,
},
order: {
type: Number,
optional: true,
},
}).validator(),
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({nodeIds, parentRef, order}) {
// get the new ancestry for the properties
let {parentDoc, ancestors} = getAncestry({parentRef});
run({ nodeIds, parentRef, order }) {
// get the new ancestry for the properties
let { parentDoc, ancestors } = getAncestry({ parentRef });
// Check permission to edit
// Check permission to edit
let rootCreature;
if (parentRef.collection === 'creatures'){
if (parentRef.collection === 'creatures') {
rootCreature = parentDoc;
} else if (parentRef.collection === 'creatureProperties'){
} else if (parentRef.collection === 'creatureProperties') {
rootCreature = getRootCreatureAncestor(parentDoc);
} else {
throw `${parentRef.collection} is not a valid parent collection`
}
} else {
throw `${parentRef.collection} is not a valid parent collection`
}
assertEditPermission(rootCreature, this.userId);
// {libraryId: hasViewPermission}
//let libraryPermissionMemoir = {};
let node;
nodeIds.forEach(nodeId => {
nodeIds.forEach(nodeId => {
// TODO: Check library view permission for each node before starting
node = insertPropertyFromNode(nodeId, ancestors, order);
});
@@ -70,18 +70,18 @@ const insertPropertyFromLibraryNode = new ValidatedMethod({
collection: CreatureProperties,
ancestorId: rootCreature._id,
});
// Return the docId of the last property, the inserted root property
return rootId;
},
// Return the docId of the last property, the inserted root property
return rootId;
},
});
function insertPropertyFromNode(nodeId, ancestors, order){
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},
removed: { $ne: true },
});
if (!node) {
if (Meteor.isClient) return;
@@ -95,7 +95,7 @@ function insertPropertyFromNode(nodeId, ancestors, order){
let oldParent = node.parent;
let nodes = LibraryNodes.find({
'ancestors.id': nodeId,
removed: {$ne: true},
removed: { $ne: true },
}).fetch();
// Convert all references into actual nodes
@@ -118,11 +118,11 @@ function insertPropertyFromNode(nodeId, ancestors, order){
// Give the docs new IDs without breaking internal references
renewDocIds({
docArray: nodes,
collectionMap: {'libraryNodes': 'creatureProperties'}
collectionMap: { 'libraryNodes': 'creatureProperties' }
});
// Order the root node
if (order === undefined){
if (order === undefined) {
setDocToLastOrder({
collection: CreatureProperties,
doc: node,
@@ -139,7 +139,7 @@ function insertPropertyFromNode(nodeId, ancestors, order){
return node;
}
function storeLibraryNodeReferences(nodes){
function storeLibraryNodeReferences(nodes) {
nodes.forEach(node => {
if (node.libraryNodeId) return;
node.libraryNodeId = node._id;
@@ -154,7 +154,7 @@ function dirtyNodes(nodes) {
// Covert node references into actual nodes
// TODO: check permissions for each library a reference node references
function reifyNodeReferences(nodes, visitedRefs = new Set(), depth = 0){
function reifyNodeReferences(nodes, visitedRefs = new Set(), depth = 0) {
depth += 1;
// New nodes added this function
let newNodes = [];
@@ -165,9 +165,9 @@ function reifyNodeReferences(nodes, visitedRefs = new Set(), depth = 0){
if (node.type !== 'reference') return true;
// We have gone too deep, keep the reference node as an error
if (depth >= 10){
if (depth >= 10) {
if (Meteor.isClient) console.warn('Reference depth limit exceeded');
node.cache = {error: 'Reference depth limit exceeded'};
node.cache = { error: 'Reference depth limit exceeded' };
return true;
}
@@ -177,17 +177,17 @@ function reifyNodeReferences(nodes, visitedRefs = new Set(), depth = 0){
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()};
} 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},
removed: { $ne: true },
}, {
sort: {order: 1},
sort: { order: 1 },
}).fetch();
// We are adding the referenced node and its descendants
@@ -195,20 +195,20 @@ function reifyNodeReferences(nodes, visitedRefs = new Set(), depth = 0){
// 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,
});
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'){
if (addedNode.type !== 'reference') {
return true;
}
// If this exact reference has already been resolved before, filter it out
if (visitedRefs.has(addedNode._id)){
if (visitedRefs.has(addedNode._id)) {
return false;
} else {
// Otherwise mark it as visited, and keep it

View File

@@ -5,28 +5,28 @@ import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
const pullFromProperty = new ValidatedMethod({
name: 'creatureProperties.pull',
validate: null,
name: 'creatureProperties.pull',
validate: null,
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({_id, path, itemId}){
run({ _id, path, itemId }) {
// Permissions
let property = CreatureProperties.findOne(_id);
let property = CreatureProperties.findOne(_id);
let rootCreature = getRootCreatureAncestor(property);
assertEditPermission(rootCreature, this.userId);
// Do work
CreatureProperties.update(_id, {
CreatureProperties.update(_id, {
$pull: { [path.join('.')]: { _id: itemId } },
$set: { dirty: true }
}, {
selector: {type: property.type},
getAutoValues: false,
});
}
}, {
selector: { type: property.type },
getAutoValues: false,
});
}
});
export default pullFromProperty;

View File

@@ -6,16 +6,16 @@ import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/ge
import { get } from 'lodash';
const pushToProperty = new ValidatedMethod({
name: 'creatureProperties.push',
validate: null,
name: 'creatureProperties.push',
validate: null,
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({_id, path, value}){
run({ _id, path, value }) {
// Permissions
let property = CreatureProperties.findOne(_id);
let property = CreatureProperties.findOne(_id);
let rootCreature = getRootCreatureAncestor(property);
assertEditPermission(rootCreature, this.userId);
@@ -25,10 +25,10 @@ const pushToProperty = new ValidatedMethod({
let schema = CreatureProperties.simpleSchema(property);
let maxCount = schema.get(joinedPath, 'maxCount');
if (Number.isFinite(maxCount)){
if (Number.isFinite(maxCount)) {
let array = get(property, path);
let currentCount = array ? array.length : 0;
if (currentCount >= maxCount){
if (currentCount >= maxCount) {
throw new Meteor.Error(
'Array is full',
`Cannot have more than ${maxCount} values`
@@ -37,13 +37,13 @@ const pushToProperty = new ValidatedMethod({
}
// Do work
CreatureProperties.update(_id, {
CreatureProperties.update(_id, {
$push: { [joinedPath]: value },
$set: { dirty: true },
}, {
selector: {type: property.type},
});
}
}, {
selector: { type: property.type },
});
}
});
export default pushToProperty;

View File

@@ -7,18 +7,18 @@ import { restore } from '/imports/api/parenting/softRemove.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
const restoreProperty = new ValidatedMethod({
name: 'creatureProperties.restore',
validate: new SimpleSchema({
_id: SimpleSchema.RegEx.Id
}).validator(),
name: 'creatureProperties.restore',
validate: new SimpleSchema({
_id: SimpleSchema.RegEx.Id
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({_id}){
run({ _id }) {
// Permissions
let property = CreatureProperties.findOne(_id);
let property = CreatureProperties.findOne(_id);
let rootCreature = getRootCreatureAncestor(property);
assertEditPermission(rootCreature, this.userId);
@@ -30,7 +30,7 @@ const restoreProperty = new ValidatedMethod({
$set: { dirty: true }
},
});
}
}
});
export default restoreProperty;

View File

@@ -17,20 +17,20 @@ const selectAmmoItem = new ValidatedMethod({
numRequests: 5,
timeInterval: 5000,
},
run({actionId, itemId, itemConsumedIndex}) {
run({ actionId, itemId, itemConsumedIndex }) {
// Permissions
let action = CreatureProperties.findOne(actionId);
let action = CreatureProperties.findOne(actionId);
let rootCreature = getRootCreatureAncestor(action);
assertEditPermission(rootCreature, this.userId);
assertEditPermission(rootCreature, this.userId);
// Check that this index has a document to edit
let itemConsumed = action.resources.itemsConsumed[itemConsumedIndex];
if (!itemConsumed){
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){
if (!itemToLink) {
throw new Meteor.Error('Item not found',
'Could not set ammo: the item was not found');
}

View File

@@ -7,24 +7,24 @@ import { softRemove } from '/imports/api/parenting/softRemove.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
const softRemoveProperty = new ValidatedMethod({
name: 'creatureProperties.softRemove',
validate: new SimpleSchema({
_id: SimpleSchema.RegEx.Id
}).validator(),
name: 'creatureProperties.softRemove',
validate: new SimpleSchema({
_id: SimpleSchema.RegEx.Id
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({_id}){
run({ _id }) {
// Permissions
let property = CreatureProperties.findOne(_id);
let property = CreatureProperties.findOne(_id);
let rootCreature = getRootCreatureAncestor(property);
assertEditPermission(rootCreature, this.userId);
// Do work
softRemove({_id, collection: CreatureProperties});
}
softRemove({ _id, collection: CreatureProperties });
}
});
export default softRemoveProperty;

View File

@@ -6,28 +6,28 @@ import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/ge
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':
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');
}
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}) {
run({ _id, path, value }) {
// Permission
let property = CreatureProperties.findOne(_id, {
fields: {type: 1, ancestors: 1}
fields: { type: 1, ancestors: 1 }
});
let rootCreature = getRootCreatureAncestor(property);
assertEditPermission(rootCreature, this.userId);
@@ -35,14 +35,14 @@ const updateCreatureProperty = new ValidatedMethod({
let pathString = path.join('.');
let modifier;
// unset empty values
if (value === null || value === undefined){
modifier = { $unset: {[pathString]: 1}, $set: { dirty: true } };
if (value === null || value === undefined) {
modifier = { $unset: { [pathString]: 1 }, $set: { dirty: true } };
} else {
modifier = { $set: {[pathString]: value, dirty: true } };
modifier = { $set: { [pathString]: value, dirty: true } };
}
CreatureProperties.update(_id, modifier, {
selector: {type: property.type},
});
CreatureProperties.update(_id, modifier, {
selector: { type: property.type },
});
},
});

View File

@@ -4,9 +4,9 @@ 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);
}
}
for (let ref of property.ancestors){
if (ref.collection === 'creatures') {
computeCreature.call(ref.id);
}
}
}

View File

@@ -8,21 +8,26 @@ import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
let Creatures = new Mongo.Collection('creatures');
let CreatureSettingsSchema = new SimpleSchema({
//slowed down by carrying too much?
useVariantEncumbrance: {
type: Boolean,
optional: true,
},
//hide spellcasting tab
hideSpellcasting: {
type: Boolean,
optional: true,
},
// Swap around the modifier and stat
swapStatAndModifier: {
type: Boolean,
optional: true,
},
//slowed down by carrying too much?
useVariantEncumbrance: {
type: Boolean,
optional: true,
},
//hide spellcasting tab
hideSpellcasting: {
type: Boolean,
optional: true,
},
//hide rest buttons
hideRestButtons: {
type: Boolean,
optional: true,
},
// Swap around the modifier and stat
swapStatAndModifier: {
type: Boolean,
optional: true,
},
// Hide all the unused stats
hideUnusedStats: {
type: Boolean,
@@ -58,28 +63,28 @@ let CreatureSettingsSchema = new SimpleSchema({
});
let CreatureSchema = new SimpleSchema({
// Strings
name: {
type: String,
defaultValue: '',
optional: true,
// Strings
name: {
type: String,
defaultValue: '',
optional: true,
max: STORAGE_LIMITS.name,
},
alignment: {
type: String,
optional: true,
},
alignment: {
type: String,
optional: true,
max: STORAGE_LIMITS.name,
},
gender: {
type: String,
optional: true,
},
gender: {
type: String,
optional: true,
max: STORAGE_LIMITS.name,
},
picture: {
type: String,
optional: true,
},
picture: {
type: String,
optional: true,
max: STORAGE_LIMITS.url,
},
},
avatarPicture: {
type: String,
optional: true,
@@ -90,37 +95,37 @@ let CreatureSchema = new SimpleSchema({
allowedLibraries: {
type: Array,
optional: true,
maxCount: 100,
},
'allowedLibraries.$': {
type: String,
maxCount: 100,
},
'allowedLibraries.$': {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
allowedLibraryCollections: {
type: Array,
},
allowedLibraryCollections: {
type: Array,
optional: true,
maxCount: 100,
},
'allowedLibraryCollections.$': {
type: String,
maxCount: 100,
},
'allowedLibraryCollections.$': {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
// Mechanics
deathSave: {
type: deathSaveSchema,
defaultValue: {},
},
// Mechanics
deathSave: {
type: deathSaveSchema,
defaultValue: {},
},
// Stats that are computed and denormalised outside of recomputation
denormalizedStats: {
type: Object,
defaultValue: {},
},
// Sum of all XP gained by this character
'denormalizedStats.xp': {
type: SimpleSchema.Integer,
defaultValue: 0,
},
'denormalizedStats.xp': {
type: SimpleSchema.Integer,
defaultValue: 0,
},
// Sum of all levels granted by milestone XP
'denormalizedStats.milestoneLevels': {
type: SimpleSchema.Integer,
@@ -133,24 +138,24 @@ let CreatureSchema = new SimpleSchema({
},
// Version of computation engine that was last used to compute this creature
computeVersion: {
type: String,
type: String,
optional: true,
},
type: {
type: String,
defaultValue: 'pc',
allowedValues: ['pc', 'npc', 'monster'],
},
},
type: {
type: String,
defaultValue: 'pc',
allowedValues: ['pc', 'npc', 'monster'],
},
damageMultipliers: {
type: Object,
blackbox: true,
defaultValue: {}
blackbox: true,
defaultValue: {}
},
variables: {
type: Object,
blackbox: true,
defaultValue: {}
},
variables: {
type: Object,
blackbox: true,
defaultValue: {}
},
computeErrors: {
type: Array,
optional: true,
@@ -161,7 +166,7 @@ let CreatureSchema = new SimpleSchema({
'computeErrors.$.type': {
type: String,
},
'computeErrors.$.details' : {
'computeErrors.$.details': {
type: Object,
blackbox: true,
optional: true,
@@ -178,11 +183,11 @@ let CreatureSchema = new SimpleSchema({
optional: true,
},
// Settings
settings: {
type: CreatureSettingsSchema,
defaultValue: {},
},
// Settings
settings: {
type: CreatureSettingsSchema,
defaultValue: {},
},
});
CreatureSchema.extend(ColorSchema);

View File

@@ -1,7 +1,7 @@
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';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import SimpleSchema from 'simpl-schema';
import simpleSchemaMixin from '/imports/api/creature/mixins/simpleSchemaMixin.js';
@@ -36,8 +36,8 @@ const changeAllowedLibraries = new ValidatedMethod({
numRequests: 10,
timeInterval: 5000,
},
run({_id, allowedLibraries, allowedLibraryCollections}) {
let creature = Creatures.findOne(_id);
run({ _id, allowedLibraries, allowedLibraryCollections }) {
let creature = Creatures.findOne(_id);
assertEditPermission(creature, this.userId);
let $set;
if (allowedLibraries) {
@@ -48,7 +48,7 @@ const changeAllowedLibraries = new ValidatedMethod({
$set.allowedLibraryCollections = allowedLibraryCollections;
}
if (!$set) return;
Creatures.update(_id, {$set});
Creatures.update(_id, { $set });
},
});
@@ -68,7 +68,7 @@ const toggleAllUserLibraries = new ValidatedMethod({
numRequests: 10,
timeInterval: 5000,
},
run({_id, value}) {
run({ _id, value }) {
if (value) {
Creatures.update(_id, {
$unset: {
@@ -87,4 +87,4 @@ const toggleAllUserLibraries = new ValidatedMethod({
},
});
export {changeAllowedLibraries, toggleAllUserLibraries};
export { changeAllowedLibraries, toggleAllUserLibraries };

View File

@@ -51,7 +51,7 @@ const insertCreature = new ValidatedMethod({
allowedLibraries,
allowedLibraryCollections,
});
// Insert experience to get character to starting level
if (startingLevel) {
insertExperienceForCreature({
@@ -70,7 +70,7 @@ const insertCreature = new ValidatedMethod({
let baseId, rulesetSlot;
defaultCharacterProperties(creatureId).forEach(prop => {
let id = CreatureProperties.insert(prop);
if (prop.name === 'Ruleset'){
if (prop.name === 'Ruleset') {
baseId = id;
rulesetSlot = prop;
}
@@ -81,7 +81,7 @@ const insertCreature = new ValidatedMethod({
insertDefaultRuleset(creatureId, baseId, userId, rulesetSlot);
}
return creatureId;
return creatureId;
},
});
@@ -95,7 +95,7 @@ function insertDefaultRuleset(creatureId, baseId, userId, slot) {
const ruleset = fillCursor.fetch()[0]
insertPropertyFromLibraryNode.call({
nodeIds: [ruleset._id],
parentRef: {id: baseId, collection: 'creatureProperties'},
parentRef: { id: baseId, collection: 'creatureProperties' },
order: 0.5,
});
}

View File

@@ -6,6 +6,7 @@ import { assertEditPermission } from '/imports/api/creature/creatures/creaturePe
import { union } from 'lodash';
import ActionContext from '/imports/api/engine/actions/ActionContext.js';
import { applyTriggers } from '/imports/api/engine/actions/applyTriggers.js';
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
const restCreature = new ValidatedMethod({
name: 'creature.methods.rest',
@@ -49,7 +50,7 @@ const restCreature = new ValidatedMethod({
applyTriggers(afterTriggers, null, actionContext);
// Insert log
actionContext.writeLog();
actionContext.writeLog();
},
});
@@ -57,88 +58,113 @@ function doRestWork(restType, actionContext) {
const creatureId = actionContext.creature._id;
// Long rests reset short rest properties as well
let resetFilter;
if (restType === 'shortRest'){
if (restType === 'shortRest') {
resetFilter = 'shortRest'
} else {
resetFilter = {$in: ['shortRest', 'longRest']}
resetFilter = { $in: ['shortRest', 'longRest'] }
}
resetProperties(creatureId, resetFilter, actionContext);
// Reset half hit dice on a long rest, starting with the highest dice
if (restType === 'longRest') {
resetHitDice(creatureId, actionContext);
}
}
export function resetProperties(creatureId, resetFilter, actionContext) {
// Only apply to active properties
let filter = {
const filter = {
'ancestors.id': creatureId,
reset: resetFilter,
removed: { $ne: true },
inactive: { $ne: true },
};
// update all attribute's damage
filter.type = 'attribute';
CreatureProperties.update(filter, {
$set: {
damage: 0,
dirty: true,
}
}, {
selector: {type: 'attribute'},
multi: true,
const attributeFilter = {
...filter,
type: 'attribute',
damage: { $nin: [0, undefined] },
}
CreatureProperties.find(attributeFilter).forEach(prop => {
damagePropertyWork({
prop,
operation: 'increment',
value: -prop.damage ?? 0,
actionContext,
logFunction(increment) {
actionContext.addLog({
name: prop.name,
value: increment < 0 ? `Restored ${-increment}` : `Removed ${-increment}`
});
}
});
});
// Update all action-like properties' usesUsed
filter.type = {$in: [
'action',
'attack',
'spell'
]};
CreatureProperties.update(filter, {
const actionFilter = {
...filter,
type: {
$in: ['action', 'spell']
},
usesUsed: { $nin: [0, undefined] },
};
CreatureProperties.find(actionFilter, {
fields: { name: 1, usesUsed: 1 }
}).forEach(prop => {
actionContext.addLog({
name: prop.name,
value: prop.usesUsed >= 0 ? `Restored ${prop.usesUsed} uses` : `Removed ${-prop.usesUsed} uses`
});
});
CreatureProperties.update(actionFilter, {
$set: {
usesUsed: 0,
dirty: true,
}
}, {
selector: {type: 'action'},
selector: { type: 'action' },
multi: true,
});
// Reset half hit dice on a long rest, starting with the highest dice
if (restType === 'longRest'){
let hitDice = CreatureProperties.find({
'ancestors.id': creatureId,
type: 'attribute',
attributeType: 'hitDice',
removed: {$ne: true},
inactive: {$ne: true},
}, {
fields: {
hitDiceSize: 1,
damage: 1,
total: 1,
}
function resetHitDice(creatureId, actionContext) {
let hitDice = CreatureProperties.find({
'ancestors.id': creatureId,
type: 'attribute',
attributeType: 'hitDice',
removed: { $ne: true },
inactive: { $ne: true },
}).fetch();
// Use a collator to do sorting in natural order
let collator = new Intl.Collator('en', {
numeric: true, sensitivity: 'base'
});
// Get the hit dice in decending order of hitDiceSize
let compare = (a, b) => collator.compare(b.hitDiceSize, a.hitDiceSize)
hitDice.sort(compare);
// Get the total number of hit dice that can be recovered this rest
let totalHd = hitDice.reduce((sum, hd) => sum + (hd.total || 0), 0);
let resetMultiplier = actionContext.creature.settings.hitDiceResetMultiplier || 0.5;
let recoverableHd = Math.max(Math.floor(totalHd * resetMultiplier), 1);
// recover each hit dice in turn until the recoverable amount is used up
let amountToRecover;
hitDice.forEach(hd => {
if (!recoverableHd) return;
amountToRecover = Math.min(recoverableHd, hd.damage ?? 0);
if (!amountToRecover) return;
recoverableHd -= amountToRecover;
damagePropertyWork({
prop: hd,
operation: 'increment',
value: -amountToRecover,
actionContext,
logFunction(increment) {
actionContext.addLog({
name: hd.name,
value: increment < 0 ? `Restored ${-increment} hit dice` : `Removed ${increment} hit dice`
});
}
}).fetch();
// Use a collator to do sorting in natural order
let collator = new Intl.Collator('en', {
numeric: true, sensitivity: 'base'
});
// Get the hit dice in decending order of hitDiceSize
let compare = (a, b) => collator.compare(b.hitDiceSize, a.hitDiceSize)
hitDice.sort(compare);
// Get the total number of hit dice that can be recovered this rest
let totalHd = hitDice.reduce((sum, hd) => sum + (hd.total || 0), 0);
let resetMultiplier = actionContext.creature.settings.hitDiceResetMultiplier || 0.5;
let recoverableHd = Math.max(Math.floor(totalHd*resetMultiplier), 1);
// recover each hit dice in turn until the recoverable amount is used up
let amountToRecover, resultingDamage;
hitDice.forEach(hd => {
if (!recoverableHd) return;
amountToRecover = Math.min(recoverableHd, hd.damage || 0);
if (!amountToRecover) return;
recoverableHd -= amountToRecover;
resultingDamage = hd.damage - amountToRecover;
CreatureProperties.update(hd._id, {
$set: {
damage: resultingDamage,
dirty: true,
}
}, {
selector: {type: 'attribute'},
});
});
}
});
}
export default restCreature;

View File

@@ -1,14 +1,14 @@
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';
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 = [
validate({ _id, path }) {
if (!_id) return false;
// Allowed fields
let allowedFields = [
'name',
'alignment',
'gender',
@@ -17,26 +17,26 @@ const updateCreature = new ValidatedMethod({
'color',
'settings',
];
if (!allowedFields.includes(path[0])){
throw new Meteor.Error('Creatures.methods.update.denied',
'This field can\'t be updated using this method');
}
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);
run({ _id, path, value }) {
let creature = Creatures.findOne(_id);
assertEditPermission(creature, this.userId);
if (value === undefined || value === null){
if (value === undefined || value === null) {
Creatures.update(_id, {
$unset: {[path.join('.')]: 1},
$unset: { [path.join('.')]: 1 },
});
} else {
Creatures.update(_id, {
$set: {[path.join('.')]: value},
$set: { [path.join('.')]: value },
});
}
},

View File

@@ -8,17 +8,17 @@ import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
let Experiences = new Mongo.Collection('experiences');
let ExperienceSchema = new SimpleSchema({
name: {
type: String,
optional: true,
name: {
type: String,
optional: true,
max: STORAGE_LIMITS.name,
},
// The amount of XP this experience gives
xp: {
type: SimpleSchema.Integer,
optional: true,
},
// The amount of XP this experience gives
xp: {
type: SimpleSchema.Integer,
optional: true,
min: 0,
},
},
// Setting levels instead of value grants whole levels
levels: {
type: SimpleSchema.Integer,
@@ -26,17 +26,17 @@ let ExperienceSchema = new SimpleSchema({
min: 0,
index: 1,
},
// The real-world date that it occured, usually sorted by date
date: {
type: Date,
autoValue: function() {
// If the date isn't set, set it to now
if (!this.isSet) {
return new Date();
}
},
// The real-world date that it occured, usually sorted by date
date: {
type: Date,
autoValue: function () {
// If the date isn't set, set it to now
if (!this.isSet) {
return new Date();
}
},
index: 1,
},
},
creatureId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
@@ -46,8 +46,8 @@ let ExperienceSchema = new SimpleSchema({
Experiences.attachSchema(ExperienceSchema);
const insertExperienceForCreature = function({experience, creatureId, userId}){
if (experience.xp){
const insertExperienceForCreature = function ({ experience, creatureId }) {
if (experience.xp) {
Creatures.update(creatureId, {
$inc: { 'denormalizedStats.xp': experience.xp },
$set: { dirty: true },
@@ -84,16 +84,16 @@ const insertExperience = new ValidatedMethod({
numRequests: 5,
timeInterval: 5000,
},
run({experience, creatureIds}) {
run({ experience, creatureIds }) {
let userId = this.userId;
if (!userId) {
throw new Meteor.Error('Experiences.methods.insert.denied',
'You need to be logged in to insert an experience');
'You need to be logged in to insert an experience');
}
let insertedIds = [];
creatureIds.forEach(creatureId => {
assertEditPermission(creatureId, userId);
let id = insertExperienceForCreature({experience, creatureId, userId});
let id = insertExperienceForCreature({ experience, creatureId, userId });
insertedIds.push(id);
});
return insertedIds;
@@ -113,17 +113,17 @@ const removeExperience = new ValidatedMethod({
numRequests: 5,
timeInterval: 5000,
},
run({experienceId}) {
run({ experienceId }) {
let userId = this.userId;
if (!userId) {
throw new Meteor.Error('Experiences.methods.remove.denied',
'You need to be logged in to remove an experience');
'You need to be logged in to remove an experience');
}
let experience = Experiences.findOne(experienceId);
if (!experience) return;
let creatureId = experience.creatureId
assertEditPermission(creatureId, userId);
if (experience.xp){
if (experience.xp) {
Creatures.update(creatureId, {
$inc: { 'denormalizedStats.xp': -experience.xp },
$set: { dirty: true },
@@ -154,11 +154,11 @@ const recomputeExperiences = new ValidatedMethod({
numRequests: 5,
timeInterval: 5000,
},
run({creatureId}) {
run({ creatureId }) {
let userId = this.userId;
if (!userId) {
throw new Meteor.Error('Experiences.methods.recompute.denied',
'You need to be logged in to recompute a creature\'s experiences');
'You need to be logged in to recompute a creature\'s experiences');
}
assertEditPermission(creatureId, userId);
@@ -167,16 +167,18 @@ const recomputeExperiences = new ValidatedMethod({
Experiences.find({
creatureId
}, {
fields: {xp: 1, levels: 1}
fields: { xp: 1, levels: 1 }
}).forEach(experience => {
xp += experience.xp || 0;
milestoneLevels += experience.levels || 0;
});
Creatures.update(creatureId, {$set: {
'denormalizedStats.xp': xp,
'denormalizedStats.milestoneLevels': milestoneLevels,
dirty: true,
}});
Creatures.update(creatureId, {
$set: {
'denormalizedStats.xp': xp,
'denormalizedStats.milestoneLevels': milestoneLevels,
dirty: true,
}
});
},
});

View File

@@ -2,33 +2,33 @@ import SimpleSchema from 'simpl-schema';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
let ExperienceSchema = new SimpleSchema({
title: {
type: String,
optional: true,
title: {
type: String,
optional: true,
max: STORAGE_LIMITS.name,
},
// Potentially long description of the event
description: {
type: String,
optional: true,
},
// Potentially long description of the event
description: {
type: String,
optional: true,
max: STORAGE_LIMITS.description,
},
// The real-world date that it occured
date: {
type: Date,
autoValue: function() {
// If the date isn't set, set it to now
if (!this.isSet) {
return new Date();
}
},
},
// The date in-world of this event
worldDate: {
type: String,
optional: true,
},
// The real-world date that it occured
date: {
type: Date,
autoValue: function () {
// If the date isn't set, set it to now
if (!this.isSet) {
return new Date();
}
},
},
// The date in-world of this event
worldDate: {
type: String,
optional: true,
max: STORAGE_LIMITS.name,
},
},
// Tags to better find this entry later
tags: {
type: Array,

View File

@@ -4,13 +4,13 @@ import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables
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/creatures/creaturePermissions.js';
import {parse, prettifyParseError} 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){
if (Meteor.isServer) {
var sendWebhookAsCreature = require('/imports/server/discord/sendWebhook.js').sendWebhookAsCreature;
}
@@ -25,17 +25,17 @@ let CreatureLogSchema = new SimpleSchema({
'content.$': {
type: LogContentSchema,
},
// The real-world date that it occured, usually sorted by date
date: {
type: Date,
autoValue: function() {
// If the date isn't set, set it to now
if (!this.isSet) {
return new Date();
}
},
// The real-world date that it occured, usually sorted by date
date: {
type: Date,
autoValue: function () {
// If the date isn't set, set it to now
if (!this.isSet) {
return new Date();
}
},
index: 1,
},
},
creatureId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
@@ -50,23 +50,23 @@ let CreatureLogSchema = new SimpleSchema({
CreatureLogs.attachSchema(CreatureLogSchema);
function removeOldLogs(creatureId){
function removeOldLogs(creatureId) {
// Find the first log that is over the limit
let firstExpiredLog = CreatureLogs.find({
creatureId
}, {
sort: {date: -1},
sort: { date: -1 },
skip: PER_CREATURE_LOG_LIMIT,
});
if (!firstExpiredLog) return;
// Remove all logs older than the one over the limit
CreatureLogs.remove({
creatureId,
date: {$lte: firstExpiredLog.date},
date: { $lte: firstExpiredLog.date },
});
}
function logToMessageData(log){
function logToMessageData(log) {
let embed = {
fields: [],
};
@@ -78,8 +78,8 @@ function logToMessageData(log){
return { embeds: [embed] };
}
function logWebhook({log, creature}){
if (Meteor.isServer){
function logWebhook({ log, creature }) {
if (Meteor.isServer) {
sendWebhookAsCreature({
creature,
data: logToMessageData(log),
@@ -94,47 +94,49 @@ const insertCreatureLog = new ValidatedMethod({
numRequests: 5,
timeInterval: 5000,
},
validate: new SimpleSchema({
log: CreatureLogSchema.omit('date'),
}).validator(),
run({log}){
validate: new SimpleSchema({
log: CreatureLogSchema.omit('date'),
}).validator(),
run({ log }) {
const creatureId = log.creatureId;
const creature = Creatures.findOne(creatureId, {fields: {
readers: 1,
writers: 1,
owner: 1,
'settings.discordWebhook': 1,
name: 1,
avatarPicture: 1,
}});
const creature = Creatures.findOne(creatureId, {
fields: {
readers: 1,
writers: 1,
owner: 1,
'settings.discordWebhook': 1,
name: 1,
avatarPicture: 1,
}
});
assertEditPermission(creature, this.userId);
// Build the new log
let id = insertCreatureLogWork({log, creature, method: this})
let id = insertCreatureLogWork({ log, creature, method: this })
return id;
},
});
export function insertCreatureLogWork({log, creature, method}){
export function insertCreatureLogWork({ log, creature, method }) {
// Build the new log
if (typeof log === 'string'){
log = {content: [{value: 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){
if (Meteor.isServer) {
method?.unblock();
removeOldLogs(creature._id);
logWebhook({log, creature});
logWebhook({ log, creature });
}
return id;
}
function equalIgnoringWhitespace(a, b){
function equalIgnoringWhitespace(a, b) {
if (typeof a !== 'string' || typeof b !== 'string') return a === b;
return a.replace(/\s/g,'') === b.replace(/\s/g, '');
return a.replace(/\s/g, '') === b.replace(/\s/g, '');
}
const logRoll = new ValidatedMethod({
@@ -144,33 +146,35 @@ const logRoll = new ValidatedMethod({
numRequests: 5,
timeInterval: 5000,
},
validate: new SimpleSchema({
roll: {
type: String,
},
creatureId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
}).validator(),
run({roll, creatureId}){
const creature = Creatures.findOne(creatureId, {fields: {
readers: 1,
writers: 1,
owner: 1,
'settings.discordWebhook': 1,
name: 1,
avatarPicture: 1,
}});
validate: new SimpleSchema({
roll: {
type: String,
},
creatureId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
}).validator(),
run({ roll, creatureId }) {
const creature = Creatures.findOne(creatureId, {
fields: {
readers: 1,
writers: 1,
owner: 1,
'settings.discordWebhook': 1,
name: 1,
avatarPicture: 1,
}
});
assertEditPermission(creature, this.userId);
const variables = CreatureVariables.findOne({ _creatureId: creatureId });
let logContent = []
let parsedResult = undefined;
try {
parsedResult = parse(roll);
} catch (e){
} catch (e) {
let error = prettifyParseError(e);
logContent.push({name: 'Parse Error', value: error});
logContent.push({ name: 'Parse Error', value: error });
}
if (parsedResult) try {
let {
@@ -184,19 +188,19 @@ const logRoll = new ValidatedMethod({
logContent.push({
value: compiledString
});
let {result: rolled} = resolve('roll', compiled, variables, context);
let { result: rolled } = resolve('roll', compiled, variables, context);
let rolledString = toString(rolled);
if (rolledString !== compiledString) logContent.push({
value: rolledString
});
let {result} = resolve('reduce', rolled, variables, context);
let { result } = resolve('reduce', rolled, variables, context);
let resultString = toString(result);
if (resultString !== rolledString) logContent.push({
value: resultString
});
} catch (e){
} catch (e) {
console.error(e);
logContent = [{name: 'Calculation error'}];
logContent = [{ name: 'Calculation error' }];
}
const log = {
content: logContent,
@@ -204,11 +208,11 @@ const logRoll = new ValidatedMethod({
date: new Date(),
};
let id = insertCreatureLogWork({log, creature, method: this});
let id = insertCreatureLogWork({ log, creature, method: this });
return id;
},
});
export default CreatureLogs;
export { CreatureLogSchema, insertCreatureLog, logRoll, PER_CREATURE_LOG_LIMIT};
export { CreatureLogSchema, insertCreatureLog, logRoll, PER_CREATURE_LOG_LIMIT };

View File

@@ -0,0 +1,324 @@
import { Meteor } from 'meteor/meteor';
import { Mongo } from 'meteor/mongo';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import SimpleSchema from 'simpl-schema';
import { softRemove } from '/imports/api/parenting/softRemove.js';
import SoftRemovableSchema from '/imports/api/parenting/SoftRemovableSchema.js';
import { storedIconsSchema } from '/imports/api/icons/Icons.js';
import '/imports/api/library/methods/index.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
import { restore } from '/imports/api/parenting/softRemove.js';
import { reorderDocs } from '/imports/api/parenting/order.js';
import { getAncestry } from '/imports/api/parenting/parenting.js';
const Docs = new Mongo.Collection('docs');
const RefSchema = new SimpleSchema({
id: {
type: String,
regEx: SimpleSchema.RegEx.Id,
index: 1
},
collection: {
type: String,
max: STORAGE_LIMITS.collectionName,
},
urlName: {
type: String,
regEx: /[a-z]+(?:[a-z]|-)+/,
min: 2,
max: STORAGE_LIMITS.variableName,
optional: true,
},
name: {
type: String,
max: STORAGE_LIMITS.description,
optional: true,
},
});
let ChildSchema = new SimpleSchema({
order: {
type: Number,
},
parent: {
type: RefSchema,
optional: true,
},
ancestors: {
type: Array,
defaultValue: [],
maxCount: STORAGE_LIMITS.ancestorCount,
},
'ancestors.$': {
type: RefSchema,
},
});
let DocSchema = new SimpleSchema({
_id: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
name: {
type: String,
max: STORAGE_LIMITS.description,
},
urlName: {
type: String,
regEx: /[a-z]+(?:[a-z]|-)+/,
min: 2,
max: STORAGE_LIMITS.variableName,
},
href: {
type: String,
},
description: {
type: String,
optional: true,
},
published: {
type: Boolean,
optional: true,
},
icon: {
type: storedIconsSchema,
optional: true,
max: STORAGE_LIMITS.icon,
},
});
let schema = new SimpleSchema({});
schema.extend(DocSchema);
schema.extend(ChildSchema);
schema.extend(SoftRemovableSchema);
Docs.attachSchema(schema);
function assertDocsEditPermission(userId) {
if (!userId || typeof userId !== 'string') throw new Meteor.Error('No user id provided');
const user = Meteor.users.findOne(userId);
if (!user) throw new Meteor.Error('User does not exist');
if (!user?.roles?.includes?.('docsWriter')) throw ('Permission denied')
}
function getDocLink(doc, urlName) {
if (!urlName) urlName = doc.urlName;
const address = ['/docs'];
doc.ancestors?.forEach(a => {
address.push(a.urlName);
});
address.push(urlName);
return address.join('/');
}
function rebuildDocAncestors(docId) {
const newDoc = Docs.findOne(docId);
Docs.find({ 'ancestors.id': docId }).forEach(doc => {
doc.ancestors.forEach((a, i) => {
if (a.id === docId) {
Docs.update(doc._id, {
$set: {
[`ancestors.${i}`]: {
id: newDoc._id,
collection: 'docs',
urlName: newDoc.urlName,
name: newDoc.name,
}
}
});
}
});
doc = Docs.findOne(doc._id);
const newLink = getDocLink(doc);
if (doc.href !== newLink) {
Docs.update(doc._id, { $set: { href: newLink } })
}
});
}
// Add a means of seeding new servers with documentation
if (Meteor.isClient) {
Docs.getJsonDocs = function () {
return JSON.stringify(Docs.find({}).fetch(), null, 2);
}
} else if (Meteor.isServer) {
Meteor.startup(() => {
if (!Docs.findOne()) {
Assets.getText('docs/defaultDocs.json', (error, string) => {
const docs = JSON.parse(string)
docs.forEach(doc => Docs.insert(doc));
});
}
});
}
const insertDoc = new ValidatedMethod({
name: 'docs.insert',
validate: null,
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({ doc, parentRef }) {
delete doc._id;
assertDocsEditPermission(this.userId);
// get the new ancestry for the properties
if (parentRef) {
var { ancestors } = getAncestry({
parentRef,
inheritedFields: { name: 1, urlName: 1 },
});
}
doc.parent = parentRef;
doc.ancestors = ancestors;
const lastOrder = Docs.find({}, { sort: { order: -1 } }).fetch()[0]?.order || 0;
doc.order = lastOrder + 1;
doc.urlName = 'new-doc-' + (lastOrder + 1);
doc.href = getDocLink(doc);
if (Docs.findOne({ href: doc.href })) {
throw new Meteor.Error('Link collision', 'A document with the same URL already exists');
}
const docId = Docs.insert(doc);
reorderDocs({
collection: Docs,
ancestorId: 'root',
});
return docId;
},
});
const updateDoc = new ValidatedMethod({
name: 'docs.update',
validate({ _id, path }) {
if (!_id) return false;
// We cannot change these fields with a simple update
switch (path[0]) {
case '_is':
return false;
}
},
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({ _id, path, value }) {
assertDocsEditPermission(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 } };
}
if (pathString === 'urlName') {
const doc = Docs.findOne(_id);
const newLink = getDocLink(doc, value);
if (Docs.findOne({ href: newLink })) {
throw new Meteor.Error('Link collision', 'A document with the same URL already exists');
}
modifier.$set = modifier.$set || {};
modifier.$set.href = newLink;
rebuildDocAncestors(_id);
}
const updates = Docs.update(_id, modifier);
if (pathString === 'name' || pathString === 'urlName') {
rebuildDocAncestors(_id);
}
reorderDocs({
collection: Docs,
ancestorId: 'root',
});
return updates;
},
});
const pushToDoc = new ValidatedMethod({
name: 'docs.push',
validate: null,
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({ _id, path, value }) {
assertDocsEditPermission(this.userId);
return Docs.update(_id, {
$push: { [path.join('.')]: value },
});
}
});
const pullFromDoc = new ValidatedMethod({
name: 'docs.pull',
validate: null,
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({ _id, path, itemId }) {
assertDocsEditPermission(this.userId);
return Docs.update(_id, {
$pull: { [path.join('.')]: { _id: itemId } },
});
}
});
const softRemoveDoc = new ValidatedMethod({
name: 'docs.softRemove',
validate: new SimpleSchema({
_id: SimpleSchema.RegEx.Id
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({ _id }) {
assertDocsEditPermission(this.userId);
softRemove({ _id, collection: Docs });
reorderDocs({
collection: Docs,
ancestorId: 'root',
});
}
});
const restoreDoc = new ValidatedMethod({
name: 'docs.restore',
validate: new SimpleSchema({
_id: SimpleSchema.RegEx.Id
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({ _id }) {
assertDocsEditPermission(this.userId);
restore({ _id, collection: Docs });
reorderDocs({
collection: Docs,
ancestorId: 'root',
});
}
});
export {
DocSchema,
insertDoc,
updateDoc,
pushToDoc,
pullFromDoc,
softRemoveDoc,
restoreDoc,
};
export default Docs;

View File

@@ -4,6 +4,7 @@ import branch from './applyPropertyByType/applyBranch.js';
import buff from './applyPropertyByType/applyBuff.js';
import buffRemover from './applyPropertyByType/applyBuffRemover.js';
import damage from './applyPropertyByType/applyDamage.js';
import folder from './applyPropertyByType/applyFolder.js';
import note from './applyPropertyByType/applyNote.js';
import roll from './applyPropertyByType/applyRoll.js';
import savingThrow from './applyPropertyByType/applySavingThrow.js';
@@ -16,6 +17,7 @@ const applyPropertyByType = {
buff,
buffRemover,
damage,
folder,
note,
roll,
savingThrow,

View File

@@ -5,8 +5,9 @@ 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';
import numberToSignedString from '/imports/api/utility/numberToSignedString.js';
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
import { resetProperties } from '/imports/api/creature/creatures/methods/restCreature.js';
export default function applyAction(node, actionContext) {
applyNodeTriggers(node, 'before', actionContext);
@@ -16,11 +17,11 @@ export default function applyAction(node, actionContext) {
// Log the name and summary
let content = { name: prop.name };
if (prop.summary?.text){
if (prop.summary?.text) {
recalculateInlineCalculations(prop.summary, actionContext);
content.value = prop.summary.value;
}
actionContext.addLog(content);
if (!prop.silent) actionContext.addLog(content);
// Spend the resources
const failed = spendResources(prop, actionContext);
@@ -29,24 +30,27 @@ export default function applyAction(node, actionContext) {
const attack = prop.attackRoll || prop.attackRollBonus;
// Attack if there is an attack roll
if (attack && attack.calculation){
if (targets.length){
if (attack && attack.calculation) {
if (targets.length) {
targets.forEach(target => {
applyAttackToTarget({attack, target, actionContext});
applyAttackToTarget({ attack, target, actionContext });
// Apply the children, but only to the current target
actionContext.targets = [target];
applyChildren(node, actionContext);
});
} else {
applyAttackWithoutTarget({attack, actionContext});
applyAttackWithoutTarget({ attack, actionContext });
applyChildren(node, actionContext);
}
} else {
applyChildren(node, actionContext);
}
if (prop.actionType === 'event' && prop.variableName) {
resetProperties(actionContext.creature._id, prop.variableName, actionContext);
}
}
function applyAttackWithoutTarget({attack, actionContext}){
function applyAttackWithoutTarget({ attack, actionContext }) {
delete actionContext.scope['$attackHit'];
delete actionContext.scope['$attackMiss'];
delete actionContext.scope['$criticalHit'];
@@ -62,16 +66,16 @@ function applyAttackWithoutTarget({attack, actionContext}){
criticalMiss,
} = rollAttack(attack, scope);
let name = criticalHit ? 'Critical Hit!' : criticalMiss ? 'Critical Miss!' : 'To Hit';
if (scope['$attackAdvantage'] === 1){
if (scope['$attackAdvantage'] === 1) {
name += ' (Advantage)';
} else if(scope['$attackAdvantage'] === -1){
} else if (scope['$attackAdvantage'] === -1) {
name += ' (Disadvantage)';
}
if (!criticalMiss){
scope['$attackHit'] = {value: true}
if (!criticalMiss) {
scope['$attackHit'] = { value: true }
}
if (!criticalHit){
scope['$attackMiss'] = {value: true};
if (!criticalHit) {
scope['$attackMiss'] = { value: true };
}
actionContext.addLog({
@@ -81,7 +85,7 @@ function applyAttackWithoutTarget({attack, actionContext}){
});
}
function applyAttackToTarget({attack, target, actionContext}){
function applyAttackToTarget({ attack, target, actionContext }) {
const scope = actionContext.scope;
delete scope['$attackHit'];
delete scope['$attackMiss'];
@@ -99,15 +103,15 @@ function applyAttackToTarget({attack, target, actionContext}){
criticalMiss,
} = rollAttack(attack, scope);
if (target.variables.armor){
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){
result > armor ? 'Hit!' : 'Miss!';
if (scope['$attackAdvantage'] === 1) {
name += ' (Advantage)';
} else if(scope['$attackAdvantage'] === -1){
} else if (scope['$attackAdvantage'] === -1) {
name += ' (Disadvantage)';
}
@@ -116,15 +120,15 @@ function applyAttackToTarget({attack, target, actionContext}){
value: `${resultPrefix}\n**${result}**`,
inline: true,
});
if (criticalMiss || result < armor){
scope['$attackMiss'] = {value: true};
if (criticalMiss || result < armor) {
scope['$attackMiss'] = { value: true };
} else {
scope['$attackHit'] = {value: true};
scope['$attackHit'] = { value: true };
}
} else {
actionContext.addLog({
name: 'Error',
value:'Target has no `armor`',
value: 'Target has no `armor`',
});
actionContext.addLog({
name: criticalHit ? 'Critical Hit!' : criticalMiss ? 'Critical Miss!' : 'To Hit',
@@ -134,10 +138,10 @@ function applyAttackToTarget({attack, target, actionContext}){
}
}
function rollAttack(attack, scope){
function rollAttack(attack, scope) {
const rollModifierText = numberToSignedString(attack.value, true);
let value, resultPrefix;
if (scope['$attackAdvantage'] === 1){
if (scope['$attackAdvantage'] === 1) {
const [a, b] = rollDice(2, 20);
if (a >= b) {
value = a;
@@ -146,7 +150,7 @@ function rollAttack(attack, scope){
value = b;
resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`;
}
} else if (scope['$attackAdvantage'] === -1){
} else if (scope['$attackAdvantage'] === -1) {
const [a, b] = rollDice(2, 20);
if (a <= b) {
value = a;
@@ -159,25 +163,26 @@ function rollAttack(attack, scope){
value = rollDice(1, 20)[0];
resultPrefix = `1d20 [${value}] ${rollModifierText}`
}
scope['$attackRoll'] = {value};
scope['$attackDiceRoll'] = { value };
const result = value + attack.value;
const {criticalHit, criticalMiss} = applyCrits(value, scope);
return {resultPrefix, result, value, criticalHit, criticalMiss};
scope['$attackRoll'] = { value: result };
const { criticalHit, criticalMiss } = applyCrits(value, scope);
return { resultPrefix, result, value, criticalHit, criticalMiss };
}
function applyCrits(value, scope){
function applyCrits(value, scope) {
let criticalHitTarget = scope.criticalHitTarget?.value || 20;
let criticalHit = value >= criticalHitTarget;
let criticalMiss;
if (criticalHit){
scope['$criticalHit'] = {value: true};
if (criticalHit) {
scope['$criticalHit'] = { value: true };
} else {
criticalMiss = value === 1;
if (criticalMiss){
scope['$criticalMiss'] = {value: true};
if (criticalMiss) {
scope['$criticalMiss'] = { value: true };
}
}
return {criticalHit, criticalMiss};
return { criticalHit, criticalMiss };
}
function applyChildren(node, actionContext) {
@@ -185,18 +190,18 @@ function applyChildren(node, actionContext) {
node.children.forEach(child => applyProperty(child, actionContext));
}
function spendResources(prop, actionContext){
function spendResources(prop, actionContext) {
// Check Uses
if (prop.usesLeft <= 0){
actionContext.addLog({
if (prop.usesLeft <= 0) {
if (!prop.silent) actionContext.addLog({
name: 'Error',
value: `${prop.name || 'action'} does not have enough uses left`,
});
return true;
}
// Resources
if (prop.insufficientResources){
actionContext.addLog({
if (prop.insufficientResources) {
if (!prop.silent) actionContext.addLog({
name: 'Error',
value: 'This creature doesn\'t have sufficient resources to perform this action',
});
@@ -209,14 +214,14 @@ function spendResources(prop, actionContext){
try {
prop.resources.itemsConsumed.forEach(itemConsumed => {
recalculateCalculation(itemConsumed.quantity, actionContext);
if (!itemConsumed.itemId){
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){
if (!item || item.ancestors[0].id !== prop.ancestors[0].id) {
throw 'The prop\'s ammo was not found on the creature';
}
if (!item.equipped){
if (!item.equipped) {
throw 'The selected ammo is not equipped';
}
if (
@@ -229,16 +234,16 @@ function spendResources(prop, actionContext){
value: itemConsumed.quantity.value,
});
let logName = item.name;
if (itemConsumed.quantity.value > 1 || itemConsumed.quantity.value < -1){
if (itemConsumed.quantity.value > 1 || itemConsumed.quantity.value < -1) {
logName = item.plural || logName;
}
if (itemConsumed.quantity.value > 0){
if (itemConsumed.quantity.value > 0) {
spendLog.push(logName + ': ' + itemConsumed.quantity.value);
} else if (itemConsumed.quantity.value < 0){
} else if (itemConsumed.quantity.value < 0) {
gainLog.push(logName + ': ' + -itemConsumed.quantity.value);
}
});
} catch (e){
} catch (e) {
actionContext.addLog({
name: 'Error',
value: e,
@@ -251,13 +256,13 @@ function spendResources(prop, actionContext){
itemQuantityAdjustments.forEach(adjustQuantityWork);
// Use uses
if (prop.usesLeft){
if (prop.usesLeft) {
CreatureProperties.update(prop._id, {
$inc: {usesUsed: 1}
$inc: { usesUsed: 1 }
}, {
selector: prop
});
actionContext.addLog({
if (!prop.silent) actionContext.addLog({
name: 'Uses left',
value: prop.usesLeft - 1,
inline: true,
@@ -270,7 +275,7 @@ function spendResources(prop, actionContext){
if (!attConsumed.quantity?.value) return;
let stat = actionContext.scope[attConsumed.variableName];
if (!stat){
if (!stat) {
spendLog.push(stat.name + ': ' + ' not found');
return;
}
@@ -280,20 +285,20 @@ function spendResources(prop, actionContext){
value: attConsumed.quantity.value,
actionContext,
});
if (attConsumed.quantity.value > 0){
if (attConsumed.quantity.value > 0) {
spendLog.push(stat.name + ': ' + attConsumed.quantity.value);
} else if (attConsumed.quantity.value < 0){
} else if (attConsumed.quantity.value < 0) {
gainLog.push(stat.name + ': ' + -attConsumed.quantity.value);
}
});
// Log all the spending
if (gainLog.length) actionContext.addLog({
if (gainLog.length && !prop.silent) actionContext.addLog({
name: 'Gained',
value: gainLog.join('\n'),
inline: true,
});
if (spendLog.length) actionContext.addLog({
if (spendLog.length && !prop.silent) actionContext.addLog({
name: 'Spent',
value: spendLog.join('\n'),
inline: true,

View File

@@ -24,7 +24,7 @@ export default function applyAdjustment(node, actionContext){
damageTargets.forEach(target => {
let stat = target.variables[prop.stat];
if (!stat?.type) {
actionContext.addLog({
if (!prop.silent) actionContext.addLog({
name: 'Error',
value: `Could not apply attribute damage, creature does not have \`${prop.stat}\` set`
});
@@ -36,7 +36,7 @@ export default function applyAdjustment(node, actionContext){
value,
actionContext,
});
actionContext.addLog({
if (!prop.silent) actionContext.addLog({
name: 'Attribute damage',
value: `${prop.stat}${prop.operation === 'set' ? ' set to' : ''}` +
` ${value}`,
@@ -44,7 +44,7 @@ export default function applyAdjustment(node, actionContext){
});
});
} else {
actionContext.addLog({
if (!prop.silent) actionContext.addLog({
name: 'Attribute damage',
value: `${prop.stat}${prop.operation === 'set' ? ' set to' : ''}` +
` ${value}`,

View File

@@ -36,25 +36,25 @@ export default function applyBranch(node, actionContext){
break;
case 'hit':
if (scope['$attackHit']?.value){
if (!targets.length) actionContext.addLog({value: '**On hit**'});
if (!targets.length && !prop.silent) actionContext.addLog({value: '**On hit**'});
applyChildren();
}
break;
case 'miss':
if (scope['$attackMiss']?.value){
if (!targets.length) actionContext.addLog({value: '**On miss**'});
if (!targets.length && !prop.silent) actionContext.addLog({value: '**On miss**'});
applyChildren();
}
break;
case 'failedSave':
if (scope['$saveFailed']?.value){
if (!targets.length) actionContext.addLog({value: '**On failed save**'});
if (!targets.length && !prop.silent) actionContext.addLog({value: '**On failed save**'});
applyChildren();
}
break;
case 'successfulSave':
if (scope['$saveSucceeded']?.value){
if (!targets.length) actionContext.addLog({value: '**On save**',});
if (!targets.length && !prop.silent) actionContext.addLog({value: '**On save**',});
applyChildren();
}
break;

View File

@@ -1,8 +1,8 @@
import {
setLineageOfDocs,
renewDocIds
setLineageOfDocs,
renewDocIds
} from '/imports/api/parenting/parenting.js';
import {setDocToLastOrder} from '/imports/api/parenting/order.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';
@@ -13,15 +13,17 @@ import logErrors from './shared/logErrors.js';
import { insertCreatureLog } from '/imports/api/creature/log/CreatureLogs.js';
import cyrb53 from '/imports/api/engine/computation/utility/cyrb53.js';
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
import INLINE_CALCULATION_REGEX from '/imports/constants/INLINE_CALCULTION_REGEX.js';
import recalculateInlineCalculations from './shared/recalculateInlineCalculations.js';
export default function applyBuff(node, actionContext){
export default function applyBuff(node, actionContext) {
applyNodeTriggers(node, 'before', actionContext);
const prop = node.node;
let buffTargets = prop.target === 'self' ? [actionContext.creature] : actionContext.targets;
// Then copy the decendants of the buff to the targets
let propList = [prop];
function addChildrenToPropList(children, { skipCrystalize } = {}){
function addChildrenToPropList(children, { skipCrystalize } = {}) {
children.forEach(child => {
if (skipCrystalize) child.node._skipCrystalize = true;
propList.push(child.node);
@@ -32,7 +34,9 @@ export default function applyBuff(node, actionContext){
});
}
addChildrenToPropList(node.children);
crystalizeVariables({propList, actionContext});
if (!prop.skipCrystalization) {
crystalizeVariables({ propList, actionContext });
}
let oldParent = {
id: prop.parent.id,
@@ -43,12 +47,17 @@ export default function applyBuff(node, actionContext){
copyNodeListToTarget(propList, target, oldParent);
//Log the buff
if (prop.name || prop.description?.value){
if (target._id === actionContext.creature._id){
let logValue = prop.description?.value
if (prop.description?.text) {
recalculateInlineCalculations(prop.description, actionContext);
logValue = prop.description?.value;
}
if ((prop.name || prop.description?.value) && !prop.silent) {
if (target._id === actionContext.creature._id) {
// Targeting self
actionContext.addLog({
name: prop.name,
value: prop.description?.value,
value: logValue,
});
} else {
// Targeting other
@@ -57,7 +66,7 @@ export default function applyBuff(node, actionContext){
creatureId: target._id,
content: [{
name: prop.name,
value: prop.description?.value,
value: logValue,
}],
}
});
@@ -69,8 +78,8 @@ export default function applyBuff(node, actionContext){
// 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}];
function copyNodeListToTarget(propList, target, oldParent) {
let ancestry = [{ collection: 'creatures', id: target._id }];
setLineageOfDocs({
docArray: propList,
newAncestry: ancestry,
@@ -90,13 +99,14 @@ function copyNodeListToTarget(propList, target, oldParent){
* Replaces all variables with their resolved values
* except variables of the form `$target.thing.total` become `thing.total`
*/
function crystalizeVariables({propList, actionContext}){
function crystalizeVariables({ propList, actionContext }) {
propList.forEach(prop => {
if (prop._skipCrystalize) {
delete prop._skipCrystalize;
return;
}
computedSchemas[prop.type].computedFields().forEach( calcKey => {
// Iterate through all the calculations and crystalize them
computedSchemas[prop.type].computedFields().forEach(calcKey => {
applyFnToKey(prop, calcKey, (prop, key) => {
const calcObj = get(prop, key);
if (!calcObj?.parseNode) return;
@@ -106,12 +116,12 @@ function crystalizeVariables({propList, actionContext}){
node.parseType !== 'accessor' && node.parseType !== 'symbol'
) return node;
// Handle variables
if (node.name === '$target'){
if (node.name === '$target') {
// strip $target
if (node.parseType === 'accessor'){
if (node.parseType === 'accessor') {
node.name = node.path.shift();
if (!node.path.length){
return symbol.create({name: node.name})
if (!node.path.length) {
return symbol.create({ name: node.name })
}
} else {
// Can't strip symbols
@@ -123,7 +133,7 @@ function crystalizeVariables({propList, actionContext}){
return node;
} else {
// Resolve all other variables
const {result, context} = resolve('reduce', node, actionContext.scope);
const { result, context } = resolve('reduce', node, actionContext.scope);
logErrors(context.errors, actionContext);
return result;
}
@@ -132,5 +142,36 @@ function crystalizeVariables({propList, actionContext}){
calcObj.hash = cyrb53(calcObj.calculation);
});
});
// For each key in the schema
computedSchemas[prop.type].inlineCalculationFields().forEach(calcKey => {
// That ends in .inlineCalculations
applyFnToKey(prop, calcKey, (prop, key) => {
const inlineCalcObj = get(prop, key);
if (!inlineCalcObj) return;
// If there is no text, skip
if (!inlineCalcObj.text) {
return;
}
// Replace all the existing calculations
let index = -1;
inlineCalcObj.text = inlineCalcObj.text.replace(INLINE_CALCULATION_REGEX, () => {
index += 1;
return `{${inlineCalcObj.inlineCalculations[index].calculation}}`;
});
// Set the value to the uncomputed string
inlineCalcObj.value = inlineCalcObj.text;
// Write a new hash
const inlineCalcHash = cyrb53(inlineCalcObj.text);
if (inlineCalcHash === inlineCalcObj.hash) {
// Skip if nothing changed
return;
}
inlineCalcObj.hash = inlineCalcHash;
});
});
});
}

View File

@@ -13,7 +13,7 @@ export default function applyBuffRemover(node, actionContext) {
const prop = node.node;
// Log Name
if (prop.name){
if (prop.name && !prop.silent){
actionContext.addLog({ name: prop.name });
}
@@ -29,7 +29,7 @@ export default function applyBuffRemover(node, actionContext) {
});
return;
}
removeBuff(nearestBuff, actionContext);
removeBuff(nearestBuff, actionContext, prop);
} else {
// Get all the buffs targeted by tags
const allBuffs = getPropertiesOfType(actionContext.creature._id, 'buff');
@@ -41,7 +41,7 @@ export default function applyBuffRemover(node, actionContext) {
if (prop.removeAll) {
// Remove all matching buffs
targetedBuffs.forEach(buff => {
removeBuff(buff, actionContext);
removeBuff(buff, actionContext, prop);
});
} else {
// Sort in reverse order
@@ -49,7 +49,7 @@ export default function applyBuffRemover(node, actionContext) {
// Remove the one with the highest order
const buff = targetedBuffs[0];
if (buff) {
removeBuff(buff, actionContext);
removeBuff(buff, actionContext, prop);
}
}
}
@@ -60,8 +60,8 @@ export default function applyBuffRemover(node, actionContext) {
node.children.forEach(child => applyProperty(child, actionContext));
}
function removeBuff(buff, actionContext) {
actionContext.addLog({
function removeBuff(buff, actionContext, prop) {
if (!prop.silent) actionContext.addLog({
name: 'Removed',
value: `${buff.name || 'Buff'}`
});

View File

@@ -1,6 +1,6 @@
import { some, intersection, difference, remove } from 'lodash';
import { some, intersection, difference, remove, includes } from 'lodash';
import applyProperty from '../applyProperty.js';
import {insertCreatureLog} from '/imports/api/creature/log/CreatureLogs.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';
@@ -9,10 +9,11 @@ import {
getPropertiesOfType
} from '/imports/api/engine/loadCreatures.js';
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
import getEffectivePropTags from '/imports/api/engine/computation/utility/getEffectivePropTags.js';
export default function applyDamage(node, actionContext){
export default function applyDamage(node, actionContext) {
applyNodeTriggers(node, 'before', actionContext);
const applyChildren = function(){
const applyChildren = function () {
applyNodeTriggers(node, 'after', actionContext);
node.children.forEach(child => applyProperty(child, actionContext));
};
@@ -28,10 +29,10 @@ export default function applyDamage(node, actionContext){
// 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},
options: { doubleRolls: criticalHit },
});
// Gather all the lines we need to log into an array
@@ -40,8 +41,8 @@ export default function applyDamage(node, actionContext){
// roll the dice only and store that string
applyEffectsToCalculationParseNode(prop.amount, actionContext.log);
const {result: rolled} = resolve('roll', prop.amount.parseNode, scope, context);
if (rolled.parseType !== 'constant'){
const { result: rolled } = resolve('roll', prop.amount.parseNode, scope, context);
if (rolled.parseType !== 'constant') {
logValue.push(toString(rolled));
}
logErrors(context.errors, actionContext);
@@ -50,13 +51,13 @@ export default function applyDamage(node, actionContext){
context.errors = [];
// Resolve the roll to a final value
const {result: reduced} = resolve('reduce', rolled, scope, context);
const { result: reduced } = resolve('reduce', rolled, scope, context);
logErrors(context.errors, actionContext);
// Store the result
if (reduced.parseType === 'constant'){
if (reduced.parseType === 'constant') {
prop.amount.value = reduced.value;
} else if (reduced.parseType === 'error'){
} else if (reduced.parseType === 'error') {
prop.amount.value = null;
} else {
prop.amount.value = toString(reduced);
@@ -64,7 +65,7 @@ export default function applyDamage(node, actionContext){
let damage = +reduced.value;
// If we didn't end up with a constant of finite amount, give up
if (reduced?.parseType !== 'constant' || !isFinite(reduced.value)){
if (reduced?.parseType !== 'constant' || !isFinite(reduced.value)) {
return applyChildren();
}
@@ -83,7 +84,7 @@ export default function applyDamage(node, actionContext){
// Memoise the damage suffix for the log
let suffix = (criticalHit ? ' critical ' : ' ') +
prop.damageType +
(prop.damageType !== 'healing' ? ' damage ': '');
(prop.damageType !== 'healing' ? ' damage ' : '');
if (damageTargets && damageTargets.length) {
// Iterate through all the targets
@@ -107,7 +108,7 @@ export default function applyDamage(node, actionContext){
});
// Log the damage done
if (target._id === actionContext.creature._id){
if (target._id === actionContext.creature._id) {
// Target is same as self, log damage as such
logValue.push(`**${damageDealt}** ${suffix} to self`);
} else {
@@ -128,7 +129,7 @@ export default function applyDamage(node, actionContext){
// There are no targets, just log the result
logValue.push(`**${damage}** ${suffix}`);
}
actionContext.addLog({
if (!prop.silent) actionContext.addLog({
name: logName,
value: logValue.join('\n'),
inline: true,
@@ -136,33 +137,33 @@ export default function applyDamage(node, actionContext){
return applyChildren();
}
function applyDamageMultipliers({target, damage, damageProp, logValue}){
function applyDamageMultipliers({ target, damage, damageProp, logValue }) {
const damageType = damageProp?.damageType;
if (!damageType) return damage;
const multiplier = target?.variables?.[damageType];
if (!multiplier) return damage;
const damageTypeText = damageType == 'healing' ? 'healing': `${damageType} damage`;
const damageTypeText = damageType == 'healing' ? 'healing' : `${damageType} damage`;
if (
multiplier.immunity &&
some(multiplier.immunities, multiplierAppliesTo(damageProp))
){
some(multiplier.immunities, multiplierAppliesTo(damageProp, 'immunity'))
) {
logValue.push(`Immune to ${damageTypeText}`);
return 0;
} else {
if (
multiplier.resistance &&
some(multiplier.resistances, multiplierAppliesTo(damageProp))
){
some(multiplier.resistances, multiplierAppliesTo(damageProp, 'resistance'))
) {
logValue.push(`Resistant to ${damageTypeText}`);
damage = Math.floor(damage / 2);
}
if (
multiplier.vulnerability &&
some(multiplier.vulnerabilities, multiplierAppliesTo(damageProp))
){
some(multiplier.vulnerabilities, multiplierAppliesTo(damageProp, 'vulnerability'))
) {
logValue.push(`Vulnerable to ${damageTypeText}`);
damage = Math.floor(damage * 2);
}
@@ -170,21 +171,25 @@ function applyDamageMultipliers({target, damage, damageProp, logValue}){
return damage;
}
function multiplierAppliesTo(damageProp){
function multiplierAppliesTo(damageProp, multiplierType) {
return multiplier => {
// Apply the default 'ignore x' tags
const effectiveTags = getEffectivePropTags(damageProp);
if (includes(effectiveTags, `ignore ${multiplierType}`)) return false;
const hasRequiredTags = difference(
multiplier.includeTags, damageProp.tags
multiplier.includeTags, effectiveTags
).length === 0;
const hasNoExcludedTags = intersection(
multiplier.excludeTags, damageProp.tags
multiplier.excludeTags, effectiveTags
).length === 0;
return hasRequiredTags && hasNoExcludedTags;
}
}
function dealDamage({target, damageType, amount, actionContext}){
function dealDamage({ target, damageType, amount, actionContext }) {
// Get all the health bars and do damage to them
let healthBars = getPropertiesOfType(target._id, 'attribute');
@@ -219,6 +224,16 @@ function dealDamage({target, damageType, amount, actionContext}){
if (damageType === 'healing') damageLeft = -totalDamage;
healthBars.forEach(healthBar => {
if (damageLeft === 0) return;
// Replace the healthbar by the one in the action context if we can
// The damagePropertyWork function bashes the prop with the damage
// So we can use the new value in later action properties
if (healthBar.variableName) {
const targetHealthBar = target.variables[healthBar.variableName];
if (targetHealthBar?._id === healthBar._id) {
healthBar = targetHealthBar;
}
}
// Do the damage
let damageAdded = damagePropertyWork({
prop: healthBar,
operation: 'increment',
@@ -226,6 +241,14 @@ function dealDamage({target, damageType, amount, actionContext}){
actionContext
});
damageLeft -= damageAdded;
// Prevent overflow
if (
damageType === 'healing' ?
healthBar.healthBarNoHealingOverflow :
healthBar.healthBarNoDamageOverflow
) {
damageLeft = 0;
}
});
return totalDamage;
}

View File

@@ -0,0 +1,11 @@
import recalculateInlineCalculations from './shared/recalculateInlineCalculations.js';
import applyProperty from '../applyProperty.js';
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
export default function applyFolder(node, actionContext) {
// Apply triggers
applyNodeTriggers(node, 'before', actionContext);
applyNodeTriggers(node, 'after', actionContext);
// Apply children
node.children.forEach(child => applyProperty(child, actionContext));
}

View File

@@ -1,10 +1,11 @@
import rollDice from '/imports/parser/rollDice.js';
import recalculateCalculation from './shared/recalculateCalculation.js';
import applyProperty from '../applyProperty.js';
import numberToSignedString from '/imports/ui/utility/numberToSignedString.js';
import numberToSignedString from '/imports/api/utility/numberToSignedString.js';
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
import { applyUnresolvedEffects } from '/imports/api/engine/actions/doCheck.js';
export default function applySavingThrow(node, actionContext){
export default function applySavingThrow(node, actionContext) {
applyNodeTriggers(node, 'before', actionContext);
const prop = node.node;
@@ -13,14 +14,14 @@ export default function applySavingThrow(node, actionContext){
recalculateCalculation(prop.dc, actionContext);
const dc = (prop.dc?.value);
if (!isFinite(dc)){
if (!isFinite(dc)) {
actionContext.addLog({
name: 'Error',
value: 'Saving throw requires a DC',
});
return node.children.forEach(child => applyProperty(child, actionContext));
}
actionContext.addLog({
if (!prop.silent) actionContext.addLog({
name: prop.name,
value: `DC **${dc}**`,
inline: true,
@@ -29,8 +30,8 @@ export default function applySavingThrow(node, actionContext){
// If there are no save targets, apply all children as if the save both
// succeeeded and failed
if (!saveTargets?.length){
scope['$saveFailed'] = {value: true};
if (!saveTargets?.length) {
scope['$saveFailed'] = { value: true };
scope['$saveSucceeded'] = { value: true };
applyNodeTriggers(node, 'after', actionContext);
return node.children.forEach(child => applyProperty(child, actionContext));
@@ -51,7 +52,7 @@ export default function applySavingThrow(node, actionContext){
const save = target.variables[prop.stat];
if (!save){
if (!save) {
actionContext.addLog({
name: 'Saving throw error',
value: 'No saving throw found: ' + prop.stat,
@@ -59,10 +60,14 @@ export default function applySavingThrow(node, actionContext){
return applyChildren();
}
const rollModifierText = numberToSignedString(save.value, true);
let rollModifierText = numberToSignedString(save.value, true);
let rollModifier = save.value
const { effectBonus, effectString } = applyUnresolvedEffects(save, scope)
rollModifierText += effectString;
rollModifier += effectBonus;
let value, values, resultPrefix;
if (save.advantage === 1){
if (save.advantage === 1) {
const [a, b] = rollDice(2, 20);
if (a >= b) {
value = a;
@@ -71,7 +76,7 @@ export default function applySavingThrow(node, actionContext){
value = b;
resultPrefix = `Advantage\n1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`;
}
} else if (save.advantage === -1){
} else if (save.advantage === -1) {
const [a, b] = rollDice(2, 20);
if (a <= b) {
value = a;
@@ -85,16 +90,16 @@ export default function applySavingThrow(node, actionContext){
value = values[0];
resultPrefix = `1d20 [ ${value} ] ${rollModifierText}`
}
scope['$saveDiceRoll'] = {value};
const result = value + save.value || 0;
scope['$saveRoll'] = {value: result};
scope['$saveDiceRoll'] = { value };
const result = value + rollModifier || 0;
scope['$saveRoll'] = { value: result };
const saveSuccess = result >= dc;
if (saveSuccess){
scope['$saveSucceeded'] = {value: true};
if (saveSuccess) {
scope['$saveSucceeded'] = { value: true };
} else {
scope['$saveFailed'] = {value: true};
scope['$saveFailed'] = { value: true };
}
actionContext.addLog({
if (!prop.silent) actionContext.addLog({
name: saveSuccess ? 'Successful save' : 'Failed save',
value: resultPrefix + '\n**' + result + '**',
inline: true,

View File

@@ -5,7 +5,7 @@ import logErrors from './logErrors.js';
export default function recalculateCalculation(calc, actionContext, context){
if (!calc?.parseNode) return;
calc._parseLevel = 'reduce';
applyEffectsToCalculationParseNode(calc, actionContext.log);
applyEffectsToCalculationParseNode(calc, actionContext);
evaluateCalculation(calc, actionContext.scope, context);
logErrors(calc.errors, actionContext.log);
logErrors(calc.errors, actionContext);
}

View File

@@ -65,7 +65,7 @@ export function applyTrigger(trigger, prop, actionContext) {
recalculateInlineCalculations(trigger.description, actionContext);
content.value = trigger.description.value;
}
actionContext.addLog(content);
if(!trigger.silent) actionContext.addLog(content);
// Get all the trigger's properties and apply them
const properties = getPropertyDecendants(actionContext.creature._id, trigger._id);

View File

@@ -41,8 +41,8 @@ const doAction = new ValidatedMethod({
let action = CreatureProperties.findOne(actionId);
const creatureId = action.ancestors[0].id;
const actionContext = new ActionContext(creatureId, targetIds, this);
// Check permissions
// Check permissions
assertEditPermission(actionContext.creature, this.userId);
actionContext.targets.forEach(target => {
assertEditPermission(target, this.userId);
@@ -56,13 +56,13 @@ const doAction = new ValidatedMethod({
properties.sort((a, b) => a.order - b.order);
// Do the action
doActionWork({properties, ancestors, actionContext, methodScope: scope});
doActionWork({ properties, ancestors, actionContext, methodScope: scope });
// Recompute all involved creatures
Creatures.update({
_id: { $in: [creatureId, ...targetIds] }
}, {
$set: {dirty: true},
$set: { dirty: true },
});
},
});
@@ -71,11 +71,11 @@ export default doAction;
export function doActionWork({
properties, ancestors, actionContext, methodScope = {},
}){
}) {
// get the docs
const ancestorScope = getAncestorScope(ancestors);
const propertyForest = nodeArrayToTree(properties);
if (propertyForest.length !== 1){
if (propertyForest.length !== 1) {
throw new Meteor.Error(`The action has ${propertyForest.length} top level properties, expected 1`);
}
@@ -91,7 +91,7 @@ export function doActionWork({
}
// Assumes ancestors are in tree order already
function getAncestorScope(ancestors){
function getAncestorScope(ancestors) {
let scope = {};
ancestors.forEach(prop => {
scope[`#${prop.type}`] = prop;

View File

@@ -9,7 +9,6 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
import { doActionWork } from '/imports/api/engine/actions/doAction.js';
import { CreatureLogSchema } from '/imports/api/creature/log/CreatureLogs.js';
import ActionContext from '/imports/api/engine/actions/ActionContext.js';
const doAction = new ValidatedMethod({
@@ -21,6 +20,10 @@ const doAction = new ValidatedMethod({
regEx: SimpleSchema.RegEx.Id,
optional: true,
},
ritual: {
type: Boolean,
optional: true,
},
targetIds: {
type: Array,
defaultValue: [],
@@ -42,13 +45,13 @@ const doAction = new ValidatedMethod({
numRequests: 10,
timeInterval: 5000,
},
run({ spellId, slotId, targetIds = [], scope = {} }) {
// Get action context
run({ spellId, slotId, ritual, targetIds = [], scope = {} }) {
// Get action context
let spell = CreatureProperties.findOne(spellId);
const creatureId = spell.ancestors[0].id;
const actionContext = new ActionContext(creatureId, targetIds, this);
// Check permissions
// Check permissions
assertEditPermission(actionContext.creature, this.userId);
actionContext.targets.forEach(target => {
assertEditPermission(target, this.userId);
@@ -65,27 +68,26 @@ const doAction = new ValidatedMethod({
let slotLevel = spell.level || 0;
let slot;
actionContext.scope['slotLevel'] = slotLevel;
if (slotId && !spell.castWithoutSpellSlots){
// If a spell requires a slot, make sure a slot is spent
if (spell.level && !spell.castWithoutSpellSlots && !(ritual && spell.ritual)) {
slot = CreatureProperties.findOne(slotId);
if (!slot){
if (!slot) {
throw new Meteor.Error('No slot',
'Slot not found to cast spell');
}
if (!slot.value){
if (!slot.value) {
throw new Meteor.Error('No slot',
'Slot depleted');
}
if (slot.attributeType !== 'spellSlot'){
if (slot.attributeType !== 'spellSlot') {
throw new Meteor.Error('Not a slot',
'The given property is not a valid spell slot');
}
if (!slot.spellSlotLevel?.value){
if (!slot.spellSlotLevel?.value) {
throw new Meteor.Error('No slot level',
'Slot does not have a spell slot level');
}
if (slot.spellSlotLevel.value < spell.level){
if (slot.spellSlotLevel.value < spell.level) {
throw new Meteor.Error('Slot too small',
'Slot is not large enough to cast spell');
}
@@ -99,16 +101,24 @@ const doAction = new ValidatedMethod({
}
// Post the slot level spent to the log
if (slot?.spellSlotLevel?.value){
if (slot?.spellSlotLevel?.value) {
actionContext.addLog({
name: `Casting using a level ${slotLevel} spell slot`
});
} else if (slotLevel) {
actionContext.addLog({
name: `Casting at level ${slotLevel}`
});
if (ritual) {
actionContext.addLog({
name: `Ritual casting at level ${slotLevel}`
});
} else {
actionContext.addLog({
name: `Casting at level ${slotLevel}`
});
}
}
actionContext.scope['slotLevel'] = slotLevel;
// Do the action
doActionWork({
properties, ancestors, actionContext, methodScope: scope,

View File

@@ -4,9 +4,10 @@ import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import rollDice from '/imports/parser/rollDice.js';
import numberToSignedString from '/imports/ui/utility/numberToSignedString.js';
import numberToSignedString from '/imports/api/utility/numberToSignedString.js';
import { applyTriggers } from '/imports/api/engine/actions/applyTriggers.js';
import ActionContext from '/imports/api/engine/actions/ActionContext.js';
import evaluateCalculation from '/imports/api/engine/computation/utility/evaluateCalculation.js';
const doCheck = new ValidatedMethod({
name: 'creatureProperties.doCheck',
@@ -22,23 +23,24 @@ const doCheck = new ValidatedMethod({
numRequests: 10,
timeInterval: 5000,
},
run({propId, scope}) {
run({ propId, scope }) {
const prop = CreatureProperties.findOne(propId);
const creatureId = prop.ancestors[0].id;
const actionContext = new ActionContext(creatureId, [creatureId], this);
Object.assign(actionContext.scope, scope);
actionContext.scope[`#${prop.type}`] = prop;
// Check permissions
assertEditPermission(actionContext.creature, this.userId);
// Do the check
doCheckWork({prop, actionContext});
doCheckWork({ prop, actionContext });
},
});
export default doCheck;
export function doCheckWork({prop, actionContext}){
export function doCheckWork({ prop, actionContext }) {
applyTriggers(actionContext.triggers.check?.before, prop, actionContext);
rollCheck(prop, actionContext);
@@ -53,17 +55,17 @@ function rollCheck(prop, actionContext) {
// get the modifier for the roll
let rollModifier;
let logName = `${prop.name} check`;
if (prop.type === 'skill'){
if (prop.type === 'skill') {
rollModifier = prop.value;
if (prop.skillType === 'save'){
if (prop.name.match(/save/i)){
if (prop.skillType === 'save') {
if (prop.name.match(/save/i)) {
logName = prop.name;
} else {
logName = prop.name ? `${prop.name} save` : 'Saving Throw';
}
}
} else if (prop.type === 'attribute'){
if (prop.attributeType === 'ability'){
} else if (prop.type === 'attribute') {
if (prop.attributeType === 'ability') {
rollModifier = prop.modifier;
} else {
rollModifier = prop.value;
@@ -72,10 +74,14 @@ function rollCheck(prop, actionContext) {
throw (`${prop.type} not supported for checks`);
}
const rollModifierText = numberToSignedString(rollModifier, true);
let rollModifierText = numberToSignedString(rollModifier, true);
const { effectBonus, effectString } = applyUnresolvedEffects(prop, scope)
rollModifierText += effectString;
rollModifier += effectBonus;
let value, values, resultPrefix;
if (scope['$checkAdvantage'] === 1){
if (scope['$checkAdvantage'] === 1) {
logName += ' (Advantage)';
const [a, b] = rollDice(2, 20);
if (a >= b) {
@@ -85,7 +91,7 @@ function rollCheck(prop, actionContext) {
value = b;
resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText} = `;
}
} else if (scope['$checkAdvantage'] === -1){
} else if (scope['$checkAdvantage'] === -1) {
logName += ' (Disadvantage)';
const [a, b] = rollDice(2, 20);
if (a <= b) {
@@ -101,8 +107,29 @@ function rollCheck(prop, actionContext) {
resultPrefix = `1d20 [ ${value} ] ${rollModifierText} = `
}
const result = (value + rollModifier) || 0;
scope['$checkDiceRoll'] = value;
scope['$checkRoll'] = result;
scope['$checkModifier'] = rollModifier;
actionContext.addLog({
name: logName,
value: `${resultPrefix} **${result}**`,
});
}
export function applyUnresolvedEffects(prop, scope) {
let effectBonus = 0;
let effectString = '';
if (!prop.effects) {
return { effectBonus, effectString };
}
prop.effects.forEach(effect => {
if (!effect.amount?.parseNode) return;
if (effect.operation !== 'add') return;
effect.amount._parseLevel = 'reduce';
evaluateCalculation(effect.amount, scope);
if (typeof effect.amount?.value !== 'number') return;
effectBonus += effect.amount.value;
effectString += ` ${effect.amount.value < 0 ? '-' : '+'} [${effect.amount.calculation}] ${Math.abs(effect.amount.value)}`
});
return { effectBonus, effectString };
}

View File

@@ -1,15 +1,30 @@
import { EJSON } from 'meteor/ejson';
import createGraph from 'ngraph.graph';
import createGraph, { Graph } from 'ngraph.graph';
import getEffectivePropTags from '/imports/api/engine/computation/utility/getEffectivePropTags.js';
interface CreatureProperty {
_id: string;
type: string;
}
export default class CreatureComputation {
constructor(properties, creature, variables){
originalPropsById: object;
propsById: object;
propsWithTag: object;
scope: object;
props: Array<CreatureProperty>;
dependencyGraph: Graph;
errors: Array<object>;
creature: object;
variables: object;
constructor(properties: Array<CreatureProperty>, creature: object, variables: object) {
// Set up fields
this.originalPropsById = {};
this.propsById = {};
this.originalPropsById = {};
this.propsById = {};
this.propsWithTag = {};
this.scope = {};
this.props = properties;
this.props = properties;
this.dependencyGraph = createGraph();
this.errors = [];
this.creature = creature;

View File

@@ -29,8 +29,8 @@ function childrenActive(prop){
// Children of disabled properties are always inactive
if (prop.disabled) return false;
switch (prop.type){
// Only equipped items have active children
case 'item': return !!prop.equipped;
// Only equipped items with non-zero quantity have active children
case 'item': return !!prop.equipped && prop.quantity !== 0;
// The children of actions, spells, and triggers are always inactive
case 'action': return false;
case 'spell': return false;

View File

@@ -10,8 +10,6 @@ export default function computeToggleDependencies(node, dependencyGraph){
prop.enabled
) return;
walkDown(node.children, child => {
// Only for children that aren't inactive
if (child.node.inactive) return;
// The child nodes depend on the toggle condition compuation
child.node._computationDetails.toggleAncestors.push(prop);
dependencyGraph.addLink(child.node._id, prop._id, 'toggle');

View File

@@ -1,7 +1,7 @@
import findAncestorByType from '/imports/api/engine/computation/utility/findAncestorByType.js';
import { traverse } from '/imports/parser/resolve.js';
export default function linkCalculationDependencies(dependencyGraph, prop, {propsById}){
export default function linkCalculationDependencies(dependencyGraph, prop, { propsById }) {
prop._computationDetails.calculations.forEach(calcObj => {
// Store resolved ancestors
const memo = {
@@ -16,12 +16,13 @@ export default function linkCalculationDependencies(dependencyGraph, prop, {prop
// Skip nodes that aren't symbols or accessors
if (node.parseType !== 'symbol' && node.parseType !== 'accessor') return;
// Link ancestor references as direct property dependencies
if (node.name[0] === '#'){
if (node.name[0] === '#') {
let ancestorProp = getAncestorProp(
node.name.slice(1), memo, prop, propsById
);
if (!ancestorProp) return;
// Link the ancestor prop as a direct dependency
// TODO: we might be referencing a calculation sub-field, depend on that instead
dependencyGraph.addLink(
calcNodeId, ancestorProp._id, 'ancestorReference'
);
@@ -34,16 +35,16 @@ export default function linkCalculationDependencies(dependencyGraph, prop, {prop
});
// Store the resolved ancestors in this calculation's local scope
if (memo.ancestors) {
calcObj._localScope = { ...calcObj._localScope, ...memo.ancestors};
calcObj._localScope = { ...calcObj._localScope, ...memo.ancestors };
}
});
}
function getAncestorProp(type, memo, prop, propsById){
if (memo.ancestors && memo.ancestors['#' + type]){
function getAncestorProp(type, memo, prop, propsById) {
if (memo.ancestors && memo.ancestors['#' + type]) {
return memo.ancestors['#' + type];
} else {
var ancestorProp = findAncestorByType( prop, type, propsById );
var ancestorProp = findAncestorByType(prop, type, propsById);
if (!memo.ancestors) memo.ancestors = {};
memo.ancestors['#' + type] = ancestorProp;
return ancestorProp;

View File

@@ -14,6 +14,7 @@ const linkDependenciesByType = {
effect: linkEffects,
proficiency: linkProficiencies,
roll: linkRoll,
pointBuy: linkPointBuy,
propertySlot: linkSlot,
skill: linkSkill,
spell: linkAction,
@@ -22,23 +23,26 @@ const linkDependenciesByType = {
toggle: linkToggle,
}
export default function linkTypeDependencies(dependencyGraph, prop, computation){
export default function linkTypeDependencies(dependencyGraph, prop, computation) {
linkDependenciesByType[prop.type]?.(dependencyGraph, prop, computation);
}
function dependOnCalc({dependencyGraph, prop, key}){
function dependOnCalc({ dependencyGraph, prop, key }) {
let calc = get(prop, key);
if (!calc) return;
if (calc.type !== '_calculation'){
if (calc.type !== '_calculation') {
throw `Expected calculation got ${calc.type}`
}
dependencyGraph.addLink(prop._id, `${prop._id}.${key}`, 'calculation');
}
function linkAction(dependencyGraph, prop, {propsById}){
function linkAction(dependencyGraph, prop, { propsById }) {
if (prop.variableName) {
dependencyGraph.addLink(prop.variableName, prop._id, 'eventDefinition');
}
// The action depends on its attack roll and uses calculations
dependOnCalc({dependencyGraph, prop, key: 'attackRoll'});
dependOnCalc({dependencyGraph, prop, key: 'uses'});
dependOnCalc({ dependencyGraph, prop, key: 'attackRoll' });
dependOnCalc({ dependencyGraph, prop, key: 'uses' });
// Link the resources the action uses
if (!prop.resources) return;
@@ -46,7 +50,7 @@ function linkAction(dependencyGraph, prop, {propsById}){
prop.resources.itemsConsumed.forEach((itemConsumed, index) => {
if (!itemConsumed.itemId) return;
const item = propsById[itemConsumed.itemId];
if (!item || item.inactive){
if (!item || item.inactive) {
// Unlink if the item doesn't exist or is inactive
itemConsumed.itemId = undefined;
return;
@@ -78,48 +82,48 @@ function linkAction(dependencyGraph, prop, {propsById}){
});
}
function linkAdjustment(dependencyGraph, prop){
function linkAdjustment(dependencyGraph, prop) {
// Adjustment depends on its amount
dependOnCalc({dependencyGraph, prop, key: 'amount'});
dependOnCalc({ dependencyGraph, prop, key: 'amount' });
}
function linkAttribute(dependencyGraph, prop){
function linkAttribute(dependencyGraph, prop) {
linkVariableName(dependencyGraph, prop);
// Depends on spellSlotLevel
dependOnCalc({dependencyGraph, prop, key: 'spellSlotLevel'});
dependOnCalc({ dependencyGraph, prop, key: 'spellSlotLevel' });
// Depends on base value
dependOnCalc({dependencyGraph, prop, key: 'baseValue'});
dependOnCalc({ dependencyGraph, prop, key: 'baseValue' });
// hit dice depend on constitution
if (prop.attributeType === 'hitDice'){
if (prop.attributeType === 'hitDice') {
dependencyGraph.addLink(prop._id, 'constitution', 'hitDiceConMod');
}
}
function linkBranch(dependencyGraph, prop){
dependOnCalc({dependencyGraph, prop, key: 'condition'});
function linkBranch(dependencyGraph, prop) {
dependOnCalc({ dependencyGraph, prop, key: 'condition' });
}
function linkBuff(dependencyGraph, prop){
dependOnCalc({dependencyGraph, prop, key: 'duration'});
function linkBuff(dependencyGraph, prop) {
dependOnCalc({ dependencyGraph, prop, key: 'duration' });
}
function linkClassLevel(dependencyGraph, prop) {
if (prop.inactive) return;
// The variableName of the prop depends on the prop
if (prop.variableName && prop.level){
if (prop.variableName && prop.level) {
dependencyGraph.addLink(prop.variableName, prop._id, 'classLevel');
// The level variable depends on the class variableName variable
let existingLevelLink = dependencyGraph.getLink('level', prop.variableName);
if (!existingLevelLink){
if (!existingLevelLink) {
dependencyGraph.addLink('level', prop.variableName, 'level');
}
}
}
function linkDamage(dependencyGraph, prop){
dependOnCalc({dependencyGraph, prop, key: 'amount'});
function linkDamage(dependencyGraph, prop) {
dependOnCalc({ dependencyGraph, prop, key: 'amount' });
}
function linkEffects(dependencyGraph, prop, computation) {
@@ -131,7 +135,7 @@ function linkEffects(dependencyGraph, prop, computation) {
if (prop.inactive) {
// Inactive effects apply to no stats
return;
} else if (prop.targetByTags){
} else if (prop.targetByTags) {
getEffectTagTargets(prop, computation).forEach(targetId => {
const targetProp = computation.propsById[targetId];
if (
@@ -146,8 +150,8 @@ function linkEffects(dependencyGraph, prop, computation) {
// Otherwise target a field on that property
const key = prop.targetField || getDefaultCalculationField(targetProp);
const calcObj = get(targetProp, key);
if (calcObj && calcObj.calculation){
dependencyGraph.addLink(`${targetProp._id}.${key}`, prop._id , 'effect');
if (calcObj && calcObj.calculation) {
dependencyGraph.addLink(`${targetProp._id}.${key}`, prop._id, 'effect');
}
}
});
@@ -160,14 +164,14 @@ function linkEffects(dependencyGraph, prop, computation) {
}
// Returns an array of IDs of the properties the effect targets
function getEffectTagTargets(effect, computation){
function getEffectTagTargets(effect, computation) {
let targets = getTargetListFromTags(effect.targetTags, computation);
let notIds = [];
if (effect.extraTags){
if (effect.extraTags) {
effect.extraTags.forEach(ex => {
if (ex.operation === 'OR') {
targets = union(targets, getTargetListFromTags(ex.tags, computation));
} else if (ex.operation === 'NOT'){
} else if (ex.operation === 'NOT') {
ex.tags.forEach(tag => {
const idList = computation.propsWithTag[tag];
if (idList) {
@@ -180,7 +184,7 @@ function getEffectTagTargets(effect, computation){
return difference(targets, notIds);
}
function getTargetListFromTags(tags, computation){
function getTargetListFromTags(tags, computation) {
const targetTagIdLists = [];
if (!tags) return [];
tags.forEach(tag => {
@@ -191,8 +195,8 @@ function getTargetListFromTags(tags, computation){
return targets;
}
function getDefaultCalculationField(prop){
switch (prop.type){
function getDefaultCalculationField(prop) {
switch (prop.type) {
case 'action': return 'attackRoll';
case 'adjustment': return 'amount';
case 'attribute': return 'baseValue';
@@ -222,13 +226,13 @@ function getDefaultCalculationField(prop){
}
}
function linkRoll(dependencyGraph, prop){
dependOnCalc({dependencyGraph, prop, key: 'roll'});
function linkRoll(dependencyGraph, prop) {
dependOnCalc({ dependencyGraph, prop, key: 'roll' });
}
function linkVariableName(dependencyGraph, prop){
function linkVariableName(dependencyGraph, prop) {
// The variableName of the prop depends on the prop if the prop is active
if (prop.variableName && !prop.inactive){
if (prop.variableName && !prop.inactive) {
dependencyGraph.addLink(prop.variableName, prop._id, 'definition');
}
}
@@ -242,7 +246,29 @@ function linkDamageMultiplier(dependencyGraph, prop) {
});
}
function linkProficiencies(dependencyGraph, prop){
function linkPointBuy(dependencyGraph, prop) {
dependOnCalc({ dependencyGraph, prop, key: 'min' });
dependOnCalc({ dependencyGraph, prop, key: 'max' });
dependOnCalc({ dependencyGraph, prop, key: 'cost' });
dependOnCalc({ dependencyGraph, prop, key: 'total' });
prop.values?.forEach(row => {
// Wrap the document in a new object so we don't bash it unintentionally
const pointBuyRow = {
...row,
type: 'pointBuyRow',
tableName: prop.name,
tableId: prop._id,
}
dependencyGraph.addNode(row._id, pointBuyRow);
linkVariableName(dependencyGraph, pointBuyRow);
dependOnCalc({ dependencyGraph, pointBuyRow, key: 'row.min' });
dependOnCalc({ dependencyGraph, pointBuyRow, key: 'row.max' });
dependOnCalc({ dependencyGraph, pointBuyRow, key: 'row.cost' });
});
if (prop.inactive) return;
}
function linkProficiencies(dependencyGraph, prop) {
// The stats depend on the proficiency
if (prop.inactive) return;
prop.stats.forEach(statName => {
@@ -251,36 +277,36 @@ function linkProficiencies(dependencyGraph, prop){
});
}
function linkSavingThrow(dependencyGraph, prop){
dependOnCalc({dependencyGraph, prop, key: 'dc'});
function linkSavingThrow(dependencyGraph, prop) {
dependOnCalc({ dependencyGraph, prop, key: 'dc' });
}
function linkSkill(dependencyGraph, prop){
function linkSkill(dependencyGraph, prop) {
// Depends on base value
dependOnCalc({ dependencyGraph, prop, key: 'baseValue' });
// Link dependents
if (prop.inactive) return;
linkVariableName(dependencyGraph, prop);
// The prop depends on the variable references as the ability
if (prop.ability){
if (prop.ability) {
dependencyGraph.addLink(prop._id, prop.ability, 'skillAbilityScore');
}
// Skills depend on the creature's proficiencyBonus
dependencyGraph.addLink(prop._id, 'proficiencyBonus', 'skillProficiencyBonus');
}
function linkSlot(dependencyGraph, prop){
dependOnCalc({dependencyGraph, prop, key: 'quantityExpected'});
dependOnCalc({dependencyGraph, prop, key: 'slotCondition'});
function linkSlot(dependencyGraph, prop) {
dependOnCalc({ dependencyGraph, prop, key: 'quantityExpected' });
dependOnCalc({ dependencyGraph, prop, key: 'slotCondition' });
}
function linkSpellList(dependencyGraph, prop){
dependOnCalc({dependencyGraph, prop, key: 'maxPrepared'});
dependOnCalc({dependencyGraph, prop, key: 'attackRollBonus'});
dependOnCalc({dependencyGraph, prop, key: 'dc'});
function linkSpellList(dependencyGraph, prop) {
dependOnCalc({ dependencyGraph, prop, key: 'maxPrepared' });
dependOnCalc({ dependencyGraph, prop, key: 'attackRollBonus' });
dependOnCalc({ dependencyGraph, prop, key: 'dc' });
}
function linkToggle(dependencyGraph, prop){
function linkToggle(dependencyGraph, prop) {
linkVariableName(dependencyGraph, prop);
dependOnCalc({dependencyGraph, prop, key: 'condition'});
dependOnCalc({ dependencyGraph, prop, key: 'condition' });
}

View File

@@ -12,7 +12,7 @@ import computeToggleDependencies from './buildComputation/computeToggleDependenc
import linkCalculationDependencies from './buildComputation/linkCalculationDependencies.js';
import linkTypeDependencies from './buildComputation/linkTypeDependencies.js';
import computeSlotQuantityFilled from './buildComputation/computeSlotQuantityFilled.js';
import CreatureComputation from './CreatureComputation.js';
import CreatureComputation from './CreatureComputation.ts';
import removeSchemaFields from './buildComputation/removeSchemaFields.js';
/**

View File

@@ -2,8 +2,10 @@ import _variable from './computeByType/computeVariable.js';
import action from './computeByType/computeAction.js';
import attribute from './computeByType/computeAttribute.js';
import skill from './computeByType/computeSkill.js';
import pointBuy from './computeByType/computePointBuy.js';
import propertySlot from './computeByType/computeSlot.js';
import container from './computeByType/computeContainer.js';
import spellList from './computeByType/computeSpellList.js';
import _calculation from './computeByType/computeCalculation.js';
export default Object.freeze({
@@ -13,6 +15,8 @@ export default Object.freeze({
attribute,
container,
skill,
pointBuy,
propertySlot,
spell: action,
spellList,
});

View File

@@ -1,8 +1,8 @@
export default function computeAction(computation, node){
export default function computeAction(computation, node) {
const prop = node.data;
if (prop.uses){
if (prop.uses) {
prop.usesLeft = prop.uses.value - (prop.usesUsed || 0);
if (!prop.usesLeft){
if (!prop.usesLeft) {
prop.insufficientResources = true;
}
}
@@ -10,19 +10,19 @@ export default function computeAction(computation, node){
if (!prop.resources) return;
prop.resources.itemsConsumed.forEach(itemConsumed => {
if (!itemConsumed.itemId) return;
if (itemConsumed.available < itemConsumed.quantity?.value){
if (itemConsumed.available < itemConsumed.quantity?.value) {
prop.insufficientResources = true;
}
});
prop.resources.attributesConsumed.forEach(attConsumed => {
if (!attConsumed.variableName) return;
if (attConsumed.available < attConsumed.quantity?.value){
if (attConsumed.available < attConsumed.quantity?.value) {
prop.insufficientResources = true;
}
});
}
function computeResources(computation, node){
function computeResources(computation, node) {
const resources = node.data?.resources;
if (!resources) return;
resources.attributesConsumed.forEach(attConsumed => {

View File

@@ -0,0 +1,53 @@
import { has } from 'lodash';
import evaluateCalculation from '../../utility/evaluateCalculation.js';
export default function computePointBuy(computation, node) {
const prop = node.data;
const tableMin = prop.min?.value || null;
const tableMax = prop.max?.value || null;
prop.spent = 0;
prop.values?.forEach(row => {
// Clean up added properties
// delete row.tableId;
// delete row.tableName;
// delete row.type;
row.spent = 0;
if (row.value === undefined) return;
const min = has(row, 'min.value') ? row.min.value : tableMin;
const max = has(row, 'max.value') ? row.max.value : tableMax;
const costFunction = EJSON.clone(row.cost || prop.cost);
if (costFunction) costFunction.parseLevel = 'reduce';
// Check min and max
if (min !== null && row.value < min) {
row.value = min;
}
if (max !== null && row.value > max) {
row.value = max;
}
// Evaluate the cost function
if (!costFunction) return;
evaluateCalculation(costFunction, { ...computation.scope, value: row.value });
// Write calculation errors
costFunction.errors?.forEach(error => {
if (error?.message) {
row.errors = row.errors || [];
error.message = 'Cost calculation error.\n' + error.message;
row.errors.push(error);
}
});
if (Number.isFinite(costFunction.value)) {
row.spent = costFunction.value;
prop.spent += costFunction.value;
}
});
prop.pointsLeft = (prop.total?.value || 0) - (prop.spent || 0);
if (prop.spent > prop.total?.value) {
prop.errors = prop.errors || [];
prop.errors.push({
type: 'pointBuyError',
message: 'Spent more than total points available',
});
}
}

View File

@@ -4,7 +4,7 @@
// by computeVariableAsSkill
export default function computeSkill(computation, node){
const prop = node.data;
prop.proficiency = prop.baseProficiency;
prop.proficiency = prop.baseProficiency || 0;
let profBonus = computation.scope['proficiencyBonus']?.value || 0;
// Multiply the proficiency bonus by the actual proficiency
if(prop.proficiency === 0.49){

View File

@@ -0,0 +1,10 @@
export default function computeSpelllist(computation, node) {
const prop = node.data;
const ability = computation.scope[prop.ability];
if (Number.isFinite(ability?.modifier)) {
prop.abilityMod = ability.modifier;
} else if (Number.isFinite(ability?.value)) {
prop.abilityMod = ability.value;
}
}

View File

@@ -7,7 +7,7 @@ import computeVariableAsToggle from './computeVariable/computeVariableAsToggle.j
import computeImplicitVariable from './computeVariable/computeImplicitVariable.js';
import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js';
export default function computeVariable(computation, node){
export default function computeVariable(computation, node) {
const scope = computation.scope;
if (!node.data) node.data = {};
aggregateLinks(computation, node);
@@ -15,7 +15,7 @@ export default function computeVariable(computation, node){
// Don't add to the scope if the node id is not a legitimate variable name
// Without this `some.thing` could break the entire sheet as a database key
if (!VARIABLE_NAME_REGEX.test(node.id)) return;
if (node.data.definingProp){
if (node.data.definingProp) {
// Add the defining variable to the scope
scope[node.id] = node.data.definingProp
} else {
@@ -24,7 +24,7 @@ export default function computeVariable(computation, node){
}
}
function aggregateLinks(computation, node){
function aggregateLinks(computation, node) {
computation.dependencyGraph.forEachLinkedNode(
node.id,
(linkedNode, link) => {
@@ -32,11 +32,12 @@ function aggregateLinks(computation, node){
// Ignore inactive props
if (linkedNode.data.inactive) return;
// Apply all the aggregations
let arg = {node, linkedNode, link, computation};
let arg = { node, linkedNode, link, computation };
aggregate.classLevel(arg);
aggregate.damageMultiplier(arg);
aggregate.definition(arg);
aggregate.effect(arg);
aggregate.eventDefinition(arg);
aggregate.inventory(arg);
aggregate.proficiency(arg);
},
@@ -44,7 +45,7 @@ function aggregateLinks(computation, node){
);
}
function combineAggregations(computation, node){
function combineAggregations(computation, node) {
combineMultiplierAggregator(node);
node.data.overridenProps?.forEach(prop => {
computeVariableProp(computation, node, prop);
@@ -52,51 +53,51 @@ function combineAggregations(computation, node){
computeVariableProp(computation, node, node.data.definingProp);
}
function computeVariableProp(computation, node, prop){
function computeVariableProp(computation, node, prop) {
if (!prop) return;
// Combine damage multipliers in all props so that they can't be overridden
if (node.data.immunity){
if (node.data.immunity) {
prop.immunity = node.data.immunity;
prop.immunities = node.data.immunities;
}
if (node.data.resistance){
if (node.data.resistance) {
prop.resistance = node.data.resistance;
prop.resistances = node.data.resistances;
}
if (node.data.vulnerability){
if (node.data.vulnerability) {
prop.vulnerability = node.data.vulnerability;
prop.vulnerabilities = node.data.vulnerabilities;
}
if (prop.type === 'attribute'){
if (prop.type === 'attribute') {
computeVariableAsAttribute(computation, node, prop);
} else if (prop.type === 'skill'){
} else if (prop.type === 'skill') {
computeVariableAsSkill(computation, node, prop);
} else if (prop.type === 'constant'){
} else if (prop.type === 'constant') {
computeVariableAsConstant(computation, node, prop);
} else if (prop.type === 'class'){
} else if (prop.type === 'class') {
computeVariableAsClass(computation, node, prop);
} else if (prop.type === 'toggle'){
} else if (prop.type === 'toggle') {
computeVariableAsToggle(computation, node, prop);
}
}
function combineMultiplierAggregator(node){
function combineMultiplierAggregator(node) {
// get a reference to the aggregator
const aggregator = node.data.multiplierAggregator;
if (!aggregator) return;
// Combine
if (aggregator.immunities?.length){
if (aggregator.immunities?.length) {
node.data.immunity = true;
node.data.immunities = aggregator.immunities;
}
if (aggregator.resistances?.length){
if (aggregator.resistances?.length) {
node.data.resistance = true;
node.data.resistances = aggregator.resistances;
}
if (aggregator.vulnerabilities?.length){
if (aggregator.vulnerabilities?.length) {
node.data.vulnerability = true;
node.data.vulnerabilities = aggregator.vulnerabilities;
}

View File

@@ -8,7 +8,13 @@ export default function aggregateDefinition({node, linkedNode, link}){
// get current defining prop
const definingProp = node.data.definingProp;
// Find the last defining prop
if (!definingProp || prop.order > definingProp.order){
if (
!definingProp ||
prop.type !== 'pointBuyRow' && (
definingProp.type === 'pointBuyRow' ||
prop.order > definingProp.order
)
) {
// override the current defining prop
overrideProp(definingProp, node);
// set this prop as the new defining prop
@@ -18,9 +24,32 @@ export default function aggregateDefinition({node, linkedNode, link}){
}
// Aggregate the base value due to the defining properties
const propBaseValue = prop.baseValue?.value;
let propBaseValue = prop.baseValue?.value;
// Point buy rows use prop.value instead of prop.baseValue
if (prop.type === 'pointBuyRow') {
propBaseValue = prop.value;
}
if (propBaseValue === undefined) return;
// Store a summary of the definition as a base value effect
node.data.effects = node.data.effects || [];
if (prop.type === 'pointBuyRow') {
node.data.effects.push({
_id: prop.tableId,
name: prop.tableName,
operation: 'base',
amount: { value: propBaseValue },
type: 'pointBuy',
});
} else {
node.data.effects.push({
_id: prop._id,
name: prop.name,
operation: 'base',
amount: { value: propBaseValue },
type: prop.type,
});
}
if (node.data.baseValue === undefined || propBaseValue > node.data.baseValue){
node.data.baseValue = propBaseValue;
}

View File

@@ -1,4 +1,6 @@
export default function aggregateEffect({node, linkedNode, link}){
import { pick } from 'lodash';
export default function aggregateEffect({ node, linkedNode, link }) {
if (link.data !== 'effect') return;
// store the effect aggregator, its presence indicates that the variable is
// targeted by effects
@@ -19,26 +21,39 @@ export default function aggregateEffect({node, linkedNode, link}){
// Store a summary of the effect itself
node.data.effects = node.data.effects || [];
// Store either just
let effectAmount;
if (!linkedNode.data.amount) {
effectAmount = undefined;
} else if (typeof linkedNode.data.amount.value === 'string') {
effectAmount = pick(linkedNode.data.amount, [
'calculation', 'parseNode', 'parseError', 'value'
]);
} else {
effectAmount = pick(linkedNode.data.amount, ['value']);
}
node.data.effects.push({
_id: linkedNode.data._id,
name: linkedNode.data.name,
operation: linkedNode.data.operation,
amount: linkedNode.data.amount && {value: linkedNode.data.amount.value},
amount: effectAmount,
type: linkedNode.data.type,
text: linkedNode.data.text,
// ancestors: linkedNode.data.ancestors,
});
// get a shorter reference to the aggregator document
const aggregator = node.data.effectAggregator;
// Get the result of the effect
const result = linkedNode.data.amount?.value;
// Skip aggregating if the result is not resolved completely
if (typeof result === 'string') return;
let result = linkedNode.data.amount?.value;
if (typeof result !== 'number') result = undefined;
// Aggregate the effect based on its operation
switch(linkedNode.data.operation){
switch (linkedNode.data.operation) {
case 'base':
// Take the largest base value
if (Number.isFinite(result)){
if(Number.isFinite(aggregator.base)){
if (Number.isFinite(result)) {
if (Number.isFinite(aggregator.base)) {
aggregator.base = Math.max(aggregator.base, result);
} else {
aggregator.base = result;

View File

@@ -0,0 +1,22 @@
export default function aggregateEventDefinition({ node, linkedNode, link }) {
// Look at all event definition links
if (link.data !== 'eventDefinition') return;
// Store which property is THE defining event and which are overridden
const prop = linkedNode.data;
// get current defining event
const definingEvent = node.data.definingEvent;
// Find the last defining event
if (
!definingEvent ||
prop.order > definingEvent.order
) {
// override the current defining prop
if (definingEvent) definingEvent.overridden = true;
// set this prop as the new defining prop
node.data.definingEvent = prop;
} else {
prop.overridden = true;
}
}

View File

@@ -1,6 +1,7 @@
import definition from './aggregateDefinition.js';
import damageMultiplier from './aggregateDamageMultiplier.js';
import effect from './aggregateEffect.js';
import eventDefinition from './aggregateEventDefinition.js';
import proficiency from './aggregateProficiency.js';
import classLevel from './aggregateClassLevel.js';
import inventory from './aggregateInventory.js';
@@ -10,6 +11,7 @@ export default Object.freeze({
damageMultiplier,
definition,
effect,
eventDefinition,
inventory,
proficiency,
});

View File

@@ -7,19 +7,19 @@ import getAggregatorResult from './getAggregatorResult.js';
export default function computeImplicitVariable(node){
const prop = {};
// Combine damage multipliers
if (node.data.immunity){
prop.immunity = node.data.immunity;
prop.immunities = node.data.immunities;
}
if (node.data.resistance){
prop.resistance = node.data.resistance;
prop.resistances = node.data.resistances;
}
if (node.data.vulnerability){
prop.vulnerability = node.data.vulnerability;
prop.vulnerabilities = node.data.vulnerabilities;
}
// Combine damage multipliers
if (node.data.immunity){
prop.immunity = node.data.immunity;
prop.immunities = node.data.immunities;
}
if (node.data.resistance){
prop.resistance = node.data.resistance;
prop.resistances = node.data.resistances;
}
if (node.data.vulnerability){
prop.vulnerability = node.data.vulnerability;
prop.vulnerabilities = node.data.vulnerabilities;
}
const result = getAggregatorResult(node);
if (result !== undefined){

View File

@@ -33,6 +33,9 @@ export default function computeVariableAsSkill(computation, node, prop){
const aggregator = node.data.effectAggregator;
const aggregatorBase = aggregator?.base || 0;
// Store effects
prop.effects = node.data.effects;
// If there is no aggregator, determine if the prop can hide, then exit
if (!aggregator){
prop.hide = statBase === undefined &&
@@ -71,8 +74,6 @@ export default function computeVariableAsSkill(computation, node, prop){
prop.fail = aggregator.fail;
// Rollbonus
prop.rollBonuses = aggregator.rollBonus;
// Store effects
prop.effects = node.data.effects;
}
function aggregateAbilityEffects({computation, skillNode, abilityNode}){

View File

@@ -4,7 +4,7 @@ export default function evaluateToggles(computation, node){
let toggles = prop._computationDetails?.toggleAncestors;
if (!toggles) return;
toggles.forEach(toggle => {
if (prop.inactive || !toggle.condition) return;
if (!toggle.condition) return;
if (!toggle.condition.value){
prop.inactive = true;
prop.deactivatedByToggle = true;

View File

@@ -9,8 +9,10 @@ export default function getEffectivePropTags(prop) {
}
// Tags for some string properties
if (prop.variableName) tags.push(prop.variableName);
if (prop.damageType) tags.push(prop.damageType);
if (prop.skillType) tags.push(prop.skillType);
if (prop.actionType) tags.push(prop.actionType);
if (prop.attributeType) tags.push(prop.attributeType);
if (prop.reset) tags.push(prop.reset);
return tags;

View File

@@ -97,7 +97,10 @@ export function getCreature(creatureId) {
if (loadedCreatures.has(creatureId)) {
const loadedCreature = loadedCreatures.get(creatureId);
const creature = loadedCreature.creature;
if (creature) return creature;
if (creature) {
const cloneCreature = EJSON.clone(creature);
return cloneCreature;
}
}
// console.time(`Cache miss on Creature: ${creatureId}`);
const creature = Creatures.findOne(creatureId);
@@ -109,7 +112,10 @@ export function getVariables(creatureId) {
if (loadedCreatures.has(creatureId)) {
const loadedCreature = loadedCreatures.get(creatureId);
const variables = loadedCreature.variables;
if (variables) return variables;
if (variables) {
const cloneVarables = EJSON.clone(variables);
return cloneVarables;
}
}
// console.time(`Cache miss on variables: ${creatureId}`);
const variables = CreatureVariables.findOne({_creatureId: creatureId});

View File

@@ -1,8 +1,13 @@
import { createS3FilesCollection } from '/imports/api/files/s3FileStorage.js';
let createS3FilesCollection;
if (Meteor.isServer) {
createS3FilesCollection = require('/imports/api/files/server/s3FileStorage.js').createS3FilesCollection
} else {
createS3FilesCollection = require('/imports/api/files/client/s3FileStorage.js').createS3FilesCollection
}
const UserImages = createS3FilesCollection({
collectionName: 'userImages',
storagePath: Meteor.isDevelopment ? '/DiceCloud/userImages/' : 'assets/app/userImages',
storagePath: Meteor.isDevelopment ? '../../../../../fileStorage/userImages' : 'assets/app/userImages',
onBeforeUpload(file) {
// Allow upload files under 10MB
if (file.size > 10485760) {

View File

@@ -0,0 +1,24 @@
// https://github.com/VeliovGroup/Meteor-Files/blob/master/docs/aws-s3-integration.md
import { FilesCollection } from 'meteor/ostrio:files';
const createS3FilesCollection = function ({
collectionName,
storagePath,
onBeforeUpload,
onAfterUpload,
debug,// = !Meteor.isProduction,
allowClientCode = false,
}) {
const collection = new FilesCollection({
collectionName,
storagePath,
onBeforeUpload,
onAfterUpload,
debug,
allowClientCode,
});
return collection;
}
export { createS3FilesCollection };

View File

@@ -21,7 +21,7 @@ Meteor.settings.useS3 = !!(
s3Conf && s3Conf.key && s3Conf.secret && s3Conf.bucket && s3Conf.endpoint
);
const bound = Meteor.bindEnvironment((callback) => {
const bound = Meteor.bindEnvironment((callback) => {
return callback();
});
@@ -29,7 +29,7 @@ let createS3FilesCollection;
/* Check settings existence in `Meteor.settings` */
/* This is the best practice for app security */
if (Meteor.isServer && Meteor.settings.useS3) {
if (Meteor.settings.useS3) {
// Create a new S3 object
const s3 = new S3({
accessKeyId: s3Conf.key,
@@ -43,14 +43,14 @@ if (Meteor.isServer && Meteor.settings.useS3) {
}
});
createS3FilesCollection = function({
createS3FilesCollection = function ({
collectionName,
storagePath,
onBeforeUpload,
onAfterUpload,
debug = !Meteor.isProduction,
debug,// = !Meteor.isProduction,
allowClientCode = false,
}){
}) {
const collection = new FilesCollection({
collectionName,
storagePath,
@@ -58,7 +58,7 @@ if (Meteor.isServer && Meteor.settings.useS3) {
onAfterUpload(fileRef) {
// Call the provided afterUpload hook first
onAfterUpload?.(fileRef);
// Start moving files to AWS:S3
// after fully received by the Meteor server
@@ -128,19 +128,19 @@ if (Meteor.isServer && Meteor.settings.useS3) {
};
if (http.request.headers.range) {
const vRef = fileRef.versions[version];
let range = clone(http.request.headers.range);
const vRef = fileRef.versions[version];
let range = clone(http.request.headers.range);
const array = range.split(/bytes=([0-9]*)-([0-9]*)/);
const start = parseInt(array[1]);
let end = parseInt(array[2]);
let end = parseInt(array[2]);
if (isNaN(end)) {
// Request data from AWS:S3 by small chunks
end = (start + this.chunkSize) - 1;
end = (start + this.chunkSize) - 1;
if (end >= vRef.size) {
end = vRef.size - 1;
end = vRef.size - 1;
}
}
opts.Range = `bytes=${start}-${end}`;
opts.Range = `bytes=${start}-${end}`;
http.request.headers.range = `bytes=${start}-${end}`;
}
@@ -198,9 +198,9 @@ if (Meteor.isServer && Meteor.settings.useS3) {
_origRemove.call(this, search);
};
collection.readJSONFile = async function(file){
collection.readJSONFile = async function (file) {
// If there is the pipepath, use s3 to get the file
if (file?.versions?.original?.meta?.pipePath){
if (file?.versions?.original?.meta?.pipePath) {
const path = file.versions.original.meta.pipePath;
const data = await s3.getObject({
Bucket: s3Conf.bucket,
@@ -217,14 +217,14 @@ if (Meteor.isServer && Meteor.settings.useS3) {
return collection;
}
} else {
createS3FilesCollection = function({
createS3FilesCollection = function ({
collectionName,
storagePath,
onBeforeUpload,
onAfterUpload,
debug = !Meteor.isProduction,
debug,// = !Meteor.isProduction,
allowClientCode = false,
}){
}) {
const collection = new FilesCollection({
collectionName,
storagePath,
@@ -234,13 +234,11 @@ if (Meteor.isServer && Meteor.settings.useS3) {
allowClientCode,
});
if (Meteor.isServer) {
// Use the normal file system to read files
collection.readJSONFile = async function(file){
const fileString = await fsp.readFile(file.path, 'utf8');
return JSON.parse(fileString);
};
}
// Use the normal file system to read files
collection.readJSONFile = async function (file) {
const fileString = await fsp.readFile(file.path, 'utf8');
return JSON.parse(fileString);
};
return collection;
}

View File

@@ -7,17 +7,17 @@ import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
let Icons = new Mongo.Collection('icons');
let iconsSchema = new SimpleSchema({
name: {
type: String,
name: {
type: String,
unique: true,
max: STORAGE_LIMITS.name,
index: 1,
},
description: {
type: String,
optional: true,
},
description: {
type: String,
optional: true,
max: STORAGE_LIMITS.description,
},
},
tags: {
type: Array,
optional: true,
@@ -38,7 +38,7 @@ if (Meteor.isServer) {
Icons._ensureIndex({
'name': 'text',
'description': 'text',
'tags': 'text',
'tags': 'text',
});
}
@@ -55,15 +55,15 @@ Icons.attachSchema(iconsSchema);
// This method does not validate icons against the schema, use wisely;
const writeIcons = new ValidatedMethod({
name: 'icons.write',
validate: null,
run(icons){
name: 'icons.write',
validate: null,
run(icons) {
assertAdmin(this.userId);
if (Meteor.isServer){
if (Meteor.isServer) {
this.unblock();
Icons.rawCollection().insert(icons, {ordered: false});
Icons.rawCollection().insert(icons, { ordered: false });
}
}
}
});
const findIcons = new ValidatedMethod({
@@ -80,11 +80,11 @@ const findIcons = new ValidatedMethod({
numRequests: 20,
timeInterval: 10000,
},
run({search}){
run({ search }) {
if (!search) return [];
if (!Meteor.isServer) return;
return Icons.find(
{ $text: {$search: search} },
{ $text: { $search: search } },
{
// relevant documents have a higher score.
fields: {

View File

@@ -20,7 +20,7 @@ import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
let Libraries = new Mongo.Collection('libraries');
let LibrarySchema = new SimpleSchema({
name: {
name: {
type: String,
max: STORAGE_LIMITS.name,
},
@@ -39,95 +39,95 @@ export default Libraries;
const insertLibrary = new ValidatedMethod({
name: 'libraries.insert',
mixins: [
simpleSchemaMixin,
mixins: [
simpleSchemaMixin,
],
schema: LibrarySchema.omit('owner'),
run(library) {
if (!this.userId) {
throw new Meteor.Error('Libraries.methods.insert.denied',
'You need to be logged in to insert a library');
'You need to be logged in to insert a library');
}
let tier = getUserTier(this.userId);
if (!tier.paidBenefits){
if (!tier.paidBenefits) {
throw new Meteor.Error('Libraries.methods.insert.denied',
`The ${tier.name} tier does not allow you to insert a library`);
`The ${tier.name} tier does not allow you to insert a library`);
}
library.owner = this.userId;
return Libraries.insert(library);
return Libraries.insert(library);
},
});
const updateLibraryName = new ValidatedMethod({
name: 'libraries.updateName',
validate: new SimpleSchema({
_id: {
type: String,
regEx: SimpleSchema.RegEx.id
},
name: {
type: String,
},
}).validator(),
name: 'libraries.updateName',
validate: new SimpleSchema({
_id: {
type: String,
regEx: SimpleSchema.RegEx.id
},
name: {
type: String,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({_id, name}){
let library = Libraries.findOne(_id);
assertEditPermission(library, this.userId);
Libraries.update(_id, {$set: {name}});
},
run({ _id, name }) {
let library = Libraries.findOne(_id);
assertEditPermission(library, this.userId);
Libraries.update(_id, { $set: { name } });
},
});
const updateLibraryDescription = new ValidatedMethod({
name: 'libraries.updateDescription',
validate: new SimpleSchema({
_id: {
type: String,
regEx: SimpleSchema.RegEx.id
},
description: {
type: String,
},
}).validator(),
name: 'libraries.updateDescription',
validate: new SimpleSchema({
_id: {
type: String,
regEx: SimpleSchema.RegEx.id
},
description: {
type: String,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({_id, description}){
let library = Libraries.findOne(_id);
assertEditPermission(library, this.userId);
Libraries.update(_id, {$set: {description}});
},
run({ _id, description }) {
let library = Libraries.findOne(_id);
assertEditPermission(library, this.userId);
Libraries.update(_id, { $set: { description } });
},
});
const removeLibrary = new ValidatedMethod({
name: 'libraries.remove',
validate: new SimpleSchema({
_id: {
type: String,
regEx: SimpleSchema.RegEx.id
},
}).validator(),
name: 'libraries.remove',
validate: new SimpleSchema({
_id: {
type: String,
regEx: SimpleSchema.RegEx.id
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({_id}){
let library = Libraries.findOne(_id);
assertOwnership(library, this.userId);
run({ _id }) {
let library = Libraries.findOne(_id);
assertOwnership(library, this.userId);
this.unblock();
removeLibaryWork(_id)
}
}
});
export function removeLibaryWork(libraryId){
export function removeLibaryWork(libraryId) {
Libraries.remove(libraryId);
LibraryNodes.remove({'ancestors.id': libraryId});
LibraryNodes.remove({ 'ancestors.id': libraryId });
}
export { LibrarySchema, insertLibrary, updateLibraryName, updateLibraryDescription, removeLibrary };

View File

@@ -23,28 +23,28 @@ let LibraryNodeSchema = new SimpleSchema({
type: String,
regEx: SimpleSchema.RegEx.Id,
},
type: {
type: {
type: String,
allowedValues: Object.keys(propertySchemasIndex),
},
tags: {
type: Array,
defaultValue: [],
tags: {
type: Array,
defaultValue: [],
maxCount: STORAGE_LIMITS.tagCount,
},
'tags.$': {
type: String,
},
'tags.$': {
type: String,
max: STORAGE_LIMITS.tagLength,
},
},
libraryTags: {
type: Array,
defaultValue: [],
type: Array,
defaultValue: [],
maxCount: STORAGE_LIMITS.tagCount,
},
'libraryTags.$': {
type: String,
},
'libraryTags.$': {
type: String,
max: STORAGE_LIMITS.tagLength,
},
},
icon: {
type: storedIconsSchema,
optional: true,
@@ -56,37 +56,37 @@ let LibraryNodeSchema = new SimpleSchema({
if (Meteor.isServer) {
LibraryNodes._ensureIndex({
'name': 'text',
'tags': 'text',
'tags': 'text',
});
}
for (let key in propertySchemasIndex){
let schema = new SimpleSchema({});
schema.extend(LibraryNodeSchema);
for (let key in propertySchemasIndex) {
let schema = new SimpleSchema({});
schema.extend(LibraryNodeSchema);
schema.extend(ColorSchema);
schema.extend(propertySchemasIndex[key]);
schema.extend(ChildSchema);
schema.extend(SoftRemovableSchema);
LibraryNodes.attachSchema(schema, {
selector: {type: key}
});
schema.extend(propertySchemasIndex[key]);
schema.extend(ChildSchema);
schema.extend(SoftRemovableSchema);
LibraryNodes.attachSchema(schema, {
selector: { type: key }
});
}
function getLibrary(node){
function getLibrary(node) {
if (!node) throw new Meteor.Error('No node provided');
let library = Libraries.findOne(node.ancestors[0].id);
if (!library) throw new Meteor.Error('Library does not exist');
return library;
}
function assertNodeEditPermission(node, userId){
function assertNodeEditPermission(node, userId) {
let lib = getLibrary(node);
return assertEditPermission(lib, userId);
}
const insertNode = new ValidatedMethod({
name: 'libraryNodes.insert',
validate: null,
validate: null,
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
@@ -95,8 +95,8 @@ const insertNode = new ValidatedMethod({
run(libraryNode) {
delete libraryNode._id;
assertNodeEditPermission(libraryNode, this.userId);
let nodeId = LibraryNodes.insert(libraryNode);
if (libraryNode.type == 'reference'){
let nodeId = LibraryNodes.insert(libraryNode);
if (libraryNode.type == 'reference') {
libraryNode._id = nodeId;
updateReferenceNodeWork(libraryNode, this.userId);
}
@@ -106,37 +106,37 @@ const insertNode = new ValidatedMethod({
const updateLibraryNode = new ValidatedMethod({
name: 'libraryNodes.update',
validate({_id, path}){
if (!_id) return false;
// We cannot change these fields with a simple update
switch (path[0]){
case 'type':
validate({ _id, path }) {
if (!_id) return false;
// We cannot change these fields with a simple update
switch (path[0]) {
case 'type':
case 'order':
case 'parent':
case 'ancestors':
return false;
}
return false;
}
},
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({_id, path, value}) {
run({ _id, path, value }) {
let node = LibraryNodes.findOne(_id);
assertNodeEditPermission(node, this.userId);
let pathString = path.join('.');
let modifier;
// unset empty values
if (value === null || value === undefined){
modifier = {$unset: {[pathString]: 1}};
if (value === null || value === undefined) {
modifier = { $unset: { [pathString]: 1 } };
} else {
modifier = {$set: {[pathString]: value}};
modifier = { $set: { [pathString]: value } };
}
let numUpdated = LibraryNodes.update(_id, modifier, {
selector: {type: node.type},
});
if (node.type == 'reference'){
let numUpdated = LibraryNodes.update(_id, modifier, {
selector: { type: node.type },
});
if (node.type == 'reference') {
node = LibraryNodes.findOne(_id);
updateReferenceNodeWork(node, this.userId);
}
@@ -145,87 +145,87 @@ const updateLibraryNode = new ValidatedMethod({
});
const pushToLibraryNode = new ValidatedMethod({
name: 'libraryNodes.push',
validate: null,
name: 'libraryNodes.push',
validate: null,
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({_id, path, value}){
let node = LibraryNodes.findOne(_id);
run({ _id, path, value }) {
let node = LibraryNodes.findOne(_id);
assertNodeEditPermission(node, this.userId);
return LibraryNodes.update(_id, {
$push: {[path.join('.')]: value},
}, {
selector: {type: node.type},
});
}
return LibraryNodes.update(_id, {
$push: { [path.join('.')]: value },
}, {
selector: { type: node.type },
});
}
});
const pullFromLibraryNode = new ValidatedMethod({
name: 'libraryNodes.pull',
validate: null,
name: 'libraryNodes.pull',
validate: null,
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({_id, path, itemId}){
let node = LibraryNodes.findOne(_id);
run({ _id, path, itemId }) {
let node = LibraryNodes.findOne(_id);
assertNodeEditPermission(node, this.userId);
return LibraryNodes.update(_id, {
$pull: {[path.join('.')]: {_id: itemId}},
}, {
selector: {type: node.type},
getAutoValues: false,
});
}
return LibraryNodes.update(_id, {
$pull: { [path.join('.')]: { _id: itemId } },
}, {
selector: { type: node.type },
getAutoValues: false,
});
}
});
const softRemoveLibraryNode = new ValidatedMethod({
name: 'libraryNodes.softRemove',
validate: new SimpleSchema({
_id: SimpleSchema.RegEx.Id
}).validator(),
name: 'libraryNodes.softRemove',
validate: new SimpleSchema({
_id: SimpleSchema.RegEx.Id
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({_id}){
let node = LibraryNodes.findOne(_id);
run({ _id }) {
let node = LibraryNodes.findOne(_id);
assertNodeEditPermission(node, this.userId);
softRemove({_id, collection: LibraryNodes});
}
softRemove({ _id, collection: LibraryNodes });
}
});
const restoreLibraryNode = new ValidatedMethod({
name: 'libraryNodes.restore',
validate: new SimpleSchema({
_id: SimpleSchema.RegEx.Id
}).validator(),
name: 'libraryNodes.restore',
validate: new SimpleSchema({
_id: SimpleSchema.RegEx.Id
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({_id}){
run({ _id }) {
// Permissions
let node = LibraryNodes.findOne(_id);
assertNodeEditPermission(node, this.userId);
// Do work
restore({_id, collection: LibraryNodes});
}
restore({ _id, collection: LibraryNodes });
}
});
export default LibraryNodes;
export {
LibraryNodeSchema,
insertNode,
updateLibraryNode,
pullFromLibraryNode,
pushToLibraryNode,
softRemoveLibraryNode,
LibraryNodeSchema,
insertNode,
updateLibraryNode,
pullFromLibraryNode,
pushToLibraryNode,
softRemoveLibraryNode,
restoreLibraryNode,
};

View File

@@ -0,0 +1,97 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import SimpleSchema from 'simpl-schema';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import { RefSchema } from '/imports/api/parenting/ChildSchema.js';
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
import {
assertDocCopyPermission,
assertDocEditPermission
} from '/imports/api/sharing/sharingPermissions.js';
import {
setLineageOfDocs,
renewDocIds
} from '/imports/api/parenting/parenting.js';
import { reorderDocs } from '/imports/api/parenting/order.js';
import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
var snackbar;
if (Meteor.isClient) {
snackbar = require(
'/imports/client/ui/components/snackbars/SnackbarQueue.js'
).snackbar
}
const DUPLICATE_CHILDREN_LIMIT = 500;
const copyLibraryNodeTo = new ValidatedMethod({
name: 'libraryNodes.copyTo',
validate: new SimpleSchema({
_id: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
parent: {
type: RefSchema,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 1,
timeInterval: 10000,
},
run({ _id, parent }) {
if (parent.collection !== 'libraryNodes' && parent.collection !== 'libraries') {
throw new Meteor.Error('Invalid destination',
'Library documents can only be copied to destinations inside other libraries'
);
}
const libraryNode = LibraryNodes.findOne(_id);
const parentDoc = fetchDocByRef(parent);
assertDocCopyPermission(libraryNode, this.userId);
assertDocEditPermission(parentDoc, this.userId);
let decendants = LibraryNodes.find({
'ancestors.id': _id,
removed: { $ne: true },
}, {
limit: DUPLICATE_CHILDREN_LIMIT + 1,
sort: { order: 1 },
}).fetch();
if (decendants.length > DUPLICATE_CHILDREN_LIMIT) {
decendants.pop();
if (Meteor.isClient) {
snackbar({
text: `Only the first ${DUPLICATE_CHILDREN_LIMIT} children were duplicated`,
});
}
}
const nodes = [libraryNode, ...decendants];
const newAncestry = parentDoc.ancestors || [];
newAncestry.push(parent);
// re-map all the ancestors
setLineageOfDocs({
docArray: nodes,
newAncestry,
oldParent: libraryNode.parent,
});
// Give the docs new IDs without breaking internal references
renewDocIds({ docArray: nodes });
// Order the root node
libraryNode.order = (parentDoc.order || 0) + 0.5;
LibraryNodes.batchInsert(nodes);
// Tree structure changed by inserts, reorder the tree
reorderDocs({
collection: LibraryNodes,
ancestorId: parent.collection === 'libraries' ? parent.id : parentDoc.ancestors[0].id,
});
},
});
export default copyLibraryNodeTo;

View File

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

View File

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

View File

@@ -22,7 +22,7 @@ let ChildSchema = new SimpleSchema({
order: {
type: Number,
},
parent: {
parent: {
type: RefSchema,
optional: true,
},

View File

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

View File

@@ -1,10 +1,10 @@
import { union, difference, sortBy, findLast } from 'lodash';
export function nodeArrayToTree(nodes){
export function nodeArrayToTree(nodes) {
// Store a dict and list of all the nodes
let nodeIndex = {};
let nodeList = [];
nodes.forEach( node => {
nodes.forEach(node => {
let treeNode = {
node: node,
children: [],
@@ -20,7 +20,7 @@ export function nodeArrayToTree(nodes){
treeNode.node.ancestors,
ancestor => !!nodeIndex[ancestor.id]
);
if (ancestorInForest){
if (ancestorInForest) {
nodeIndex[ancestorInForest.id].children.push(treeNode);
} else {
forest.push(treeNode);
@@ -33,13 +33,13 @@ export function nodeArrayToTree(nodes){
export default function nodesToTree({
collection, ancestorId, filter, options = {},
includeFilteredDocAncestors = false, includeFilteredDocDescendants = false
}){
}) {
// Setup the filter
let collectionFilter = {
'ancestors.id': ancestorId,
'removed': {$ne: true},
'removed': { $ne: true },
};
if (filter){
if (filter) {
collectionFilter = {
...collectionFilter,
...filter,
@@ -49,7 +49,7 @@ export default function nodesToTree({
let collectionSort = {
order: 1
};
if (options && options.sort){
if (options && options.sort) {
collectionSort = {
...collectionSort,
...options.sort,
@@ -58,7 +58,7 @@ export default function nodesToTree({
let collectionOptions = {
sort: collectionSort,
}
if (options){
if (options) {
collectionOptions = {
...collectionOptions,
...options,
@@ -74,10 +74,10 @@ export default function nodesToTree({
let ancestors = [];
let ancestorIds = [];
let docIds = [];
if (filter && (includeFilteredDocAncestors || includeFilteredDocDescendants)){
if (filter && (includeFilteredDocAncestors || includeFilteredDocDescendants)) {
docIds = docs.map(doc => doc._id)
}
if (filter && includeFilteredDocAncestors){
if (filter && includeFilteredDocAncestors) {
// Add all ancestor ids to an array
docs.forEach(doc => {
ancestorIds = union(ancestorIds, doc.ancestors.map(ref => ref.id));
@@ -86,19 +86,19 @@ export default function nodesToTree({
ancestorIds = difference(ancestorIds, docIds);
// Get the docs from the collection, don't worry about `removed` docs,
// if their descendant was not removed, neither are they
ancestors = collection.find({_id: {$in: ancestorIds}}).map(doc => {
ancestors = collection.find({ _id: { $in: ancestorIds } }).map(doc => {
// Mark that the nodes are ancestors of the found nodes
doc._ancestorOfMatchedDocument = true;
return doc;
});
}
let descendants = [];
if (filter && includeFilteredDocDescendants){
if (filter && includeFilteredDocDescendants) {
let exludeIds = union(ancestorIds, docIds);
descendants = collection.find({
'_id': {$nin: exludeIds},
'ancestors.id': {$in: docIds},
'removed': {$ne: true},
'_id': { $nin: exludeIds },
'ancestors.id': { $in: docIds },
'removed': { $ne: true },
}).map(doc => {
// Mark that the nodes are descendants of the found nodes
doc._descendantOfMatchedDocument = true;

View File

@@ -2,6 +2,7 @@ import SimpleSchema from 'simpl-schema';
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
import { storedIconsSchema } from '/imports/api/icons/Icons.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js';
/*
* Actions are things a character can do
@@ -24,9 +25,17 @@ let ActionSchema = createPropertySchema({
// long actions take longer than 1 round to cast
actionType: {
type: String,
allowedValues: ['action', 'bonus', 'attack', 'reaction', 'free', 'long'],
allowedValues: ['action', 'bonus', 'attack', 'reaction', 'free', 'long', 'event'],
defaultValue: 'action',
},
// If the action type is an event, what is the variable name of that event?
variableName: {
type: String,
optional: true,
regEx: VARIABLE_NAME_REGEX,
min: 2,
max: STORAGE_LIMITS.variableName,
},
// Who is the action directed at
target: {
type: String,
@@ -56,8 +65,10 @@ let ActionSchema = createPropertySchema({
// How this action's uses are reset automatically
reset: {
type: String,
allowedValues: ['longRest', 'shortRest'],
optional: true,
regEx: VARIABLE_NAME_REGEX,
min: 2,
max: STORAGE_LIMITS.variableName,
},
// Resources
resources: {
@@ -74,7 +85,7 @@ let ActionSchema = createPropertySchema({
'resources.itemsConsumed.$._id': {
type: String,
regEx: SimpleSchema.RegEx.Id,
autoValue(){
autoValue() {
if (!this.isSet) return Random.id();
}
},
@@ -101,7 +112,7 @@ let ActionSchema = createPropertySchema({
'resources.attributesConsumed.$._id': {
type: String,
regEx: SimpleSchema.RegEx.Id,
autoValue(){
autoValue() {
if (!this.isSet) return Random.id();
}
},
@@ -114,6 +125,11 @@ let ActionSchema = createPropertySchema({
type: 'fieldToCompute',
optional: true,
},
// Prevent the property from showing up in the log
silent: {
type: Boolean,
optional: true,
},
});
const ComputedOnlyActionSchema = createPropertySchema({
@@ -146,6 +162,12 @@ const ComputedOnlyActionSchema = createPropertySchema({
optional: true,
removeBeforeCompute: true,
},
// Denormalised tag if event is overridden by one with the same variable name
overridden: {
type: Boolean,
optional: true,
removeBeforeCompute: true,
},
// Resources
resources: {
type: Object,
@@ -213,4 +235,4 @@ const ComputedActionSchema = new SimpleSchema()
.extend(ActionSchema)
.extend(ComputedOnlyActionSchema);
export { ActionSchema, ComputedOnlyActionSchema, ComputedActionSchema};
export { ActionSchema, ComputedOnlyActionSchema, ComputedActionSchema };

View File

@@ -3,7 +3,7 @@ import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
const AdjustmentSchema = createPropertySchema({
// The roll that determines how much to change the attribute
// The roll that determines how much to change the attribute
// This can be simplified, but should only compute when activated
amount: {
type: 'fieldToCompute',
@@ -11,26 +11,31 @@ const AdjustmentSchema = createPropertySchema({
optional: true,
defaultValue: 1,
},
// Who this adjustment applies to
target: {
type: String,
// Who this adjustment applies to
target: {
type: String,
defaultValue: 'target',
allowedValues: [
allowedValues: [
'self',
'target',
],
},
// The stat this rolls applies to
stat: {
type: String,
},
// The stat this rolls applies to
stat: {
type: String,
optional: true,
max: STORAGE_LIMITS.variableName,
},
},
operation: {
type: String,
allowedValues: ['set', 'increment'],
defaultValue: 'increment',
},
// Prevent the property from showing up in the log
silent: {
type: Boolean,
optional: true,
},
});
const ComputedOnlyAdjustmentSchema = createPropertySchema({

View File

@@ -8,34 +8,33 @@ import createPropertySchema from '/imports/api/properties/subSchemas/createPrope
*/
let AttributeSchema = createPropertySchema({
name: {
type: String,
type: String,
optional: true,
max: STORAGE_LIMITS.name,
},
},
// The technical, lowercase, single-word name used in formulae
variableName: {
type: String,
optional: true,
regEx: VARIABLE_NAME_REGEX,
regEx: VARIABLE_NAME_REGEX,
min: 2,
max: STORAGE_LIMITS.variableName,
},
// How it is displayed and computed is determined by type
// How it is displayed and computed is determined by type
attributeType: {
type: String,
allowedValues: [
'ability', //Strength, Dex, Con, etc.
'stat', // Speed, Armor Class
'modifier', // Proficiency Bonus, displayed as +x
'modifier', // Proficiency Bonus, displayed as +x
'hitDice', // d12 hit dice
'healthBar', // Hitpoints, Temporary Hitpoints, can take damage
'bar', // Displayed as a health bar, can't take damage
'healthBar', // Hitpoints, Temporary Hitpoints
'resource', // Rages, sorcery points
'spellSlot', // Level 1, 2, 3... spell slots
'utility', // Aren't displayed, Jump height, Carry capacity
],
defaultValue: 'stat',
index: 1,
index: 1,
},
// For type hitDice, the size needs to be stored separately
hitDiceSize: {
@@ -46,19 +45,19 @@ let AttributeSchema = createPropertySchema({
// For type spellSlot, the level needs to be stored separately
spellSlotLevel: {
type: 'fieldToCompute',
optional: true,
optional: true,
},
// For type healthBar midColor, and lowColor can be set separately from the
// property's color, which is used as the undamaged color
'healthBarColorMid': {
type: String,
regEx: /^#([a-f0-9]{3}){1,2}\b$/i,
optional: true,
type: String,
regEx: /^#([a-f0-9]{3}){1,2}\b$/i,
optional: true,
},
'healthBarColorLow': {
type: String,
regEx: /^#([a-f0-9]{3}){1,2}\b$/i,
optional: true,
type: String,
regEx: /^#([a-f0-9]{3}){1,2}\b$/i,
optional: true,
},
// Control how the health bar takes damage or healing
healthBarNoDamage: {
@@ -68,7 +67,17 @@ let AttributeSchema = createPropertySchema({
healthBarNoHealing: {
type: Boolean,
optional: true,
},
},
// Control how the health bar handles overflow
healthBarNoDamageOverflow: {
type: Boolean,
optional: true,
},
healthBarNoHealingOverflow: {
type: Boolean,
optional: true,
},
// Control when the health bar takes damage or healing
healthBarDamageOrder: {
type: SimpleSchema.Integer,
optional: true,
@@ -77,17 +86,17 @@ let AttributeSchema = createPropertySchema({
type: SimpleSchema.Integer,
optional: true,
},
// The starting value, before effects
baseValue: {
// The starting value, before effects
baseValue: {
type: 'fieldToCompute',
optional: true,
},
optional: true,
},
// Description of what the attribute is used for
description: {
type: 'inlineCalculationFieldToCompute',
optional: true,
},
// The damage done to the attribute, should always compute as positive
optional: true,
},
// The damage done to the attribute, should always compute as positive
damage: {
type: SimpleSchema.Integer,
optional: true,
@@ -107,11 +116,21 @@ let AttributeSchema = createPropertySchema({
type: Boolean,
optional: true,
},
// Automatically zero the adjustment on these conditions
hideWhenTotalZero: {
type: Boolean,
optional: true,
},
hideWhenValueZero: {
type: Boolean,
optional: true,
},
// Automatically zero the adjustment on these conditions
reset: {
type: String,
optional: true,
allowedValues: ['shortRest', 'longRest'],
regEx: VARIABLE_NAME_REGEX,
min: 2,
max: STORAGE_LIMITS.variableName,
},
});
@@ -126,9 +145,9 @@ let ComputedOnlyAttributeSchema = createPropertySchema({
},
spellSlotLevel: {
type: 'computedOnlyField',
optional: true,
optional: true,
},
// The computed value of the attribute
// The computed value of the attribute
total: {
type: SimpleSchema.oneOf(Number, String, Boolean),
optional: true,
@@ -137,27 +156,27 @@ let ComputedOnlyAttributeSchema = createPropertySchema({
// The computed value of the attribute minus the damage
value: {
type: SimpleSchema.oneOf(Number, String, Boolean),
defaultValue: 0,
defaultValue: 0,
optional: true,
removeBeforeCompute: true,
},
// The computed modifier, provided the attribute type is `ability`
modifier: {
type: SimpleSchema.Integer,
optional: true,
// The computed modifier, provided the attribute type is `ability`
modifier: {
type: SimpleSchema.Integer,
optional: true,
removeBeforeCompute: true,
},
},
// Attributes with proficiency grant it to all skills based on the attribute
proficiency: {
type: Number,
type: Number,
allowedValues: [0, 0.49, 0.5, 1, 2],
optional: true,
optional: true,
removeBeforeCompute: true,
},
},
// The computed creature constitution modifier for hit dice
constitutionMod: {
type: Number,
optional: true,
optional: true,
removeBeforeCompute: true,
},
// Should this attribute hide
@@ -176,6 +195,7 @@ let ComputedOnlyAttributeSchema = createPropertySchema({
effects: {
type: Array,
optional: true,
removeBeforeCompute: true,
},
'effects.$': {
type: Object,

View File

@@ -3,8 +3,8 @@ import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
let BranchSchema = createPropertySchema({
branchType: {
type: String,
branchType: {
type: String,
allowedValues: [
// Uses the condition field to determine whether to apply children
'if',
@@ -26,7 +26,7 @@ let BranchSchema = createPropertySchema({
//'option',
],
defaultValue: 'if',
},
},
text: {
type: String,
optional: true,
@@ -37,6 +37,11 @@ let BranchSchema = createPropertySchema({
optional: true,
parseLevel: 'compile',
},
// Prevent the property from showing up in the log
silent: {
type: Boolean,
optional: true,
},
});
let ComputedOnlyBranchSchema = createPropertySchema({

View File

@@ -4,31 +4,31 @@ import createPropertySchema from '/imports/api/properties/subSchemas/createPrope
let BuffRemoverSchema = createPropertySchema({
name: {
type: String,
optional: true,
type: String,
optional: true,
max: STORAGE_LIMITS.name,
},
// This will remove just the nearest ancestor buff
targetParentBuff: {
type: Boolean,
optional: true,
},
// The following only applies when not targeting the parent buff
// Which character to remove buffs from
target: {
type: String,
allowedValues: [
},
// This will remove just the nearest ancestor buff
targetParentBuff: {
type: Boolean,
optional: true,
},
// The following only applies when not targeting the parent buff
// Which character to remove buffs from
target: {
type: String,
allowedValues: [
'self',
'target',
],
defaultValue: 'target',
},
// remove 1 or remove all
removeAll: {
type: Boolean,
defaultValue: 'target',
},
// remove 1 or remove all
removeAll: {
type: Boolean,
optional: true,
defaultValue: true,
},
},
// Buffs to remove based on tags:
targetTags: {
type: Array,
@@ -50,7 +50,7 @@ let BuffRemoverSchema = createPropertySchema({
'extraTags.$._id': {
type: String,
regEx: SimpleSchema.RegEx.Id,
autoValue(){
autoValue() {
if (!this.isSet) return Random.id();
}
},
@@ -68,6 +68,11 @@ let BuffRemoverSchema = createPropertySchema({
type: String,
max: STORAGE_LIMITS.tagLength,
},
// Prevent the property from showing up in the log
silent: {
type: Boolean,
optional: true,
},
});
let ComputedOnlyBuffRemoverSchema = createPropertySchema({});

View File

@@ -4,64 +4,74 @@ import createPropertySchema from '/imports/api/properties/subSchemas/createPrope
let BuffSchema = createPropertySchema({
name: {
type: String,
optional: true,
type: String,
optional: true,
max: STORAGE_LIMITS.name,
},
description: {
type: 'inlineCalculationFieldToCompute',
optional: true,
},
hideRemoveButton: {
type: Boolean,
optional: true,
},
},
description: {
type: 'inlineCalculationFieldToCompute',
optional: true,
},
hideRemoveButton: {
type: Boolean,
optional: true,
},
// How many rounds this buff lasts
duration: {
type: 'fieldToCompute',
optional: true,
},
duration: {
type: 'fieldToCompute',
optional: true,
},
target: {
type: String,
allowedValues: [
type: String,
allowedValues: [
'self',
'target',
],
defaultValue: 'target',
},
defaultValue: 'target',
},
// Prevent the property from showing up in the log
silent: {
type: Boolean,
optional: true,
},
// Prevent the children from being crystalized
skipCrystalization: {
type: Boolean,
optional: true,
},
});
let ComputedOnlyBuffSchema = createPropertySchema({
description: {
type: 'computedOnlyInlineCalculationField',
optional: true,
max: STORAGE_LIMITS.description,
},
duration: {
type: 'computedOnlyField',
optional: true,
},
durationSpent: {
type: Number,
optional: true,
min: 0,
},
appliedBy: {
type: Object,
description: {
type: 'computedOnlyInlineCalculationField',
optional: true,
},
'appliedBy.name': {
type: String,
max: STORAGE_LIMITS.description,
},
duration: {
type: 'computedOnlyField',
optional: true,
},
durationSpent: {
type: Number,
optional: true,
min: 0,
},
appliedBy: {
type: Object,
optional: true,
},
'appliedBy.name': {
type: String,
max: STORAGE_LIMITS.name,
},
'appliedBy.id': {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
'appliedBy.collection': {
type: String,
},
'appliedBy.id': {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
'appliedBy.collection': {
type: String,
max: STORAGE_LIMITS.collectionName,
},
},
});
const ComputedBuffSchema = new SimpleSchema()

View File

@@ -4,26 +4,26 @@ import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
const ClassLevelSchema = createPropertySchema({
name: {
type: String,
optional: true,
name: {
type: String,
optional: true,
max: STORAGE_LIMITS.name,
},
description: {
type: 'inlineCalculationFieldToCompute',
optional: true,
},
// The name of this class level's variable
variableName: {
},
description: {
type: 'inlineCalculationFieldToCompute',
optional: true,
},
// The name of this class level's variable
variableName: {
type: String,
min: 2,
regEx: VARIABLE_NAME_REGEX,
regEx: VARIABLE_NAME_REGEX,
max: STORAGE_LIMITS.variableName,
optional: true,
},
level: {
level: {
type: SimpleSchema.Integer,
defaultValue: 1,
defaultValue: 1,
max: STORAGE_LIMITS.levelMax,
},
// Filters out of UI if condition isn't met, but isn't otherwise enforced
@@ -34,7 +34,7 @@ const ClassLevelSchema = createPropertySchema({
},
});
const ComputedOnlyClassLevelSchema = createPropertySchema({
const ComputedOnlyClassLevelSchema = createPropertySchema({
description: {
type: 'computedOnlyInlineCalculationField',
optional: true,

View File

@@ -13,29 +13,29 @@ import resolve, { Context, traverse } from '/imports/parser/resolve.js';
*/
let ConstantSchema = new SimpleSchema({
name: {
type: String,
optional: true,
type: String,
optional: true,
max: STORAGE_LIMITS.name,
},
},
// The technical, lowercase, single-word name used in formulae
variableName: {
type: String,
regEx: VARIABLE_NAME_REGEX,
regEx: VARIABLE_NAME_REGEX,
min: 2,
defaultValue: 'newConstant',
max: STORAGE_LIMITS.variableName,
},
// The input value to be parsed, must return a constant node or an array
// The input value to be parsed, must return a constant node or an array
// of constant nodes to be valid
calculation: {
type: String,
optional: true,
calculation: {
type: String,
optional: true,
max: STORAGE_LIMITS.calculation,
},
},
errors: {
type: Array,
maxCount: STORAGE_LIMITS.errorCount,
autoValue(){
autoValue() {
let calc = this.field('calculation');
if (!calc.isSet && this.isModifier) {
this.unset()
@@ -44,27 +44,27 @@ let ConstantSchema = new SimpleSchema({
let string = calc.value;
if (!string) return [];
// Evaluate the calculation with no scope
let {result, context} = parseString(string);
let { result, context } = parseString(string);
// Any existing errors will result in an early failure
if (context && context.errors.length) return context.errors;
// Ban variables in constants if necessary
result && traverse(result, node => {
if (node.parseType === 'symbol' || node.parseType === 'accessor'){
if (node.parseType === 'symbol' || node.parseType === 'accessor') {
context.error('Variables can\'t be used to define a constant');
}
});
return context && context.errors || [];
}
},
'errors.$':{
'errors.$': {
type: ErrorSchema,
},
});
function parseString(string, fn = 'compile'){
function parseString(string, fn = 'compile') {
let context = new Context();
if (!string){
return {result: string, context};
if (!string) {
return { result: string, context };
}
// Parse the string using mathjs
@@ -74,11 +74,11 @@ function parseString(string, fn = 'compile'){
} catch (e) {
let message = prettifyParseError(e);
context.error(message);
return {context};
return { context };
}
if (!node) return {context};
let {result} = resolve(fn, node, {/*empty scope*/}, context);
return {result, context}
if (!node) return { context };
let { result } = resolve(fn, node, {/*empty scope*/ }, context);
return { result, context }
}
const ComputedOnlyConstantSchema = new SimpleSchema({});

View File

@@ -3,61 +3,61 @@ import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
let ContainerSchema = createPropertySchema({
name: {
type: String,
optional: true,
trim: false,
name: {
type: String,
optional: true,
trim: false,
max: STORAGE_LIMITS.name,
},
carried: {
type: Boolean,
defaultValue: true,
optional: true,
},
contentsWeightless: {
type: Boolean,
optional: true,
},
weight: {
type: Number,
min: 0,
},
carried: {
type: Boolean,
defaultValue: true,
optional: true,
},
value: {
type: Number,
min: 0,
},
contentsWeightless: {
type: Boolean,
optional: true,
},
description: {
type: 'inlineCalculationFieldToCompute',
optional: true,
},
},
weight: {
type: Number,
min: 0,
optional: true,
},
value: {
type: Number,
min: 0,
optional: true,
},
description: {
type: 'inlineCalculationFieldToCompute',
optional: true,
},
});
const ComputedOnlyContainerSchema = createPropertySchema({
description: {
type: 'computedOnlyInlineCalculationField',
optional: true,
},
type: 'computedOnlyInlineCalculationField',
optional: true,
},
// Weight of all the contents, zero if `contentsWeightless` is true
contentsWeight:{
contentsWeight: {
type: Number,
optional: true,
removeBeforeCompute: true,
},
// Weight of all the carried contents (some sub-containers might not be carried)
// zero if `contentsWeightless` is true
carriedWeight:{
carriedWeight: {
type: Number,
optional: true,
removeBeforeCompute: true,
},
contentsValue:{
contentsValue: {
type: Number,
optional: true,
removeBeforeCompute: true,
},
carriedValue:{
carriedValue: {
type: Number,
optional: true,
removeBeforeCompute: true,
@@ -65,7 +65,7 @@ const ComputedOnlyContainerSchema = createPropertySchema({
});
const ComputedContainerSchema = new SimpleSchema()
.extend(ComputedOnlyContainerSchema)
.extend(ContainerSchema);
.extend(ComputedOnlyContainerSchema)
.extend(ContainerSchema);
export { ContainerSchema, ComputedOnlyContainerSchema, ComputedContainerSchema };

View File

@@ -8,10 +8,10 @@ import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js';
*/
let DamageMultiplierSchema = new SimpleSchema({
name: {
type: String,
optional: true,
type: String,
optional: true,
max: STORAGE_LIMITS.name,
},
},
damageTypes: {
type: Array,
defaultValue: [],
@@ -23,11 +23,11 @@ let DamageMultiplierSchema = new SimpleSchema({
max: STORAGE_LIMITS.calculation,
regEx: VARIABLE_NAME_REGEX,
},
// The value of the damage multiplier
value: {
// The value of the damage multiplier
value: {
type: Number,
defaultValue: 0.5,
allowedValues: [0, 0.5, 2],
defaultValue: 0.5,
allowedValues: [0, 0.5, 2],
},
// Tags which bypass this multiplier (OR)
excludeTags: {

View File

@@ -4,7 +4,7 @@ import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js';
const DamageSchema = createPropertySchema({
// The roll that determines how much to damage the attribute
// The roll that determines how much to damage the attribute
// This can be simplified, but only computed when applied
amount: {
type: 'fieldToCompute',
@@ -12,21 +12,26 @@ const DamageSchema = createPropertySchema({
defaultValue: '1d8 + strength.modifier',
parseLevel: 'compile',
},
// Who this damage applies to
target: {
type: String,
// Who this damage applies to
target: {
type: String,
defaultValue: 'target',
allowedValues: [
allowedValues: [
'self',
'target',
],
},
damageType: {
type: String,
},
damageType: {
type: String,
max: STORAGE_LIMITS.calculation,
defaultValue: 'slashing',
defaultValue: 'slashing',
regEx: VARIABLE_NAME_REGEX,
},
},
// Prevent the property from showing up in the log
silent: {
type: Boolean,
optional: true,
},
});
const ComputedOnlyDamageSchema = createPropertySchema({

View File

@@ -3,19 +3,19 @@ import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
let FeatureSchema = createPropertySchema({
name: {
type: String,
name: {
type: String,
max: STORAGE_LIMITS.name,
optional: true,
},
summary: {
type: 'inlineCalculationFieldToCompute',
optional: true,
},
},
summary: {
type: 'inlineCalculationFieldToCompute',
optional: true,
},
description: {
type: 'inlineCalculationFieldToCompute',
optional: true,
},
type: 'inlineCalculationFieldToCompute',
optional: true,
},
});
let ComputedOnlyFeatureSchema = createPropertySchema({

View File

@@ -7,6 +7,29 @@ let FolderSchema = new createPropertySchema({
name: {
type: String,
max: STORAGE_LIMITS.name,
optional: true,
},
groupStats: {
type: Boolean,
optional: true,
},
hideStatsGroup: {
type: Boolean,
optional: true,
},
tab: {
type: String,
optional: true,
allowedValues: [
'stats', 'features', 'actions', 'spells', 'inventory', 'journal', 'build'
],
},
location: {
type: String,
optional: true,
allowedValues: [
'start', 'events', 'stats', 'skills', 'proficiencies', 'end'
],
},
});

View File

@@ -3,58 +3,58 @@ import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
const ItemSchema = createPropertySchema({
name: {
type: String,
optional: true,
name: {
type: String,
optional: true,
max: STORAGE_LIMITS.name,
},
// Plural name of the item, if there is more than one
plural: {
type: String,
optional: true,
},
// Plural name of the item, if there is more than one
plural: {
type: String,
optional: true,
max: STORAGE_LIMITS.name,
},
},
description: {
type: 'inlineCalculationFieldToCompute',
optional: true,
},
// Number currently held
quantity: {
type: SimpleSchema.Integer,
min: 0,
defaultValue: 1
},
// Weight per item in the stack
weight: {
type: Number,
min: 0,
type: 'inlineCalculationFieldToCompute',
optional: true,
},
// Value per item in the stack, in gold pieces
value: {
type: Number,
min: 0,
},
// Number currently held
quantity: {
type: SimpleSchema.Integer,
min: 0,
defaultValue: 1
},
// Weight per item in the stack
weight: {
type: Number,
min: 0,
optional: true,
},
// If this item is equipped, it requires attunement
requiresAttunement: {
type: Boolean,
optional: true,
},
},
// Value per item in the stack, in gold pieces
value: {
type: Number,
min: 0,
optional: true,
},
// If this item is equipped, it requires attunement
requiresAttunement: {
type: Boolean,
optional: true,
},
attuned: {
type: Boolean,
optional: true,
},
// Show increment/decrement buttons in item lists
showIncrement: {
type: Boolean,
optional: true,
},
// Unequipped items shouldn't affect creature stats
equipped: {
type: Boolean,
defaultValue: false,
},
type: Boolean,
optional: true,
},
// Show increment/decrement buttons in item lists
showIncrement: {
type: Boolean,
optional: true,
},
// Unequipped items shouldn't affect creature stats
equipped: {
type: Boolean,
defaultValue: false,
},
});
let ComputedOnlyItemSchema = createPropertySchema({

View File

@@ -3,19 +3,19 @@ import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
let NoteSchema = createPropertySchema({
name: {
type: String,
optional: true,
name: {
type: String,
optional: true,
max: STORAGE_LIMITS.name,
},
summary: {
type: 'inlineCalculationFieldToCompute',
optional: true,
},
},
summary: {
type: 'inlineCalculationFieldToCompute',
optional: true,
},
description: {
type: 'inlineCalculationFieldToCompute',
optional: true,
},
type: 'inlineCalculationFieldToCompute',
optional: true,
},
});
let ComputedOnlyNoteSchema = createPropertySchema({

View File

@@ -2,6 +2,7 @@ import SimpleSchema from 'simpl-schema';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js';
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js';
/*
* PointBuys are reason-value attached to skills and abilities
@@ -13,13 +14,6 @@ let PointBuySchema = createPropertySchema({
optional: true,
max: STORAGE_LIMITS.name,
},
variableName: {
type: String,
optional: true,
regEx: VARIABLE_NAME_REGEX,
min: 2,
max: STORAGE_LIMITS.variableName,
},
ignored: {
type: Boolean,
optional: true,
@@ -27,10 +21,18 @@ let PointBuySchema = createPropertySchema({
'values': {
type: Array,
defaultValue: [],
maxCount: STORAGE_LIMITS.pointBuyRowsCount,
},
'values.$': {
type: Object,
},
'values.$._id': {
type: String,
regEx: SimpleSchema.RegEx.Id,
autoValue(){
if (!this.isSet) return Random.id();
}
},
'values.$.name': {
type: String,
optional: true,
@@ -47,6 +49,18 @@ let PointBuySchema = createPropertySchema({
type: Number,
optional: true,
},
'values.$.min': {
type: 'fieldToCompute',
optional: true,
},
'values.$.max': {
type: 'fieldToCompute',
optional: true,
},
'values.$.cost': {
type: 'fieldToCompute',
optional: true,
},
min: {
type: 'fieldToCompute',
optional: true,
@@ -62,6 +76,7 @@ let PointBuySchema = createPropertySchema({
cost: {
type: 'fieldToCompute',
optional: true,
parseLevel: 'compile',
},
});
@@ -74,11 +89,46 @@ const ComputedOnlyPointBuySchema = createPropertySchema({
type: 'computedOnlyField',
optional: true,
},
total: {
cost: {
type: 'computedOnlyField',
optional: true,
parseLevel: 'compile',
},
'values': {
type: Array,
defaultValue: [],
maxCount: STORAGE_LIMITS.pointBuyRowsCount,
},
'values.$': {
type: Object,
},
'values.$.min': {
type: 'computedOnlyField',
optional: true,
},
cost: {
'values.$.max': {
type: 'computedOnlyField',
optional: true,
},
'values.$.cost': {
type: 'computedOnlyField',
optional: true,
parseLevel: 'compile',
},
'values.$.spent': {
type: Number,
optional: true,
removeBeforeCompute: true,
},
'values.$.errors': {
type: Array,
optional: true,
removeBeforeCompute: true,
},
'values.$.errors.$': {
type: ErrorSchema,
},
total: {
type: 'computedOnlyField',
optional: true,
},
@@ -87,6 +137,19 @@ const ComputedOnlyPointBuySchema = createPropertySchema({
optional: true,
removeBeforeCompute: true,
},
pointsLeft: {
type: Number,
optional: true,
removeBeforeCompute: true,
},
errors: {
type: Array,
optional: true,
removeBeforeCompute: true,
},
'errors.$': {
type: ErrorSchema,
},
});
const ComputedPointBuySchema = new SimpleSchema()

View File

@@ -2,28 +2,28 @@ import SimpleSchema from 'simpl-schema';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
let ProficiencySchema = new SimpleSchema({
name: {
type: String,
optional: true,
name: {
type: String,
optional: true,
max: STORAGE_LIMITS.name,
},
// The variableNames of the skills, tags, or attributes to apply proficiency to
stats: {
type: Array,
defaultValue: [],
},
// The variableNames of the skills, tags, or attributes to apply proficiency to
stats: {
type: Array,
defaultValue: [],
maxCount: STORAGE_LIMITS.statsToTarget,
},
'stats.$': {
type: String,
},
'stats.$': {
type: String,
max: STORAGE_LIMITS.variableName,
},
// A number representing how proficient the character is
},
// A number representing how proficient the character is
// where 0.49 is half rounded down and 0.5 is half rounded up
value: {
type: Number,
allowedValues: [0.49, 0.5, 1, 2],
defaultValue: 1,
},
value: {
type: Number,
allowedValues: [0.49, 0.5, 1, 2],
defaultValue: 1,
},
});
const ComputedOnlyProficiencySchema = new SimpleSchema({});

View File

@@ -23,14 +23,14 @@ import createPropertySchema from '/imports/api/properties/subSchemas/createPrope
*/
let RollSchema = createPropertySchema({
name: {
type: String,
type: String,
defaultValue: 'New Roll',
max: STORAGE_LIMITS.name,
},
},
// The technical, lowercase, single-word name used in formulae
variableName: {
type: String,
regEx: VARIABLE_NAME_REGEX,
regEx: VARIABLE_NAME_REGEX,
min: 2,
defaultValue: 'newRoll',
max: STORAGE_LIMITS.variableName,

View File

@@ -16,20 +16,25 @@ let SavingThrowSchema = createPropertySchema({
optional: true,
},
// Who this saving throw applies to
target: {
type: String,
target: {
type: String,
defaultValue: 'target',
allowedValues: [
allowedValues: [
'self',
'target',
],
},
},
// The variable name of save to roll
stat: {
type: String,
optional: true,
max: STORAGE_LIMITS.variableName,
},
// Prevent the property from showing up in the log
silent: {
type: Boolean,
optional: true,
},
});
const ComputedOnlySavingThrowSchema = createPropertySchema({

View File

@@ -9,10 +9,10 @@ import createPropertySchema from '/imports/api/properties/subSchemas/createPrope
*/
let SkillSchema = createPropertySchema({
name: {
type: String,
optional: true,
type: String,
optional: true,
max: STORAGE_LIMITS.name,
},
},
// The technical, lowercase, single-word name used in formulae
// Ignored for skilltype = save
variableName: {
@@ -22,33 +22,33 @@ let SkillSchema = createPropertySchema({
max: STORAGE_LIMITS.variableName,
optional: true,
},
// The variable name of the ability this skill relies on
// The variable name of the ability this skill relies on
ability: {
type: String,
optional: true,
max: STORAGE_LIMITS.variableName,
},
// What type of skill is this
// What type of skill is this
skillType: {
type: String,
allowedValues: [
'skill',
'save',
'check',
'check',
'tool',
'weapon',
'armor',
'language',
'utility', //not displayed anywhere
'utility', //not displayed anywhere
],
defaultValue: 'skill',
},
// The base proficiency of this skill
baseProficiency: {
type: Number,
optional: true,
// The base proficiency of this skill
baseProficiency: {
type: Number,
optional: true,
allowedValues: [0.49, 0.5, 1, 2],
},
},
// The starting value, before effects
baseValue: {
type: 'fieldToCompute',
@@ -56,16 +56,16 @@ let SkillSchema = createPropertySchema({
},
// Description of what the skill is used for
description: {
type: 'inlineCalculationFieldToCompute',
optional: true,
},
type: 'inlineCalculationFieldToCompute',
optional: true,
},
});
let ComputedOnlySkillSchema = createPropertySchema({
// Computed value of skill to be added to skill rolls
// Computed value of skill to be added to skill rolls
value: {
type: Number,
defaultValue: 0,
defaultValue: 0,
optional: true,
removeBeforeCompute: true,
},
@@ -75,33 +75,33 @@ let ComputedOnlySkillSchema = createPropertySchema({
optional: true,
},
description: {
type: 'computedOnlyInlineCalculationField',
optional: true,
},
// Computed value added by the ability
abilityMod: {
type: SimpleSchema.Integer,
optional: true,
type: 'computedOnlyInlineCalculationField',
optional: true,
},
// Computed value added by the ability
abilityMod: {
type: SimpleSchema.Integer,
optional: true,
removeBeforeCompute: true,
},
// Computed advantage/disadvantage
},
// Computed advantage/disadvantage
advantage: {
type: SimpleSchema.Integer,
optional: true,
allowedValues: [-1, 0, 1],
removeBeforeCompute: true,
},
// Computed bonus to passive checks
// Computed bonus to passive checks
passiveBonus: {
type: Number,
optional: true,
removeBeforeCompute: true,
},
// Computed proficiency multiplier
// Computed proficiency multiplier
proficiency: {
type: Number,
allowedValues: [0, 0.49, 0.5, 1, 2],
defaultValue: 0,
defaultValue: 0,
removeBeforeCompute: true,
},
// Compiled text of all conditional benefits
@@ -113,7 +113,7 @@ let ComputedOnlySkillSchema = createPropertySchema({
'conditionalBenefits.$': {
type: String,
},
// Computed number of things forcing this skill to fail
// Computed number of things forcing this skill to fail
fail: {
type: SimpleSchema.Integer,
optional: true,
@@ -135,6 +135,7 @@ let ComputedOnlySkillSchema = createPropertySchema({
effects: {
type: Array,
optional: true,
removeBeforeCompute: true,
},
'effects.$': {
type: Object,

View File

@@ -3,20 +3,26 @@ import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
let SpellListSchema = createPropertySchema({
name: {
type: String,
optional: true,
name: {
type: String,
optional: true,
max: STORAGE_LIMITS.name,
},
},
description: {
type: 'inlineCalculationFieldToCompute',
optional: true,
},
// Calculation of how many spells in this list can be prepared
maxPrepared: {
type: 'inlineCalculationFieldToCompute',
optional: true,
},
// Calculation of how many spells in this list can be prepared
maxPrepared: {
type: 'fieldToCompute',
optional: true,
},
// The variable name of the ability this spell relies on
ability: {
type: String,
optional: true,
max: STORAGE_LIMITS.variableName,
},
// Calculation of The attack roll bonus used by spell attacks in this list
attackRollBonus: {
type: 'fieldToCompute',
@@ -38,6 +44,12 @@ const ComputedOnlySpellListSchema = createPropertySchema({
type: 'computedOnlyField',
optional: true,
},
// Computed value determined by the ability
abilityMod: {
type: SimpleSchema.Integer,
optional: true,
removeBeforeCompute: true,
},
attackRollBonus: {
type: 'computedOnlyField',
optional: true,

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