Compare commits

...

524 Commits

Author SHA1 Message Date
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
b3ed77964f Hotfix library select turning into links incorrectly 2023-06-28 10:22:38 +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
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
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
5ad0de9eb7 Bumped version 2022-11-25 16:36:44 +02:00
Stefan Zermatten
0b377fcb71 Attributes show their children in stats cards 2022-11-25 16:27:18 +02:00
Stefan Zermatten
1f26fbf00e Iterated on stat grouping cards
adde slots, spell lists, children of slot fillers
hid properties in most places
spell slots in correct order
2022-11-25 13:25:38 +02:00
Stefan Zermatten
bb1e9624ad Fixed hit dice missing on stats tab 2022-11-25 12:14:47 +02:00
Stefan Zermatten
bda446858e Fixed spell tab btn not hiding correctly on mobile 2022-11-24 15:04:35 +02:00
Stefan Zermatten
e19e91f7e0 Fixed broken $attackRoll always returning 0 2022-11-24 14:51:05 +02:00
Stefan Zermatten
bac9fc98dd Fixed order of stats tab, unhid sneaky folders 2022-11-24 14:48:58 +02:00
Stefan Zermatten
420663c149 Hotfix 2 crashy boogaloo 2022-11-24 14:10:22 +02:00
Stefan Zermatten
23d44e54e3 Hotfixed S3 not loading 2022-11-24 13:39:28 +02:00
Stefan Zermatten
881496e9c1 Removed all md docs 2022-11-24 13:09:03 +02:00
Stefan Zermatten
002c767d1a Bumped version 2022-11-24 12:57:53 +02:00
Stefan Zermatten
9aaf31d5cf Merge branch 'version-2-dev' into version-2 2022-11-24 12:56:30 +02:00
Stefan Zermatten
d05cd2fa19 Iterated on docs, fixed links 2022-11-24 12:29:00 +02:00
Stefan Zermatten
f13774df11 Removed old mardown docs 2022-11-24 11:56:20 +02:00
Stefan Zermatten
cc78ba948e Merge branch 'version-2-dev' of https://github.com/ThaumRystra/DiceCloud into version-2-dev 2022-11-24 11:45:17 +02:00
Stefan Zermatten
c6bfb84bb0 Updated default docs 2022-11-24 11:45:13 +02:00
Stefan Zermatten
7e49100d14 Fixed dragging on mobile 2022-11-23 15:52:45 +02:00
Stefan Zermatten
c3ac49a8c4 Fixed default doc importing 2022-11-23 15:51:09 +02:00
Stefan Zermatten
fd9d525ba9 fixed: Show only the last event with a var name 2022-11-22 21:59:44 +02:00
Stefan Zermatten
d947b62ba4 Fixed events not being selectable for reset in libraries 2022-11-22 21:29:08 +02:00
Stefan Zermatten
046509224e Fixed library page janky loading 2022-11-22 21:21:53 +02:00
Stefan Zermatten
63bcf023ee Big UI overhaul
Moved tabs to bottom nav on mobile
Added actions tab
Conditional benefits on skills/saves show on stats tab
2022-11-22 20:51:21 +02:00
Stefan Zermatten
9741d1d56c Got in-dialog help working with new docs 2022-11-22 03:01:59 +02:00
Stefan Zermatten
0f12c98408 Merge branch 'version-2-dev' of https://github.com/ThaumRystra/DiceCloud into version-2-dev 2022-11-22 02:34:38 +02:00
Stefan Zermatten
254390e1c1 Added default doc seeding for new servers 2022-11-22 02:34:34 +02:00
Stefan Zermatten
ad2f43712d Merge pull request #300 from Jonpot/patch-1
Fix skill.md typo
2022-11-22 02:33:13 +02:00
Stefan Zermatten
55f8dac0db Merge pull request #302 from Jonpot/patch-3
Fix Action scope variables
2022-11-22 02:32:17 +02:00
Jonpot
9f8c3f0f3d Update applyAction.js
As per the docs, $attackDiceRoll should be the value of the d20 before modifiers, and $attackRoll should the the total value, after modifiers. Pre-patch, the former variable is never defined, and the latter variable has the wrong value.
2022-11-21 16:20:48 -08:00
Jonpot
56bd41f435 Fix skill.md typo 2022-11-21 15:49:01 -08:00
Stefan Zermatten
063d4288ef Point buys can now guess cost while dragging slide 2022-11-22 01:45:25 +02:00
Stefan Zermatten
a3355dd988 stat grouping is now everywhere
This lead to a complete refactor of the stats page
Some things might break
2022-11-22 00:56:10 +02:00
Stefan Zermatten
d2649fd66e Overhauled how documentation works 2022-11-21 18:22:49 +02:00
Stefan Zermatten
e619734ee1 Fixed checks not having access to #skill 2022-11-21 13:16:54 +02:00
Stefan Zermatten
5108b32624 Added line breaks in parser strings
Actual line breaks and \n both work
2022-11-20 00:19:41 +02:00
Stefan Zermatten
a9b389023e added todo 2022-11-19 23:44:02 +02:00
Stefan Zermatten
e06d2befc4 Fixed damage multipliers not using implicit tags 2022-11-19 23:24:11 +02:00
Stefan Zermatten
04c9c4cfc2 Fixed bug where updates on sliders weren't debounced 2022-11-19 22:39:35 +02:00
Stefan Zermatten
cc7dc257fb Fixed attributes not showing base value in lib 2022-11-19 19:35:46 +02:00
Stefan Zermatten
f3deadb3f1 Fixed buff descriptions not calced before logged 2022-11-19 19:25:51 +02:00
Stefan Zermatten
dcfb380e57 Fixed saving throw tag targeted effects
They will now roll dice effects before saving
2022-11-19 19:15:53 +02:00
Stefan Zermatten
a568cdfb1e Fixed NaNing of skills that have rolled effects 2022-11-19 18:57:26 +02:00
Stefan Zermatten
cea63e6a8e Moved dev file storage to inside meteor app folder
Prevented verbose logging of file storage
2022-11-19 18:34:52 +02:00
Stefan Zermatten
b6b0cfbb9b Fixed triggers on attribute reset on rest 2022-11-19 18:12:51 +02:00
Stefan Zermatten
428aeef635 Removed HMR test text 2022-11-19 17:56:36 +02:00
Stefan Zermatten
e3644eb9e8 Moved UI to client folder to fix HMR 2022-11-19 17:51:50 +02:00
Stefan Zermatten
060b5f93ca Reduced bundle size and updates packages 2022-11-19 17:19:07 +02:00
Stefan Zermatten
0f3a96da17 Spell list ability modifier can take non-abilities
defaults to .value if .modifier is undefined
is now undefined for no .modifier or .value
2022-11-18 14:21:22 +02:00
Stefan Zermatten
c55d572134 Bumped version 2022-11-16 23:52:08 +02:00
Stefan Zermatten
0a2b60990e Merge branch 'version-2-dev' into version-2 2022-11-16 23:51:37 +02:00
Stefan Zermatten
a437ff5aef Fixed log of recovering HD not having names 2022-11-09 15:02:41 +02:00
Stefan Zermatten
3d31d62860 Completed folder stat grouping UI 2022-11-09 14:58:52 +02:00
Stefan Zermatten
8377231254 Began work on stat grouping by folder 2022-11-09 00:00:54 +02:00
Stefan Zermatten
1ec29365cb Added custom sheet events
Made rest buttons optional
2022-11-08 23:01:09 +02:00
Stefan Zermatten
60b21c1901 Fixed bugs with effects
they were not providing advantage or conditional benefits
2022-11-08 18:29:27 +02:00
Stefan Zermatten
03f87b0afa Added spellcasting ability to spell lists 2022-11-08 18:09:00 +02:00
Stefan Zermatten
48291d2c8f Added help to property creation forms 2022-11-08 17:17:26 +02:00
Stefan Zermatten
1cedf55fbf Merge branch 'version-2-print' into version-2-dev 2022-11-08 17:00:52 +02:00
Stefan Zermatten
bed4d4b162 Fixed logo not showing 2022-11-08 16:59:52 +02:00
Stefan Zermatten
a1d992ec8d Fixed blank multipliers box 2022-11-07 16:38:54 +02:00
Stefan Zermatten
008ef62517 Printing implemented, needs print button on sheet 2022-11-07 16:18:35 +02:00
Stefan Zermatten
c436309ba8 Work on column-based print layout 2022-11-07 00:07:42 +02:00
Stefan Zermatten
0bfdb73b47 Added a quick exit for migrations if the database is new 2022-11-04 12:34:37 +02:00
Stefan Zermatten
a462cc5ca2 Updated packages 2022-11-04 12:34:22 +02:00
Stefan Zermatten
5d57a74667 Merge branch 'version-2-dev' into version-2 2022-11-03 20:52:26 +02:00
Stefan Zermatten
21b0029df7 bumped version 2022-11-03 20:51:58 +02:00
Stefan Zermatten
c0ccafa787 Added overflow stops to health bars 2022-11-03 20:50:10 +02:00
Stefan Zermatten
d63ad9ea8f Added hide when total/value zero to attributes 2022-11-03 20:39:02 +02:00
Stefan Zermatten
8f56a60fb1 Added copy-to and related sharing permissions 2022-11-03 20:18:59 +02:00
Stefan Zermatten
358ae46627 Began work on copy to for library nodes 2022-11-03 19:08:44 +02:00
Stefan Zermatten
cc915410da Fixed query hitting mongo with too much regex 2022-10-25 19:00:02 +02:00
Stefan Zermatten
0b1db3c40c Updated meteor 2022-10-18 15:40:41 +02:00
Stefan Zermatten
0ad7e659d2 updated docs to include create a class guide 2022-10-18 15:40:17 +02:00
Stefan Zermatten
58c3875dc7 Hotifix: Casting cantrips without a spell slot 2022-10-12 07:36:42 +02:00
Stefan Zermatten
84f506f1fe Added $checkDiceRoll $checkRoll $checkModifier variables 2022-10-12 07:32:39 +02:00
Stefan Zermatten
a5b4b20324 Added link to V2 2022-10-11 14:08:33 +02:00
Stefan Zermatten
d0a3ccc76a bumped version 2022-10-10 16:54:57 +02:00
Stefan Zermatten
93ac9215c2 Merge branch 'version-2-dev' into version-2 2022-10-10 16:53:10 +02:00
Stefan Zermatten
a6b501a62c Fixed error on missing group prop in tree node 2022-10-10 16:51:02 +02:00
Stefan Zermatten
e956bacf07 Added actionType to effective tags 2022-10-10 16:49:10 +02:00
Stefan Zermatten
60b6b283b1 Folders now get their children applied by actions 2022-10-10 16:45:53 +02:00
Stefan Zermatten
1c9b390551 Added ritual casting 2022-10-09 23:11:06 +02:00
Stefan Zermatten
21a487635d Removed unused code from action cards 2022-10-09 21:56:42 +02:00
Stefan Zermatten
c92a26d5e6 Action cards no longer display folder or the descendants of buffs 2022-10-09 21:56:01 +02:00
Stefan Zermatten
49b514b8f3 Load common dialogs more aggressively 2022-10-09 20:55:50 +02:00
Stefan Zermatten
5cb835c536 Got basic typescript tools working 2022-10-09 17:33:43 +02:00
Stefan Zermatten
aa8f2d230d Hunted the last of the \t's to extinction 2022-10-09 16:56:28 +02:00
Stefan Zermatten
2fa913b09a Applied style rules to genocide all \t characters 2022-10-09 16:01:36 +02:00
Stefan Zermatten
de598c70a7 Fixed rolled effects not applying to checks 2022-10-09 11:10:50 +02:00
Stefan Zermatten
baecdeff24 Fixed bug where items with zero quantity have active children 2022-10-09 10:10:21 +02:00
Stefan Zermatten
d4b7d22b5f Fixed toggled off spells showing in spell list 2022-09-26 09:43:00 +02:00
Stefan Zermatten
87f79737e8 Fixed empty calculated inline fields showing calc 2022-09-25 12:39:49 +02:00
Stefan Zermatten
9f0ffe13f8 Updated meteor to fix observer bugs 2022-09-13 17:34:46 +02:00
Stefan Zermatten
adaa31d76c damage tags to ignore multipliers 2022-09-13 17:34:30 +02:00
Stefan Zermatten
b051d764f8 Slot cards have slot color as outline 2022-09-13 15:47:31 +02:00
Stefan Zermatten
ffb5b4a4f3 Libraries show name in page title 2022-09-13 15:44:37 +02:00
Stefan Zermatten
fd87b7fb75 Added advantage popup to spell cast 2022-09-09 13:20:54 +02:00
Stefan Zermatten
f035902842 Removed unused file 2022-09-08 14:47:12 +02:00
Stefan Zermatten
dbc5f7253f Finished basic docs 2022-09-05 14:36:39 +02:00
Stefan Zermatten
f0e7253374 Updated docs 2022-09-01 13:33:28 +02:00
Stefan Zermatten
ffe37bf907 Added more property help docs 2022-09-01 12:18:29 +02:00
Stefan Zermatten
a63e2099d3 Added documentation UI and began documenting props 2022-08-31 14:43:38 +02:00
Stefan Zermatten
0308e4e7a7 Merge branch 'version-2' into version-2-dev 2022-08-29 11:30:55 +02:00
Stefan Zermatten
43f8df09f0 Fixed client crash when effects target calcs 2022-08-26 09:42:34 +02:00
Stefan Zermatten
b6ed9ffb74 Merge branch 'version-2-dev' into version-2 2022-08-25 15:24:25 +02:00
Stefan Zermatten
a84da7d8a5 Buffs can skip var freezing, freeze inline calcs 2022-08-25 15:10:56 +02:00
Stefan Zermatten
249aebea0f Allowed some properties to return damaged action values
When a prop is damaged during an action, it now tries
to show its new value during the rest of that action
2022-08-25 15:10:36 +02:00
Stefan Zermatten
11a527481e Show Point buy on Build tab 2022-08-25 13:18:24 +02:00
Stefan Zermatten
8d729216b5 Properties now have their variable name as a default tag 2022-08-25 12:15:12 +02:00
Stefan Zermatten
1677e8c424 Fixed silence missing from trigger form 2022-08-25 12:14:32 +02:00
Stefan Zermatten
987aacbb67 Silence for triggers also 2022-08-25 12:12:07 +02:00
Stefan Zermatten
2714d0b9d5 Added the ability to silence most action props 2022-08-25 12:10:51 +02:00
Stefan Zermatten
1d98c41168 Fixed slotLevel not having the right value in spell scope 2022-08-25 11:40:30 +02:00
Stefan Zermatten
e42ec4b862 Continued work on point buy UI 2022-08-23 14:44:35 +02:00
Stefan Zermatten
59fc5ab851 Continued work on point buy UI 2022-08-22 15:07:40 +02:00
Stefan Zermatten
5d14c392e8 Added creature new variables to API 2022-08-22 11:58:48 +02:00
Stefan Zermatten
c6ca8c1fa4 Added point buy to computation engine 2022-08-19 14:03:12 +02:00
Stefan Zermatten
28307e26c3 Fixed some issues with skill display 2022-08-19 14:03:03 +02:00
Stefan Zermatten
6d42eb62f0 Merge branch 'version-2' into version-2-dev 2022-08-19 09:18:55 +02:00
Stefan Zermatten
877c9ca099 Fixed cache bashing in checks
Cache should only return clones of data,
not references to the cached data
2022-08-17 17:21:18 +02:00
Stefan Zermatten
9b652fc133 Added point buy form 2022-08-17 13:42:47 +02:00
Stefan Zermatten
7d66c06107 Fixed class level up w/ subscribed collections 2022-08-17 12:45:54 +02:00
Stefan Zermatten
21629138f0 Added Buff Removed action trigger 2022-08-17 12:28:00 +02:00
Stefan Zermatten
59a488256b Added buff removers 2022-08-17 11:38:30 +02:00
Stefan Zermatten
766519b4a3 Prevented inactive properties from creating deps
Engine might work differently
2022-08-17 09:40:26 +02:00
Stefan Zermatten
e7f73d0e54 Stopped crystalizing variables in nested buffs 2022-08-17 09:39:45 +02:00
Stefan Zermatten
193d5eec50 Changed slot cards to column layout 2022-08-16 13:40:58 +02:00
Stefan Zermatten
9284c9ad76 Fixed decimal stats being rounded down 2022-08-16 13:05:56 +02:00
Stefan Zermatten
f86152675f Added button to unhide hidden slots 2022-08-16 12:31:37 +02:00
Stefan Zermatten
cbac5264cd Added delete buttons to slot fill card 2022-08-16 11:44:08 +02:00
Stefan Zermatten
34e3325464 Fixed dependency loops created by inactive props
depending on their parent toggles
2022-08-16 11:19:16 +02:00
Stefan Zermatten
79c9e67ce2 Fixed icons being missing from buff-applied props 2022-08-16 10:11:13 +02:00
Stefan Zermatten
4c2aabf90d Fixed character sheet toolbar alignment on mobile 2022-08-16 10:03:07 +02:00
Stefan Zermatten
48331d3806 Fixed added properties being added based on tree
tab selection even when on other tabs
2022-08-16 09:49:34 +02:00
Stefan Zermatten
45f05d0d34 Fixed bug where actions targeting self
weren't applying props to self
2022-08-16 09:26:40 +02:00
Stefan Zermatten
58629c92f4 Added build command to package.json 2022-08-15 16:10:40 +02:00
Stefan Zermatten
719af548f0 Merge branch 'version-2-dev' into version-2 2022-08-15 15:42:54 +02:00
Stefan Zermatten
f2a1861279 Fixed Slot cards not using markdown 2022-08-15 15:38:57 +02:00
Stefan Zermatten
38c3b6ff1f Fixed tier paid benefit error text 2022-08-15 15:38:36 +02:00
Stefan Zermatten
23e848fe40 Fixed hit dice recovery calculation on long rest 2022-08-15 15:12:14 +02:00
Stefan Zermatten
4d6cdf50bd Fixed tree search input missing types 2022-08-15 14:47:36 +02:00
Stefan Zermatten
1cf9f3b5fd Fixed conditional benefits on abilities not showing on skills 2022-08-15 14:36:20 +02:00
Stefan Zermatten
8164b79667 Improved markdown formatting
Fixed pre-code breaking out of containers
2022-08-15 14:19:42 +02:00
Stefan Zermatten
360df79004 Fixed after save trigger not firing when no targets 2022-08-15 12:31:56 +02:00
Stefan Zermatten
a8f163ff33 Removed trigger.summary 2022-08-15 12:29:58 +02:00
Stefan Zermatten
36b3b80850 Moved triggers in action props to run before children 2022-08-15 12:07:57 +02:00
Stefan Zermatten
1d22f4c054 Hid tags on trigger form when they're not needed 2022-08-15 10:59:35 +02:00
Stefan Zermatten
99e4e8d6bb Fixed some issues with effect tag targeting 2022-08-15 10:53:38 +02:00
Stefan Zermatten
2bb3265356 Fixed client error in creature form 2022-08-15 10:32:27 +02:00
Stefan Zermatten
263f2d8424 Fixed failing tests 2022-08-15 09:38:34 +02:00
Stefan Zermatten
ee0e764294 Refactored entire action engine
Triggers needed action context to function outside of the action engine
proper, so now it's been abstracted into its own class
2022-08-13 00:22:32 +02:00
Stefan Zermatten
13fc0c0b12 Triggers can fire on character sheet checks 2022-08-12 19:52:58 +02:00
Stefan Zermatten
ecfeeaccd9 Breadcrumbs now show when editing property 2022-08-12 19:20:23 +02:00
Stefan Zermatten
b324fb1f03 Stopped triggers from firing if they are inactive 2022-08-12 19:19:58 +02:00
Stefan Zermatten
8d34cc1369 Fixed trigger form hint text mentioning slots instead of trigger 2022-08-12 18:10:24 +02:00
Stefan Zermatten
839c2488b2 Merge branch 'version-2' into version-2-dev 2022-08-12 17:57:18 +02:00
Stefan Zermatten
fd79bc2bb3 Removed empty dependency loop errors 2022-07-26 14:09:51 +02:00
Stefan Zermatten
1050442606 Children of triggers are now inactive on the sheet 2022-07-26 13:43:55 +02:00
Stefan Zermatten
53ed271ea2 Calculation errors moved to the build page
Can be hidden, restyled to improve usability
in light mode
2022-07-26 13:33:05 +02:00
Stefan Zermatten
6ccbf204eb Turned FileColelction logging off for production 2022-07-26 12:04:13 +02:00
Stefan Zermatten
d44d4e0315 Increased timeout and retries on S3 config 2022-07-26 11:53:18 +02:00
Stefan Zermatten
2b8f7e4927 Fixed fillers with type not showing in slot fill 2022-07-26 11:33:47 +02:00
Stefan Zermatten
65e7ce6dce removed unused import 2022-07-26 09:10:24 +02:00
Stefan Zermatten
24cc87d6f7 Fixed deleted libraryNodes being shown in library 2022-07-26 09:07:20 +02:00
Stefan Zermatten
03578b2066 fixed classes without variable names breaking the sheet 2022-07-25 15:21:38 +02:00
Stefan Zermatten
6ea882a053 Fixed trigger conditions not working on rest 2022-07-25 15:13:47 +02:00
Stefan Zermatten
bec65be170 Merge branch 'version-2-dev' into version-2 2022-07-25 09:40:31 +02:00
Stefan Zermatten
0483a7effc Tag targeting attributes and skills with effects
now works like normal effects would
2022-07-24 22:32:40 +02:00
Stefan Zermatten
4c5c537f29 Improved slot tag handling 2022-07-24 21:50:31 +02:00
Stefan Zermatten
a0c2822dac Added "extra" damage type
Takes on the same damage type as the last damage applied during the
same action, otherwise deals "extra" damage
2022-07-24 20:38:42 +02:00
Stefan Zermatten
27a21aed59 Added error alert to stat tab for dependency loops 2022-07-24 19:59:43 +02:00
Stefan Zermatten
1da2d319fb Improved handling of tag targeting 2022-07-24 15:22:07 +02:00
Stefan Zermatten
82879aaa60 Added admin override to view permission 2022-07-24 15:12:12 +02:00
Stefan Zermatten
be654d5d45 Added tree root dialog to breadcrumbs 2022-07-24 14:46:27 +02:00
Stefan Zermatten
f88ffcf0c3 Hardened archive upload slightly 2022-07-24 14:17:39 +02:00
Stefan Zermatten
8b62bac83f Added classes and level up button to build tab 2022-07-24 14:17:27 +02:00
Stefan Zermatten
e698b4b838 Added Dai as Paragon 2022-07-21 09:05:27 +02:00
Stefan Zermatten
566d6a4fef Triggers 🤫 2022-07-20 15:57:38 +02:00
Stefan Zermatten
6f7e742eb9 More of the sheet conforms to library allowances 2022-07-20 00:09:58 +02:00
Stefan Zermatten
0c06f60b7e Fixed typo "Transfer Onwership" 2022-07-19 19:25:12 +02:00
Stefan Zermatten
f8e9131bdd Vastly improved new character UX
Characters now can limit which libraries they allow
2022-07-18 13:45:14 +02:00
Stefan Zermatten
bf9639ae59 Library Collections UI built 2022-07-17 22:48:48 +02:00
Stefan Zermatten
ee89a052bc Added libraryCollections on server 2022-07-13 23:16:25 +02:00
Stefan Zermatten
59ef7527b7 Fixed some errors with character insertion/deletion 2022-07-09 12:53:44 +02:00
Stefan Zermatten
b8a03245ea Level up dialog now working 2022-07-05 15:40:55 +02:00
Stefan Zermatten
1a71c2cfa7 Began implementing class level up UI 2022-07-04 13:55:41 +02:00
Stefan Zermatten
292388dead Iterated on class UI 2022-06-29 15:35:12 +02:00
Stefan Zermatten
00272e7b55 Action cards now show their decendants 2022-06-29 15:27:12 +02:00
Stefan Zermatten
f07f05ca2c Moved creature variables to their own collection
Another big change to the engine, expect bugs
2022-06-29 14:54:25 +02:00
Stefan Zermatten
9dd84a83d2 Started removing scope from creature doc 2022-06-24 10:50:35 +02:00
Stefan Zermatten
b2f89eceee Fixed some bugs with characters not recomputing
TODO: remove .variables cache from creature document, it's not viable
2022-06-23 08:39:48 +02:00
Stefan Zermatten
b484a27637 iterated on class form to match new schema 2022-06-23 08:39:15 +02:00
Stefan Zermatten
da5143693f Iterated on class UI 2022-06-21 11:08:45 +02:00
Stefan Zermatten
9cc4186171 Reduced data load in slot fill dialog 2022-06-21 11:00:50 +02:00
Stefan Zermatten
9f59a6cf86 Fixed long folder names not truncating correctly 2022-06-20 15:11:39 +02:00
Stefan Zermatten
fdaa035bfb Fixed Shakespearean typo in slot filer description 2022-06-20 14:50:04 +02:00
Stefan Zermatten
b31760af0c Don't show children expansion panel if not needed 2022-06-20 14:22:13 +02:00
Stefan Zermatten
a8ffa2f786 Fixed denormalized creature variables not removed 2022-06-20 13:05:38 +02:00
Stefan Zermatten
9b1ec46064 Added children properties to prop edit dialog 2022-06-08 12:22:51 +02:00
Stefan Zermatten
11f373ddd8 Improved slotCard UI 2022-06-07 23:27:14 +02:00
Stefan Zermatten
e7e8f938ed Improved archive storage calculations and errors 2022-06-07 23:21:48 +02:00
Stefan Zermatten
28934baac9 Implemented archive upload handling 2022-06-07 23:01:06 +02:00
Stefan Zermatten
385ac17812 Added big slot cards to build tab, improved build tab 2022-06-07 21:43:35 +02:00
Stefan Zermatten
a04935c5b4 Updated packages 2022-06-07 18:51:07 +02:00
Stefan Zermatten
ccc861b6fa Improved character subscription performance
By limiting fields that can trigger an autorun
2022-05-11 15:52:44 +02:00
Stefan Zermatten
6702f431d0 Fixed bug where removed library nodes kept showing 2022-05-11 15:52:02 +02:00
Stefan Zermatten
1b3efae81a Replaced manual recompute calls with dirty flag settings 2022-05-11 15:42:29 +02:00
Stefan Zermatten
7a35c66904 Removed performance logging from loading creatures 2022-05-11 13:33:56 +02:00
Stefan Zermatten
78cd8ffc8d Creatures are now cached in memory for computation
Also removed dependency group calculation because the optimisation isn't
as useful to reduce DB calls if the creature is in memory anyway
2022-05-11 13:30:33 +02:00
Stefan Zermatten
23fa6fe634 Progress on dependency updates 2022-05-09 16:32:15 +02:00
Stefan Zermatten
caf50d1578 Merge branch 'version-2' into version-2-dev 2022-05-09 12:46:09 +02:00
Stefan Zermatten
df7889edd9 Reduced fields loaded by library tree view
This should improve performance a little for large libraries,
at the expense of loading when a property is selected
2022-05-09 11:23:46 +02:00
Stefan Zermatten
ddc7f87a4a Replaced unsupported cron manager with new one 2022-05-03 11:29:27 +02:00
Stefan Zermatten
33fa22c187 Removed stray log 2022-05-02 23:31:52 +02:00
Stefan Zermatten
2e3f0320f3 Added dependency grouping, but commented out for now until it's needed 2022-05-02 23:31:10 +02:00
Stefan Zermatten
0b7c20e616 Added APM monitoring 2022-05-02 18:48:41 +02:00
Stefan Zermatten
abb8890070 Build card being converted into build tree
Still need to be able to delete fillers
2022-05-02 12:32:14 +02:00
Stefan Zermatten
8dbcae1060 Fixed FAB appearing on wrong tabs 2022-05-01 23:43:31 +02:00
Stefan Zermatten
3a18bce7e6 Added error message for unsupported accessors 2022-05-01 22:54:30 +02:00
Stefan Zermatten
3e97baaaaa Progress on storing user images 2022-04-25 16:16:17 +02:00
Stefan Zermatten
ea32c54f57 Fixed massive writes to creature.variables on calc
Now only writes changed variables, preventing oplog from being
polluted with massive updates
2022-04-25 13:57:39 +02:00
Stefan Zermatten
6b724cf365 Dicecloud instances without db version numbers won't go into migration mode 2022-04-25 11:16:02 +02:00
Stefan Zermatten
8b44c83741 Added archive upload UI 2022-04-23 11:35:11 +02:00
Stefan Zermatten
2ca9ac5342 Added storage stats to the account page 2022-04-23 09:52:20 +02:00
Stefan Zermatten
7609e916c6 Implemented remove button on archive files 2022-04-23 09:35:29 +02:00
Stefan Zermatten
f440e030cf Fixed bug in generating error messages when out of character slots 2022-04-22 11:31:59 +02:00
Stefan Zermatten
13b6689ba4 Progress on file system UI 2022-04-22 11:30:59 +02:00
Stefan Zermatten
b28bcbe079 Work on general UI for user files 2022-04-21 22:08:18 +02:00
Stefan Zermatten
ffa6353a3f Merge branch 'version-2' into version-2-dev 2022-04-21 20:25:23 +02:00
Stefan Zermatten
33f60c2c6d Merge branch 'version-2' of https://github.com/ThaumRystra/DiceCloud into version-2 2022-04-21 10:59:22 +02:00
Stefan Zermatten
dcc95486b3 fixed parser error when using incorrect call args 2022-04-21 10:59:19 +02:00
Stefan Zermatten
88bc223daa Began working on build tab 2022-04-20 13:56:06 +02:00
Stefan Zermatten
cbc42f8500 fix: buff descriptions aren't being calculated 2022-04-15 18:13:03 +02:00
Stefan Zermatten
cc24690a67 Fixed bug where children of rolls would be applied 2022-04-09 15:02:44 +02:00
Stefan Zermatten
4d5cb3ed50 Merge branch 'version-2-dev' into version-2 2022-04-08 18:41:48 +02:00
Stefan Zermatten
c3d9ee7589 Default health bars are now the app's primary color 2022-04-08 18:40:53 +02:00
Stefan Zermatten
d9f572504d Merge branch 'version-2' into version-2-dev 2022-04-08 17:59:20 +02:00
Stefan Zermatten
cb80f3a6da Custom health bar colors. Also works for setting mid and low health colors 2022-04-08 17:58:45 +02:00
Stefan Zermatten
e89d415e7e Fixed error handling of compute engine crashes 2022-03-09 08:24:55 +02:00
Stefan Zermatten
ac8f19bcfb Hotfix for skills without effects breaking calculations 2022-03-09 08:09:16 +02:00
Stefan Zermatten
788cbb182d Action system improvements
- Actions/spells now display their summary, not their description
- All save branches and attack branches run when there are no targets
- Improved action logging
- Index branch lets you customise a choice of children to run
2022-03-09 01:31:09 +02:00
Stefan Zermatten
c68667be9c Added data validation diagnostics for offline use 2022-03-08 15:04:51 +02:00
Stefan Zermatten
fada07e048 Improved handling of poorly migrated archive creatures 2022-03-08 14:12:11 +02:00
Stefan Zermatten
12fc9b1be3 Added summary field back to spell form 2022-03-08 13:17:39 +02:00
Stefan Zermatten
e7f718c785 Prevented updates from running on the server if they fail client validation 2022-03-08 13:15:48 +02:00
Stefan Zermatten
9732db8d67 Hid damage multiplier card if there are no multipliers 2022-03-05 18:56:34 +02:00
Stefan Zermatten
73ca6dc364 Damage multipliers are now applied to damage dealt 2022-03-05 18:40:18 +02:00
Stefan Zermatten
10242b596f Updated test cases to account for new damage multipliers 2022-03-05 17:59:50 +02:00
Stefan Zermatten
782f2cdc73 Added default tags to properties
#type, damageType, skillType, attributeType, reset
2022-03-05 17:52:15 +02:00
Stefan Zermatten
a8ebf6a1de Tags now wrap in damage multiplier viewer 2022-03-05 17:48:52 +02:00
Stefan Zermatten
7dcd0aeff2 Fixed single-select combobox not showing rules errors 2022-03-05 16:30:53 +02:00
Stefan Zermatten
a19e7d0514 Prevented errors from crashing archive restoration 2022-03-05 16:24:42 +02:00
Stefan Zermatten
2442ae4fa0 Overhauled damage multipliers UX
Form and viewer revamp
custom damage types
Variables: `bludgeoning.resistance`
2022-03-05 16:23:21 +02:00
Stefan Zermatten
545050cfa3 Fixed attack disadvantage being treated as advantage 2022-03-04 16:09:00 +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
744 changed files with 36799 additions and 16837 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
build

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

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.

1
app/.gitignore vendored
View File

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

View File

@@ -3,30 +3,30 @@
# 'meteor add' and 'meteor remove' will edit this file for you,
# but you can also edit it by hand.
accounts-password@2.2.0
random@1.2.0
underscore@1.0.10
zegenie:redis-oplog
accounts-password@2.3.4
random@1.2.1
underscore@1.0.13
dburles:mongo-collection-instances
accounts-google@1.4.0
email@2.2.0
email@2.2.5
meteor-base@1.5.1
mobile-experience@1.1.0
mongo@1.14.6
session@1.2.0
tracker@1.2.0
logging@1.3.1
mongo@1.16.6
session@1.2.1
tracker@1.3.2
logging@1.3.2
reload@1.3.1
ejson@1.1.1
check@1.3.1
standard-minifier-js@2.8.0
ejson@1.1.3
check@1.3.2
standard-minifier-js@2.8.1
shell-server@0.5.0
ecmascript@0.16.1
ecmascript@0.16.7
es5-shim@4.8.0
percolate:synced-cron
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.1
dynamic-import@0.7.3
ddp-rate-limiter@1.2.0
rate-limit@1.1.1
mdg:validated-method
static-html@1.3.2
aldeed:collection2
@@ -38,12 +38,16 @@ 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
ostrio:files
simple:rest-bearer-token-parser
simple:rest-json-error-handler
littledata:synced-cron
#mdg:meteor-apm-agent
typescript@4.9.4
seba:minifiers-autoprefixer
mixmax:smart-disconnect

View File

@@ -1 +1 @@
METEOR@2.6.1
METEOR@2.12

View File

@@ -1,7 +1,7 @@
accounts-base@2.2.1
accounts-base@2.2.8
accounts-google@1.4.0
accounts-oauth@1.4.0
accounts-password@2.2.0
accounts-oauth@1.4.2
accounts-password@2.3.4
accounts-patreon@0.1.0
akryum:npm-check@0.1.2
akryum:vue-component@0.15.2
@@ -12,72 +12,74 @@ aldeed:collection2@3.5.0
aldeed:schema-index@3.0.0
allow-deny@1.1.1
autoupdate@1.8.0
babel-compiler@7.8.1
babel-runtime@1.5.0
babel-compiler@7.10.4
babel-runtime@1.5.1
base64@1.0.12
binary-heap@1.0.11
blaze-tools@1.1.2
blaze-tools@1.1.3
boilerplate-generator@1.7.1
bozhao:link-accounts@2.4.0
bozhao:link-accounts@2.6.1
caching-compiler@1.2.2
caching-html-compiler@1.2.1
callback-hook@1.4.0
check@1.3.1
callback-hook@1.5.1
check@1.3.2
coffeescript@2.4.1
coffeescript-compiler@2.4.1
dburles:mongo-collection-instances@0.3.6
ddp@1.4.0
ddp-client@2.5.0
ddp@1.4.1
ddp-client@2.6.1
ddp-common@1.4.0
ddp-rate-limiter@1.1.0
ddp-server@2.5.0
diff-sequence@1.1.1
dynamic-import@0.7.2
ecmascript@0.16.1
ecmascript-runtime@0.8.0
ddp-rate-limiter@1.2.0
ddp-server@2.6.1
diff-sequence@1.1.2
dynamic-import@0.7.3
ecmascript@0.16.7
ecmascript-runtime@0.8.1
ecmascript-runtime-client@0.12.1
ecmascript-runtime-server@0.11.0
ejson@1.1.1
email@2.2.0
ejson@1.1.3
email@2.2.5
es5-shim@4.8.0
fetch@0.1.1
geojson-utils@1.0.10
google-oauth@1.4.1
fetch@0.1.3
geojson-utils@1.0.11
google-oauth@1.4.3
hot-code-push@1.0.4
html-tools@1.1.2
html-tools@1.1.3
htmljs@1.1.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
littledata:synced-cron@1.5.1
localstorage@1.2.0
logging@1.3.1
mdg:validated-method@1.2.0
meteor@1.10.0
logging@1.3.2
mdg:validated-method@1.3.0
meteor@1.11.2
meteor-base@1.5.1
meteortesting:browser-tests@1.3.5
meteortesting:mocha@2.0.3
meteortesting:browser-tests@1.4.2
meteortesting:mocha@2.1.0
meteortesting:mocha-core@8.1.2
mikowals:batch-insert@1.3.0
minifier-css@1.6.0
minifier-js@2.7.3
minimongo@1.8.0
minifier-css@1.6.4
minifier-js@2.7.5
minimongo@1.9.3
mixmax:smart-disconnect@0.0.5
mobile-experience@1.1.0
mobile-status-bar@1.1.0
modern-browsers@0.1.7
modules@0.18.0
modules-runtime@0.12.0
mongo@1.14.6
mongo-decimal@0.1.2
modern-browsers@0.1.9
modules@0.19.0
modules-runtime@0.13.1
mongo@1.16.6
mongo-decimal@0.1.3
mongo-dev-server@1.1.0
mongo-id@1.0.8
npm-mongo@4.3.1
oauth@2.1.1
oauth2@1.3.1
npm-mongo@4.16.0
oauth@2.2.0
oauth2@1.3.2
ordered-dict@1.1.0
ostrio:cookies@2.7.0
ostrio:files@2.0.1
ostrio:cookies@2.7.2
ostrio:files@2.3.3
patreon-oauth@0.1.0
peerlibrary:assert@0.3.0
peerlibrary:check-extension@0.7.0
@@ -89,21 +91,20 @@ peerlibrary:reactive-mongo@0.4.1
peerlibrary:reactive-publish@0.10.0
peerlibrary:server-autorun@0.8.0
peerlibrary:subscription-data@0.8.0
percolate:migrations@1.0.3
percolate:synced-cron@1.3.2
promise@0.12.0
percolate:migrations@1.1.0
promise@0.12.2
raix:eventemitter@1.0.0
random@1.2.0
rate-limit@1.0.9
react-fast-refresh@0.2.2
reactive-dict@1.3.0
reactive-var@1.0.11
random@1.2.1
rate-limit@1.1.1
react-fast-refresh@0.2.7
reactive-dict@1.3.1
reactive-var@1.0.12
reload@1.3.1
retry@1.1.0
routepolicy@1.1.1
seba:minifiers-autoprefixer@2.0.1
service-configuration@1.3.0
session@1.2.0
service-configuration@1.3.1
session@1.2.1
sha@1.0.9
shell-server@0.5.0
simple:json-routes@2.3.1
@@ -111,16 +112,17 @@ simple:rest@1.2.1
simple:rest-bearer-token-parser@1.1.1
simple:rest-json-error-handler@1.1.1
simple:rest-method-mixin@1.1.0
socket-stream-client@0.4.0
spacebars-compiler@1.3.0
standard-minifier-js@2.8.0
socket-stream-client@0.5.1
spacebars-compiler@1.3.1
standard-minifier-js@2.8.1
static-html@1.3.2
templating-tools@1.2.1
templating-tools@1.2.2
tmeasday:check-npm-versions@1.0.2
tracker@1.2.0
typescript@4.4.1
underscore@1.0.10
tracker@1.3.2
typescript@4.9.4
underscore@1.0.13
url@1.3.2
webapp@1.13.1
webapp-hashing@1.1.0
webapp@1.13.5
webapp-hashing@1.1.1
zegenie:redis-oplog@2.0.16
zer0th:meteor-vuetify-loader@0.1.41

View File

@@ -2,38 +2,38 @@
<link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/@mdi/font@5.x/css/materialdesignicons.min.css" rel="stylesheet">
<meta name="viewport" content="width=device-width initial-scale=1.0, user-scalable=no">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<link rel="apple-touch-icon" sizes="57x57" href="/apple-touch-icon-57x57.png?v=lk6WXp6Pmj">
<link rel="apple-touch-icon" sizes="60x60" href="/apple-touch-icon-60x60.png?v=lk6WXp6Pmj">
<link rel="apple-touch-icon" sizes="72x72" href="/apple-touch-icon-72x72.png?v=lk6WXp6Pmj">
<link rel="apple-touch-icon" sizes="76x76" href="/apple-touch-icon-76x76.png?v=lk6WXp6Pmj">
<link rel="apple-touch-icon" sizes="114x114" href="/apple-touch-icon-114x114.png?v=lk6WXp6Pmj">
<link rel="apple-touch-icon" sizes="120x120" href="/apple-touch-icon-120x120.png?v=lk6WXp6Pmj">
<link rel="apple-touch-icon" sizes="144x144" href="/apple-touch-icon-144x144.png?v=lk6WXp6Pmj">
<link rel="apple-touch-icon" sizes="152x152" href="/apple-touch-icon-152x152.png?v=lk6WXp6Pmj">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon-180x180.png?v=lk6WXp6Pmj">
<link rel="icon" type="image/png" href="/favicon-32x32.png?v=lk6WXp6Pmj" sizes="32x32">
<link rel="icon" type="image/png" href="/favicon-194x194.png?v=lk6WXp6Pmj" sizes="194x194">
<link rel="icon" type="image/png" href="/favicon-96x96.png?v=lk6WXp6Pmj" sizes="96x96">
<link rel="icon" type="image/png" href="/android-chrome-192x192.png?v=lk6WXp6Pmj" sizes="192x192">
<link rel="icon" type="image/png" href="/favicon-16x16.png?v=lk6WXp6Pmj" sizes="16x16">
<link rel="manifest" href="/manifest.json?v=lk6WXp6Pmj">
<link rel="shortcut icon" href="/favicon.ico?v=lk6WXp6Pmj">
<meta name="msapplication-TileColor" content="#b91d1d">
<meta name="msapplication-TileImage" content="/mstile-144x144.png?v=lk6WXp6Pmj">
<meta name="theme-color" content="#d12929">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<link rel="apple-touch-icon" sizes="57x57" href="/apple-touch-icon-57x57.png?v=lk6WXp6Pmj">
<link rel="apple-touch-icon" sizes="60x60" href="/apple-touch-icon-60x60.png?v=lk6WXp6Pmj">
<link rel="apple-touch-icon" sizes="72x72" href="/apple-touch-icon-72x72.png?v=lk6WXp6Pmj">
<link rel="apple-touch-icon" sizes="76x76" href="/apple-touch-icon-76x76.png?v=lk6WXp6Pmj">
<link rel="apple-touch-icon" sizes="114x114" href="/apple-touch-icon-114x114.png?v=lk6WXp6Pmj">
<link rel="apple-touch-icon" sizes="120x120" href="/apple-touch-icon-120x120.png?v=lk6WXp6Pmj">
<link rel="apple-touch-icon" sizes="144x144" href="/apple-touch-icon-144x144.png?v=lk6WXp6Pmj">
<link rel="apple-touch-icon" sizes="152x152" href="/apple-touch-icon-152x152.png?v=lk6WXp6Pmj">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon-180x180.png?v=lk6WXp6Pmj">
<link rel="icon" type="image/png" href="/favicon-32x32.png?v=lk6WXp6Pmj" sizes="32x32">
<link rel="icon" type="image/png" href="/favicon-194x194.png?v=lk6WXp6Pmj" sizes="194x194">
<link rel="icon" type="image/png" href="/favicon-96x96.png?v=lk6WXp6Pmj" sizes="96x96">
<link rel="icon" type="image/png" href="/android-chrome-192x192.png?v=lk6WXp6Pmj" sizes="192x192">
<link rel="icon" type="image/png" href="/favicon-16x16.png?v=lk6WXp6Pmj" sizes="16x16">
<link rel="manifest" href="/manifest.json?v=lk6WXp6Pmj">
<link rel="shortcut icon" href="/favicon.ico?v=lk6WXp6Pmj">
<meta name="msapplication-TileColor" content="#b91d1d">
<meta name="msapplication-TileImage" content="/mstile-144x144.png?v=lk6WXp6Pmj">
<meta name="theme-color" content="#d12929">
<style type="text/css" media="print">
@page {
margin: 0mm;
}
html {
margin: 0px;
}
* {
-webkit-transition: none !important;
transition: none !important;
}
</style>
<style type="text/css" media="print">
@page {
margin: 0mm;
}
html {
margin: 0px;
}
* {
-webkit-transition: none !important;
transition: none !important;
}
</style>
</head>

View File

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

View File

@@ -0,0 +1,27 @@
import SimpleSchema from 'simpl-schema';
// Actions are creature actions that have been partially executed and not yet resolved
// They require some user input to progress
let Actions = new Mongo.Collection('actions');
let CreaturePropertySchema = new SimpleSchema({
_id: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
// Which creature is taking the action
_creatureId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
// The user who began taking the action
user: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
// The property that is about to be applied
property: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
});

View File

@@ -1,17 +1,58 @@
import { createS3FilesCollection } from '/imports/api/files/s3FileStorage.js';
import SimpleSchema from 'simpl-schema';
import { incrementFileStorageUsed } from '/imports/api/users/methods/updateFileStorageUsed.js';
import { CreaturePropertySchema } from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { CreatureSchema } from '/imports/api/creature/creatures/Creatures.js';
let createS3FilesCollection;
if (Meteor.isServer) {
createS3FilesCollection = require('/imports/api/files/server/s3FileStorage.js').createS3FilesCollection
} else {
createS3FilesCollection = require('/imports/api/files/client/s3FileStorage.js').createS3FilesCollection
}
const ArchiveCreatureFiles = createS3FilesCollection({
collectionName: 'archiveCreatureFiles',
storagePath: Meteor.isDevelopment ? '/DiceCloud/archiveCreatures/' : 'assets/app/archiveCreatures',
storagePath: Meteor.isDevelopment ? '../../../../../fileStorage/archiveCreatures' : 'assets/app/archiveCreatures',
onBeforeUpload(file) {
// Allow upload files under 10MB, and only in json format
if (file.size > 10485760) {
return 'Please upload with size equal or less than 10MB';
}
if (!/json/i.test(file.extension)){
if (!/json/i.test(file.extension)) {
return 'Please upload only a JSON file';
}
return true;
},
onAfterUpload(file) {
if (Meteor.isServer) incrementFileStorageUsed(file.userId, file.size);
}
});
let archiveSchema = new SimpleSchema({
meta: {
type: Object,
blackbox: true,
},
creature: CreatureSchema,
properties: {
type: Array,
},
'properties.$': CreaturePropertySchema,
experiences: {
type: Array,
},
'experiences.$': {
type: Object,
blackbox: true,
},
logs: {
type: Array,
},
'logs.$': {
type: Object,
blackbox: true,
},
});
export default ArchiveCreatureFiles;
export { archiveSchema };

View File

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

View File

@@ -68,7 +68,7 @@ const archiveCreatureToFile = new ValidatedMethod({
async run({creatureId}) {
assertOwnership(creatureId, this.userId);
if (Meteor.isServer){
archiveCreature(creatureId);
archiveCreature(creatureId, this.userId);
} else {
removeCreatureWork(creatureId);
}

View File

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

View File

@@ -0,0 +1,40 @@
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';
const removeArchiveCreature = new ValidatedMethod({
name: 'ArchiveCreatureFiles.methods.removeArchiveCreature',
validate: new SimpleSchema({
'fileId': {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
async run({ fileId }) {
// fetch the file
const file = ArchiveCreatureFiles.findOne({ _id: fileId }).get();
if (!file) {
throw new Meteor.Error('File not found',
'The requested creature archive does not exist');
}
// Assert ownership
const userId = file?.userId;
if (!userId || userId !== this.userId) {
throw new Meteor.Error('Permission denied',
'You can only restore creatures you own');
}
//Remove the archive once the restore succeeded
ArchiveCreatureFiles.remove({ _id: fileId });
// Update the user's file storage limits
incrementFileStorageUsed(userId, -file.size);
},
});
export default removeArchiveCreature;

View File

@@ -8,13 +8,17 @@ 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';
let migrateArchive;
if (Meteor.isServer){
migrateArchive = require('/imports/migrations/server/migrateArchive.js').default;
if (Meteor.isServer) {
migrateArchive = require('/imports/migrations/archive/migrateArchive.js').default;
}
function restoreCreature(archive){
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.')
}
@@ -22,18 +26,31 @@ function restoreCreature(archive){
// Migrate and verify the archive meets the current schema
migrateArchive(archive);
// Asset that the archive is safe
verifyArchiveSafety(archive);
// Don't upload creatures twice
const existingCreature = Creatures.findOne(archive.creature._id, {
fields: { _id: 1 }
});
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;
// Insert the creature sub documents
// They still have their original _id's
Creatures.insert(archive.creature);
try {
// Add all the properties
if (archive.properties && archive.properties.length){
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) {
@@ -56,26 +73,31 @@ 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');
}
if (Meteor.isServer){
assertHasCharactersSlots(this.userId);
if (Meteor.isServer) {
// Read the file data
const archive = await ArchiveCreatureFiles.readJSONFile(file);
restoreCreature(archive);
restoreCreature(archive, this.userId);
}
//Remove the archive once the restore succeeded
ArchiveCreatureFiles.remove({_id: fileId});
ArchiveCreatureFiles.remove({ _id: fileId });
// Update the user's file storage limits
incrementFileStorageUsed(userId, -file.size);
},
});

View File

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

View File

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

View File

@@ -4,25 +4,25 @@ import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
let CreatureFolders = new Mongo.Collection('creatureFolders');
let creatureFolderSchema = new SimpleSchema({
name: {
type: String,
trim: false,
optional: true,
name: {
type: String,
trim: false,
optional: true,
max: STORAGE_LIMITS.name,
},
creatures: {
type: Array,
defaultValue: [],
},
'creatures.$': {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
owner: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
creatures: {
type: Array,
defaultValue: [],
},
'creatures.$': {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
owner: {
type: String,
regEx: SimpleSchema.RegEx.Id,
index: 1,
},
},
archived: {
type: Boolean,
optional: true,

View File

@@ -18,23 +18,23 @@ let CreaturePropertySchema = new SimpleSchema({
type: String,
optional: true,
},
type: {
type: {
type: String,
allowedValues: Object.keys(propertySchemasIndex),
},
tags: {
type: Array,
defaultValue: [],
tags: {
type: Array,
defaultValue: [],
maxCount: STORAGE_LIMITS.tagCount,
},
'tags.$': {
type: String,
},
'tags.$': {
type: String,
max: STORAGE_LIMITS.tagLength,
},
disabled: {
type: Boolean,
optional: true,
},
},
disabled: {
type: Boolean,
optional: true,
},
icon: {
type: storedIconsSchema,
optional: true,
@@ -46,6 +46,12 @@ let CreaturePropertySchema = new SimpleSchema({
regEx: SimpleSchema.RegEx.Id,
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 = new SimpleSchema({
@@ -82,28 +88,37 @@ const DenormalisedOnlyCreaturePropertySchema = new SimpleSchema({
index: 1,
removeBeforeCompute: true,
},
deactivatingToggleId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
optional: true,
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);
for (let key in propertySchemasIndex) {
let schema = new SimpleSchema({});
schema.extend(propertySchemasIndex[key]);
schema.extend(CreaturePropertySchema);
schema.extend(ColorSchema);
schema.extend(ChildSchema);
schema.extend(SoftRemovableSchema);
CreatureProperties.attachSchema(schema, {
selector: {type: key}
});
schema.extend(ChildSchema);
schema.extend(SoftRemovableSchema);
CreatureProperties.attachSchema(schema, {
selector: { type: key }
});
}
import '/imports/api/creature/creatureProperties/methods/index.js';
//import '/imports/api/creature/actions/doAction.js';
//import '/imports/api/creature/actions/castSpellWithSlot.js';
export default CreatureProperties;
export {
DenormalisedOnlyCreaturePropertySchema,
CreaturePropertySchema,
CreaturePropertySchema,
};

View File

@@ -4,7 +4,6 @@ import SimpleSchema from 'simpl-schema';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import computeCreature from '/imports/api/engine/computeCreature.js';
const adjustQuantity = new ValidatedMethod({
name: 'creatureProperties.adjustQuantity',
@@ -21,43 +20,40 @@ const adjustQuantity = new ValidatedMethod({
numRequests: 5,
timeInterval: 5000,
},
run({_id, operation, value}) {
run({ _id, operation, value }) {
// Permissions
let property = CreatureProperties.findOne(_id);
let property = CreatureProperties.findOne(_id);
let rootCreature = getRootCreatureAncestor(property);
assertEditPermission(rootCreature, this.userId);
assertEditPermission(rootCreature, this.userId);
// Do work
adjustQuantityWork({property, operation, value});
// Changing quantity does not change dependencies, but recomputing the
// inventory changes many deps at once, so recompute fully
computeCreature(rootCreature._id);
adjustQuantityWork({ property, operation, value });
},
});
export function adjustQuantityWork({property, operation, value}){
export function adjustQuantityWork({ property, operation, value }) {
// Check if property has quantity
let schema = CreatureProperties.simpleSchema(property);
if (!schema.allowsKey('quantity')){
if (!schema.allowsKey('quantity')) {
throw new Meteor.Error(
'Adjust quantity failed',
`Property of type "${property.type}" doesn't have a quantity`
);
}
if (operation === 'set'){
if (operation === 'set') {
CreatureProperties.update(property._id, {
$set: {quantity: value}
$set: { quantity: value, dirty: true }
}, {
selector: property
});
} else if (operation === 'increment'){
} else if (operation === 'increment') {
// value here is 'damage'
value = -value;
let currentQuantity = property.quantity;
if (currentQuantity + value < 0) value = -currentQuantity;
CreatureProperties.update(property._id, {
$inc: {quantity: value}
$inc: { quantity: value },
$set: { dirty: true }
}, {
selector: property
});

View File

@@ -0,0 +1,189 @@
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 {
assertEditPermission,
assertDocEditPermission,
assertCopyPermission
} from '/imports/api/sharing/sharingPermissions.js';
import {
setLineageOfDocs,
getAncestry,
renewDocIds
} from '/imports/api/parenting/parenting.js';
import { reorderDocs } from '/imports/api/parenting/order.js';
import { setDocToLastOrder } from '/imports/api/parenting/order.js';
import Libraries from '/imports/api/library/Libraries.js';
const DUPLICATE_CHILDREN_LIMIT = 500;
const copyPropertyToLibrary = new ValidatedMethod({
name: 'creatureProperties.copyPropertyToLibrary',
validate: new SimpleSchema({
propId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
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
let { parentDoc, ancestors } = getAncestry({ parentRef });
// Check permission to edit the destination
let rootLibrary;
if (parentRef.collection === 'libraries') {
rootLibrary = parentDoc;
} else if (parentRef.collection === 'libraryNodes') {
rootLibrary = Libraries.findOne(parentDoc.ancestors[0].id)
} else {
throw `${parentRef.collection} is not a valid parent collection`
}
assertEditPermission(rootLibrary, this.userId);
const insertedRootNode = insertNodeFromProperty(propId, ancestors, order, this);
// Tree structure changed by inserts, reorder the tree
reorderDocs({
collection: LibraryNodes,
ancestorId: rootLibrary._id,
});
// Return the docId of the inserted root property
return insertedRootNode?._id;
},
});
function insertNodeFromProperty(propId, ancestors, 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 oldParent = prop.parent;
const propCursor = CreatureProperties.find({
'ancestors.id': propId,
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);
// re-map all the ancestors
setLineageOfDocs({
docArray: props,
newAncestry: ancestors,
oldParent,
});
// Give the docs new IDs without breaking internal references
renewDocIds({
docArray: props,
collectionMap: { 'creatureProperties': 'libraryNodes' }
});
// Order the root node
if (order === undefined) {
setDocToLastOrder({
collection: LibraryNodes,
doc: prop,
});
} else {
prop.order = order;
}
// Insert the props as library nodes
LibraryNodes.batchInsert(props);
return prop;
}
/**
*
* @param {[Property]} props The properties to check
* @param {String} 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: { ancestors: 1 }
}).forEach(node => {
sourceLibIds.add(node.ancestors?.[0]?.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 default copyPropertyToLibrary;

View File

@@ -1,53 +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 Creatures from '/imports/api/creature/creatures/Creatures.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
const damagePropertiesByName = new ValidatedMethod({
name: 'CreatureProperties.damagePropertiesByName',
validate: new SimpleSchema({
creatureId: SimpleSchema.RegEx.Id,
variableName: {
type: String,
},
operation: {
type: String,
allowedValues: ['set', 'increment']
},
value: Number,
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 20,
timeInterval: 5000,
},
run({creatureId, variableName, operation, value}) {
// Check permissions
let creature = Creatures.findOne(creatureId, {
fields: {
variables: 1,
owner: 1,
readers: 1,
writers: 1,
},
});
assertEditPermission(creature, this.userId);
CreatureProperties.find({
'ancestors.id': creatureId,
variableName,
removed: {$ne: false},
inactive: {$ne: true},
}).forEach(property => {
// Check if property can take damage
let schema = CreatureProperties.simpleSchema(property);
if (!schema.allowsKey('damage')) return;
// Damage the property
damagePropertyWork({property, operation, value});
});
}
});
export default damagePropertiesByName;

View File

@@ -2,9 +2,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 { computeCreatureDependencyGroup } from '/imports/api/engine/computeCreature.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',
@@ -21,60 +21,121 @@ const damageProperty = new ValidatedMethod({
numRequests: 20,
timeInterval: 5000,
},
run({_id, operation, value}) {
// Check permissions
let property = CreatureProperties.findOne(_id);
if (!property) throw new Meteor.Error(
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'
);
let rootCreature = getRootCreatureAncestor(property);
assertEditPermission(rootCreature, this.userId);
// Check if property can take damage
let schema = CreatureProperties.simpleSchema(property);
if (!schema.allowsKey('damage')){
throw new Meteor.Error(
'Damage property failed',
`Property of type "${property.type}" can't be damaged`
);
}
let result = damagePropertyWork({property, operation, value});
// Dependencies can't be changed through damage, only recompute deps
computeCreatureDependencyGroup(property);
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({property, operation, value}){
let damage, newValue;
if (operation === 'set'){
const total = property.total || 0;
export function damagePropertyWork({ prop, operation, value, actionContext, logFunction }) {
// 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: -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']?.value;
} else {
value = -actionContext.scope['~healing']?.value;
}
} else {
value = actionContext.scope['~set']?.value;
}
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) damage = total;
if (damage > total && !prop.ignoreLowerLimit) damage = total;
// Damage must be positive
if (damage < 0) damage = 0;
newValue = property.total - damage;
} else if (operation === 'increment'){
let currentValue = property.value || 0;
let currentDamage = property.damage || 0;
let increment = value;
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;
logFunction?.(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) increment = currentValue;
if (increment > currentValue && !prop.ignoreLowerLimit) increment = currentValue;
// Can't decrease damage below zero
if (-increment > currentDamage) increment = -currentDamage;
if (-increment > currentDamage && !prop.ignoreUpperLimit) increment = -currentDamage;
damage = currentDamage + increment;
newValue = property.total - damage;
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;
logFunction?.(increment);
}
// Write the results
CreatureProperties.update(property._id, {
$set: {damage, value: newValue}
}, {
selector: property
});
return damage;
applyTriggers(actionContext.triggers?.damageProperty?.after, prop, actionContext);
if (operation === 'set') {
return damage;
} else if (operation === 'increment') {
return increment;
}
}
export default damageProperty;

View File

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

View File

@@ -5,15 +5,14 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import {
setLineageOfDocs,
renewDocIds
setLineageOfDocs,
renewDocIds
} from '/imports/api/parenting/parenting.js';
import { reorderDocs } from '/imports/api/parenting/order.js';
import computeCreature from '/imports/api/engine/computeCreature.js';
var snackbar;
if (Meteor.isClient){
if (Meteor.isClient) {
snackbar = require(
'/imports/ui/components/snackbars/SnackbarQueue.js'
'/imports/client/ui/components/snackbars/SnackbarQueue.js'
).snackbar
}
@@ -32,7 +31,7 @@ const duplicateProperty = new ValidatedMethod({
numRequests: 5,
timeInterval: 5000,
},
run({_id}) {
run({ _id }) {
let property = CreatureProperties.findOne(_id);
let creature = getRootCreatureAncestor(property);
@@ -43,19 +42,24 @@ const duplicateProperty = new ValidatedMethod({
let 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,
removed: {$ne: true},
}, {
'ancestors.id': _id,
removed: { $ne: true },
}, {
limit: DUPLICATE_CHILDREN_LIMIT + 1,
sort: {order: 1},
sort: { order: 1 },
}).fetch();
// Alert the user if the limit was hit
if (nodes.length > DUPLICATE_CHILDREN_LIMIT){
if (nodes.length > DUPLICATE_CHILDREN_LIMIT) {
nodes.pop();
if (Meteor.isClient){
if (Meteor.isClient) {
snackbar({
text: `Only the first ${DUPLICATE_CHILDREN_LIMIT} children were duplicated`,
});
@@ -64,22 +68,25 @@ const duplicateProperty = new ValidatedMethod({
// re-map all the ancestors
setLineageOfDocs({
docArray: nodes,
newAncestry : [
docArray: nodes,
newAncestry: [
...property.ancestors,
{id: propertyId, collection: 'creatureProperties'}
{ id: propertyId, collection: 'creatureProperties' }
],
oldParent : {id: _id, collection: 'creatureProperties'},
});
oldParent: { id: _id, collection: 'creatureProperties' },
});
// Give the docs new IDs without breaking internal references
renewDocIds({docArray: nodes});
renewDocIds({ docArray: nodes });
// Order the root node
property.order += 0.5;
// Mark the sheet as needing recompute
property.dirty = true;
// Insert the properties
CreatureProperties.batchInsert([property, ...nodes]);
CreatureProperties.batchInsert([property, ...nodes]);
// Tree structure changed by inserts, reorder the tree
reorderDocs({
@@ -87,9 +94,6 @@ const duplicateProperty = new ValidatedMethod({
ancestorId: property.ancestors[0].id,
});
// Inserting a creature property invalidates dependencies: full recompute
computeCreature(creature._id);
return propertyId;
},
});

View File

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

View File

@@ -3,28 +3,27 @@ import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import computeCreature from '/imports/api/engine/computeCreature.js';
const flipToggle = new ValidatedMethod({
name: 'creatureProperties.flipToggle',
validate({_id}){
if (!_id) throw new Meteor.Error('No _id', '_id is required');
validate({ _id }) {
if (!_id) throw new Meteor.Error('No _id', '_id is required');
},
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({_id}) {
run({ _id }) {
// Permission
let property = CreatureProperties.findOne(_id, {
fields: {type: 1, ancestors: 1, enabled: 1, disabled: 1}
fields: { type: 1, ancestors: 1, enabled: 1, disabled: 1 }
});
if (property.type !== 'toggle'){
if (property.type !== 'toggle') {
throw new Meteor.Error('wrong property',
'This method can only be applied to toggles');
}
if (!property.enabled && !property.disabled){
if (!property.enabled && !property.disabled) {
throw new Meteor.Error('Computed toggle',
'Can\'t flip a toggle that is computed')
}
@@ -33,15 +32,15 @@ const flipToggle = new ValidatedMethod({
// Invert the current value, disabled is the canonical store of value
const currentValue = !property.disabled;
CreatureProperties.update(_id, {$set: {
enabled: !currentValue,
disabled: currentValue,
}}, {
selector: {type: 'toggle'},
});
// Updating a toggle is likely to change the whole tree, do a full recompute
computeCreature(rootCreature._id);
CreatureProperties.update(_id, {
$set: {
enabled: !currentValue,
disabled: currentValue,
dirty: true,
}
}, {
selector: { type: 'toggle' },
});
},
});

View File

@@ -1,43 +1,63 @@
export default function getSlotFillFilter({slot, libraryIds}){
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},
fillSlots: true,
removed: { $ne: true },
$and: []
};
if (libraryIds){
filter['ancestors.id'] = {$in: libraryIds};
}
if (slot.slotType){
filter['ancestors.id'] = { $in: libraryIds };
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',
}, {
slotFillerType: 'classLevel',
}]
});
if (slot.variableName) {
filter.variableName = slot.variableName;
}
// Only search for levels the class needs
if (slot.missingLevels && slot.missingLevels.length) {
filter.level = { $in: slot.missingLevels };
} else {
filter.level = { $gt: slot.level || 0 };
}
}
let tagsOr = [];
let tagsNor = [];
if (slot.slotTags && slot.slotTags.length){
tagsOr.push({tags: {$all: slot.slotTags}});
let tagsNin = [];
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'){
tagsNor.push({tags: {$all: extra.tags}});
if (extra.operation === 'OR') {
tagsOr.push({ libraryTags: { $all: extra.tags } });
} else if (extra.operation === 'NOT') {
tagsNin.push(...extra.tags);
}
});
}
if (tagsOr.length){
filter.$and.push({$or: tagsOr});
if (tagsOr.length) {
filter.$or = tagsOr;
}
if (tagsNor.length){
filter.$and.push({$nor: tagsNor});
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.js';
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'] }
}],
'ancestors.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'] } },
],
'ancestors.id': { $in: ['libraryId1', 'libraryId2'] },
removed: { $ne: true },
fillSlots: true,
});
});
});

View File

@@ -1,7 +1,6 @@
import '/imports/api/creature/creatureProperties/methods/adjustQuantity.js';
import '/imports/api/creature/creatureProperties/methods/damagePropertiesByName.js';
import '/imports/api/creature/creatureProperties/methods/copyPropertyToLibrary.js';
import '/imports/api/creature/creatureProperties/methods/damageProperty.js';
import '/imports/api/creature/creatureProperties/methods/dealDamage.js';
import '/imports/api/creature/creatureProperties/methods/duplicateProperty.js';
import '/imports/api/creature/creatureProperties/methods/equipItem.js';
import '/imports/api/creature/creatureProperties/methods/insertProperty.js';

View File

@@ -5,7 +5,6 @@ import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/ge
import SimpleSchema from 'simpl-schema';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import { reorderDocs } from '/imports/api/parenting/order.js';
import computeCreature from '/imports/api/engine/computeCreature.js';
import { getAncestry } from '/imports/api/parenting/parenting.js';
import getParentRefByTag from '/imports/api/creature/creatureProperties/methods/getParentRefByTag.js';
import { RefSchema } from '/imports/api/parenting/ChildSchema.js';
@@ -13,7 +12,7 @@ import { getHighestOrder } from '/imports/api/parenting/order.js';
const insertProperty = new ValidatedMethod({
name: 'creatureProperties.insert',
validate: new SimpleSchema({
validate: new SimpleSchema({
creatureProperty: {
type: Object,
blackbox: true,
@@ -25,25 +24,25 @@ const insertProperty = new ValidatedMethod({
numRequests: 5,
timeInterval: 5000,
},
run({creatureProperty, parentRef}) {
run({ creatureProperty, parentRef }) {
// get the new ancestry for the properties
let {parentDoc, ancestors} = getAncestry({parentRef});
let { parentDoc, ancestors } = getAncestry({ parentRef });
// Check permission to edit
// Check permission to edit
let rootCreature;
if (parentRef.collection === 'creatures'){
if (parentRef.collection === 'creatures') {
rootCreature = parentDoc;
} else if (parentRef.collection === 'creatureProperties'){
} else if (parentRef.collection === 'creatureProperties') {
rootCreature = getRootCreatureAncestor(parentDoc);
} else {
throw `${parentRef.collection} is not a valid parent collection`
}
} else {
throw `${parentRef.collection} is not a valid parent collection`
}
assertEditPermission(rootCreature, this.userId);
creatureProperty.parent = parentRef;
creatureProperty.ancestors = ancestors;
return insertPropertyWork({
return insertPropertyWork({
property: creatureProperty,
creature: rootCreature,
});
@@ -76,31 +75,31 @@ const insertPropertyAsChildOfTag = new ValidatedMethod({
numRequests: 5,
timeInterval: 5000,
},
run({creatureProperty, creatureId, tag, tagDefaultName}) {
run({ creatureProperty, creatureId, tag, tagDefaultName }) {
let parentRef = getParentRefByTag(creatureId, tag);
if (!parentRef){
if (!parentRef) {
// Use the creature as the parent and mark that we need to insert the folder first later
var insertFolderFirst = true;
parentRef = {id: creatureId, collection: 'creatures'};
parentRef = { id: creatureId, collection: 'creatures' };
}
// get the new ancestry for the properties
let {parentDoc, ancestors} = getAncestry({parentRef});
let { parentDoc, ancestors } = getAncestry({ parentRef });
// Check permission to edit
let rootCreature;
if (parentRef.collection === 'creatures'){
if (parentRef.collection === 'creatures') {
rootCreature = parentDoc;
} else if (parentRef.collection === 'creatureProperties'){
} else if (parentRef.collection === 'creatureProperties') {
rootCreature = getRootCreatureAncestor(parentDoc);
} else {
throw `${parentRef.collection} is not a valid parent collection`
}
} else {
throw `${parentRef.collection} is not a valid parent collection`
}
assertEditPermission(rootCreature, this.userId);
// Add the folder first if we need to
if (insertFolderFirst){
if (insertFolderFirst) {
let order = getHighestOrder({
collection: CreatureProperties,
ancestorId: parentRef.id,
@@ -114,7 +113,7 @@ const insertPropertyAsChildOfTag = new ValidatedMethod({
order,
});
// Make the folder our new parent
let newParentRef = {id, collection: 'creatureProperties'};
let newParentRef = { id, collection: 'creatureProperties' };
ancestors = [parentRef, newParentRef];
parentRef = newParentRef;
creatureProperty.order = order + 1;
@@ -123,23 +122,22 @@ const insertPropertyAsChildOfTag = new ValidatedMethod({
creatureProperty.parent = parentRef;
creatureProperty.ancestors = ancestors;
return insertPropertyWork({
return insertPropertyWork({
property: creatureProperty,
creature: rootCreature,
});
},
});
export function insertPropertyWork({property, creature}){
export function insertPropertyWork({ property, creature }) {
delete property._id;
property.dirty = true;
let _id = CreatureProperties.insert(property);
// Tree structure changed by insert, reorder the tree
reorderDocs({
collection: CreatureProperties,
ancestorId: creature._id,
});
// Inserting a creature property invalidates dependencies: full recompute
computeCreature(creature._id);
return _id;
}

View File

@@ -5,60 +5,59 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
import { RefSchema } from '/imports/api/parenting/ChildSchema.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import computeCreature from '/imports/api/engine/computeCreature.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import {
setLineageOfDocs,
getAncestry,
renewDocIds
setLineageOfDocs,
getAncestry,
renewDocIds
} from '/imports/api/parenting/parenting.js';
import { reorderDocs } from '/imports/api/parenting/order.js';
import { setDocToLastOrder } from '/imports/api/parenting/order.js';
import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
const insertPropertyFromLibraryNode = new ValidatedMethod({
name: 'creatureProperties.insertPropertyFromLibraryNode',
validate: new SimpleSchema({
name: 'creatureProperties.insertPropertyFromLibraryNode',
validate: new SimpleSchema({
nodeIds: {
type: Array,
max: 20,
},
'nodeIds.$': {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
parentRef: {
type: RefSchema,
},
'nodeIds.$': {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
parentRef: {
type: RefSchema,
},
order: {
type: Number,
optional: true,
},
}).validator(),
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({nodeIds, parentRef, order}) {
// get the new ancestry for the properties
let {parentDoc, ancestors} = getAncestry({parentRef});
run({ nodeIds, parentRef, order }) {
// get the new ancestry for the properties
let { parentDoc, ancestors } = getAncestry({ parentRef });
// Check permission to edit
// Check permission to edit
let rootCreature;
if (parentRef.collection === 'creatures'){
if (parentRef.collection === 'creatures') {
rootCreature = parentDoc;
} else if (parentRef.collection === 'creatureProperties'){
} else if (parentRef.collection === 'creatureProperties') {
rootCreature = getRootCreatureAncestor(parentDoc);
} else {
throw `${parentRef.collection} is not a valid parent collection`
}
} else {
throw `${parentRef.collection} is not a valid parent collection`
}
assertEditPermission(rootCreature, this.userId);
// {libraryId: hasViewPermission}
//let libraryPermissionMemoir = {};
let node;
nodeIds.forEach(nodeId => {
nodeIds.forEach(nodeId => {
// TODO: Check library view permission for each node before starting
node = insertPropertyFromNode(nodeId, ancestors, order);
});
@@ -71,21 +70,18 @@ const insertPropertyFromLibraryNode = new ValidatedMethod({
collection: CreatureProperties,
ancestorId: rootCreature._id,
});
// Inserting a creature property invalidates dependencies: full recompute
computeCreature(rootCreature._id);
// Return the docId of the last property, the inserted root property
return rootId;
},
// Return the docId of the last property, the inserted root property
return rootId;
},
});
function insertPropertyFromNode(nodeId, ancestors, order){
function insertPropertyFromNode(nodeId, ancestors, order) {
// Fetch the library node and its decendents, provided they have not been
// removed
// TODO: Check permission to read the library this node is in
let node = LibraryNodes.findOne({
_id: nodeId,
removed: {$ne: true},
removed: { $ne: true },
});
if (!node) {
if (Meteor.isClient) return;
@@ -99,16 +95,17 @@ function insertPropertyFromNode(nodeId, ancestors, order){
let oldParent = node.parent;
let nodes = LibraryNodes.find({
'ancestors.id': nodeId,
removed: {$ne: true},
removed: { $ne: true },
}).fetch();
// Convert all references into actual nodes
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);
// set libraryNodeIds
storeLibraryNodeReferences(nodes);
@@ -122,11 +119,11 @@ function insertPropertyFromNode(nodeId, ancestors, order){
// Give the docs new IDs without breaking internal references
renewDocIds({
docArray: nodes,
collectionMap: {'libraryNodes': 'creatureProperties'}
collectionMap: { 'libraryNodes': 'creatureProperties' }
});
// Order the root node
if (order === undefined){
if (order === undefined) {
setDocToLastOrder({
collection: CreatureProperties,
doc: node,
@@ -135,22 +132,30 @@ function insertPropertyFromNode(nodeId, ancestors, order){
node.order = order;
}
// Mark all nodes as dirty
dirtyNodes(nodes);
// Insert the creature properties
CreatureProperties.batchInsert(nodes);
return node;
}
function storeLibraryNodeReferences(nodes){
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){
function reifyNodeReferences(nodes, visitedRefs = new Set(), depth = 0) {
depth += 1;
// New nodes added this function
let newNodes = [];
@@ -161,9 +166,9 @@ function reifyNodeReferences(nodes, visitedRefs = new Set(), depth = 0){
if (node.type !== 'reference') return true;
// We have gone too deep, keep the reference node as an error
if (depth >= 10){
if (depth >= 10) {
if (Meteor.isClient) console.warn('Reference depth limit exceeded');
node.cache = {error: 'Reference depth limit exceeded'};
node.cache = { error: 'Reference depth limit exceeded' };
return true;
}
@@ -173,17 +178,17 @@ function reifyNodeReferences(nodes, visitedRefs = new Set(), depth = 0){
referencedNode.order = node.order;
// We are definitely replacing this node, so add it to the list
visitedRefs.add(node._id);
} catch (e){
node.cache = {error: e.reason || e.message || e.toString()};
} catch (e) {
node.cache = { error: e.reason || e.message || e.toString() };
return true;
}
// Get all the descendants of the referenced node
let descendents = LibraryNodes.find({
'ancestors.id': referencedNode._id,
removed: {$ne: true},
removed: { $ne: true },
}, {
sort: {order: 1},
sort: { order: 1 },
}).fetch();
// We are adding the referenced node and its descendants
@@ -191,20 +196,20 @@ function reifyNodeReferences(nodes, visitedRefs = new Set(), depth = 0){
// re-map all the ancestors to parent the new sub-tree into our existing
// node tree
setLineageOfDocs({
docArray: addedNodes,
newAncestry: node.ancestors,
oldParent: referencedNode.parent,
});
setLineageOfDocs({
docArray: addedNodes,
newAncestry: node.ancestors,
oldParent: referencedNode.parent,
});
// Filter all the looped references
addedNodes = addedNodes.filter(addedNode => {
// Add all non-reference nodes
if (addedNode.type !== 'reference'){
if (addedNode.type !== 'reference') {
return true;
}
// If this exact reference has already been resolved before, filter it out
if (visitedRefs.has(addedNode._id)){
if (visitedRefs.has(addedNode._id)) {
return false;
} else {
// Otherwise mark it as visited, and keep it

View File

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

View File

@@ -3,20 +3,19 @@ import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import computeCreature from '/imports/api/engine/computeCreature.js';
import { get } from 'lodash';
const pushToProperty = new ValidatedMethod({
name: 'creatureProperties.push',
validate: null,
name: 'creatureProperties.push',
validate: null,
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({_id, path, value}){
run({ _id, path, value }) {
// Permissions
let property = CreatureProperties.findOne(_id);
let property = CreatureProperties.findOne(_id);
let rootCreature = getRootCreatureAncestor(property);
assertEditPermission(rootCreature, this.userId);
@@ -26,10 +25,10 @@ const pushToProperty = new ValidatedMethod({
let schema = CreatureProperties.simpleSchema(property);
let maxCount = schema.get(joinedPath, 'maxCount');
if (Number.isFinite(maxCount)){
if (Number.isFinite(maxCount)) {
let array = get(property, path);
let currentCount = array ? array.length : 0;
if (currentCount >= maxCount){
if (currentCount >= maxCount) {
throw new Meteor.Error(
'Array is full',
`Cannot have more than ${maxCount} values`
@@ -38,15 +37,13 @@ const pushToProperty = new ValidatedMethod({
}
// Do work
CreatureProperties.update(_id, {
$push: {[joinedPath]: value},
}, {
selector: {type: property.type},
});
// TODO figure out if this method can change deps or not
computeCreature(rootCreature._id);
}
CreatureProperties.update(_id, {
$push: { [joinedPath]: value },
$set: { dirty: true },
}, {
selector: { type: property.type },
});
}
});
export default pushToProperty;

View File

@@ -5,30 +5,32 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import { restore } from '/imports/api/parenting/softRemove.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import computeCreature from '/imports/api/engine/computeCreature.js';
const restoreProperty = new ValidatedMethod({
name: 'creatureProperties.restore',
validate: new SimpleSchema({
_id: SimpleSchema.RegEx.Id
}).validator(),
name: 'creatureProperties.restore',
validate: new SimpleSchema({
_id: SimpleSchema.RegEx.Id
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({_id}){
run({ _id }) {
// Permissions
let property = CreatureProperties.findOne(_id);
let property = CreatureProperties.findOne(_id);
let rootCreature = getRootCreatureAncestor(property);
assertEditPermission(rootCreature, this.userId);
// Do work
restore({_id, collection: CreatureProperties});
// Changes dependency tree by restoring children
computeCreature(rootCreature._id);
}
restore({
_id,
collection: CreatureProperties,
extraUpdates: {
$set: { dirty: true }
},
});
}
});
export default restoreProperty;

View File

@@ -4,7 +4,6 @@ import SimpleSchema from 'simpl-schema';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import computeCreature from '/imports/api/engine/computeCreature.js';
const selectAmmoItem = new ValidatedMethod({
name: 'creatureProperties.selectAmmoItem',
@@ -18,34 +17,29 @@ const selectAmmoItem = new ValidatedMethod({
numRequests: 5,
timeInterval: 5000,
},
run({actionId, itemId, itemConsumedIndex}) {
run({ actionId, itemId, itemConsumedIndex }) {
// Permissions
let action = CreatureProperties.findOne(actionId);
let action = CreatureProperties.findOne(actionId);
let rootCreature = getRootCreatureAncestor(action);
assertEditPermission(rootCreature, this.userId);
assertEditPermission(rootCreature, this.userId);
// Check that this index has a document to edit
let itemConsumed = action.resources.itemsConsumed[itemConsumedIndex];
if (!itemConsumed){
if (!itemConsumed) {
throw new Meteor.Error('Resouce not found',
'Could not set ammo, because the ammo document was not found');
}
let itemToLink = CreatureProperties.findOne(itemId);
if (!itemToLink){
if (!itemToLink) {
throw new Meteor.Error('Item not found',
'Could not set ammo: the item was not found');
}
let path = `resources.itemsConsumed.${itemConsumedIndex}.itemId`;
CreatureProperties.update(actionId, {
$set: {[path]: itemId}
$set: { [path]: itemId, dirty: true }
}, {
selector: action,
});
// Changing the linked item does change the dependency tree
// TODO: We can predict exactly which deps will be affected instead of
// recomputing the entire creature
computeCreature(rootCreature._id);
},
});

View File

@@ -5,30 +5,26 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import { softRemove } from '/imports/api/parenting/softRemove.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import computeCreature from '/imports/api/engine/computeCreature.js';
const softRemoveProperty = new ValidatedMethod({
name: 'creatureProperties.softRemove',
validate: new SimpleSchema({
_id: SimpleSchema.RegEx.Id
}).validator(),
name: 'creatureProperties.softRemove',
validate: new SimpleSchema({
_id: SimpleSchema.RegEx.Id
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({_id}){
run({ _id }) {
// Permissions
let property = CreatureProperties.findOne(_id);
let property = CreatureProperties.findOne(_id);
let rootCreature = getRootCreatureAncestor(property);
assertEditPermission(rootCreature, this.userId);
// Do work
softRemove({_id, collection: CreatureProperties});
// Changes dependency tree by removing children
computeCreature(rootCreature._id);
}
softRemove({ _id, collection: CreatureProperties });
}
});
export default softRemoveProperty;

View File

@@ -3,32 +3,31 @@ import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import computeCreature from '/imports/api/engine/computeCreature.js';
const updateCreatureProperty = new ValidatedMethod({
name: 'creatureProperties.update',
validate({_id, path}){
if (!_id) throw new Meteor.Error('No _id', '_id is required');
// We cannot change these fields with a simple update
switch (path[0]){
case 'type':
validate({ _id, path }) {
if (!_id) throw new Meteor.Error('No _id', '_id is required');
// We cannot change these fields with a simple update
switch (path[0]) {
case 'type':
case 'order':
case 'parent':
case 'ancestors':
case 'damage':
throw new Meteor.Error('Permission denied',
'This property can\'t be updated directly');
}
case 'damage':
throw new Meteor.Error('Permission denied',
'This property can\'t be updated directly');
}
},
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({_id, path, value}) {
run({ _id, path, value }) {
// Permission
let property = CreatureProperties.findOne(_id, {
fields: {type: 1, ancestors: 1}
fields: { type: 1, ancestors: 1 }
});
let rootCreature = getRootCreatureAncestor(property);
assertEditPermission(rootCreature, this.userId);
@@ -36,18 +35,14 @@ const updateCreatureProperty = new ValidatedMethod({
let pathString = path.join('.');
let modifier;
// unset empty values
if (value === null || value === undefined){
modifier = {$unset: {[pathString]: 1}};
if (value === null || value === undefined) {
modifier = { $unset: { [pathString]: 1 }, $set: { dirty: true } };
} else {
modifier = {$set: {[pathString]: value}};
modifier = { $set: { [pathString]: value, dirty: true } };
}
CreatureProperties.update(_id, modifier, {
selector: {type: property.type},
});
// Updating a property is likely to change dependencies, do a full recompute
// denormalised stats might change, so fetch the creature again
computeCreature(rootCreature._id);
CreatureProperties.update(_id, modifier, {
selector: { type: property.type },
});
},
});

View File

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

View File

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

@@ -8,21 +8,26 @@ import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
let Creatures = new Mongo.Collection('creatures');
let CreatureSettingsSchema = new SimpleSchema({
//slowed down by carrying too much?
useVariantEncumbrance: {
type: Boolean,
optional: true,
},
//hide spellcasting tab
hideSpellcasting: {
type: Boolean,
optional: true,
},
// Swap around the modifier and stat
swapStatAndModifier: {
type: Boolean,
optional: true,
},
//slowed down by carrying too much?
useVariantEncumbrance: {
type: Boolean,
optional: true,
},
//hide spellcasting tab
hideSpellcasting: {
type: Boolean,
optional: true,
},
//hide rest buttons
hideRestButtons: {
type: Boolean,
optional: true,
},
// Swap around the modifier and stat
swapStatAndModifier: {
type: Boolean,
optional: true,
},
// Hide all the unused stats
hideUnusedStats: {
type: Boolean,
@@ -38,6 +43,11 @@ let CreatureSettingsSchema = new SimpleSchema({
type: Boolean,
optional: true,
},
// Hide calculation errors
hideCalculationErrors: {
type: Boolean,
optional: true,
},
// How much each hitDice resets on a long rest
hitDiceResetMultiplier: {
type: Number,
@@ -53,73 +63,94 @@ let CreatureSettingsSchema = new SimpleSchema({
});
let CreatureSchema = new SimpleSchema({
// Strings
name: {
type: String,
defaultValue: '',
optional: true,
// Strings
name: {
type: String,
defaultValue: '',
optional: true,
max: STORAGE_LIMITS.name,
},
alignment: {
type: String,
optional: true,
},
alignment: {
type: String,
optional: true,
max: STORAGE_LIMITS.name,
},
gender: {
type: String,
optional: true,
},
gender: {
type: String,
optional: true,
max: STORAGE_LIMITS.name,
},
picture: {
type: String,
optional: true,
},
picture: {
type: String,
optional: true,
max: STORAGE_LIMITS.url,
},
},
avatarPicture: {
type: String,
optional: true,
max: STORAGE_LIMITS.url,
},
// Mechanics
deathSave: {
type: deathSaveSchema,
defaultValue: {},
},
// Libraries
allowedLibraries: {
type: Array,
optional: true,
maxCount: 100,
},
'allowedLibraries.$': {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
allowedLibraryCollections: {
type: Array,
optional: true,
maxCount: 100,
},
'allowedLibraryCollections.$': {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
// Mechanics
deathSave: {
type: deathSaveSchema,
defaultValue: {},
},
// Stats that are computed and denormalised outside of recomputation
denormalizedStats: {
type: Object,
defaultValue: {},
},
// Sum of all XP gained by this character
'denormalizedStats.xp': {
type: SimpleSchema.Integer,
defaultValue: 0,
},
'denormalizedStats.xp': {
type: SimpleSchema.Integer,
defaultValue: 0,
},
// Sum of all levels granted by milestone XP
'denormalizedStats.milestoneLevels': {
type: SimpleSchema.Integer,
defaultValue: 0,
},
// Does the character need a recompute?
dirty: {
type: Boolean,
optional: true,
},
// Version of computation engine that was last used to compute this creature
computeVersion: {
type: String,
type: String,
optional: true,
},
type: {
type: String,
defaultValue: 'pc',
allowedValues: ['pc', 'npc', 'monster'],
},
},
type: {
type: String,
defaultValue: 'pc',
allowedValues: ['pc', 'npc', 'monster'],
},
damageMultipliers: {
type: Object,
blackbox: true,
defaultValue: {}
blackbox: true,
defaultValue: {}
},
variables: {
type: Object,
blackbox: true,
defaultValue: {}
},
computeErrors: {
type: Array,
optional: true,
@@ -130,9 +161,10 @@ let CreatureSchema = new SimpleSchema({
'computeErrors.$.type': {
type: String,
},
'computeErrors.$.details' : {
'computeErrors.$.details': {
type: Object,
blackbox: true,
optional: true,
},
// Tabletop
@@ -146,11 +178,11 @@ let CreatureSchema = new SimpleSchema({
optional: true,
},
// Settings
settings: {
type: CreatureSettingsSchema,
defaultValue: {},
},
// Settings
settings: {
type: CreatureSettingsSchema,
defaultValue: {},
},
});
CreatureSchema.extend(ColorSchema);
@@ -159,8 +191,8 @@ CreatureSchema.extend(SharingSchema);
Creatures.attachSchema(CreatureSchema);
import '/imports/api/creature/creatures/methods/index.js';
import '/imports/api/engine/actions/doAction.js';
export default Creatures;
export { CreatureSchema };
import '/imports/api/engine/actions/doAction.js';

View File

@@ -10,7 +10,7 @@ export default function defaultCharacterProperties(creatureId){
{
type: 'propertySlot',
name: 'Ruleset',
description: {text: 'Choose a starting point for your character, this will define the basic setup of your character sheet. Without a base, your sheet will be empty.'},
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'},

View File

@@ -0,0 +1,22 @@
import { getUserTier } from '/imports/api/users/patreon/tiers.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
export default function assertHasCharactersSlots(userId) {
if (characterSlotsRemaining(userId) <= 0) {
throw new Meteor.Error('characterSlotLimit',
'No character slots left')
}
}
export function characterSlotsRemaining(userId) {
let tier = getUserTier(userId);
const currentCharacterCount = Creatures.find({
owner: userId,
}, {
fields: { _id: 1 },
}).count();
if (tier.characterSlots === -1) {
return Number.POSITIVE_INFINITY;
}
return tier.characterSlots - currentCharacterCount;
}

View File

@@ -0,0 +1,90 @@
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 SimpleSchema from 'simpl-schema';
import simpleSchemaMixin from '/imports/api/creature/mixins/simpleSchemaMixin.js';
const changeAllowedLibraries = new ValidatedMethod({
name: 'creatures.changeAllowedLibraries',
mixins: [RateLimiterMixin, simpleSchemaMixin],
schema: new SimpleSchema({
_id: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
allowedLibraries: {
type: Array,
optional: true,
maxCount: 100,
},
'allowedLibraries.$': {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
allowedLibraryCollections: {
type: Array,
optional: true,
maxCount: 100,
},
'allowedLibraryCollections.$': {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
}),
rateLimit: {
numRequests: 10,
timeInterval: 5000,
},
run({ _id, allowedLibraries, allowedLibraryCollections }) {
let creature = Creatures.findOne(_id);
assertEditPermission(creature, this.userId);
let $set;
if (allowedLibraries) {
$set = { allowedLibraries }
}
if (allowedLibraryCollections) {
if (!$set) $set = {};
$set.allowedLibraryCollections = allowedLibraryCollections;
}
if (!$set) return;
Creatures.update(_id, { $set });
},
});
const toggleAllUserLibraries = new ValidatedMethod({
name: 'creatures.removeLibraryLimits',
mixins: [RateLimiterMixin, simpleSchemaMixin],
schema: new SimpleSchema({
_id: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
value: {
type: Boolean,
},
}),
rateLimit: {
numRequests: 10,
timeInterval: 5000,
},
run({ _id, value }) {
if (value) {
Creatures.update(_id, {
$unset: {
allowedLibraryCollections: 1,
allowedLibraries: 1,
},
});
} else {
Creatures.update(_id, {
$set: {
allowedLibraryCollections: [],
allowedLibraries: [],
},
});
}
},
});
export { changeAllowedLibraries, toggleAllUserLibraries };

View File

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

View File

@@ -1,70 +1,104 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
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 { getUserTier } from '/imports/api/users/patreon/tiers.js';
import defaultCharacterProperties from '/imports/api/creature/creatures/defaultCharacterProperties.js';
import insertPropertyFromLibraryNode from '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js';
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 SimpleSchema from 'simpl-schema';
const insertCreature = new ValidatedMethod({
name: 'creatures.insertCreature',
validate: null,
mixins: [RateLimiterMixin],
mixins: [RateLimiterMixin, simpleSchemaMixin],
schema: CreatureSchema.pick(
'name',
'gender',
'alignment',
'allowedLibraries',
'allowedLibraryCollections',
).extend({
'startingLevel': {
type: SimpleSchema.Integer,
min: 0,
},
}),
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run() {
if (!this.userId) {
run({ name, gender, alignment, startingLevel,
allowedLibraries, allowedLibraryCollections }) {
const userId = this.userId
if (!userId) {
throw new Meteor.Error('Creatures.methods.insert.denied',
'You need to be logged in to insert a creature');
}
let tier = getUserTier(this.userId);
let currentCharacterCount = Creatures.find({
owner: this.userId,
}, {
fields: {_id: 1},
}).count();
if (
tier.characterSlots !== -1 &&
currentCharacterCount >= tier.characterSlots
){
throw new Meteor.Error('Creatures.methods.insert.denied',
`You are already at your limit of ${tier.characterSlots} characters`)
'You need to be logged in to insert a creature');
}
// Create the creature document
assertHasCharactersSlots(userId);
// Create the creature document
let creatureId = Creatures.insert({
owner: this.userId,
});
// Insert the default properties
// Not batchInsert because we want the properties cleaned by the schema
let baseId;
defaultCharacterProperties(creatureId).forEach(prop => {
let id = CreatureProperties.insert(prop);
if (prop.name === 'Ruleset'){
baseId = id;
}
owner: userId,
name,
gender,
alignment,
allowedLibraries,
allowedLibraryCollections,
});
if (Meteor.isServer){
// Insert the 5e ruleset as the default base
insertPropertyFromLibraryNode.call({
nodeIds: ['iHbhfcg3AL5isSWbw'],
parentRef: {id: baseId, collection: 'creatureProperties'},
order: 0.5,
// Insert experience to get character to starting level
if (startingLevel) {
insertExperienceForCreature({
experience: {
name: 'Starting level',
levels: startingLevel,
creatureId
},
creatureId,
userId,
});
}
return creatureId;
// Insert the default properties
// Not batchInsert because we want the properties cleaned by the schema
let baseId, rulesetSlot;
defaultCharacterProperties(creatureId).forEach(prop => {
let id = CreatureProperties.insert(prop);
if (prop.name === 'Ruleset') {
baseId = id;
rulesetSlot = prop;
}
});
// If the user only has a single ruleset subscribed, use it by default
if (Meteor.isServer) {
insertDefaultRuleset(creatureId, baseId, userId, rulesetSlot);
}
return creatureId;
},
});
// If the user only has a single ruleset subscribed, insert it by default
function insertDefaultRuleset(creatureId, baseId, userId, slot) {
const libraryIds = getCreatureLibraryIds(creatureId, userId);
const filter = getSlotFillFilter({ slot, libraryIds });
const fillCursor = LibraryNodes.find(filter, { fields: { _id: 1 } });
const numRulesets = fillCursor.count();
if (numRulesets === 1) {
const ruleset = fillCursor.fetch()[0]
insertPropertyFromLibraryNode.call({
nodeIds: [ruleset._id],
parentRef: { id: baseId, collection: 'creatureProperties' },
order: 0.5,
});
}
}
export default insertCreature;

View File

@@ -3,11 +3,13 @@ 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';
function removeRelatedDocuments(creatureId){
CreatureVariables.remove({_creatureId: creatureId});
CreatureProperties.remove({'ancestors.id': creatureId});
CreatureLogs.remove({creatureId});
Experiences.remove({creatureId});

View File

@@ -1,13 +1,15 @@
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 { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import computeCreature from '/imports/api/engine/computeCreature.js';
import { union } from 'lodash';
import ActionContext from '/imports/api/engine/actions/ActionContext.js';
import { applyTriggers } from '/imports/api/engine/actions/applyTriggers.js';
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
const restCreature = new ValidatedMethod({
name: 'creature.methods.longRest',
name: 'creature.methods.rest',
validate: new SimpleSchema({
creatureId: {
type: String,
@@ -23,94 +25,146 @@ const restCreature = new ValidatedMethod({
numRequests: 5,
timeInterval: 5000,
},
run({creatureId, restType}) {
let creature = Creatures.findOne(creatureId, {
fields: {
owner: 1,
writers: 1,
settings: 1,
}
}) ;
// Need edit permissions
assertEditPermission(creature, this.userId);
run({ creatureId, restType }) {
// Get action context
const actionContext = new ActionContext(creatureId, [creatureId], this);
// Check permissions
assertEditPermission(actionContext.creature, this.userId);
// 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}
}, {
selector: {type: 'attribute'},
multi: true,
// 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',
});
// Update all action-like properties' usesUsed
filter.type = {$in: [
'action',
'attack',
'spell'
]};
CreatureProperties.update(filter, {
$set: {usesUsed: 0}
}, {
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,
value: 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.value || 0), 0);
let resetMultiplier = 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}
}, {
selector: {type: 'attribute'},
});
});
}
computeCreature(creatureId);
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'] }
}
resetProperties(creatureId, resetFilter, actionContext);
// Reset half hit dice on a long rest, starting with the highest dice
if (restType === 'longRest') {
resetHitDice(creatureId, actionContext);
}
}
export function resetProperties(creatureId, resetFilter, actionContext) {
// Only apply to active properties
const filter = {
'ancestors.id': creatureId,
reset: resetFilter,
removed: { $ne: true },
inactive: { $ne: true },
};
// update all attribute's damage
const attributeFilter = {
...filter,
type: 'attribute',
damage: { $nin: [0, undefined] },
}
CreatureProperties.find(attributeFilter).forEach(prop => {
damagePropertyWork({
prop,
operation: 'increment',
value: -prop.damage ?? 0,
actionContext,
logFunction(increment) {
actionContext.addLog({
name: prop.name,
value: increment < 0 ? `Restored ${-increment}` : `Removed ${-increment}`
});
}
});
});
// Update all action-like properties' usesUsed
const actionFilter = {
...filter,
type: {
$in: ['action', 'spell']
},
usesUsed: { $nin: [0, undefined] },
};
CreatureProperties.find(actionFilter, {
fields: { name: 1, usesUsed: 1 }
}).forEach(prop => {
actionContext.addLog({
name: prop.name,
value: prop.usesUsed >= 0 ? `Restored ${prop.usesUsed} uses` : `Removed ${-prop.usesUsed} uses`
});
});
CreatureProperties.update(actionFilter, {
$set: {
usesUsed: 0,
dirty: true,
}
}, {
selector: { type: 'action' },
multi: true,
});
}
function resetHitDice(creatureId, actionContext) {
let hitDice = CreatureProperties.find({
'ancestors.id': creatureId,
type: 'attribute',
attributeType: 'hitDice',
removed: { $ne: true },
inactive: { $ne: true },
}).fetch();
// Use a collator to do sorting in natural order
let collator = new Intl.Collator('en', {
numeric: true, sensitivity: 'base'
});
// Get the hit dice in decending order of hitDiceSize
let compare = (a, b) => collator.compare(b.hitDiceSize, a.hitDiceSize)
hitDice.sort(compare);
// Get the total number of hit dice that can be recovered this rest
let totalHd = hitDice.reduce((sum, hd) => sum + (hd.total || 0), 0);
let resetMultiplier = actionContext.creature.settings.hitDiceResetMultiplier || 0.5;
let recoverableHd = Math.max(Math.floor(totalHd * resetMultiplier), 1);
// recover each hit dice in turn until the recoverable amount is used up
let amountToRecover;
hitDice.forEach(hd => {
if (!recoverableHd) return;
amountToRecover = Math.min(recoverableHd, hd.damage ?? 0);
if (!amountToRecover) return;
recoverableHd -= amountToRecover;
damagePropertyWork({
prop: hd,
operation: 'increment',
value: -amountToRecover,
actionContext,
logFunction(increment) {
actionContext.addLog({
name: hd.name,
value: increment < 0 ? `Restored ${-increment} hit dice` : `Removed ${increment} hit dice`
});
}
});
});
}
export default restCreature;

View File

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

View File

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

View File

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

View File

@@ -1,15 +1,17 @@
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 { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import {assertEditPermission} from '/imports/api/creature/creatures/creaturePermissions.js';
import {parse, prettifyParseError} from '/imports/parser/parser.js';
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import { parse, prettifyParseError } from '/imports/parser/parser.js';
import resolve, { toString } from '/imports/parser/resolve.js';
const PER_CREATURE_LOG_LIMIT = 100;
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
if (Meteor.isServer){
const PER_CREATURE_LOG_LIMIT = 100;
if (Meteor.isServer) {
var sendWebhookAsCreature = require('/imports/server/discord/sendWebhook.js').sendWebhookAsCreature;
}
@@ -24,17 +26,17 @@ let CreatureLogSchema = new SimpleSchema({
'content.$': {
type: LogContentSchema,
},
// The real-world date that it occured, usually sorted by date
date: {
type: Date,
autoValue: function() {
// If the date isn't set, set it to now
if (!this.isSet) {
return new Date();
}
},
// The real-world date that it occured, usually sorted by date
date: {
type: Date,
autoValue: function () {
// If the date isn't set, set it to now
if (!this.isSet) {
return new Date();
}
},
index: 1,
},
},
creatureId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
@@ -49,36 +51,47 @@ let CreatureLogSchema = new SimpleSchema({
CreatureLogs.attachSchema(CreatureLogSchema);
function removeOldLogs(creatureId){
function removeOldLogs(creatureId) {
// Find the first log that is over the limit
let firstExpiredLog = CreatureLogs.find({
creatureId
}, {
sort: {date: -1},
sort: { date: -1 },
skip: PER_CREATURE_LOG_LIMIT,
});
if (!firstExpiredLog) return;
// Remove all logs older than the one over the limit
CreatureLogs.remove({
creatureId,
date: {$lte: firstExpiredLog.date},
date: { $lte: firstExpiredLog.date },
});
}
function logToMessageData(log){
function logToMessageData(log) {
let embed = {
fields: [],
};
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] };
}
function logWebhook({log, creature}){
if (Meteor.isServer){
function logWebhook({ log, creature }) {
if (Meteor.isServer) {
sendWebhookAsCreature({
creature,
data: logToMessageData(log),
@@ -93,47 +106,57 @@ const insertCreatureLog = new ValidatedMethod({
numRequests: 5,
timeInterval: 5000,
},
validate: new SimpleSchema({
log: CreatureLogSchema.omit('date'),
}).validator(),
run({log}){
validate: new SimpleSchema({
log: CreatureLogSchema.omit('date'),
}).validator(),
run({ log }) {
const creatureId = log.creatureId;
const creature = Creatures.findOne(creatureId, {fields: {
readers: 1,
writers: 1,
owner: 1,
'settings.discordWebhook': 1,
name: 1,
avatarPicture: 1,
}});
const creature = Creatures.findOne(creatureId, {
fields: {
readers: 1,
writers: 1,
owner: 1,
'settings.discordWebhook': 1,
name: 1,
avatarPicture: 1,
}
});
assertEditPermission(creature, this.userId);
// Build the new log
let id = insertCreatureLogWork({log, creature, method: this})
let id = insertCreatureLogWork({ log, creature, method: this })
return id;
},
});
export function insertCreatureLogWork({log, creature, method}){
export function insertCreatureLogWork({ log, creature, method }) {
// Build the new log
if (typeof log === 'string'){
log = {content: [{value: log}]};
if (typeof log === 'string') {
log = { content: [{ value: log }] };
}
if (!log.content?.length) return;
// 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();
// Insert it
let id = CreatureLogs.insert(log);
if (Meteor.isServer){
if (Meteor.isServer) {
method?.unblock();
removeOldLogs(creature._id);
logWebhook({log, creature});
logWebhook({ log, creature });
}
return id;
}
function equalIgnoringWhitespace(a, b){
function equalIgnoringWhitespace(a, b) {
if (typeof a !== 'string' || typeof b !== 'string') return a === b;
return a.replace(/\s/g,'') === b.replace(/\s/g, '');
return a.replace(/\s/g, '') === b.replace(/\s/g, '');
}
const logRoll = new ValidatedMethod({
@@ -143,39 +166,41 @@ const logRoll = new ValidatedMethod({
numRequests: 5,
timeInterval: 5000,
},
validate: new SimpleSchema({
roll: {
type: String,
},
creatureId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
}).validator(),
run({roll, creatureId}){
const creature = Creatures.findOne(creatureId, {fields: {
variables: 1,
readers: 1,
writers: 1,
owner: 1,
'settings.discordWebhook': 1,
name: 1,
avatarPicture: 1,
}});
validate: new SimpleSchema({
roll: {
type: String,
},
creatureId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
}).validator(),
run({ roll, creatureId }) {
const creature = Creatures.findOne(creatureId, {
fields: {
readers: 1,
writers: 1,
owner: 1,
'settings.discordWebhook': 1,
name: 1,
avatarPicture: 1,
}
});
assertEditPermission(creature, this.userId);
const variables = CreatureVariables.findOne({ _creatureId: creatureId });
let logContent = []
let parsedResult = undefined;
try {
parsedResult = parse(roll);
} catch (e){
} catch (e) {
let error = prettifyParseError(e);
logContent.push({name: 'Parse Error', value: error});
logContent.push({ name: 'Parse Error', value: error });
}
if (parsedResult) try {
let {
result: compiled,
context
} = resolve('compile', parsedResult, creature.variables);
} = resolve('compile', parsedResult, variables);
const compiledString = toString(compiled);
if (!equalIgnoringWhitespace(compiledString, roll)) logContent.push({
value: roll
@@ -183,19 +208,19 @@ const logRoll = new ValidatedMethod({
logContent.push({
value: compiledString
});
let {result: rolled} = resolve('roll', compiled, creature.variables, context);
let { result: rolled } = resolve('roll', compiled, variables, context);
let rolledString = toString(rolled);
if (rolledString !== compiledString) logContent.push({
value: rolledString
});
let {result} = resolve('reduce', rolled, creature.variables, context);
let { result } = resolve('reduce', rolled, variables, context);
let resultString = toString(result);
if (resultString !== rolledString) logContent.push({
value: resultString
});
} catch (e){
} catch (e) {
console.error(e);
logContent = [{name: 'Calculation error'}];
logContent = [{ name: 'Calculation error' }];
}
const log = {
content: logContent,
@@ -203,11 +228,11 @@ const logRoll = new ValidatedMethod({
date: new Date(),
};
let id = insertCreatureLogWork({log, creature, method: this});
let id = insertCreatureLogWork({ log, creature, method: this });
return id;
},
});
export default CreatureLogs;
export { CreatureLogSchema, insertCreatureLog, logRoll};
export { CreatureLogSchema, insertCreatureLog, logRoll, PER_CREATURE_LOG_LIMIT };

View File

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

View File

@@ -0,0 +1,78 @@
import { CreatureLogSchema, insertCreatureLogWork } from '/imports/api/creature/log/CreatureLogs.js';
import {
getCreature, getVariables, getPropertiesOfType
} from '/imports/api/engine/loadCreatures.js';
import { groupBy, remove } from 'lodash';
export default class ActionContext{
constructor(creatureId, targetIds = [], method) {
// Get the creature
this.creature = getCreature(creatureId)
if (!this.creature) {
throw new Meteor.Error('No Creature', `No creature could be found with id: ${creatureId}`)
}
// Create a log
this.log = CreatureLogSchema.clean({
creatureId: creatureId,
creatureName: this.creature.name,
});
// Get the variables of the acting creature
this.creature.variables = getVariables(creatureId);
delete this.creature.variables._id;
delete this.creature.variables._creatureId;
// Alias as scope
this.scope = this.creature.variables;
// Get the targets and their variables
this.targets = [];
targetIds.forEach(targetId => {
let target;
if (targetId === creatureId) {
target = this.creature;
} else {
target = getCreature(targetId);
target.variables = getVariables(targetId);
delete target.variables._id;
delete target.variables._creatureId;
}
this.targets.push(target);
});
// Store a reference to the method for inserting the log
this.method = method;
// Get triggers
this.triggers = getPropertiesOfType(creatureId, 'trigger');
// Remove deleted or inactive triggers
remove(this.triggers, trigger => trigger.removed || trigger.inactive);
// Sort triggers by order
this.triggers.sort((a, b) => a.order - b.order);
// Group the triggers into triggers.<event>.<timing> or
// triggers.doActionProperty.<propertyType>.<timing>
this.triggers = groupBy(this.triggers, 'event');
for (let event in this.triggers) {
if (event === 'doActionProperty') {
this.triggers[event] = groupBy(this.triggers[event], 'actionPropertyType');
for (let propertyType in this.triggers[event]) {
this.triggers[event][propertyType] = groupBy(this.triggers[event][propertyType], 'timing');
}
} else {
this.triggers[event] = groupBy(this.triggers[event], 'timing');
}
}
}
addLog(content) {
if (content.name || content.value){
this.log.content.push(content);
}
}
writeLog() {
insertCreatureLogWork({
log: this.log,
creature: this.creature,
method: this.method,
});
}
}

View File

@@ -2,7 +2,9 @@ import action from './applyPropertyByType/applyAction.js';
import adjustment from './applyPropertyByType/applyAdjustment.js';
import branch from './applyPropertyByType/applyBranch.js';
import buff from './applyPropertyByType/applyBuff.js';
import buffRemover from './applyPropertyByType/applyBuffRemover.js';
import damage from './applyPropertyByType/applyDamage.js';
import folder from './applyPropertyByType/applyFolder.js';
import note from './applyPropertyByType/applyNote.js';
import roll from './applyPropertyByType/applyRoll.js';
import savingThrow from './applyPropertyByType/applySavingThrow.js';
@@ -13,7 +15,9 @@ const applyPropertyByType = {
adjustment,
branch,
buff,
buffRemover,
damage,
folder,
note,
roll,
savingThrow,
@@ -21,7 +25,7 @@ const applyPropertyByType = {
toggle,
};
export default function applyProperty(node, opts, ...rest){
opts.scope[`#${node.node.type}`] = node.node;
return applyPropertyByType[node.node.type]?.(node, opts, ...rest);
export default function applyProperty(node, actionContext, ...rest) {
actionContext.scope[`#${node.node.type}`] = node.node;
applyPropertyByType[node.node.type]?.(node, actionContext, ...rest);
}

View File

@@ -5,54 +5,62 @@ import applyProperty from '../applyProperty.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { adjustQuantityWork } from '/imports/api/creature/creatureProperties/methods/adjustQuantity.js';
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
import numberToSignedString from '/imports/ui/utility/numberToSignedString.js';
import numberToSignedString from '/imports/api/utility/numberToSignedString.js';
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
import { resetProperties } from '/imports/api/creature/creatures/methods/restCreature.js';
import { getPropertyDecendants } from '/imports/api/engine/loadCreatures.js';
import { nodeArrayToTree } from '/imports/api/parenting/nodesToTree.js';
export default function applyAction(node, {creature, targets, scope, log}){
export default function applyAction(node, actionContext) {
applyNodeTriggers(node, 'before', actionContext);
const prop = node.node;
if (prop.target === 'self') targets = [creature];
if (prop.target === 'self') actionContext.targets = [actionContext.creature];
const targets = actionContext.targets;
// Log the name and description
// Log the name and summary
let content = { name: prop.name };
if (prop.description?.text){
recalculateInlineCalculations(prop.description, scope, log);
content.value = prop.description.value;
}
if (content.name || content.value){
log.content.push(content);
if (prop.summary?.text) {
recalculateInlineCalculations(prop.summary, actionContext);
content.value = prop.summary.value;
}
if (!prop.silent) actionContext.addLog(content);
// Spend the resources
const failed = spendResources({prop, log, scope});
const failed = spendResources(prop, actionContext);
if (failed) return;
const attack = prop.attackRoll || prop.attackRollBonus;
// Attack if there is an attack roll
if (attack && attack.calculation){
if (targets.length){
if (attack && attack.calculation) {
if (targets.length) {
targets.forEach(target => {
applyAttackToTarget({attack, target, scope, log});
applyAttackToTarget({ attack, target, actionContext });
// Apply the children, but only to the current target
applyChildren(node, {targets: [target], scope, log});
actionContext.targets = [target];
applyChildren(node, actionContext);
});
} else {
applyAttackWithoutTarget({attack, scope, log});
applyChildren(node, {creature, targets, scope, log});
applyAttackWithoutTarget({ attack, actionContext });
applyChildren(node, actionContext);
}
} else {
applyChildren(node, {creature, targets, scope, log});
applyChildren(node, actionContext);
}
if (prop.actionType === 'event' && prop.variableName) {
resetProperties(actionContext.creature._id, prop.variableName, actionContext);
}
}
function applyAttackWithoutTarget({attack, scope, log}){
delete scope['$attackHit'];
delete scope['$attackMiss'];
delete scope['$criticalHit'];
delete scope['$criticalMiss'];
delete scope['$attackRoll'];
recalculateCalculation(attack, scope, log);
function applyAttackWithoutTarget({ attack, actionContext }) {
delete actionContext.scope['~attackHit'];
delete actionContext.scope['~attackMiss'];
delete actionContext.scope['~criticalHit'];
delete actionContext.scope['~criticalMiss'];
delete actionContext.scope['~attackRoll'];
recalculateCalculation(attack, actionContext);
const scope = actionContext.scope;
let {
resultPrefix,
result,
@@ -60,27 +68,35 @@ function applyAttackWithoutTarget({attack, scope, log}){
criticalMiss,
} = rollAttack(attack, scope);
let name = criticalHit ? 'Critical Hit!' : criticalMiss ? 'Critical Miss!' : 'To Hit';
if (scope['$attackAdvantage'] === 1){
if (scope['~attackAdvantage']?.value === 1) {
name += ' (Advantage)';
} else if(scope['$attackAdvantage'] === -1){
} else if (scope['~attackAdvantage']?.value === -1) {
name += ' (Disadvantage)';
}
log.content.push({
if (!criticalMiss) {
scope['~attackHit'] = { value: true }
}
if (!criticalHit) {
scope['~attackMiss'] = { value: true };
}
actionContext.addLog({
name,
value: `${resultPrefix}\n**${result}**`,
inline: true,
});
}
function applyAttackToTarget({attack, target, scope, log}){
delete scope['$attackHit'];
delete scope['$attackMiss'];
delete scope['$criticalHit'];
delete scope['$criticalMiss'];
delete scope['$attackDiceRoll'];
delete scope['$attackRoll'];
function applyAttackToTarget({ attack, target, actionContext }) {
const scope = actionContext.scope;
delete scope['~attackHit'];
delete scope['~attackMiss'];
delete scope['~criticalHit'];
delete scope['~criticalMiss'];
delete scope['~attackDiceRoll'];
delete scope['~attackRoll'];
recalculateCalculation(attack, scope, log);
recalculateCalculation(attack, actionContext);
let {
resultPrefix,
@@ -89,34 +105,34 @@ function applyAttackToTarget({attack, target, scope, log}){
criticalMiss,
} = rollAttack(attack, scope);
if (target.variables.armor){
if (target.variables.armor) {
const armor = target.variables.armor.value;
let name = criticalHit ? 'Critical Hit!' :
criticalMiss ? 'Critical Miss!' :
result > armor ? 'Hit!' : 'Miss!';
if (scope['$attackAdvantage'] === 1){
result > armor ? 'Hit!' : 'Miss!';
if (scope['~attackAdvantage']?.value === 1) {
name += ' (Advantage)';
} else if(scope['$attackAdvantage'] === -1){
} else if (scope['~attackAdvantage']?.value === -1) {
name += ' (Disadvantage)';
}
log.content.push({
actionContext.addLog({
name,
value: `${resultPrefix}\n**${result}**`,
inline: true,
});
if ((result > armor) || (criticalHit)){
scope['$attackHit'] = true;
if (criticalMiss || result < armor) {
scope['~attackMiss'] = { value: true };
} else {
scope['$attackMiss'] = true;
scope['~attackHit'] = { value: true };
}
} else {
log.content.push({
actionContext.addLog({
name: 'Error',
value:'Target has no `armor`',
value: 'Target has no `armor`',
});
log.content.push({
actionContext.addLog({
name: criticalHit ? 'Critical Hit!' : criticalMiss ? 'Critical Miss!' : 'To Hit',
value: `${resultPrefix}\n**${result}**`,
inline: true,
@@ -124,10 +140,10 @@ function applyAttackToTarget({attack, target, scope, log}){
}
}
function rollAttack(attack, scope){
function rollAttack(attack, scope) {
const rollModifierText = numberToSignedString(attack.value, true);
let value, resultPrefix;
if (attack.advantage === 1 || scope['$attackAdvantage']){
if (scope['~attackAdvantage']?.value === 1) {
const [a, b] = rollDice(2, 20);
if (a >= b) {
value = a;
@@ -136,7 +152,7 @@ function rollAttack(attack, scope){
value = b;
resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`;
}
} else if (attack.advantage === -1 || scope['$attackDisadvantage']){
} else if (scope['~attackAdvantage']?.value === -1) {
const [a, b] = rollDice(2, 20);
if (a <= b) {
value = a;
@@ -149,48 +165,45 @@ function rollAttack(attack, scope){
value = rollDice(1, 20)[0];
resultPrefix = `1d20 [${value}] ${rollModifierText}`
}
scope['$attackRoll'] = {value};
scope['~attackDiceRoll'] = { value };
const result = value + attack.value;
const {criticalHit, criticalMiss} = applyCrits(value, scope);
return {resultPrefix, result, value, criticalHit, criticalMiss};
scope['~attackRoll'] = { value: result };
const { criticalHit, criticalMiss } = applyCrits(value, scope);
return { resultPrefix, result, value, criticalHit, criticalMiss };
}
function applyCrits(value, scope){
let criticalHitTarget = scope.criticalHitTarget?.value || 20;
function applyCrits(value, scope) {
const criticalHitTarget = scope['~criticalHitTarget']?.value || 20;
let criticalHit = value >= criticalHitTarget;
let criticalMiss;
if (criticalHit){
scope['$criticalHit'] = {value: true};
scope['$attackHit'] = {value: true};
if (criticalHit) {
scope['~criticalHit'] = { value: true };
} else {
criticalMiss = value === 1;
if (criticalMiss){
scope['$criticalMiss'] = 1;
scope['$attackMiss'] = {value: true};
} else {
// Untargeted attacks hit by default
scope['$attackHit'] = {value: true}
if (criticalMiss) {
scope['~criticalMiss'] = { value: true };
}
}
return {criticalHit, criticalMiss};
return { criticalHit, criticalMiss };
}
function applyChildren(node, args){
node.children.forEach(child => applyProperty(child, args));
function applyChildren(node, actionContext) {
applyNodeTriggers(node, 'after', actionContext);
node.children.forEach(child => applyProperty(child, actionContext));
}
function spendResources({prop, log, scope}){
function spendResources(prop, actionContext) {
// Check Uses
if (prop.usesLeft <= 0){
log.content.push({
if (prop.usesLeft <= 0) {
if (!prop.silent) actionContext.addLog({
name: 'Error',
value: `${prop.name || 'action'} does not have enough uses left`,
});
return true;
}
// Resources
if (prop.insufficientResources){
log.content.push({
if (prop.insufficientResources) {
if (!prop.silent) actionContext.addLog({
name: 'Error',
value: 'This creature doesn\'t have sufficient resources to perform this action',
});
@@ -200,21 +213,19 @@ function spendResources({prop, log, scope}){
let itemQuantityAdjustments = [];
let spendLog = [];
let gainLog = [];
let ammoChildren = [];
try {
prop.resources.itemsConsumed.forEach(itemConsumed => {
recalculateCalculation(itemConsumed.quantity, scope, log);
if (!itemConsumed.itemId){
recalculateCalculation(itemConsumed.quantity, actionContext);
if (!itemConsumed.itemId) {
throw 'No ammo was selected for this prop';
}
let item = CreatureProperties.findOne(itemConsumed.itemId);
if (!item || item.ancestors[0].id !== prop.ancestors[0].id){
if (!item || item.ancestors[0].id !== prop.ancestors[0].id) {
throw 'The prop\'s ammo was not found on the creature';
}
if (!item.equipped){
throw 'The selected ammo is not equipped';
}
if (
!itemConsumed.quantity.value ||
!itemConsumed?.quantity?.value ||
!isFinite(itemConsumed.quantity.value)
) return;
itemQuantityAdjustments.push({
@@ -223,20 +234,22 @@ function spendResources({prop, log, scope}){
value: itemConsumed.quantity.value,
});
let logName = item.name;
if (itemConsumed.quantity.value > 1 || itemConsumed.quantity.value < -1){
if (itemConsumed.quantity.value > 1 || itemConsumed.quantity.value < -1) {
logName = item.plural || logName;
}
if (itemConsumed.quantity.value > 0){
if (itemConsumed.quantity.value > 0) {
spendLog.push(logName + ': ' + itemConsumed.quantity.value);
} else if (itemConsumed.quantity.value < 0){
} else if (itemConsumed.quantity.value < 0) {
gainLog.push(logName + ': ' + -itemConsumed.quantity.value);
}
ammoChildren.push(...getItemChildren(item, actionContext, prop));
});
} catch (e){
log.content.push({
} catch (e) {
actionContext.addLog({
name: 'Error',
value: e,
value: e.toString(),
});
console.error(e);
return true;
}
// No more errors should be thrown after this line
@@ -245,13 +258,13 @@ function spendResources({prop, log, scope}){
itemQuantityAdjustments.forEach(adjustQuantityWork);
// Use uses
if (prop.usesLeft){
if (prop.usesLeft) {
CreatureProperties.update(prop._id, {
$inc: {usesUsed: 1}
$inc: { usesUsed: 1 }
}, {
selector: prop
});
log.content.push({
if (!prop.silent) actionContext.addLog({
name: 'Uses left',
value: prop.usesLeft - 1,
inline: true,
@@ -260,35 +273,68 @@ function spendResources({prop, log, scope}){
// Damage stats
prop.resources.attributesConsumed.forEach(attConsumed => {
recalculateCalculation(attConsumed.quantity, scope, log);
recalculateCalculation(attConsumed.quantity, actionContext);
if (!attConsumed.quantity?.value) return;
let stat = scope[attConsumed.variableName];
if (!stat){
let stat = actionContext.scope[attConsumed.variableName];
if (!stat) {
spendLog.push(stat.name + ': ' + ' not found');
return;
}
damagePropertyWork({
property: stat,
prop: stat,
operation: 'increment',
value: attConsumed.quantity.value,
actionContext,
});
if (attConsumed.quantity.value > 0){
if (attConsumed.quantity.value > 0) {
spendLog.push(stat.name + ': ' + attConsumed.quantity.value);
} else if (attConsumed.quantity.value < 0){
} else if (attConsumed.quantity.value < 0) {
gainLog.push(stat.name + ': ' + -attConsumed.quantity.value);
}
});
// Log all the spending
if (gainLog.length) log.content.push({
if (gainLog.length && !prop.silent) actionContext.addLog({
name: 'Gained',
value: gainLog.join('\n'),
inline: true,
});
if (spendLog.length) log.content.push({
if (spendLog.length && !prop.silent) actionContext.addLog({
name: 'Spent',
value: spendLog.join('\n'),
inline: true,
});
// Apply the ammo children
ammoChildren.forEach(prop => {
applyProperty(prop, actionContext);
});
}
function getItemChildren(item, actionContext, prop) {
// Skip if the prop or the item are ancestors of one another, otherwise infinite loop
if (hasAncestorRelationship(item, prop)) return [];
// Get the item children
const itemProperties = getPropertyDecendants(actionContext.creature._id, item._id);
// Tree them up
const propertyForest = nodeArrayToTree(itemProperties);
return propertyForest
}
function hasAncestorRelationship(a, b) {
let top, bottom;
if (a.ancestors.length === b.ancestors.length) {
// Can't be ancestors of one another if they have the same number of ancestors
return false;
} else if (a.ancestors.length > b.ancestors.length) {
// longer ancestor list goes on the bottom
top = b;
bottom = a;
} else {
top = a;
bottom = b;
}
const expectedAncestorPosition = top.ancestors.length;
return bottom.ancestors[expectedAncestorPosition]?.id === top._id;
}

View File

@@ -1,41 +1,42 @@
import applyProperty from '../applyProperty.js';
import recalculateCalculation from './shared/recalculateCalculation.js';
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
export default function applyAdjustment(node, {
creature, targets, scope, log
}){
export default function applyAdjustment(node, actionContext){
applyNodeTriggers(node, 'before', actionContext);
const prop = node.node;
const damageTargets = prop.target === 'self' ? [creature] : targets;
const damageTargets = prop.target === 'self' ? [actionContext.creature] : actionContext.targets;
if (!prop.amount) {
return applyChildren(node, {creature, targets, scope, log});
return applyChildren(node, actionContext);
}
// Evaluate the amount
recalculateCalculation(prop.amount, scope, log);
recalculateCalculation(prop.amount, actionContext);
const value = +prop.amount.value;
if (!isFinite(value)) {
return applyChildren(node, {creature, targets, scope, log});
return applyChildren(node, actionContext);
}
if (damageTargets?.length) {
damageTargets.forEach(target => {
let stat = target.variables[prop.stat];
if (!stat?.type) {
log.content.push({
if (!prop.silent) actionContext.addLog({
name: 'Error',
value: `Could not apply attribute damage, creature does not have \`${prop.stat}\` set`
});
return applyChildren(node, {creature, targets, scope, log});
return applyChildren(node, actionContext);
}
damagePropertyWork({
property: stat,
prop: stat,
operation: prop.operation,
value: value,
value,
actionContext,
});
log.content.push({
if (!prop.silent) actionContext.addLog({
name: 'Attribute damage',
value: `${prop.stat}${prop.operation === 'set' ? ' set to' : ''}` +
` ${value}`,
@@ -43,7 +44,7 @@ export default function applyAdjustment(node, {
});
});
} else {
log.content.push({
if (!prop.silent) actionContext.addLog({
name: 'Attribute damage',
value: `${prop.stat}${prop.operation === 'set' ? ' set to' : ''}` +
` ${value}`,
@@ -51,9 +52,10 @@ export default function applyAdjustment(node, {
});
}
return applyChildren(node, {creature, targets, scope, log});
return applyChildren(node, actionContext);
}
function applyChildren(node, args){
node.children.forEach(child => applyProperty(child, args));
function applyChildren(node, actionContext){
applyNodeTriggers(node, 'after', actionContext);
node.children.forEach(child => applyProperty(child, actionContext));
}

View File

@@ -1,47 +1,76 @@
import applyProperty from '../applyProperty.js';
import recalculateCalculation from './shared/recalculateCalculation.js';
import rollDice from '/imports/parser/rollDice.js';
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
export default function applyBranch(node, {
creature, targets, scope, log
}){
const applyChildren = function(){
node.children.forEach(child => applyProperty(child, {
creature, targets, scope, log
}));
export default function applyBranch(node, actionContext) {
applyNodeTriggers(node, 'before', actionContext);
const applyChildren = function () {
applyNodeTriggers(node, 'after', actionContext);
node.children.forEach(child => applyProperty(child, actionContext));
};
const scope = actionContext.scope;
const targets = actionContext.targets;
const prop = node.node;
switch(prop.branchType){
switch (prop.branchType) {
case 'if':
recalculateCalculation(prop.condition, scope, log);
recalculateCalculation(prop.condition, actionContext);
if (prop.condition?.value) applyChildren();
break;
case 'index':
if (node.children.length) {
recalculateCalculation(prop.condition, actionContext);
if (!isFinite(prop.condition?.value)) {
actionContext.addLog({
name: 'Branch Error',
value: 'Index did not resolve into a valid number'
});
break;
}
let index = Math.floor(prop.condition?.value);
if (index < 1) index = 1;
if (index > node.children.length) index = node.children.length;
applyNodeTriggers(node, 'after', actionContext);
applyProperty(node.children[index - 1], actionContext);
}
break;
case 'hit':
if (scope['$attackHit']?.value) applyChildren();
if (scope['~attackHit']?.value) {
if (!targets.length && !prop.silent) actionContext.addLog({ value: '**On hit**' });
applyChildren();
}
break;
case 'miss':
if (scope['$attackMiss']?.value) applyChildren();
if (scope['~attackMiss']?.value) {
if (!targets.length && !prop.silent) actionContext.addLog({ value: '**On miss**' });
applyChildren();
}
break;
case 'failedSave':
if (scope['$saveFailed']?.value) applyChildren();
if (scope['~saveFailed']?.value) {
if (!targets.length && !prop.silent) actionContext.addLog({ value: '**On failed save**' });
applyChildren();
}
break;
case 'successfulSave':
if (scope['$saveSucceeded']?.value) applyChildren();
if (scope['~saveSucceeded']?.value) {
if (!targets.length && !prop.silent) actionContext.addLog({ value: '**On save**', });
applyChildren();
}
break;
case 'random':
if (node.children.length){
if (node.children.length) {
let index = rollDice(1, node.children.length)[0] - 1;
applyProperty(node.children[index], {
creature, targets, scope, log
});
applyNodeTriggers(node, 'after', actionContext);
applyProperty(node.children[index], actionContext);
}
break;
case 'eachTarget':
if (targets.length){
if (targets.length) {
targets.forEach(target => {
node.children.forEach(child => applyProperty(child, {
creature, targets: [target], scope, log
}));
applyNodeTriggers(node, 'after', actionContext);
actionContext.targets = [target]
node.children.forEach(child => applyProperty(child, actionContext));
});
} else {
applyChildren();

View File

@@ -1,8 +1,8 @@
import {
setLineageOfDocs,
renewDocIds
setLineageOfDocs,
renewDocIds
} from '/imports/api/parenting/parenting.js';
import {setDocToLastOrder} from '/imports/api/parenting/order.js';
import { setDocToLastOrder } from '/imports/api/parenting/order.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import computedSchemas from '/imports/api/properties/computedPropertySchemasIndex.js';
import applyFnToKey from '/imports/api/engine/computation/utility/applyFnToKey.js';
@@ -10,36 +10,79 @@ import { get } from 'lodash';
import resolve, { map, toString } from '/imports/parser/resolve.js';
import symbol from '/imports/parser/parseTree/symbol.js';
import logErrors from './shared/logErrors.js';
import { insertCreatureLog } from '/imports/api/creature/log/CreatureLogs.js';
import cyrb53 from '/imports/api/engine/computation/utility/cyrb53.js';
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
import INLINE_CALCULATION_REGEX from '/imports/constants/INLINE_CALCULTION_REGEX.js';
import recalculateInlineCalculations from './shared/recalculateInlineCalculations.js';
export default function applyBuff(node, {creature, targets, scope, log}){
export default function applyBuff(node, actionContext) {
applyNodeTriggers(node, 'before', actionContext);
const prop = node.node;
let buffTargets = prop.target === 'self' ? [creature] : targets;
let buffTargets = prop.target === 'self' ? [actionContext.creature] : actionContext.targets;
// Then copy the decendants of the buff to the targets
// Mark the buff as dirty for recalculation
prop.dirty = true;
// Then copy the descendants of the buff to the targets
let propList = [prop];
function addChildrenToPropList(children){
function addChildrenToPropList(children, { skipCrystalize } = {}) {
children.forEach(child => {
if (skipCrystalize) child.node._skipCrystalize = true;
propList.push(child.node);
addChildrenToPropList(child.children);
// recursively add the child's children, but don't crystalize nested buffs
addChildrenToPropList(child.children, {
skipCrystalize: skipCrystalize || child.node.type === 'buff'
});
});
}
addChildrenToPropList(node.children);
crystalizeVariables({propList, scope, log});
if (!prop.skipCrystalization) {
crystalizeVariables({ propList, actionContext });
}
let oldParent = {
id: prop.parent.id,
collection: prop.parent.collection,
};
buffTargets.forEach(target => {
// Apply the buff
copyNodeListToTarget(propList, target, oldParent);
//Log the buff
let logValue = prop.description?.value
if (prop.description?.text) {
recalculateInlineCalculations(prop.description, actionContext);
logValue = prop.description?.value;
}
if ((prop.name || prop.description?.value) && !prop.silent) {
if (target._id === actionContext.creature._id) {
// Targeting self
actionContext.addLog({
name: prop.name,
value: logValue,
});
} else {
// Targeting other
insertCreatureLog.call({
log: {
creatureId: target._id,
content: [{
name: prop.name,
value: logValue,
}],
}
});
}
}
});
applyNodeTriggers(node, 'after', actionContext);
// Don't apply the children of the buff, they get copied to the target instead
}
function copyNodeListToTarget(propList, target, oldParent){
let ancestry = [{collection: 'creatures', id: target._id}];
function copyNodeListToTarget(propList, target, oldParent) {
let ancestry = [{ collection: 'creatures', id: target._id }];
setLineageOfDocs({
docArray: propList,
newAncestry: ancestry,
@@ -57,11 +100,16 @@ function copyNodeListToTarget(propList, target, oldParent){
/**
* Replaces all variables with their resolved values
* except variables of the form `$target.thing.total` become `thing.total`
* except variables of the form `~target.thing.total` become `thing.total`
*/
function crystalizeVariables({propList, scope, log}){
function crystalizeVariables({ propList, actionContext }) {
propList.forEach(prop => {
computedSchemas[prop.type].computedFields().forEach( calcKey => {
if (prop._skipCrystalize) {
delete prop._skipCrystalize;
return;
}
// Iterate through all the calculations and crystalize them
computedSchemas[prop.type].computedFields().forEach(calcKey => {
applyFnToKey(prop, calcKey, (prop, key) => {
const calcObj = get(prop, key);
if (!calcObj?.parseNode) return;
@@ -71,25 +119,25 @@ function crystalizeVariables({propList, scope, log}){
node.parseType !== 'accessor' && node.parseType !== 'symbol'
) return node;
// Handle variables
if (node.name === '$target'){
// strip $target
if (node.parseType === 'accessor'){
if (node.name === '~target') {
// strip ~target
if (node.parseType === 'accessor') {
node.name = node.path.shift();
if (!node.path.length){
return symbol.create({name: node.name})
if (!node.path.length) {
return symbol.create({ name: node.name })
}
} else {
// Can't strip symbols
log.content.push({
actionContext.addLog({
name: 'Error',
value: 'Variable `$target` should not be used without a property: $target.property',
value: 'Variable `~target` should not be used without a property: ~target.property',
});
}
return node;
} else {
// Resolve all other variables
const {result, context} = resolve('reduce', node, scope);
logErrors(context.errors, log);
const { result, context } = resolve('reduce', node, actionContext.scope);
logErrors(context.errors, actionContext);
return result;
}
});
@@ -97,5 +145,36 @@ function crystalizeVariables({propList, scope, log}){
calcObj.hash = cyrb53(calcObj.calculation);
});
});
// For each key in the schema
computedSchemas[prop.type].inlineCalculationFields().forEach(calcKey => {
// That ends in .inlineCalculations
applyFnToKey(prop, calcKey, (prop, key) => {
const inlineCalcObj = get(prop, key);
if (!inlineCalcObj) return;
// If there is no text, skip
if (!inlineCalcObj.text) {
return;
}
// Replace all the existing calculations
let index = -1;
inlineCalcObj.text = inlineCalcObj.text.replace(INLINE_CALCULATION_REGEX, () => {
index += 1;
return `{${inlineCalcObj.inlineCalculations[index].calculation}}`;
});
// Set the value to the uncomputed string
inlineCalcObj.value = inlineCalcObj.text;
// Write a new hash
const inlineCalcHash = cyrb53(inlineCalcObj.text);
if (inlineCalcHash === inlineCalcObj.hash) {
// Skip if nothing changed
return;
}
inlineCalcObj.hash = inlineCalcHash;
});
});
});
}

View File

@@ -0,0 +1,101 @@
import { findLast, difference, intersection, filter } from 'lodash';
import applyProperty from '../applyProperty.js';
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
import { getProperyAncestors, getPropertiesOfType } from '/imports/api/engine/loadCreatures.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { softRemove } from '/imports/api/parenting/softRemove.js';
import getEffectivePropTags from '/imports/api/engine/computation/utility/getEffectivePropTags.js';
export default function applyBuffRemover(node, actionContext) {
// Apply triggers
applyNodeTriggers(node, 'before', actionContext);
const prop = node.node;
// Log Name
if (prop.name && !prop.silent){
actionContext.addLog({ name: prop.name });
}
// Remove buffs
if (prop.targetParentBuff) {
// Remove nearest ancestor buff
const ancestors = getProperyAncestors(actionContext.creature._id, prop._id);
const nearestBuff = findLast(ancestors, ancestor => ancestor.type === 'buff');
if (!nearestBuff) {
actionContext.addLog({
name: 'Error',
value: 'Buff remover does not have a parent buff to remove',
});
return;
}
removeBuff(nearestBuff, actionContext, prop);
} else {
// Get all the buffs targeted by tags
const allBuffs = getPropertiesOfType(actionContext.creature._id, 'buff');
const targetedBuffs = filter(allBuffs, buff => {
if (buff.inactive) return false;
if (buffRemoverMatchTags(prop, buff)) return true;
});
// Remove the buffs
if (prop.removeAll) {
// Remove all matching buffs
targetedBuffs.forEach(buff => {
removeBuff(buff, actionContext, prop);
});
} else {
// Sort in reverse order
targetedBuffs.sort((a, b) => b.order - a.order);
// Remove the one with the highest order
const buff = targetedBuffs[0];
if (buff) {
removeBuff(buff, actionContext, prop);
}
}
}
// Apply triggers
applyNodeTriggers(node, 'after', actionContext);
// Apply children
node.children.forEach(child => applyProperty(child, actionContext));
}
function removeBuff(buff, actionContext, prop) {
if (!prop.silent) actionContext.addLog({
name: 'Removed',
value: `${buff.name || 'Buff'}`
});
softRemove({ _id: buff._id, collection: CreatureProperties });
}
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

@@ -1,92 +1,114 @@
import { some, intersection, difference, remove, includes } from 'lodash';
import applyProperty from '../applyProperty.js';
import { dealDamageWork } from '/imports/api/creature/creatureProperties/methods/dealDamage.js';
import {insertCreatureLog} from '/imports/api/creature/log/CreatureLogs.js';
import { insertCreatureLog } from '/imports/api/creature/log/CreatureLogs.js';
import resolve, { Context, toString } from '/imports/parser/resolve.js';
import logErrors from './shared/logErrors.js';
import applyEffectsToCalculationParseNode from '/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js';
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
import {
getPropertiesOfType
} from '/imports/api/engine/loadCreatures.js';
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
import getEffectivePropTags from '/imports/api/engine/computation/utility/getEffectivePropTags.js';
export default function applyDamage(node, {
creature, targets, scope, log
}){
const applyChildren = function(){
node.children.forEach(child => applyProperty(child, {
creature, targets, scope, log
}));
export default function applyDamage(node, actionContext) {
applyNodeTriggers(node, 'before', actionContext);
const applyChildren = function () {
applyNodeTriggers(node, 'after', actionContext);
node.children.forEach(child => applyProperty(child, actionContext));
};
const prop = node.node;
const scope = actionContext.scope;
// Skip if there is no parse node to work with
if (!prop.amount.parseNode) return;
if (!prop.amount?.parseNode) return;
// Choose target
let damageTargets = prop.target === 'self' ? [creature] : targets;
let damageTargets = prop.target === 'self' ? [actionContext.creature] : actionContext.targets;
// Determine if the hit is critical
let criticalHit = scope['$criticalHit']?.value &&
let criticalHit = scope['~criticalHit']?.value &&
prop.damageType !== 'healing' // Can't critically heal
;
;
// Double the damage rolls if the hit is critical
let context = new Context({
options: {doubleRolls: criticalHit},
options: { doubleRolls: criticalHit },
});
// Gather all the lines we need to log into an array
const logValue = [];
const logName = prop.damageType === 'healing' ? 'Healing' : 'Damage';
// Compile the dice roll and store that string first
// const {result: compiled} = resolve('compiled', prop.amount.parseNode, scope, context);
// logValue.push(toString(compiled));
// logErrors(context.errors, log);
// roll the dice only and store that string
applyEffectsToCalculationParseNode(prop.amount, log);
const {result: rolled} = resolve('roll', prop.amount.parseNode, scope, context);
logValue.push(toString(rolled));
logErrors(context.errors, log);
applyEffectsToCalculationParseNode(prop.amount, actionContext.log);
const { result: rolled } = resolve('roll', prop.amount.parseNode, scope, context);
if (rolled.parseType !== 'constant') {
logValue.push(toString(rolled));
}
logErrors(context.errors, actionContext);
// Reset the errors so we don't log the same errors twice
context.errors = [];
// Resolve the roll to a final value
const {result: reduced} = resolve('reduce', rolled, scope, context);
logErrors(context.errors, log);
const { result: reduced } = resolve('reduce', rolled, scope, context);
logErrors(context.errors, actionContext);
// Store the result
if (reduced.parseType === 'constant'){
if (reduced.parseType === 'constant') {
prop.amount.value = reduced.value;
} else if (reduced.parseType === 'error'){
} else if (reduced.parseType === 'error') {
prop.amount.value = null;
} else {
prop.amount.value = toString(reduced);
}
const damage = +reduced.value;
let damage = +reduced.value;
// If we didn't end up with a constant of finite amount, give up
if (reduced?.parseType !== 'constant' && !isFinite(reduced.value)){
if (reduced?.parseType !== 'constant' || !isFinite(reduced.value)) {
return applyChildren();
}
// Round the damage to a whole number
damage = Math.floor(damage);
// Convert extra damage into the stored type
if (prop.damageType === 'extra' && scope['~lastDamageType']?.value) {
prop.damageType = scope['~lastDamageType']?.value;
}
// Store current damage type
if (prop.damageType !== 'healing') {
scope['~lastDamageType'] = { value: prop.damageType };
}
// Memoise the damage suffix for the log
let suffix = (criticalHit ? ' critical ' : ' ') +
prop.damageType +
(prop.damageType !== 'healing' ? ' damage ': '');
(prop.damageType !== 'healing' ? ' damage ' : '');
if (damageTargets && damageTargets.length) {
// Iterate through all the targets
damageTargets.forEach(target => {
// Apply weaknesses/resistances/immunities
damage = applyDamageMultipliers({
target,
damage,
damageProp: prop,
logValue
});
actionContext.target = [target];
// Deal the damage to the target
let damageDealt = dealDamageWork({
creature: target,
let damageDealt = dealDamage({
target,
damageType: prop.damageType,
amount: damage,
actionContext
});
// Log the damage done
if (target._id === creature._id){
if (target._id === actionContext.creature._id) {
// Target is same as self, log damage as such
logValue.push(`**${damageDealt}** ${suffix} to self`);
} else {
@@ -107,10 +129,130 @@ export default function applyDamage(node, {
// There are no targets, just log the result
logValue.push(`**${damage}** ${suffix}`);
}
log.content.push({
if (!prop.silent) actionContext.addLog({
name: logName,
value: logValue.join('\n'),
inline: true,
});
return applyChildren();
}
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;
}
}
function dealDamage({ target, damageType, amount, actionContext }) {
// Get all the health bars and do damage to them
let healthBars = getPropertiesOfType(target._id, '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 - b.healthBarDamageOrder;
} else {
diff = a.healthBarHealingOrder - b.healthBarHealingOrder;
}
if (Number.isFinite(diff)) {
return diff;
} else {
return a.order - b.order;
}
});
// Deal the damage to each healthbar in order until all damage is done
const totalDamage = amount;
let damageLeft = totalDamage;
if (damageType === 'healing') damageLeft = -totalDamage;
healthBars.forEach(healthBar => {
if (damageLeft === 0) return;
// Replace the healthbar by the one in the action context if we can
// The damagePropertyWork function bashes the prop with the damage
// So we can use the new value in later action properties
if (healthBar.variableName) {
const targetHealthBar = target.variables[healthBar.variableName];
if (targetHealthBar?._id === healthBar._id) {
healthBar = targetHealthBar;
}
}
// Do the damage
let damageAdded = damagePropertyWork({
prop: healthBar,
operation: 'increment',
value: damageLeft,
actionContext
});
damageLeft -= damageAdded;
// Prevent overflow
if (
damageType === 'healing' ?
healthBar.healthBarNoHealingOverflow :
healthBar.healthBarNoDamageOverflow
) {
damageLeft = 0;
}
});
return totalDamage;
}

View File

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

View File

@@ -1,25 +1,27 @@
import recalculateInlineCalculations from './shared/recalculateInlineCalculations.js';
import applyProperty from '../applyProperty.js';
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
export default function applyNote(node, {creature, targets, scope, log}){
export default function applyNote(node, actionContext){
applyNodeTriggers(node, 'before', actionContext);
const prop = node.node;
// Log Name, summary
let content = { name: prop.name };
if (prop.summary?.text){
recalculateInlineCalculations(prop.summary, scope, log);
recalculateInlineCalculations(prop.summary, actionContext);
content.value = prop.summary.value;
}
if (content.name || content.value){
log.content.push(content);
actionContext.addLog(content);
}
// Log description
if (prop.description?.text){
recalculateInlineCalculations(prop.description, scope, log);
log.content.push({value: prop.description.value});
recalculateInlineCalculations(prop.description, actionContext);
actionContext.addLog({value: prop.description.value});
}
// Apply triggers
applyNodeTriggers(node, 'after', actionContext);
// Apply children
node.children.forEach(child => applyProperty(child, {
creature, targets, scope, log
}));
node.children.forEach(child => applyProperty(child, actionContext));
}

View File

@@ -1,22 +1,61 @@
import applyProperty from '../applyProperty.js';
import recalculateCalculation from './shared/recalculateCalculation.js';
import logErrors from './shared/logErrors.js';
import applyEffectsToCalculationParseNode from '/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js';
import resolve, { toString } from '/imports/parser/resolve.js';
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
export default function applyRoll(node, {creature, targets, scope, log}){
export default function applyRoll(node, actionContext) {
applyNodeTriggers(node, 'before', actionContext);
const prop = node.node;
if (prop.roll?.calculation){
recalculateCalculation(prop.roll, scope, log);
const applyChildren = function () {
applyNodeTriggers(node, 'after', actionContext);
node.children.forEach(child => applyProperty(child, actionContext));
};
if (isFinite(prop.roll.value)){
scope[prop.variableName] = prop.roll.value;
if (prop.roll?.calculation) {
const logValue = [];
// roll the dice only and store that string
applyEffectsToCalculationParseNode(prop.roll, actionContext);
const { result: rolled, context } = resolve('roll', prop.roll.parseNode, actionContext.scope);
if (rolled.parseType !== 'constant') {
logValue.push(toString(rolled));
}
logErrors(context.errors, actionContext);
// Reset the errors so we don't log the same errors twice
context.errors = [];
// Resolve the roll to a final value
const { result: reduced } = resolve('reduce', rolled, actionContext.scope, context);
logErrors(context.errors, actionContext);
// Store the result
if (reduced.parseType === 'constant') {
prop.roll.value = reduced.value;
} else if (reduced.parseType === 'error') {
prop.roll.value = null;
} else {
prop.roll.value = toString(reduced);
}
// If we didn't end up with a constant or a number of finite value, give up
if (reduced?.parseType !== 'constant' || (reduced.valueType === 'number' && !isFinite(reduced.value))) {
return applyChildren();
}
const value = reduced.value;
actionContext.scope[prop.variableName] = { value };
logValue.push(`**${value}**`);
if (!prop.silent) {
actionContext.addLog({
name: prop.name,
value: logValue.join('\n'),
inline: true,
});
}
log.content.push({
name: prop.name,
value: prop.variableName + ' = ' + prop.roll.calculation + ' = ' + prop.roll.value,
inline: true,
});
}
return node.children.forEach(child => applyProperty(child, {
creature, targets, scope, log
}));
return applyChildren();
}

View File

@@ -1,93 +1,112 @@
import rollDice from '/imports/parser/rollDice.js';
import recalculateCalculation from './shared/recalculateCalculation.js';
import applyProperty from '../applyProperty.js';
import numberToSignedString from '/imports/ui/utility/numberToSignedString.js';
import numberToSignedString from '/imports/api/utility/numberToSignedString.js';
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
import { applyUnresolvedEffects } from '/imports/api/engine/actions/doCheck.js';
export default function applySavingThrow(node, {creature, targets, scope, log}){
export default function applySavingThrow(node, actionContext) {
applyNodeTriggers(node, 'before', actionContext);
const prop = node.node;
const originalTargets = actionContext.targets;
let saveTargets = prop.target === 'self' ? [creature] : targets;
let saveTargets = prop.target === 'self' ? [actionContext.creature] : actionContext.targets;
recalculateCalculation(prop.dc, scope, log);
recalculateCalculation(prop.dc, actionContext);
const dc = (prop.dc?.value);
if (!isFinite(dc)){
log.content.push({
if (!isFinite(dc)) {
actionContext.addLog({
name: 'Error',
value: 'Saving throw requires a DC',
});
return node.children.forEach(child => applyProperty(child, {
creature, targets, scope, log
}));
return node.children.forEach(child => applyProperty(child, actionContext));
}
log.content.push({
if (!prop.silent) actionContext.addLog({
name: prop.name,
value: ' DC ' + dc,
value: `DC **${dc}**`,
inline: true,
});
const scope = actionContext.scope;
// If there are no save targets, apply all children as if the save both
// succeeeded and failed
if (!saveTargets?.length) {
scope['~saveFailed'] = { value: true };
scope['~saveSucceeded'] = { value: true };
applyNodeTriggers(node, 'after', actionContext);
return node.children.forEach(child => applyProperty(child, actionContext));
}
// Each target makes the saving throw
saveTargets.forEach(target => {
delete scope['$saveFailed'];
delete scope['$saveSucceeded'];
delete scope['$saveDiceRoll'];
delete scope['$saveRoll'];
delete scope['~saveFailed'];
delete scope['~saveSucceeded'];
delete scope['~saveDiceRoll'];
delete scope['~saveRoll'];
const applyChildren = function(){
node.children.forEach(child => applyProperty(child, {
creature, targets: [target], scope, log
}));
const applyChildren = function () {
actionContext.targets = [target]
applyNodeTriggers(node, 'after', actionContext);
node.children.forEach(child => applyProperty(child, actionContext));
};
const save = target.variables[prop.stat];
if (!save){
log.content.push({
if (!save) {
actionContext.addLog({
name: 'Saving throw error',
value: 'No saving throw found: ' + prop.stat,
});
return applyChildren();
}
const rollModifierText = numberToSignedString(save.value, true);
let rollModifierText = numberToSignedString(save.value, true);
let rollModifier = save.value
const { effectBonus, effectString } = applyUnresolvedEffects(save, scope)
rollModifierText += effectString;
rollModifier += effectBonus;
let value, values, resultPrefix;
if (save.advantage === 1){
if (save.advantage === 1) {
const [a, b] = rollDice(2, 20);
if (a >= b) {
value = a;
resultPrefix = `Advantage: 1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText} = `;
resultPrefix = `Advantage\n1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText}`;
} else {
value = b;
resultPrefix = `Advantage: 1d20 [ ~~${a}~~, ${b} ] ${rollModifierText} = `;
resultPrefix = `Advantage\n1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`;
}
} else if (save.advantage === -1){
} else if (save.advantage === -1) {
const [a, b] = rollDice(2, 20);
if (a <= b) {
value = a;
resultPrefix = `Disadvantage: 1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText} = `;
resultPrefix = `Disadvantage\n1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText}`;
} else {
value = b;
resultPrefix = `Disadvantage: 1d20 [ ~~${a}~~, ${b} ] ${rollModifierText} = `;
resultPrefix = `Disadvantage\n1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`;
}
} else {
values = rollDice(1, 20);
value = values[0];
resultPrefix = `1d20 [ ${value} ] ${rollModifierText} = `
resultPrefix = `1d20 [ ${value} ] ${rollModifierText}`
}
scope['$saveDiceRoll'] = {value};
const result = value + save.value || 0;
scope['$saveRoll'] = {value: result};
scope['~saveDiceRoll'] = { value };
const result = value + rollModifier || 0;
scope['~saveRoll'] = { value: result };
const saveSuccess = result >= dc;
if (saveSuccess){
scope['$saveSucceeded'] = {value: true};
if (saveSuccess) {
scope['~saveSucceeded'] = { value: true };
} else {
scope['$saveFailed'] = {value: true};
scope['~saveFailed'] = { value: true };
}
log.content.push({
name: 'Save',
value: resultPrefix + result + (saveSuccess ? 'Passed' : 'Failed'),
if (!prop.silent) actionContext.addLog({
name: saveSuccess ? 'Successful save' : 'Failed save',
value: resultPrefix + '\n**' + result + '**',
inline: true,
});
return applyChildren();
});
// reset the targets after the save to each child
actionContext.targets = originalTargets;
}

View File

@@ -1,14 +1,13 @@
import applyProperty from '../applyProperty.js';
import recalculateCalculation from './shared/recalculateCalculation.js';
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
export default function applyToggle(node, {
creature, targets, scope, log
}){
export default function applyToggle(node, actionContext){
applyNodeTriggers(node, 'before', actionContext);
const prop = node.node;
recalculateCalculation(prop.condition, scope, log);
recalculateCalculation(prop.condition, actionContext);
if (prop.condition?.value) {
return node.children.forEach(child => applyProperty(child, {
creature, targets, scope, log
}));
applyNodeTriggers(node, 'after', actionContext);
return node.children.forEach(child => applyProperty(child, actionContext));
}
}

View File

@@ -2,9 +2,8 @@ import operator from '/imports/parser/parseTree/operator.js';
import { parse } from '/imports/parser/parser.js';
import logErrors from './logErrors.js';
export default function applyEffectsToCalculationParseNode(calcObj, log){
if (!calcObj.effects) return;
calcObj.effects.forEach(effect => {
export default function applyEffectsToCalculationParseNode(calcObj, actionContext) {
calcObj.effects?.forEach(effect => {
if (effect.operation !== 'add') return;
if (!effect.amount) return;
if (effect.amount.value === null) return;
@@ -17,8 +16,31 @@ export default function applyEffectsToCalculationParseNode(calcObj, log){
operator: '+',
fn: 'add'
});
} catch (e){
logErrors([e], log)
} catch (e) {
logErrors([e], actionContext)
}
});
// Add the highest proficiency as well
let highestProficiency;
calcObj.proficiencies?.forEach(proficiency => {
if (
proficiency.value > highestProficiency
|| (highestProficiency === undefined && Number.isFinite(proficiency.value))
) {
highestProficiency = proficiency.value;
}
});
if (highestProficiency) {
try {
let profParseNode = parse(highestProficiency.toString());
calcObj.parseNode = operator.create({
left: calcObj.parseNode,
right: profParseNode,
operator: '+',
fn: 'add'
});
} catch (e) {
logErrors([e], actionContext)
}
}
}

View File

@@ -1,7 +1,7 @@
export default function logErrors(errors, log){
export default function logErrors(errors, actionContext){
errors?.forEach(error => {
if (error.type !== 'info'){
log.content.push({name: 'Error', value: error.message});
actionContext.addLog({name: 'Error', value: error.message});
}
});
}

View File

@@ -2,10 +2,10 @@ import evaluateCalculation from '/imports/api/engine/computation/utility/evaluat
import applyEffectsToCalculationParseNode from '/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js';
import logErrors from './logErrors.js';
export default function recalculateCalculation(calc, scope, log, context){
export default function recalculateCalculation(calc, actionContext, context) {
if (!calc?.parseNode) return;
calc._parseLevel = 'reduce';
applyEffectsToCalculationParseNode(calc, log);
evaluateCalculation(calc, scope, context);
logErrors(calc.errors, log);
applyEffectsToCalculationParseNode(calc, actionContext);
evaluateCalculation(calc, actionContext.scope, context);
logErrors(calc.errors, actionContext);
}

View File

@@ -1,12 +1,12 @@
import embedInlineCalculations from '/imports/api/engine/computation/utility/embedInlineCalculations.js';
import recalculateCalculation from './recalculateCalculation.js'
export default function recalculateInlineCalculations(inlineCalcObj, scope, log){
export default function recalculateInlineCalculations(inlineCalcObj, actionContext){
// Skip if there are no calculations
if (!inlineCalcObj?.inlineCalculations?.length) return;
// Recalculate each calculation with the current scope
inlineCalcObj.inlineCalculations.forEach(calc => {
recalculateCalculation(calc, scope, log);
recalculateCalculation(calc, actionContext);
});
// Embed the new calculated values
embedInlineCalculations(inlineCalcObj);

View File

@@ -0,0 +1,114 @@
import recalculateCalculation from '/imports/api/engine/actions/applyPropertyByType/shared/recalculateCalculation.js';
import recalculateInlineCalculations from '/imports/api/engine/actions/applyPropertyByType/shared/recalculateInlineCalculations.js';
import { getPropertyDecendants } from '/imports/api/engine/loadCreatures.js';
import { nodeArrayToTree } from '/imports/api/parenting/nodesToTree.js';
import applyProperty from '/imports/api/engine/actions/applyProperty.js';
import { difference, intersection } from 'lodash';
import getEffectivePropTags from '/imports/api/engine/computation/utility/getEffectivePropTags.js';
export function applyNodeTriggers(node, timing, actionContext) {
const prop = node.node;
const type = prop.type;
const triggers = actionContext.triggers?.doActionProperty?.[type]?.[timing];
if (triggers) {
triggers.forEach(trigger => {
applyTrigger(trigger, prop, actionContext);
});
}
}
export function applyTriggers(triggers = [], prop, actionContext) {
// Apply the triggers
triggers.forEach(trigger => {
applyTrigger(trigger, prop, actionContext)
});
}
export function applyTrigger(trigger, prop, actionContext) {
// If there is a prop we are applying the trigger from,
// don't fire if the tags don't match
if (prop && !triggerMatchTags(trigger, prop)) {
return;
}
// Prevent trigger from firing if it's inactive
if (trigger.inactive) {
return;
}
// Prevent triggers from firing if their condition is false
if (trigger.condition?.parseNode) {
recalculateCalculation(trigger.condition, actionContext);
if (!trigger.condition.value) return;
}
// Prevent triggers from firing themselves in a loop
if (trigger.firing) {
/*
log.content.push({
name: trigger.name || 'Trigger',
value: 'Trigger can\'t fire itself',
inline: true,
});
*/
return;
}
trigger.firing = true;
// Fire the trigger
const content = {
name: trigger.name || 'Trigger',
value: trigger.description,
inline: false,
}
if (trigger.description?.text) {
recalculateInlineCalculations(trigger.description, actionContext);
content.value = trigger.description.value;
}
if (!trigger.silent) actionContext.addLog(content);
// Get all the trigger's properties and apply them
const properties = getPropertyDecendants(actionContext.creature._id, trigger._id);
properties.sort((a, b) => a.order - b.order);
const propertyForest = nodeArrayToTree(properties);
propertyForest.forEach(node => {
applyProperty(node, actionContext);
});
trigger.firing = false;
}
export function triggerMatchTags(trigger, prop) {
let matched = false;
const propTags = getEffectivePropTags(prop);
// Check the target tags
if (
!trigger.targetTags?.length ||
difference(trigger.targetTags, propTags).length === 0
) {
matched = true;
}
// Check the extra tags
if (trigger.extraTags) {
for (const extra of trigger.extraTags) {
if (extra.operation === 'OR') {
if (matched) break;
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).length > 0
) {
matched = false;
break;
}
}
}
}
return matched;
}

View File

@@ -0,0 +1,67 @@
import { triggerMatchTags } from '/imports/api/engine/actions/applyTriggers.js';
import clean from '/imports/api/engine/computation/utility/cleanProp.testFn.js';
import { assert } from 'chai';
export default function () {
const prop = clean({
id: 'propWithTags',
type: 'action',
tags: ['yes1', 'notUsed', 'no1', 'yes2', 'no2', 'or1', 'or2'],
});
const positiveProp = clean({
id: 'propWithTags',
type: 'action',
tags: ['yes1', 'notUsed', 'yes2', 'or1', 'or2'],
});
assert.isTrue(
triggerMatchTags(clean({
type: 'trigger',
targetTags: ['yes1'],
}), prop),
'Trigger matches on a single target tag'
);
assert.isTrue(
triggerMatchTags(clean({
type: 'trigger',
targetTags: ['yes1', 'yes2'],
}), prop),
'Trigger matches on a multiple target tags'
);
assert.isFalse(
triggerMatchTags(clean({
type: 'trigger',
targetTags: ['yes1'],
extraTags: [{ operation: 'NOT', tags: ['no1'] }]
}), prop),
'Trigger correctly fails to match when not tags are present'
);
assert.isFalse(
triggerMatchTags(clean({
type: 'trigger',
extraTags: [{ operation: 'NOT', tags: ['no1'] }]
}), prop),
'Trigger correctly fails to match when only not tags are present'
);
assert.isTrue(
triggerMatchTags(clean({
type: 'trigger',
extraTags: [{ operation: 'NOT', tags: ['no1'] }]
}), positiveProp),
'Trigger matches when only not tags are present'
);
assert.isTrue(
triggerMatchTags(clean({
type: 'trigger',
extraTags: [{ operation: 'OR', tags: ['or1'] }]
}), positiveProp),
'Trigger matches when OR tags are present'
);
assert.isTrue(
triggerMatchTags(clean({
type: 'trigger',
targetTags: ['missing1'],
extraTags: [{ operation: 'OR', tags: ['or1'] }]
}), positiveProp),
'Trigger matches when only OR tags are present'
);
}

View File

@@ -1,14 +1,15 @@
import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { CreatureLogSchema, insertCreatureLogWork } from '/imports/api/creature/log/CreatureLogs.js';
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import { nodeArrayToTree } from '/imports/api/parenting/nodesToTree.js';
import {
getProperyAncestors, getPropertyDecendants
} from '/imports/api/engine/loadCreatures.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import applyProperty from './applyProperty.js';
import computeCreature from '/imports/api/engine/computeCreature.js';
import ActionContext from '/imports/api/engine/actions/ActionContext.js';
const doAction = new ValidatedMethod({
name: 'creatureProperties.doAction',
@@ -35,51 +36,33 @@ const doAction = new ValidatedMethod({
numRequests: 10,
timeInterval: 5000,
},
run({actionId, targetIds = [], scope}) {
run({ actionId, targetIds = [], scope }) {
// Get action context
let action = CreatureProperties.findOne(actionId);
// Check permissions
let creature = getRootCreatureAncestor(action);
const creatureId = action.ancestors[0].id;
const actionContext = new ActionContext(creatureId, targetIds, this);
assertEditPermission(creature, this.userId);
// Get all the targets and make sure we can edit them
let targets = [];
targetIds.forEach(targetId => {
let target = Creatures.findOne(targetId);
// Check permissions
assertEditPermission(actionContext.creature, this.userId);
actionContext.targets.forEach(target => {
assertEditPermission(target, this.userId);
targets.push(target);
});
// Fetch all the action's ancestor creatureProperties
const ancestorIds = [];
action.ancestors.forEach(ref => {
if (ref.collection === 'creatureProperties') {
ancestorIds.push(ref.id);
}
});
const ancestors = getProperyAncestors(creatureId, action._id);
ancestors.sort((a, b) => a.order - b.order);
// Get cursor of ancestors
const ancestors = CreatureProperties.find({
_id: {$in: ancestorIds},
}, {
sort: {order: 1},
});
// Get cursor of the properties
const properties = CreatureProperties.find({
$or: [{_id: action._id}, {'ancestors.id': action._id}],
removed: {$ne: true},
}, {
sort: {order: 1},
});
const properties = getPropertyDecendants(creatureId, action._id);
properties.push(action);
properties.sort((a, b) => a.order - b.order);
// Do the action
doActionWork({creature, targets, properties, ancestors, method: this, methodScope: scope});
doActionWork({ properties, ancestors, actionContext, methodScope: scope });
// Recompute all involved creatures
computeCreature(creature._id);
targets.forEach(target => {
computeCreature(target._id);
Creatures.update({
_id: { $in: [creatureId, ...targetIds] }
}, {
$set: { dirty: true },
});
},
});
@@ -87,41 +70,28 @@ const doAction = new ValidatedMethod({
export default doAction;
export function doActionWork({
creature, targets, properties, ancestors, method, methodScope = {}, log
}){
properties, ancestors, actionContext, methodScope = {},
}) {
// get the docs
const ancestorScope = getAncestorScope(ancestors);
const propertyForest = nodeArrayToTree(properties);
if (propertyForest.length !== 1){
if (propertyForest.length !== 1) {
throw new Meteor.Error(`The action has ${propertyForest.length} top level properties, expected 1`);
}
// Create the log
if (!log) log = CreatureLogSchema.clean({
creatureId: creature._id,
creatureName: creature.name,
});
// Include the ancestry and method scope in the context scope
Object.assign(actionContext.scope, ancestorScope, methodScope);
// Apply the top level property, it is responsible for applying its children
// recursively
const scope = {
...creature.variables,
...ancestorScope,
...methodScope
}
applyProperty(propertyForest[0], {
creature,
targets,
scope,
log,
});
applyProperty(propertyForest[0], actionContext);
// Insert the log
insertCreatureLogWork({log, creature, method});
actionContext.writeLog();
}
// Assumes ancestors are in tree order already
function getAncestorScope(ancestors){
function getAncestorScope(ancestors) {
let scope = {};
ancestors.forEach(prop => {
scope[`#${prop.type}`] = prop;

View File

@@ -1,11 +1,58 @@
import '/imports/api/simpleSchemaConfig.js';
//import testTypes from './testTypes/index.js';
import applyTriggers from '/imports/api/engine/actions/applyTriggers.testFn.js';
import { doActionWork } from './doAction.js';
import createAction from './tests/createAction.testFn.js';
import { CreatureLogSchema } from '/imports/api/creature/log/CreatureLogs.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
describe('Do Action', function(){
it('Does an empty action', function(){
doActionWork(createAction({properties: [{type: 'action'}]}));
function cleanProp(prop) {
let schema = CreatureProperties.simpleSchema(prop);
return schema.clean(prop);
}
function cleanCreature(creature) {
let schema = Creatures.simpleSchema(creature);
return schema.clean(creature);
}
// Fake ActionContext to test actions with
const creatureId = 'actionTestCreatureId';
const creatureName = 'Action Test Creature';
const testActionContext = {
creature: cleanCreature({
_id: creatureId,
}),
log: CreatureLogSchema.clean({
creatureId: creatureId,
creatureName: creatureName,
}),
scope: {},
addLog(content) {
if (content.name || content.value) {
this.log.content.push(content);
}
},
writeLog: () => { },
}
const action = cleanProp({
type: 'action',
});
const actionAncestors = [];
describe('Do Action', function () {
it('Does an empty action', function () {
doActionWork({
properties: [action],
ancestors: actionAncestors,
actionContext: testActionContext,
methodScope: {},
});
});
//testTypes.forEach(test => it(test.text, test.fn));
});
describe('Action utility functions', function () {
it('Triggers match tags', applyTriggers);
})

View File

@@ -1,14 +1,15 @@
import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import {
getProperyAncestors, getPropertyDecendants
} from '/imports/api/engine/loadCreatures.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
import { doActionWork } from '/imports/api/engine/actions/doAction.js';
import computeCreature from '/imports/api/engine/computeCreature.js';
import { CreatureLogSchema } from '/imports/api/creature/log/CreatureLogs.js';
import ActionContext from '/imports/api/engine/actions/ActionContext.js';
const doAction = new ValidatedMethod({
name: 'creatureProperties.doCastSpell',
@@ -19,6 +20,10 @@ const doAction = new ValidatedMethod({
regEx: SimpleSchema.RegEx.Id,
optional: true,
},
ritual: {
type: Boolean,
optional: true,
},
targetIds: {
type: Array,
defaultValue: [],
@@ -40,101 +45,91 @@ const doAction = new ValidatedMethod({
numRequests: 10,
timeInterval: 5000,
},
run({spellId, slotId, targetIds = [], scope = {}}) {
run({ spellId, slotId, ritual, targetIds = [], scope = {} }) {
// Get action context
let spell = CreatureProperties.findOne(spellId);
// Check permissions
let creature = getRootCreatureAncestor(spell);
const creatureId = spell.ancestors[0].id;
const actionContext = new ActionContext(creatureId, targetIds, this);
assertEditPermission(creature, this.userId);
// Get all the targets and make sure we can edit them
let targets = [];
targetIds.forEach(targetId => {
let target = Creatures.findOne(targetId);
// Check permissions
assertEditPermission(actionContext.creature, this.userId);
actionContext.targets.forEach(target => {
assertEditPermission(target, this.userId);
targets.push(target);
});
// Fetch all the action's ancestor creatureProperties
const ancestorIds = [];
spell.ancestors.forEach(ref => {
if (ref.collection === 'creatureProperties') {
ancestorIds.push(ref.id);
}
});
const ancestors = getProperyAncestors(creatureId, spell._id);
ancestors.sort((a, b) => a.order - b.order);
// Get cursor of ancestors
const ancestors = CreatureProperties.find({
_id: {$in: ancestorIds},
}, {
sort: {order: 1},
});
// Get cursor of the properties
const properties = CreatureProperties.find({
$or: [{_id: spell._id}, {'ancestors.id': spell._id}],
removed: {$ne: true},
}, {
sort: {order: 1},
});
const properties = getPropertyDecendants(creatureId, spell._id);
properties.push(spell);
properties.sort((a, b) => a.order - b.order);
// Spend the appropriate slot
let slotLevel = spell.level || 0;
let slot;
if (slotId && !spell.castWithoutSpellSlots){
// If a spell requires a slot, make sure a slot is spent
if (spell.level && !spell.castWithoutSpellSlots && !(ritual && spell.ritual)) {
slot = CreatureProperties.findOne(slotId);
if (!slot){
if (!slot) {
throw new Meteor.Error('No slot',
'Slot not found to cast spell');
}
if (!slot.value){
if (!slot.value) {
throw new Meteor.Error('No slot',
'Slot depleted');
}
if (slot.attributeType !== 'spellSlot'){
if (slot.attributeType !== 'spellSlot') {
throw new Meteor.Error('Not a slot',
'The given property is not a valid spell slot');
}
if (!slot.spellSlotLevel?.value){
if (!slot.spellSlotLevel?.value) {
throw new Meteor.Error('No slot level',
'Slot does not have a spell slot level');
}
if (slot.spellSlotLevel.value < spell.level){
if (slot.spellSlotLevel.value < spell.level) {
throw new Meteor.Error('Slot too small',
'Slot is not large enough to cast spell');
}
slotLevel = slot.spellSlotLevel.value;
damagePropertyWork({
property: slot,
prop: slot,
operation: 'increment',
value: 1,
actionContext,
});
}
scope['slotLevel'] = slotLevel;
// Post the slot level spent to the log
const log = CreatureLogSchema.clean({
creatureId: creature._id,
creatureName: creature.name,
});
if (slot?.spellSlotLevel?.value){
log.content.push({
if (slot?.spellSlotLevel?.value) {
actionContext.addLog({
name: `Casting using a level ${slotLevel} spell slot`
});
} else if (slotLevel) {
log.content.push({
name: `Casting at level ${slotLevel}`
});
if (ritual) {
actionContext.addLog({
name: `Ritual casting at level ${slotLevel}`
});
} else {
actionContext.addLog({
name: `Casting at level ${slotLevel}`
});
}
}
// Do the action
doActionWork({creature, targets, properties, ancestors, method: this, methodScope: scope, log});
actionContext.scope['slotLevel'] = { value: slotLevel };
actionContext.scope['~slotLevel'] = { value: slotLevel };
// Recompute all involved creatures
computeCreature(creature._id);
targets.forEach(target => {
computeCreature(target._id);
// Do the action
doActionWork({
properties, ancestors, actionContext, methodScope: scope,
});
// Force the characters involved to recalculate
Creatures.update({
_id: { $in: [creatureId, ...targetIds] }
}, {
$set: { dirty: true },
});
},
});

View File

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

View File

@@ -1,26 +0,0 @@
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
export default function createAction({
creature = {_id: 'creatureId'},
targets = [],
properties = [],
ancestors = [],
method
} = {}){
properties = properties.map(cleanProp);
ancestors = ancestors.map(cleanProp);
creature = cleanCreature(creature);
ancestors = ancestors.map(cleanCreature);
return {creature, targets, properties, ancestors, method};
}
function cleanProp(prop){
let schema = CreatureProperties.simpleSchema(prop);
return schema.clean(prop);
}
function cleanCreature(creature){
let schema = Creatures.simpleSchema(creature);
return schema.clean(creature);
}

View File

@@ -1,6 +0,0 @@
import applyAction from './applyAction.testFn.js';
export default [{
text: 'Applies actions',
fn: applyAction,
},];

View File

@@ -1,38 +0,0 @@
import { EJSON } from 'meteor/ejson';
import createGraph from 'ngraph.graph';
export default class CreatureComputation {
constructor(properties){
// Set up fields
this.originalPropsById = {};
this.propsById = {};
this.propsWithTag = {};
this.scope = {};
this.props = properties;
this.dependencyGraph = createGraph();
this.errors = [];
// Store properties for easy access later
properties.forEach(prop => {
// Store a copy of the unmodified prop
// EJSON clone is ~4x faster than lodash cloneDeep for EJSONable objects
this.originalPropsById[prop._id] = EJSON.clone(prop);
// Store by id
this.propsById[prop._id] = prop;
// Store sets of ids in each tag
if (prop.tags){
prop.tags.forEach(tag => {
if (this.propsWithTag[tag]){
this.propsWithTag[tag].push(prop._id);
} else {
this.propsWithTag[tag] = [prop._id];
}
});
}
// Store the prop in the dependency graph
this.dependencyGraph.addNode(prop._id, prop);
});
}
}

View File

@@ -0,0 +1,55 @@
import { EJSON } from 'meteor/ejson';
import createGraph, { Graph } from 'ngraph.graph';
import getEffectivePropTags from '/imports/api/engine/computation/utility/getEffectivePropTags.js';
interface CreatureProperty {
_id: string;
type: string;
}
export default class CreatureComputation {
originalPropsById: object;
propsById: object;
propsWithTag: object;
scope: object;
props: Array<CreatureProperty>;
dependencyGraph: Graph;
errors: Array<object>;
creature: object;
variables: object;
constructor(properties: Array<CreatureProperty>, creature: object, variables: object) {
// Set up fields
this.originalPropsById = {};
this.propsById = {};
this.propsWithTag = {};
this.scope = {};
this.props = properties;
this.dependencyGraph = createGraph();
this.errors = [];
this.creature = creature;
this.variables = variables;
// Store properties for easy access later
properties.forEach(prop => {
// Store a copy of the unmodified prop
// EJSON clone is ~4x faster than lodash cloneDeep for EJSONable objects
this.originalPropsById[prop._id] = EJSON.clone(prop);
// Store by id
this.propsById[prop._id] = prop;
// Store sets of ids in each tag
getEffectivePropTags(prop).forEach(tag => {
if (!tag) return;
if (this.propsWithTag[tag]) {
this.propsWithTag[tag].push(prop._id);
} else {
this.propsWithTag[tag] = [prop._id];
}
});
// Store the prop in the dependency graph
this.dependencyGraph.addNode(prop._id, prop);
});
}
}

View File

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

View File

@@ -2,7 +2,7 @@
* Only computes `totalFilled`, need to compute `quantityExpected.value`
* before `spacesLeft` can be computed
*/
export default function computeSlotQuantityFilled(node, dependencyGraph){
export default function computeSlotQuantityFilled(node, dependencyGraph) {
let slot = node.node;
if (slot.type !== 'propertySlot') return;
slot.totalFilled = 0;
@@ -10,9 +10,8 @@ export default function computeSlotQuantityFilled(node, dependencyGraph){
let childProp = child.node;
dependencyGraph.addLink(slot._id, childProp._id, 'slotFill');
if (
childProp.type === 'slotFiller' &&
Number.isFinite(childProp.slotQuantityFilled)
){
) {
slot.totalFilled += childProp.slotQuantityFilled;
} else {
slot.totalFilled++;

View File

@@ -1,17 +1,32 @@
import walkDown from '/imports/api/engine/computation/utility/walkdown.js';
import { getEffectTagTargets } from '/imports/api/engine/computation/buildComputation/linkTypeDependencies.js';
export default function computeToggleDependencies(node, dependencyGraph){
export default function computeToggleDependencies(node, dependencyGraph, computation, forest) {
const prop = node.node;
// Only for toggles that aren't inactive and aren't set to enabled or disabled
if (
prop.inactive ||
prop.type !== 'toggle' ||
prop.disabled ||
prop.enabled
) return;
// Only for toggles
if (prop.type !== 'toggle') return;
if (prop.targetByTags) {
// Find all the props targeted by tags, and disable them and their children
getEffectTagTargets(prop, computation).forEach(targetId => {
const target = forest.nodeIndex[targetId];
if (!target) return;
target.node._computationDetails.toggleAncestors.push(prop);
dependencyGraph.addLink(target.node._id, prop._id, 'toggle');
walkDown(target.children, child => {
// The child nodes depend on the toggle
child.node._computationDetails.toggleAncestors.push(prop);
dependencyGraph.addLink(child.node._id, prop._id, 'toggle');
});
});
}
// We don't need to link direct children of static toggles, it's already done
if (prop.disabled || prop.enabled) return;
walkDown(node.children, child => {
// The child nodes depend on the toggle
child.node._computationDetails.toggleAncestors.push(prop);
// The child nodes depend on the toggle condition compuation
dependencyGraph.addLink(child.node._id, prop._id, 'toggle');
});
}

View File

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

View File

@@ -1,4 +1,4 @@
import { get, intersection, difference } from 'lodash';
import { get, intersection, difference, union } from 'lodash';
const linkDependenciesByType = {
action: linkAction,
@@ -14,6 +14,7 @@ const linkDependenciesByType = {
effect: linkEffects,
proficiency: linkProficiencies,
roll: linkRoll,
pointBuy: linkPointBuy,
propertySlot: linkSlot,
skill: linkSkill,
spell: linkAction,
@@ -22,23 +23,26 @@ const linkDependenciesByType = {
toggle: linkToggle,
}
export default function linkTypeDependencies(dependencyGraph, prop, computation){
export default function linkTypeDependencies(dependencyGraph, prop, computation) {
linkDependenciesByType[prop.type]?.(dependencyGraph, prop, computation);
}
function dependOnCalc({dependencyGraph, prop, key}){
function dependOnCalc({ dependencyGraph, prop, key }) {
let calc = get(prop, key);
if (!calc) return;
if (calc.type !== '_calculation'){
throw `Expected calculation got ${calc.type}`
if (!calc?.type) return;
if (calc.type !== '_calculation') {
throw `Failed to dependOnCal for prop: ${prop._id}, key: ${key}. Expected calculation got ${calc.type}`
}
dependencyGraph.addLink(prop._id, `${prop._id}.${key}`, 'calculation');
}
function linkAction(dependencyGraph, prop, {propsById}){
function linkAction(dependencyGraph, prop, { propsById }) {
if (prop.variableName) {
dependencyGraph.addLink(prop.variableName, prop._id, 'eventDefinition');
}
// The action depends on its attack roll and uses calculations
dependOnCalc({dependencyGraph, prop, key: 'attackRoll'});
dependOnCalc({dependencyGraph, prop, key: 'uses'});
dependOnCalc({ dependencyGraph, prop, key: 'attackRoll' });
dependOnCalc({ dependencyGraph, prop, key: 'uses' });
// Link the resources the action uses
if (!prop.resources) return;
@@ -46,7 +50,7 @@ function linkAction(dependencyGraph, prop, {propsById}){
prop.resources.itemsConsumed.forEach((itemConsumed, index) => {
if (!itemConsumed.itemId) return;
const item = propsById[itemConsumed.itemId];
if (!item || item.inactive){
if (!item || item.inactive) {
// Unlink if the item doesn't exist or is inactive
itemConsumed.itemId = undefined;
return;
@@ -78,60 +82,77 @@ function linkAction(dependencyGraph, prop, {propsById}){
});
}
function linkAdjustment(dependencyGraph, prop){
function linkAdjustment(dependencyGraph, prop) {
// Adjustment depends on its amount
dependOnCalc({dependencyGraph, prop, key: 'amount'});
dependOnCalc({ dependencyGraph, prop, key: 'amount' });
}
function linkAttribute(dependencyGraph, prop){
function linkAttribute(dependencyGraph, prop) {
linkVariableName(dependencyGraph, prop);
// Depends on spellSlotLevel
dependOnCalc({dependencyGraph, prop, key: 'spellSlotLevel'});
dependOnCalc({ dependencyGraph, prop, key: 'spellSlotLevel' });
// Depends on base value
dependOnCalc({dependencyGraph, prop, key: 'baseValue'});
dependOnCalc({ dependencyGraph, prop, key: 'baseValue' });
// hit dice depend on constitution
if (prop.attributeType === 'hitDice'){
if (prop.attributeType === 'hitDice') {
dependencyGraph.addLink(prop._id, 'constitution', 'hitDiceConMod');
}
}
function linkBranch(dependencyGraph, prop){
dependOnCalc({dependencyGraph, prop, key: 'condition'});
function linkBranch(dependencyGraph, prop) {
dependOnCalc({ dependencyGraph, prop, key: 'condition' });
}
function linkBuff(dependencyGraph, prop){
dependOnCalc({dependencyGraph, prop, key: 'duration'});
function linkBuff(dependencyGraph, prop) {
dependOnCalc({ dependencyGraph, prop, key: 'duration' });
}
function linkClassLevel(dependencyGraph, prop){
function linkClassLevel(dependencyGraph, prop) {
if (prop.inactive) return;
// The variableName of the prop depends on the prop
if (prop.variableName && prop.level){
if (prop.variableName && prop.level) {
dependencyGraph.addLink(prop.variableName, prop._id, 'classLevel');
// The level variable depends on the class variableName variable
let existingLevelLink = dependencyGraph.getLink('level', prop.variableName);
if (!existingLevelLink){
if (!existingLevelLink) {
dependencyGraph.addLink('level', prop.variableName, 'level');
}
}
}
function linkDamage(dependencyGraph, prop){
dependOnCalc({dependencyGraph, prop, key: 'amount'});
function linkDamage(dependencyGraph, prop) {
dependOnCalc({ dependencyGraph, prop, key: 'amount' });
}
function linkEffects(dependencyGraph, prop, computation){
function linkEffects(dependencyGraph, prop, computation) {
// The effect depends on its amount calculation
dependOnCalc({dependencyGraph, prop, key: 'amount'});
dependOnCalc({ dependencyGraph, prop, key: 'amount' });
// Inactive effects aren't going to impact their targeted stats
if (prop.inactive) return;
// The stats depend on the effect
if (prop.targetByTags){
if (prop.inactive) {
// Inactive effects apply to no stats
return;
} else if (prop.targetByTags) {
getEffectTagTargets(prop, computation).forEach(targetId => {
const targetProp = computation.propsById[targetId];
const key = prop.targetField || getDefaultCalculationField(targetProp);
const calcObj = get(targetProp, key);
if (calcObj && calcObj.calculation){
dependencyGraph.addLink(`${targetProp._id}.${key}`, prop._id , 'effect');
if (
(targetProp.type === 'attribute' || targetProp.type === 'skill')
&& targetProp.variableName
&& !prop.targetField
) {
// If the field wasn't specified and we're targeting an attribute or
// skill, just treat it like a normal effect on its variable name
dependencyGraph.addLink(targetProp.variableName, prop._id, 'effect');
} else {
// Otherwise target a field on that property
const key = prop.targetField || getDefaultCalculationField(targetProp);
const calcObj = get(targetProp, key);
if (calcObj && calcObj.calculation) {
dependencyGraph.addLink(`${targetProp._id}.${key}`, prop._id, 'effect');
}
}
});
} else {
@@ -143,17 +164,19 @@ function linkEffects(dependencyGraph, prop, computation){
}
// Returns an array of IDs of the properties the effect targets
function getEffectTagTargets(effect, computation){
const targets = getTargetListFromTags(effect.targetTags, computation);
const notIds = [];
if (effect.extraTags){
export function getEffectTagTargets(effect, computation) {
let targets = getTargetListFromTags(effect.targetTags, computation);
let notIds = [];
if (effect.extraTags) {
effect.extraTags.forEach(ex => {
if (ex.operation === 'OR'){
targets.push(...getTargetListFromTags(ex.tags, computation));
} else if (ex.operation === 'NOT'){
if (ex.operation === 'OR') {
targets = union(targets, getTargetListFromTags(ex.tags, computation));
} else if (ex.operation === 'NOT') {
ex.tags.forEach(tag => {
const idList = computation.propsWithTag[tag];
if (idList) notIds.push(...computation.propsWithTag[tag])
if (idList) {
notIds = union(notIds, computation.propsWithTag[tag]);
}
});
}
});
@@ -161,19 +184,19 @@ function getEffectTagTargets(effect, computation){
return difference(targets, notIds);
}
function getTargetListFromTags(tags, computation){
function getTargetListFromTags(tags, computation) {
const targetTagIdLists = [];
if (!tags) return [];
tags.forEach(tag => {
const idList = computation.propsWithTag[tag];
if (idList) targetTagIdLists.push(idList);
const idList = computation.propsWithTag[tag] || [];
targetTagIdLists.push(idList);
});
const targets = intersection(...targetTagIdLists);
return targets;
}
function getDefaultCalculationField(prop){
switch (prop.type){
function getDefaultCalculationField(prop) {
switch (prop.type) {
case 'action': return 'attackRoll';
case 'adjustment': return 'amount';
case 'attribute': return 'baseValue';
@@ -195,7 +218,6 @@ function getDefaultCalculationField(prop){
case 'roll': return 'roll';
case 'savingThrow': return 'dc';
case 'skill': return 'baseValue';
case 'slotFiller': return null;
case 'slot': return 'quantityExpected';
case 'spellList': return 'attackRollBonus';
case 'spell': return null;
@@ -203,62 +225,126 @@ function getDefaultCalculationField(prop){
}
}
function linkRoll(dependencyGraph, prop){
dependOnCalc({dependencyGraph, prop, key: 'roll'});
function linkRoll(dependencyGraph, prop) {
dependOnCalc({ dependencyGraph, prop, key: 'roll' });
}
function linkVariableName(dependencyGraph, prop){
// The variableName of the prop depends on the prop
if (prop.variableName){
function linkVariableName(dependencyGraph, prop) {
// The variableName of the prop depends on the prop if the prop is active
if (prop.variableName && !prop.inactive) {
dependencyGraph.addLink(prop.variableName, prop._id, 'definition');
}
}
function linkDamageMultiplier(dependencyGraph, prop){
function linkDamageMultiplier(dependencyGraph, prop) {
if (prop.inactive) return;
prop.damageTypes.forEach(damageType => {
// Remove all non-letter characters from the damage name
const damageName = damageType.replace(/[^a-z]/gi, '')
dependencyGraph.addLink(`${damageName}Multiplier`, prop._id, prop.type);
dependencyGraph.addLink(damageName, prop._id, prop.type);
});
}
function linkProficiencies(dependencyGraph, prop){
function linkPointBuy(dependencyGraph, prop) {
dependOnCalc({ dependencyGraph, prop, key: 'min' });
dependOnCalc({ dependencyGraph, prop, key: 'max' });
dependOnCalc({ dependencyGraph, prop, key: 'cost' });
dependOnCalc({ dependencyGraph, prop, key: 'total' });
prop.values?.forEach(row => {
// Wrap the document in a new object so we don't bash it unintentionally
const pointBuyRow = {
...row,
type: 'pointBuyRow',
tableName: prop.name,
tableId: prop._id,
}
dependencyGraph.addNode(row._id, pointBuyRow);
linkVariableName(dependencyGraph, pointBuyRow);
dependOnCalc({ dependencyGraph, pointBuyRow, key: 'row.min' });
dependOnCalc({ dependencyGraph, pointBuyRow, key: 'row.max' });
dependOnCalc({ dependencyGraph, pointBuyRow, key: 'row.cost' });
});
if (prop.inactive) return;
}
function linkProficiencies(dependencyGraph, prop, computation) {
// The stats depend on the proficiency
prop.stats.forEach(statName => {
if (!statName) return;
dependencyGraph.addLink(statName, prop._id, prop.type);
});
if (prop.inactive) return;
if (prop.targetByTags) {
// Tag targeted proficiencies depend on the creature's proficiencyBonus,
// since they add it directly to the targeted field
dependencyGraph.addLink(prop._id, 'proficiencyBonus', 'skillProficiencyBonus');
getEffectTagTargets(prop, computation).forEach(targetId => {
const targetProp = computation.propsById[targetId];
if (
(targetProp.type === 'attribute' || targetProp.type === 'skill')
&& targetProp.variableName
&& !prop.targetField
) {
// If the field wasn't specified and we're targeting an attribute or
// skill, just treat it like a normal proficiency on its variable name
dependencyGraph.addLink(targetProp.variableName, prop._id, 'proficiency');
} else {
// Otherwise target a field on that property
const key = prop.targetField || getDefaultCalculationField(targetProp);
const calcObj = get(targetProp, key);
if (calcObj && calcObj.calculation) {
dependencyGraph.addLink(`${targetProp._id}.${key}`, prop._id, 'proficiency');
}
}
});
} else {
prop.stats.forEach(statName => {
if (!statName) return;
dependencyGraph.addLink(statName, prop._id, 'proficiency');
});
}
}
function linkSavingThrow(dependencyGraph, prop){
dependOnCalc({dependencyGraph, prop, key: 'dc'});
function linkSavingThrow(dependencyGraph, prop) {
dependOnCalc({ dependencyGraph, prop, key: 'dc' });
}
function linkSkill(dependencyGraph, prop){
function linkSkill(dependencyGraph, prop, computation) {
// Depends on base value
dependOnCalc({ dependencyGraph, prop, key: 'baseValue' });
// Link dependents
if (prop.inactive) return;
linkVariableName(dependencyGraph, prop);
// The prop depends on the variable references as the ability
if (prop.ability){
if (prop.ability) {
dependencyGraph.addLink(prop._id, prop.ability, 'skillAbilityScore');
}
// Skills depend on the creature's proficiencyBonus
dependencyGraph.addLink(prop._id, 'proficiencyBonus', 'skillProficiencyBonus');
// Depends on base value
dependOnCalc({dependencyGraph, prop, key: 'baseValue'});
// Skills can apply their value as a proficiency bonus to calculations based on tag
if (prop.targetByTags) {
getEffectTagTargets(prop, computation).forEach(targetId => {
const targetProp = computation.propsById[targetId];
// Always target a field on the target property, applying a skill to an attribute or
// other skill isn't supported
const key = prop.targetField || getDefaultCalculationField(targetProp);
const calcObj = get(targetProp, key);
if (calcObj && calcObj.calculation) {
dependencyGraph.addLink(`${targetProp._id}.${key}`, prop._id, 'proficiency');
}
});
}
}
function linkSlot(dependencyGraph, prop){
dependOnCalc({dependencyGraph, prop, key: 'quantityExpected'});
dependOnCalc({dependencyGraph, prop, key: 'slotCondition'});
function linkSlot(dependencyGraph, prop) {
dependOnCalc({ dependencyGraph, prop, key: 'quantityExpected' });
dependOnCalc({ dependencyGraph, prop, key: 'slotCondition' });
}
function linkSpellList(dependencyGraph, prop){
dependOnCalc({dependencyGraph, prop, key: 'maxPrepared'});
dependOnCalc({dependencyGraph, prop, key: 'attackRollBonus'});
dependOnCalc({dependencyGraph, prop, key: 'dc'});
function linkSpellList(dependencyGraph, prop) {
dependOnCalc({ dependencyGraph, prop, key: 'maxPrepared' });
dependOnCalc({ dependencyGraph, prop, key: 'attackRollBonus' });
dependOnCalc({ dependencyGraph, prop, key: 'dc' });
}
function linkToggle(dependencyGraph, prop){
function linkToggle(dependencyGraph, prop) {
linkVariableName(dependencyGraph, prop);
dependOnCalc({dependencyGraph, prop, key: 'condition'});
dependOnCalc({ dependencyGraph, prop, key: 'condition' });
}

View File

@@ -5,14 +5,14 @@ import { get, unset } from 'lodash';
import errorNode from '/imports/parser/parseTree/error.js';
import cyrb53 from '/imports/api/engine/computation/utility/cyrb53.js';
export default function parseCalculationFields(prop, schemas){
export default function parseCalculationFields(prop, schemas) {
discoverInlineCalculationFields(prop, schemas);
parseAllCalculationFields(prop, schemas);
}
function discoverInlineCalculationFields(prop, schemas){
function discoverInlineCalculationFields(prop, schemas) {
// For each key in the schema
schemas[prop.type].inlineCalculationFields().forEach( calcKey => {
schemas[prop.type]?.inlineCalculationFields?.()?.forEach(calcKey => {
// That ends in .inlineCalculations
applyFnToKey(prop, calcKey, (prop, key) => {
const inlineCalcObj = get(prop, key);
@@ -22,7 +22,7 @@ function discoverInlineCalculationFields(prop, schemas){
// Extract the calculations and store them on the property
let string = inlineCalcObj.text;
// If there is no text, delete the whole field
if (!string){
if (!string) {
unset(prop, calcKey);
return;
}
@@ -32,7 +32,7 @@ function discoverInlineCalculationFields(prop, schemas){
// Has the text, if it matches the existing hash, stop
const inlineCalcHash = cyrb53(inlineCalcObj.text);
if (inlineCalcHash === inlineCalcObj.hash){
if (inlineCalcHash === inlineCalcObj.hash) {
return;
}
inlineCalcObj.hash = inlineCalcHash;
@@ -41,7 +41,7 @@ function discoverInlineCalculationFields(prop, schemas){
// It will be re set including the embedded calculation at the end of
// the computation
let matches = string.matchAll(INLINE_CALCULATION_REGEX);
for (let match of matches){
for (let match of matches) {
let calculation = match[1];
inlineCalcObj.inlineCalculations.push({
calculation,
@@ -51,9 +51,9 @@ function discoverInlineCalculationFields(prop, schemas){
});
}
function parseAllCalculationFields(prop, schemas){
function parseAllCalculationFields(prop, schemas) {
// For each computed key in the schema
schemas[prop.type].computedFields().forEach( calcKey => {
schemas[prop.type]?.computedFields?.()?.forEach(calcKey => {
// Determine the level the calculation should compute down to
let parseLevel = schemas[prop.type].getDefinition(calcKey).parseLevel || 'reduce';
@@ -66,7 +66,7 @@ function parseAllCalculationFields(prop, schemas){
const calcObj = get(prop, key);
if (!calcObj) return;
// Delete the whole calculation object if the calculation string isn't set
if (!calcObj.calculation){
if (!calcObj.calculation) {
unset(prop, calcKey);
return;
}
@@ -84,10 +84,10 @@ function parseAllCalculationFields(prop, schemas){
});
}
function parseCalculation(calcObj){
function parseCalculation(calcObj) {
const calcHash = cyrb53(calcObj.calculation);
// If the cached parse calculation is equal to the calculation, skip
if (calcHash === calcObj.hash){
if (calcHash === calcObj.hash) {
return;
}
calcObj.hash = calcHash;
@@ -100,6 +100,6 @@ function parseCalculation(calcObj){
message: prettifyParseError(e),
};
calcObj.parseError = error;
calcObj.parseNode = errorNode.create({error});
calcObj.parseNode = errorNode.create({ error });
}
}

View File

@@ -1,9 +1,9 @@
import applyFnToKey from '../utility/applyFnToKey.js';
import { unset } from 'lodash';
export default function removeSchemaFields(schemas, prop){
export default function removeSchemaFields(schemas, prop) {
schemas.forEach(schema => {
schema.removeBeforeComputeFields().forEach(
schema?.removeBeforeComputeFields?.().forEach(
key => applyFnToKey(prop, key, unset)
);
});

View File

@@ -2,7 +2,7 @@ import { buildComputationFromProps } from '/imports/api/engine/computation/build
import { assert } from 'chai';
import clean from '../../utility/cleanProp.testFn.js';
export default function(){
export default function () {
const computation = buildComputationFromProps(testProperties);
const totalFilled = computation.propsById['slotId'].totalFilled;
assert.equal(totalFilled, 4);
@@ -13,24 +13,24 @@ var testProperties = [
clean({
_id: 'slotId',
type: 'propertySlot',
ancestors: [{id: 'charId'}],
ancestors: [{ id: 'charId' }],
}),
// Children
clean({
_id: 'slotFillerId',
type: 'slotFiller',
type: 'folder',
slotQuantityFilled: 3,
slotFillerType: 'item',
ancestors: [{id: 'charId'}, {id: 'slotId'}],
ancestors: [{ id: 'charId' }, { id: 'slotId' }],
}),
clean({
_id: 'slotChildId',
type: 'item',
ancestors: [{id: 'charId'}, {id: 'slotId'}],
ancestors: [{ id: 'charId' }, { id: 'slotId' }],
}),
clean({
_id: 'slotGrandchildId',
type: 'effect',
ancestors: [{id: 'charId'}, {id: 'slotId'}, {id: 'slotChildId'}],
ancestors: [{ id: 'charId' }, { id: 'slotId' }, { id: 'slotChildId' }],
}),
];

View File

@@ -1,8 +1,7 @@
import { nodeArrayToTree } from '/imports/api/parenting/nodesToTree.js';
import CreatureProperties,
{ DenormalisedOnlyCreaturePropertySchema as denormSchema }
import { DenormalisedOnlyCreaturePropertySchema as denormSchema }
from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import { getProperties, getCreature, getVariables } from '/imports/api/engine/loadCreatures.js';
import computedOnlySchemas from '/imports/api/properties/computedOnlyPropertySchemasIndex.js';
import computedSchemas from '/imports/api/properties/computedPropertySchemasIndex.js';
import linkInventory from './buildComputation/linkInventory.js';
@@ -13,7 +12,7 @@ import computeToggleDependencies from './buildComputation/computeToggleDependenc
import linkCalculationDependencies from './buildComputation/linkCalculationDependencies.js';
import linkTypeDependencies from './buildComputation/linkTypeDependencies.js';
import computeSlotQuantityFilled from './buildComputation/computeSlotQuantityFilled.js';
import CreatureComputation from './CreatureComputation.js';
import CreatureComputation from './CreatureComputation.ts';
import removeSchemaFields from './buildComputation/removeSchemaFields.js';
/**
@@ -30,31 +29,17 @@ import removeSchemaFields from './buildComputation/removeSchemaFields.js';
* computed toggles
*/
export default function buildCreatureComputation(creatureId){
export default function buildCreatureComputation(creatureId) {
const creature = getCreature(creatureId);
const variables = getVariables(creatureId);
const properties = getProperties(creatureId);
const computation = buildComputationFromProps(properties, creature);
const computation = buildComputationFromProps(properties, creature, variables);
return computation;
}
function getProperties(creatureId){
return CreatureProperties.find({
'ancestors.id': creatureId,
'removed': {$ne: true},
}, {
sort: {order: 1}
}).fetch();
}
export function buildComputationFromProps(properties, creature, variables) {
function getCreature(creatureId){
return Creatures.findOne(creatureId, {
denormalizedStats: 1,
});
}
export function buildComputationFromProps(properties, creature){
const computation = new CreatureComputation(properties);
const computation = new CreatureComputation(properties, creature, variables);
// Dependency graph where edge(a, b) means a depends on b
// The graph includes all dependencies even of inactive properties
// such that any properties changing without changing their dependencies
@@ -64,14 +49,14 @@ export function buildComputationFromProps(properties, creature){
const dependencyGraph = computation.dependencyGraph;
// Link the denormalizedStats from the creature
if (creature && creature.denormalizedStats){
if (creature.denormalizedStats.xp){
if (creature && creature.denormalizedStats) {
if (creature.denormalizedStats.xp) {
dependencyGraph.addNode('xp', {
baseValue: creature.denormalizedStats.xp,
type: '_variable'
});
}
if (creature.denormalizedStats.milestoneLevels){
if (creature.denormalizedStats.milestoneLevels) {
dependencyGraph.addNode('milestoneLevels', {
baseValue: creature.denormalizedStats.milestoneLevels,
type: '_variable'
@@ -81,8 +66,10 @@ export function buildComputationFromProps(properties, creature){
// Process the properties one by one
properties.forEach(prop => {
// The prop has been processed, it's no longer dirty
delete prop.dirty;
let computedSchema = computedOnlySchemas[prop.type];
const computedSchema = computedOnlySchemas[prop.type];
removeSchemaFields([computedSchema, denormSchema], prop);
// Add a place to store all the computation details
@@ -102,7 +89,11 @@ export function buildComputationFromProps(properties, creature){
// Walk the property trees computing things that need to be inherited
walkDown(forest, node => {
computeInactiveStatus(node);
computeToggleDependencies(node, dependencyGraph);
});
// Inactive status must be complete for the whole tree before toggle deps
// are calculated
walkDown(forest, node => {
computeToggleDependencies(node, dependencyGraph, computation, forest);
computeSlotQuantityFilled(node, dependencyGraph);
});
@@ -114,5 +105,6 @@ export function buildComputationFromProps(properties, creature){
linkTypeDependencies(dependencyGraph, prop, computation);
linkCalculationDependencies(dependencyGraph, prop, computation);
});
return computation;
}

View File

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

View File

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

View File

@@ -1,12 +1,17 @@
import evaluateCalculation from '../../utility/evaluateCalculation.js';
import { getPropertyName } from '/imports/constants/PROPERTIES.js';
export default function computeCalculation(computation, node){
export default function computeCalculation(computation, node) {
const calcObj = node.data;
evaluateCalculation(calcObj, computation.scope);
if (calcObj.effects || calcObj.proficiencies) {
calcObj.baseValue = calcObj.value;
}
aggregateCalculationEffects(node, computation);
aggregateCalculationProficiencies(node, computation);
}
export function aggregateCalculationEffects(node, computation){
function aggregateCalculationEffects(node, computation) {
const calcObj = node.data;
delete calcObj.effects;
computation.dependencyGraph.forEachLinkedNode(
@@ -34,15 +39,77 @@ export function aggregateCalculationEffects(node, computation){
},
true // enumerate only outbound links
);
if (calcObj.effects && typeof calcObj.value === 'number'){
calcObj.baseValue = calcObj.value;
if (calcObj.effects && typeof calcObj.value === 'number') {
calcObj.effects.forEach(effect => {
if (
effect.operation === 'add' &&
effect.amount && typeof effect.amount.value === 'number'
){
) {
calcObj.value += effect.amount.value
}
});
}
}
function aggregateCalculationProficiencies(node, computation) {
const calcObj = node.data;
delete calcObj.proficiencies;
delete calcObj.proficiency;
let profBonus = computation.scope['proficiencyBonus']?.value || 0;
// Go through all the links and collect them on the calculation
computation.dependencyGraph.forEachLinkedNode(
node.id,
(linkedNode, link) => {
// Only proficiency links
if (link.data !== 'proficiency') return;
// That have data
if (!linkedNode.data) return;
// Ignoring inactive props
if (linkedNode.data.inactive) return;
// Compute the proficiency and value
let proficiency, value;
if (linkedNode.data.type === 'proficiency') {
proficiency = linkedNode.data.value || 0;
// Multiply the proficiency bonus by the actual proficiency
if (proficiency === 0.49) {
// Round down proficiency bonus in the special case
value = Math.floor(profBonus * 0.5);
} else {
value = Math.ceil(profBonus * proficiency);
}
} else if (linkedNode.data.type === 'skill') {
value = linkedNode.data.value || 0;
proficiency = linkedNode.data.proficiency || 0;
}
// Collate proficiencies
calcObj.proficiencies = calcObj.proficiencies || [];
calcObj.proficiencies.push({
_id: linkedNode.data._id,
name: linkedNode.data.name,
type: linkedNode.data.type,
proficiency,
value,
});
},
true // enumerate only outbound links
);
// Apply the highest proficiency, marking all others as overridden
if (calcObj.proficiencies && typeof calcObj.value === 'number') {
calcObj.proficiency = 0;
calcObj.proficiencyBonus = 0;
let currentProf;
calcObj.proficiencies.forEach(prof => {
if (prof.value > calcObj.proficiencyBonus) {
if (currentProf) currentProf.overridden = true;
calcObj.proficiencyBonus = prof.value;
calcObj.proficiency = prof.proficiency;
currentProf = prof;
} else {
prof.overridden = true;
}
});
calcObj.value += calcObj.proficiencyBonus;
}
}

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
export default function computSlot(computation, node){
export default function computeSlot(computation, node) {
const prop = node.data;
if (prop.quantityExpected && prop.quantityExpected.value){
if (prop.quantityExpected && prop.quantityExpected.value) {
prop.spaceLeft = prop.quantityExpected.value - prop.totalFilled;
}
}

View File

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

View File

@@ -0,0 +1,7 @@
export default function computeToggle(computation, node) {
const prop = node.data;
if (!prop.enabled && !prop.disabled && prop.condition && !prop.condition.value) {
prop.inactive = true;
prop.deactivatedBySelf = true;
}
}

View File

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

View File

@@ -1,22 +1,36 @@
import { pick } from 'lodash';
export default function aggregateDamageMultipliers({node, linkedNode, link}){
if (link.data !== 'damageMultiplier') return;
const multiplierValue = linkedNode.data.value;
if (multiplierValue === undefined) return;
// Store an aggregator, its presence indicates damage multipliers target this
// variable
if (!node.data.multiplierAggregator) node.data.multiplierAggregator = {
immunityCount: 0,
resistanceCount: 0,
vulnerabilityCount: 0,
immunities: [],
resistances: [],
vulnerabilities: [],
}
// Store a short reference to the aggregator
const aggregator = node.data.multiplierAggregator;
// Sum the counts of each type of multiplier
// Make a stripped down copy of the multiplier to store in the aggregator
const keysToStore = ['_id', 'name'];
if (linkedNode.data.excludeTags?.length){
keysToStore.push('excludeTags');
}
if (linkedNode.data.includeTags?.length){
keysToStore.push('includeTags');
}
const storedMultiplier = pick(linkedNode.data, keysToStore);
// Store the multiplier in the appropriate field
if (multiplierValue === 0){
aggregator.immunityCount += 1;
aggregator.immunities.push(storedMultiplier);
} else if (multiplierValue === 0.5){
aggregator.resistanceCount += 1;
aggregator.resistances.push(storedMultiplier);
} else if (multiplierValue === 2){
aggregator.vulnerabilityCount += 1;
aggregator.vulnerabilities.push(storedMultiplier);
}
}

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