Compare commits

...

704 Commits

Author SHA1 Message Date
7e572b654c Update Dockerfile 2026-04-22 10:28:17 +02:00
ThaumRystra
88fc9c8321 Merge pull request #397 from StormDragon894/patch-1
Update invite link to use the new domain
2026-01-23 10:08:07 +02:00
StormDragon894
76b0023e37 Update invite link to use the new domain 2026-01-23 13:40:22 +11:00
ThaumRystra
899dcf42b1 Fixed various action calling issues 2025-07-25 19:15:40 +02:00
ThaumRystra
41e9fb4069 Fix inline calculations reducing instead of compiling 2025-07-25 18:50:22 +02:00
ThaumRystra
51a20ac205 Fix bug activating property slots under actions 2025-07-25 18:50:02 +02:00
ThaumRystra
5dc202fc17 Fix bug in dependency loop detection 2025-07-25 18:27:18 +02:00
ThaumRystra
8b5d36fd66 fix bug duplicating library nodes 2025-07-25 18:19:05 +02:00
ThaumRystra
7063312135 fix wrong assert imported in app code 2025-05-02 18:50:11 +02:00
Thaum Rystra
ae5a159e58 Fix TypeScript errors in action engine 2025-05-02 15:38:18 +02:00
ThaumRystra
d42d2a724e Fixed library nodes not having a default schema 2025-05-02 13:37:11 +02:00
ThaumRystra
8453bd9d86 Rudimentary customization in check dialog 2025-01-25 21:53:50 +02:00
ThaumRystra
bd6d5c34d3 Fixed monsters appearing in sidebar 2025-01-25 21:18:09 +02:00
ThaumRystra
a6bf29ece1 Moved char subscription to page so it isn't re-fired on tabletops 2025-01-25 21:17:59 +02:00
ThaumRystra
b8ed9aa658 Fix self-targeted spells not casting correctly 2025-01-25 20:51:20 +02:00
ThaumRystra
c42da1d492 Added remove button to buff cards on tabletop, closes #380 2025-01-25 20:44:35 +02:00
ThaumRystra
4993506ec9 Fixed spell casting 2025-01-25 14:29:26 +02:00
ThaumRystra
0b499b9c98 Big improvements in UX for tabletop actions 2025-01-24 16:13:36 +02:00
ThaumRystra
5b68190570 Iterated on Tabletop UX 2025-01-23 23:22:16 +02:00
ThaumRystra
a09a32eb9a Added latest compute date to creatures 2025-01-23 17:58:45 +02:00
ThaumRystra
e508951fb9 Fixed: tagless triggers should target everything not nothing.
also fixed: check triggers now correctly fire on skill used for checks
2025-01-20 21:03:59 +02:00
ThaumRystra
3315b5607a Fixed bug where rolls weren't recalculating in actions 2025-01-20 19:59:57 +02:00
ThaumRystra
be47b90c32 Fixed bug where tag-targeting a calculation's base value would crash the sheet 2025-01-16 20:32:30 +02:00
ThaumRystra
0bf8fdc6d3 Fixed failing tests 2025-01-16 16:24:56 +02:00
ThaumRystra
a2d2f43bed Removed custom Collection2 package 2025-01-16 10:59:13 +02:00
Thaum Rystra
15ecc05e21 The type system starts to infect the computation engine 2025-01-15 18:36:26 +02:00
Thaum Rystra
12789f1c5e fixed issue with isSpell no longer being defined 2025-01-15 17:40:44 +02:00
ThaumRystra
c0d1412463 Improved typing of Creature Properties 2025-01-14 13:21:43 +02:00
ThaumRystra
1b05b8d3bf Improved typing of creature properties 2025-01-12 23:28:43 +02:00
ThaumRystra
0125367085 Migrate props to typed schemas 2025-01-12 19:29:26 +02:00
ThaumRystra
0f32afd25a Moved attributes and adjustments to typed schemas 2025-01-10 09:35:30 +02:00
ThaumRystra
fcf6a84b01 Began experimenting with dragging typings out of simple schema 2025-01-01 22:39:54 +02:00
ThaumRystra
e76ad64a7d Fixed regression in applying buffs 2024-12-01 22:10:53 +02:00
ThaumRystra
75fe3e8fe2 Fixed #382 dice roll functions not working 2024-12-01 22:02:47 +02:00
ThaumRystra
0e56421c3a Tested and fixed critical hits not doubling damage 2024-12-01 21:16:53 +02:00
ThaumRystra
19ae78b23b closed Imported characters need to be recalculated before adding to a tabletop #375 2024-11-09 16:43:18 +02:00
ThaumRystra
a40cae1327 Removed stray console logging
closes Creatures summoned to tabletops from libraries are missing their properties #373
2024-11-09 15:49:36 +02:00
ThaumRystra
3949ea3448 Fixed creature templates added to tabletops not getting their properties set 2024-11-09 15:44:06 +02:00
ThaumRystra
7c648f67a3 Fixed tag targeted effects on overridden attributes applying multiple times
Closes #358
2024-11-09 15:24:56 +02:00
ThaumRystra
5c80975bc1 Fixed slot fillers in fill dialog not showing children when expanded 2024-11-09 14:21:15 +02:00
ThaumRystra
40025dc7d3 Fixed spaces in slot fill search returning no results 2024-11-09 14:20:42 +02:00
ThaumRystra
43958a90bd Removed drag handles from spells in spell lists
Spell lists often have spells from many sources, and dragging spells around in a UI that doesn't show what you might be breaking is a foot gun.
2024-11-09 13:43:01 +02:00
ThaumRystra
6e5a335a39 Fixed item drag drop re-ordering 2024-11-09 13:28:15 +02:00
ThaumRystra
2c431293e0 Tested that triggers fire for all properties.
Fixed some action engine issues found.
2024-11-07 00:22:46 +02:00
ThaumRystra
057772c60a Merge pull request #346 from Jonpot/patch-8
Fixes incorrect comment in Containers.js
2024-11-05 13:34:01 +02:00
Thaum Rystra
9a19262ee7 Added icons to feature cards #341 2024-11-05 13:25:37 +02:00
ThaumRystra
7c877b9176 Merge pull request #334 from kgscialdone/fix-treesearch-references
Fix Reference missing from type search in library tree
2024-11-05 12:31:56 +02:00
ThaumRystra
37c6128337 Merge pull request #301 from Jonpot/patch-2
Update ActionForm with hints that represent what will be logged by the action engine
2024-11-05 12:23:16 +02:00
Thaum Rystra
48601050de Merge branch 'develop' into pr/Jonpot/301 2024-11-05 12:22:09 +02:00
ThaumRystra
c0a8f76035 Added extra logging to s3 read errors 2024-11-03 17:18:43 +02:00
ThaumRystra
8268ea85ce Fixed duplicating properties losing their children 2024-11-03 16:15:22 +02:00
ThaumRystra
f8708b0c31 Fixed point buy defined variable names not being available in actions 2024-11-03 15:57:32 +02:00
ThaumRystra
a206aad904 Prevented inserting duplicate creature variables documents 2024-11-03 14:35:42 +02:00
ThaumRystra
865c7a5ca2 Ensure creature properties are sorted before being migrated from an archive 2024-11-03 14:35:25 +02:00
ThaumRystra
c4c0b47f58 Fixed broken import 2024-11-03 14:24:59 +02:00
Thaum Rystra
449e4fe681 Improved import migrations from main site 2024-10-31 11:34:14 +02:00
Thaum Rystra
6944e75d50 Removed insert new creature button from creature add to tabletop dialog 2024-10-30 18:00:41 +02:00
Thaum Rystra
84282cef6c Hid silenced content from the logs 2024-10-30 17:53:39 +02:00
Thaum Rystra
2a5a97f04a Fixed loading filtered properties in contexts where iterators don't have .filter 2024-10-30 10:07:08 +02:00
Thaum Rystra
87325df9fb Merge branch 'develop' of https://github.com/ThaumRystra/DiceCloud into develop 2024-10-30 10:03:51 +02:00
Thaum Rystra
ce9753fa9c Fixed effects not applying to damage in actions 2024-10-30 10:03:47 +02:00
ThaumRystra
18c1e8481e Fixed libraries being created without an owner 2024-10-29 19:43:43 +02:00
ThaumRystra
7c5c37cfdf Stopped ghost properties showing up in actions 2024-10-29 19:22:58 +02:00
ThaumRystra
417d1287a6 Made sure doAction's promise resolves or rejects 2024-10-29 17:01:51 +02:00
ThaumRystra
681d1e5739 Improved how effects on calculated fields are displayed 2024-10-29 16:47:55 +02:00
ThaumRystra
b90cc2e467 Removed damage effects from tree views
The totals of those effects are already accounted for in the displayed number
2024-10-29 16:06:46 +02:00
ThaumRystra
fe035a2ff1 Added missing schema version to json route meta 2024-10-29 16:02:46 +02:00
Thaum Rystra
74bd291426 Revert "Rolled back to Meteor 1.13, removed publication strategies"
This reverts commit 90aaf2d0fc.
2024-10-28 15:18:50 +02:00
Thaum Rystra
90aaf2d0fc Rolled back to Meteor 1.13, removed publication strategies 2024-10-28 15:09:59 +02:00
Thaum Rystra
f44afd8b6a Hid make check and roll dice icons from creature bar in tabletop 2024-10-28 14:31:43 +02:00
Thaum Rystra
e8158ba531 Iterate on action engine and tree data store migration 2024-10-28 14:26:48 +02:00
Thaum Rystra
e887daf543 Show resulting action on action dialog before committing 2024-10-28 14:26:01 +02:00
Thaum Rystra
8f8c9c28aa Refactored actions, 'cast a spell' task now works 2024-10-28 12:28:36 +02:00
ThaumRystra
804c5f3aee Started implementing spells in action engine 2024-10-27 12:51:48 +02:00
ThaumRystra
01df7898cc Tested action toggles 2024-10-15 21:17:33 +02:00
ThaumRystra
7a480e333c Fixed saving throws contaminating later saves with their result 2024-10-15 21:12:55 +02:00
Thaum Rystra
5c7234ec62 Adding failing saving throw tests 2024-10-14 22:00:27 +02:00
Thaum Rystra
6887a46be4 Merge branch 'develop' of https://github.com/ThaumRystra/DiceCloud into develop 2024-10-14 21:52:54 +02:00
Thaum Rystra
b772a4eaa1 Tested folders, notes, and rolls 2024-10-14 21:52:37 +02:00
ThaumRystra
f37149f466 Improved character sheet printing 2024-10-13 15:58:08 +02:00
ThaumRystra
efc52b4f68 Tested damage property 2024-09-28 14:45:46 +02:00
ThaumRystra
b1ab65f095 Tested and fixed buff remover properties 2024-09-28 12:45:30 +02:00
ThaumRystra
d998d7d2ef Removed stray 'only' test 2024-09-08 21:09:34 +02:00
ThaumRystra
588a91f60c Fixed typo in test description 2024-09-08 21:06:48 +02:00
ThaumRystra
0d4db7ca91 Tested action engine apply buff 2024-09-08 21:04:18 +02:00
ThaumRystra
f9e10550ad Fixed healthbar display in tabletop 2024-09-06 23:17:53 +02:00
ThaumRystra
e2ffaa203a Made sure uploads respect storage limits 2024-09-06 22:58:07 +02:00
ThaumRystra
679a123a2a Fixed references sorting first in slot fillers 2024-09-06 21:40:39 +02:00
ThaumRystra
b867cf182b Fixed overflow on character cards not working 2024-09-06 20:40:10 +02:00
ThaumRystra
f9b42f9d9a Added mouse wheel scroll Y support to character tabletop footer 2024-09-06 20:38:11 +02:00
ThaumRystra
f9c0317b95 Added previous value of att to set log 2024-09-06 17:30:52 +02:00
ThaumRystra
87527b0677 Fixed typescript errors in old action engine tests 2024-09-06 17:19:12 +02:00
ThaumRystra
aeae360150 Fixed typo in file name:
INLINE_CALCULTION_REGEX ->  INLINE_CALCULATION_REGEX
2024-09-06 17:16:59 +02:00
ThaumRystra
eb98d0b711 Tested action branch properties 2024-09-06 17:12:04 +02:00
ThaumRystra
e11fb50103 Added the ability to import creatures from other instances of DiceCloud 2024-09-03 22:54:44 +02:00
Thaum Rystra
2a3357ce5c Moved upload input to front of file lists 2024-09-03 16:32:10 +02:00
Thaum Rystra
5727afcfa8 Added ThumbHash to user images 2024-09-03 16:02:56 +02:00
ThaumRystra
a5d50d5ac2 Limited tabletop logs, priority over character log limit 2024-09-03 00:19:04 +02:00
ThaumRystra
fe804a5d9a Added image select input to tabletop form 2024-09-03 00:18:42 +02:00
ThaumRystra
23b25f1606 Added buff card to tabletop bar 2024-09-03 00:01:44 +02:00
ThaumRystra
e8c9058084 Made folders into groups in tabletop 2024-09-02 23:50:13 +02:00
ThaumRystra
2f2a062273 Fixed remove properties not being correctly bulk written 2024-09-02 23:29:16 +02:00
ThaumRystra
b1e7ac1161 Added tests for applying adjustments 2024-09-02 22:10:19 +02:00
ThaumRystra
f4d9806f6f Added user images header to file page 2024-09-02 21:32:46 +02:00
ThaumRystra
9af8b734f1 Finished MVP of user file upload 2024-09-02 20:56:10 +02:00
Thaum Rystra
da0b653582 Fixed upload state in image uploads 2024-08-29 15:54:52 +02:00
Thaum Rystra
a30ce140d2 Used file input for character portrait and avatar 2024-08-29 15:42:26 +02:00
Thaum Rystra
8eabd16601 Upload image button working 2024-08-29 15:30:13 +02:00
ThaumRystra
9ce76dcd0c Refining image input ui 2024-07-28 16:47:06 +02:00
ThaumRystra
a8310c4817 Progress on user image UI 2024-07-28 03:04:27 +02:00
Thaum Rystra
b853922749 Began file input 2024-07-25 16:22:26 +02:00
Thaum Rystra
6e0233da6e Fixed error on test bulk write with no tabletopId 2024-07-10 12:16:16 +02:00
Thaum Rystra
cba27bb578 Fixed tabletop neighbor characters leaking into the sidebar when logged out 2024-07-02 13:26:21 +02:00
Thaum Rystra
d052d55eb1 Fixed user uploaded image removal 2024-07-02 13:25:18 +02:00
Thaum Rystra
0ed1e4797d Fixed archiving when not using S3 file storage 2024-07-02 13:24:55 +02:00
Thaum Rystra
02944f4de7 Disabled smart disconnect, not compatible with NO_MERGE_MULTI pub strat 2024-07-02 12:57:56 +02:00
ThaumRystra
599c9f0de0 Fixed rare compute crash when having empty action resources 2024-06-16 12:35:09 +02:00
ThaumRystra
ada4acc72d Updated Meteor 2024-06-15 19:04:37 +02:00
ThaumRystra
019e3d323d Fixed publication strategies for memory management 2024-06-15 19:04:30 +02:00
Thaum Rystra
e763b15453 Made heavy collections NO_MERGE_MULTI publication strategy 2024-06-13 10:46:52 +02:00
ThaumRystra
6742b4bc00 Actions targeting other creatures now works on both tabletop and character sheet 2024-06-12 20:50:18 +02:00
Thaum Rystra
621f284cff Iterated on tabletops 2024-06-12 17:30:37 +02:00
ThaumRystra
a5292cf0f2 Started fixing action target selection 2024-06-12 15:43:56 +02:00
Thaum Rystra
4921a34dfe Fixed adding creatures from libraries -> tabletops 2024-06-05 17:29:50 +02:00
Thaum Rystra
a00292a523 Improved observer reuse for loaded creatures 2024-06-05 17:29:33 +02:00
Thaum Rystra
37916e266d Fixed slot fill dialog to new parenting schema 2024-06-05 17:28:58 +02:00
Thaum Rystra
b60dcc5e73 Removed creature variables during migration 2024-06-05 17:28:39 +02:00
Thaum Rystra
36ff5daa54 Fixed bug with select-outside throwing error for undefined elements 2024-06-05 17:10:44 +02:00
ThaumRystra
99c14099dc Began adding creature templates to libraries 2024-06-05 15:10:22 +02:00
ThaumRystra
6070c499cc Fixed broken library node insert button 2024-05-31 20:36:09 +02:00
ThaumRystra
6e8c970287 Actions in tabletops show logs correctly 2024-05-31 20:01:42 +02:00
ThaumRystra
028c1b7463 Merge branch 'develop' of https://github.com/ThaumRystra/DiceCloud into develop 2024-05-31 18:05:57 +02:00
ThaumRystra
e9372c97ee Fixed tabletop action card 2024-05-31 18:05:54 +02:00
Thaum Rystra
0891702bd9 Fixed some typing errors 2024-05-31 17:13:58 +02:00
ThaumRystra
68fb743302 Made delete item button only show up in character sheet 2024-05-29 00:51:07 +02:00
ThaumRystra
a03f379b6e Added attunement to item viewer 2024-05-29 00:50:27 +02:00
ThaumRystra
3dacfa170c Delete item button from item viewer 2024-05-29 00:23:29 +02:00
ThaumRystra
4b7035b5a5 Removed old cookie 2024-05-28 23:10:35 +02:00
Thaum Rystra
b98a8b1ddf Updated documentation to new parenting format 2024-05-28 13:05:19 +02:00
Thaum Rystra
772e55ece5 Changed task triggers to be stored differently so that attribute check and damage triggers don't overlap 2024-05-21 17:41:20 +02:00
Thaum Rystra
8eb702cde3 Removed future page from sidebar until it's implemented 2024-05-21 17:33:11 +02:00
Thaum Rystra
4547eb8b10 Fixed firing actions from the detail view 2024-05-15 16:07:23 +02:00
Thaum Rystra
1266794db7 Fixed subtle trigger bugs that break LoV hit dice extension 2024-05-15 16:06:56 +02:00
Thaum Rystra
4fc897deec Folders are now sorted by name 2024-05-15 12:45:45 +02:00
Thaum Rystra
f47c311883 Finished tabletop management UI 2024-05-14 18:09:09 +02:00
Thaum Rystra
07a7849911 Fixed function signatures of buff removers and folders that could break triggers 2024-05-14 10:03:15 +02:00
Thaum Rystra
195704ed7a Tested and fixed triggers in new action engine 2024-05-14 10:02:54 +02:00
Thaum Rystra
4a52c3af19 Fixed a lot of UI to match new parenting API 2024-05-08 12:10:43 +02:00
Thaum Rystra
620634c6fd Merge branch 'develop' of https://github.com/ThaumRystra/DiceCloud into develop 2024-05-07 11:03:26 +02:00
Thaum Rystra
73e4a3ae2c Fixed actionDialog script language 2024-05-07 11:00:43 +02:00
ThaumRystra
9a6cb4b23f Started removing old action engine methods 2024-05-04 19:25:38 +02:00
ThaumRystra
c11013eddb Fixed soft-remove of props and library props 2024-05-04 18:57:56 +02:00
ThaumRystra
c5f6ce81bd Replaced damageProperty with new action engine 2024-05-04 10:37:25 +02:00
ThaumRystra
982897897f Fixed Incrementing props from stats tab 2024-05-03 14:29:42 +02:00
ThaumRystra
de1eb64285 Fixed action card not loading on action 2024-05-02 20:42:03 +02:00
ThaumRystra
445c2bb9c7 Fixed action cards briefly flashing dialog that wasn't needed 2024-05-02 20:41:52 +02:00
ThaumRystra
927fea9e8c Fixed characters not recalculating on action 2024-05-02 20:41:26 +02:00
ThaumRystra
7e3a16c96d Added reset task, fixed sheet reset buttons 2024-05-02 20:41:04 +02:00
ThaumRystra
9cd6ca5c6e Choice branch UI working! 2024-04-30 14:54:16 +02:00
ThaumRystra
19669e729c Updated Meteor packages 2024-04-30 13:12:24 +02:00
ThaumRystra
3483a6d34f Fixed actions using their own parent as ammo creating an infinite loop 2024-04-30 13:03:33 +02:00
ThaumRystra
e62f536749 replaced all sorting by order with sorting by left 2024-04-30 12:51:34 +02:00
ThaumRystra
9d3f9ce14a Improved action dialog style 2024-04-30 12:47:53 +02:00
ThaumRystra
8b3e95e1ae Fixed property trees in detail dialogs not showing any props 2024-04-30 12:47:24 +02:00
Thaum Rystra
a40163b9cf Implemented checks at least back to 2.0 functionality in new action engine 2024-04-17 19:37:38 +02:00
ThaumRystra
24d7f3074a Merge branch 'develop' of https://github.com/ThaumRystra/DiceCloud into develop 2024-04-17 10:10:32 +02:00
ThaumRystra
acb30c6dd8 Game icons as a font 2024-04-17 10:10:29 +02:00
Thaum Rystra
78266a93ee Some progress on user image uploading 2024-04-17 09:03:21 +02:00
ThaumRystra
7b662a647a Improved hexagon hb 2024-04-13 11:57:29 +02:00
Thaum Rystra
08640f2bf2 Moved tabletop characters to left side of the screen 2024-04-12 17:05:20 +02:00
Thaum Rystra
4793b34a55 Made undefined variable names zero in compile step 2024-04-12 11:36:11 +02:00
Thaum Rystra
3a7c3765c6 Removed example 3D map, see feature-react-three branch for vtt work 2024-04-08 15:33:39 +02:00
ThaumRystra
92a1b73b32 removed stray console log 2024-04-06 12:41:50 +02:00
ThaumRystra
51fa1e7e0b Fixed action dialog applying actions 2024-04-06 12:41:15 +02:00
Thaum Rystra
1fb1eb83c7 Implementing persisting action result mutations 2024-04-02 17:46:31 +02:00
Thaum Rystra
2cbfc5d099 removed stray import 2024-03-31 08:49:43 +02:00
Thaum Rystra
6138be8083 Progress on action choices 2024-03-30 21:12:35 +02:00
Thaum Rystra
6c3d4b91eb Interrupt UI progress 2024-03-29 16:42:50 +02:00
Thaum Rystra
e083490cb3 Action interruption progress 2024-03-26 23:25:18 +02:00
Thaum Rystra
57220c7972 Fixed potential empty tier 2024-03-26 17:28:52 +02:00
Thaum Rystra
1614e655c5 Fixed some types not being found and some errors Identified by typescript 2024-03-26 16:37:03 +02:00
Thaum Rystra
a3ea5e1408 Don't remove creatures until archive has written to s3 2024-03-26 13:40:58 +02:00
Thaum Rystra
3a6639fbc3 Stopped typescript shouting about things you can't fix in js 2024-03-26 10:20:48 +02:00
Thaum Rystra
352e89ee9a Fixed parent.id => parentId 2024-03-26 09:58:01 +02:00
Thaum Rystra
0eb763dc0e Fixed build tree 2024-03-26 09:54:11 +02:00
Thaum Rystra
d2a553abda Removed redis-oplog sub-package 2024-03-25 09:47:48 +02:00
Thaum Rystra
225b895833 Fixed some character sheet Query errors 2024-03-23 21:06:58 +02:00
Thaum Rystra
359d645f6b Fixing UI for 2.1 data changes 2024-03-23 16:02:28 +02:00
Thaum Rystra
293deaa592 More action test cases 2024-03-18 20:40:20 +02:00
ThaumRystra
3c78f5b2f5 Iterated on tabletop model 2024-03-08 15:26:17 +02:00
Thaum Rystra
b13ca8c64b More testing of action system, now with test coverage 2024-02-27 15:30:40 +02:00
Thaum Rystra
5141704e23 Fixed failing tests and action engine props 2024-02-22 09:11:00 +02:00
Thaum Rystra
b41d26b3ad Fixed failing tests, tested parser more 2024-02-21 10:36:32 +02:00
Thaum Rystra
ac15512bc5 Typescript all the parser things 2024-02-20 23:21:12 +02:00
Thaum Rystra
3ea492ee78 Started with async inputs to actions 2024-02-19 22:31:18 +02:00
Thaum Rystra
55a6b16c31 Fixed failing action engine tests, moved more engine parts to ts 2024-02-19 15:39:16 +02:00
Thaum Rystra
c721374278 Moved parser to typescript 2024-02-19 12:45:54 +02:00
Thaum Rystra
5b7d352323 Started splitting engine tests into individual files and testing action props 2024-02-16 16:16:18 +02:00
Thaum Rystra
60117308f7 Removed stray console logs 2024-02-16 10:47:33 +02:00
Thaum Rystra
aee9d6b8cb refactored action engine into individual files 2024-02-16 10:30:50 +02:00
Thaum Rystra
19f7d40386 Removed v1 data sources 2024-02-06 09:43:08 +02:00
Jonpot
1759427ca7 Fixes incorrect comment in Containers.js
I don't know which way this bug goes, but right now the comment on `contentsWeight` says it's 0 if contentsWeightless is checked. This isn't the case (however carriedWeight does follow this rule)
2024-01-24 12:29:02 -08:00
Thaum Rystra
73a36678b9 Fixed stray semicolon in action test 2024-01-09 11:41:14 +02:00
Thaum Rystra
cab5215b24 Added target character to action engine test 2024-01-08 16:49:23 +02:00
ThaumRystra
1361205a7a started implementing buffs in new action engine 2024-01-02 07:47:06 +02:00
ThaumRystra
e886be8f04 Fixed showstopping bugs with tree organize functions 2023-12-31 18:06:31 +02:00
ThaumRystra
9d07953a40 Renamed action engine tests 2023-12-30 14:14:05 +02:00
ThaumRystra
3b80f53929 updated packages 2023-12-30 14:13:56 +02:00
ThaumRystra
9196e5bdef Renamed Action.ts to ActionEngine.ts 2023-12-27 11:43:24 +02:00
ThaumRystra
a06e9a0960 Fixed failing tests 2023-12-23 12:31:14 +02:00
ThaumRystra
4c778fa282 Rewrote parenting organize methods to avoid rebuilds 2023-12-20 20:02:29 +02:00
ThaumRystra
4a349ea906 Fixed a lot broken with nested sets 2023-12-18 23:12:39 +02:00
ThaumRystra
a891f26b63 Fixed broken service worker 2023-12-18 20:44:30 +02:00
ThaumRystra
c628af1314 Merge branch 'feature-tabletop' into develop 2023-12-18 18:35:56 +02:00
ThaumRystra
3ba603407e Merge feature-nested-sets into develop 2023-12-18 18:27:17 +02:00
ThaumRystra
1e90b345e2 Continued implementing action props in new engine 2023-12-16 10:29:10 +02:00
ThaumRystra
862c2ceb99 Finished move to 3 tier action methods 2023-12-15 12:25:32 +02:00
ThaumRystra
99bf5fd832 Began attempting 3-tier method of action engine 2023-12-14 22:27:22 +02:00
Thaum Rystra
8fe040a12a Iterated on action dialog 2023-12-06 19:58:01 +02:00
ThaumRystra
64a1bfeda5 Began work on new action UI dialog 2023-12-06 07:36:02 +02:00
ThaumRystra
24438d5a92 Changed how before and after triggers are handled 2023-11-25 11:59:32 +02:00
Thaum Rystra
f676dfd928 Began working on applying actions
(consuming resources)
2023-11-21 15:54:30 +02:00
Thaum Rystra
d44537076c removed unnecessary task/subtask type field 2023-11-21 15:38:24 +02:00
Thaum Rystra
3a67752b1f Refactored damage props into a subtask 2023-11-21 15:36:37 +02:00
Thaum Rystra
97e3552dc3 Fixed roll test 2023-11-21 13:23:59 +02:00
Thaum Rystra
936ca862db Added Rolls to action system rewrite 2023-11-21 11:35:07 +02:00
ThaumRystra
fa9f64dd51 Iterated on how tasks are pushed to queue 2023-11-20 21:31:55 +02:00
ThaumRystra
b5e59c7147 Improved action attribute damage handling 2023-11-18 16:03:48 +02:00
ThaumRystra
59a9433dc7 Tested and fixed adjustments 2023-11-17 17:34:48 +02:00
Thaum Rystra
95c3e882d7 Added Adjustments 2023-11-17 12:00:19 +02:00
ThaumRystra
581f99d467 Tested and fixed branches and notes
in new action interrupt system
2023-11-16 22:21:48 +02:00
Thaum Rystra
375a84226d Added action tests 2023-11-16 14:00:26 +02:00
ThaumRystra
6d8dfc2255 Added function to start an action 2023-11-15 23:42:56 +02:00
ThaumRystra
40a5b72755 Biting the bullet, started rewriting Action engine 2023-11-15 23:19:58 +02:00
Thaum Rystra
0c495726ba Added compute triggers to store trigger ids on their
targeted props. Needs testing
2023-11-15 15:42:09 +02:00
Thaum Rystra
6162f2fe90 Failed attempt at using method calls to manage awaited method 2023-11-14 13:55:17 +02:00
ThaumRystra
5a2df36e8b Began migrating action engine to async
To suspending actions to await user input
2023-11-13 00:24:51 +02:00
ThaumRystra
800ef3328c Fixed checks not applying rolled effects 2023-11-12 21:11:04 +02:00
ThaumRystra
2e3e6e22b6 Fixed skill and attribute effect lists
Now using effectId lists
2023-11-12 17:38:51 +02:00
ThaumRystra
b32b6db21a Fixed issue: toggled off point buys still applying 2023-11-12 16:38:55 +02:00
ThaumRystra
aaec54f36a Fixed docs siblings display on small screens 2023-11-12 15:05:46 +02:00
ThaumRystra
d50ad58526 Added the ability to rearrange Docs 2023-11-12 14:49:15 +02:00
ThaumRystra
d578268e99 Disabled migration testing
re-enable when merging feature-nested-sets
2023-11-11 13:39:43 +02:00
ThaumRystra
fbc8ed977a Added commutative simplification for + and * 2023-11-11 13:31:31 +02:00
ThaumRystra
3b1f61aa0a Added and fixed some tests for tag targeted effects 2023-11-11 13:31:15 +02:00
ThaumRystra
6e0364b636 Merge branch 'develop' into feature-nested-sets 2023-11-11 10:01:34 +02:00
ThaumRystra
6af9246ad1 Added roll and reduce calculation utility 2023-11-09 23:25:18 +02:00
ThaumRystra
4d1dec8956 Fixed bug when trying to calculate empty calcs 2023-11-09 23:10:08 +02:00
ThaumRystra
7eada9effe Replaced most properties in creature variables with links
instead of storing the entire property twice
2023-11-09 23:05:05 +02:00
ThaumRystra
84edd74ff3 Allowed accessing of constant arrays from accessor 2023-11-09 23:04:40 +02:00
ThaumRystra
c25dcc0a09 Removed Symbol parse node
Too much overlap with accessors, so now
all symbols are a special case of accessors
2023-11-09 23:04:18 +02:00
ThaumRystra
2117a63945 Removed evaluateCalculation.js 2023-11-09 18:43:13 +02:00
Thaum Rystra
9e5b6b11e1 Settling on a data structure to balance compatibility
with not being wrong
2023-11-09 16:08:04 +02:00
ThaumRystra
6ce7542c4b Changed aggregation schema of computed fields 2023-11-01 11:12:18 +02:00
Thaum Rystra
243684d206 Updated packages 2023-10-21 11:50:00 +02:00
Thaum Rystra
ff6edd398b Added denormalized computation fields 2023-10-21 11:49:50 +02:00
Thaum Rystra
b4993b86b5 Continued migration to nested sets 2023-10-04 14:27:06 +02:00
Thaum Rystra
f63d2ad254 Began migration of queries to nested sets 2023-10-03 16:28:20 +02:00
Thaum Rystra
ea058ba650 Added ßlue, Embodiment of Greed to about page 2023-10-02 10:17:01 +02:00
ThaumRystra
28a19f2037 Migrated some UI to nested sets, app starts now 2023-10-01 18:49:20 +02:00
ThaumRystra
e4590de3a7 Migrated insert prop methods to nested sets 2023-10-01 17:30:21 +02:00
ThaumRystra
fb7413dba4 Added migration to the new parenting schema 2023-09-29 17:59:17 +02:00
ThaumRystra
caea82bcc9 Fixed author name in package.json 2023-09-28 23:06:59 +02:00
ThaumRystra
60f542e64e Migrated loadCreatures to nested sets 2023-09-28 23:00:36 +02:00
ThaumRystra
ece4a9391a Removed .js from all imports to smooth ts migration 2023-09-28 21:27:05 +02:00
ThaumRystra
97790264d3 All tests passing... but do not be fooled 2023-09-28 21:16:16 +02:00
ThaumRystra
09c66aff0b Fixed more failing tests to match nested sets 2023-09-28 20:57:35 +02:00
Thaum Rystra
60c13643fb Fixed inactive test for new parenting structure 2023-09-28 15:16:52 +02:00
Thaum Rystra
3bd2806bc6 Moved action system to new tree format 2023-09-28 14:49:32 +02:00
Thaum Rystra
e6963ec865 Began the great TypeScript Migration
It's helping a lot to move to the new parenting system
2023-09-28 14:16:34 +02:00
Thaum Rystra
26e4d0bcc7 Merge branch 'develop' into feature-nested-sets 2023-09-28 10:25:27 +02:00
ThaumRystra
bc83fe98b4 Merge branch 'develop' into feature-tabletop 2023-09-24 19:10:04 +02:00
ThaumRystra
593d75a4bc Fixed missing margin in print inventory 2023-09-23 13:28:14 +02:00
ThaumRystra
ac8bd2cddb Iterated on printing format 2023-09-23 12:48:05 +02:00
ThaumRystra
6204be2240 Fixed printing on Chrome 2023-09-23 12:47:31 +02:00
ThaumRystra
643e7892c8 Minor redesign of printed character sheets 2023-09-22 22:10:11 +02:00
ThaumRystra
df8f9c085f Fixed regression in actions breaking ui and uses 2023-09-22 16:52:21 +02:00
ThaumRystra
ad15020f0b Removed per-row point buy cost/min/max 2023-09-22 16:46:26 +02:00
ThaumRystra
745296c1db Fixed regression: point buy cost calc failing 2023-09-22 16:46:14 +02:00
ThaumRystra
bfbb31d30c Fixed performance regression in dependency graph speed 2023-09-22 16:45:40 +02:00
ThaumRystra
572078c2fa Fixed dependency loop when props target self by tags 2023-09-22 15:53:39 +02:00
Thaum Rystra
7754482da7 Fixed damage tree node not rendering if it has no amount 2023-09-20 15:43:16 +02:00
Thaum Rystra
044240e2dd Fixed empty calculations unable to be targeted by effects 2023-09-20 15:42:40 +02:00
Thaum Rystra
d3c533dfa1 Moved ammo to its own pseudo property
Triggers now work on ammo
#amo now works as well
2023-09-20 14:03:37 +02:00
Thaum Rystra
3a3deca867 Removed x not found, set to 0 info messages from parser 2023-09-20 12:51:15 +02:00
Thaum Rystra
9d833a1fe3 Removed ammo as ~ammo, items should be applied
#ammo will be the way to referenced last used ammo
2023-09-19 16:48:48 +02:00
Thaum Rystra
98ac2e7122 Made ammo consumed available in scope as ~ammo 2023-09-19 15:10:32 +02:00
Thaum Rystra
f62a8bead4 Added damage saving throws to engine 2023-09-19 15:01:36 +02:00
Thaum Rystra
3be45b28c3 Made damage save form a bit more intuitive 2023-09-19 12:56:33 +02:00
Thaum Rystra
5222c240c7 Added saving throw to damage viewer 2023-09-19 12:50:29 +02:00
ThaumRystra
b9ed79d638 iterated 2023-09-18 20:39:34 +02:00
Thaum Rystra
31a614d335 Relaxed creature property update rate limit 2023-09-18 15:50:48 +02:00
Thaum Rystra
2545b9dd47 Added "save for half" to damage form 2023-09-18 15:50:35 +02:00
Thaum Rystra
691fe5f2e2 Merge commit 'f1b001933179ad0d4b1454c6ac2bd8792c181429' into develop 2023-09-18 14:28:21 +02:00
Thaum Rystra
c274153c79 Duplicating properties now renews root sub-doc ids 2023-09-18 14:12:30 +02:00
Thaum Rystra
673f187373 Removed stray function 2023-09-18 13:48:30 +02:00
Thaum Rystra
d058039464 Renewing Ids now renews sub-doc ids as well 2023-09-18 13:46:54 +02:00
Thaum Rystra
c7bb4b8097 Fixed issue where duplicated point buys cause row-id collision in dep graph 2023-09-18 13:31:48 +02:00
ThaumRystra
d57e49f969 Began rewrite of all parenting functions to nested sets
What have I gotten myself into :(
2023-09-13 23:18:03 +02:00
Jonpot
9efd38b2fb Unbumped version in package.json 2023-09-13 05:39:29 -07:00
ThaumRystra
00395a3e79 Added comment to start pull request 2023-09-13 11:18:57 +02:00
ThaumRystra
c251b70ff6 Breaking: children of notes are no longer inactive 2023-09-13 11:08:55 +02:00
ThaumRystra
f1b0019331 Added pull request template 2023-09-13 10:35:07 +02:00
ThaumRystra
1b21e69b40 Only top-level notes are displayed in journal 2023-09-13 10:05:45 +02:00
ThaumRystra
9aa8a50c81 Merge pull request #335 from KatrinaKitten/add-reference-tag-extensions
Make references add their own tags to the referenced property on insert
2023-09-07 04:06:47 -07:00
Katrina Scialdone
cbb83f2ef3 Use lodash union instead of convert to Set and back 2023-09-07 04:47:40 -06:00
ThaumRystra
02f2361389 Merge pull request #337 from KatrinaKitten/fix-function-resolution-errors
Fix erroring functions causing a compile error
2023-09-07 03:44:46 -07:00
ThaumRystra
88eb09d03e Merge pull request #319 from captainbowtie/docker-v2
captainbowtie Updated docker files
2023-09-07 03:29:14 -07:00
Stefan Zermatten
64e4d80b5b A little bit of threejs prototyping 2023-09-05 23:31:40 +02:00
Katrina Scialdone
6ecfaa9df5 Fix erroring functions causing a compile error
If a function's implementation creates a Javascript error, it would return as a compile error due to the `error` variable being shadowed internally by this try-catch. This commit fixes that, as well as clarifies the fixed error messages with "internal error".
2023-09-04 18:23:24 -06:00
Stefan Zermatten
1847525e62 iteration on tabletop UI 2023-09-04 14:02:45 +02:00
Stefan Zermatten
4ea28acdee Merge commit 'd9b978cb82109b71c05d03807a8558ba1dc537a4' into feature-tabletop 2023-09-04 11:02:13 +02:00
Katrina Scialdone
078fa7cbeb Make references add their own tags to the ref'd property on insert 2023-09-03 16:26:37 -06:00
Katrina Scialdone
78be0737cb Fix Reference missing from type search in library tree 2023-09-03 15:52:02 -06:00
Stefan Zermatten
d9b978cb82 Merge pull request #331 from piepie62/develop
Show effects on Damage properties in action view
2023-08-29 10:45:39 +02:00
Chris Feger
33d7b52325 Show effects on Damage properties in action view 2023-08-24 13:00:19 -07:00
Stefan Zermatten
9537eaa3e6 Began adding save-for-half to damage props 2023-08-24 15:16:48 +02:00
Stefan Zermatten
cc3913db1d Merge branch 'master' into develop 2023-08-24 14:51:22 +02:00
Stefan Zermatten
75ea462e64 Added conditions to action resources 2023-08-24 14:51:09 +02:00
Stefan Zermatten
206fbb7586 Bumped version 2023-08-24 13:04:00 +02:00
Stefan Zermatten
46016b40ed Merge branch 'develop' 2023-08-24 13:03:37 +02:00
Stefan Zermatten
7fa993de47 Slot fill now shows selected slots while searching 2023-08-24 13:00:26 +02:00
Stefan Zermatten
d973463126 Prevented props deactivated by toggles from firing 2023-08-24 12:35:54 +02:00
Stefan Zermatten
d578a87632 parser fix for chain indexing into nested arrays
`[ [‘a1’, ‘a2’ ], [‘b1’, ‘b2’] ][2][1]` => `'b1'`
2023-08-24 12:29:44 +02:00
Stefan Zermatten
0ee77d705a Fixed actions erroring with undefined resources 2023-08-24 12:19:55 +02:00
Stefan Zermatten
90c92cdd8c Prevented character color button from going camo 2023-08-24 12:15:32 +02:00
Stefan Zermatten
1778111c75 Toggles now wont apply tag targeting if deactivated 2023-08-24 12:09:14 +02:00
Stefan Zermatten
0df7763366 Cleaned up bad messy floats in container weights 2023-08-24 11:42:47 +02:00
Stefan Zermatten
8b23a4bc24 Clean props when copying creature -> lib
prevents creature specific fields from leaking
into libraries
2023-08-24 11:28:02 +02:00
Stefan Zermatten
f794dbf45a Slots act as folder when used in actions 2023-08-24 11:21:39 +02:00
Stefan Zermatten
b2f47052aa Fixed ol and ul number truncation in prop viewers 2023-08-24 11:17:26 +02:00
Stefan Zermatten
7c2b7419d9 Fixed content weightless carried containers weight 2023-08-24 11:10:19 +02:00
Stefan Zermatten
b8387c5ab1 Added "after children" trigger timing 2023-08-24 10:56:13 +02:00
Stefan Zermatten
5909c985e3 Fixed spell slots with non-standard values ui 2023-08-24 10:33:45 +02:00
Stefan Zermatten
61db08a19b Long attribute consumed values fixed, string values disable action 2023-08-24 10:24:06 +02:00
Stefan Zermatten
2fc316de36 Removed redis-oplog, it wasn't working 2023-08-24 10:11:59 +02:00
Stefan Zermatten
e7acdc259c Bumped package number 2023-08-14 09:56:09 +02:00
Stefan Zermatten
686a85c768 Hotfix XSS vulnerability due to failed sanitization 2023-08-14 09:55:45 +02:00
Thaum Rystra
8b215ce159 Fixed slot fill cards not column layouting 2023-08-07 12:37:37 +02:00
Stefan Zermatten
b0208c76ca Moved menu up a bit because h-scrollbar 2023-08-02 15:56:05 +02:00
Stefan Zermatten
03e694fcea Merge branch 'develop' into feature-tabletop 2023-08-02 11:36:01 +02:00
Stefan Zermatten
e89b4946d5 Improvements to level up and slot fill
- class level refs work with level up
- Improve UI
- Fixed level up backfill repeating levels when selecting higher levels
multiple times
- Allowed user to ignore slot fill condition
- Auto load more if many disabled fillers
2023-08-02 11:27:17 +02:00
Stefan Zermatten
5240faeaff Merge remote-tracking branch 'origin/master' into develop 2023-08-02 08:54:53 +02:00
Stefan Zermatten
d9e0679734 Hotfix order increment breaking property insert 2023-08-01 12:09:21 +02:00
Stefan Zermatten
31c2580a9b Bumped version 2023-08-01 11:30:53 +02:00
Stefan Zermatten
e86a1269c9 Merge branch 'develop' 2023-08-01 11:29:59 +02:00
Stefan Zermatten
a262d773c0 Reduced snackbar timeout to 15s 2023-08-01 11:28:39 +02:00
Stefan Zermatten
7ea972d476 Fixed level up backfill selecting too many props
Fixed out of order
2023-08-01 11:28:17 +02:00
Stefan Zermatten
0e5bf39958 Bumped version 2023-07-21 16:18:50 +02:00
Stefan Zermatten
5d8485123e Merge branch 'develop' 2023-07-21 16:17:42 +02:00
Stefan Zermatten
85f13713f2 Merge branch 'develop' of https://github.com/ThaumRystra/DiceCloud into develop 2023-07-21 16:14:14 +02:00
Stefan Zermatten
b0afc86ad4 "fixed" column layout again
As yet untested on Safari
2023-07-21 16:12:50 +02:00
Stefan Zermatten
30fabce7f1 Removed variables object from creature docs 2023-07-20 11:13:57 +02:00
Stefan Zermatten
4133a0f78c Fixed proficiency bonus not applying in actions 2023-07-19 19:40:59 +02:00
Stefan Zermatten
2b1a6de1e5 Relaxed rate limiting on duplicating library props 2023-07-19 19:03:36 +02:00
Stefan Zermatten
25e2523d51 Fixed resources in folder increment button loading forever 2023-07-19 18:57:06 +02:00
Stefan Zermatten
7072e9ba97 Fixed searching by tag in slot fillers 2023-07-19 18:49:18 +02:00
Stefan Zermatten
6dcce2e36a Improved tabletop creature bar 2023-07-17 15:56:30 +02:00
Stefan Zermatten
a568510f43 Progress on Tabletop CRPG-style design 2023-07-07 13:49:31 +02:00
Stefan Zermatten
eef4adfab7 Started on new Tabletop design idea 2023-07-07 09:55:55 +02:00
Stefan Zermatten
7796d9de76 Merge commit 'ace284e265a65006263fcecab7865c5e1ebd899f' into feature-tabletop 2023-06-30 13:44:53 +02:00
Stefan Zermatten
b3ed77964f Hotfix library select turning into links incorrectly 2023-06-28 10:22:38 +02:00
Stefan Zermatten
0899343717 Re-enabled tabletop links 2023-06-27 16:25:35 +02:00
Stefan Zermatten
23af519691 Merge branch 'master' into version-2-tabletop 2023-06-27 10:41:44 +02:00
Stefan Zermatten
912fff64a8 Bumped version 2023-06-27 09:33:22 +02:00
Stefan Zermatten
22d51eacab Added second library tree for multi-track drifting 2023-06-26 16:31:23 +02:00
Stefan Zermatten
7562e29fac Increased power of tree searching 2023-06-26 14:45:19 +02:00
Stefan Zermatten
d4cac831e6 Sorted lib browser by sub count 2023-06-24 00:55:22 +02:00
Stefan Zermatten
5112ecd0c7 Fixed migration not counting collection subs 2023-06-24 00:50:34 +02:00
Stefan Zermatten
6c7308ebf8 Fixed library options not showing in create dialog 2023-06-24 00:28:03 +02:00
Stefan Zermatten
c50c512587 Fixed buff $target.var -> ~target.var
to skip crystallization
2023-06-23 23:31:54 +02:00
Stefan Zermatten
93dfbc8a93 Fixed search for library prop not using tags 2023-06-23 23:29:46 +02:00
Stefan Zermatten
2c89323764 Fixed errors with empty quantity ammo resources 2023-06-23 12:21:51 +02:00
Stefan Zermatten
33576e02fa Fixed users failing to create because of a bad hook 2023-06-23 09:55:48 +02:00
Stefan Zermatten
81cfc3919e Improved migrations 2023-06-23 09:42:14 +02:00
Stefan Zermatten
bae621cd47 Hotfix migrations 2023-06-22 15:52:42 +02:00
Stefan Zermatten
70edd7b2c0 Don't batch prop updates in migration to save memory
Should run slower, but within memory constraints
2023-06-22 13:45:55 +02:00
Stefan Zermatten
9504b4299b Bumped version 2023-06-22 12:08:06 +02:00
Stefan Zermatten
6a381a5e09 Merge commit 'd7abb591e2b47c088b15ed23e825280338b02632' 2023-06-22 12:06:15 +02:00
Stefan Zermatten
d7abb591e2 Prevented error if old data is used in char calc 2023-06-22 11:43:45 +02:00
Stefan Zermatten
83537f1c24 Fixed regression with refs failing to reify 2023-06-22 11:21:17 +02:00
Stefan Zermatten
8abd629fb6 Folders now only hide their immediate children 2023-06-22 10:50:04 +02:00
Stefan Zermatten
3843fcff97 Click on reference props to go to them 2023-06-21 16:15:44 +02:00
Stefan Zermatten
d9ef848c4e Ammo doesn't need to be equipped 2023-06-21 15:27:59 +02:00
Stefan Zermatten
c1544213e7 Ammo used by an action now applies its children 2023-06-21 15:27:40 +02:00
Stefan Zermatten
4f4779c3e5 Fixed migration issue with slot.slotType = slotFiller 2023-06-21 14:31:15 +02:00
Stefan Zermatten
7457372e13 Added "Copy to Library" 2023-06-21 14:30:48 +02:00
Stefan Zermatten
fcdb7ca287 Fixed +child button not disabled when no permission 2023-06-21 13:11:08 +02:00
Stefan Zermatten
bc1c57de85 Duplicate property changes variableName by default 2023-06-21 13:10:22 +02:00
Stefan Zermatten
b42a873a5f Made owner of sheets and libraries more visible 2023-06-21 13:00:07 +02:00
Stefan Zermatten
77ae2d9de8 Added Vibes, Kell of Nothing to about page 2023-06-20 13:31:14 +02:00
Stefan Zermatten
cbb8d3f184 Improved UX of importing archive files 2023-06-20 13:30:52 +02:00
Stefan Zermatten
beb4d94676 Added archive migrations to schema version 2 2023-06-20 13:30:35 +02:00
Stefan Zermatten
3af4528788 Fixed drag and drop of characters between parties 2023-06-14 22:34:17 +02:00
Stefan Zermatten
4b9802d6a0 Removed slotFillers from every part of the app 2023-06-14 20:11:00 +02:00
Stefan Zermatten
fad59f8674 Added tag targeted toggles
May God have mercy on us all
2023-06-14 15:49:08 +02:00
Stefan Zermatten
c24247cf38 Replaced dash-minus with unicode minus in most places 2023-06-14 13:57:30 +02:00
Stefan Zermatten
04de76d20e Skills can now apply to calcs by tag 2023-06-14 13:56:44 +02:00
Stefan Zermatten
442aea2bbe Forms try to hold your place better
Expanding form sections keeps them expanded when
changing property viewed or opening a new prop
Disabled auto-focus, because it forces scroll
2023-06-13 14:27:32 +02:00
Stefan Zermatten
1fe7ed8972 Fixed library form details missing in skills 2023-06-13 13:50:30 +02:00
Stefan Zermatten
957aabcb82 Improved property viewers
Slot fill data included in library viewer
Breadcrumbs and children in lib view
breadcrumbs and children work on tree tab
2023-06-13 12:48:35 +02:00
Stefan Zermatten
c580970d6d Dark mode now free, respects device theme 2023-06-13 11:15:21 +02:00
Stefan Zermatten
8954668f5a Added migration for $ to ~ in calcs 2023-06-12 23:02:01 +02:00
Stefan Zermatten
c314c0ab05 Added basic community library browser 2023-06-12 22:16:20 +02:00
Stefan Zermatten
9ae8d63fc4 Fixed, saving throw changing target of later props
the target will only be changed for the children of
the save
2023-06-07 15:20:04 +02:00
Stefan Zermatten
40b04e519f Fixed folders in same location being out of tree order 2023-06-07 15:11:35 +02:00
Stefan Zermatten
308f3e735b Removed "hide when full" switch from slots
It currently doesn't do anything in this iteration of slot UI
2023-06-07 15:06:28 +02:00
Stefan Zermatten
f66190463a Fixed spell lists w/ no max prepared can't prepare 2023-06-07 15:01:00 +02:00
Stefan Zermatten
3950db8672 Passive bonus now gets +-5 with adv/disadvantage 2023-06-07 14:58:32 +02:00
Stefan Zermatten
af421eef9c Removed references to DiceCloud being in beta 2023-06-07 14:54:12 +02:00
Stefan Zermatten
26affda339 Fixed rest triggered buffs not recalculating sheet 2023-06-07 14:51:53 +02:00
Stefan Zermatten
60172f8a31 Fixed errors logging when some fields aren't used 2023-06-07 14:43:20 +02:00
Stefan Zermatten
ea02416353 Fixed critical hit target changing 2023-06-07 14:43:01 +02:00
Stefan Zermatten
f7461f40d6 Fixed calculated toggles not hiding some props from the sheet 2023-06-07 14:35:42 +02:00
Stefan Zermatten
e49dea469f Fixed bug where buff would delete parent prop
in group card view
2023-06-07 14:25:28 +02:00
Stefan Zermatten
85d97abbee Made sure atts respect damage rules on recalc 2023-06-07 14:19:31 +02:00
Stefan Zermatten
c00e618f85 Added library node "searchable" switch 2023-06-07 14:19:06 +02:00
Stefan Zermatten
6e47395327 Fixed some error text not showing in create dialog 2023-06-07 13:52:21 +02:00
Stefan Zermatten
3acf42394d Fixed errors thrown when overloading discord webhooks 2023-06-07 13:50:14 +02:00
Stefan Zermatten
6bc737f850 Fixed advantage with new action scope prefix: '~' 2023-06-07 12:29:54 +02:00
Stefan Zermatten
4d6c6b6094 Added advantage to attributes
ability checks will be automatically made with adv
when appropriate
2023-06-07 12:29:29 +02:00
Stefan Zermatten
15ff16bb8c Fixed multi-click on restore archive character 2023-06-07 11:57:28 +02:00
Stefan Zermatten
d4e5a2a529 Fixed visual glitch with filling 1 space slot with 0 cost filler 2023-06-07 11:55:31 +02:00
Stefan Zermatten
6291071e0d Fixed overriden props showing up in stats tab 2023-06-07 11:37:44 +02:00
Stefan Zermatten
35ebed81dd Fixed slot fill test in library prop edit 2023-06-07 11:16:07 +02:00
Stefan Zermatten
54e54ef5a8 Removed unused dialogs 2023-06-07 10:44:49 +02:00
Stefan Zermatten
99b5ad4e82 Fixed library node insert button 2023-06-07 10:36:45 +02:00
Stefan Zermatten
e068cf27b3 Added multi level up to the level up dialog 2023-06-07 10:00:18 +02:00
Stefan Zermatten
56ca4b1680 Improved slot fill UI
Added custom button
Prop insert form disabled +child
No backdrop close creation forms
2023-06-06 12:35:22 +02:00
Stefan Zermatten
a25ab2040c removed some css to improve chrome col layout perf 2023-06-05 15:45:26 +02:00
Stefan Zermatten
1096c53f49 Improved slot filling UI usability 2023-06-05 15:44:53 +02:00
Stefan Zermatten
513c0f7148 Added health check api endpoint 2023-06-01 11:19:17 +02:00
Stefan Zermatten
2b4ab6258d Added redis-oplog as submodule 2023-06-01 11:01:39 +02:00
Stefan Zermatten
376d3bc522 Added redis oplog with collection caching 2023-06-01 08:30:12 +02:00
Stefan Zermatten
b402fdf517 Moved dep graph to creature form, improved style 2023-05-29 13:18:27 +02:00
Stefan Zermatten
16d3ea9d53 Added dependency graph viewer 2023-05-29 10:38:38 +02:00
Stefan Zermatten
c6a3619178 Added tag-targeted profs to calculation viewers 2023-05-17 13:50:22 +02:00
Stefan Zermatten
1795316664 Fixed regression: missing target field 2023-05-17 13:22:58 +02:00
Stefan Zermatten
862e25eb0c Added tag targeting to proficiency form 2023-05-17 11:42:35 +02:00
Stefan Zermatten
0a3ea7672f Form overhaul: roll -> trigger
roll, savingThrow, skill, slot, slotfiller, spell, spellList,
toggle, trigger
2023-05-17 11:32:39 +02:00
Stefan Zermatten
4c34986fb7 Form overhaul: Reference Form 2023-05-16 21:50:50 +02:00
Stefan Zermatten
bf6fb358e6 Form overhaul: Proficiency Form 2023-05-16 21:24:21 +02:00
Stefan Zermatten
9f01b85df3 Form overhaul: Point Buy Form 2023-05-16 21:21:25 +02:00
Stefan Zermatten
ce07766fb4 Form overhaul: Note Form 2023-05-16 21:03:26 +02:00
Stefan Zermatten
6133f25416 Form overhaul: Item Form 2023-05-16 21:02:26 +02:00
Stefan Zermatten
f120ddb75a Form ovehaul: Folder Form 2023-05-16 20:56:20 +02:00
Stefan Zermatten
c8a53a0235 Form overhaul: Feature Form 2023-05-16 20:54:38 +02:00
Stefan Zermatten
b65b4b4497 Form overhaul: Effect Form 2023-05-16 20:53:36 +02:00
Stefan Zermatten
8a4bfa8475 Form overhaul: Damage Multiplier Form 2023-05-16 20:51:56 +02:00
Stefan Zermatten
ea2416aaea Form overhaul: Damage Form 2023-05-16 20:41:30 +02:00
Stefan Zermatten
44703a5aa5 Form overhaul: Container Form 2023-05-16 20:37:32 +02:00
Stefan Zermatten
53958fde92 Form overhaul: Constant Form 2023-05-16 20:30:21 +02:00
Stefan Zermatten
0dbd5903b3 Form overhaul: Class Level Form 2023-05-16 20:26:44 +02:00
Stefan Zermatten
57ca3ecb01 Form overhaul: Class Form 2023-05-16 20:20:39 +02:00
Stefan Zermatten
1bc48330e0 Extracted tag targeting form into a component 2023-05-16 20:02:59 +02:00
Stefan Zermatten
0b8d824b2d Removed stray name field from att form 2023-05-16 19:34:24 +02:00
Stefan Zermatten
ff2c5f5427 Form overhaul: Buff Remover Form 2023-05-16 19:28:49 +02:00
Stefan Zermatten
2c6cd7d243 Created smart toggles for limited choice fields 2023-05-16 19:28:32 +02:00
Stefan Zermatten
baf99c65b3 Form overhaul: Buff form 2023-05-16 17:02:06 +02:00
Stefan Zermatten
b82e3d6f4c Form overhaul: branch form 2023-05-16 16:58:38 +02:00
Stefan Zermatten
35f6037236 Form overhaul: Attribute form 2023-05-16 16:54:16 +02:00
Stefan Zermatten
8289e9bd11 Form overhaul: Adjustment form 2023-05-16 16:37:42 +02:00
Stefan Zermatten
35c48ccd33 Form overhaul: Action form 2023-05-16 13:17:49 +02:00
Stefan Zermatten
b7be15ad70 Fixed package issue breaking vue slots 2023-05-07 00:23:00 +02:00
Stefan Zermatten
b82061b8d4 Fixed regression: spell slot bubbles are clickable again 2023-05-06 22:59:26 +02:00
Stefan Zermatten
142072d810 Debounced resource up/down with optimistic ui 2023-05-06 22:58:53 +02:00
Stefan Zermatten
4550661a59 Tested and fixed proficiencies by tag 2023-05-06 10:45:03 +02:00
Stefan Zermatten
9fb85b8c50 Updated packages 2023-05-05 14:25:10 +02:00
Allen Barr
7eda742e69 Update docker files to work with v2
Updated docker files to work with version 2, and added mongo container to compose file
2023-05-03 13:23:50 -05:00
Stefan Zermatten
30a0c4d2a9 Added warning for >1k props 2023-05-01 20:09:54 +02:00
Stefan Zermatten
16de798916 slot fill filter now looks at libraryTags not tags 2023-05-01 18:30:13 +02:00
Stefan Zermatten
656a079c58 Expand build slot tree by 2 levels by default 2023-04-21 11:31:03 +02:00
Stefan Zermatten
93b0fe1885 Added noBackdropClose to prop create dialog 2023-04-21 11:18:35 +02:00
Stefan Zermatten
0bf5954472 Updated packages 2023-04-21 11:17:46 +02:00
Stefan Zermatten
9e4bbe0d1b Progress aligning and improving node/prop forms 2023-04-20 15:37:12 +02:00
Stefan Zermatten
a58ccc0e0e Outlined snackbars in red 2023-04-20 15:05:45 +02:00
Stefan Zermatten
ad7166f576 Made custom outlined fields work on backgrounds 2023-04-20 12:18:29 +02:00
Stefan Zermatten
cf09abaa57 Progress on forms overhaul: insert lib node broken 2023-04-17 21:43:46 +02:00
Stefan Zermatten
d643886a7f Added migration for libraryTags 2023-04-17 15:28:06 +02:00
Stefan Zermatten
90235a5bc6 Merge branch 'develop' of https://github.com/ThaumRystra/DiceCloud into develop 2023-04-14 13:04:49 +02:00
Stefan Zermatten
775e1fa842 Merge pull request #316 from Jonpot/patch-7
Fix triggerMatchTags function to correctly handle 'NOT' operation
2023-04-14 13:03:53 +02:00
Stefan Zermatten
288a086ffe Fixed typo in trigger test 2023-04-14 13:02:27 +02:00
Stefan Zermatten
e961fd2b98 Added failing test case for #316 trigger match bug 2023-04-14 12:55:30 +02:00
Stefan Zermatten
32e5b0a9f6 fixed: props all have slotQuantityFilled now 2023-04-14 12:02:46 +02:00
Stefan Zermatten
b914415ef0 Merge pull request #312 from AaronTraas/patch-1
Fix link to dicecloud.com
2023-04-14 11:34:12 +02:00
Stefan Zermatten
2ee3fe1e12 Merge pull request #314 from Jonpot/patch-5
Fix health bar filtering in dealDamage function
2023-04-14 11:31:56 +02:00
Jonpot
9992da711a Fix triggerMatchTags function to correctly handle 'NOT' operation
This pull request addresses an issue in the triggerMatchTags function where it returned an incorrect result when the targetTags property was empty and the 'NOT' operation was evaluated in the extraTags. The function would return true instead of false when it should have.

Changes:

- Replaced the forEach loop with a for...of loop to iterate through trigger.extraTags to properly set the matched variable and break the loop when necessary.
- Updated the condition for the 'NOT' operation to set matched to false and break the loop when the condition is met.
2023-04-06 14:49:07 -07:00
Jonpot
c3c05a0727 Fix health bar filtering in dealDamage function
This pull request addresses an issue with the dealDamage function where health bars with the healthBarNoHealing flag set to True were still being healed. The problem was related to the logic used to filter out health bars that should not be affected by the current damage type.

Changes:

- Replaced the remove function with the native JavaScript filter function.
- Updated the filter condition to correctly filter health bars based on the damage type (healing or damage).
2023-04-06 12:20:44 -07:00
Stefan Zermatten
53e88af93a Worked on universal property form 2023-04-03 16:35:29 +02:00
Stefan Zermatten
b9588c83b1 Added dice functions to parse engine 2023-04-01 11:27:52 +02:00
Stefan Zermatten
8e610c2cd8 Slot fill fields given to all lib nodes 2023-03-31 12:21:30 +02:00
Stefan Zermatten
25e053c473 Merge branch 'develop' of https://github.com/ThaumRystra/DiceCloud into develop 2023-03-31 12:20:34 +02:00
Stefan Zermatten
76a0918a78 Fixed crash where null ability broke skills 2023-03-31 12:19:15 +02:00
Aaron Traas
61e72ad874 Fix link to dicecloud.com
The link wasn't working, because the protocol wasn't in the link, therefore Github's Markdown processor was treating it as a relative link.
2023-03-23 11:53:20 -04:00
Stefan Zermatten
50cb6185ce Added proficiency target by tag to backend 2023-03-17 17:45:05 +02:00
Stefan Zermatten
2b7851ab32 Added smart-disconnect + connection notifications 2023-02-27 14:23:50 +02:00
Stefan Zermatten
deca9786b9 Fixed grammar to account for d8 not being a name 2023-01-31 20:55:35 +02:00
Stefan Zermatten
67da641244 Migrated internal variables to ~ prefix 2023-01-31 20:36:26 +02:00
Stefan Zermatten
16f5fe91ea Normalized all scope['$...'] to be unwrapped values 2023-01-31 15:55:02 +02:00
Stefan Zermatten
cbbbcaf56a Changed variable name regex to allow $ and _ 2023-01-31 14:51:39 +02:00
Stefan Zermatten
72d932538b Fixed bug where rolls could not set strings 2023-01-31 14:51:22 +02:00
Stefan Zermatten
265e3bf970 Merge branch 'master' into develop 2023-01-31 13:53:38 +02:00
Stefan Zermatten
ace284e265 removed stray log 2023-01-21 12:15:09 +02:00
Stefan Zermatten
ceb170cbcf Tabletop targeted actions now work 2023-01-15 14:18:17 +02:00
Stefan Zermatten
25e6b19b49 Progress on tabletop UI design 2023-01-15 01:17:00 +02:00
Stefan Zermatten
cd45008f38 Merge branch 'develop' into version-2-tabletop 2023-01-14 19:12:17 +02:00
Stefan Zermatten
e0a397af78 Merge pull request #308 from ThaumRystra/dependabot/npm_and_yarn/app/qs-6.5.3
Bump qs from 6.5.2 to 6.5.3 in /app
2023-01-14 14:24:52 +02:00
Stefan Zermatten
3532898be9 updated jsconfig 2023-01-14 14:20:39 +02:00
dependabot[bot]
055ddefae6 Bump qs from 6.5.2 to 6.5.3 in /app
Bumps [qs](https://github.com/ljharb/qs) from 6.5.2 to 6.5.3.
- [Release notes](https://github.com/ljharb/qs/releases)
- [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ljharb/qs/compare/v6.5.2...v6.5.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-12-07 19:15:37 +00:00
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
16af33b52f Began designing on-sheet initiative 2022-11-28 00:23:52 +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
2015e674dc fixed broken imports 2022-11-24 11:35:34 +02:00
Stefan Zermatten
2ebd598546 Merge branch 'version-2-dev' into version-2-tabletop 2022-11-23 16:00:31 +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
55eae9ceca Update ActionForm.vue
As is, the hints for the Summary and Description field are outdated
2022-11-21 16:14:55 -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
cc915410da Fixed query hitting mongo with too much regex 2022-10-25 19:00:02 +02:00
Stefan Zermatten
104095e4ae Merge branch 'version-2' into version-2-tabletop 2022-10-22 19:29:31 +02:00
Stefan Zermatten
a5b4b20324 Added link to V2 2022-10-11 14:08:33 +02:00
Stefan Zermatten
8af5734c93 THREE? 2022-04-25 11:03:17 +02:00
Stefan Zermatten
13bf1ff410 Merge branch 'version-2-dev' into version-2-tabletop 2022-04-23 15:18:03 +02:00
Stefan Zermatten
afb76f6ac4 Iterated on tabletop 2022-04-15 22:36:20 +02:00
Stefan Zermatten
3235d81684 Added actions to tabletop, used character logs instead. 2022-04-15 20:26:10 +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
1040 changed files with 60595 additions and 29441 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "app/packages/redis-oplog"]
path = app/packages/redis-oplog
url = https://github.com/ramezrafla/redis-oplog.git

17
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,17 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Meteor: Test",
"type": "node",
"request": "launch",
"cwd": "${workspaceFolder}/app",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run-script",
"test"
],
"outputCapture": "std",
}
]
}

50
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,50 @@
{
"cSpell.words": [
"alea",
"armor",
"autorun",
"blackbox",
"cantrip",
"Cantrips",
"Crit",
"Crits",
"cyrb",
"denormalize",
"denormalized",
"EJSON",
"healthbar",
"healthbars",
"Hitpoints",
"jank",
"meteortesting",
"multigraph",
"nearley",
"ngraph",
"nonreactive",
"ostrio",
"pather",
"recomputation",
"Ruleset",
"snackbars",
"Spellcasting",
"Subheaders",
"thumbhash",
"uncomputed",
"untarget",
"vars",
"vuedraggable",
"vuetify",
"Vuex",
"walkdown"
],
"javascript.preferences.importModuleSpecifier": "non-relative",
"javascript.preferences.importModuleSpecifierEnding": "minimal",
"javascript.preferences.organizeImports": {
"enabled": true,
},
"typescript.preferences.importModuleSpecifier": "non-relative",
"typescript.preferences.importModuleSpecifierEnding": "minimal",
"typescript.preferences.organizeImports": {
"enabled": true,
}
}

View File

@@ -1,14 +1,29 @@
FROM ubuntu:latest
RUN apt-get update --quiet \
&& apt-get install --quiet --yes \
bsdtar \
curl \
git
RUN ln --symbolic --force $(which bsdtar) $(which tar)
RUN useradd --create-home --shell /bin/bash dicecloud
USER dicecloud
WORKDIR /home/dicecloud
RUN curl https://install.meteor.com/?release=1.8.0.2 | sh
ENV PATH="${PATH}:/home/dicecloud/.meteor"
COPY dev.sh ./dev.sh
ENTRYPOINT ./dev.sh
FROM ubuntu:jammy
USER root
RUN adduser --system mt
RUN apt-get update && apt-get install -y ca-certificates curl gnupg git
RUN mkdir -p /etc/apt/keyrings
RUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key \
| gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
RUN echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" \
> /etc/apt/sources.list.d/nodesource.list
RUN apt-get update && apt-get install -y nodejs
USER mt
RUN curl https://install.meteor.com/ | sh
WORKDIR /home/mt
RUN git clone https://github.com/ThaumRystra/DiceCloud dicecloud
WORKDIR /home/mt/dicecloud/app
RUN npm install --production
ENV PATH=$PATH:/home/mt/.meteor
RUN meteor build --directory ~/dc/ --architecture os.linux.x86_64
WORKDIR /home/mt/dc/bundle/programs/server
RUN npm install
WORKDIR /home/mt/dc/bundle
RUN rm -r /home/mt/dicecloud
ENTRYPOINT node main.js

View File

@@ -1,7 +1,7 @@
DiceCloud
========
This is the repo for [DiceCloud](dicecloud.com).
This is the repo for [DiceCloud](https://dicecloud.com).
DiceCloud is a free, auditable, real-time character sheet for D&D 5e.

5
app/.gitignore vendored
View File

@@ -3,6 +3,10 @@
.demeteorized
.cache
.vscode
.coverage
.nyc_output
.DS_Store
fileStorage
settings.json
public/components
public/_imports.html
@@ -10,3 +14,4 @@ private/oldClient
nohup.out
node_modules
dump
*.crt

View File

@@ -3,29 +3,27 @@
# 'meteor add' and 'meteor remove' will edit this file for you,
# but you can also edit it by hand.
accounts-password@2.3.1
random@1.2.0
underscore@1.0.10
accounts-password@2.4.0
random@1.2.1
underscore@1.6.1
dburles:mongo-collection-instances
accounts-google@1.4.0
email@2.2.1
email@2.2.6
meteor-base@1.5.1
mobile-experience@1.1.0
mongo@1.16.0
session@1.2.0
tracker@1.2.0
logging@1.3.1
mobile-experience@1.1.1
mongo@1.16.10
session@1.2.1
tracker@1.3.3
logging@1.3.4
reload@1.3.1
ejson@1.1.2
check@1.3.1
ejson@1.1.3
check@1.4.1
standard-minifier-js@2.8.1
shell-server@0.5.0
ecmascript@0.16.2
es5-shim@4.8.0
service-configuration@1.3.0
dynamic-import@0.7.2
ddp-rate-limiter@1.1.0
rate-limit@1.0.9
service-configuration@1.3.4
dynamic-import@0.7.3
ddp-rate-limiter@1.2.1
rate-limit@1.1.1
mdg:validated-method
static-html@1.3.2
aldeed:collection2
@@ -37,9 +35,8 @@ simple:rest
simple:rest-method-mixin
mikowals:batch-insert
peerlibrary:subscription-data
seba:minifiers-autoprefixer
zer0th:meteor-vuetify-loader
akryum:vue-component
akryum:vue-component@0.15.2
akryum:vue-router2
percolate:migrations
meteortesting:mocha
@@ -47,5 +44,12 @@ ostrio:files
simple:rest-bearer-token-parser
simple:rest-json-error-handler
littledata:synced-cron
mdg:meteor-apm-agent
typescript@4.5.4
#mdg:meteor-apm-agent
seba:minifiers-autoprefixer
#mixmax:smart-disconnect
zodern:types
zodern:fix-async-stubs
typescript@4.9.5
ecmascript@0.16.8
lmieulet:meteor-legacy-coverage
lmieulet:meteor-coverage

View File

@@ -1 +1 @@
METEOR@2.8.0
METEOR@2.16

View File

@@ -1,87 +1,86 @@
accounts-base@2.2.4
accounts-base@2.2.11
accounts-google@1.4.0
accounts-oauth@1.4.1
accounts-password@2.3.1
accounts-oauth@1.4.4
accounts-password@2.4.0
accounts-patreon@0.1.0
akryum:npm-check@0.1.2
akryum:vue-component@0.15.2
akryum:vue-component@0.16.0
akryum:vue-component-dev-client@0.4.7
akryum:vue-component-dev-server@0.1.4
akryum:vue-router2@0.2.3
aldeed:collection2@3.5.0
aldeed:schema-index@3.0.0
aldeed:schema-index@3.1.0
allow-deny@1.1.1
autoupdate@1.8.0
babel-compiler@7.9.2
babel-compiler@7.10.5
babel-runtime@1.5.1
base64@1.0.12
binary-heap@1.0.11
blaze-tools@1.1.3
boilerplate-generator@1.7.1
bozhao:link-accounts@2.6.1
blaze-tools@1.1.4
boilerplate-generator@1.7.2
bozhao:link-accounts@2.8.0
caching-compiler@1.2.2
caching-html-compiler@1.2.1
callback-hook@1.4.0
check@1.3.1
coffeescript@2.4.1
caching-html-compiler@1.2.2
callback-hook@1.5.1
check@1.4.1
coffeescript@2.7.0
coffeescript-compiler@2.4.1
dburles:mongo-collection-instances@0.3.6
ddp@1.4.0
ddp-client@2.6.0
ddp-common@1.4.0
ddp-rate-limiter@1.1.0
ddp-server@2.6.0
diff-sequence@1.1.1
dynamic-import@0.7.2
ecmascript@0.16.2
ecmascript-runtime@0.8.0
dburles:mongo-collection-instances@0.4.0
ddp@1.4.1
ddp-client@2.6.2
ddp-common@1.4.1
ddp-rate-limiter@1.2.1
ddp-server@2.7.1
diff-sequence@1.1.2
dynamic-import@0.7.3
ecmascript@0.16.8
ecmascript-runtime@0.8.1
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.6
es5-shim@4.8.0
fetch@0.1.1
geojson-utils@1.0.10
google-oauth@1.4.2
fetch@0.1.4
geojson-utils@1.0.11
google-oauth@1.4.4
hot-code-push@1.0.4
html-tools@1.1.3
htmljs@1.1.1
html-tools@1.1.4
htmljs@1.2.1
http@2.0.0
id-map@1.1.1
inter-process-messaging@0.1.1
lai:collection-extensions@0.3.0
launch-screen@1.3.0
lai:collection-extensions@0.4.0
launch-screen@2.0.0
littledata:synced-cron@1.5.1
livedata@1.0.18
lmieulet:meteor-coverage@4.1.0
lmieulet:meteor-legacy-coverage@0.2.0
localstorage@1.2.0
logging@1.3.1
mdg:meteor-apm-agent@3.5.1
mdg:validated-method@1.2.0
meteor@1.10.1
logging@1.3.4
mdg:validated-method@1.3.0
meteor@1.11.5
meteor-base@1.5.1
meteortesting:browser-tests@1.3.5
meteortesting:mocha@2.0.3
meteortesting:browser-tests@1.5.3
meteortesting:mocha@2.1.0
meteortesting:mocha-core@8.1.2
mikowals:batch-insert@1.3.0
minifier-css@1.6.1
minifier-js@2.7.5
minimongo@1.9.0
mobile-experience@1.1.0
minifier-css@1.6.4
minifier-js@2.8.0
minimongo@1.9.4
mobile-experience@1.1.1
mobile-status-bar@1.1.0
modern-browsers@0.1.8
modules@0.19.0
modules-runtime@0.13.0
mongo@1.16.0
modern-browsers@0.1.10
modules@0.20.0
modules-runtime@0.13.1
mongo@1.16.10
mongo-decimal@0.1.3
mongo-dev-server@1.1.0
mongo-id@1.0.8
mongo-livedata@1.0.12
npm-mongo@4.9.0
oauth@2.1.2
oauth2@1.3.1
npm-mongo@4.17.2
oauth@2.2.1
oauth2@1.3.2
ordered-dict@1.1.0
ostrio:cookies@2.7.2
ostrio:files@2.3.0
ostrio:cookies@2.8.1
ostrio:files@2.3.3
patreon-oauth@0.1.0
peerlibrary:assert@0.3.0
peerlibrary:check-extension@0.7.0
@@ -93,37 +92,39 @@ 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.1
promise@0.12.2
raix:eventemitter@1.0.0
random@1.2.0
rate-limit@1.0.9
react-fast-refresh@0.2.3
reactive-dict@1.3.0
reactive-var@1.0.11
random@1.2.1
rate-limit@1.1.1
react-fast-refresh@0.2.8
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.4
session@1.2.1
sha@1.0.9
shell-server@0.5.0
simple:json-routes@2.3.1
simple:rest@1.2.1
simple:rest-bearer-token-parser@1.1.1
simple:rest-json-error-handler@1.1.1
simple:rest-json-error-handler@1.1.3
simple:rest-method-mixin@1.1.0
socket-stream-client@0.5.0
spacebars-compiler@1.3.1
socket-stream-client@0.5.2
spacebars-compiler@1.3.2
standard-minifier-js@2.8.1
static-html@1.3.2
templating-tools@1.2.2
templating-tools@1.2.3
tmeasday:check-npm-versions@1.0.2
tracker@1.2.0
typescript@4.5.4
underscore@1.0.10
tracker@1.3.3
typescript@4.9.5
underscore@1.6.1
url@1.3.2
webapp@1.13.1
webapp-hashing@1.1.0
webapp@1.13.8
webapp-hashing@1.1.1
zer0th:meteor-vuetify-loader@0.1.41
zodern:fix-async-stubs@1.0.2
zodern:types@1.0.13

12328
app/client/game-icons.css Normal file

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,6 +1,7 @@
{
"public": {
"environment": "production",
"disablePatreon": true
"disablePatreon": true,
"disallowCreatureApiImport": false
}
}
}

View File

@@ -0,0 +1,3 @@
declare module 'ddp-rate-limiter-mixin' {
export const RateLimiterMixin: <T>(options: T) => T;
}

3
app/imports/@types/ddp.d.ts vendored Normal file
View File

@@ -0,0 +1,3 @@
declare namespace DDP {
function randomStream(seed: string): typeof Random;
}

View File

@@ -0,0 +1,244 @@
declare module 'meteor/ostrio:files' {
import { Meteor } from 'meteor/meteor';
import { Mongo } from 'meteor/mongo';
import { ReactiveVar } from 'meteor/reactive-var';
import { SimpleSchemaDefinition } from 'simpl-schema';
import * as http from 'http';
import { IncomingMessage } from 'connect';
interface Params {
_id: string;
query: { [key: string]: string };
name: string;
version: string;
}
interface ContextHTTP {
request: IncomingMessage;
response: http.ServerResponse;
params: Params;
}
interface ContextUser {
userId: string;
user: () => Meteor.User;
}
interface ContextUpload {
file: object;
/** On server only. */
chunkId?: number;
/** On server only. */
eof?: boolean;
}
interface Version<MetadataType> {
extension: string;
meta: MetadataType;
path: string;
size: number;
type: string;
}
class FileObj<MetadataType> {
_id: string;
size: number;
name: string;
type: string;
path: string;
isVideo: boolean;
isAudio: boolean;
isImage: boolean;
isText: boolean;
isJSON: boolean;
isPDF: boolean;
ext?: string;
extension?: string;
extensionWithDot: string;
_storagePath: string;
_downloadRoute: string;
_collectionName: string;
public?: boolean;
meta?: MetadataType;
userId?: string;
updatedAt?: Date;
versions: {
[propName: string]: Version<MetadataType>;
};
mime: string;
'mime-type': string;
}
class FileRef<MetadataType> extends FileObj<MetadataType> {
remove: (callback?: (error: Meteor.Error) => void) => void;
link: (version?: string, location?: string) => string;
get: (property?: string) => any;
fetch: () => Array<FileObj<MetadataType>>;
with: () => FileCursor<MetadataType>;
}
interface FileData<MetadataType> {
size: number;
type: string;
mime: string;
'mime-type': string;
ext: string;
extension: string;
name: string;
meta: MetadataType;
}
interface FilesCollectionConfig<MetadataType> {
storagePath?: string | ((fileObj: FileObj<MetadataType>) => string);
collection?: Mongo.Collection<FileObj<MetadataType>>;
collectionName?: string;
continueUploadTTL?: string;
ddp?: object;
cacheControl?: string;
responseHeaders?: { [x: string]: string } | ((responseCode?: string, fileRef?: FileRef<MetadataType>, versionRef?: Version<MetadataType>, version?: string) => { [x: string]: string });
throttle?: number | boolean;
downloadRoute?: string;
schema?: SimpleSchemaDefinition;
chunkSize?: number;
namingFunction?: (fileObj: FileObj<MetadataType>) => string;
permissions?: number;
parentDirPermissions?: number;
integrityCheck?: boolean;
strict?: boolean;
downloadCallback?: (this: ContextHTTP & ContextUser, fileObj: FileObj<MetadataType>) => boolean;
protected?: boolean | ((this: ContextHTTP & ContextUser, fileObj: FileObj<MetadataType>) => boolean | number);
public?: boolean;
onBeforeUpload?: (this: ContextUpload & ContextUser, fileData: FileData<MetadataType>) => boolean | string;
onBeforeRemove?: (this: ContextUser, cursor: Mongo.Cursor<FileObj<MetadataType>>) => boolean;
onInitiateUpload?: (this: ContextUpload & ContextUser, fileData: FileData<MetadataType>) => void;
onAfterUpload?: (fileRef: FileRef<MetadataType>) => any;
onAfterRemove?: (files: ReadonlyArray<FileObj<MetadataType>>) => any;
onbeforeunloadMessage?: string | (() => string);
allowClientCode?: boolean;
debug?: boolean;
interceptDownload?: (http: object, fileRef: FileRef<MetadataType>, version: string) => boolean;
}
interface SearchOptions<MetadataType, TransformAdditions> {
sort?: Mongo.SortSpecifier;
skip?: number;
limit?: number;
fields?: Mongo.FieldSpecifier;
reactive?: boolean;
transform?: (fileObj: FileObj<MetadataType>) => FileObj<MetadataType> & TransformAdditions;
}
interface InsertOptions<MetadataType> {
file: File | object | string;
fileId?: string;
fileName?: string;
isBase64?: boolean;
meta?: MetadataType;
transport?: 'ddp' | 'http';
ddp?: object;
onStart?: (error: Meteor.Error, fileData: FileData<MetadataType>) => any;
onUploaded?: (error: Meteor.Error, fileRef: FileRef<MetadataType>) => any;
onAbort?: (fileData: FileData<MetadataType>) => any;
onError?: (error: Meteor.Error, fileData: FileData<MetadataType>) => any;
onProgress?: (progress: number, fileData: FileData<MetadataType>) => any;
onBeforeUpload?: (fileData: FileData<MetadataType>) => any;
chunkSize?: number | 'dynamic';
allowWebWorkers?: boolean;
type?: string;
}
interface LoadOptions<MetadataType> {
fileName: string;
meta?: MetadataType;
type?: string;
size?: number;
userId?: string;
fileId?: string;
}
class FileUpload {
file: File;
onPause: ReactiveVar<boolean>;
progress: ReactiveVar<number>;
estimateTime: ReactiveVar<number>;
estimateSpeed: ReactiveVar<number>;
state: ReactiveVar<'active' | 'paused' | 'aborted' | 'completed'>;
pause(): void;
continue(): void;
toggle(): void;
pipe(): void;
start(): void;
on(event: string, callback: () => void): void;
}
class FileCursor<MetadataType> extends FileRef<MetadataType> { }
class FilesCursor<MetadataType, TransformAdditions> extends Mongo.Cursor<FileObj<MetadataType>> {
cursor: Mongo.Cursor<FileObj<MetadataType>>; // Refers to base cursor? Why is this existing?
get(): Array<FileCursor<MetadataType> & TransformAdditions>;
hasNext(): boolean;
next(): FileCursor<MetadataType> & TransformAdditions;
hasPrevious(): boolean;
previous(): FileCursor<MetadataType> & TransformAdditions;
first(): FileCursor<MetadataType> & TransformAdditions;
last(): FileCursor<MetadataType> & TransformAdditions;
remove(callback?: (err: object) => void): void;
each(callback: (cursor: FileCursor<MetadataType> & TransformAdditions) => void): void;
current(): object | undefined;
}
class FilesCollection<MetadataType = { [x: string]: any }> {
collection: Mongo.Collection<FileObj<MetadataType>>;
schema: SimpleSchemaDefinition;
constructor(config: FilesCollectionConfig<MetadataType>)
/**
* Find and return Cursor for matching documents.
*
* @param selector [[http://docs.meteor.com/api/collections.html#selectors | Mongo-Style selector]]
* @param options [[http://docs.meteor.com/api/collections.html#sortspecifiers | Mongo-Style selector Options]]
*
* @template TransformAdditions Additional properties provided by transforming a document with options.tranform().
* Note that removing fields with a transform function is not currently supported as this may break
* functions defined on a FileRef or FileCursor.
*/
find<TransformAdditions = {}>(
selector?: Mongo.Selector<Partial<FileObj<MetadataType>>>,
options?: SearchOptions<MetadataType, TransformAdditions>
): FilesCursor<MetadataType, TransformAdditions>;
/**
* Finds the first document that matches the selector, as ordered by sort and skip options.
*
* @param selector [[http://docs.meteor.com/api/collections.html#selectors | Mongo-Style selector]]
* @param options [[http://docs.meteor.com/api/collections.html#sortspecifiers | Mongo-Style selector Options]]
*
* @template TransformAdditions Additional properties provided by transforming a document with options.tranform().
* Note that removing fields with a transform function is not currently supported as this may break
* functions defined on a FileRef or FileCursor.
*/
findOne<TransformAdditions = {}>(
selector?: Mongo.Selector<Partial<FileObj<MetadataType>>> | string,
options?: SearchOptions<MetadataType, TransformAdditions>
): FileCursor<MetadataType> & TransformAdditions;
insert(settings: InsertOptions<MetadataType>, autoStart?: boolean): FileUpload;
remove(select: Mongo.Selector<FileObj<MetadataType>> | string, callback?: (error: Meteor.Error) => void): FilesCollection<MetadataType>;
update(select: Mongo.Selector<FileObj<MetadataType>> | string, modifier: Mongo.Modifier<FileObj<MetadataType>>, options?: {
multi?: boolean;
upsert?: boolean;
arrayFilters?: Array<{ [identifier: string]: any }>;
}, callback?: (error: Meteor.Error, insertedCount: number) => void): FilesCollection<MetadataType>;
link(fileRef: FileRef<MetadataType>, version?: string): string;
allow(options: Mongo.AllowDenyOptions): void;
deny(options: Mongo.AllowDenyOptions): void;
denyClient(): void;
on(event: string, callback: (fileRef: FileRef<MetadataType>) => void): void;
unlink(fileRef: FileRef<MetadataType>, version?: string): FilesCollection<MetadataType>;
addFile(path: string, opts: LoadOptions<MetadataType>, callback?: (err: any, fileRef: FileRef<MetadataType>) => any, proceedAfterUpload?: boolean): FilesCollection<MetadataType>;
load(url: string, opts: LoadOptions<MetadataType>, callback?: (err: object, fileRef: FileRef<MetadataType>) => any, proceedAfterUpload?: boolean): FilesCollection<MetadataType>;
write(buffer: Buffer, opts: LoadOptions<MetadataType>, callback?: (err: object, fileRef: FileRef<MetadataType>) => any, proceedAfterUpload?: boolean): FilesCollection<MetadataType>;
}
}

5
app/imports/@types/meteor.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
declare namespace Meteor {
interface User {
roles?: string[];
}
}

49
app/imports/@types/mongo.d.ts vendored Normal file
View File

@@ -0,0 +1,49 @@
type SimpleSchema = import('simpl-schema').default;
type TypedSimpleSchema<T> = import('imports/api/utility/TypedSimpleSchema').TypedSimpleSchema<T>;
declare namespace Mongo {
interface CollectionStatic {
get: <T>(
collectionName: string, options?: { connection: Meteor.Connection }
) => Mongo.Collection<T>;
}
type SchemaOptions = {
/**
* Set to `true` if your document must be passed through the collection's transform to properly validate
*/
transform?: boolean,
/**
* Set to `true` to replace any existing schema instead of combining
*/
replace?: boolean
selector?: any;
}
interface Collection<T> {
schema: TypedSimpleSchema<T>;
simpleSchema<U extends Partial<T>>(selector?: U): TypedSimpleSchema<T & U>;
/**
* Use this method to attach a schema to a collection created by another package,
* such as Meteor.users. It is most likely unsafe to call this method more than
* once for a single collection, or to call this for a collection that had a
* schema object passed to its constructor.
* @param ss SimpleSchema instance or a schema definition object from which to create a new SimpleSchema instance
* @param options Options
*
*/
attachSchema(ss: SimpleSchema | TypedSimpleSchema<T>, options?: SchemaOptions): void;
update(
selector: Selector<T> | ObjectID | string,
modifier: Modifier<T>,
options?: {
multi?: boolean | undefined;
upsert?: boolean | undefined;
arrayFilters?: Array<{ [identifier: string]: any }> | undefined;
// Add Collection2 options
selector?: Record<string, any>;
getAutoValues?: boolean;
},
callback?: FunctionConstructor,
): number;
}
}

View File

@@ -0,0 +1,27 @@
declare module 'meteor/mdg:validated-method' {
interface ValidatedMethodOptionsMixinFields<TRunArg, TRunReturn> {
rateLimit: {
numRequests: number,
timeInterval: number,
};
}
type Return<TFunc> = TFunc extends (...args: any[]) => infer TReturn ? TReturn : never;
type Argument<TFunc> = TFunc extends (...args: infer TArgs) => any ? TArgs extends [infer TArg] ? TArg
: NoArguments
: never;
interface ValidatedMethod<TName extends string, TRun extends (...args: any[]) => any> {
callAsync: Argument<TRun> extends NoArguments
// methods with no argument can be called with () or just a callback
?
& ((unusedArg: any, callback?: (error: Meteor.Error, result: Return<TRun>) => void) => void)
& ((callback?: (error: Meteor.Error | undefined, result: Return<TRun>) => void) => void)
& (() => Return<TRun>)
// methods with arguments require those arguments to be called
:
& ((
arg: Argument<TRun>,
callback?: (error: Meteor.Error | undefined, result: Return<TRun>) => void,
) => void)
& ((arg: Argument<TRun>) => Return<TRun>);
}
}

15
app/imports/@types/vue-meteor.d.ts vendored Normal file
View File

@@ -0,0 +1,15 @@
import Vue from 'vue';
declare module 'vue/types/options' {
interface ComponentOptions<V extends Vue> {
meteor?: any;
}
}
declare module 'vue/types/vue' {
interface Vue {
$subscribe: (name: string, params: any[]) => void;
$autorun: (fn: () => void) => number;
$subReady: Record<string, boolean>;
}
}

View File

@@ -1,18 +1,28 @@
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';
import { incrementFileStorageUsed } from '/imports/api/users/methods/updateFileStorageUsed';
import { CreaturePropertySchema } from '/imports/api/creature/creatureProperties/CreatureProperties';
import { CreatureSchema } from '/imports/api/creature/creatures/Creatures';
import assertUserHasFileSpace from '/imports/api/files/assertUserHasFileSpace';
let createS3FilesCollection;
if (Meteor.isServer) {
createS3FilesCollection = require('/imports/api/files/server/s3FileStorage').createS3FilesCollection
} else {
createS3FilesCollection = require('/imports/api/files/client/s3FileStorage').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)){
// Make sure the user has enough space
assertUserHasFileSpace(Meteor.userId(), file.size);
// Only accept JSON
if (!/json/i.test(file.extension)) {
return 'Please upload only a JSON file';
}
return true;

View File

@@ -1,21 +1,24 @@
import SCHEMA_VERSION from '/imports/constants/SCHEMA_VERSION.js';
import { Meteor } from 'meteor/meteor';
import SCHEMA_VERSION from '/imports/constants/SCHEMA_VERSION';
import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import { assertOwnership } from '/imports/api/creature/creatures/creaturePermissions.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js';
import Experiences from '/imports/api/creature/experience/Experiences.js';
import { removeCreatureWork } from '/imports/api/creature/creatures/methods/removeCreature.js';
import ArchiveCreatureFiles from '/imports/api/creature/archive/ArchiveCreatureFiles.js';
import { assertOwnership } from '/imports/api/creature/creatures/creaturePermissions';
import Creatures from '/imports/api/creature/creatures/Creatures';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
import CreatureLogs from '/imports/api/creature/log/CreatureLogs';
import Experiences from '/imports/api/creature/experience/Experiences';
import { removeCreatureWork } from '/imports/api/creature/creatures/methods/removeCreature';
import ArchiveCreatureFiles from '/imports/api/creature/archive/ArchiveCreatureFiles';
import { getFilter } from '/imports/api/parenting/parentingFunctions';
export function getArchiveObj(creatureId){
export function getArchiveObj(creatureId) {
// Build the archive document
const creature = Creatures.findOne(creatureId);
const properties = CreatureProperties.find({'ancestors.id': creatureId}).fetch();
const experiences = Experiences.find({creatureId}).fetch();
const logs = CreatureLogs.find({creatureId}).fetch();
if (!creature) throw new Meteor.Error('creature-not-found', 'Creature not found');
const properties = CreatureProperties.find({ ...getFilter.descendantsOfRoot(creatureId) }).fetch();
const experiences = Experiences.find({ creatureId }).fetch();
const logs = CreatureLogs.find({ creatureId }).fetch();
let archiveCreature = {
meta: {
type: 'DiceCloud V2 Creature Archive',
@@ -31,7 +34,7 @@ export function getArchiveObj(creatureId){
return archiveCreature;
}
export function archiveCreature(creatureId){
export const archiveCreature = Meteor.wrapAsync(function archiveCreatureFn(creatureId, callback) {
const archive = getArchiveObj(creatureId);
const buffer = Buffer.from(JSON.stringify(archive, null, 2));
ArchiveCreatureFiles.write(buffer, {
@@ -43,21 +46,40 @@ export function archiveCreature(creatureId){
creatureId: archive.creature._id,
creatureName: archive.creature.name,
},
}, (error) => {
if (error){
throw error;
} else {
}, (error, fileRef) => {
if (error) {
// If there is an error already, just call the callback
callback(error);
} else if (!Meteor.settings.useS3) {
// If we aren't using s3, remove the creature and call the callback
removeCreatureWork(creatureId);
callback();
} else {
// Wait for s3Result event that occurs when the s3 attempt to write ends.
// If it's successful, remove the creature, otherwise callback with error
const resultHandler = (s3Error, resultRef) => {
// This event is for a different file, ignore it
if (resultRef._id !== fileRef._id) return;
// Remove this handler, we are only running it once for this fileId
ArchiveCreatureFiles.off('s3Result', resultHandler);
// Remove the creature if there was no error
if (!s3Error) {
removeCreatureWork(creatureId);
}
// Alert the callback that we're done
callback(s3Error);
}
ArchiveCreatureFiles.on('s3Result', resultHandler);
}
}, true);
}
});
const archiveCreatureToFile = new ValidatedMethod({
name: 'Creatures.methods.archiveCreatureToFile',
validate: new SimpleSchema({
'creatureId': {
type: String,
regEx: SimpleSchema.RegEx.Id,
max: 32,
},
}).validator(),
mixins: [RateLimiterMixin],
@@ -65,10 +87,10 @@ const archiveCreatureToFile = new ValidatedMethod({
numRequests: 10,
timeInterval: 5000,
},
async run({creatureId}) {
async run({ creatureId }) {
assertOwnership(creatureId, this.userId);
if (Meteor.isServer){
archiveCreature(creatureId, this.userId);
if (Meteor.isServer) {
archiveCreature(creatureId);
} else {
removeCreatureWork(creatureId);
}

View File

@@ -1,3 +1,3 @@
import '/imports/api/creature/archive/methods/archiveCreatureToFile.js';
import '/imports/api/creature/archive/methods/restoreCreatureFromFile.js';
import '/imports/api/creature/archive/methods/removeArchiveCreature.js';
import '/imports/api/creature/archive/methods/archiveCreatureToFile';
import '/imports/api/creature/archive/methods/restoreCreatureFromFile';
import '/imports/api/creature/archive/methods/removeArchiveCreature';

View File

@@ -1,15 +1,15 @@
import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import ArchiveCreatureFiles from '/imports/api/creature/archive/ArchiveCreatureFiles.js';
import { incrementFileStorageUsed } from '/imports/api/users/methods/updateFileStorageUsed.js';
import ArchiveCreatureFiles from '/imports/api/creature/archive/ArchiveCreatureFiles';
import { incrementFileStorageUsed } from '/imports/api/users/methods/updateFileStorageUsed';
const removeArchiveCreature = new ValidatedMethod({
name: 'ArchiveCreatureFiles.methods.removeArchiveCreature',
validate: new SimpleSchema({
'fileId': {
type: String,
regEx: SimpleSchema.RegEx.Id,
max: 32,
},
}).validator(),
mixins: [RateLimiterMixin],

View File

@@ -1,24 +1,24 @@
import SCHEMA_VERSION from '/imports/constants/SCHEMA_VERSION.js';
import SCHEMA_VERSION from '/imports/constants/SCHEMA_VERSION';
import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js';
import Experiences from '/imports/api/creature/experience/Experiences.js';
import { removeCreatureWork } from '/imports/api/creature/creatures/methods/removeCreature.js';
import ArchiveCreatureFiles from '/imports/api/creature/archive/ArchiveCreatureFiles.js';
import assertHasCharactersSlots from '/imports/api/creature/creatures/methods/assertHasCharacterSlots.js';
import { incrementFileStorageUsed } from '/imports/api/users/methods/updateFileStorageUsed.js';
import verifyArchiveSafety from '/imports/api/creature/archive/methods/verifyArchiveSafety.js';
import Creatures from '/imports/api/creature/creatures/Creatures';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
import CreatureLogs from '/imports/api/creature/log/CreatureLogs';
import Experiences from '/imports/api/creature/experience/Experiences';
import { removeCreatureWork } from '/imports/api/creature/creatures/methods/removeCreature';
import ArchiveCreatureFiles from '/imports/api/creature/archive/ArchiveCreatureFiles';
import assertHasCharactersSlots from '/imports/api/creature/creatures/methods/assertHasCharacterSlots';
import { incrementFileStorageUsed } from '/imports/api/users/methods/updateFileStorageUsed';
import verifyArchiveSafety from '/imports/api/creature/archive/methods/verifyArchiveSafety';
let migrateArchive;
if (Meteor.isServer){
migrateArchive = require('/imports/migrations/server/migrateArchive.js').default;
if (Meteor.isServer) {
migrateArchive = require('/imports/migrations/archive/migrateArchive').default;
}
function restoreCreature(archive, userId){
if (SCHEMA_VERSION < archive.meta.schemaVersion){
function restoreCreature(archive, userId) {
if (SCHEMA_VERSION < archive.meta.schemaVersion) {
throw new Meteor.Error('Incompatible',
'The archive file is from a newer version. Update required to read.')
}
@@ -35,7 +35,7 @@ function restoreCreature(archive, userId){
});
if (existingCreature) throw new Meteor.Error('Already exists',
'The creature you are trying to restore already exists.')
// Ensure the user owns the restored creature
archive.creature.owner = userId;
@@ -44,13 +44,13 @@ function restoreCreature(archive, userId){
Creatures.insert(archive.creature);
try {
// Add all the properties
if (archive.properties && archive.properties.length){
if (archive.properties && archive.properties.length) {
CreatureProperties.batchInsert(archive.properties);
}
if (archive.experiences && archive.experiences.length){
if (archive.experiences && archive.experiences.length) {
Experiences.batchInsert(archive.experiences);
}
if (archive.logs && archive.logs.length){
if (archive.logs && archive.logs.length) {
CreatureLogs.batchInsert(archive.logs);
}
} catch (e) {
@@ -65,7 +65,7 @@ const restoreCreaturefromFile = new ValidatedMethod({
validate: new SimpleSchema({
'fileId': {
type: String,
regEx: SimpleSchema.RegEx.Id,
max: 32,
},
}).validator(),
mixins: [RateLimiterMixin],
@@ -73,23 +73,23 @@ const restoreCreaturefromFile = new ValidatedMethod({
numRequests: 10,
timeInterval: 5000,
},
async run({fileId}) {
async run({ fileId }) {
// fetch the file
const file = ArchiveCreatureFiles.findOne({_id: fileId}).get();
if (!file){
const file = ArchiveCreatureFiles.findOne({ _id: fileId }).get();
if (!file) {
throw new Meteor.Error('File not found',
'The requested creature archive does not exist');
'The requested creature archive does not exist');
}
// Assert ownership
const userId = file?.userId;
if (!userId || userId !== this.userId){
if (!userId || userId !== this.userId) {
throw new Meteor.Error('Permission denied',
'You can only restore creatures you own');
'You can only restore creatures you own');
}
assertHasCharactersSlots(this.userId);
if (Meteor.isServer){
if (Meteor.isServer) {
// Read the file data
const archive = await ArchiveCreatureFiles.readJSONFile(file);
restoreCreature(archive, this.userId);

View File

@@ -1,7 +1,7 @@
import { slice } from 'lodash';
import PER_CREATURE_LOG_LIMIT from '/imports/api/creature/log/CreatureLogs.js';
import { PER_CREATURE_LOG_LIMIT } from '/imports/api/creature/log/CreatureLogs';
export default function verifyArchiveSafety({ meta, creature, properties, experiences, logs }){
export default function verifyArchiveSafety({ creature, properties, experiences, logs }) {
const creatureId = creature._id;
// Check lengths of arrays
@@ -21,7 +21,7 @@ export default function verifyArchiveSafety({ meta, creature, properties, experi
}
});
properties.forEach(prop => {
if (prop.ancestors[0].id !== creatureId) {
if (prop.root?.id !== creatureId) {
throw new Meteor.Error('Malicious prop', 'Properties contains an entry for the wrong creature');
}
});

View File

@@ -1,5 +1,5 @@
import SimpleSchema from 'simpl-schema';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS';
let CreatureFolders = new Mongo.Collection('creatureFolders');
@@ -16,11 +16,11 @@ let creatureFolderSchema = new SimpleSchema({
},
'creatures.$': {
type: String,
regEx: SimpleSchema.RegEx.Id,
max: 32,
},
owner: {
type: String,
regEx: SimpleSchema.RegEx.Id,
max: 32,
index: 1,
},
archived: {
@@ -35,5 +35,5 @@ let creatureFolderSchema = new SimpleSchema({
CreatureFolders.attachSchema(creatureFolderSchema);
import '/imports/api/creature/creatureFolders/methods.js/index.js';
import '/imports/api/creature/creatureFolders/methods.js/index';
export default CreatureFolders;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,112 +0,0 @@
import { Mongo } from 'meteor/mongo';
import SimpleSchema from 'simpl-schema';
import ColorSchema from '/imports/api/properties/subSchemas/ColorSchema.js';
import ChildSchema from '/imports/api/parenting/ChildSchema.js';
import SoftRemovableSchema from '/imports/api/parenting/SoftRemovableSchema.js';
import propertySchemasIndex from '/imports/api/properties/computedPropertySchemasIndex.js';
import { storedIconsSchema } from '/imports/api/icons/Icons.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
let CreatureProperties = new Mongo.Collection('creatureProperties');
let CreaturePropertySchema = new SimpleSchema({
_id: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
_migrationError: {
type: String,
optional: true,
},
type: {
type: String,
allowedValues: Object.keys(propertySchemasIndex),
},
tags: {
type: Array,
defaultValue: [],
maxCount: STORAGE_LIMITS.tagCount,
},
'tags.$': {
type: String,
max: STORAGE_LIMITS.tagLength,
},
disabled: {
type: Boolean,
optional: true,
},
icon: {
type: storedIconsSchema,
optional: true,
max: STORAGE_LIMITS.icon,
},
// Reference to the library node that this property was copied from
libraryNodeId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
optional: true,
},
});
const DenormalisedOnlyCreaturePropertySchema = new SimpleSchema({
// Denormalised flag if this property is inactive on the sheet for any reason
// Including being disabled, or a decendent of a disabled property
inactive: {
type: Boolean,
optional: true,
index: 1,
removeBeforeCompute: true,
},
// Denormalised flag if this property was made inactive by an inactive
// ancestor. True if this property has an inactive ancestor even if this
// property is itself inactive
deactivatedByAncestor: {
type: Boolean,
optional: true,
index: 1,
removeBeforeCompute: true,
},
// Denormalised flag if this property was made inactive because of its own
// state
deactivatedBySelf: {
type: Boolean,
optional: true,
index: 1,
removeBeforeCompute: true,
},
// Denormalised flag if this property was made inactive because of a toggle
// calculation. Either an ancestor toggle calculation or its own.
deactivatedByToggle: {
type: Boolean,
optional: true,
index: 1,
removeBeforeCompute: true,
},
// When this is true on any property, the creature needs to be recomputed
dirty: {
type: Boolean,
// Default to true because new properties cause a recomputation
defaultValue: true,
optional: true,
},
});
CreaturePropertySchema.extend(DenormalisedOnlyCreaturePropertySchema);
for (let key in propertySchemasIndex) {
let schema = new SimpleSchema({});
schema.extend(propertySchemasIndex[key]);
schema.extend(CreaturePropertySchema);
schema.extend(ColorSchema);
schema.extend(ChildSchema);
schema.extend(SoftRemovableSchema);
CreatureProperties.attachSchema(schema, {
selector: { type: key }
});
}
export default CreatureProperties;
export {
DenormalisedOnlyCreaturePropertySchema,
CreaturePropertySchema,
};

View File

@@ -0,0 +1,174 @@
import SimpleSchema from 'simpl-schema';
import ColorSchema from '/imports/api/properties/subSchemas/ColorSchema';
import ChildSchema from '/imports/api/parenting/ChildSchema';
import SoftRemovableSchema from '/imports/api/parenting/SoftRemovableSchema';
import propertySchemasIndex from '/imports/api/properties/computedPropertySchemasIndex';
import { storedIconsSchema } from '/imports/api/icons/Icons';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS';
import { ConvertToUnion, InferType, TypedSimpleSchema } from '/imports/api/utility/TypedSimpleSchema';
import { Simplify } from 'type-fest';
import type { PropertyType } from '/imports/api/properties/PropertyType.type';
const PreComputeCreaturePropertySchema = TypedSimpleSchema.from({
_id: {
type: String,
max: 32,
},
_migrationError: {
type: String,
optional: true,
},
type: {
type: String,
allowedValues: Object.keys(propertySchemasIndex),
},
tags: {
type: Array,
defaultValue: [],
maxCount: STORAGE_LIMITS.tagCount,
},
'tags.$': {
type: String,
max: STORAGE_LIMITS.tagLength,
},
disabled: {
type: Boolean,
optional: true,
},
icon: {
type: storedIconsSchema,
optional: true,
max: STORAGE_LIMITS.icon,
},
// Reference to the library node that this property was copied from
libraryNodeId: {
type: String,
max: 32,
optional: true,
},
// Fill more than one quantity in a slot, like feats and ability score
// improvements, filtered out of UI if there isn't space in quantityExpected
slotQuantityFilled: {
type: SimpleSchema.Integer,
optional: true, // Undefined implies 1
},
});
const DenormalisedOnlyCreaturePropertySchema = TypedSimpleSchema.from({
// Denormalised flag if this property is inactive on the sheet for any reason
// Including being disabled, or a descendant of a disabled property
inactive: {
type: Boolean,
optional: true,
removeBeforeCompute: true,
},
// Denormalised flag if this property was made inactive by an inactive
// ancestor. True if this property has an inactive ancestor even if this
// property is itself inactive
deactivatedByAncestor: {
type: Boolean,
optional: true,
removeBeforeCompute: true,
},
// Denormalised flag if this property was made inactive because of its own
// state
deactivatedBySelf: {
type: Boolean,
optional: true,
removeBeforeCompute: true,
},
// Denormalised flag if this property was made inactive because of a toggle
// calculation. Either an ancestor toggle calculation or its own.
deactivatedByToggle: {
type: Boolean,
optional: true,
removeBeforeCompute: true,
},
deactivatingToggleId: {
type: String,
max: 32,
optional: true,
removeBeforeCompute: true,
},
// Triggers that fire when this property is applied
'triggerIds': {
type: Object,
optional: true,
removeBeforeCompute: true,
},
'triggerIds.before': {
type: Array,
optional: true,
},
'triggerIds.before.$': {
type: String,
max: 32,
},
'triggerIds.after': {
type: Array,
optional: true,
},
'triggerIds.after.$': {
type: String,
max: 32,
},
'triggerIds.afterChildren': {
type: Array,
optional: true,
},
'triggerIds.afterChildren.$': {
type: String,
max: 32,
},
// When this is true on any property, the creature needs to be recomputed
dirty: {
type: Boolean,
// Default to true because new properties cause a recomputation
defaultValue: true,
optional: true,
},
});
const CreaturePropertySchema = PreComputeCreaturePropertySchema.extend(DenormalisedOnlyCreaturePropertySchema);
export type CreaturePropertyTypes = {
[T in PropertyType]: Simplify<
{ type: T }
& InferType<typeof propertySchemasIndex[T]>
> & Simplify<
Exclude<InferType<typeof CreaturePropertySchema>, 'type'>
& InferType<typeof ColorSchema>
& InferType<typeof ChildSchema>
& InferType<typeof SoftRemovableSchema>
>
}
export type CreatureProperty = Simplify<ConvertToUnion<CreaturePropertyTypes>>;
const CreatureProperties = new Mongo.Collection<CreatureProperty>('creatureProperties');
const genericCreaturePropertySchema = TypedSimpleSchema.from({})
.extend(CreaturePropertySchema)
.extend(ColorSchema)
.extend(ChildSchema)
.extend(SoftRemovableSchema);
// Attach the default schema
CreatureProperties.attachSchema(genericCreaturePropertySchema);
// Attach the schemas for each type
let key: keyof typeof propertySchemasIndex;
for (key in propertySchemasIndex) {
const schema = TypedSimpleSchema.from({})
.extend(propertySchemasIndex[key])
.extend(genericCreaturePropertySchema)
CreatureProperties.attachSchema(schema, {
selector: { type: key }
});
}
export default CreatureProperties;
export {
DenormalisedOnlyCreaturePropertySchema,
CreaturePropertySchema,
};

View File

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

View File

@@ -1,9 +1,9 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import SimpleSchema from 'simpl-schema';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions';
const adjustQuantity = new ValidatedMethod({
name: 'creatureProperties.adjustQuantity',

View File

@@ -0,0 +1,182 @@
import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
import LibraryNodes from '/imports/api/library/LibraryNodes';
import { RefSchema } from '/imports/api/parenting/ChildSchema';
import {
assertEditPermission,
assertDocEditPermission,
assertCopyPermission
} from '/imports/api/sharing/sharingPermissions';
import {
fetchDocByRef,
getFilter,
renewDocIds
} from '/imports/api/parenting/parentingFunctions';
import { rebuildNestedSets } from '/imports/api/parenting/parentingFunctions';
import Libraries from '/imports/api/library/Libraries';
const DUPLICATE_CHILDREN_LIMIT = 500;
const copyPropertyToLibrary = new ValidatedMethod({
name: 'creatureProperties.copyPropertyToLibrary',
validate: new SimpleSchema({
propId: {
type: String,
max: 32,
},
parentRef: {
type: RefSchema,
},
order: {
type: Number,
optional: true,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 1,
timeInterval: 5000,
},
run({ propId, parentRef, order }) {
// get the new ancestry for the properties
const parentDoc = fetchDocByRef(parentRef);
// Check permission to edit the destination
let rootLibrary;
if (parentRef.collection === 'libraries') {
rootLibrary = parentDoc;
} else if (parentRef.collection === 'libraryNodes') {
rootLibrary = Libraries.findOne(parentDoc.root.id)
} else {
throw `${parentRef.collection} is not a valid parent collection`
}
assertEditPermission(rootLibrary, this.userId);
const insertedRootNode = insertNodeFromProperty(propId, order, this);
// Tree structure changed by inserts, reorder the tree
rebuildNestedSets(LibraryNodes, rootLibrary._id);
// Return the docId of the inserted root property
return insertedRootNode?._id;
},
});
function insertNodeFromProperty(propId, order, method) {
// Fetch the property and its descendants, provided they have not been
// removed
let prop = CreatureProperties.findOne({
_id: propId,
removed: { $ne: true },
});
if (!prop) {
if (Meteor.isClient) return;
else {
throw new Meteor.Error(
'Insert property from library failed',
`No property with id '${propId}' was found`
);
}
}
// Make sure we can edit this property
assertDocEditPermission(prop, method.userId);
let oldParentId = prop.parentId;
const propCursor = CreatureProperties.find({
...getFilter.descendants(prop),
removed: { $ne: true },
});
// Make sure there aren't too many descendants
if (propCursor.count() > DUPLICATE_CHILDREN_LIMIT) {
throw new Meteor.Error('Copy children limit',
`The property has over ${DUPLICATE_CHILDREN_LIMIT} descendants and cannot be copied`);
}
let props = propCursor.fetch();
// The root prop is first in the array of props
// It must get the first generated ID to prevent flickering
props = [prop, ...props];
// If the docs came from a library, that library must consent to this user copying their
// properties
assertSourceLibraryCopyPermission(props, method);
// Give the docs new IDs without breaking internal references
renewDocIds({
docArray: props,
collectionMap: { 'creatureProperties': 'libraryNodes' }
});
// Order the root node
prop.left = Number.MAX_SAFE_INTEGER - 1;
prop.right = Number.MAX_SAFE_INTEGER;
// Clean the props
props = cleanProps(props);
// Insert the props as library nodes
LibraryNodes.batchInsert(props);
return prop;
}
/**
*
* @param props The properties to check
* @param userId The userId trying to copy these properties to a library
* Checks that every property can be copied out of the library that originated it by this user
*/
function assertSourceLibraryCopyPermission(props, method) {
// Skip on the client
if (method.isSimulation) return;
// Get all the library node ids that are sources for these properties
const libraryNodeIds = [];
props.forEach(prop => {
if (prop.libraryNodeId) libraryNodeIds.push(prop.libraryNodeId);
});
if (!libraryNodeIds.length) return;
// Get the actual library Ids that each of these source nodes came from
const sourceLibIds = new Set();
LibraryNodes.find({
_id: { $in: libraryNodeIds }
}, {
fields: { root: 1 }
}).forEach(node => {
sourceLibIds.add(node.root.id);
});
// Assert copy permission on each of those libraries
Libraries.find({
_id: { $in: Array.from(sourceLibIds) }
}, {
fields: {
name: 1,
owner: 1,
readers: 1,
writers: 1,
public: 1,
readersCanCopy: 1,
}
}).forEach(lib => {
try {
assertCopyPermission(lib, method.userId);
} catch (e) {
throw new Meteor.Error('Copy permission denied',
`One of the properties you are copying comes from ${lib.name}, which you do not have permission to copy from`);
}
});
}
export function cleanProps(props) {
return props.map(prop => {
let schema = LibraryNodes.simpleSchema(prop);
return schema.clean(prop);
});
}
export default copyPropertyToLibrary;

View File

@@ -1,139 +0,0 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import SimpleSchema from 'simpl-schema';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import { applyTriggers } from '/imports/api/engine/actions/applyTriggers.js';
import ActionContext from '/imports/api/engine/actions/ActionContext.js';
const damageProperty = new ValidatedMethod({
name: 'creatureProperties.damage',
validate: new SimpleSchema({
_id: SimpleSchema.RegEx.Id,
operation: {
type: String,
allowedValues: ['set', 'increment']
},
value: Number,
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 20,
timeInterval: 5000,
},
run({ _id, operation, value }) {
// Get action context
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
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`
);
}
// 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 }) {
// Save the value to the scope before applying the before triggers
if (operation === 'increment') {
if (value >= 0) {
actionContext.scope['$damage'] = value;
} else {
actionContext.scope['$healing'] = -value;
}
} else {
actionContext.scope['$set'] = value;
}
applyTriggers(actionContext.triggers?.damageProperty?.before, prop, actionContext);
// fetch the value from the scope after the before triggers, in case they changed them
if (operation === 'increment') {
if (value >= 0) {
value = actionContext.scope['$damage'];
} else {
value = -actionContext.scope['$healing'];
}
} else {
value = actionContext.scope['$set'];
}
let damage, newValue, increment;
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
damage = total - value;
// Damage can't exceed total value
if (damage > total && !prop.ignoreLowerLimit) damage = total;
// Damage must be positive
if (damage < 0 && !prop.ignoreUpperLimit) damage = 0;
newValue = prop.total - damage;
// Write the results
CreatureProperties.update(prop._id, {
$set: { damage, value: newValue, dirty: true }
}, {
selector: prop
});
// Also write it straight to the prop so that it is updated in the actionContext
prop.damage = damage;
prop.value = newValue;
} else if (operation === 'increment') {
let currentValue = prop.value || 0;
let currentDamage = prop.damage || 0;
increment = value;
// Can't increase damage above the remaining value
if (increment > currentValue && !prop.ignoreLowerLimit) increment = currentValue;
// Can't decrease damage below zero
if (-increment > currentDamage && !prop.ignoreUpperLimit) increment = -currentDamage;
damage = currentDamage + increment;
newValue = prop.total - damage;
// Write the results
CreatureProperties.update(prop._id, {
$inc: { damage: increment, value: -increment },
$set: { dirty: true },
}, {
selector: prop
});
// Also write it straight to the prop so that it is updated in the actionContext
prop.damage += increment;
prop.value -= increment;
}
applyTriggers(actionContext.triggers?.damageProperty?.after, prop, actionContext);
if (operation === 'set') {
return damage;
} else if (operation === 'increment') {
return increment;
}
}
export default damageProperty;

View File

@@ -1,18 +1,18 @@
import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor';
import {
setLineageOfDocs,
getFilter,
renewDocIds
} from '/imports/api/parenting/parenting.js';
import { reorderDocs } from '/imports/api/parenting/order.js';
} from '/imports/api/parenting/parentingFunctions';
import { rebuildNestedSets } from '/imports/api/parenting/parentingFunctions';
var snackbar;
if (Meteor.isClient) {
snackbar = require(
'/imports/ui/components/snackbars/SnackbarQueue.js'
'/imports/client/ui/components/snackbars/SnackbarQueue'
).snackbar
}
@@ -23,7 +23,7 @@ const duplicateProperty = new ValidatedMethod({
validate: new SimpleSchema({
_id: {
type: String,
regEx: SimpleSchema.RegEx.Id,
max: 32,
}
}).validator(),
mixins: [RateLimiterMixin],
@@ -33,22 +33,29 @@ const duplicateProperty = new ValidatedMethod({
},
run({ _id }) {
let property = CreatureProperties.findOne(_id);
let creature = getRootCreatureAncestor(property);
if (!property) throw new Meteor.Error('not-found', 'The source property was not found');
const creature = getRootCreatureAncestor(property);
assertEditPermission(creature, this.userId);
// Renew the doc ID
let randomSrc = DDP.randomStream('duplicateProperty');
let propertyId = randomSrc.id();
const randomSrc = DDP.randomStream('duplicateProperty');
const propertyId = randomSrc.id();
property._id = propertyId;
// Change the variableName so it isn't immediately overridden
if (property.variableName) {
property.variableName += 'Copy'
}
// Get all the descendants
let nodes = CreatureProperties.find({
'ancestors.id': _id,
const nodes = CreatureProperties.find({
...getFilter.descendants(property),
removed: { $ne: true },
}, {
limit: DUPLICATE_CHILDREN_LIMIT + 1,
sort: { order: 1 },
sort: { left: 1 },
}).fetch();
// Alert the user if the limit was hit
@@ -61,33 +68,28 @@ const duplicateProperty = new ValidatedMethod({
}
}
// re-map all the ancestors
setLineageOfDocs({
docArray: nodes,
newAncestry: [
...property.ancestors,
{ id: propertyId, collection: 'creatureProperties' }
],
oldParent: { id: _id, collection: 'creatureProperties' },
// Give the docs new IDs without breaking internal references
const allNodes = [property, ...nodes];
renewDocIds({
docArray: allNodes,
idMap: {
[_id]: propertyId,
[propertyId]: propertyId,
},
});
// Give the docs new IDs without breaking internal references
renewDocIds({ docArray: nodes });
// Order the root node
property.order += 0.5;
property.left = Number.MAX_SAFE_INTEGER - 1;
property.right = Number.MAX_SAFE_INTEGER;
// Mark the sheet as needing recompute
property.dirty = true;
// Insert the properties
CreatureProperties.batchInsert([property, ...nodes]);
CreatureProperties.batchInsert(allNodes);
// Tree structure changed by inserts, reorder the tree
reorderDocs({
collection: CreatureProperties,
ancestorId: property.ancestors[0].id,
});
rebuildNestedSets(CreatureProperties, property.root.id);
return propertyId;
},

View File

@@ -1,11 +1,11 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import { organizeDoc } from '/imports/api/parenting/organizeMethods.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import BUILT_IN_TAGS from '/imports/constants/BUILT_IN_TAGS.js';
import getParentRefByTag from '/imports/api/creature/creatureProperties/methods/getParentRefByTag.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions';
import { organizeDoc } from '/imports/api/parenting/organizeMethods';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor';
import BUILT_IN_TAGS from '/imports/constants/BUILT_IN_TAGS';
import getParentRefByTag from './getParentByTag';
// Equipping or unequipping an item will also change its parent
const equipItem = new ValidatedMethod({
@@ -36,7 +36,7 @@ const equipItem = new ValidatedMethod({
let parentRef = getParentRefByTag(creature._id, tag);
if (!parentRef) parentRef = { id: creature._id, collection: 'creatures' };
organizeDoc.call({
organizeDoc.callAsync({
docRef: {
id: _id,
collection: 'creatureProperties',

View File

@@ -1,8 +1,8 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor';
const flipToggle = new ValidatedMethod({
name: 'creatureProperties.flipToggle',
@@ -17,7 +17,7 @@ const flipToggle = new ValidatedMethod({
run({ _id }) {
// Permission
let property = CreatureProperties.findOne(_id, {
fields: { type: 1, ancestors: 1, enabled: 1, disabled: 1 }
fields: { type: 1, root: 1, enabled: 1, disabled: 1 }
});
if (property.type !== 'toggle') {
throw new Meteor.Error('wrong property',

View File

@@ -0,0 +1,13 @@
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
import { getFilter } from '/imports/api/parenting/parentingFunctions';
export default function getParentByTag(creatureId, tag) {
return CreatureProperties.findOne({
...getFilter.descendantsOfRoot(creatureId),
removed: { $ne: true },
inactive: { $ne: true },
tags: tag,
}, {
sort: { left: 1 },
});
}

View File

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

View File

@@ -1,62 +1,78 @@
export default function getSlotFillFilter({slot, libraryIds}){
import { getFilter } from '/imports/api/parenting/parentingFunctions';
export default function getSlotFillFilter({ slot, libraryIds }) {
if (!slot) throw 'Slot is required for getSlotFillFilter';
if (!libraryIds) throw 'LibraryIds is required for getSlotFillFilter';
let filter = {
removed: {$ne: true},
$and: []
fillSlots: true,
removed: { $ne: true },
$and: [],
};
if (libraryIds){
filter['ancestors.id'] = {$in: libraryIds};
if (libraryIds.length) {
Object.assign(
filter,
getFilter.descendantsOfAllRoots(libraryIds)
);
}
if (slot.slotType){
if (slot.slotType) {
filter.$and.push({
$or: [{
type: slot.slotType
},{
type: 'slotFiller',
}, {
slotFillerType: slot.slotType,
}]
});
} else if (slot.type === 'class') {
filter.$and.push({
$or: [{
type: 'classLevel',
},{
type: 'slotFiller',
slotFillerType: 'classLevel',
}]
});
if (slot.variableName) {
filter.variableName = slot.variableName;
const classLevelFilter = {
type: 'classLevel',
};
const slotFillerFilter = {
slotFillerType: 'classLevel',
};
// Match variable name or tags
if (slot.variableName) {
classLevelFilter.variableName = slot.variableName;
slotFillerFilter.libraryTags = slot.variableName;
}
// Only search for levels the class needs
if (slot.missingLevels && slot.missingLevels.length) {
filter.level = {$in: slot.missingLevels};
classLevelFilter.level = { $in: slot.missingLevels };
slotFillerFilter['cache.node.level'] = { $in: slot.missingLevels };
} else {
filter.level = (slot.level || 0) + 1;
classLevelFilter.level = { $gt: slot.level || 0 };
slotFillerFilter['cache.node.level'] = { $gt: slot.level || 0 };
}
filter.$and.push({
$or: [classLevelFilter, slotFillerFilter]
});
}
let tagsOr = [];
let tagsNin = [];
if (slot.slotTags && slot.slotTags.length){
tagsOr.push({tags: {$all: slot.slotTags}});
if (slot.slotTags && slot.slotTags.length) {
tagsOr.push({ libraryTags: { $all: slot.slotTags } });
}
if (slot.extraTags && slot.extraTags.length){
if (slot.extraTags && slot.extraTags.length) {
slot.extraTags.forEach(extra => {
if (!extra.tags || !extra.tags.length) return;
if (extra.operation === 'OR'){
tagsOr.push({tags: {$all: extra.tags}});
} else if (extra.operation === 'NOT'){
if (extra.operation === 'OR') {
tagsOr.push({ libraryTags: { $all: extra.tags } });
} else if (extra.operation === 'NOT') {
tagsNin.push(...extra.tags);
}
});
}
if (tagsOr.length){
if (tagsOr.length) {
filter.$or = tagsOr;
}
if (tagsNin.length){
filter.$and.push({tags: {$nin: tagsNin}});
if (tagsNin.length) {
filter.$and.push({ libraryTags: { $nin: tagsNin } });
}
if (!filter.$and.length){
if (!filter.$and.length) {
delete filter.$and;
}
return filter;

View File

@@ -0,0 +1,85 @@
import { assert } from 'chai';
import getSlotFillFilter from '/imports/api/creature/creatureProperties/methods/getSlotFillFilter';
describe('Slot fill filter', function () {
it('Gives error if arguments aren\'t provided', function () {
assert.throws(
() => getSlotFillFilter(undefined),
null, null, 'Passing undefined should give an error'
);
assert.throws(
() => getSlotFillFilter({
slot: { slotTags: ['tag1'] },
}),
null, null, 'Passing no libraryIds should give an error'
);
assert.throws(
() => getSlotFillFilter({
libraryIds: ['libraryId1'],
}),
null, null, 'Passing no slot should give an error'
);
});
it('filters using basic slot tags', function () {
const filter = getSlotFillFilter({
slot: {
slotTags: ['tag1', 'tag2']
},
libraryIds: ['libraryId1', 'libraryId2'],
});
assert.deepStrictEqual(filter, {
$or: [{
libraryTags: { $all: ['tag1', 'tag2'] }
}],
'root.id': { $in: ['libraryId1', 'libraryId2'] },
removed: { $ne: true },
fillSlots: true,
});
});
it('filters using slot type', function () {
const filter = getSlotFillFilter({
slot: {
slotTags: ['tag1', 'tag2'],
slotType: 'feature',
},
libraryIds: ['libraryId1', 'libraryId2']
});
assert.deepStrictEqual(filter.$and, [{
$or: [{
type: 'feature'
}, {
slotFillerType: 'feature',
}],
}]);
});
it('filters using extra tags', function () {
const filter = getSlotFillFilter({
slot: {
slotTags: ['tag1', 'tag2'],
extraTags: [
{ operation: 'OR', tags: ['tag3', 'tag4'] },
{ operation: 'NOT', tags: ['tag5', 'tag6'] },
{ operation: 'NOT', tags: ['tag7', 'tag8'] },
],
},
libraryIds: ['libraryId1', 'libraryId2'],
});
assert.deepStrictEqual(filter, {
$or: [
{ libraryTags: { $all: ['tag1', 'tag2'] } },
{ libraryTags: { $all: ['tag3', 'tag4'] } },
],
$and: [
{ libraryTags: { $nin: ['tag5', 'tag6', 'tag7', 'tag8'] } },
],
'root.id': { $in: ['libraryId1', 'libraryId2'] },
removed: { $ne: true },
fillSlots: true,
});
});
});

View File

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

View File

@@ -1,14 +1,12 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor';
import SimpleSchema from 'simpl-schema';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import { reorderDocs } from '/imports/api/parenting/order.js';
import { getAncestry } from '/imports/api/parenting/parenting.js';
import getParentRefByTag from '/imports/api/creature/creatureProperties/methods/getParentRefByTag.js';
import { RefSchema } from '/imports/api/parenting/ChildSchema.js';
import { getHighestOrder } from '/imports/api/parenting/order.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions';
import { fetchDocByRef, rebuildNestedSets } from '/imports/api/parenting/parentingFunctions';
import getParentRefByTag from './getParentByTag';
import { RefSchema } from '/imports/api/parenting/ChildSchema';
const insertProperty = new ValidatedMethod({
name: 'creatureProperties.insert',
@@ -25,27 +23,23 @@ const insertProperty = new ValidatedMethod({
timeInterval: 5000,
},
run({ creatureProperty, parentRef }) {
// get the new ancestry for the properties
let { parentDoc, ancestors } = getAncestry({ parentRef });
let rootCreature;
const parentDoc = fetchDocByRef(parentRef);
// Check permission to edit
let rootCreature;
if (parentRef.collection === 'creatures') {
rootCreature = parentDoc;
} else if (parentRef.collection === 'creatureProperties') {
rootCreature = getRootCreatureAncestor(parentDoc);
creatureProperty.parentId = parentDoc._id;
} else {
throw `${parentRef.collection} is not a valid parent collection`
}
assertEditPermission(rootCreature, this.userId);
creatureProperty.parent = parentRef;
creatureProperty.ancestors = ancestors;
creatureProperty.root = { collection: 'creatures', id: rootCreature._id };
return insertPropertyWork({
property: creatureProperty,
creature: rootCreature,
});
return insertPropertyWork(creatureProperty);
},
});
@@ -58,7 +52,7 @@ const insertPropertyAsChildOfTag = new ValidatedMethod({
},
creatureId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
max: 32,
},
tag: {
type: String,
@@ -77,18 +71,17 @@ const insertPropertyAsChildOfTag = new ValidatedMethod({
},
run({ creatureProperty, creatureId, tag, tagDefaultName }) {
let parentRef = getParentRefByTag(creatureId, tag);
let insertFolderFirst = false;
if (!parentRef) {
// Use the creature as the parent and mark that we need to insert the folder first later
var insertFolderFirst = true;
insertFolderFirst = true;
parentRef = { id: creatureId, collection: 'creatures' };
}
// get the new ancestry for the properties
let { parentDoc, ancestors } = getAncestry({ parentRef });
// Check permission to edit
let rootCreature;
const parentDoc = fetchDocByRef(parentRef);
if (parentRef.collection === 'creatures') {
rootCreature = parentDoc;
} else if (parentRef.collection === 'creatureProperties') {
@@ -98,46 +91,34 @@ const insertPropertyAsChildOfTag = new ValidatedMethod({
}
assertEditPermission(rootCreature, this.userId);
const root = { collection: 'creatures', id: rootCreature._id };
// Add the folder first if we need to
if (insertFolderFirst) {
let order = getHighestOrder({
collection: CreatureProperties,
ancestorId: parentRef.id,
}) + 1;
let id = CreatureProperties.insert({
type: 'folder',
name: tagDefaultName || (tag.charAt(0).toUpperCase() + tag.slice(1)),
tags: [tag],
parent: parentRef,
ancestors: [parentRef],
order,
// parentId: undefined,
root,
});
// Make the folder our new parent
let newParentRef = { id, collection: 'creatureProperties' };
ancestors = [parentRef, newParentRef];
parentRef = newParentRef;
creatureProperty.order = order + 1;
parentRef = { id, collection: 'creatureProperties' };
}
creatureProperty.parent = parentRef;
creatureProperty.ancestors = ancestors;
creatureProperty.root = root;
creatureProperty.parentId = parentRef.id;
return insertPropertyWork({
property: creatureProperty,
creature: rootCreature,
});
return insertPropertyWork(creatureProperty);
},
});
export function insertPropertyWork({ property, creature }) {
export function insertPropertyWork(property) {
delete property._id;
property.dirty = true;
let _id = CreatureProperties.insert(property);
// Tree structure changed by insert, reorder the tree
reorderDocs({
collection: CreatureProperties,
ancestorId: creature._id,
});
rebuildNestedSets(CreatureProperties, property.root.id);
return _id;
}

View File

@@ -1,19 +1,18 @@
import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
import { RefSchema } from '/imports/api/parenting/ChildSchema.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
import LibraryNodes from '/imports/api/library/LibraryNodes';
import { RefSchema } from '/imports/api/parenting/ChildSchema';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions';
import {
setLineageOfDocs,
getAncestry,
renewDocIds
} from '/imports/api/parenting/parenting.js';
import { reorderDocs } from '/imports/api/parenting/order.js';
import { setDocToLastOrder } from '/imports/api/parenting/order.js';
import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
renewDocIds,
fetchDocByRef,
rebuildNestedSets,
getFilter
} from '/imports/api/parenting/parentingFunctions';
import { union } from 'lodash';
const insertPropertyFromLibraryNode = new ValidatedMethod({
name: 'creatureProperties.insertPropertyFromLibraryNode',
@@ -24,24 +23,20 @@ const insertPropertyFromLibraryNode = new ValidatedMethod({
},
'nodeIds.$': {
type: String,
regEx: SimpleSchema.RegEx.Id,
max: 32,
},
parentRef: {
type: RefSchema,
},
order: {
type: Number,
optional: true,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({ nodeIds, parentRef, order }) {
run({ nodeIds, parentRef }) {
// get the new ancestry for the properties
let { parentDoc, ancestors } = getAncestry({ parentRef });
const parentDoc = fetchDocByRef(parentRef);
// Check permission to edit
let rootCreature;
@@ -54,37 +49,32 @@ const insertPropertyFromLibraryNode = new ValidatedMethod({
}
assertEditPermission(rootCreature, this.userId);
// {libraryId: hasViewPermission}
//let libraryPermissionMemoir = {};
const root = { collection: 'creatures', id: rootCreature._id };
const parentId = parentRef.id;
let node;
nodeIds.forEach(nodeId => {
// TODO: Check library view permission for each node before starting
node = insertPropertyFromNode(nodeId, ancestors, order);
node = insertPropertyFromNode(nodeId, root, parentId);
});
// get one of the root inserted docs
let rootId = node._id;
// Tree structure changed by inserts, reorder the tree
reorderDocs({
collection: CreatureProperties,
ancestorId: rootCreature._id,
});
// Return the docId of the last property, the inserted root property
return rootId;
rebuildNestedSets(CreatureProperties, rootCreature._id);
// get one of the root inserted docs
const lastInsertedId = node?._id;
return lastInsertedId;
},
});
function insertPropertyFromNode(nodeId, ancestors, order) {
// Fetch the library node and its decendents, provided they have not been
function insertPropertyFromNode(nodeId, root, parentId) {
// Fetch the library node and its descendants, provided they have not been
// removed
// TODO: Check permission to read the library this node is in
let node = LibraryNodes.findOne({
_id: nodeId,
removed: { $ne: true },
});
if (!node) {
if (Meteor.isClient) return;
if (Meteor.isClient) return {};
else {
throw new Meteor.Error(
'Insert property from library failed',
@@ -92,69 +82,51 @@ function insertPropertyFromNode(nodeId, ancestors, order) {
);
}
}
let oldParent = node.parent;
let nodes = LibraryNodes.find({
'ancestors.id': nodeId,
...getFilter.descendants(node),
removed: { $ne: true },
}).fetch();
// Convert all references into actual nodes
nodes = reifyNodeReferences(nodes);
// The root node is first in the array of nodes
// It must get the first generated ID to prevent flickering
nodes = [node, ...nodes];
// Convert all references into actual nodes
nodes = reifyNodeReferences(nodes);
// Refetch the root node, it might have been reified
node = nodes[0] || node;
// set libraryNodeIds
storeLibraryNodeReferences(nodes);
// re-map all the ancestors
setLineageOfDocs({
docArray: nodes,
newAncestry: ancestors,
oldParent,
});
// Give the docs new IDs without breaking internal references
renewDocIds({
docArray: nodes,
collectionMap: { 'libraryNodes': 'creatureProperties' }
});
// Order the root node
if (order === undefined) {
setDocToLastOrder({
collection: CreatureProperties,
doc: node,
});
} else {
node.order = order;
}
// Mark root node as dirty
node.dirty = true;
// Mark all nodes as dirty
dirtyNodes(nodes);
// Move the root node to the end of the order
node.left = Number.MAX_SAFE_INTEGER;
// Insert the creature properties
CreatureProperties.batchInsert(nodes);
return node;
}
function storeLibraryNodeReferences(nodes) {
export function storeLibraryNodeReferences(nodes) {
nodes.forEach(node => {
if (node.libraryNodeId) return;
node.libraryNodeId = node._id;
});
}
function dirtyNodes(nodes) {
nodes.forEach(node => {
node.dirty = true;
});
}
// Covert node references into actual nodes
// TODO: check permissions for each library a reference node references
function reifyNodeReferences(nodes, visitedRefs = new Set(), depth = 0) {
export function reifyNodeReferences(nodes, visitedRefs = new Set(), depth = 0) {
depth += 1;
// New nodes added this function
let newNodes = [];
@@ -174,7 +146,7 @@ function reifyNodeReferences(nodes, visitedRefs = new Set(), depth = 0) {
let referencedNode
try {
referencedNode = fetchDocByRef(node.ref);
referencedNode.order = node.order;
referencedNode.tags = union(node.tags, referencedNode.tags);
// We are definitely replacing this node, so add it to the list
visitedRefs.add(node._id);
} catch (e) {
@@ -183,23 +155,15 @@ function reifyNodeReferences(nodes, visitedRefs = new Set(), depth = 0) {
}
// Get all the descendants of the referenced node
let descendents = LibraryNodes.find({
'ancestors.id': referencedNode._id,
let descendants = LibraryNodes.find({
...getFilter.descendants(referencedNode),
removed: { $ne: true },
}, {
sort: { order: 1 },
sort: { left: 1 },
}).fetch();
// We are adding the referenced node and its descendants
let addedNodes = [referencedNode, ...descendents];
// re-map all the ancestors to parent the new sub-tree into our existing
// node tree
setLineageOfDocs({
docArray: addedNodes,
newAncestry: node.ancestors,
oldParent: referencedNode.parent,
});
let addedNodes = [referencedNode, ...descendants];
// Filter all the looped references
addedNodes = addedNodes.filter(addedNode => {

View File

@@ -1,8 +1,8 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor';
const pullFromProperty = new ValidatedMethod({
name: 'creatureProperties.pull',

View File

@@ -1,8 +1,8 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor';
import { get } from 'lodash';
const pushToProperty = new ValidatedMethod({

View File

@@ -1,10 +1,10 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import SimpleSchema from 'simpl-schema';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import { restore } from '/imports/api/parenting/softRemove.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions';
import { restore } from '/imports/api/parenting/softRemove';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor';
const restoreProperty = new ValidatedMethod({
name: 'creatureProperties.restore',
@@ -23,13 +23,7 @@ const restoreProperty = new ValidatedMethod({
assertEditPermission(rootCreature, this.userId);
// Do work
restore({
_id,
collection: CreatureProperties,
extraUpdates: {
$set: { dirty: true }
},
});
restore(CreatureProperties, property, { $set: { dirty: true } });
}
});

View File

@@ -1,9 +1,9 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import SimpleSchema from 'simpl-schema';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions';
const selectAmmoItem = new ValidatedMethod({
name: 'creatureProperties.selectAmmoItem',

View File

@@ -1,10 +1,10 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import SimpleSchema from 'simpl-schema';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import { softRemove } from '/imports/api/parenting/softRemove.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions';
import { softRemove } from '/imports/api/parenting/softRemove';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor';
const softRemoveProperty = new ValidatedMethod({
name: 'creatureProperties.softRemove',
@@ -23,7 +23,7 @@ const softRemoveProperty = new ValidatedMethod({
assertEditPermission(rootCreature, this.userId);
// Do work
softRemove({ _id, collection: CreatureProperties });
softRemove(CreatureProperties, property);
}
});

View File

@@ -1,8 +1,7 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
import { assertDocEditPermission } from '/imports/api/sharing/sharingPermissions';
const updateCreatureProperty = new ValidatedMethod({
name: 'creatureProperties.update',
@@ -11,28 +10,27 @@ const updateCreatureProperty = new ValidatedMethod({
// We cannot change these fields with a simple update
switch (path[0]) {
case 'type':
case 'order':
case 'parent':
case 'ancestors':
case 'damage':
case 'root':
case 'left':
case 'right':
case 'parentId':
throw new Meteor.Error('Permission denied',
'This property can\'t be updated directly');
}
},
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
numRequests: 12,
timeInterval: 5000,
},
run({ _id, path, value }) {
// Permission
let property = CreatureProperties.findOne(_id, {
fields: { type: 1, ancestors: 1 }
const property = CreatureProperties.findOne(_id, {
fields: { type: 1, root: 1 }
});
let rootCreature = getRootCreatureAncestor(property);
assertEditPermission(rootCreature, this.userId);
assertDocEditPermission(property, this.userId);
let pathString = path.join('.');
const pathString = path.join('.');
let modifier;
// unset empty values
if (value === null || value === undefined) {

View File

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

View File

@@ -1,21 +0,0 @@
//set up the collection for creature variables
let CreatureVariables = new Mongo.Collection('creatureVariables');
// Unique index on _creatureId
if (Meteor.isServer) {
CreatureVariables._ensureIndex({ _creatureId: 1 }, { unique: true })
}
/** No schema because the structure isn't known until compute time
* Expect documents to looke like:
* {
* _id: "nE8Ngd6K4L4jSxLY2",
* _creatureId: "nE8Ngd6K4L4jSxLY2", // indexed reference to the creature
* explicitlyDefinedVariableName: {...some creatureProperty}
* implicitVariableName: {value: 10},
* undefinedVariableName: {},
* }
* Where top level fields that don't start with `_` are variables on the sheet
**/
export default CreatureVariables;

View File

@@ -0,0 +1,103 @@
import { getSingleProperty } from '/imports/api/engine/loadCreatures';
import ParseNode from '/imports/parser/parseTree/ParseNode';
import array from '/imports/parser/parseTree/array';
import constant, { isFiniteNode } from '/imports/parser/parseTree/constant';
//set up the collection for creature variables
const CreatureVariables = new Mongo.Collection('creatureVariables');
// Unique index on _creatureId
if (Meteor.isServer) {
CreatureVariables._ensureIndex({ _creatureId: 1 }, { unique: true })
}
/** No schema because the structure isn't known until compute time
* Expect documents to look like:
* {
* _id: "nE8Ngd6K4L4jSxLY2",
* _creatureId: "nE8Ngd6K4L4jSxLY2", // indexed reference to the creature
* explicitlyDefinedVariableName: {...some creatureProperty},
* // Must be found in CreatureProperties before using:
* linkedProperty: { _propId: "nE8Ngd6K1234SxLY2" }
* implicitVariableName: {value: 10},
* undefinedVariableName: {},
* }
* Where top level fields that don't start with `_` are variables on the sheet
**/
/**
* Get the property from the given scope, respecting properties that are just a link to the actual
* property document
*/
export function getFromScope(name: string, scope) {
let value = scope?.[name];
if (value?._propId) {
const [propId, rowIdentifier, rowNumber] = value._propId.split('_');
value = getSingleProperty(scope._creatureId, propId);
if (rowIdentifier === 'row' && value?.type === 'pointBuy') {
value = value.values[rowNumber];
}
}
return value;
}
export function getNumberFromScope(name, scope) {
const parseNode = getParseNodeFromScope(name, scope);
if (!parseNode || !isFiniteNode(parseNode)) {
return undefined;
}
return parseNode.value;
}
export async function getConstantValueFromScope(
name, scope
) {
const parseNode = getParseNodeFromScope(name, scope);
if (!parseNode) return;
if (parseNode.parseType !== 'constant') return;
return parseNode.value;
}
export function getParseNodeFromScope(name, scope): ParseNode | undefined {
let value = getFromScope(name, scope);
if (!value) return;
let valueType = getType(value);
// Iterate into object.values
while (valueType === 'object') {
// Prefer the valueNode over the value
if (value.valueNode) {
value = value.valueNode;
} else {
value = value.value;
}
valueType = getType(value);
}
// Return a discovered parse node
if (valueType === 'parseNode') {
return value;
}
// Return a parse node based on the constant type returned
if (valueType === 'string' || valueType === 'number' || valueType === 'boolean') {
return constant.create({ value });
}
// Return a parser array
if (valueType === 'array') {
// If the first value is a parse node, assume all the values are
if (getType(value[0]) === 'parseNode') {
return array.create({
values: value,
});
}
// Create the array from js primitives instead
return array.fromConstantArray(value);
}
}
function getType(val) {
if (!val) return typeof val;
if (Array.isArray(val)) return 'array';
if (val.parseType) return 'parseNode';
return typeof val;
}
export default CreatureVariables;

View File

@@ -1,13 +1,11 @@
import SimpleSchema from 'simpl-schema';
import deathSaveSchema from '/imports/api/properties/subSchemas/DeathSavesSchema.js'
import ColorSchema from '/imports/api/properties/subSchemas/ColorSchema.js';
import SharingSchema from '/imports/api/sharing/SharingSchema.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
import ColorSchema from '/imports/api/properties/subSchemas/ColorSchema';
import SharingSchema from '/imports/api/sharing/SharingSchema';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS';
import { InferType, TypedSimpleSchema } from '/imports/api/utility/TypedSimpleSchema';
import type { Simplify } from 'type-fest';
//set up the collection for creatures
let Creatures = new Mongo.Collection('creatures');
let CreatureSettingsSchema = new SimpleSchema({
const CreatureSettingsSchema = TypedSimpleSchema.from({
//slowed down by carrying too much?
useVariantEncumbrance: {
type: Boolean,
@@ -18,6 +16,11 @@ let CreatureSettingsSchema = new SimpleSchema({
type: Boolean,
optional: true,
},
//hide rest buttons
hideRestButtons: {
type: Boolean,
optional: true,
},
// Swap around the modifier and stat
swapStatAndModifier: {
type: Boolean,
@@ -57,7 +60,7 @@ let CreatureSettingsSchema = new SimpleSchema({
},
});
let CreatureSchema = new SimpleSchema({
const CreatureSchema = TypedSimpleSchema.from({
// Strings
name: {
type: String,
@@ -94,7 +97,7 @@ let CreatureSchema = new SimpleSchema({
},
'allowedLibraries.$': {
type: String,
regEx: SimpleSchema.RegEx.Id,
max: 32,
},
allowedLibraryCollections: {
type: Array,
@@ -103,14 +106,9 @@ let CreatureSchema = new SimpleSchema({
},
'allowedLibraryCollections.$': {
type: String,
regEx: SimpleSchema.RegEx.Id,
max: 32,
},
// Mechanics
deathSave: {
type: deathSaveSchema,
defaultValue: {},
},
// Stats that are computed and denormalised outside of recomputation
denormalizedStats: {
type: Object,
@@ -126,6 +124,10 @@ let CreatureSchema = new SimpleSchema({
type: SimpleSchema.Integer,
defaultValue: 0,
},
propCount: {
type: SimpleSchema.Integer,
defaultValue: 0,
},
// Does the character need a recompute?
dirty: {
type: Boolean,
@@ -141,16 +143,6 @@ let CreatureSchema = new SimpleSchema({
defaultValue: 'pc',
allowedValues: ['pc', 'npc', 'monster'],
},
damageMultipliers: {
type: Object,
blackbox: true,
defaultValue: {}
},
variables: {
type: Object,
blackbox: true,
defaultValue: {}
},
computeErrors: {
type: Array,
optional: true,
@@ -166,11 +158,16 @@ let CreatureSchema = new SimpleSchema({
blackbox: true,
optional: true,
},
lastComputedAt: {
type: Date,
optional: true,
},
// Tabletop
tabletop: {
tabletopId: {
index: 1,
type: String,
regEx: SimpleSchema.RegEx.id,
max: 32,
optional: true,
},
initiativeRoll: {
@@ -183,16 +180,15 @@ let CreatureSchema = new SimpleSchema({
type: CreatureSettingsSchema,
defaultValue: {},
},
});
})
.extend(ColorSchema)
.extend(SharingSchema);
CreatureSchema.extend(ColorSchema);
CreatureSchema.extend(SharingSchema);
export type Creature = Simplify<{ _id: string } & InferType<typeof CreatureSchema>>;
//set up the collection for creatures
const Creatures = new Mongo.Collection<Creature>('creatures');
Creatures.attachSchema(CreatureSchema);
export default Creatures;
export { CreatureSchema };
import '/imports/api/engine/actions/doAction.js';

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import { getUserTier } from '/imports/api/users/patreon/tiers.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import { getUserTier } from '/imports/api/users/patreon/tiers';
import Creatures from '/imports/api/creature/creatures/Creatures';
export default function assertHasCharactersSlots(userId) {
if (characterSlotsRemaining(userId) <= 0) {

View File

@@ -1,9 +1,9 @@
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 Creatures from '/imports/api/creature/creatures/Creatures';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions';
import SimpleSchema from 'simpl-schema';
import simpleSchemaMixin from '/imports/api/creature/mixins/simpleSchemaMixin.js';
import simpleSchemaMixin from '/imports/api/creature/mixins/simpleSchemaMixin';
const changeAllowedLibraries = new ValidatedMethod({
name: 'creatures.changeAllowedLibraries',
@@ -11,7 +11,7 @@ const changeAllowedLibraries = new ValidatedMethod({
schema: new SimpleSchema({
_id: {
type: String,
regEx: SimpleSchema.RegEx.Id,
max: 32,
},
allowedLibraries: {
type: Array,
@@ -20,7 +20,7 @@ const changeAllowedLibraries = new ValidatedMethod({
},
'allowedLibraries.$': {
type: String,
regEx: SimpleSchema.RegEx.Id,
max: 32,
},
allowedLibraryCollections: {
type: Array,
@@ -29,7 +29,7 @@ const changeAllowedLibraries = new ValidatedMethod({
},
'allowedLibraryCollections.$': {
type: String,
regEx: SimpleSchema.RegEx.Id,
max: 32,
},
}),
rateLimit: {
@@ -58,7 +58,7 @@ const toggleAllUserLibraries = new ValidatedMethod({
schema: new SimpleSchema({
_id: {
type: String,
regEx: SimpleSchema.RegEx.Id,
max: 32,
},
value: {
type: Boolean,

View File

@@ -0,0 +1,107 @@
import SCHEMA_VERSION from '/imports/constants/SCHEMA_VERSION';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import Creatures from '/imports/api/creature/creatures/Creatures';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
import CreatureLogs from '/imports/api/creature/log/CreatureLogs';
import Experiences from '/imports/api/creature/experience/Experiences';
import { removeCreatureWork } from '/imports/api/creature/creatures/methods/removeCreature';
import assertHasCharactersSlots from '/imports/api/creature/creatures/methods/assertHasCharacterSlots';
import verifyArchiveSafety from '/imports/api/creature/archive/methods/verifyArchiveSafety';
let migrateApiCreature;
if (Meteor.isServer) {
migrateApiCreature = require('/imports/migrations/apiCreature/migrateApiCreature.js').default;
}
function importApiCreature(apiCreature, userId) {
const apiVersion = apiCreature.meta?.schemaVersion ?? 2;
const creature = apiCreature.creatures[0];
const creatureId = creature._id;
if (SCHEMA_VERSION < apiVersion) {
throw new Meteor.Error('Incompatible',
'The creature on the remote server is from a newer version of DiceCloud')
}
// Migrate and verify the archive meets the current schema
migrateApiCreature(apiCreature);
// Asset that the api creature is (mildly) safe
verifyArchiveSafety({
creature,
properties: apiCreature.creatureProperties ?? [],
experiences: apiCreature.experiences ?? [],
logs: apiCreature.logs ?? [],
});
// Don't upload creatures twice
const existingCreature = Creatures.findOne(creature._id, {
fields: { _id: 1 }
});
if (existingCreature) throw new Meteor.Error('Already exists',
'The creature you are trying to import already exists in this database.')
// Ensure the user owns the restored creature
creature.owner = userId;
// Remove the sharing permissions, the ids of users on this instance aren't going to match
creature.readers = [];
creature.writers = [];
// Mark the creature as dirty so that it recomputes
creature.dirty = true;
// Ensure there is only 1 creature being imported
if (apiCreature.creatures.length !== 1) {
throw new Meteor.Error('invalid-import',
'One and only one creature must be imported at a time'
)
}
// Insert the creature sub documents
// They still have their original _id's
Creatures.insert(creature);
try {
// Add all the properties
if (apiCreature.creatureProperties && apiCreature.creatureProperties.length) {
CreatureProperties.batchInsert(apiCreature.creatureProperties);
}
if (apiCreature.experiences && apiCreature.experiences.length) {
Experiences.batchInsert(apiCreature.experiences);
}
if (apiCreature.logs && apiCreature.logs.length) {
CreatureLogs.batchInsert(apiCreature.logs);
}
} catch (e) {
// If the above fails, delete the inserted creature
removeCreatureWork(creatureId);
throw e;
}
return creatureId;
}
const importCharacterFromDiceCloudInstance = new ValidatedMethod({
name: 'Creatures.methods.importFromInstance',
validate: null,
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 10,
timeInterval: 5000,
},
async run({ characterData }) {
if (Meteor.settings.public.disallowCreatureApiImport) throw new Meteor.Error('not-allowed',
'This instance of DiceCloud has disallowed creature imports')
// fetch the file
if (!characterData) {
throw new Meteor.Error('no-input',
'No character data was provided');
}
assertHasCharactersSlots(this.userId);
if (Meteor.isServer) {
return importApiCreature(characterData, this.userId)
}
},
});
export default importCharacterFromDiceCloudInstance;

View File

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

View File

@@ -1,21 +1,21 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import simpleSchemaMixin from '/imports/api/creature/mixins/simpleSchemaMixin.js';
import Creatures, { CreatureSchema } from '/imports/api/creature/creatures/Creatures.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import defaultCharacterProperties from '/imports/api/creature/creatures/defaultCharacterProperties.js';
import insertPropertyFromLibraryNode from '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js';
import assertHasCharactersSlots from '/imports/api/creature/creatures/methods/assertHasCharacterSlots.js';
import getSlotFillFilter from '/imports/api/creature/creatureProperties/methods/getSlotFillFilter.js';
import getCreatureLibraryIds from '/imports/api/library/getCreatureLibraryIds.js';
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
import { insertExperienceForCreature } from '/imports/api/creature/experience/Experiences.js';
import simpleSchemaMixin from '/imports/api/creature/mixins/simpleSchemaMixin';
import Creatures, { CreatureSchema } from '/imports/api/creature/creatures/Creatures';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
import defaultCharacterProperties from '/imports/api/creature/creatures/defaultCharacterProperties';
import insertPropertyFromLibraryNode from '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode';
import assertHasCharactersSlots from '/imports/api/creature/creatures/methods/assertHasCharacterSlots';
import getSlotFillFilter from '/imports/api/creature/creatureProperties/methods/getSlotFillFilter';
import getCreatureLibraryIds from '/imports/api/library/getCreatureLibraryIds';
import LibraryNodes from '/imports/api/library/LibraryNodes';
import { insertExperienceForCreature } from '/imports/api/creature/experience/Experiences';
import SimpleSchema from 'simpl-schema';
const insertCreature = new ValidatedMethod({
name: 'creatures.insertCreature',
mixins: [RateLimiterMixin, simpleSchemaMixin],
schema: CreatureSchema.pick(
validate: CreatureSchema.pick(
'name',
'gender',
'alignment',
@@ -26,7 +26,7 @@ const insertCreature = new ValidatedMethod({
type: SimpleSchema.Integer,
min: 0,
},
}),
}).validator(),
rateLimit: {
numRequests: 5,
timeInterval: 5000,
@@ -48,8 +48,13 @@ const insertCreature = new ValidatedMethod({
name,
gender,
alignment,
type: 'pc',
allowedLibraries,
allowedLibraryCollections,
settings: {},
readers: [],
writers: [],
public: false,
});
// Insert experience to get character to starting level
@@ -61,7 +66,6 @@ const insertCreature = new ValidatedMethod({
creatureId
},
creatureId,
userId,
});
}
@@ -96,7 +100,6 @@ function insertDefaultRuleset(creatureId, baseId, userId, slot) {
insertPropertyFromLibraryNode.call({
nodeIds: [ruleset._id],
parentRef: { id: baseId, collection: 'creatureProperties' },
order: 0.5,
});
}
}

View File

@@ -1,18 +1,19 @@
import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import { assertOwnership } from '/imports/api/creature/creatures/creaturePermissions.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js';
import Experiences from '/imports/api/creature/experience/Experiences.js';
import { assertOwnership } from '/imports/api/creature/creatures/creaturePermissions';
import Creatures from '/imports/api/creature/creatures/Creatures';
import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
import CreatureLogs from '/imports/api/creature/log/CreatureLogs';
import Experiences from '/imports/api/creature/experience/Experiences';
import { getFilter } from '/imports/api/parenting/parentingFunctions';
function removeRelatedDocuments(creatureId){
CreatureVariables.remove({_creatureId: creatureId});
CreatureProperties.remove({'ancestors.id': creatureId});
CreatureLogs.remove({creatureId});
Experiences.remove({creatureId});
function removeRelatedDocuments(creatureId) {
CreatureVariables.remove({ _creatureId: creatureId });
CreatureProperties.remove(getFilter.descendantsOfRoot(creatureId));
CreatureLogs.remove({ creatureId });
Experiences.remove({ creatureId });
}
const removeCreature = new ValidatedMethod({
@@ -20,7 +21,7 @@ const removeCreature = new ValidatedMethod({
validate: new SimpleSchema({
charId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
max: 32,
},
}).validator(),
mixins: [RateLimiterMixin],
@@ -28,14 +29,14 @@ const removeCreature = new ValidatedMethod({
numRequests: 5,
timeInterval: 5000,
},
run({charId}) {
run({ charId }) {
assertOwnership(charId, this.userId)
this.unblock();
removeCreatureWork(charId)
},
});
export function removeCreatureWork(creatureId){
export function removeCreatureWork(creatureId) {
Creatures.remove(creatureId);
removeRelatedDocuments(creatureId);
}

View File

@@ -1,144 +0,0 @@
import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import { union } from 'lodash';
import ActionContext from '/imports/api/engine/actions/ActionContext.js';
import { applyTriggers } from '/imports/api/engine/actions/applyTriggers.js';
const restCreature = new ValidatedMethod({
name: 'creature.methods.rest',
validate: new SimpleSchema({
creatureId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
restType: {
type: String,
allowedValues: ['shortRest', 'longRest'],
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({ creatureId, restType }) {
// Get action context
const actionContext = new ActionContext(creatureId, [creatureId], this);
// Check permissions
assertEditPermission(actionContext.creature, this.userId);
// Join, sort, and apply before triggers
const beforeTriggers = union(
actionContext.triggers.anyRest?.before, actionContext.triggers[restType]?.before
).sort((a, b) => a.order - b.order);
applyTriggers(beforeTriggers, null, actionContext);
// Rest
actionContext.addLog({
name: restType === 'shortRest' ? 'Short rest' : 'Long rest',
});
doRestWork(restType, actionContext);
// Join, sort, and apply after triggers
const afterTriggers = union(
actionContext.triggers.anyRest?.after, actionContext.triggers[restType]?.after
).sort((a, b) => a.order - b.order);
applyTriggers(afterTriggers, null, actionContext);
// Insert log
actionContext.writeLog();
},
});
function doRestWork(restType, actionContext) {
const creatureId = actionContext.creature._id;
// Long rests reset short rest properties as well
let resetFilter;
if (restType === 'shortRest'){
resetFilter = 'shortRest'
} else {
resetFilter = {$in: ['shortRest', 'longRest']}
}
// Only apply to active properties
let filter = {
'ancestors.id': creatureId,
reset: resetFilter,
removed: { $ne: true },
inactive: { $ne: true },
};
// update all attribute's damage
filter.type = 'attribute';
CreatureProperties.update(filter, {
$set: {
damage: 0,
dirty: true,
}
}, {
selector: {type: 'attribute'},
multi: true,
});
// Update all action-like properties' usesUsed
filter.type = {$in: [
'action',
'attack',
'spell'
]};
CreatureProperties.update(filter, {
$set: {
usesUsed: 0,
dirty: true,
}
}, {
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,
}
}).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,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 Creatures from '/imports/api/creature/creatures/Creatures';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions';
const updateCreature = new ValidatedMethod({
name: 'creatures.update',

View File

@@ -1,9 +1,9 @@
import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions';
import Creatures from '/imports/api/creature/creatures/Creatures';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS';
let Experiences = new Mongo.Collection('experiences');
@@ -26,7 +26,7 @@ let ExperienceSchema = new SimpleSchema({
min: 0,
index: 1,
},
// The real-world date that it occured, usually sorted by date
// The real-world date that it occurred, usually sorted by date
date: {
type: Date,
autoValue: function () {
@@ -39,7 +39,7 @@ let ExperienceSchema = new SimpleSchema({
},
creatureId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
max: 32,
index: 1,
},
});
@@ -76,7 +76,7 @@ const insertExperience = new ValidatedMethod({
},
'creatureIds.$': {
type: String,
regEx: SimpleSchema.RegEx.Id,
max: 32,
},
}).validator(),
mixins: [RateLimiterMixin],
@@ -93,7 +93,7 @@ const insertExperience = new ValidatedMethod({
let insertedIds = [];
creatureIds.forEach(creatureId => {
assertEditPermission(creatureId, userId);
let id = insertExperienceForCreature({ experience, creatureId, userId });
let id = insertExperienceForCreature({ experience, creatureId });
insertedIds.push(id);
});
return insertedIds;
@@ -105,7 +105,7 @@ const removeExperience = new ValidatedMethod({
validate: new SimpleSchema({
experienceId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
max: 32,
},
}).validator(),
mixins: [RateLimiterMixin],
@@ -146,7 +146,7 @@ const recomputeExperiences = new ValidatedMethod({
validate: new SimpleSchema({
creatureId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
max: 32,
},
}).validator(),
mixins: [RateLimiterMixin],

View File

@@ -1,5 +1,5 @@
import SimpleSchema from 'simpl-schema';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS';
let ExperienceSchema = new SimpleSchema({
title: {
@@ -42,7 +42,7 @@ let ExperienceSchema = new SimpleSchema({
// ID of the journal this entry belongs to
journalId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
max: 32,
index: 1,
}
});

View File

@@ -1,17 +1,19 @@
import SimpleSchema from 'simpl-schema';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables.js';
import LogContentSchema from '/imports/api/creature/log/LogContentSchema.js';
import Creatures from '/imports/api/creature/creatures/Creatures';
import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables';
import LogContentSchema from '/imports/api/creature/log/LogContentSchema';
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 resolve, { toString } from '/imports/parser/resolve.js';
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions';
import { parse, prettifyParseError } from '/imports/parser/parser';
import resolve from '/imports/parser/resolve';
import toString from '/imports/parser/toString';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS';
const PER_CREATURE_LOG_LIMIT = 100;
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
if (Meteor.isServer) {
var sendWebhookAsCreature = require('/imports/server/discord/sendWebhook.js').sendWebhookAsCreature;
var sendWebhookAsCreature = require('/imports/server/discord/sendWebhook').sendWebhookAsCreature;
}
let CreatureLogs = new Mongo.Collection('creatureLogs');
@@ -36,11 +38,22 @@ let CreatureLogSchema = new SimpleSchema({
},
index: 1,
},
// The acting creature initiating the logged events
creatureId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
index: 1,
},
// The tabletop this log is associated with
tabletopId: {
type: String,
optional: true,
index: 1,
},
// The action that caused this log entry
actionId: {
type: String,
optional: true,
},
creatureName: {
type: String,
optional: true,
@@ -50,11 +63,17 @@ let CreatureLogSchema = new SimpleSchema({
CreatureLogs.attachSchema(CreatureLogSchema);
function removeOldLogs(creatureId) {
function removeOldLogs({ creatureId, tabletopId }) {
let filter;
if (creatureId && tabletopId || (!creatureId && !tabletopId)) {
throw Error('Provide either creatureId or tabletopId')
} else if (creatureId) {
filter = { creatureId };
} else if (tabletopId) {
filter = { tabletopId }
}
// Find the first log that is over the limit
let firstExpiredLog = CreatureLogs.find({
creatureId
}, {
let firstExpiredLog = CreatureLogs.find(filter, {
sort: { date: -1 },
skip: PER_CREATURE_LOG_LIMIT,
});
@@ -70,10 +89,21 @@ function logToMessageData(log) {
let embed = {
fields: [],
};
log.content.forEach(field => {
log.content.forEach((field, index) => {
// Empty character for blank names
if (!field.name) field.name = '\u200b';
if (!field.value) field.value = '\u200b';
embed.fields.push(field);
// Enforce Discord field character limits
if (field.name?.length > 256) {
field.name = field.name.substring(0, 255);
}
if (field.value?.length > 1024) {
field.value = field.value.substring(0, 1024 - 3) + '...';
}
// Enforce Discord 25 field limit
if (index < 25) {
embed.fields.push(field);
}
});
return { embeds: [embed] };
}
@@ -107,6 +137,7 @@ const insertCreatureLog = new ValidatedMethod({
'settings.discordWebhook': 1,
name: 1,
avatarPicture: 1,
tabletop: 1,
}
});
assertEditPermission(creature, this.userId);
@@ -122,13 +153,27 @@ export function insertCreatureLogWork({ log, creature, method }) {
log = { content: [{ value: log }] };
}
if (!log.content?.length) return;
// Truncate the string lengths to fit the log content schema
log.content.forEach((logItem) => {
if (logItem.value?.length > STORAGE_LIMITS.summary) {
logItem.value = logItem.value.substring(0, STORAGE_LIMITS.summary - 3) + '...';
}
});
log.date = new Date();
if (creature && creature.tabletop) log.tabletopId = creature.tabletop;
// Insert it
let id = CreatureLogs.insert(log);
if (Meteor.isServer) {
method?.unblock();
removeOldLogs(creature._id);
logWebhook({ log, creature });
if (creature) {
logWebhook({ log, creature });
}
if (log.tabletopId) {
removeOldLogs({ tabletopId: log.tabletopId });
} else {
removeOldLogs({ creatureId: creature._id });
}
}
return id;
}
@@ -152,22 +197,29 @@ const logRoll = new ValidatedMethod({
},
creatureId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
max: 32,
optional: true,
},
}).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 });
async run({ roll, creatureId }) {
if (!creatureId) throw new Meteor.Error('no-id',
'A creature id must be given'
);
let creature;
if (creatureId) {
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 {
@@ -180,7 +232,7 @@ const logRoll = new ValidatedMethod({
let {
result: compiled,
context
} = resolve('compile', parsedResult, variables);
} = await resolve('compile', parsedResult, variables);
const compiledString = toString(compiled);
if (!equalIgnoringWhitespace(compiledString, roll)) logContent.push({
value: roll
@@ -188,12 +240,12 @@ const logRoll = new ValidatedMethod({
logContent.push({
value: compiledString
});
let { result: rolled } = resolve('roll', compiled, variables, context);
let { result: rolled } = await 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 } = await resolve('reduce', rolled, variables, context);
let resultString = toString(result);
if (resultString !== rolledString) logContent.push({
value: resultString

View File

@@ -1,7 +1,18 @@
import SimpleSchema from 'simpl-schema';
import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js';
import RollDetailsSchema from '/imports/api/properties/subSchemas/RollDetailsSchema.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema';
import RollDetailsSchema from '/imports/api/properties/subSchemas/RollDetailsSchema';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS';
export interface LogContent {
name?: string
value?: string
inline?: boolean
context?: {
errors: any[]
rolls: any[]
doubleRolls?: boolean
}
}
let LogContentSchema = new SimpleSchema({
// The name of the field, included in discord webhook message
@@ -22,11 +33,16 @@ let LogContentSchema = new SimpleSchema({
type: Boolean,
optional: true,
},
// This log entry was silenced
silenced: {
type: Boolean,
optional: true,
},
context: {
type: Object,
optional: true,
},
'context.errors':{
'context.errors': {
type: Array,
defaultValue: [],
maxCount: STORAGE_LIMITS.errorCount,
@@ -46,6 +62,13 @@ let LogContentSchema = new SimpleSchema({
type: Boolean,
optional: true,
},
targetIds: {
type: Array,
optional: true,
},
'targetIds.$': {
type: String,
}
});
export default LogContentSchema;

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,3 +0,0 @@
if (Meteor.isServer) throw 'Client side only collection, don\'t import on server';
const Docs = new Mongo.Collection('docs');
export default Docs;

View File

@@ -0,0 +1,287 @@
import { Meteor } from 'meteor/meteor';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import SimpleSchema from 'simpl-schema';
import { softRemove } from '/imports/api/parenting/softRemove';
import SoftRemovableSchema from '/imports/api/parenting/SoftRemovableSchema';
import { storedIconsSchema } from '/imports/api/icons/Icons';
import '/imports/api/library/methods/index';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS';
import { restore } from '/imports/api/parenting/softRemove';
import { getFilter, rebuildNestedSets, moveDocWithinRoot } from '/imports/api/parenting/parentingFunctions';
import ChildSchema, { TreeDoc } from '/imports/api/parenting/ChildSchema';
// Give the docs a common root, so they can share parenting logic
export const DOC_ROOT_ID = 'DDDDDDDDDDDDDDDDD'
type Doc = {
_id: string,
name: string,
urlName: string,
href: string,
description?: string,
published?: true,
icon?: {
name: string,
shape: string,
},
} & TreeDoc;
const Docs: Mongo.Collection<Doc> & {
getJsonDocs?: () => string
} = new Mongo.Collection<Doc>('docs');
const DocSchema = new SimpleSchema({
_id: {
type: String,
max: 32,
},
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,
},
});
const schema = new SimpleSchema({});
schema.extend(DocSchema);
schema.extend(ChildSchema);
schema.extend(SoftRemovableSchema);
// @ts-expect-error No attach schema in types
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: Doc, urlName?: string) {
if (!urlName) urlName = doc.urlName;
const address = ['/docs'];
const ancestorDocs = Docs.find(getFilter.ancestors(doc));
ancestorDocs?.forEach(a => {
address.push(a.urlName);
});
address.push(urlName);
return address.join('/');
}
// 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()) {
console.info('No docs found, filling documentation with defaults');
Assets.getText('docs/defaultDocs.json', (error, string) => {
const docs = JSON.parse(string)
docs.forEach(doc => Docs.insert(doc));
rebuildNestedSets(Docs, DOC_ROOT_ID);
});
}
});
}
const insertDoc = new ValidatedMethod({
name: 'docs.insert',
validate: null,
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({ doc, parentId }) {
delete doc._id;
assertDocsEditPermission(this.userId);
doc.parentId = parentId;
doc.root = {
collection: 'docs',
id: DOC_ROOT_ID,
};
const lastOrder = Docs.find({}, { sort: { left: -1 }, limit: 1 }).fetch()[0]?.left || 0;
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);
rebuildNestedSets(Docs, DOC_ROOT_ID);
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);
const 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);
if (!doc) throw new Meteor.Error('Not Found', 'The document you are trying to edit was not found');
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;
}
const updates = Docs.update(_id, modifier);
rebuildNestedSets(Docs, DOC_ROOT_ID);
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(Docs, _id);
rebuildNestedSets(Docs, DOC_ROOT_ID);
}
});
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('docs', _id);
rebuildNestedSets(Docs, DOC_ROOT_ID);
}
});
const organizeDoc = new ValidatedMethod({
name: 'docs.organizeDoc',
validate: new SimpleSchema({
docId: String,
newPosition: Number,
skipClient: {
type: Boolean,
optional: true,
}
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
async run({ docId, newPosition, skipClient }: { docId: string, newPosition: number, skipClient?: boolean }) {
if (skipClient && this.isSimulation) {
return;
}
assertDocsEditPermission(this.userId);
const doc = Docs.findOne(docId);
if (!doc) throw new Meteor.Error('not found', 'The doc you are moving was not found');
// Move the doc
await moveDocWithinRoot(doc, Docs, newPosition);
},
});
export {
DocSchema,
insertDoc,
updateDoc,
pushToDoc,
pullFromDoc,
softRemoveDoc,
restoreDoc,
organizeDoc,
};
export default Docs;

View File

@@ -0,0 +1,136 @@
import SimpleSchema from 'simpl-schema';
import TaskResult from './tasks/TaskResult';
import LogContentSchema from '/imports/api/creature/log/LogContentSchema';
import Task from './tasks/Task';
const EngineActions = new Mongo.Collection<EngineAction>('actions');
export interface EngineAction {
_id?: string;
_isSimulation?: boolean;
_stepThrough?: boolean;
_decisions?: any[],
task: Task;
creatureId: string;
tabletopId?: string;
results: TaskResult[];
taskCount: number;
}
const ActionSchema = new SimpleSchema({
creatureId: {
type: String,
max: 32,
// @ts-expect-error index not defined
index: 1,
},
rootPropId: {
type: String,
max: 32,
optional: true,
},
tabletopId: {
type: String,
max: 32,
optional: true,
// @ts-expect-error index not defined
index: 1,
},
task: {
type: Object,
blackbox: true,
},
// Applied properties
results: {
type: Array,
defaultValue: [],
},
'results.$': {
type: Object,
},
// The property and target ids popped off the task stack
// Pushing these to the top of the stack and deleting the results from this point onwards
// Should re-run the action identically from this point
'results.$.propId': {
type: String,
max: 32,
},
'results.$.targetIds': {
type: Array,
defaultValue: [],
},
'results.$.targetIds.$': {
type: String,
max: 32,
},
// Changes that override the local scope
'results.$.scope': {
type: Object,
optional: true,
blackbox: true,
},
// Changes that consume pushed values from the local scope
'results.$.popScope': {
type: Object,
optional: true,
blackbox: true,
},
// Changes that push values to the local scope
'results.$.pushScope': {
type: Object,
optional: true,
blackbox: true,
},
// database changes
'results.$.mutations': {
type: Array,
optional: true,
},
'results.$.mutations.$': {
type: Object,
},
'results.$.mutations.$.targetIds': {
type: Array,
},
'results.$.mutations.$.targetIds.$': {
type: String,
max: 32,
},
'results.$.mutations.$.updates': {
type: Array,
optional: true,
},
'results.$.mutations.$.updates.$': {
type: Object,
},
'results.$.mutations.$.updates.$.propId': {
type: String,
max: 32,
},
// Required, because CreatureProperties.update requires a selector of { type }
'results.$.mutations.$.updates.$.type': {
type: String,
},
'results.$.mutations.$.updates.$.set': {
type: Object,
optional: true,
blackbox: true,
},
'results.$.mutations.$.updates.$.inc': {
type: Object,
optional: true,
blackbox: true,
},
'results.$.mutations.$.contents': {
type: Array,
optional: true,
},
'results.$.mutations.$.contents.$': {
type: LogContentSchema,
},
});
EngineActions.attachSchema(ActionSchema);
export default EngineActions;
export { ActionSchema }

View File

@@ -0,0 +1,466 @@
import { assert } from 'chai';
import {
allLogContent,
allMutations,
allUpdates,
createTestCreature,
getRandomIds,
removeAllCreaturesAndProps,
runActionById,
TestCreature
} from '/imports/api/engine/action/functions/actionEngineTest.testFn';
import { LogContent, Mutation, Update } from '/imports/api/engine/action/tasks/TaskResult';
import Alea from 'alea';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
const [
creatureId, targetCreatureId, targetCreature2Id, emptyActionId, selfActionId, attackActionId,
usesActionId, attackMissId, attackNoTargetId, usesResourcesActionId, ammoId, resourceAttId,
consumeAmmoId, consumeResourceId, noUsesActionId, insufficientResourcesActionId,
attributeResetByEventId, eventActionId, advantageAttackId, advantageEffectId, disadvantageAttackId, disadvantageEffectId,
] = getRandomIds(100);
const actionTestCreature: TestCreature = {
_id: creatureId,
props: [
// Empty
{
_id: emptyActionId,
type: 'action',
summary: { text: 'Summary text 1 + 1 = {1 + 1}' }
},
// Attack that targets self
{
_id: selfActionId,
type: 'action',
target: 'self',
},
// Attack that hits
{
_id: attackActionId,
type: 'action',
attackRoll: { calculation: '10' },
},
// Attack that misses
{
_id: attackMissId,
type: 'action',
attackRoll: { calculation: '-5' },
},
// Attack that has Advantage
{
_id: advantageAttackId,
type: 'action',
attackRoll: { calculation: '0' },
tags: ['hasAdvantage'],
},
{
_id: advantageEffectId,
type: 'effect',
operation: 'advantage',
targetByTags: true,
targetTags: ['hasAdvantage'],
},
// Attack that has Disadvantage
{
_id: disadvantageAttackId,
type: 'action',
attackRoll: { calculation: '0' },
tags: ['hasDisadvantage'],
},
{
_id: disadvantageEffectId,
type: 'effect',
operation: 'disadvantage',
targetByTags: true,
targetTags: ['hasDisadvantage'],
},
// Attack that has no target
{
_id: attackNoTargetId,
type: 'action',
attackRoll: { calculation: '1' },
},
// Disable crits
{
type: 'attribute',
attributeType: 'stat',
variableName: '~criticalHitTarget',
baseValue: { calculation: '21' },
},
{
type: 'attribute',
attributeType: 'stat',
variableName: '~criticalMissTarget',
baseValue: { calculation: '0' },
},
// Has uses
{
_id: usesActionId,
type: 'action',
uses: { calculation: '3' },
usesUsed: 1,
reset: 'longRest',
},
// Not enough uses
{
_id: noUsesActionId,
type: 'action',
uses: { calculation: '5' },
usesUsed: 5,
reset: 'longRest',
},
// Uses Resources
{
_id: ammoId,
type: 'item',
quantity: 12,
tags: ['ammo']
},
{
_id: resourceAttId,
type: 'attribute',
name: 'Resource Name',
attributeType: 'stat',
baseValue: { calculation: '7' },
variableName: 'resourceVar',
},
{
_id: usesResourcesActionId,
type: 'action',
resources: {
itemsConsumed: [{
_id: consumeAmmoId,
tag: 'ammo',
quantity: { calculation: '3' },
itemId: ammoId,
}],
attributesConsumed: [{
_id: consumeResourceId,
variableName: 'resourceVar',
quantity: { calculation: '2' },
}],
conditions: [],
}
},
{
_id: insufficientResourcesActionId,
type: 'action',
resources: {
attributesConsumed: [{
_id: consumeResourceId,
variableName: 'resourceVar',
quantity: { calculation: '9001' },
}],
itemsConsumed: [],
conditions: [],
}
},
// Events and resetting attributes
{
_id: attributeResetByEventId,
type: 'attribute',
name: 'Attribute Reset By testEvent Event',
attributeType: 'stat',
baseValue: { calculation: '27' },
damage: 13,
variableName: 'resetByEventAtt',
reset: 'testEvent'
},
{
_id: eventActionId,
type: 'action',
actionType: 'event',
variableName: 'testEvent',
},
],
}
const actionTargetCreature: TestCreature = {
_id: targetCreatureId,
props: [
{
type: 'attribute',
attributeType: 'stat',
variableName: 'armor',
baseValue: { calculation: '10' },
}
]
}
const actionTargetCreature2: TestCreature = {
_id: targetCreature2Id,
props: [
{
type: 'attribute',
attributeType: 'stat',
variableName: 'armor',
baseValue: { calculation: '10' },
}
]
}
describe('Apply Action Properties', function () {
// Increase timeout
this.timeout(8000);
before(async function () {
await removeAllCreaturesAndProps();
await createTestCreature(actionTestCreature);
await createTestCreature(actionTargetCreature);
await createTestCreature(actionTargetCreature2);
});
it('should generate random numbers reliably given consistent seeds', function () {
const aleaFraction = Alea('test', 'seeds');
const randomNumbers = [aleaFraction(), aleaFraction(), aleaFraction()];
assert.deepEqual(randomNumbers, [
0.19889510236680508, 0.9176857066340744, 0.042551583144813776
]);
});
it('should run empty actions', async function () {
const action = await runActionById(emptyActionId);
assert.exists(action);
assert.deepEqual(allMutations(action), [{
contents: [{
name: 'Action',
value: 'Summary text 1 + 1 = 2',
}],
targetIds: [],
}]);
});
it('should target self when set', async function () {
const action = await runActionById(selfActionId);
assert.exists(action);
assert.deepEqual(allMutations(action), [{
contents: [{
name: 'Action',
}],
targetIds: [creatureId],
}]);
});
it('should make attack rolls against no targets', async function () {
const action = await runActionById(attackNoTargetId, []);
const expectedMutations: Mutation[] = [
{
contents: [{ name: 'Action' }],
targetIds: [],
}, {
contents: [{
name: 'To Hit',
value: '1d20 [10] + 1\n**11**',
inline: true,
}],
targetIds: [],
}
];
assert.deepEqual(allMutations(action), expectedMutations);
})
it('should make attack rolls against multiple creatures', async function () {
const action = await runActionById(attackActionId, [
targetCreatureId,
targetCreature2Id,
]);
const expectedMutations: Mutation[] = [
{
contents: [{ name: 'Action' }],
targetIds: [targetCreatureId, targetCreature2Id]
}, {
contents: [{
inline: true,
name: 'Hit!',
value: '1d20 [10] + 10\n**20**',
}],
targetIds: [targetCreatureId],
}, {
contents: [{
inline: true,
name: 'Hit!',
value: '1d20 [10] + 10\n**20**',
}],
targetIds: [targetCreature2Id],
},
];
assert.deepEqual(allMutations(action), expectedMutations);
});
it('should make attack rolls that use uses', async function () {
const action = await runActionById(usesActionId, [targetCreatureId]);
const expectedUpdates: Update[] = [
{
propId: usesActionId,
type: 'action',
inc: { usesUsed: 1, usesLeft: -1 },
}
]
assert.deepEqual(allUpdates(action), expectedUpdates);
});
it('should fail to make attacks that have no uses left', async function () {
const action = await runActionById(noUsesActionId, [targetCreatureId]);
const expectedContent: LogContent[] = [
{
name: 'Action'
}, {
name: 'Error',
value: 'Action does not have enough uses left'
}
]
assert.deepEqual(allLogContent(action), expectedContent);
});
it('should make attack rolls that miss', async function () {
const action = await runActionById(attackMissId, [targetCreatureId]);
const expectedMutations: Mutation[] = [
{
contents: [{ name: 'Action' }],
targetIds: [targetCreatureId],
}, {
contents: [{
inline: true,
name: 'Miss!',
value: '1d20 [10] 5\n**5**', // DiceCloud uses unicode minus
}],
targetIds: [targetCreatureId],
}
];
assert.deepEqual(allMutations(action), expectedMutations);
});
it('should make attack rolls that roll with advantage', async function () {
const prop = await CreatureProperties.findOneAsync(advantageAttackId);
assert(prop);
assert(prop.type === 'action')
assert.equal(prop.attackRoll?.advantage, 1, 'The attack roll should have advantage');
const action = await runActionById(advantageAttackId, [targetCreatureId]);
const expectedMutations: Mutation[] = [
{
contents: [{ name: 'Action' }],
targetIds: [targetCreatureId],
}, {
contents: [{
inline: true,
name: 'Hit! (Advantage)',
value: '1d20 [ ~~10~~, 11 ] + 0\n**11**',
}],
targetIds: [targetCreatureId],
}
];
assert.deepEqual(allMutations(action), expectedMutations);
});
it('should make attack rolls that roll with disadvantage', async function () {
const prop = await CreatureProperties.findOneAsync(disadvantageAttackId);
assert(prop);
assert(prop.type === 'action');
assert.equal(prop.attackRoll?.disadvantage, 1, 'The attack roll should have disadvantage');
const action = await runActionById(disadvantageAttackId, [targetCreatureId]);
const expectedMutations: Mutation[] = [
{
contents: [{ name: 'Action' }],
targetIds: [targetCreatureId],
}, {
contents: [{
inline: true,
name: 'Hit! (Disadvantage)',
value: '1d20 [ 10, ~~11~~ ] + 0\n**10**',
}],
targetIds: [targetCreatureId],
}
];
assert.deepEqual(allMutations(action), expectedMutations);
});
it('actions should consume resources', async function () {
const action = await runActionById(usesResourcesActionId, []);
const expectedMutations: Mutation[] = [
{
contents: [{ name: 'Action' }],
targetIds: []
},
{
contents: [{
inline: true,
name: 'Stat damaged',
value: '2 Resource Name',
}],
targetIds: [creatureId],
updates: [{
inc: {
damage: 2,
value: -2
},
propId: resourceAttId,
type: 'attribute'
}],
},
{
targetIds: [],
updates: [
{
inc: {
quantity: -3
},
propId: ammoId,
type: 'item',
}
]
}
];
assert.deepEqual(allMutations(action), expectedMutations);
});
it('should handle insufficient resources', async function () {
const action = await runActionById(insufficientResourcesActionId, []);
const expectedMutations: Mutation[] = [
{
contents: [{
name: 'Action'
}, {
name: 'Error',
value: 'This creature doesn\'t have sufficient resources to perform this action',
}],
targetIds: [],
},
];
assert.deepEqual(allMutations(action), expectedMutations);
});
it('should reset attributes when events happen', async function () {
const action = await runActionById(eventActionId, []);
const expectedMutations: Mutation[] = [
{
contents: [{
name: 'Action'
}],
targetIds: [],
},
{
contents: [
{
inline: true,
name: 'Stat restored',
value: '+13 Attribute Reset By testEvent Event',
},
],
targetIds: [creatureId],
updates: [
{
inc: {
damage: -13,
value: 13,
},
propId: attributeResetByEventId,
type: 'attribute',
},
],
}
];
assert.deepEqual(allMutations(action), expectedMutations);
});
});

View File

@@ -0,0 +1,268 @@
import { EngineAction } from '/imports/api/engine/action/EngineActions';
import { PropTask } from '../tasks/Task';
import TaskResult, { LogContent } from '../tasks/TaskResult';
import { getVariables } from '/imports/api/engine/loadCreatures';
import getPropertyTitle from '/imports/api/utility/getPropertyTitle';
import recalculateInlineCalculations from '/imports/api/engine/action/functions/recalculateInlineCalculations';
import spendResources from '/imports/api/engine/action/functions/spendResources';
import { applyAfterChildrenTriggers, applyAfterTriggers, applyChildren } from '/imports/api/engine/action/functions/applyTaskGroups';
import recalculateCalculation from '/imports/api/engine/action/functions/recalculateCalculation';
import { getEffectiveActionScope } from '/imports/api/engine/action/functions/getEffectiveActionScope';
import numberToSignedString from '/imports/api/utility/numberToSignedString';
import { getNumberFromScope } from '/imports/api/creature/creatures/CreatureVariables';
import InputProvider from '/imports/api/engine/action/functions/userInput/InputProvider';
import { CalculatedField } from '/imports/api/properties/subSchemas/computedField';
import applyResetTask from '/imports/api/engine/action/tasks/applyResetTask';
import { CreaturePropertyTypes } from '/imports/api/creature/creatureProperties/CreatureProperties';
export default async function applyActionProperty(
task: PropTask, action: EngineAction, result: TaskResult, userInput: InputProvider
): Promise<void> {
const prop = task.prop;
if (prop.type !== 'action' && prop.type !== 'spell') {
throw new Meteor.Error('wrong-property', `Expected an action or a spell, got ${prop.type} instead`);
}
const targetIds = prop.target === 'self' ? [action.creatureId] : task.targetIds;
// If the action is a a spell, make sure we have spell slot defined
if (prop.type === 'spell') {
const scope = await getEffectiveActionScope(action);
if (!('slotLevel' in scope)) {
result.pushScope = {
'~slotLevel': { value: prop.level },
'slotLevel': { value: prop.level },
};
}
}
//Log the name and summary, check that the property has enough resources to fire
if (prop.summary?.text) {
await recalculateInlineCalculations(prop.summary, action, 'reduce', userInput);
}
result.appendLog({
name: getPropertyTitle(prop),
...prop.summary && { value: prop.summary.value },
silenced: prop.silent,
}, targetIds);
// Check Uses
if (prop.usesLeft !== undefined && prop.usesLeft <= 0) {
result.appendLog({
name: 'Error',
value: `${getPropertyTitle(prop)} does not have enough uses left`,
silenced: prop.silent,
}, targetIds);
return;
}
// Check Resources
if (prop.insufficientResources) {
result.appendLog({
name: 'Error',
value: 'This creature doesn\'t have sufficient resources to perform this action',
silenced: prop.silent,
}, targetIds);
return;
}
await spendResources(action, prop, targetIds, result, userInput);
const attack = prop.attackRoll;
// Attack if there is an attack roll
if (attack && attack.calculation) {
if (targetIds.length) {
for (const targetId of targetIds) {
await applyAttackToTarget(task, action, attack, targetId, result, userInput);
await applyAfterTriggers(action, prop, [targetId], userInput);
await applyChildren(action, prop, [targetId], userInput);
}
} else {
await applyAttackWithoutTarget(action, prop, attack, result, userInput);
await applyAfterTriggers(action, prop, targetIds, userInput);
await applyChildren(action, prop, targetIds, userInput);
}
} else {
await applyAfterTriggers(action, prop, targetIds, userInput);
await applyChildren(action, prop, targetIds, userInput);
}
if (prop.actionType === 'event' && prop.variableName) {
await applyResetTask({
subtaskFn: 'reset',
eventName: prop.variableName,
targetIds: [action.creatureId],
}, action, result, userInput);
}
// Finish
return await applyAfterChildrenTriggers(action, prop, targetIds, userInput);
}
async function applyAttackToTarget(
task: PropTask, action: EngineAction, attack: CalculatedField, targetId: string,
taskResult: TaskResult, userInput: InputProvider
) {
const prop = task.prop as CreaturePropertyTypes['action'] | CreaturePropertyTypes['spell'];
taskResult.pushScope = {
'~attackHit': {},
'~attackMiss': {},
'~criticalHit': {},
'~criticalMiss': {},
'~attackRoll': {},
}
await recalculateCalculation(attack, action, 'reduce', userInput);
const scope = await getEffectiveActionScope(action);
const contents: LogContent[] = [];
const {
resultPrefix,
result,
criticalHit,
criticalMiss,
advantage
} = await rollAttack(attack, scope, taskResult.pushScope, userInput);
const targetScope = getVariables(targetId);
const targetArmor = getNumberFromScope('armor', targetScope)
if (targetArmor !== undefined) {
let name = criticalHit ? 'Critical Hit!' :
criticalMiss ? 'Critical Miss!' :
result >= targetArmor ? 'Hit!' : 'Miss!';
if (advantage === 1) {
name += ' (Advantage)';
} else if (advantage === -1) {
name += ' (Disadvantage)';
}
contents.push({
name,
value: `${resultPrefix}\n**${result}**`,
inline: true,
...prop.silent && { silenced: true },
});
if (criticalMiss || result < targetArmor) {
taskResult.pushScope['~attackMiss'] = { value: true };
} else {
taskResult.pushScope['~attackHit'] = { value: true };
}
} else {
contents.push({
name: 'Error',
value: 'Target has no `armor`',
inline: true,
...prop.silent && { silenced: true },
}, {
name: criticalHit ? 'Critical Hit!' : criticalMiss ? 'Critical Miss!' : 'To Hit',
value: `${resultPrefix}\n**${result}**`,
inline: true,
...prop.silent && { silenced: true },
});
}
if (contents.length) {
taskResult.mutations.push({
contents,
targetIds: [targetId],
});
}
}
async function applyAttackWithoutTarget(action, prop, attack, taskResult: TaskResult, userInput: InputProvider) {
taskResult.pushScope = {
'~attackHit': {},
'~attackMiss': {},
'~criticalHit': {},
'~criticalMiss': {},
'~attackRoll': {},
}
await recalculateCalculation(attack, action, 'reduce', userInput);
const scope = await getEffectiveActionScope(action);
const {
resultPrefix,
result,
criticalHit,
criticalMiss,
advantage,
} = await rollAttack(attack, scope, taskResult.pushScope, userInput);
let name = criticalHit ? 'Critical Hit!' : criticalMiss ? 'Critical Miss!' : 'To Hit';
if (advantage === 1) {
name += ' (Advantage)';
} else if (advantage === -1) {
name += ' (Disadvantage)';
}
if (!criticalMiss) {
taskResult.pushScope['~attackHit'] = { value: true }
}
if (!criticalHit) {
taskResult.pushScope['~attackMiss'] = { value: true };
}
taskResult.mutations.push({
contents: [{
name,
value: `${resultPrefix}\n**${result}**`,
inline: true,
...prop.silent && { silenced: true },
}],
targetIds: [],
});
}
async function rollAttack(attack, scope: any, resultPushScope, userInput: InputProvider) {
const advantage: 0 | 1 | -1 = await userInput.advantage(
(!!attack.advantage && !attack.disadvantage) ? 1 :
(!attack.advantage && !!attack.disadvantage) ? -1 :
0
);
const rollModifierText = numberToSignedString(attack.value, true);
let value, resultPrefix;
if (advantage === 1) {
const [[a, b]] = await userInput.rollDice([{ number: 2, diceSize: 20 }]);
if (a >= b) {
value = a;
resultPrefix = `1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText}`;
} else {
value = b;
resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`;
}
} else if (advantage === -1) {
const [[a, b]] = await userInput.rollDice([{ number: 2, diceSize: 20 }]);
if (a <= b) {
value = a;
resultPrefix = `1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText}`;
} else {
value = b;
resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`;
}
} else {
[[value]] = await userInput.rollDice([{ number: 1, diceSize: 20 }]);
resultPrefix = `1d20 [${value}] ${rollModifierText}`
}
resultPushScope['~attackDiceRoll'] = { value };
const result = value + attack.value;
resultPushScope['~attackRoll'] = { value: result };
const { criticalHit, criticalMiss } = applyCrits(value, scope, resultPushScope);
return { resultPrefix, result, value, criticalHit, criticalMiss, advantage };
}
function applyCrits(value, scope, resultPushScope) {
const scopeCritTarget = getNumberFromScope('~criticalHitTarget', scope);
const criticalHitTarget = scopeCritTarget !== undefined &&
Number.isFinite(scopeCritTarget) ? scopeCritTarget : 20;
const scopeCritMissTarget = getNumberFromScope('~criticalMissTarget', scope);
const criticalMissTarget = scopeCritMissTarget !== undefined &&
Number.isFinite(scopeCritMissTarget) ? scopeCritMissTarget : 1;
const criticalHit = value >= criticalHitTarget;
const criticalMiss = value <= criticalMissTarget;
if (criticalHit) {
resultPushScope['~criticalHit'] = { value: true };
} else if (criticalMiss) {
resultPushScope['~criticalMiss'] = { value: true };
}
return { criticalHit, criticalMiss };
}

View File

@@ -0,0 +1,165 @@
import { assert } from 'chai';
import {
allMutations,
createTestCreature,
getRandomIds,
removeAllCreaturesAndProps,
runActionById,
TestCreature
} from '/imports/api/engine/action/functions/actionEngineTest.testFn';
const [
creatureId, targetCreatureId, targetCreature2Id, adjustmentToTargetId, adjustmentToSelfId, targetCreatureStrengthId, targetCreature2StrengthId, selfDexterityId
] = getRandomIds(100);
const actionTestCreature: TestCreature = {
_id: creatureId,
props: [
{
_id: adjustmentToTargetId,
type: 'adjustment',
target: 'target',
stat: 'strength',
operation: 'increment',
amount: { calculation: '2' }
},
{
_id: adjustmentToSelfId,
type: 'adjustment',
target: 'self',
stat: 'dexterity',
operation: 'set',
amount: { calculation: '11' }
},
{
_id: selfDexterityId,
type: 'attribute',
name: 'Dexterity',
attributeType: 'ability',
variableName: 'dexterity',
baseValue: { calculation: '13' },
},
],
}
const actionTargetCreature: TestCreature = {
_id: targetCreatureId,
props: [
{
_id: targetCreatureStrengthId,
type: 'attribute',
attributeType: 'ability',
variableName: 'strength',
baseValue: { calculation: '12' },
}
]
}
const actionTargetCreature2: TestCreature = {
_id: targetCreature2Id,
props: [
{
_id: targetCreature2StrengthId,
type: 'attribute',
attributeType: 'ability',
variableName: 'strength',
baseValue: { calculation: '18' },
}
]
}
describe('Apply Adjustment Properties', function () {
// Increase timeout
this.timeout(8000);
before(async function () {
await removeAllCreaturesAndProps();
await createTestCreature(actionTestCreature);
await createTestCreature(actionTargetCreature);
await createTestCreature(actionTargetCreature2);
});
it('Adjusts the attributes of self', async function () {
const action = await runActionById(adjustmentToSelfId);
assert.exists(action);
assert.deepEqual(allMutations(action), [{
contents: [
{
inline: true,
name: 'Attribute damage',
value: 'Dexterity set from 13 to 11',
}
],
targetIds: [creatureId],
updates: [
{
propId: selfDexterityId,
type: 'attribute',
set: { damage: 2, value: 11 },
},
],
}]);
});
it('Adjusts the attributes of a single target', async function () {
const action = await runActionById(adjustmentToTargetId, [targetCreatureId]);
assert.exists(action);
assert.deepEqual(allMutations(action), [{
contents: [
{
inline: true,
name: 'Ability damaged',
value: '2 Attribute',
}
],
targetIds: [targetCreatureId],
updates: [
{
propId: targetCreatureStrengthId,
type: 'attribute',
inc: { damage: 2, value: -2 },
},
],
}]);
});
it('Adjusts the attributes of multiple targets', async function () {
const action = await runActionById(adjustmentToTargetId, [
targetCreatureId, targetCreature2Id
]);
assert.exists(action);
assert.deepEqual(allMutations(action), [{
contents: [
{
inline: true,
name: 'Ability damaged',
value: '2 Attribute',
}
],
targetIds: [targetCreatureId],
updates: [
{
propId: targetCreatureStrengthId,
type: 'attribute',
inc: { damage: 2, value: -2 },
},
],
}, {
contents: [
{
inline: true,
name: 'Ability damaged',
value: '2 Attribute',
}
],
targetIds: [targetCreature2Id],
updates: [
{
propId: targetCreature2StrengthId,
type: 'attribute',
inc: { damage: 2, value: -2 },
},
],
}]);
});
});

View File

@@ -0,0 +1,77 @@
import { EngineAction } from '/imports/api/engine/action/EngineActions';
import InputProvider from '/imports/api/engine/action/functions/userInput/InputProvider';
import { applyDefaultAfterPropTasks, applyTaskToEachTarget } from '/imports/api/engine/action/functions/applyTaskGroups';
import recalculateCalculation from '/imports/api/engine/action/functions/recalculateCalculation';
import { PropTask } from '/imports/api/engine/action/tasks/Task';
import TaskResult from '/imports/api/engine/action/tasks/TaskResult';
import applyTask from '/imports/api/engine/action/tasks/applyTask';
import { getSingleProperty, getVariables } from '/imports/api/engine/loadCreatures';
import getPropertyTitle from '/imports/api/utility/getPropertyTitle';
import { CreatureProperty } from '/imports/api/creature/creatureProperties/CreatureProperties';
export default async function applyAdjustmentProperty(
task: PropTask, action: EngineAction, result: TaskResult, userInput: InputProvider
): Promise<void> {
const prop = task.prop;
if (prop.type !== 'adjustment') {
throw new Meteor.Error('wrong-property', `Expected an adjustment, got ${prop.type} instead`);
}
const damageTargetIds = prop.target === 'self' ? [action.creatureId] : task.targetIds;
if (damageTargetIds.length > 1) {
return applyTaskToEachTarget(action, task, damageTargetIds, userInput);
}
// Get the operation and value and push the damage hooks to the queue
if (!prop.amount) {
result.appendLog({
name: 'Error',
value: 'Attribute damage does not have an amount set',
silenced: prop.silent,
}, damageTargetIds);
return;
}
// Evaluate the amount
await recalculateCalculation(prop.amount, action, 'reduce', userInput);
const value = Number(prop.amount.value ?? 0);
if (!isFinite(value)) {
result.appendLog({
name: 'Error',
value: 'Attribute damage does not have a finite amount set',
silenced: prop.silent,
}, damageTargetIds);
return;
}
if (damageTargetIds.length && damageTargetIds.length !== 1) {
throw new Meteor.Error('1 target Expected', 'At this step, only a single target is supported');
}
const targetId = damageTargetIds[0];
let stat: CreatureProperty | undefined;
if (targetId && prop.stat) {
const statId = getVariables(targetId)?.[prop.stat]?._propId;
stat = statId && getSingleProperty(targetId, statId);
if (!stat?.type) {
result.appendLog({
name: 'Error',
value: `Could not apply attribute damage, creature does not have \`${prop.stat}\` set`,
silenced: prop.silent,
}, damageTargetIds);
return;
}
}
await applyTask(action, {
targetIds: damageTargetIds,
subtaskFn: 'damageProp',
params: {
title: getPropertyTitle(prop),
operation: prop.operation,
value,
targetProp: stat ?? { name: prop.stat ?? '' },
},
}, userInput);
return applyDefaultAfterPropTasks(action, prop, damageTargetIds, userInput);
}

View File

@@ -0,0 +1,376 @@
import { assert } from 'chai';
import {
allMutations,
createTestCreature,
getRandomIds,
removeAllCreaturesAndProps,
runActionById
} from '/imports/api/engine/action/functions/actionEngineTest.testFn';
const [
creatureId, targetCreatureId, ifTrueBranchId, ifFalseBranchId, indexBranchId, attackHitId, attackMissId, saveSucceedId, saveFailId, randomBranchId, targetCreature2Id, eachTargetBranchId, choiceBranchId,
] = getRandomIds(100);
const actionTestCreature = {
_id: creatureId,
props: [
// If branch
{
_id: ifTrueBranchId,
type: 'branch',
branchType: 'if',
condition: { calculation: 'true' },
children: [
{
type: 'note',
summary: { text: 'this should run' },
},
],
},
{
_id: ifFalseBranchId,
type: 'branch',
branchType: 'if',
condition: { calculation: 'false' },
children: [
{
type: 'note',
summary: { text: 'this should not run' },
},
],
},
// index branch
{
_id: indexBranchId,
type: 'branch',
branchType: 'index',
condition: { calculation: '1 + 1' },
children: [
{
type: 'note',
summary: { text: 'FAIL: index child 1 should not run' },
},
{
type: 'note',
summary: { text: 'Child 2 should run' },
},
{
type: 'note',
summary: { text: 'FAIL: index child 3 should not run' },
},
],
},
// Hit and miss branches
{
_id: attackHitId,
type: 'action',
attackRoll: { calculation: '1' },
children: [
{
type: 'branch',
branchType: 'hit',
children: [{
type: 'note',
summary: { text: 'attack hit branch' }
}],
},
{
type: 'branch',
branchType: 'miss',
children: [{
type: 'note',
summary: { text: 'attack miss branch' }
}],
},
]
},
{
_id: attackMissId,
type: 'action',
attackRoll: { calculation: '-1' },
children: [
{
type: 'branch',
branchType: 'hit',
children: [{
type: 'note',
summary: { text: 'attack hit branch' }
}],
},
{
type: 'branch',
branchType: 'miss',
children: [{
type: 'note',
summary: { text: 'attack miss branch' }
}],
},
]
},
// Save and fail save branch
{
_id: saveSucceedId,
type: 'savingThrow',
dc: { calculation: '10' },
target: 'target',
stat: 'strengthSave',
children: [
{
type: 'branch',
branchType: 'successfulSave',
children: [{
type: 'note',
summary: { text: 'made save branch' }
}],
},
{
type: 'branch',
branchType: 'failedSave',
children: [{
type: 'note',
summary: { text: 'failed save branch' }
}],
},
]
},
{
_id: saveFailId,
type: 'savingThrow',
dc: { calculation: '15' },
target: 'target',
stat: 'strengthSave',
children: [
{
type: 'branch',
branchType: 'successfulSave',
children: [{
type: 'note',
summary: { text: 'made save branch' }
}],
},
{
type: 'branch',
branchType: 'failedSave',
children: [{
type: 'note',
summary: { text: 'failed save branch' }
}],
},
]
},
// Random branch
{
_id: randomBranchId,
type: 'branch',
branchType: 'random',
children: [
{
type: 'note',
summary: { text: 'FAIL: random child 1 should not run' },
},
{
type: 'note',
summary: { text: 'Random child 2 should run' },
},
{
type: 'note',
summary: { text: 'FAIL: random child 3 should not run' },
},
],
},
// Each target branch
{
_id: eachTargetBranchId,
type: 'branch',
branchType: 'eachTarget',
children: [
{
type: 'note',
summary: { text: 'some note' }
}
]
},
// Choice branch
{
_id: choiceBranchId,
type: 'branch',
branchType: 'choice',
children: [
{
type: 'note',
summary: { text: 'Choice child 1 should run' },
},
{
type: 'note',
summary: { text: 'Fail: choice child 2 should not run' },
},
{
type: 'note',
summary: { text: 'Fail: choice child 3 should not run' },
},
],
},
],
}
const actionTargetCreature = {
_id: targetCreatureId,
props: [
{
type: 'attribute',
attributeType: 'stat',
variableName: 'armor',
baseValue: { calculation: '10' },
},
{
type: 'skill',
skillType: 'save',
variableName: 'strengthSave',
baseValue: { calculation: '3' },
},
]
}
const actionTargetCreature2 = {
_id: targetCreature2Id,
props: [
{
type: 'attribute',
attributeType: 'stat',
variableName: 'armor',
baseValue: { calculation: '15' },
},
]
}
describe('Apply Branch Properties', function () {
// Increase timeout
this.timeout(8000);
before(async function () {
await removeAllCreaturesAndProps();
await createTestCreature(actionTestCreature);
await createTestCreature(actionTargetCreature);
await createTestCreature(actionTargetCreature2);
});
// If branch
it('Runs an if branch with a true condition', async function () {
const action = await runActionById(ifTrueBranchId);
assert.deepEqual(allMutations(action), [{
contents: [{ value: 'this should run' }],
targetIds: [],
}]);
});
it('runs an if branch with a false condition', async function () {
const action = await runActionById(ifFalseBranchId);
assert.deepEqual(allMutations(action), []);
});
it('runs an if branch and chooses the correct child', async function () {
const action = await runActionById(indexBranchId);
assert.deepEqual(allMutations(action), [{
contents: [{ value: 'Child 2 should run' }],
targetIds: [],
}]);
});
// Hit and miss branch
it('Runs only hit branches on an attack that hits', async function () {
const action = await runActionById(attackHitId, [targetCreatureId]);
assert.deepEqual(allMutations(action), [{
contents: [{ name: 'Action' }],
targetIds: [targetCreatureId],
}, {
contents: [{ inline: true, name: 'Hit!', value: '1d20 [10] + 1\n**11**' }],
targetIds: [targetCreatureId],
}, {
contents: [{ value: 'attack hit branch' }],
targetIds: [targetCreatureId],
}]);
});
it('Runs only miss branches on an attack that misses', async function () {
const action = await runActionById(attackMissId, [targetCreatureId]);
assert.deepEqual(allMutations(action), [{
contents: [{ name: 'Action' }],
targetIds: [targetCreatureId],
}, {
contents: [{ inline: true, name: 'Miss!', value: '1d20 [10] 1\n**9**' }],
targetIds: [targetCreatureId],
}, {
contents: [{ value: 'attack miss branch' }],
targetIds: [targetCreatureId],
}]);
});
// Save succeed and fail branches
it('Runs only miss branches on an attack that misses', async function () {
const action = await runActionById(saveSucceedId, [targetCreatureId]);
assert.deepEqual(allMutations(action), [{
contents: [{
name: 'Saving throw',
value: 'DC **10**',
inline: true
}, {
name: 'Successful save',
value: '1d20 [ 10 ] + 3\n**13**',
inline: true
}],
targetIds: [targetCreatureId],
}, {
contents: [{ value: 'made save branch' }],
targetIds: [targetCreatureId],
}]);
});
it('Runs only miss branches on an attack that misses', async function () {
const action = await runActionById(saveFailId, [targetCreatureId]);
assert.deepEqual(allMutations(action), [{
contents: [{
name: 'Saving throw',
value: 'DC **15**',
inline: true
}, {
name: 'Failed save',
value: '1d20 [ 10 ] + 3\n**13**',
inline: true
}],
targetIds: [targetCreatureId],
}, {
contents: [{ value: 'failed save branch' }],
targetIds: [targetCreatureId],
}]);
});
// Random branches, RNG is fixed at average for testing, so child 2 should run
it('runs a random branch and chooses the correct child', async function () {
const action = await runActionById(randomBranchId);
assert.deepEqual(allMutations(action), [{
contents: [{ value: 'Random child 2 should run' }],
targetIds: [],
}]);
});
// Branches can split actions across targets
it('Can split actions to targets using a branch', async function () {
const action = await runActionById(eachTargetBranchId, [targetCreatureId, targetCreature2Id]);
assert.deepEqual(allMutations(action), [{
contents: [{ value: 'some note' }],
targetIds: [targetCreatureId],
}, {
contents: [{ value: 'some note' }],
targetIds: [targetCreature2Id],
}]);
});
// Choice branches, choices are fixed to first option for testing
it('runs a choice branch and chooses the correct child', async function () {
const action = await runActionById(choiceBranchId);
assert.deepEqual(allMutations(action), [{
contents: [{ value: 'Choice child 1 should run' }],
targetIds: [],
}]);
});
});

View File

@@ -0,0 +1,153 @@
import { filter } from 'lodash';
import { EngineAction } from '/imports/api/engine/action/EngineActions';
import InputProvider from '/imports/api/engine/action/functions/userInput/InputProvider';
import { applyAfterPropTasksForSingleChild, applyAfterPropTasksForSomeChildren, applyAfterTasksSkipChildren, applyDefaultAfterPropTasks, applyTaskToEachTarget } from '/imports/api/engine/action/functions/applyTaskGroups';
import { getEffectiveActionScope } from '/imports/api/engine/action/functions/getEffectiveActionScope';
import recalculateCalculation from '/imports/api/engine/action/functions/recalculateCalculation';
import { PropTask } from '/imports/api/engine/action/tasks/Task';
import TaskResult from '/imports/api/engine/action/tasks/TaskResult';
import { getPropertyChildren } from '/imports/api/engine/loadCreatures';
export default async function applyBranchProperty(
task: PropTask, action: EngineAction, result: TaskResult, userInput: InputProvider
): Promise<void> {
const prop = task.prop;
if (prop.type !== 'branch') {
throw new Meteor.Error('wrong-property', `Expected a branch, got ${prop.type} instead`);
}
const targets = task.targetIds;
switch (prop.branchType) {
case 'if': {
if (!prop.condition) {
result.appendLog({
name: 'Branch Error',
value: 'If branch does not have a condition set',
silenced: prop.silent,
}, targets);
return applyAfterTasksSkipChildren(action, prop, targets, userInput);
}
await recalculateCalculation(prop.condition, action, 'reduce', userInput);
if (prop.condition?.value) {
return applyDefaultAfterPropTasks(action, prop, targets, userInput);
} else {
return applyAfterTasksSkipChildren(action, prop, targets, userInput);
}
}
case 'index': {
const children = await getPropertyChildren(action.creatureId, prop);
if (!children.length) {
return applyAfterTasksSkipChildren(action, prop, targets, userInput);
}
if (!prop.condition) {
result.appendLog({
name: 'Branch Error',
value: 'Index branch does not have a condition set',
silenced: prop.silent,
}, targets);
return applyAfterTasksSkipChildren(action, prop, targets, userInput);
}
await recalculateCalculation(prop.condition, action, 'reduce', userInput);
let index = Number(prop.condition.value);
if (!isFinite(index)) {
result.appendLog({
name: 'Branch Error',
value: `Index did not resolve into a valid number, got \`${prop.condition?.value}\` instead`,
silenced: prop.silent,
}, targets);
return applyAfterTasksSkipChildren(action, prop, targets, userInput);
}
index = Math.floor(index);
if (index < 1) index = 1;
if (index > children.length) index = children.length;
const child = children[index - 1];
return applyAfterPropTasksForSingleChild(action, prop, child, targets, userInput);
}
case 'hit': {
const scope = await getEffectiveActionScope(action);
if (scope['~attackHit']?.value) {
if (!targets.length && !prop.silent) {
result.appendLog({
value: '**On hit**',
silenced: prop.silent,
}, targets);
}
return applyDefaultAfterPropTasks(action, prop, targets, userInput);
} else {
return applyAfterTasksSkipChildren(action, prop, targets, userInput);
}
}
case 'miss': {
const scope = await getEffectiveActionScope(action);
if (scope['~attackMiss']?.value) {
if (!targets.length && !prop.silent) {
result.appendLog({
value: '**On miss**',
silenced: prop.silent,
}, targets);
}
return applyDefaultAfterPropTasks(action, prop, targets, userInput);
} else {
return applyAfterTasksSkipChildren(action, prop, targets, userInput);
}
}
case 'failedSave': {
const scope = await getEffectiveActionScope(action);
if (scope['~saveFailed']?.value) {
if (!targets.length && !prop.silent) {
result.appendLog({
value: '**On failed save**',
silenced: prop.silent,
}, targets);
}
return applyDefaultAfterPropTasks(action, prop, targets, userInput);
} else {
return applyAfterTasksSkipChildren(action, prop, targets, userInput);
}
}
case 'successfulSave': {
const scope = await getEffectiveActionScope(action);
if (scope['~saveSucceeded']?.value) {
if (!targets.length && !prop.silent) {
result.appendLog({
value: '**On save**',
silenced: prop.silent,
}, targets);
}
return applyDefaultAfterPropTasks(action, prop, targets, userInput);
} else {
return applyAfterTasksSkipChildren(action, prop, targets, userInput);
}
}
case 'random': {
const children = await getPropertyChildren(action.creatureId, prop);
if (children.length) {
const index = (await userInput.rollDice([{ number: 1, diceSize: children.length }]))[0][0];
const child = children[index - 1];
return applyAfterPropTasksForSingleChild(action, prop, child, targets, userInput);
} else {
return applyAfterTasksSkipChildren(action, prop, targets, userInput);
}
}
case 'eachTarget':
if (targets.length > 1) {
return applyTaskToEachTarget(action, task, targets, userInput);
}
return applyDefaultAfterPropTasks(action, prop, targets, userInput);
case 'choice': {
const children = await getPropertyChildren(action.creatureId, prop);
let choices: string[];
let chosenChildren: typeof children = [];
if (children.length) {
choices = await userInput.choose(children);
chosenChildren = filter(children, child => choices.includes(child._id));
}
if (!children.length || !chosenChildren.length) {
return applyAfterTasksSkipChildren(action, prop, targets, userInput);
}
return applyAfterPropTasksForSomeChildren(action, prop, chosenChildren, targets, userInput);
}
}
}

View File

@@ -0,0 +1,115 @@
import { assert } from 'chai';
import {
allMutations,
createTestCreature,
getRandomIds,
removeAllCreaturesAndProps,
runActionById,
TestCreature
} from '/imports/api/engine/action/functions/actionEngineTest.testFn';
const [
creatureId, targetCreatureId, buffId
] = getRandomIds(100);
const actionTestCreature: TestCreature = {
_id: creatureId,
props: [
{
_id: buffId,
type: 'buff',
description: { text: 'This buff reduces AC of target by difference between the strength of caster {strength} and the target {~target.strength}' },
children: [
{
type: 'effect',
stats: ['armor'],
operation: 'add',
amount: { calculation: '~target.strength - strength' },
},
],
},
{
type: 'attribute',
attributeType: 'stat',
variableName: 'strength',
baseValue: { calculation: '18' },
},
],
};
const actionTargetCreature: TestCreature = {
_id: targetCreatureId,
props: [
{
type: 'attribute',
attributeType: 'stat',
variableName: 'armor',
baseValue: { calculation: '10' },
},
{
type: 'attribute',
attributeType: 'ability',
variableName: 'strength',
baseValue: { calculation: '12' },
},
],
};
describe('Apply Buff Properties', function () {
// Increase timeout
this.timeout(8000);
before(async function () {
await removeAllCreaturesAndProps();
await createTestCreature(actionTestCreature);
await createTestCreature(actionTargetCreature);
});
it('Applies a buff and freezes some variables', async function () {
const action = await runActionById(buffId, [targetCreatureId]);
const mutations = allMutations(action);
// Get random Ids of inserted props
const insertedBuffId = mutations?.[1]?.inserts?.[0]?._id;
const insertedEffectId = mutations?.[1]?.inserts?.[1]?._id;
assert.deepEqual(mutations, [{
contents: [{
name: 'Buff',
// TODO Make target strength available in action scope to fix: 'target 0' -> 'target 12'
value: 'This buff reduces AC of target by difference between the strength of caster 18 and the target 0',
}],
targetIds: [targetCreatureId],
}, {
contents: [],
inserts: [{
_id: insertedBuffId,
type: 'buff',
description: {
text: 'This buff reduces AC of target by difference between the strength of caster {18} and the target {strength}'
},
left: 1,
right: 4,
root: {
collection: 'creatures',
id: targetCreatureId,
},
tags: [],
target: 'target',
}, {
_id: insertedEffectId,
type: 'effect',
stats: ['armor'],
operation: 'add',
amount: { calculation: 'strength - 18' },
left: 2,
right: 3,
parentId: insertedBuffId,
root: {
collection: 'creatures',
id: targetCreatureId,
},
tags: [],
}],
targetIds: [targetCreatureId],
}]);
});
});

View File

@@ -0,0 +1,178 @@
import { get } from 'lodash';
import { EngineAction } from '/imports/api/engine/action/EngineActions';
import { PropTask } from '/imports/api/engine/action/tasks/Task';
import { getPropertyDescendants } from '/imports/api/engine/loadCreatures';
import resolve from '/imports/parser/resolve';
import map from '/imports/parser/map';
import toString from '/imports/parser/toString';
import computedSchemas from '/imports/api/properties/computedOnlyPropertySchemasIndex.js';
import applyFnToKey, { applyFnToKeyAsync } from '/imports/api/engine/computation/utility/applyFnToKey';
import accessor from '/imports/parser/parseTree/accessor';
import TaskResult, { Mutation } from '/imports/api/engine/action/tasks/TaskResult';
import { getEffectiveActionScope } from '/imports/api/engine/action/functions/getEffectiveActionScope';
import cyrb53 from '/imports/api/engine/computation/utility/cyrb53';
import { renewDocIds } from '/imports/api/parenting/parentingFunctions';
import { cleanProps } from '/imports/api/creature/creatureProperties/methods/copyPropertyToLibrary';
import recalculateInlineCalculations from '/imports/api/engine/action/functions/recalculateInlineCalculations';
import getPropertyTitle from '/imports/api/utility/getPropertyTitle';
import INLINE_CALCULATION_REGEX from '/imports/constants/INLINE_CALCULATION_REGEX';
import { applyAfterTasksSkipChildren } from '/imports/api/engine/action/functions/applyTaskGroups';
import InputProvider from '/imports/api/engine/action/functions/userInput/InputProvider';
export default async function applyBuffProperty(
task: PropTask, action: EngineAction, result: TaskResult, userInput: InputProvider
) {
const prop = EJSON.clone(task.prop);
if (prop.type !== 'buff') {
throw new Meteor.Error('wrong-property', `Expected a buff, got ${prop.type} instead`);
}
const targetIds = prop.target === 'self' ? [action.creatureId] : task.targetIds;
// Log the buff and return if there are no targets
if (!targetIds.length) {
await logBuff(prop, targetIds, action, userInput, result);
await applyAfterTasksSkipChildren(action, prop, targetIds, userInput);
return;
}
// Get the buff and its descendants
const propList = [
EJSON.clone(prop),
...getPropertyDescendants(action.creatureId, prop._id),
];
// Crystallize the variables
if (!prop.skipCrystalization) {
await crystallizeVariables(action, propList, task, result);
}
for (const target of targetIds) {
// Create a per-target mutation
const mutation: Mutation = { targetIds: [target], contents: [] };
// Create a per-target copy of the propList
let targetPropList = EJSON.clone(propList);
// Give the properties new IDs as descendants of the target
renewDocIds({
docArray: targetPropList,
idMap: {
...prop.parentId && { [prop.parentId]: null },
[prop.root.id]: target,
},
collectionMap: { [prop.root.collection]: 'creatures' }
});
//Log the buff
await logBuff(prop, targetIds, action, userInput, result);
// remove all the computed fields
targetPropList = cleanProps(targetPropList);
// Insert the props in the mutation
mutation.inserts = targetPropList;
// Add the mutation to the results
result.mutations.push(mutation);
}
await applyAfterTasksSkipChildren(action, prop, targetIds, userInput);
}
async function logBuff(prop, targetIds, action, userInput, result) {
//Log the buff
let logValue = prop.description?.value
if (prop.description?.text) {
await recalculateInlineCalculations(prop.description, action, 'reduce', userInput);
logValue = prop.description?.value;
}
result.appendLog({
name: getPropertyTitle(prop),
...logValue && { value: logValue },
silenced: prop.silent,
}, targetIds);
}
/**
* Replaces all variables with their resolved values
* except variables of the form `~target.thing.total` become `thing.total`
*/
async function crystallizeVariables(
action: EngineAction, propList: any[], task: PropTask, result: TaskResult
) {
const scope = await getEffectiveActionScope(action);
for (const prop of propList) {
// Iterate through all the calculations and crystallize them
for (const calcKey of computedSchemas[prop.type].computedFields()) {
await applyFnToKeyAsync(prop, calcKey, async (prop, key) => {
const calcObj = get(prop, key);
if (!calcObj?.parseNode) return;
calcObj.parseNode = await map(calcObj.parseNode, async node => {
// Skip nodes that aren't symbols or accessors
if (
node.parseType !== 'accessor'
) return node;
// Handle variables
if (node.parseType === 'accessor' && node.name === '~target') {
// strip ~target
if (node.path?.length > 0) {
const name = node.path.shift();
return accessor.create({
name,
path: node.path?.length ? node.path : undefined,
});
} else {
// Can't strip if there isn't anything in the path after ~target
result.appendLog({
name: 'Error',
value: 'Variable `~target` should not be used without a property: ~target.property',
silenced: prop.silent,
}, task.targetIds);
}
return node;
} else {
// Resolve all other variables
const { result: nodeResult, context } = await resolve('reduce', node, scope);
result.appendParserContextErrors(context, task.targetIds);
return nodeResult;
}
});
calcObj.calculation = toString(calcObj.parseNode);
calcObj.hash = cyrb53(calcObj.calculation);
});
}
// For each key in the schema
for (const calcKey of computedSchemas[prop.type].inlineCalculationFields()) {
// 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

@@ -0,0 +1,98 @@
import { assert } from 'chai';
import {
allMutations,
createTestCreature,
getRandomIds,
removeAllCreaturesAndProps,
runActionById,
TestCreature
} from '/imports/api/engine/action/functions/actionEngineTest.testFn';
const [
creatureId, otherCreatureId, buffId, removeParentBuffId, removeTargetBuffsId,
] = getRandomIds(100);
const actionTestCreature: TestCreature = {
_id: creatureId,
props: [
{
_id: buffId,
type: 'buff',
description: { text: 'This buff reduces AC of target by difference between the strength of caster {strength} and the target {~target.strength}' },
tags: ['some buff'],
children: [
{
type: 'effect',
stats: ['armor'],
operation: 'add',
amount: { calculation: '~target.strength - strength' },
},
{
_id: removeParentBuffId,
type: 'buffRemover',
targetParentBuff: true,
target: 'self',
},
],
},
{
type: 'attribute',
attributeType: 'stat',
variableName: 'strength',
baseValue: { calculation: '18' },
},
],
};
const actionOtherCreature: TestCreature = {
_id: otherCreatureId,
props: [
{
_id: removeTargetBuffsId,
type: 'buffRemover',
target: 'target',
targetTags: ['some buff']
},
],
};
describe('Apply Buff Remover Properties', function () {
// Increase timeout
this.timeout(8000);
beforeEach(async function () {
await removeAllCreaturesAndProps();
await createTestCreature(actionTestCreature);
await createTestCreature(actionOtherCreature);
});
it('removes a parent buff', async function () {
const action = await runActionById(removeParentBuffId);
const mutations = allMutations(action);
assert.deepEqual(mutations, [{
contents: [{
name: 'Removed',
value: 'Buff',
}],
removals: [{
propId: buffId,
}],
targetIds: []
}]);
});
it('removes a tag targeted buff', async function () {
const action = await runActionById(removeTargetBuffsId, [creatureId]);
const mutations = allMutations(action);
assert.deepEqual(mutations, [{
contents: [{
name: 'Removed',
value: 'Buff',
}],
removals: [{
propId: buffId,
}],
targetIds: [creatureId]
}]);
});
});

View File

@@ -0,0 +1,123 @@
import { PropTask } from '/imports/api/engine/action/tasks/Task';
import TaskResult from '/imports/api/engine/action/tasks/TaskResult';
import getPropertyTitle from '/imports/api/utility/getPropertyTitle';
import { findLast, filter, difference, intersection } from 'lodash';
import { getPropertiesOfType, getPropertyAncestors } from '/imports/api/engine/loadCreatures';
import getEffectivePropTags from '/imports/api/engine/computation/utility/getEffectivePropTags';
import { applyDefaultAfterPropTasks, applyTaskToEachTarget } from '/imports/api/engine/action/functions/applyTaskGroups';
import { EngineAction } from '/imports/api/engine/action/EngineActions';
import InputProvider from '/imports/api/engine/action/functions/userInput/InputProvider';
import { CreaturePropertyTypes } from '/imports/api/creature/creatureProperties/CreatureProperties';
export default async function applyBuffRemoverProperty(
task: PropTask, action: EngineAction, result: TaskResult, userInput: InputProvider
) {
const prop = task.prop as CreaturePropertyTypes['buffRemover'];
const targetIds = prop.target === 'self' ? [action.creatureId] : task.targetIds;
if (prop.name) {
// Log Name
result.appendLog({
name: getPropertyTitle(prop),
silenced: prop.silent,
}, task.targetIds)
}
if (targetIds.length > 1) {
return applyTaskToEachTarget(action, task, targetIds, userInput);
}
if (!targetIds.length) {
return applyDefaultAfterPropTasks(action, prop, task.targetIds, userInput);
}
if (targetIds.length !== 1) {
throw 'At this step, only a single target is supported'
}
const targetId = targetIds[0];
// Remove buffs
if (prop.targetParentBuff) {
// Remove nearest ancestor buff
const ancestors = getPropertyAncestors(action.creatureId, prop._id);
const nearestBuff = findLast(ancestors, ancestor => ancestor.type === 'buff');
if (!nearestBuff) {
result.appendLog({
name: 'Error',
value: 'Buff remover does not have a parent buff to remove',
silenced: prop.silent,
}, [targetId]);
return;
}
removeBuff(nearestBuff, prop, result);
} else {
// Get all the buffs targeted by tags
const allBuffs = getPropertiesOfType(targetId, 'buff');
const targetedBuffs = filter(allBuffs, (buff): boolean => {
if (buff.inactive) return false;
if (buffRemoverMatchTags(prop, buff)) return true;
return false;
});
// Remove the buffs
if (prop.removeAll) {
// Remove all matching buffs
targetedBuffs.forEach(buff => {
removeBuff(buff, prop, result);
});
} else {
// Sort in reverse order
targetedBuffs.sort((a, b) => b.left - a.left);
// Remove the one with the highest order
const buff = targetedBuffs[0];
if (buff) {
removeBuff(buff, prop, result);
}
}
}
return applyDefaultAfterPropTasks(action, prop, task.targetIds, userInput);
}
function removeBuff(buff: any, prop, result: TaskResult) {
result.mutations.push({
targetIds: result.targetIds,
removals: [{ propId: buff._id }],
contents: [{
name: 'Removed',
value: `${buff.name || 'Buff'}`,
...prop.silent && { silenced: true },
}],
});
}
function buffRemoverMatchTags(buffRemover, prop) {
let matched = false;
const propTags = getEffectivePropTags(prop);
// Check the target tags
if (
!buffRemover.targetTags?.length ||
difference(buffRemover.targetTags, propTags).length === 0
) {
matched = true;
}
// Check the extra tags
buffRemover.extraTags?.forEach(extra => {
if (extra.operation === 'OR') {
if (matched) return;
if (
!extra.tags.length ||
difference(extra.tags, propTags).length === 0
) {
matched = true;
}
} else if (extra.operation === 'NOT') {
if (
extra.tags.length &&
intersection(extra.tags, propTags)
) {
return false;
}
}
});
return matched;
}

View File

@@ -0,0 +1,36 @@
import { EngineAction } from '/imports/api/engine/action/EngineActions';
import { PropTask } from '/imports/api/engine/action/tasks/Task';
import recalculateInlineCalculations from '/imports/api/engine/action/functions/recalculateInlineCalculations';
import getPropertyTitle from '/imports/api/utility/getPropertyTitle';
export default async function applyCreatureTemplateProperty(
task: PropTask, action: EngineAction, result, userInput
): Promise<void> {
const prop = task.prop;
if (prop.type !== 'creature') {
throw new Meteor.Error('wrong-property', `Expected a creature, got ${prop.type} instead`);
}
// Log the Creature that is about to be summoned
let logValue = prop.description?.value
if (prop.description?.text) {
await recalculateInlineCalculations(prop.description, action, 'reduce', userInput);
logValue = prop.description?.value;
}
// There are no targets for creature templates
// Creatures are always summoned as children of the action's creature
result.appendLog({
name: getPropertyTitle(prop),
value: logValue,
silenced: prop.silent,
}, []);
result.appendLog({
name: 'Warning',
value: 'Creature summoning is not yet implemented...',
silenced: prop.silent,
}, []);
return;
}

View File

@@ -0,0 +1,292 @@
import { assert } from 'chai';
import {
allMutations,
createTestCreature,
getRandomIds,
removeAllCreaturesAndProps,
runActionById,
TestCreature
} from '/imports/api/engine/action/functions/actionEngineTest.testFn';
import { critInputProvider } from '../functions/userInput/inputProviderForTests.testFn';
const [
creatureId, targetCreatureId, targetCreature2Id, damageTargetId, damageSelfId, targetCreatureHitPointsId, targetCreature2HitPointsId, selfHitPointsId, damageWithEffectsId, effectId, effect2Id,
] = getRandomIds(20);
const actionTestCreature: TestCreature = {
_id: creatureId,
props: [
{
_id: damageTargetId,
type: 'damage',
target: 'target',
amount: { calculation: '2d6 + 7' }
},
{
_id: damageSelfId,
type: 'damage',
target: 'self',
amount: { calculation: '1d12 + 7' }
},
{
_id: selfHitPointsId,
type: 'attribute',
name: 'Hit Points',
attributeType: 'healthBar',
variableName: 'hitPoints',
baseValue: { calculation: '20' },
},
{
_id: damageWithEffectsId,
type: 'damage',
target: 'target',
amount: { calculation: '1d13 + 3' },
tags: ['tag']
},
{
_id: effectId,
type: 'effect',
operation: 'add',
amount: { calculation: '1' },
targetByTags: true,
targetTags: ['tag'],
},
{
_id: effect2Id,
type: 'effect',
operation: 'mul',
amount: { calculation: '2' },
targetByTags: true,
targetTags: ['tag'],
},
],
}
const actionTargetCreature: TestCreature = {
_id: targetCreatureId,
props: [
{
_id: targetCreatureHitPointsId,
type: 'attribute',
name: 'Hit Points',
attributeType: 'healthBar',
variableName: 'hitPoints',
baseValue: { calculation: '33' },
}
]
}
const actionTargetCreature2: TestCreature = {
_id: targetCreature2Id,
props: [
{
_id: targetCreature2HitPointsId,
type: 'attribute',
name: 'Hit Points',
attributeType: 'healthBar',
variableName: 'hitPoints',
baseValue: { calculation: '47' },
}
]
}
describe('Apply Damage Properties', function () {
// Increase timeout
this.timeout(8000);
before(async function () {
await removeAllCreaturesAndProps();
await createTestCreature(actionTestCreature);
await createTestCreature(actionTargetCreature);
await createTestCreature(actionTargetCreature2);
});
it('Damages self', async function () {
const action = await runActionById(damageSelfId);
assert.exists(action);
assert.deepEqual(allMutations(action), [{
contents: [
{
inline: true,
name: 'Damage',
value: '1d12 [6] + 7',
}
],
targetIds: [creatureId],
}, {
contents: [{
inline: true,
name: 'Health bar damaged',
value: '13 Hit Points',
}],
updates: [
{
propId: selfHitPointsId,
type: 'attribute',
inc: { damage: 13, value: -13 },
},
],
targetIds: [creatureId],
}]);
});
it('Damages a single target', async function () {
const action = await runActionById(damageTargetId, [targetCreatureId]);
assert.exists(action);
assert.deepEqual(allMutations(action), [{
contents: [
{
inline: true,
name: 'Damage',
value: '2d6 [3, 4] + 7',
}
],
targetIds: [targetCreatureId],
}, {
contents: [
{
inline: true,
name: 'Health bar damaged',
value: '14 Hit Points',
}
],
targetIds: [targetCreatureId],
updates: [
{
propId: targetCreatureHitPointsId,
type: 'attribute',
inc: { damage: 14, value: -14 },
},
],
}]);
});
it('Damages multiple targets', async function () {
const action = await runActionById(damageTargetId, [
targetCreatureId, targetCreature2Id
]);
assert.exists(action);
assert.deepEqual(allMutations(action), [{
contents: [
{
inline: true,
name: 'Damage',
value: '2d6 [3, 4] + 7',
}
],
targetIds: [
targetCreatureId,
targetCreature2Id,
],
}, {
contents: [
{
inline: true,
name: 'Health bar damaged',
value: '14 Hit Points',
}
],
targetIds: [targetCreatureId],
updates: [
{
propId: targetCreatureHitPointsId,
type: 'attribute',
inc: { damage: 14, value: -14 },
},
],
}, {
contents: [
{
inline: true,
name: 'Health bar damaged',
value: '14 Hit Points',
}
],
targetIds: [targetCreature2Id],
updates: [
{
propId: targetCreature2HitPointsId,
type: 'attribute',
inc: { damage: 14, value: -14 },
},
],
}]);
});
it('Applies effects when doing damage', async function () {
const action = await runActionById(damageWithEffectsId, [targetCreatureId]);
assert.exists(action);
assert.deepEqual(allMutations(action), [{
contents: [
{
inline: true,
name: 'Damage',
value: '(1d13 [7] + 4) * 2',
}
],
targetIds: [targetCreatureId],
}, {
contents: [
{
inline: true,
name: 'Health bar damaged',
value: '22 Hit Points',
}
],
targetIds: [targetCreatureId],
updates: [
{
propId: targetCreatureHitPointsId,
type: 'attribute',
inc: { damage: 22, value: -22 },
},
],
}]);
});
it('Doubles damage on a critical hit', async function () {
const [
creatureId, damageId, actionId
] = getRandomIds(3);
const testCreature: TestCreature = {
_id: creatureId,
props: [
{
_id: actionId,
type: 'action',
attackRoll: { calculation: '10' },
children: [
{
_id: damageId,
type: 'damage',
target: 'target',
amount: { calculation: '2d6 + 7' }
},
]
},
],
};
await createTestCreature(testCreature);
const action = await runActionById(actionId, [], critInputProvider);
assert.exists(action);
assert.deepEqual(allMutations(action), [{
'contents': [{ 'name': 'Action' }],
'targetIds': []
}, {
'contents': [{
'inline': true,
'name': 'Critical Hit!',
'value': '1d20 [20] + 10\n**30**'
}],
'targetIds': [],
}, {
'contents': [{
'inline': true,
'name': 'Damage',
'value': '2d6 [3, 4, 5, 6] + 7\n**25** critical slashing damage',
}],
'targetIds': [],
}]);
});
});

View File

@@ -0,0 +1,332 @@
import { some, includes, difference, intersection } from 'lodash';
import { getConstantValueFromScope } from '/imports/api/creature/creatures/CreatureVariables';
import { EngineAction } from '/imports/api/engine/action/EngineActions';
import { applyDefaultAfterPropTasks } from '/imports/api/engine/action/functions/applyTaskGroups';
import { getEffectiveActionScope } from '/imports/api/engine/action/functions/getEffectiveActionScope';
import recalculateCalculation from '/imports/api/engine/action/functions/recalculateCalculation';
import { PropTask } from '/imports/api/engine/action/tasks/Task';
import TaskResult from '/imports/api/engine/action/tasks/TaskResult';
import { isFiniteNode } from '/imports/parser/parseTree/constant';
import resolve from '/imports/parser/resolve';
import toString from '/imports/parser/toString';
import { getPropertiesOfType } from '/imports/api/engine/loadCreatures';
import applyTask from '/imports/api/engine/action/tasks/applyTask';
import InputProvider from '/imports/api/engine/action/functions/userInput/InputProvider';
import getEffectivePropTags from '/imports/api/engine/computation/utility/getEffectivePropTags';
import Context from '/imports/parser/types/Context';
import applySavingThrowProperty from '/imports/api/engine/action/applyProperties/applySavingThrowProperty';
import assert from 'node:assert';
export default async function applyDamageProperty(
task: PropTask, action: EngineAction, result: TaskResult, inputProvider: InputProvider
) {
const prop = task.prop;
if (prop.type !== 'damage') {
throw new Meteor.Error('wrong-property', `Expected damage, got ${prop.type} instead`);
}
const scope = await getEffectiveActionScope(action);
// Choose target
const damageTargets = prop.target === 'self' ? [action.creatureId] : task.targetIds;
// Skip if there is no parse node to work with
if (!prop.amount?.valueNode) {
return applyDefaultAfterPropTasks(action, prop, damageTargets, inputProvider);
}
// Determine if the hit is critical
const criticalHit = await getConstantValueFromScope('~criticalHit', scope)
&& prop.damageType !== 'healing'; // Can't critically heal
// Double the damage rolls if the hit is critical
const context = new Context({
options: { doubleRolls: criticalHit },
});
// Gather all the lines we need to log into an array
const logValue: string[] = [];
const logName = prop.damageType === 'healing' ? 'Healing' : 'Damage';
// roll the dice only and store that string
await recalculateCalculation(prop.amount, action, 'compile', inputProvider);
const { result: rolled } = await resolve('roll', prop.amount.valueNode, scope, context, inputProvider);
if (rolled.parseType !== 'constant') {
logValue.push(toString(rolled));
}
result.appendParserContextErrors(context, damageTargets);
// Reset the errors so we don't log the same errors twice
context.errors = [];
// Resolve the roll to a final value
const { result: reduced } = await resolve('reduce', rolled, scope, context, inputProvider);
result.appendParserContextErrors(context, damageTargets);
// Store the result
let damage: number | undefined = undefined;
if (reduced.parseType === 'constant') {
prop.amount.value = reduced.value;
if (typeof reduced.value === 'number') {
damage = reduced.value;
}
} else if (reduced.parseType === 'error') {
prop.amount.value = undefined;
} else {
prop.amount.value = toString(reduced);
}
// If we didn't end up with damage of finite amount, give up
if (
typeof damage !== 'number'
|| !isFinite(damage)
) {
return applyDefaultAfterPropTasks(action, prop, damageTargets, inputProvider);
}
// Round the damage to a whole number
damage = Math.floor(damage);
scope['~damage'] = { value: damage };
// Convert extra damage into the stored type
const lastDamageType = await getConstantValueFromScope('~lastDamageType', scope);
if (prop.damageType === 'extra' && typeof lastDamageType === 'string') {
prop.damageType = lastDamageType;
}
// Store current damage type
if (prop.damageType !== 'healing') {
scope['~lastDamageType'] = { value: prop.damageType };
}
// Memoise the damage suffix for the log
const suffix = (criticalHit ? 'critical ' : '') +
prop.damageType +
(prop.damageType !== 'healing' ? ' damage' : '');
// If there is a save, calculate the save damage
let damageOnSave, saveProp, saveRoll;
if (prop.save) {
if (prop.save.damageFunction?.calculation) {
await recalculateCalculation(prop.save.damageFunction, action, 'compile', inputProvider);
context.errors = [];
assert(prop.save.damageFunction.valueNode, 'Expected value to be defined after recalculateCalculation');
const { result: saveDamageRolled } = await resolve(
'roll', prop.save.damageFunction.valueNode, scope, context, inputProvider
);
saveRoll = toString(saveDamageRolled);
const { result: saveDamageResult } = await resolve(
'reduce', saveDamageRolled, scope, context, inputProvider
);
result.appendParserContextErrors(context, damageTargets);
// If we didn't end up with a constant of finite amount, give up
if (
!isFiniteNode(saveDamageResult)
) {
return applyDefaultAfterPropTasks(action, prop, damageTargets, inputProvider);
}
// Round the damage to a whole number
damageOnSave = Math.floor(saveDamageResult.value);
} else {
damageOnSave = Math.floor(damage / 2);
}
saveProp = {
node: {
...prop.save,
name: prop.save.stat,
silent: prop.silent,
},
children: [],
}
}
if (damageTargets && damageTargets.length) {
// Iterate through all the targets
for (const target of damageTargets) {
let damageToApply = damage || 0;
// If there is a saving throw, apply that first
if (prop.save) {
await applySavingThrowProperty({
prop: saveProp,
targetIds: task.targetIds,
}, action, result, inputProvider);
if (await getConstantValueFromScope('~saveSucceeded', scope)) {
// Log the total damage
logValue.push(toString(reduced));
// Log the save damage
const damageText = damageFunctionText(prop.save);
if (damageText) {
logValue.push(damageText);
} else {
logValue.push(
'**Damage on successful save**',
prop.save.damageFunction?.calculation ?? '',
saveRoll
);
}
damageToApply = damageOnSave;
}
}
// Apply weaknesses/resistances/immunities
damageToApply = applyDamageMultipliers({
target,
damage: damageToApply,
damageProp: prop,
logValue
});
// Deal the damage to the target
await dealDamage(
action, prop, result, inputProvider, target, prop.damageType, damageToApply
);
}
} else {
// There are no targets, just log the result
logValue.push(`**${damage}** ${suffix}`);
if (prop.save) {
await applySavingThrowProperty(saveProp, action, result, inputProvider);
await applySavingThrowProperty({
prop: saveProp,
targetIds: task.targetIds,
}, action, result, inputProvider);
logValue.push(`**${damageOnSave}** ${suffix} on a successful save`);
}
}
if (logValue.length) result.appendLog({
name: logName,
value: logValue.join('\n'),
inline: true,
silenced: prop.silent,
}, damageTargets);
return applyDefaultAfterPropTasks(action, prop, damageTargets, inputProvider);
}
function damageFunctionText(save) {
if (!save) return;
if (!save.damageFunction) {
return '**Half damage on successful save**';
}
if (save.damageFunction.calculation == '0' || save.damageFunction.value === 0) {
return '**No damage on successful save**'
}
}
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`;
if (
multiplier.immunity &&
some(multiplier.immunities, multiplierAppliesTo(damageProp, 'immunity'))
) {
logValue.push(`Immune to ${damageTypeText}`);
return 0;
} else {
if (
multiplier.resistance &&
some(multiplier.resistances, multiplierAppliesTo(damageProp, 'resistance'))
) {
logValue.push(`Resistant to ${damageTypeText}`);
damage = Math.floor(damage / 2);
}
if (
multiplier.vulnerability &&
some(multiplier.vulnerabilities, multiplierAppliesTo(damageProp, 'vulnerability'))
) {
logValue.push(`Vulnerable to ${damageTypeText}`);
damage = Math.floor(damage * 2);
}
}
return damage;
}
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, effectiveTags
).length === 0;
const hasNoExcludedTags = intersection(
multiplier.excludeTags, effectiveTags
).length === 0;
return hasRequiredTags && hasNoExcludedTags;
}
}
async function dealDamage(
action: EngineAction, prop: any, result: TaskResult, userInput: InputProvider,
targetId: string, damageType: string, amount: number
) {
// Get all the health bars and do damage to them
let healthBars = getPropertiesOfType(targetId, 'attribute');
// Keep only the healthbars that can take damage/healing
healthBars = healthBars.filter((bar) => {
if (bar.attributeType !== 'healthBar' || bar.inactive || bar.removed || bar.overridden) {
return false;
}
if (damageType === 'healing' && bar.healthBarNoHealing) {
return false;
}
if (damageType !== 'healing' && amount >= 0 && bar.healthBarNoDamage) {
return false;
}
return true;
});
// Sort healthbars by damage/healing order or tree order as a fallback
healthBars.sort((a, b) => {
let diff;
if (amount >= 0) {
diff = (a.healthBarDamageOrder ?? 0) - (b.healthBarDamageOrder ?? 0);
} else {
diff = (a.healthBarHealingOrder ?? 0) - (b.healthBarHealingOrder ?? 0);
}
if (Number.isFinite(diff)) {
return diff;
} else {
return a.left - b.left;
}
});
// Deal the damage to each healthbar in order until all damage is done
const totalDamage = amount;
let damageLeft = totalDamage;
if (damageType === 'healing') damageLeft = -totalDamage;
for (const healthBar of healthBars) {
if (damageLeft === 0) return;
// Do the damage
const damageAdded = await applyTask(action, {
targetIds: [targetId],
subtaskFn: 'damageProp',
params: {
operation: 'increment',
value: +damageLeft || 0,
targetProp: healthBar,
},
}, userInput);
damageLeft -= damageAdded;
// Prevent overflow
if (
damageType === 'healing' ?
healthBar.healthBarNoHealingOverflow :
healthBar.healthBarNoDamageOverflow
) {
damageLeft = 0;
}
}
return totalDamage;
}

View File

@@ -0,0 +1,50 @@
import { assert } from 'chai';
import {
allMutations,
createTestCreature,
getRandomIds,
removeAllCreaturesAndProps,
runActionById,
TestCreature
} from '/imports/api/engine/action/functions/actionEngineTest.testFn';
const [
creatureId, folderId
] = getRandomIds(100);
const actionTestCreature: TestCreature = {
_id: creatureId,
props: [
{
_id: folderId,
type: 'folder',
children: [{
type: 'note',
summary: { text: 'this should run' },
}],
},
],
}
describe('Apply folder properties', function () {
// Increase timeout
this.timeout(8000);
before(async function () {
await removeAllCreaturesAndProps();
await createTestCreature(actionTestCreature);
});
it('Applies the children of the folder', async function () {
const action = await runActionById(folderId);
assert.exists(action);
assert.deepEqual(allMutations(action), [{
contents: [
{
value: 'this should run'
}
],
targetIds: [],
}]);
});
});

View File

@@ -0,0 +1,16 @@
import { EngineAction } from '/imports/api/engine/action/EngineActions';
import { applyDefaultAfterPropTasks } from '/imports/api/engine/action/functions/applyTaskGroups';
import { PropTask } from '/imports/api/engine/action/tasks/Task';
export default async function applyFolderProperty(
task: PropTask, action: EngineAction, result, userInput
): Promise<void> {
const prop = task.prop;
if (prop.type !== 'folder' && prop.type !== 'propertySlot') {
throw new Meteor.Error('wrong-property', `Expected a folder, got ${prop.type} instead`);
}
return applyDefaultAfterPropTasks(action, prop, task.targetIds, userInput);
}

View File

@@ -0,0 +1,48 @@
import { assert } from 'chai';
import {
allMutations,
createTestCreature,
getRandomIds,
removeAllCreaturesAndProps,
runActionById
} from '/imports/api/engine/action/functions/actionEngineTest.testFn';
const [
creatureId, noteId
] = getRandomIds(2);
const actionTestCreature = {
_id: creatureId,
props: [
{
_id: noteId,
type: 'note',
name: 'Note Name',
summary: { text: 'Note summary {1 + 2}' }
},
],
}
describe('Apply note properties', function () {
// Increase timeout
this.timeout(8000);
before(async function () {
await removeAllCreaturesAndProps();
await createTestCreature(actionTestCreature);
});
it('Applies the note text', async function () {
const action = await runActionById(noteId);
assert.exists(action);
assert.deepEqual(allMutations(action), [{
contents: [
{
name: 'Note Name',
value: 'Note summary 3'
}
],
targetIds: [],
}]);
});
});

View File

@@ -0,0 +1,38 @@
import { EngineAction } from '/imports/api/engine/action/EngineActions';
import InputProvider from '/imports/api/engine/action/functions/userInput/InputProvider';
import { applyDefaultAfterPropTasks } from '/imports/api/engine/action/functions/applyTaskGroups';
import recalculateInlineCalculations from '/imports/api/engine/action/functions/recalculateInlineCalculations';
import { PropTask } from '/imports/api/engine/action/tasks/Task';
import TaskResult, { LogContent } from '/imports/api/engine/action/tasks/TaskResult';
export default async function applyNoteProperty(
task: PropTask, action: EngineAction, result: TaskResult, inputProvider: InputProvider
): Promise<void> {
const prop = task.prop;
if (prop.type !== 'note') {
throw new Meteor.Error('wrong-property', `Expected a note, got ${prop.type} instead`);
}
const logContent: LogContent & { silenced: boolean | undefined; } = {
silenced: prop.silent,
};
if (prop.name) logContent.name = prop.name;
if (prop.summary?.text) {
await recalculateInlineCalculations(prop.summary, action, 'reduce', inputProvider);
logContent.value = prop.summary.value;
}
if (logContent.name || logContent.value) {
result.appendLog(logContent, task.targetIds);
}
// Log description
if (prop.description?.text) {
await recalculateInlineCalculations(prop.description, action, 'reduce', inputProvider);
result.appendLog({
value: prop.description.value,
silenced: prop.silent,
}, task.targetIds);
}
return applyDefaultAfterPropTasks(action, prop, task.targetIds, inputProvider);
}

View File

@@ -0,0 +1,54 @@
import { assert } from 'chai';
import {
allLogContent,
createTestCreature,
getRandomIds,
removeAllCreaturesAndProps,
runActionById,
TestCreature
} from '/imports/api/engine/action/functions/actionEngineTest.testFn';
const [
creatureId, rollId,
] = getRandomIds(2);
const actionTestCreature: TestCreature = {
_id: creatureId,
props: [
{
_id: rollId,
type: 'roll',
name: 'Roll Name',
variableName: 'roll1',
roll: { calculation: '7 + 15' },
children: [
{
type: 'note',
summary: { text: 'roll: {roll1}' },
},
],
},
],
};
describe('Apply roll properties', function () {
// Increase timeout
this.timeout(8000);
before(async function () {
await removeAllCreaturesAndProps();
await createTestCreature(actionTestCreature);
});
it('Saves the value of the roll into the variable name', async function () {
const action = await runActionById(rollId);
assert.exists(action);
assert.deepEqual(allLogContent(action), [{
inline: true,
name: 'Roll Name',
value: '**22**',
}, {
value: 'roll: 22',
}]);
});
});

View File

@@ -0,0 +1,69 @@
import { EngineAction } from '/imports/api/engine/action/EngineActions';
import InputProvider from '/imports/api/engine/action/functions/userInput/InputProvider';
import { applyDefaultAfterPropTasks } from '/imports/api/engine/action/functions/applyTaskGroups';
import { rollAndReduceCalculation } from '/imports/api/engine/action/functions/recalculateCalculation';
import { PropTask } from '/imports/api/engine/action/tasks/Task';
import TaskResult from '/imports/api/engine/action/tasks/TaskResult';
import { isFiniteNode } from '/imports/parser/parseTree/constant';
import toString from '/imports/parser/toString';
export default async function applyRollProperty(
task: PropTask, action: EngineAction, result: TaskResult, inputProvider: InputProvider
): Promise<void> {
const prop = task.prop;
if (prop.type !== 'roll') {
throw new Meteor.Error('wrong-property', `Expected a roll, got ${prop.type} instead`);
}
// If there isn't a calculation, just apply the children instead
if (!prop.roll?.calculation) {
return applyDefaultAfterPropTasks(action, prop, task.targetIds, inputProvider);
}
const logValue: string[] = [];
// roll the dice only and store that string
const {
rolled, reduced, errors
} = await rollAndReduceCalculation(prop.roll, action, inputProvider);
if (rolled.parseType !== 'constant') {
logValue.push(toString(rolled));
}
errors?.forEach(error => {
result.appendLog({
name: 'Error',
value: error.message,
silenced: prop.silent,
}, task.targetIds);
});
// Store the result
if (reduced.parseType === 'constant') {
prop.roll.value = reduced.value;
} else if (reduced.parseType === 'error') {
prop.roll.value = undefined;
} else {
prop.roll.value = toString(reduced);
}
// If we didn't end up with a constant or a number of finite value, give up
if (!isFiniteNode(reduced)) {
return applyDefaultAfterPropTasks(action, prop, task.targetIds, inputProvider);
}
const value = reduced.value;
result.scope[prop.variableName] = { value };
logValue.push(`**${value}**`);
result.appendLog({
name: prop.name,
value: logValue.join('\n'),
inline: true,
silenced: prop.silent,
}, task.targetIds);
// Apply children
return applyDefaultAfterPropTasks(action, prop, task.targetIds, inputProvider);
}

View File

@@ -0,0 +1,115 @@
import { assert } from 'chai';
import {
allMutations,
createTestCreature,
getRandomIds,
removeAllCreaturesAndProps,
runActionById,
TestCreature
} from '/imports/api/engine/action/functions/actionEngineTest.testFn';
const [
creatureId, savingThrowId, targetCreatureId, targetCreature2Id
] = getRandomIds(4);
const actionTestCreature: TestCreature = {
_id: creatureId,
props: [
{
_id: savingThrowId,
type: 'savingThrow',
name: 'Strength Save',
dc: { calculation: '10 + 3' },
stat: 'strengthSave',
children: [{
type: 'branch',
branchType: 'successfulSave',
children: [{
type: 'note',
summary: { text: 'note to apply on save' },
}],
}, {
type: 'branch',
branchType: 'failedSave',
children: [{
type: 'note',
summary: { text: 'note to apply on failed save' },
}],
}],
},
],
}
const actionTargetCreature: TestCreature = {
_id: targetCreatureId,
props: [
{
type: 'skill',
variableName: 'strengthSave',
baseValue: { calculation: '3' },
},
],
}
const actionTargetCreature2: TestCreature = {
_id: targetCreature2Id,
props: [
{
type: 'skill',
variableName: 'strengthSave',
baseValue: { calculation: '2' },
},
],
}
describe('Apply saving throw properties', function () {
// Increase timeout
this.timeout(8000);
before(async function () {
await removeAllCreaturesAndProps();
await createTestCreature(actionTestCreature);
await createTestCreature(actionTargetCreature);
await createTestCreature(actionTargetCreature2);
});
it('Makes multiple creatures make saves', async function () {
const action = await runActionById(savingThrowId, [targetCreatureId, targetCreature2Id]);
assert.exists(action);
assert.deepEqual(allMutations(action), [
{
'contents': [{
'inline': true,
'name': 'Strength Save',
'value': 'DC **13**',
}, {
'inline': true,
'name': 'Successful save',
'value': '1d20 [ 10 ] + 3\n**13**',
}],
'targetIds': [targetCreatureId],
}, {
'contents': [{
'value': 'note to apply on save',
}],
'targetIds': [targetCreatureId],
}, {
'contents': [{
'inline': true,
'name': 'Strength Save',
'value': 'DC **13**',
}, {
'inline': true,
'name': 'Failed save',
'value': '1d20 [ 10 ] + 2\n**12**',
}],
'targetIds': [targetCreature2Id],
}, {
'contents': [{
'value': 'note to apply on failed save',
}],
'targetIds': [targetCreature2Id],
},
],
);
});
});

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