Compare commits

...

127 Commits

Author SHA1 Message Date
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
240 changed files with 7365 additions and 4197 deletions

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.

View File

@@ -3,29 +3,30 @@
# 'meteor add' and 'meteor remove' will edit this file for you,
# but you can also edit it by hand.
accounts-password@2.3.1
zegenie:redis-oplog
accounts-password@2.3.4
random@1.2.1
underscore@1.0.11
underscore@1.0.13
dburles:mongo-collection-instances
accounts-google@1.4.0
email@2.2.2
email@2.2.5
meteor-base@1.5.1
mobile-experience@1.1.0
mongo@1.16.1
mongo@1.16.6
session@1.2.1
tracker@1.2.1
logging@1.3.1
tracker@1.3.2
logging@1.3.2
reload@1.3.1
ejson@1.1.3
check@1.3.2
standard-minifier-js@2.8.1
shell-server@0.5.0
ecmascript@0.16.3
ecmascript@0.16.7
es5-shim@4.8.0
service-configuration@1.3.1
dynamic-import@0.7.2
ddp-rate-limiter@1.1.1
rate-limit@1.0.9
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,7 +39,7 @@ simple:rest-method-mixin
mikowals:batch-insert
peerlibrary:subscription-data
zer0th:meteor-vuetify-loader
akryum:vue-component
akryum:vue-component@0.15.2
akryum:vue-router2
percolate:migrations
meteortesting:mocha
@@ -46,6 +47,7 @@ ostrio:files
simple:rest-bearer-token-parser
simple:rest-json-error-handler
littledata:synced-cron
mdg:meteor-apm-agent
typescript@4.5.4
#mdg:meteor-apm-agent
typescript@4.9.4
seba:minifiers-autoprefixer
mixmax:smart-disconnect

View File

@@ -1 +1 @@
METEOR@2.8.1
METEOR@2.12

View File

@@ -1,7 +1,7 @@
accounts-base@2.2.5
accounts-base@2.2.8
accounts-google@1.4.0
accounts-oauth@1.4.1
accounts-password@2.3.1
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,7 +12,7 @@ aldeed:collection2@3.5.0
aldeed:schema-index@3.0.0
allow-deny@1.1.1
autoupdate@1.8.0
babel-compiler@7.9.2
babel-compiler@7.10.4
babel-runtime@1.5.1
base64@1.0.12
binary-heap@1.0.11
@@ -21,7 +21,7 @@ boilerplate-generator@1.7.1
bozhao:link-accounts@2.6.1
caching-compiler@1.2.2
caching-html-compiler@1.2.1
callback-hook@1.4.0
callback-hook@1.5.1
check@1.3.2
coffeescript@2.4.1
coffeescript-compiler@2.4.1
@@ -29,20 +29,20 @@ dburles:mongo-collection-instances@0.3.6
ddp@1.4.1
ddp-client@2.6.1
ddp-common@1.4.0
ddp-rate-limiter@1.1.1
ddp-server@2.6.0
ddp-rate-limiter@1.2.0
ddp-server@2.6.1
diff-sequence@1.1.2
dynamic-import@0.7.2
ecmascript@0.16.3
ecmascript-runtime@0.8.0
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.3
email@2.2.2
email@2.2.5
es5-shim@4.8.0
fetch@0.1.2
fetch@0.1.3
geojson-utils@1.0.11
google-oauth@1.4.2
google-oauth@1.4.3
hot-code-push@1.0.4
html-tools@1.1.3
htmljs@1.1.1
@@ -52,36 +52,34 @@ inter-process-messaging@0.1.1
lai:collection-extensions@0.3.0
launch-screen@1.3.0
littledata:synced-cron@1.5.1
livedata@1.0.18
localstorage@1.2.0
logging@1.3.1
mdg:meteor-apm-agent@3.5.1
mdg:validated-method@1.2.0
meteor@1.10.2
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.1
minifier-css@1.6.4
minifier-js@2.7.5
minimongo@1.9.0
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.9
modules@0.19.0
modules-runtime@0.13.1
mongo@1.16.1
mongo@1.16.6
mongo-decimal@0.1.3
mongo-dev-server@1.1.0
mongo-id@1.0.8
mongo-livedata@1.0.12
npm-mongo@4.11.0
oauth@2.1.2
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.2
ostrio:files@2.3.2
ostrio:files@2.3.3
patreon-oauth@0.1.0
peerlibrary:assert@0.3.0
peerlibrary:check-extension@0.7.0
@@ -94,11 +92,11 @@ peerlibrary:reactive-publish@0.10.0
peerlibrary:server-autorun@0.8.0
peerlibrary:subscription-data@0.8.0
percolate:migrations@1.1.0
promise@0.12.1
promise@0.12.2
raix:eventemitter@1.0.0
random@1.2.1
rate-limit@1.0.9
react-fast-refresh@0.2.3
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
@@ -114,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.5.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.2
tmeasday:check-npm-versions@1.0.2
tracker@1.2.1
typescript@4.5.4
underscore@1.0.11
tracker@1.3.2
typescript@4.9.4
underscore@1.0.13
url@1.3.2
webapp@1.13.2
webapp@1.13.5
webapp-hashing@1.1.1
zegenie:redis-oplog@2.0.16
zer0th:meteor-vuetify-loader@0.1.41

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

@@ -13,12 +13,12 @@ import { incrementFileStorageUsed } from '/imports/api/users/methods/updateFileS
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, userId){
if (SCHEMA_VERSION < archive.meta.schemaVersion){
function restoreCreature(archive, userId) {
if (SCHEMA_VERSION < archive.meta.schemaVersion) {
throw new Meteor.Error('Incompatible',
'The archive file is from a newer version. Update required to read.')
}
@@ -35,7 +35,7 @@ function restoreCreature(archive, userId){
});
if (existingCreature) throw new Meteor.Error('Already exists',
'The creature you are trying to restore already exists.')
// Ensure the user owns the restored creature
archive.creature.owner = userId;
@@ -44,13 +44,13 @@ function restoreCreature(archive, userId){
Creatures.insert(archive.creature);
try {
// Add all the properties
if (archive.properties && archive.properties.length){
if (archive.properties && archive.properties.length) {
CreatureProperties.batchInsert(archive.properties);
}
if (archive.experiences && archive.experiences.length){
if (archive.experiences && archive.experiences.length) {
Experiences.batchInsert(archive.experiences);
}
if (archive.logs && archive.logs.length){
if (archive.logs && archive.logs.length) {
CreatureLogs.batchInsert(archive.logs);
}
} catch (e) {
@@ -73,23 +73,23 @@ const restoreCreaturefromFile = new ValidatedMethod({
numRequests: 10,
timeInterval: 5000,
},
async run({fileId}) {
async run({ fileId }) {
// fetch the file
const file = ArchiveCreatureFiles.findOne({_id: fileId}).get();
if (!file){
const file = ArchiveCreatureFiles.findOne({ _id: fileId }).get();
if (!file) {
throw new Meteor.Error('File not found',
'The requested creature archive does not exist');
'The requested creature archive does not exist');
}
// Assert ownership
const userId = file?.userId;
if (!userId || userId !== this.userId){
if (!userId || userId !== this.userId) {
throw new Meteor.Error('Permission denied',
'You can only restore creatures you own');
'You can only restore creatures you own');
}
assertHasCharactersSlots(this.userId);
if (Meteor.isServer){
if (Meteor.isServer) {
// Read the file data
const archive = await ArchiveCreatureFiles.readJSONFile(file);
restoreCreature(archive, this.userId);

View File

@@ -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,6 +88,12 @@ 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,

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

@@ -64,12 +64,12 @@ export function damagePropertyWork({ prop, operation, value, actionContext, logF
// Save the value to the scope before applying the before triggers
if (operation === 'increment') {
if (value >= 0) {
actionContext.scope['$damage'] = value;
actionContext.scope['~damage'] = { value };
} else {
actionContext.scope['$healing'] = -value;
actionContext.scope['~healing'] = { value: -value };
}
} else {
actionContext.scope['$set'] = value;
actionContext.scope['~set'] = { value };
}
applyTriggers(actionContext.triggers?.damageProperty?.before, prop, actionContext);
@@ -77,12 +77,12 @@ export function damagePropertyWork({ prop, operation, value, actionContext, logF
// 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 = actionContext.scope['~damage']?.value;
} else {
value = -actionContext.scope['$healing'];
value = -actionContext.scope['~healing']?.value;
}
} else {
value = actionContext.scope['$set'];
value = actionContext.scope['~set']?.value;
}
let damage, newValue, increment;

View File

@@ -42,6 +42,11 @@ 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,

View File

@@ -1,17 +1,19 @@
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,
}]
});
@@ -19,44 +21,43 @@ export default function getSlotFillFilter({slot, libraryIds}){
filter.$and.push({
$or: [{
type: 'classLevel',
},{
type: 'slotFiller',
}, {
slotFillerType: 'classLevel',
}]
});
if (slot.variableName) {
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};
filter.level = { $in: slot.missingLevels };
} else {
filter.level = (slot.level || 0) + 1;
filter.level = { $gt: slot.level || 0 };
}
}
let tagsOr = [];
let tagsNin = [];
if (slot.slotTags && slot.slotTags.length){
tagsOr.push({tags: {$all: slot.slotTags}});
if (slot.slotTags && slot.slotTags.length) {
tagsOr.push({ libraryTags: { $all: slot.slotTags } });
}
if (slot.extraTags && slot.extraTags.length){
if (slot.extraTags && slot.extraTags.length) {
slot.extraTags.forEach(extra => {
if (!extra.tags || !extra.tags.length) return;
if (extra.operation === 'OR'){
tagsOr.push({tags: {$all: extra.tags}});
} else if (extra.operation === 'NOT'){
if (extra.operation === 'OR') {
tagsOr.push({ libraryTags: { $all: extra.tags } });
} else if (extra.operation === 'NOT') {
tagsNin.push(...extra.tags);
}
});
}
if (tagsOr.length){
if (tagsOr.length) {
filter.$or = tagsOr;
}
if (tagsNin.length){
filter.$and.push({tags: {$nin: tagsNin}});
if (tagsNin.length) {
filter.$and.push({ libraryTags: { $nin: tagsNin } });
}
if (!filter.$and.length){
if (!filter.$and.length) {
delete filter.$and;
}
return filter;

View File

@@ -0,0 +1,85 @@
import { assert } from 'chai';
import getSlotFillFilter from '/imports/api/creature/creatureProperties/methods/getSlotFillFilter.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,4 +1,5 @@
import '/imports/api/creature/creatureProperties/methods/adjustQuantity.js';
import '/imports/api/creature/creatureProperties/methods/copyPropertyToLibrary.js';
import '/imports/api/creature/creatureProperties/methods/damageProperty.js';
import '/imports/api/creature/creatureProperties/methods/duplicateProperty.js';
import '/imports/api/creature/creatureProperties/methods/equipItem.js';

View File

@@ -98,13 +98,14 @@ function insertPropertyFromNode(nodeId, ancestors, order) {
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);

View File

@@ -7,9 +7,10 @@ import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import { parse, prettifyParseError } from '/imports/parser/parser.js';
import resolve, { toString } from '/imports/parser/resolve.js';
const PER_CREATURE_LOG_LIMIT = 100;
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
const PER_CREATURE_LOG_LIMIT = 100;
if (Meteor.isServer) {
var sendWebhookAsCreature = require('/imports/server/discord/sendWebhook.js').sendWebhookAsCreature;
}
@@ -70,10 +71,21 @@ function logToMessageData(log) {
let embed = {
fields: [],
};
log.content.forEach(field => {
log.content.forEach((field, index) => {
// Empty character for blank names
if (!field.name) field.name = '\u200b';
if (!field.value) field.value = '\u200b';
embed.fields.push(field);
// Enforce Discord field character limits
if (field.name?.length > 256) {
field.name = field.name.substring(0, 255);
}
if (field.value?.length > 1024) {
field.value = field.value.substring(0, 1024 - 3) + '...';
}
// Enforce Discord 25 field limit
if (index < 25) {
embed.fields.push(field);
}
});
return { embeds: [embed] };
}
@@ -122,7 +134,15 @@ export function insertCreatureLogWork({ log, creature, method }) {
log = { content: [{ value: log }] };
}
if (!log.content?.length) return;
// Truncate the string lengths to fit the log content schema
log.content.forEach((logItem) => {
if (logItem.value?.length > STORAGE_LIMITS.summary) {
logItem.value = logItem.value.substring(0, STORAGE_LIMITS.summary - 3) + '...';
}
});
log.date = new Date();
// Insert it
let id = CreatureLogs.insert(log);
if (Meteor.isServer) {

View File

@@ -8,6 +8,8 @@ import { damagePropertyWork } from '/imports/api/creature/creatureProperties/met
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, actionContext) {
applyNodeTriggers(node, 'before', actionContext);
@@ -51,11 +53,11 @@ export default function applyAction(node, actionContext) {
}
function applyAttackWithoutTarget({ attack, actionContext }) {
delete actionContext.scope['$attackHit'];
delete actionContext.scope['$attackMiss'];
delete actionContext.scope['$criticalHit'];
delete actionContext.scope['$criticalMiss'];
delete actionContext.scope['$attackRoll'];
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;
@@ -66,16 +68,16 @@ function applyAttackWithoutTarget({ attack, actionContext }) {
criticalMiss,
} = rollAttack(attack, scope);
let name = criticalHit ? 'Critical Hit!' : criticalMiss ? 'Critical Miss!' : 'To Hit';
if (scope['$attackAdvantage'] === 1) {
if (scope['~attackAdvantage']?.value === 1) {
name += ' (Advantage)';
} else if (scope['$attackAdvantage'] === -1) {
} else if (scope['~attackAdvantage']?.value === -1) {
name += ' (Disadvantage)';
}
if (!criticalMiss) {
scope['$attackHit'] = { value: true }
scope['~attackHit'] = { value: true }
}
if (!criticalHit) {
scope['$attackMiss'] = { value: true };
scope['~attackMiss'] = { value: true };
}
actionContext.addLog({
@@ -87,12 +89,12 @@ function applyAttackWithoutTarget({ attack, actionContext }) {
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'];
delete scope['~attackHit'];
delete scope['~attackMiss'];
delete scope['~criticalHit'];
delete scope['~criticalMiss'];
delete scope['~attackDiceRoll'];
delete scope['~attackRoll'];
recalculateCalculation(attack, actionContext);
@@ -109,9 +111,9 @@ function applyAttackToTarget({ attack, target, actionContext }) {
let name = criticalHit ? 'Critical Hit!' :
criticalMiss ? 'Critical Miss!' :
result > armor ? 'Hit!' : 'Miss!';
if (scope['$attackAdvantage'] === 1) {
if (scope['~attackAdvantage']?.value === 1) {
name += ' (Advantage)';
} else if (scope['$attackAdvantage'] === -1) {
} else if (scope['~attackAdvantage']?.value === -1) {
name += ' (Disadvantage)';
}
@@ -121,9 +123,9 @@ function applyAttackToTarget({ attack, target, actionContext }) {
inline: true,
});
if (criticalMiss || result < armor) {
scope['$attackMiss'] = { value: true };
scope['~attackMiss'] = { value: true };
} else {
scope['$attackHit'] = { value: true };
scope['~attackHit'] = { value: true };
}
} else {
actionContext.addLog({
@@ -141,7 +143,7 @@ function applyAttackToTarget({ attack, target, actionContext }) {
function rollAttack(attack, scope) {
const rollModifierText = numberToSignedString(attack.value, true);
let value, resultPrefix;
if (scope['$attackAdvantage'] === 1) {
if (scope['~attackAdvantage']?.value === 1) {
const [a, b] = rollDice(2, 20);
if (a >= b) {
value = a;
@@ -150,7 +152,7 @@ function rollAttack(attack, scope) {
value = b;
resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`;
}
} else if (scope['$attackAdvantage'] === -1) {
} else if (scope['~attackAdvantage']?.value === -1) {
const [a, b] = rollDice(2, 20);
if (a <= b) {
value = a;
@@ -163,23 +165,23 @@ function rollAttack(attack, scope) {
value = rollDice(1, 20)[0];
resultPrefix = `1d20 [${value}] ${rollModifierText}`
}
scope['$attackDiceRoll'] = { value };
scope['~attackDiceRoll'] = { value };
const result = value + attack.value;
scope['$attackRoll'] = { value: result };
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;
const criticalHitTarget = scope['~criticalHitTarget']?.value || 20;
let criticalHit = value >= criticalHitTarget;
let criticalMiss;
if (criticalHit) {
scope['$criticalHit'] = { value: true };
scope['~criticalHit'] = { value: true };
} else {
criticalMiss = value === 1;
if (criticalMiss) {
scope['$criticalMiss'] = { value: true };
scope['~criticalMiss'] = { value: true };
}
}
return { criticalHit, criticalMiss };
@@ -211,6 +213,7 @@ function spendResources(prop, actionContext) {
let itemQuantityAdjustments = [];
let spendLog = [];
let gainLog = [];
let ammoChildren = [];
try {
prop.resources.itemsConsumed.forEach(itemConsumed => {
recalculateCalculation(itemConsumed.quantity, actionContext);
@@ -221,9 +224,6 @@ function spendResources(prop, actionContext) {
if (!item || item.ancestors[0].id !== prop.ancestors[0].id) {
throw 'The prop\'s ammo was not found on the creature';
}
if (!item.equipped) {
throw 'The selected ammo is not equipped';
}
if (
!itemConsumed.quantity.value ||
!isFinite(itemConsumed.quantity.value)
@@ -242,6 +242,7 @@ function spendResources(prop, actionContext) {
} else if (itemConsumed.quantity.value < 0) {
gainLog.push(logName + ': ' + -itemConsumed.quantity.value);
}
ammoChildren.push(...getItemChildren(item, actionContext, prop));
});
} catch (e) {
actionContext.addLog({
@@ -303,4 +304,36 @@ function spendResources(prop, actionContext) {
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

@@ -3,22 +3,22 @@ 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, actionContext){
export default function applyBranch(node, actionContext) {
applyNodeTriggers(node, 'before', actionContext);
const applyChildren = function(){
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, actionContext);
if (prop.condition?.value) applyChildren();
break;
case 'index':
if (node.children.length){
if (node.children.length) {
recalculateCalculation(prop.condition, actionContext);
if (!isFinite(prop.condition?.value)) {
actionContext.addLog({
@@ -35,31 +35,31 @@ export default function applyBranch(node, actionContext){
}
break;
case 'hit':
if (scope['$attackHit']?.value){
if (!targets.length && !prop.silent) actionContext.addLog({value: '**On hit**'});
if (scope['~attackHit']?.value) {
if (!targets.length && !prop.silent) actionContext.addLog({ value: '**On hit**' });
applyChildren();
}
break;
case 'miss':
if (scope['$attackMiss']?.value){
if (!targets.length && !prop.silent) actionContext.addLog({value: '**On miss**'});
if (scope['~attackMiss']?.value) {
if (!targets.length && !prop.silent) actionContext.addLog({ value: '**On miss**' });
applyChildren();
}
break;
case 'failedSave':
if (scope['$saveFailed']?.value){
if (!targets.length && !prop.silent) actionContext.addLog({value: '**On failed save**'});
if (scope['~saveFailed']?.value) {
if (!targets.length && !prop.silent) actionContext.addLog({ value: '**On failed save**' });
applyChildren();
}
break;
case 'successfulSave':
if (scope['$saveSucceeded']?.value){
if (!targets.length && !prop.silent) actionContext.addLog({value: '**On save**',});
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;
applyNodeTriggers(node, 'after', actionContext);
applyProperty(node.children[index], actionContext);

View File

@@ -21,7 +21,10 @@ export default function applyBuff(node, actionContext) {
const prop = node.node;
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, { skipCrystalize } = {}) {
children.forEach(child => {

View File

@@ -27,7 +27,7 @@ export default function applyDamage(node, actionContext) {
// Choose target
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
@@ -73,12 +73,12 @@ export default function applyDamage(node, actionContext) {
damage = Math.floor(damage);
// Convert extra damage into the stored type
if (prop.damageType === 'extra' && scope['$lastDamageType']) {
prop.damageType = scope['$lastDamageType'];
if (prop.damageType === 'extra' && scope['~lastDamageType']?.value) {
prop.damageType = scope['~lastDamageType']?.value;
}
// Store current damage type
if (prop.damageType !== 'healing') {
scope['$lastDamageType'] = prop.damageType;
scope['~lastDamageType'] = { value: prop.damageType };
}
// Memoise the damage suffix for the log
@@ -194,14 +194,18 @@ function dealDamage({ target, damageType, amount, actionContext }) {
let healthBars = getPropertiesOfType(target._id, 'attribute');
// Keep only the healthbars that can take damage/healing
remove(healthBars, (bar) =>
bar.attributeType !== 'healthBar' ||
bar.inactive ||
bar.removed ||
bar.overridden ||
(amount >= 0 && bar.healthBarNoDamage) ||
(amount < 0 && bar.healthBarNoHealing)
);
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) => {

View File

@@ -4,22 +4,22 @@ import applyEffectsToCalculationParseNode from '/imports/api/engine/actions/appl
import resolve, { toString } from '/imports/parser/resolve.js';
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
export default function applyRoll(node, actionContext){
export default function applyRoll(node, actionContext) {
applyNodeTriggers(node, 'before', actionContext);
const prop = node.node;
const applyChildren = function(){
const applyChildren = function () {
applyNodeTriggers(node, 'after', actionContext);
node.children.forEach(child => applyProperty(child, actionContext));
};
if (prop.roll?.calculation){
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'){
const { result: rolled, context } = resolve('roll', prop.roll.parseNode, actionContext.scope);
if (rolled.parseType !== 'constant') {
logValue.push(toString(rolled));
}
logErrors(context.errors, actionContext);
@@ -28,28 +28,28 @@ export default function applyRoll(node, actionContext){
context.errors = [];
// Resolve the roll to a final value
const {result: reduced} = resolve('reduce', rolled, actionContext.scope, context);
const { result: reduced } = resolve('reduce', rolled, actionContext.scope, context);
logErrors(context.errors, actionContext);
// Store the result
if (reduced.parseType === 'constant'){
if (reduced.parseType === 'constant') {
prop.roll.value = reduced.value;
} else if (reduced.parseType === 'error'){
} else if (reduced.parseType === 'error') {
prop.roll.value = null;
} else {
prop.roll.value = toString(reduced);
}
// If we didn't end up with a constant of finite amount, give up
if (reduced?.parseType !== 'constant' || !isFinite(reduced.value)){
// 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;
actionContext.scope[prop.variableName] = { value };
logValue.push(`**${value}**`);
if (!prop.silent){
if (!prop.silent) {
actionContext.addLog({
name: prop.name,
value: logValue.join('\n'),

View File

@@ -8,6 +8,7 @@ import { applyUnresolvedEffects } from '/imports/api/engine/actions/doCheck.js';
export default function applySavingThrow(node, actionContext) {
applyNodeTriggers(node, 'before', actionContext);
const prop = node.node;
const originalTargets = actionContext.targets;
let saveTargets = prop.target === 'self' ? [actionContext.creature] : actionContext.targets;
@@ -31,22 +32,22 @@ export default function applySavingThrow(node, actionContext) {
// If there are no save targets, apply all children as if the save both
// succeeeded and failed
if (!saveTargets?.length) {
scope['$saveFailed'] = { value: true };
scope['$saveSucceeded'] = { value: true };
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 () {
applyNodeTriggers(node, 'after', actionContext);
actionContext.targets = [target]
applyNodeTriggers(node, 'after', actionContext);
node.children.forEach(child => applyProperty(child, actionContext));
};
@@ -90,14 +91,14 @@ export default function applySavingThrow(node, actionContext) {
value = values[0];
resultPrefix = `1d20 [ ${value} ] ${rollModifierText}`
}
scope['$saveDiceRoll'] = { value };
scope['~saveDiceRoll'] = { value };
const result = value + rollModifier || 0;
scope['$saveRoll'] = { value: result };
scope['~saveRoll'] = { value: result };
const saveSuccess = result >= dc;
if (saveSuccess) {
scope['$saveSucceeded'] = { value: true };
scope['~saveSucceeded'] = { value: true };
} else {
scope['$saveFailed'] = { value: true };
scope['~saveFailed'] = { value: true };
}
if (!prop.silent) actionContext.addLog({
name: saveSuccess ? 'Successful save' : 'Failed save',
@@ -106,4 +107,6 @@ export default function applySavingThrow(node, actionContext) {
});
return applyChildren();
});
// reset the targets after the save to each child
actionContext.targets = originalTargets;
}

View File

@@ -35,7 +35,7 @@ export function applyTrigger(trigger, prop, actionContext) {
if (trigger.inactive) {
return;
}
// Prevent triggers from firing if their condition is false
if (trigger.condition?.parseNode) {
recalculateCalculation(trigger.condition, actionContext);
@@ -61,11 +61,11 @@ export function applyTrigger(trigger, prop, actionContext) {
value: trigger.description,
inline: false,
}
if (trigger.description?.text){
if (trigger.description?.text) {
recalculateInlineCalculations(trigger.description, actionContext);
content.value = trigger.description.value;
}
if(!trigger.silent) actionContext.addLog(content);
if (!trigger.silent) actionContext.addLog(content);
// Get all the trigger's properties and apply them
const properties = getPropertyDecendants(actionContext.creature._id, trigger._id);
@@ -78,7 +78,7 @@ export function applyTrigger(trigger, prop, actionContext) {
trigger.firing = false;
}
function triggerMatchTags(trigger, prop) {
export function triggerMatchTags(trigger, prop) {
let matched = false;
const propTags = getEffectivePropTags(prop);
// Check the target tags
@@ -89,23 +89,26 @@ function triggerMatchTags(trigger, prop) {
matched = true;
}
// Check the extra tags
trigger.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;
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,16 +1,17 @@
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 { 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';
function cleanProp(prop){
function cleanProp(prop) {
let schema = CreatureProperties.simpleSchema(prop);
return schema.clean(prop);
}
function cleanCreature(creature){
function cleanCreature(creature) {
let schema = Creatures.simpleSchema(creature);
return schema.clean(creature);
}
@@ -28,7 +29,7 @@ const testActionContext = {
}),
scope: {},
addLog(content) {
if (content.name || content.value){
if (content.name || content.value) {
this.log.content.push(content);
}
},
@@ -40,8 +41,8 @@ const action = cleanProp({
});
const actionAncestors = [];
describe('Do Action', function(){
it('Does an empty action', function(){
describe('Do Action', function () {
it('Does an empty action', function () {
doActionWork({
properties: [action],
ancestors: actionAncestors,
@@ -51,3 +52,7 @@ describe('Do Action', function(){
});
//testTypes.forEach(test => it(test.text, test.fn));
});
describe('Action utility functions', function () {
it('Triggers match tags', applyTriggers);
})

View File

@@ -117,7 +117,8 @@ const doAction = new ValidatedMethod({
}
}
actionContext.scope['slotLevel'] = slotLevel;
actionContext.scope['slotLevel'] = { value: slotLevel };
actionContext.scope['~slotLevel'] = { value: slotLevel };
// Do the action
doActionWork({

View File

@@ -81,7 +81,7 @@ function rollCheck(prop, actionContext) {
rollModifier += effectBonus;
let value, values, resultPrefix;
if (scope['$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, actionContext) {
value = b;
resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText} = `;
}
} else if (scope['$checkAdvantage'] === -1) {
} else if (scope['~checkAdvantage']?.value === -1) {
logName += ' (Disadvantage)';
const [a, b] = rollDice(2, 20);
if (a <= b) {
@@ -107,9 +107,9 @@ function rollCheck(prop, actionContext) {
resultPrefix = `1d20 [ ${value} ] ${rollModifierText} = `
}
const result = (value + rollModifier) || 0;
scope['$checkDiceRoll'] = value;
scope['$checkRoll'] = result;
scope['$checkModifier'] = rollModifier;
scope['~checkDiceRoll'] = { value };
scope['~checkRoll'] = { value: result };
scope['~checkModifier'] = { value: rollModifier };
actionContext.addLog({
name: logName,
value: `${resultPrefix} **${result}**`,

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,16 +1,31 @@
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 condition compuation
// The child nodes depend on the toggle
child.node._computationDetails.toggleAncestors.push(prop);
dependencyGraph.addLink(child.node._id, prop._id, 'toggle');
});

View File

@@ -164,7 +164,7 @@ function linkEffects(dependencyGraph, prop, computation) {
}
// Returns an array of IDs of the properties the effect targets
function getEffectTagTargets(effect, computation) {
export function getEffectTagTargets(effect, computation) {
let targets = getTargetListFromTags(effect.targetTags, computation);
let notIds = [];
if (effect.extraTags) {
@@ -218,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;
@@ -268,20 +267,45 @@ function linkPointBuy(dependencyGraph, prop) {
if (prop.inactive) return;
}
function linkProficiencies(dependencyGraph, prop) {
function linkProficiencies(dependencyGraph, prop, computation) {
// The stats depend on the proficiency
if (prop.inactive) return;
prop.stats.forEach(statName => {
if (!statName) return;
dependencyGraph.addLink(statName, prop._id, prop.type);
});
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 linkSkill(dependencyGraph, prop) {
function linkSkill(dependencyGraph, prop, computation) {
// Depends on base value
dependOnCalc({ dependencyGraph, prop, key: 'baseValue' });
// Link dependents
@@ -293,6 +317,20 @@ function linkSkill(dependencyGraph, prop) {
}
// Skills depend on the creature's proficiencyBonus
dependencyGraph.addLink(prop._id, 'proficiencyBonus', 'skillProficiencyBonus');
// 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) {

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

@@ -29,7 +29,7 @@ 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);
@@ -37,7 +37,7 @@ export default function buildCreatureComputation(creatureId){
return computation;
}
export function buildComputationFromProps(properties, creature, variables){
export function buildComputationFromProps(properties, creature, variables) {
const computation = new CreatureComputation(properties, creature, variables);
// Dependency graph where edge(a, b) means a depends on b
@@ -49,14 +49,14 @@ export function buildComputationFromProps(properties, creature, variables){
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'
@@ -93,7 +93,7 @@ export function buildComputationFromProps(properties, creature, variables){
// Inactive status must be complete for the whole tree before toggle deps
// are calculated
walkDown(forest, node => {
computeToggleDependencies(node, dependencyGraph);
computeToggleDependencies(node, dependencyGraph, computation, forest);
computeSlotQuantityFilled(node, dependencyGraph);
});

View File

@@ -6,6 +6,7 @@ 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({
@@ -19,4 +20,5 @@ export default Object.freeze({
propertySlot,
spell: action,
spellList,
toggle,
});

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

@@ -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,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

@@ -1,16 +1,40 @@
import getAggregatorResult from './getAggregatorResult.js';
export default function computeVariableAsAttribute(computation, node, prop){
export default function computeVariableAsAttribute(computation, node, prop) {
let result = getAggregatorResult(node) || 0;
prop.total = result;
// Apply damage in a way that respects the damage rules, modifying damage if need be
// Bound the damage
if (!prop.ignoreLowerLimit && prop.damage > prop.total) {
console.log(`reducing damage from ${prop.damage} to ${prop.total}`);
prop.damage = prop.total;
}
if (!prop.ignoreUpperLimit && prop.damage < 0) {
console.log(`increasing damage from ${prop.damage} to 0`);
prop.damage = 0;
}
// Apply damage
prop.value = prop.total - (prop.damage || 0);
// Proficiency
prop.proficiency = node.data.proficiency;
// Advantage/disadvantage
const aggregator = node.data.effectAggregator;
if (aggregator) {
if (aggregator.advantage && !aggregator.disadvantage) {
prop.advantage = 1;
} else if (aggregator.disadvantage && !aggregator.advantage) {
prop.advantage = -1;
} else {
prop.advantage = 0;
}
}
// Ability scores get modifiers
if (prop.attributeType === 'ability'){
if (prop.attributeType === 'ability') {
prop.modifier = Math.floor((prop.value - 10) / 2);
}

View File

@@ -1,12 +1,12 @@
import aggregate from './aggregate/index.js';
export default function computeVariableAsSkill(computation, node, prop){
export default function computeVariableAsSkill(computation, node, prop) {
// Skills are based on some ability Modifier
let ability = computation.scope[prop.ability];
prop.abilityMod = ability?.modifier || 0;
// Inherit the ability's skill effects and proficiencies if skill is not a save
if (prop.skillType !== 'save' && ability){
if (prop.skillType !== 'save' && ability) {
aggregateAbilityEffects({
computation,
skillNode: node,
@@ -21,7 +21,7 @@ export default function computeVariableAsSkill(computation, node, prop){
let profBonus = computation.scope['proficiencyBonus']?.value || 0;
// Multiply the proficiency bonus by the actual proficiency
if(prop.proficiency === 0.49){
if (prop.proficiency === 0.49) {
// Round down proficiency bonus in the special case
profBonus = Math.floor(profBonus * 0.5);
} else {
@@ -37,7 +37,7 @@ export default function computeVariableAsSkill(computation, node, prop){
prop.effects = node.data.effects;
// If there is no aggregator, determine if the prop can hide, then exit
if (!aggregator){
if (!aggregator) {
prop.hide = statBase === undefined &&
prop.proficiency == 0 ||
undefined;
@@ -52,20 +52,32 @@ export default function computeVariableAsSkill(computation, node, prop){
if (aggregator.set !== undefined) {
result = aggregator.set;
}
if (Number.isFinite(result)){
if (Number.isFinite(result)) {
result = Math.floor(result);
}
prop.value = result;
// Advantage/disadvantage
if (aggregator.advantage && !aggregator.disadvantage){
if (aggregator.advantage && !aggregator.disadvantage) {
prop.advantage = 1;
} else if (aggregator.disadvantage && !aggregator.advantage){
} else if (aggregator.disadvantage && !aggregator.advantage) {
prop.advantage = -1;
} else {
prop.advantage = 0;
}
// Passive bonus
prop.passiveBonus = aggregator.passiveAdd;
// +/- 5 to passive bonus if the skill has advantage/disadvantage
if (
prop.advantage === 1
&& Number.isFinite(prop.passiveBonus)
) {
prop.passiveBonus += 5;
} else if (
prop.advantage === -1
&& Number.isFinite(prop.passiveBonus)
) {
prop.bassiveBonus -= 5;
}
// conditional benefits
prop.conditionalBenefits = aggregator.conditional;
// Roll bonuses
@@ -76,7 +88,8 @@ export default function computeVariableAsSkill(computation, node, prop){
prop.rollBonuses = aggregator.rollBonus;
}
function aggregateAbilityEffects({computation, skillNode, abilityNode}){
function aggregateAbilityEffects({ computation, skillNode, abilityNode }) {
if (!abilityNode?.id) return;
computation.dependencyGraph.forEachLinkedNode(
abilityNode.id,
(linkedNode, link) => {
@@ -85,15 +98,15 @@ function aggregateAbilityEffects({computation, skillNode, abilityNode}){
if (linkedNode.data.inactive) return;
// Check that the link is a valid effect/proficiency to pass on
// to a skill from its ability
if (link.data === 'effect'){
if (link.data === 'effect') {
if (![
'advantage', 'disadvantage', 'passiveAdd', 'fail', 'conditional'
].includes(linkedNode.data.operation)){
].includes(linkedNode.data.operation)) {
return;
}
}
// Apply the aggregations
let arg = {node: skillNode, linkedNode, link};
let arg = { node: skillNode, linkedNode, link };
aggregate.effect(arg);
aggregate.proficiency(arg);
},

View File

@@ -1,13 +1,16 @@
export default function evaluateToggles(computation, node){
export default function evaluateToggles(computation, node) {
let prop = node.data;
if (!prop) return;
let toggles = prop._computationDetails?.toggleAncestors;
if (!toggles) return;
toggles.forEach(toggle => {
if (!toggle.condition) return;
if (!toggle.condition.value){
if (
(!toggle.enabled && !toggle.disabled && toggle.condition && !toggle.condition.value)
|| (toggle.disabled)
) {
prop.inactive = true;
prop.deactivatedByToggle = true;
prop.deactivatingToggleId = toggle._id;
}
});
}

View File

@@ -0,0 +1,64 @@
import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation.js';
import { assert } from 'chai';
import computeCreatureComputation from '../../computeCreatureComputation.js';
import clean from '../../utility/cleanProp.testFn.js';
export default function () {
const computation = buildComputationFromProps(testProperties);
computeCreatureComputation(computation);
const prop = id => computation.propsById[id];
assert.equal(
prop('strengthId').value, 8,
'The proficiency bonus should not change the strength score'
);
assert.equal(
prop('strengthId').modifier, -1,
'The proficiency bonus should not change the strength modifier'
);
assert.exists(prop('actionId').attackRoll.proficiencies, 'The proficiency aggregator should be here')
assert.exists(prop('actionId').attackRoll.proficiencies[0], 'The proficiency should be here')
// attack roll = strength.mod + proficiencyBonus/2 rounded down
// = -1 + 13/2 = -1 + 6 = 5
assert.equal(
prop('actionId').attackRoll.value, 5,
'The proficiency should apply correctly to modify the attack roll'
);
}
var testProperties = [
clean({
_id: 'strengthId',
variableName: 'strength',
type: 'attribute',
attributeType: 'ability',
baseValue: {
calculation: '8'
},
}),
clean({
_id: 'actionId',
type: 'action',
ancestors: [{ id: 'charId' }],
attackRoll: {
calculation: 'strength.modifier',
},
tags: ['rapier', 'martial weapon', 'weapon', 'attack']
}),
clean({
_id: 'profBonusId',
type: 'attribute',
variableName: 'proficiencyBonus',
ancestors: [{ id: 'charId' }],
baseValue: {
calculation: '13'
},
}),
clean({
_id: 'tagTargetedProficiency',
type: 'proficiency',
stats: ['strength'], // Should be ignored, we are targeting by tags
value: 0.49,
targetByTags: true,
targetTags: ['martial weapon']
}),
];

View File

@@ -6,29 +6,33 @@ import computeInventory from './computeInventory.testFn.js';
import computeDamageMultipliers from './computeDamageMultipliers.testFn.js';
import computeEffects from './computeEffects.testFn.js';
import computeSkills from './computeSkills.testFn.js';
import computeProficiencies from './computeProficiencies.testFn.js';
export default [{
text: 'Computes actions',
fn: computeAction,
},{
}, {
text: 'Computes attributes',
fn: computeAttribute,
},{
}, {
text: 'Computes classes',
fn: computeClasses,
},{
}, {
text: 'Computes constants',
fn: computeConstants,
},{
}, {
text: 'Computes inventory',
fn: computeInventory,
},{
}, {
text: 'Computes damage multipliers',
fn: computeDamageMultipliers,
},{
}, {
text: 'Computes effects',
fn: computeEffects,
},{
}, {
text: 'Computes skills',
fn: computeSkills,
}, {
text: 'Computes proficiencies',
fn: computeProficiencies,
}];

View File

@@ -3,12 +3,12 @@ import { EJSON } from 'meteor/ejson';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import propertySchemasIndex from '/imports/api/properties/computedOnlyPropertySchemasIndex.js';
export default function writeAlteredProperties(computation){
export default function writeAlteredProperties(computation) {
let bulkWriteOperations = [];
// Loop through all properties on the memo
computation.props.forEach(changed => {
let schema = propertySchemasIndex[changed.type];
if (!schema){
if (!schema) {
console.warn('No schema for ' + changed.type);
return;
}
@@ -20,12 +20,13 @@ export default function writeAlteredProperties(computation){
'deactivatedBySelf',
'deactivatedByAncestor',
'deactivatedByToggle',
'deactivatingToggleId',
'damage',
'dirty',
...schema.objectKeys(),
];
op = addChangedKeysToOp(op, keys, original, changed);
if (op){
if (op) {
bulkWriteOperations.push(op);
}
});
@@ -37,10 +38,10 @@ function addChangedKeysToOp(op, keys, original, changed) {
// Loop through all keys that can be changed by computation
// and compile an operation that sets all those keys
for (let key of keys) {
if (!EJSON.equals(original[key], changed[key])){
if (!EJSON.equals(original[key], changed[key])) {
if (!op) op = newOperation(original._id, changed.type);
let value = changed[key];
if (value === undefined){
if (value === undefined) {
// Unset values that become undefined
addUnsetOp(op, key);
} else {
@@ -52,32 +53,32 @@ function addChangedKeysToOp(op, keys, original, changed) {
return op;
}
function newOperation(_id, type){
function newOperation(_id, type) {
let newOp = {
updateOne: {
filter: {_id},
filter: { _id },
update: {},
}
};
if (Meteor.isClient){
if (Meteor.isClient) {
newOp.type = type;
}
return newOp;
}
function addSetOp(op, key, value){
if (op.updateOne.update.$set){
function addSetOp(op, key, value) {
if (op.updateOne.update.$set) {
op.updateOne.update.$set[key] = value;
} else {
op.updateOne.update.$set = {[key]: value};
op.updateOne.update.$set = { [key]: value };
}
}
function addUnsetOp(op, key){
if (op.updateOne.update.$unset){
function addUnsetOp(op, key) {
if (op.updateOne.update.$unset) {
op.updateOne.update.$unset[key] = 1;
} else {
op.updateOne.update.$unset = {[key]: 1};
op.updateOne.update.$unset = { [key]: 1 };
}
}
@@ -100,14 +101,14 @@ function writePropertiesSequentially(bulkWriteOps) {
// in the UI because of incompatibility with latency compensation. If the
// duplicate redraws can be fixed, this is a strictly better way of processing
// writes
function bulkWriteProperties(bulkWriteOps){
function bulkWriteProperties(bulkWriteOps) {
if (!bulkWriteOps.length) return;
// bulkWrite is only available on the server
if (Meteor.isServer) {
CreatureProperties.rawCollection().bulkWrite(
bulkWriteOps,
{ordered : false},
function(e){
{ ordered: false },
function (e) {
if (e) {
console.error('Bulk write failed: ');
console.error(e);

View File

@@ -15,18 +15,21 @@ export default function writeScope(creatureId, computation) {
let $set, $unset;
for (const key in scope){
for (const key in scope) {
// Mongo can't handle keys that start with a dollar sign
if (key[0] === '$' || key[0] === '_') continue;
// Remove large properties that aren't likely to be accessed
delete scope[key].parent;
delete scope[key].ancestors;
// Remove empty keys
for (const subKey in scope[key]) {
if (scope[key][subKey] === undefined) {
delete scope[key][subKey]
}
}
// Only update changed fields
if (!EJSON.equals(variables[key], scope[key])) {
if (!$set) $set = {};
@@ -53,9 +56,19 @@ export default function writeScope(creatureId, computation) {
const update = {};
if ($set) update.$set = $set;
if ($unset) update.$unset = $unset;
CreatureVariables.update({_creatureId: creatureId}, update);
CreatureVariables.update({ _creatureId: creatureId }, update);
}
if (computation.creature?.dirty) {
Creatures.update({_id: creatureId}, {$unset: { dirty: 1 }});
Creatures.update({ _id: creatureId }, { $unset: { dirty: 1 } });
}
}
/*
function calculateSize(computation) {
const sizeEstimator = {
creature: computation.creature,
variables: computation.variables,
props: computation.originalPropsById,
};
return MongoInternals.NpmModule.BSON.calculateObjectSize(sizeEstimator, { checkKeys: false })
}
*/

View File

@@ -4,7 +4,7 @@ import writeAlteredProperties from './computation/writeComputation/writeAlteredP
import writeScope from './computation/writeComputation/writeScope.js';
import writeErrors from './computation/writeComputation/writeErrors.js';
export default function computeCreature(creatureId){
export default function computeCreature(creatureId) {
if (Meteor.isClient) return;
// console.log('compute ' + creatureId);
const computation = buildCreatureComputation(creatureId);
@@ -16,7 +16,7 @@ function computeComputation(computation, creatureId) {
computeCreatureComputation(computation);
writeAlteredProperties(computation);
writeScope(creatureId, computation);
} catch (e){
} catch (e) {
const errorText = e.reason || e.message || e.toString();
computation.errors.push({
type: 'crash',
@@ -32,6 +32,19 @@ function computeComputation(computation, creatureId) {
console.error(logError);
throw e;
} finally {
checkPropertyCount(computation)
writeErrors(creatureId, computation.errors);
}
}
const MAX_PROPS = 1000;
function checkPropertyCount(computation) {
const count = computation.props.length;
if (count <= MAX_PROPS) return;
computation.errors.push({
type: 'warning',
details: {
error: `This character sheet has too many properties and may perform poorly ( ${count} / ${MAX_PROPS} )`
},
});
}

View File

@@ -29,6 +29,16 @@ let LibrarySchema = new SimpleSchema({
optional: true,
max: STORAGE_LIMITS.summary,
},
showInMarket: {
index: 1,
type: Boolean,
optional: true,
},
subscriberCount: {
index: 1,
type: Number,
optional: true,
},
});
LibrarySchema.extend(SharingSchema);
@@ -104,6 +114,29 @@ const updateLibraryDescription = new ValidatedMethod({
},
});
const updateLibraryShowInMarket = new ValidatedMethod({
name: 'libraries.updateShowInMarket',
validate: new SimpleSchema({
_id: {
type: String,
regEx: SimpleSchema.RegEx.id
},
value: {
type: Boolean,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({ _id, value }) {
let library = Libraries.findOne(_id);
assertEditPermission(library, this.userId);
Libraries.update(_id, { $set: { showInMarket: value } });
},
});
const removeLibrary = new ValidatedMethod({
name: 'libraries.remove',
validate: new SimpleSchema({
@@ -130,4 +163,4 @@ export function removeLibaryWork(libraryId) {
LibraryNodes.remove({ 'ancestors.id': libraryId });
}
export { LibrarySchema, insertLibrary, updateLibraryName, updateLibraryDescription, removeLibrary };
export { LibrarySchema, insertLibrary, updateLibraryName, updateLibraryDescription, updateLibraryShowInMarket, removeLibrary };

View File

@@ -32,6 +32,16 @@ const LibraryCollectionSchema = new SimpleSchema({
type: String,
regEx: SimpleSchema.RegEx.Id,
},
showInMarket: {
index: 1,
type: Boolean,
optional: true,
},
subscriberCount: {
index: 1,
type: Number,
optional: true,
},
});
LibraryCollectionSchema.extend(SharingSchema);
@@ -48,12 +58,12 @@ const insertLibraryCollection = new ValidatedMethod({
run(libraryCollection) {
if (!this.userId) {
throw new Meteor.Error('LibraryCollections.methods.insert.denied',
'You need to be logged in to insert a library');
'You need to be logged in to insert a library');
}
let tier = getUserTier(this.userId);
if (!tier.paidBenefits){
if (!tier.paidBenefits) {
throw new Meteor.Error('LibraryCollections.methods.insert.denied',
`The ${tier.name} tier does not allow you to insert a library collection`);
`The ${tier.name} tier does not allow you to insert a library collection`);
}
libraryCollection.owner = this.userId;
return LibraryCollections.insert(libraryCollection);
@@ -72,7 +82,7 @@ const updateLibraryCollection = new ValidatedMethod({
},
update: {
type: LibraryCollectionSchema
.pick('name', 'description', 'libraries')
.pick('name', 'description', 'libraries', 'showInMarket')
.extend({ //make libraries optional
libraries: {
optional: true,
@@ -85,7 +95,7 @@ const updateLibraryCollection = new ValidatedMethod({
numRequests: 5,
timeInterval: 5000,
},
run({_id, update}){
run({ _id, update }) {
const libraryCollection = LibraryCollections.findOne(_id, {
fields: {
owner: 1,
@@ -93,7 +103,7 @@ const updateLibraryCollection = new ValidatedMethod({
}
});
assertEditPermission(libraryCollection, this.userId);
return LibraryCollections.update(_id, {$set: update});
return LibraryCollections.update(_id, { $set: update });
},
});
@@ -110,7 +120,7 @@ const removeLibraryCollection = new ValidatedMethod({
numRequests: 5,
timeInterval: 5000,
},
run({_id}){
run({ _id }) {
const libraryCollection = LibraryCollections.findOne(_id, {
fields: {
owner: 1,

View File

@@ -4,7 +4,7 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import SimpleSchema from 'simpl-schema';
import ColorSchema from '/imports/api/properties/subSchemas/ColorSchema.js';
import ChildSchema from '/imports/api/parenting/ChildSchema.js';
import ChildSchema, { RefSchema } from '/imports/api/parenting/ChildSchema.js';
import propertySchemasIndex from '/imports/api/properties/propertySchemasIndex.js';
import Libraries from '/imports/api/library/Libraries.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
@@ -15,6 +15,8 @@ import '/imports/api/library/methods/index.js';
import { updateReferenceNodeWork } from '/imports/api/library/methods/updateReferenceNode.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
import { restore } from '/imports/api/parenting/softRemove.js';
import { getAncestry } from '/imports/api/parenting/parenting.js';
import { reorderDocs } from '/imports/api/parenting/order.js';
let LibraryNodes = new Mongo.Collection('libraryNodes');
@@ -36,20 +38,66 @@ let LibraryNodeSchema = new SimpleSchema({
type: String,
max: STORAGE_LIMITS.tagLength,
},
icon: {
type: storedIconsSchema,
optional: true,
max: STORAGE_LIMITS.icon,
},
// Library-specific properties, these can be stripped from the resulting
// creature properties
// Will this property show up in the slot-fill dialog
fillSlots: {
type: Boolean,
optional: true,
index: 1,
},
// Will this property show up in the insert-from-library dialog
searchable: {
type: Boolean,
optional: true,
index: 1,
},
libraryTags: {
type: Array,
defaultValue: [],
optional: true,
maxCount: STORAGE_LIMITS.tagCount,
},
'libraryTags.$': {
type: String,
max: STORAGE_LIMITS.tagLength,
},
icon: {
type: storedIconsSchema,
// Overrides the type when searching for properties
slotFillerType: {
type: String,
optional: true,
max: STORAGE_LIMITS.icon,
}
max: STORAGE_LIMITS.variableName,
},
// Image to display when filling the slot
slotFillImage: {
type: String,
optional: true,
max: STORAGE_LIMITS.url,
},
// 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
},
// Filters out of UI if condition isn't met, but isn't otherwise enforced
slotFillerCondition: {
type: String,
optional: true,
max: STORAGE_LIMITS.calculation,
},
// Text to display if slot filler condition fails
slotFillerConditionNote: {
type: String,
optional: true,
max: STORAGE_LIMITS.calculation,
},
});
// Set up server side search index
@@ -86,20 +134,56 @@ function assertNodeEditPermission(node, userId) {
const insertNode = new ValidatedMethod({
name: 'libraryNodes.insert',
validate: null,
validate: new SimpleSchema({
libraryNode: {
type: Object,
blackbox: true,
},
parentRef: RefSchema,
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run(libraryNode) {
run({ libraryNode, parentRef }) {
// get the new ancestry
let { parentDoc, ancestors } = getAncestry({ parentRef });
// Check permission to edit
let root;
if (parentRef.collection === 'libraries') {
root = parentDoc;
} else if (parentRef.collection === 'libraryNodes') {
root = Libraries.findOne(parentDoc.ancestors[0].id);
} else {
throw `${parentRef.collection} is not a valid parent collection`
}
assertEditPermission(root, this.userId);
// Set the ancestry of the library node
libraryNode.parent = parentRef;
libraryNode.ancestors = ancestors;
// Remove its ID if it came with one to force a random one to be generated
// server-side
delete libraryNode._id;
assertNodeEditPermission(libraryNode, this.userId);
let nodeId = LibraryNodes.insert(libraryNode);
// Insert the node
const nodeId = LibraryNodes.insert(libraryNode);
// Update the node if it was a reference node
if (libraryNode.type == 'reference') {
libraryNode._id = nodeId;
updateReferenceNodeWork(libraryNode, this.userId);
}
// Tree structure changed by insert, reorder the tree
reorderDocs({
collection: LibraryNodes,
ancestorId: root._id,
});
// Return the id of the inserted node
return nodeId;
},
});
@@ -119,7 +203,7 @@ const updateLibraryNode = new ValidatedMethod({
},
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
numRequests: 15,
timeInterval: 5000,
},
run({ _id, path, value }) {

View File

@@ -4,6 +4,7 @@ import getUserLibraryIds from './getUserLibraryIds';
import { intersection, union } from 'lodash';
export default function getCreatureLibraryIds(creature, userId) {
if (!userId) console.log('no userId, returning empty array');
if (!userId) return [];
// Get the ids of libraries the user is permitted to view
@@ -17,14 +18,14 @@ export default function getCreatureLibraryIds(creature, userId) {
allowedLibraryCollections: 1,
}
});
if (!creature) return [];
if (!creature) return userLibIds;
}
// If the creature does not restrict the libraries, let it use them all
if (!creature.allowedLibraryCollections && !creature.allowedLibraries) {
return userLibIds;
}
// Get the ids of the libraries that the creature allows
const allowedCollections = creature.allowedLibraryCollections || [];
let creatureLibIds = creature.allowedLibraries || [];

View File

@@ -0,0 +1,18 @@
export default function getDefaultSlotFiller(slot) {
if (typeof slot !== 'object') throw 'getDefaultSlotFiller requires a slot';
if (slot.type !== 'propertySlot') throw 'provided slot must be a propertySlot';
let slotType = slot.slotType;
if (!slotType || slot.slotType === 'slotFiller') {
slotType = 'folder';
}
const filler = {
type: slotType,
libraryTags: slot.slotTags || [],
name: 'Custom ' + slot.name || 'slot filler',
parent: { collection: 'creatureProperties', id: slot._id },
ancestors: [...slot.ancestors, { collection: 'creatureProperties', id: slot._id }],
};
return filler;
}

View File

@@ -21,7 +21,7 @@ const updateReferenceNode = new ValidatedMethod({
numRequests: 5,
timeInterval: 5000,
},
run({_id}) {
run({ _id }) {
let userId = this.userId;
let node = LibraryNodes.findOne(_id);
assertDocEditPermission(node, userId);
@@ -29,15 +29,15 @@ const updateReferenceNode = new ValidatedMethod({
},
});
function writeCache(_id, cache){
LibraryNodes.update(_id, {$set: {cache}}, {
selector: {type: 'reference'},
function writeCache(_id, cache) {
LibraryNodes.update(_id, { $set: { cache } }, {
selector: { type: 'reference' },
});
}
function updateReferenceNodeWork(node, userId){
function updateReferenceNodeWork(node, userId) {
let cache = {}
if (!node.ref){
if (!node.ref?.collection || !node.ref?.id) {
writeCache(node._id, cache);
return;
}
@@ -45,20 +45,23 @@ function updateReferenceNodeWork(node, userId){
try {
doc = fetchDocByRef(node.ref);
if (doc.removed) throw 'Property has been deleted';
if (doc.ancestors[0].id !== node.ancestors[0].id){
if (doc.ancestors[0].id !== node.ancestors[0].id) {
library = fetchDocByRef(doc.ancestors[0]);
assertViewPermission(library, userId)
}
} catch(e){
cache = {error: e.reason || e.message || e.toString()}
} catch (e) {
cache = { error: e.reason || e.message || e.toString() }
writeCache(node._id, cache);
return;
}
cache = {
node: doc,
};
if (library){
cache.library = {name: library.name};
if (library) {
cache.library = {
id: library._id,
name: library.name,
};
}
writeCache(node._id, cache);
}

View File

@@ -26,6 +26,7 @@ export function nodeArrayToTree(nodes) {
forest.push(treeNode);
}
});
forest.nodeIndex = nodeIndex;
return forest;
}

View File

@@ -23,13 +23,20 @@ const organizeDoc = new ValidatedMethod({
type: Boolean,
optional: true,
},
skipClient: {
type: Boolean,
optional: true,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({docRef, parentRef, order, skipRecompute}) {
run({ docRef, parentRef, order, skipRecompute, skipClient }) {
if (skipClient && this.isSimulation) {
return;
}
let doc = fetchDocByRef(docRef);
let collection = getCollectionByName(docRef.collection);
// The user must be able to edit both the doc and its parent to move it
@@ -39,23 +46,23 @@ const organizeDoc = new ValidatedMethod({
assertDocEditPermission(parent, this.userId);
// Change the doc's parent
updateParent({docRef, parentRef});
updateParent({ docRef, parentRef });
// Change the doc's order to be a half step ahead of its target location
collection.update(doc._id, {$set: {order}}, {selector: {type: 'any'}});
collection.update(doc._id, { $set: { order } }, { selector: { type: 'any' } });
// Reorder both ancestors' documents
let oldAncestorId = doc.ancestors[0].id;
reorderDocs({collection, ancestorId: oldAncestorId});
reorderDocs({ collection, ancestorId: oldAncestorId });
let newAncestorId = getRootId(parent);
if (newAncestorId !== oldAncestorId){
reorderDocs({collection, ancestorId: newAncestorId});
if (newAncestorId !== oldAncestorId) {
reorderDocs({ collection, ancestorId: newAncestorId });
}
// Figure out which creatures need to be recalculated after this move
let docCreatures = getCreatureAncestors(doc);
let parentCreatures = getCreatureAncestors(parent);
if (!skipRecompute){
if (!skipRecompute) {
let creaturesToRecompute = union(docCreatures, parentCreatures);
// Mark the creatures for recompute
Creatures.update({
@@ -81,10 +88,10 @@ const reorderDoc = new ValidatedMethod({
numRequests: 5,
timeInterval: 5000,
},
run({docRef, order}) {
run({ docRef, order }) {
let doc = fetchDocByRef(docRef);
assertDocEditPermission(doc, this.userId);
safeUpdateDocOrder({docRef, order});
safeUpdateDocOrder({ docRef, order });
// Recompute the affected creatures
const ancestors = getCreatureAncestors(doc);
if (ancestors.length) {
@@ -97,22 +104,22 @@ const reorderDoc = new ValidatedMethod({
},
});
function getRootId(doc){
if (doc.ancestors && doc.ancestors.length && doc.ancestors[0]){
function getRootId(doc) {
if (doc.ancestors && doc.ancestors.length && doc.ancestors[0]) {
return doc.ancestors[0].id;
} else {
return doc._id;
}
}
function getCreatureAncestors(doc){
function getCreatureAncestors(doc) {
let ids = [];
if(doc.type === 'pc' || doc.type === 'npc' || doc.type === 'monster'){
if (doc.type === 'pc' || doc.type === 'npc' || doc.type === 'monster') {
ids.push(doc._id);
}
if (doc.ancestors){
if (doc.ancestors) {
doc.ancestors.forEach(ancestorRef => {
if (ancestorRef.collection === 'creatures'){
if (ancestorRef.collection === 'creatures') {
ids.push(ancestorRef.id);
}
});

View File

@@ -50,7 +50,6 @@ let ActionSchema = createPropertySchema({
attackRoll: {
type: 'fieldToCompute',
optional: true,
defaultValue: 'strength.modifier + proficiencyBonus',
},
// Calculation of how many times this action can be used
uses: {

View File

@@ -173,6 +173,13 @@ let ComputedOnlyAttributeSchema = createPropertySchema({
optional: true,
removeBeforeCompute: true,
},
// Attributes with advantage grant it to all skills based on the attribute
advantage: {
type: SimpleSchema.Integer,
optional: true,
allowedValues: [-1, 0, 1],
removeBeforeCompute: true,
},
// The computed creature constitution modifier for hit dice
constitutionMod: {
type: Number,

View File

@@ -26,12 +26,6 @@ const ClassLevelSchema = createPropertySchema({
defaultValue: 1,
max: STORAGE_LIMITS.levelMax,
},
// Filters out of UI if condition isn't met, but isn't otherwise enforced
slotFillerCondition: {
type: String,
optional: true,
max: STORAGE_LIMITS.calculation,
},
});
const ComputedOnlyClassLevelSchema = createPropertySchema({

View File

@@ -1,6 +1,7 @@
import SimpleSchema from 'simpl-schema';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
import TagTargetingSchema from '/imports/api/properties/subSchemas/TagTargetingSchema.js';
/*
* Effects are reason-value attached to skills and abilities
@@ -50,57 +51,7 @@ let EffectSchema = createPropertySchema({
type: String,
max: STORAGE_LIMITS.variableName,
},
// True when targeting by tags instead of stats
targetByTags: {
type: Boolean,
optional: true,
},
// If targeting by tags, the field which will be targeted
targetField: {
type: String,
optional: true,
max: STORAGE_LIMITS.variableName,
},
// Which tags the effect is applied to
targetTags: {
type: Array,
optional: true,
maxCount: STORAGE_LIMITS.tagCount,
},
'targetTags.$': {
type: String,
max: STORAGE_LIMITS.tagLength,
},
extraTags: {
type: Array,
optional: true,
maxCount: STORAGE_LIMITS.extraTagsCount,
},
'extraTags.$': {
type: Object,
},
'extraTags.$._id': {
type: String,
regEx: SimpleSchema.RegEx.Id,
autoValue(){
if (!this.isSet) return Random.id();
}
},
'extraTags.$.operation': {
type: String,
allowedValues: ['OR', 'NOT'],
defaultValue: 'OR',
},
'extraTags.$.tags': {
type: Array,
defaultValue: [],
maxCount: STORAGE_LIMITS.tagCount,
},
'extraTags.$.tags.$': {
type: String,
max: STORAGE_LIMITS.tagLength,
},
});
}).extend(TagTargetingSchema);
const ComputedOnlyEffectSchema = createPropertySchema({
amount: {

View File

@@ -1,14 +1,19 @@
import SimpleSchema from 'simpl-schema';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
// Folders organize a character sheet into a tree, particularly to group things
// like 'race' and 'background'
let FolderSchema = new createPropertySchema({
let FolderSchema = createPropertySchema({
name: {
type: String,
max: STORAGE_LIMITS.name,
optional: true,
},
description: {
type: 'inlineCalculationFieldToCompute',
optional: true,
},
groupStats: {
type: Boolean,
optional: true,
@@ -33,6 +38,15 @@ let FolderSchema = new createPropertySchema({
},
});
const ComputedOnlyFolderSchema = new createPropertySchema({});
const ComputedOnlyFolderSchema = createPropertySchema({
description: {
type: 'computedOnlyInlineCalculationField',
optional: true,
},
});
export { FolderSchema, ComputedOnlyFolderSchema };
const ComputedFolderSchema = new SimpleSchema()
.extend(FolderSchema)
.extend(ComputedOnlyFolderSchema);
export { FolderSchema, ComputedFolderSchema, ComputedOnlyFolderSchema };

View File

@@ -1,5 +1,6 @@
import SimpleSchema from 'simpl-schema';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
import TagTargetingSchema from '/imports/api/properties/subSchemas/TagTargetingSchema.js';
let ProficiencySchema = new SimpleSchema({
name: {
@@ -24,7 +25,7 @@ let ProficiencySchema = new SimpleSchema({
allowedValues: [0.49, 0.5, 1, 2],
defaultValue: 1,
},
});
}).extend(TagTargetingSchema);
const ComputedOnlyProficiencySchema = new SimpleSchema({});

View File

@@ -51,6 +51,10 @@ let ReferenceSchema = new SimpleSchema({
type: Object,
optional: true,
},
'cache.library.id': {
type: String,
optional: true,
},
'cache.library.name': {
type: String,
optional: true,

View File

@@ -2,6 +2,7 @@ import SimpleSchema from 'simpl-schema';
import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
import TagTargetingSchema from '/imports/api/properties/subSchemas/TagTargetingSchema.js';
/*
* Skills are anything that results in a modifier to be added to a D20
@@ -59,7 +60,8 @@ let SkillSchema = createPropertySchema({
type: 'inlineCalculationFieldToCompute',
optional: true,
},
});
// Skills can apply their value to other calculations as a proficiency using tag targeting
}).extend(TagTargetingSchema);
let ComputedOnlySkillSchema = createPropertySchema({
// Computed value of skill to be added to skill rolls

View File

@@ -1,44 +0,0 @@
import SimpleSchema from 'simpl-schema';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
// SlotFiller fillers specifically fill a slot with a bit more control than
// other properties
let SlotFillerSchema = new SimpleSchema({
name: {
type: String,
optional: true,
max: STORAGE_LIMITS.name,
},
picture: {
type: String,
optional: true,
max: STORAGE_LIMITS.url,
},
description: {
type: String,
optional: true,
max: STORAGE_LIMITS.description,
},
// Overrides the type when searching for properties
slotFillerType: {
type: String,
optional: true,
max: STORAGE_LIMITS.variableName,
},
// 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,
defaultValue: 1,
},
// Filters out of UI if condition isn't met, but isn't otherwise enforced
slotFillerCondition: {
type: String,
optional: true,
max: STORAGE_LIMITS.calculation,
},
});
const ComputedOnlySlotFillerSchema = new SimpleSchema({});
export { SlotFillerSchema, ComputedOnlySlotFillerSchema };

View File

@@ -1,6 +1,7 @@
import SimpleSchema from 'simpl-schema';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
import TagTargetingSchema from '/imports/api/properties/subSchemas/TagTargetingSchema.js';
const ToggleSchema = createPropertySchema({
name: {
@@ -31,7 +32,7 @@ const ToggleSchema = createPropertySchema({
type: 'fieldToCompute',
optional: true,
},
});
}).extend(TagTargetingSchema);
const ComputedOnlyToggleSchema = createPropertySchema({
condition: {

View File

@@ -23,7 +23,6 @@ import { ComputedOnlyRollSchema } from '/imports/api/properties/Rolls.js';
import { ComputedOnlySavingThrowSchema } from '/imports/api/properties/SavingThrows.js';
import { ComputedOnlySkillSchema } from '/imports/api/properties/Skills.js';
import { ComputedOnlySlotSchema } from '/imports/api/properties/Slots.js';
import { ComputedOnlySlotFillerSchema } from '/imports/api/properties/SlotFillers.js';
import { ComputedOnlySpellSchema } from '/imports/api/properties/Spells.js';
import { ComputedOnlySpellListSchema } from '/imports/api/properties/SpellLists.js';
import { ComputedOnlyToggleSchema } from '/imports/api/properties/Toggles.js';
@@ -54,7 +53,6 @@ const propertySchemasIndex = {
roll: ComputedOnlyRollSchema,
savingThrow: ComputedOnlySavingThrowSchema,
skill: ComputedOnlySkillSchema,
slotFiller: ComputedOnlySlotFillerSchema,
spellList: ComputedOnlySpellListSchema,
spell: ComputedOnlySpellSchema,
toggle: ComputedOnlyToggleSchema,

View File

@@ -13,7 +13,7 @@ import { ComputedDamageSchema } from '/imports/api/properties/Damages.js';
import { DamageMultiplierSchema } from '/imports/api/properties/DamageMultipliers.js';
import { ComputedEffectSchema } from '/imports/api/properties/Effects.js';
import { ComputedFeatureSchema } from '/imports/api/properties/Features.js';
import { FolderSchema } from '/imports/api/properties/Folders.js';
import { ComputedFolderSchema } from '/imports/api/properties/Folders.js';
import { ComputedItemSchema } from '/imports/api/properties/Items.js';
import { ComputedNoteSchema } from '/imports/api/properties/Notes.js';
import { ComputedPointBuySchema } from '/imports/api/properties/PointBuys.js';
@@ -23,7 +23,6 @@ import { ComputedRollSchema } from '/imports/api/properties/Rolls.js';
import { ComputedSavingThrowSchema } from '/imports/api/properties/SavingThrows.js';
import { ComputedSkillSchema } from '/imports/api/properties/Skills.js';
import { ComputedSlotSchema } from '/imports/api/properties/Slots.js';
import { SlotFillerSchema } from '/imports/api/properties/SlotFillers.js';
import { ComputedSpellSchema } from '/imports/api/properties/Spells.js';
import { ComputedSpellListSchema } from '/imports/api/properties/SpellLists.js';
import { ComputedToggleSchema } from '/imports/api/properties/Toggles.js';
@@ -43,7 +42,7 @@ const propertySchemasIndex = {
damageMultiplier: DamageMultiplierSchema,
effect: ComputedEffectSchema,
feature: ComputedFeatureSchema,
folder: FolderSchema,
folder: ComputedFolderSchema,
note: ComputedNoteSchema,
pointBuy: ComputedPointBuySchema,
proficiency: ProficiencySchema,
@@ -52,7 +51,6 @@ const propertySchemasIndex = {
roll: ComputedRollSchema,
savingThrow: ComputedSavingThrowSchema,
skill: ComputedSkillSchema,
slotFiller: SlotFillerSchema,
spellList: ComputedSpellListSchema,
spell: ComputedSpellSchema,
toggle: ComputedToggleSchema,

View File

@@ -21,7 +21,6 @@ import { RollSchema } from '/imports/api/properties/Rolls.js';
import { SavingThrowSchema } from '/imports/api/properties/SavingThrows.js';
import { SkillSchema } from '/imports/api/properties/Skills.js';
import { SlotSchema } from '/imports/api/properties/Slots.js';
import { SlotFillerSchema } from '/imports/api/properties/SlotFillers.js';
import { SpellListSchema } from '/imports/api/properties/SpellLists.js';
import { SpellSchema } from '/imports/api/properties/Spells.js';
import { ToggleSchema } from '/imports/api/properties/Toggles.js';
@@ -52,7 +51,6 @@ const propertySchemasIndex = {
roll: RollSchema,
savingThrow: SavingThrowSchema,
skill: SkillSchema,
slotFiller: SlotFillerSchema,
spellList: SpellListSchema,
spell: SpellSchema,
toggle: ToggleSchema,

View File

@@ -0,0 +1,57 @@
import SimpleSchema from 'simpl-schema';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
const TagTargetingSchema = new SimpleSchema({
// True when targeting by tags instead of stats
targetByTags: {
type: Boolean,
optional: true,
},
// If targeting by tags, the field which will be targeted
targetField: {
type: String,
optional: true,
max: STORAGE_LIMITS.variableName,
},
// Which tags the effect is applied to
targetTags: {
type: Array,
optional: true,
maxCount: STORAGE_LIMITS.tagCount,
},
'targetTags.$': {
type: String,
max: STORAGE_LIMITS.tagLength,
},
extraTags: {
type: Array,
optional: true,
maxCount: STORAGE_LIMITS.extraTagsCount,
},
'extraTags.$': {
type: Object,
},
'extraTags.$._id': {
type: String,
regEx: SimpleSchema.RegEx.Id,
autoValue() {
if (!this.isSet) return Random.id();
}
},
'extraTags.$.operation': {
type: String,
allowedValues: ['OR', 'NOT'],
defaultValue: 'OR',
},
'extraTags.$.tags': {
type: Array,
defaultValue: [],
maxCount: STORAGE_LIMITS.tagCount,
},
'extraTags.$.tags.$': {
type: String,
max: STORAGE_LIMITS.tagLength,
},
});
export default TagTargetingSchema;

View File

@@ -1,11 +1,12 @@
import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import Libraries from '/imports/api/library/Libraries.js';
import LibraryCollections from '/imports/api/library/LibraryCollections.js';
import '/imports/api/users/methods/deleteMyAccount.js';
import '/imports/api/users/methods/addEmail.js';
import '/imports/api/users/methods/removeEmail.js';
import '/imports/api/users/methods/updateFileStorageUsed.js';
import { some } from 'lodash';
const defaultLibraries = process.env.DEFAULT_LIBRARIES && process.env.DEFAULT_LIBRARIES.split(',') || [];
const defaultLibraryCollections = process.env.DEFAULT_LIBRARY_COLLECTIONS && process.env.DEFAULT_LIBRARY_COLLECTIONS.split(',') || [];
@@ -142,12 +143,12 @@ Meteor.users.generateApiKey = new ValidatedMethod({
Meteor.users.setDarkMode = new ValidatedMethod({
name: 'users.setDarkMode',
validate: new SimpleSchema({
darkMode: { type: Boolean },
darkMode: { type: Boolean, optional: true },
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
timeInterval: 2000,
},
run({ darkMode }) {
if (!this.userId) return;
@@ -250,6 +251,29 @@ Meteor.users.setPreference = new ValidatedMethod({
},
});
if (Meteor.isServer) {
Accounts.onCreateUser(() => {
if (defaultLibraries?.length) {
Libraries.update({
_id: { $in: defaultLibraries }
}, {
$inc: { subscriberCount: 1 }
}, {
multi: true,
}, () => {/**/ });
}
if (defaultLibraryCollections?.length) {
LibraryCollections.update({
_id: { $in: defaultLibraryCollections }
}, {
$inc: { subscriberCount: 1 }
}, {
multi: true,
}, () => {/**/ });
}
});
}
Meteor.users.subscribeToLibrary = new ValidatedMethod({
name: 'users.subscribeToLibrary',
validate: new SimpleSchema({
@@ -264,15 +288,17 @@ Meteor.users.subscribeToLibrary = new ValidatedMethod({
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
timeInterval: 2000,
},
run({ libraryId, subscribe }) {
if (!this.userId) throw 'Can only subscribe if logged in';
if (subscribe) {
Libraries.update({ _id: libraryId }, { $inc: { subscriberCount: 1 } }, () => {/**/ });
return Meteor.users.update(this.userId, {
$addToSet: { subscribedLibraries: libraryId },
});
} else {
Libraries.update({ _id: libraryId }, { $inc: { subscriberCount: -1 } }, () => {/**/ });
return Meteor.users.update(this.userId, {
$pullAll: { subscribedLibraries: libraryId },
});
@@ -299,10 +325,12 @@ Meteor.users.subscribeToLibraryCollection = new ValidatedMethod({
run({ libraryCollectionId, subscribe }) {
if (!this.userId) throw 'Can only subscribe if logged in';
if (subscribe) {
LibraryCollections.update({ _id: libraryCollectionId }, { $inc: { subscriberCount: 1 } }, () => {/**/ });
return Meteor.users.update(this.userId, {
$addToSet: { subscribedLibraryCollections: libraryCollectionId },
});
} else {
LibraryCollections.update({ _id: libraryCollectionId }, { $inc: { subscriberCount: -1 } }, () => {/**/ });
return Meteor.users.update(this.userId, {
$pullAll: { subscribedLibraryCollections: libraryCollectionId },
});

View File

@@ -0,0 +1,3 @@
export default function escapeRegex(string) {
return string.replace(/[/\-\\^$*+?.()|[\]{}]/g, '');
}

View File

@@ -1,10 +1,11 @@
export default function numberToSignedString(number, spaced){
export default function numberToSignedString(number, spaced) {
if (typeof number !== 'number') return number;
if (number === 0){
if (number === 0) {
return spaced ? '+ 0' : '+0';
} else if (number > 0){
} else if (number > 0) {
return spaced ? `+ ${number}` : `+${number}`;
} else {
return spaced ? `- ${Math.abs(number) || number}` : `${number}`;
// Uses the unicode minus sign '' instead of a dash '-' to help line up numbers nicely
return spaced ? ` ${Math.abs(number) || number}` : `${Math.abs(number) || number}`;
}
}

View File

@@ -7,16 +7,18 @@
>
<template #activator="{ on }">
<v-btn
:outlined="!!label"
:icon="!label"
:tile="!label"
:min-width="label && 108"
:height="height"
:width="width"
:disabled="context.editPermission === false"
v-on="on"
>
{{ label }}
<v-icon
:right="!!label"
:color="label && value"
:color="value"
>
mdi-format-paint
</v-icon>
@@ -137,7 +139,15 @@
label: {
type: String,
default: undefined,
}
},
height: {
type: Number,
default: undefined,
},
width: {
type: Number,
default: undefined,
},
},
data(){ return {
colors: [

View File

@@ -16,6 +16,14 @@ export default {
wideColumns: Boolean,
},
};
/*
Removed to improve chrome layout performance, put it back if there are rendering errors
.column-layout>span>div {
display: table;
table-layout: fixed;
}
*/
</script>
<style lang="css">
@@ -39,14 +47,9 @@ export default {
.column-layout>div,
.column-layout>span>div {
display: table;
table-layout: fixed;
width: 100%;
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
transform: translateX(0);
-webkit-transform: translateX(0);
-webkit-column-break-inside: avoid;
page-break-inside: avoid;
break-inside: avoid;
padding: 4px;

View File

@@ -19,7 +19,10 @@
</v-btn>
</template>
<v-sheet class="d-flex flex-column align-center justify-center">
<v-btn-toggle v-model="dataAdvantage">
<v-btn-toggle
v-model="dataAdvantage"
color="accent"
>
<v-btn :value="-1">
Disadvantage
</v-btn>

View File

@@ -13,7 +13,7 @@
<v-divider vertical />
<div
class="flex layout column"
style="background-color: inherit; overflow: hidden;"
style="background-color: inherit; overflow: hidden; min-height: 100%;"
data-id="selected-node-card"
>
<slot name="detail" />

View File

@@ -5,26 +5,32 @@
transition="slide-y-transition"
min-width="290px"
style="overflow-y: auto;"
left
>
<template #activator="{ on }">
<v-btn
:loading="loading"
outlined
:min-width="108"
v-bind="$attrs"
:outlined="!!label"
:icon="!label"
:tile="!label"
:min-width="label && 108"
:height="height"
:width="width"
:style="buttonStyle"
:disabled="context.editPermission === false"
v-bind="$attrs"
v-on="on"
>
{{ label }}
<svg-icon
v-if="safeValue && safeValue.shape"
right
class="ml-2"
:class="{'ml-2': !!label}"
:shape="safeValue.shape"
/>
<v-icon
v-else
right
:right="!!label"
>
mdi-select-search
</v-icon>
@@ -87,15 +93,26 @@ export default {
SvgIcon,
},
mixins: [SmartInput],
inject: {
context: { default: {} }
},
props: {
label: {
type: String,
default: 'Icon',
default: undefined,
},
buttonStyle: {
type: String,
default: undefined,
},
height: {
type: Number,
default: undefined,
},
width: {
type: Number,
default: undefined,
},
},
data() {
return {

View File

@@ -3,7 +3,7 @@
v-bind="$attrs"
:disabled="isDisabled"
:loading="loading"
@click="click"
@click.stop.prevent="click"
>
<slot />
</v-btn>
@@ -41,7 +41,7 @@ export default {
} else if (Number.isFinite(this.context.debounceTime)){
return this.context.debounceTime;
} else {
return 750;
return 400;
}
},
},
@@ -62,11 +62,12 @@ export default {
this.$emit('click', this.acknowledgeChange);
},
clicks() {
if (!this.$listeners?.clicks) return;
this.loading = true;
this.$emit('clicks', this.timesClicked, this.acknowledgeChange);
this.timesClicked = 0;
},
acknowledgeChange(error){
acknowledgeChange(error) {
this.loading = false;
if (error) {
console.error(error)

View File

@@ -0,0 +1,76 @@
<template lang="html">
<outlined-input
:name="label"
class="mb-6 pt-1"
>
<v-btn-toggle
v-bind="$attrs"
mandatory
tile
group
:value="safeValue"
color="accent"
style="flex-wrap: wrap;"
>
<v-btn
v-for="(option, i) in options"
:key="`toggle-option-${i}`"
:value="option.value"
:disabled="isDisabled || (clickedValue != option.value && loading)"
:plain="clickedValue != option.value && loading"
:loading="clickedValue == option.value && loading"
height="42"
v-on="(value == option.value) ? {} : { click() { click(option.value) } }"
>
<v-icon
v-if="option.icon"
left
>
{{ option.icon }}
</v-icon>
{{ option.name }}
</v-btn>
</v-btn-toggle>
<v-expand-transition>
<div
v-if="errors.length"
class="pa-2 error--text"
>
{{ errors.join('\n\n') }}
</div>
</v-expand-transition>
</outlined-input>
</template>
<script lang="js">
import SmartInput from '/imports/client/ui/components/global/SmartInputMixin.js';
import OutlinedInput from '/imports/client/ui/properties/viewers/shared/OutlinedInput.vue';
export default {
components: {
OutlinedInput,
},
mixins: [SmartInput],
props: {
label: {
type: String,
default: '',
},
options: {
type: Array,
default: () => [],
}
},
data() {
return {
clickedValue: undefined,
};
},
methods: {
click(val) {
this.clickedValue = val;
this.change(val);
},
}
};
</script>

View File

@@ -16,6 +16,9 @@
<template #append>
<slot name="value" />
</template>
<template #prepend>
<slot name="prepend" />
</template>
</v-text-field>
</template>

View File

@@ -10,6 +10,7 @@ import SmartBtn from '/imports/client/ui/components/global/SmartBtn.vue';
import SmartCombobox from '/imports/client/ui/components/global/SmartCombobox.vue';
import SmartCheckbox from '/imports/client/ui/components/global/SmartCheckbox.vue';
import SmartSwitch from '/imports/client/ui/components/global/SmartSwitch.vue';
import SmartToggle from '/imports/client/ui/components/global/SmartToggle.vue';
import SvgIcon from '/imports/client/ui/components/global/SvgIcon.vue';
import SmartSlider from '/imports/client/ui/components/global/SmartSlider.vue';
@@ -24,4 +25,5 @@ Vue.component('SmartCombobox', SmartCombobox);
Vue.component('SmartCheckbox', SmartCheckbox);
Vue.component('SmartSlider', SmartSlider);
Vue.component('SmartSwitch', SmartSwitch);
Vue.component('SmartToggle', SmartToggle);
Vue.component('SvgIcon', SvgIcon);

View File

@@ -29,11 +29,6 @@
style="flex-shrink: 0;"
>
<v-spacer />
<color-picker
v-if="$listeners && $listeners['color-changed']"
:value="model.color"
@input="colorChanged"
/>
<v-menu
v-if="$listeners && (
$listeners.move ||
@@ -109,6 +104,20 @@
<v-icon>mdi-send</v-icon>
</v-list-item-action>
</v-list-item>
<v-list-item
v-if="$listeners && $listeners['copy-to-library'] && userPaid"
:disabled="context.editPermission === false"
@click="$emit('copy-to-library')"
>
<v-list-item-content>
<v-list-item-title>
Copy to library
</v-list-item-title>
</v-list-item-content>
<v-list-item-action>
<v-icon>mdi-content-duplicate</v-icon>
</v-list-item-action>
</v-list-item>
<v-list-item
v-if="$listeners && $listeners.remove"
:disabled="context.editPermission === false"
@@ -165,14 +174,13 @@
import isDarkColor from '/imports/client/ui/utility/isDarkColor.js';
import PropertyIcon from '/imports/client/ui/properties/shared/PropertyIcon.vue';
import { getPropertyName } from '/imports/constants/PROPERTIES.js';
import ColorPicker from '/imports/client/ui/components/ColorPicker.vue';
import getThemeColor from '/imports/client/ui/utility/getThemeColor.js';
import PROPERTIES from '/imports/constants/PROPERTIES.js';
import { assertUserHasPaidBenefits } from '/imports/api/users/patreon/tiers.js';
export default {
components: {
PropertyIcon,
ColorPicker,
},
inject: {
context: { default: {} }
@@ -211,6 +219,16 @@ export default {
return propDef && propDef.docsPath;
},
},
meteor: {
userPaid() {
try {
assertUserHasPaidBenefits(Meteor.user())
return true;
} catch (e) {
return false;
}
},
},
methods: {
colorChanged(value){
this.$emit('color-changed', value);

View File

@@ -2,6 +2,8 @@
<v-snackbar
bottom
left
outlined
color="accent"
v-bind="$attrs"
:value="isShown"
:timeout="timeout"
@@ -50,7 +52,7 @@ export default {
props: {
timeout: {
type: Number,
default: 6000,
default: 6000000,
},
pause: {
type: Number,

View File

@@ -2,18 +2,21 @@
<div class="creature-form">
<text-field
label="Name"
:disabled="!editPermission"
:value="model.name"
:error-messages="errors.name"
@change="(value, ack) => $emit('change', {path: ['name'], value, ack})"
/>
<text-field
label="Alignment"
:disabled="!editPermission"
:value="model.alignment"
:error-messages="errors.alignment"
@change="(value, ack) => $emit('change', {path: ['alignment'], value, ack})"
/>
<text-field
label="Gender"
:disabled="!editPermission"
:value="model.gender"
:error-messages="errors.gender"
@change="(value, ack) => $emit('change', {path: ['gender'], value, ack})"
@@ -21,6 +24,7 @@
<text-field
label="Picture URL"
hint="A link to a high resolution image"
:disabled="!editPermission"
:value="model.picture"
:error-messages="errors.picture"
@change="(value, ack) => $emit('change', {path: ['picture'], value, ack})"
@@ -28,6 +32,7 @@
<text-field
label="Avatar picture URL"
hint="A link to a smaller, square image to use as an avatar"
:disabled="!editPermission"
:value="model.avatarPicture"
:error-messages="errors.avatarPicture"
@change="(value, ack) => $emit('change', {path: ['avatarPicture'], value, ack})"
@@ -36,21 +41,25 @@
<form-section name="Settings">
<v-switch
label="Hide redundant stats"
:disabled="!editPermission"
:input-value="model.settings.hideUnusedStats"
@change="value => $emit('change', {path: ['settings','hideUnusedStats'], value: !!value})"
/>
<v-switch
label="Hide rest buttons"
:disabled="!editPermission"
:input-value="model.settings.hideRestButtons"
@change="value => $emit('change', {path: ['settings','hideRestButtons'], value: !!value})"
/>
<v-switch
label="Show spells tab"
:disabled="!editPermission"
:input-value="!model.settings.hideSpellsTab"
@change="changeHideSpellsTab"
/>
<v-switch
label="Show tree tab"
:disabled="!editPermission"
:input-value="model.settings.showTreeTab"
@change="changeShowTreeTab"
/>
@@ -62,6 +71,7 @@
min="0"
max="1"
step="0.1"
:disabled="!editPermission"
:value="model.settings.hitDiceResetMultiplier"
@change="(value, ack) => $emit('change', {path: ['settings','hitDiceResetMultiplier'], value, ack})"
/>
@@ -69,6 +79,7 @@
label="Discord Webhook URL"
hint="This creature's logs will be posted to the discord channel"
placeholder="https://discordapp.com/api/webhooks/<id>/<token>"
:disabled="!editPermission"
:value="model.settings.discordWebhook"
@change="(value, ack) => $emit('change', {path: ['settings','discordWebhook'], value, ack})"
/>
@@ -96,12 +107,13 @@
<form-section name="Libraries">
<smart-switch
label="All user libraries"
:disabled="!editPermission"
:value="allUserLibraries"
@change="allUserLibrariesChange"
/>
<library-list
selection
:disabled="!model.allowedLibraries && !model.allowedLibraryCollections"
:disabled="!editPermission || (!model.allowedLibraries && !model.allowedLibraryCollections)"
:libraries-selected="model.allowedLibraries"
:library-collections-selected="model.allowedLibraryCollections"
:libraries-selected-by-collections="librariesSelectedByCollections"
@@ -120,6 +132,18 @@
{{ libraryWriteError }}
</p>
</form-section>
<form-section name="Debug">
<v-btn
data-id="dependency-graph-button"
text
@click="showDependencyGraph"
>
<v-icon left>
mdi-graph
</v-icon>
Dependency Graph
</v-btn>
</form-section>
</form-sections>
</div>
</template>
@@ -130,6 +154,7 @@ import FormSection, { FormSections } from '/imports/client/ui/properties/forms/s
import LibraryList from '/imports/client/ui/library/LibraryList.vue';
import LibraryCollections from '/imports/api/library/LibraryCollections.js';
import { changeAllowedLibraries, toggleAllUserLibraries } from '/imports/api/creature/creatures/methods/changeAllowedLibraries.js';
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
export default {
components: {
@@ -215,6 +240,14 @@ export default {
});
return ids;
},
editPermission() {
try {
assertEditPermission(this.model, Meteor.userId());
return true;
} catch (e) {
return false;
}
},
},
methods: {
changeShowTreeTab(value) {
@@ -267,6 +300,15 @@ export default {
this.dirty = true;
this.updateAllowedLibraryCollections();
},
showDependencyGraph() {
this.$store.commit('pushDialogStack', {
component: 'dependency-graph-dialog',
elementId: 'dependency-graph-button',
data: {
creatureId: this.model._id,
},
});
},
},
};
</script>

View File

@@ -43,7 +43,7 @@
{{ node.name }}
</span>
<fill-slot-button
v-if="(node.quantityExpected && node.quantityExpected.value === 1) && node.spaceLeft === 1"
v-if="canFillWithOne"
:model="node"
/>
</div>
@@ -93,6 +93,7 @@
v-if="showExpanded"
:children="computedChildren"
:parent-slot-id="computedSlotId"
:depth="depth"
@selected="e => $emit('selected', e)"
/>
<div v-else>
@@ -141,6 +142,10 @@ export default {
context: { default: {} }
},
props: {
depth: {
type: Number,
default: 0,
},
node: {
type: Object,
required: true,
@@ -155,7 +160,7 @@ export default {
},
},
data(){return {
expanded: false,
expanded: this.depth <= 2,
/* expand if there's a slot needing attention:
this.node._descendantCanFill || (
this.node.type === 'propertySlot' &&
@@ -170,7 +175,8 @@ export default {
this.children.length === 1 &&
this.children[0].node.type !== 'propertySlot' &&
this.node.quantityExpected &&
this.node.quantityExpected.value === 1;
this.node.quantityExpected.value === 1 &&
!this.canFill;
},
isSlot(){
return this.node.type === 'propertySlot';
@@ -180,15 +186,18 @@ export default {
},
canFillWithOne(){
return this.isSlot &&
this.node.quantityExpected &&
this.canFill &&
this.node.quantityExpected &&
this.node.quantityExpected.value === 1 &&
this.node.spaceLeft === 1
this.node.spaceLeft === 1 &&
!this.children?.length;
},
canFillWithMany(){
return this.isSlot && (
return this.isSlot && this.canFill && (
!this.node.quantityExpected ||
this.node.quantityExpected.value === 0 ||
(this.node.quantityExpected.value > 1 && this.node.spaceLeft > 0)
(this.node.quantityExpected.value > 1 && this.node.spaceLeft > 0) ||
(this.node.quantityExpected.value === 1 && this.children?.length)
);
},
hasChildren(){

View File

@@ -6,6 +6,7 @@
:node="child.node"
:children="child.children"
:parent-slot-id="parentSlotId"
:depth="depth + 1"
@selected="e => $emit('selected', e)"
/>
</div>
@@ -27,6 +28,10 @@ export default {
type: String,
default: undefined,
},
depth: {
type: Number,
default: 0,
},
},
data() {
return {

View File

@@ -153,12 +153,13 @@
}
this.$store.commit('pushDialogStack', {
component: 'add-creature-property-dialog',
component: 'insert-property-dialog',
elementId: 'insert-creature-property-type-' + forcedType,
data: {
parentDoc: forcedType ? undefined : parent,
forcedType,
creatureId: this.creatureId,
noBackdropClose: true,
},
callback(result){
if (!result){

View File

@@ -29,10 +29,10 @@
bottom
left
transition="slide-y-transition"
data-id="creature-menu"
>
<template #activator="{ on }">
<v-btn
data-id="creature-menu"
icon
v-on="on"
>
@@ -40,6 +40,35 @@
</v-btn>
</template>
<v-list>
<v-list-item
v-if="!isOwner && ownerName"
two-line
disabled
>
<v-list-item-avatar>
<v-icon>
mdi-account
</v-icon>
</v-list-item-avatar>
<v-list-item-content>
<v-list-item-title>
{{ ownerName }}
</v-list-item-title>
<v-list-item-subtitle>
Sheet owner
</v-list-item-subtitle>
</v-list-item-content>
</v-list-item>
<v-list-item
v-if="!isOwner"
@click="unshareWithMe"
>
<v-list-item-title>
<v-icon left>
mdi-cancel
</v-icon> Unshare with me
</v-list-item-title>
</v-list-item>
<v-list-item :to="printUrl">
<v-list-item-title>
<v-icon left>
@@ -47,37 +76,31 @@
</v-icon> Print
</v-list-item-title>
</v-list-item>
<template v-if="editPermission">
<v-list-item @click="deleteCharacter">
<v-list-item-title>
<v-icon left>
mdi-delete
</v-icon> Delete
</v-list-item-title>
</v-list-item>
<v-list-item @click="showCharacterForm">
<v-list-item-title>
<v-icon left>
mdi-pencil
</v-icon> Edit details
</v-list-item-title>
</v-list-item>
<v-list-item @click="showShareDialog">
<v-list-item-title>
<v-icon left>
mdi-share-variant
</v-icon> Sharing
</v-list-item-title>
</v-list-item>
</template>
<v-list-item @click="showCharacterForm">
<v-list-item-title>
<v-icon left>
mdi-pencil
</v-icon> Edit details
</v-list-item-title>
</v-list-item>
<v-list-item
v-else
@click="unshareWithMe"
:disabled="!isOwner"
@click="showShareDialog"
>
<v-list-item-title>
<v-icon left>
mdi-share-variant
</v-icon> Sharing
</v-list-item-title>
</v-list-item>
<v-list-item
:disabled="!isOwner"
@click="deleteCharacter"
>
<v-list-item-title>
<v-icon left>
mdi-delete
</v-icon> Unshare with me
</v-icon> Delete
</v-list-item-title>
</v-list-item>
</v-list>
@@ -266,6 +289,15 @@ export default {
return false;
}
},
isOwner() {
if (!this.creature) return;
return Meteor.userId() === this.creature.owner;
},
ownerName() {
if (!this.creature) return;
const username = Meteor.users.findOne(this.creature.owner)?.username;
return username;
},
},
}
</script>

View File

@@ -150,11 +150,12 @@ export default {
addProperty(){
let parentPropertyId = this._id;
this.$store.commit('pushDialogStack', {
component: 'add-creature-property-dialog',
component: 'insert-property-dialog',
elementId: 'insert-creature-property-btn',
data: {
parentDoc: this.creature,
creatureId: this._id,
noBackdropClose: true,
},
callback(result){
if (!result) return;

View File

@@ -70,6 +70,8 @@ export default {
return CreatureProperties.find({
'ancestors.id': {
$eq: this.creatureId,
},
'parent.id': {
$nin: folderIds,
},
type: 'action',

View File

@@ -68,6 +68,8 @@ export default {
return CreatureProperties.find({
'ancestors.id': {
$eq: this.creatureId,
},
'parent.id': {
$nin: folderIds,
},
type: 'feature',

View File

@@ -155,6 +155,8 @@ export default {
return CreatureProperties.find({
'ancestors.id': {
$eq: this.creatureId,
},
'parent.id': {
$nin: this.folderIds,
},
type: 'container',
@@ -179,7 +181,10 @@ export default {
return CreatureProperties.find({
'ancestors.id': {
$eq: this.creatureId,
$nin: [...this.containerIds, ...this.folderIds],
$nin: this.containerIds,
},
'parent.id': {
$nin: this.folderIds,
},
type: 'container',
removed: { $ne: true },
@@ -192,12 +197,16 @@ export default {
return CreatureProperties.find({
'ancestors.id': {
$eq: this.creatureId,
$nin: [...this.containerIds, ...this.folderIds],
$nin: this.containerIds,
},
'parent.id': {
$nin: this.folderIds,
},
type: 'item',
equipped: { $ne: true },
removed: { $ne: true },
deactivatedByAncestor: { $ne: true },
deactivatedByToggle: { $ne: true },
}, {
sort: { order: 1 },
});

View File

@@ -72,6 +72,8 @@ export default {
return CreatureProperties.find({
'ancestors.id': {
$eq: this.creatureId,
},
'parent.id': {
$nin: folderIds,
},
type: 'note',

View File

@@ -102,6 +102,8 @@ export default {
return CreatureProperties.find({
'ancestors.id': {
$eq: this.creatureId,
},
'parent.id': {
$nin: this.folderIds,
},
inactive: { $ne: true },
@@ -121,6 +123,8 @@ export default {
return CreatureProperties.find({
'ancestors.id': {
$eq: this.creatureId,
},
'parent.id': {
$nin: this.folderIds,
},
type: 'spellList',
@@ -142,7 +146,10 @@ export default {
return CreatureProperties.find({
'ancestors.id': {
$eq: this.creatureId,
$nin: [...this.spellListIds, ...this.folderIds],
$nin: this.spellListIds,
},
'parent.id': {
$nin: this.folderIds,
},
type: 'spell',
removed: { $ne: true },
@@ -159,7 +166,10 @@ export default {
return CreatureProperties.find({
'ancestors.id': {
$eq: this.creatureId,
$nin: [...this.spellListIds, ...this.folderIds],
$nin: this.spellListIds,
},
'parent.id': {
$nin: this.folderIds,
},
type: 'spellList',
removed: { $ne: true },

View File

@@ -425,15 +425,11 @@ function walkDown(forest, callback){
const propertyHandlers = {
folder(prop) {
let skipChildren;
let propPath = null;
if (prop.groupStats && prop.hideStatsGroup) {
skipChildren = true;
}
if (prop.groupStats && prop.tab === 'stats') {
propPath = ['folder', prop.location]
}
return { skipChildren, propPath }
return { propPath }
},
attribute(prop) {
if (
@@ -456,7 +452,7 @@ const propertyHandlers = {
},
toggle(prop) {
if (
prop.deactivatedByAncestor || !prop.showUI
prop.deactivatedByToggle || prop.deactivatedByAncestor || !prop.showUI
) return { propPath: null };
return { propPath: 'toggle' };
},
@@ -524,12 +520,25 @@ export default {
properties() {
const creature = this.creature;
if (!creature) return;
const folderIds = CreatureProperties.find({
'ancestors.id': this.creatureId,
type: 'folder',
groupStats: true,
hideStatsGroup: true,
removed: { $ne: true },
inactive: { $ne: true },
}, { fields: { _id: 1 } }).map(folder => folder._id);
const filter = {
'ancestors.id': this.creatureId,
'parent.id': {
$nin: folderIds,
},
$or: [
{ inactive: { $ne: true } },
{ type: 'toggle' },
],
overridden: {$ne: true},
removed: { $ne: true },
type: {
$in: [
@@ -579,6 +588,7 @@ export default {
'ancestors.id': this.creatureId,
removed: { $ne: true },
deactivatedByAncestor: { $ne: true },
deactivatedByToggle: { $ne: true },
showUI: true,
}, {
sort: { order: 1 }
@@ -600,13 +610,15 @@ export default {
data: { _id },
});
},
incrementChange(_id, { type, value }) {
incrementChange(_id, { type, value, ack }) {
damageProperty.call({
_id,
operation: type,
value: -value
}, error => {
if (error) {
if (ack) {
ack(error);
} else if (error) {
snackbar({ text: error.reason || error.message || error.toString() });
console.error(error);
}

View File

@@ -46,6 +46,7 @@
:_id="selectedNodeId"
@removed="selectedNodeId = undefined"
@duplicated="id => selectedNodeId = id"
@select-sub-property="clickNode"
/>
</template>
</tree-detail-layout>

View File

@@ -34,6 +34,16 @@
:key="index + 'dependencyLoopError'"
:model="error"
/>
<v-alert
v-else-if="error.type === 'warning'"
:key="index + 'otherError'"
border="bottom"
colored-border
elevation="2"
type="warning"
>
{{ error.details.error }}
</v-alert>
<v-alert
v-else
:key="index + 'otherError'"

View File

@@ -152,6 +152,7 @@ export default {
removed: { $ne: true },
equipped: { $ne: true },
deactivatedByAncestor: { $ne: true },
deactivatedByToggle: { $ne: true },
}, {
sort: { order: 1 },
}).fetch();
@@ -168,6 +169,7 @@ export default {
equipped: { $ne: true },
removed: { $ne: true },
deactivatedByAncestor: { $ne: true },
deactivatedByToggle: { $ne: true },
}, {
sort: { order: 1 },
});

View File

@@ -450,6 +450,7 @@ export default {
type: 'toggle',
removed: { $ne: true },
deactivatedByAncestor: { $ne: true },
deactivatedByToggle: { $ne: true },
showUI: true,
}, {
sort: { order: 1 }

View File

@@ -2,59 +2,78 @@
lang="html"
functional
>
<v-list-item
v-bind="$attrs"
:class="isSelected && 'primary--text v-list-item--active'"
:dense="dense"
v-on="selection ? { click() {$emit('click')} } : {}"
<draggable
v-model="dataItems"
:group="'item-list'"
:animation="200"
:sort="false"
ghost-class="item-to-creature-ghost"
draggable=".no-real-items"
style="position: relative;"
@change="dropItem"
>
<v-list-item-avatar
:color="isSelected ? 'red darken-1' : model.color || 'grey'"
:size="dense ? 30 : undefined"
class="white--text"
style="transition: background 0.3s;"
<v-list-item
slot="header"
v-bind="$attrs"
:class="{
'primary--text v-list-item--active': isSelected,
'item-to-creature-drag-over': dragover,
}"
:dense="dense"
v-on="selection ? { click() {$emit('click')} } : {}"
>
<v-fade-transition leave-absolute>
<v-icon v-if="isSelected">
mdi-check
</v-icon>
<img
v-else-if="model.avatarPicture"
:src="model.avatarPicture"
:alt="model.name"
>
<template v-else>
<span>
{{ model.initial }}
</span>
</template>
</v-fade-transition>
</v-list-item-avatar>
<v-list-item-content>
<v-list-item-title>
{{ model.name }}
</v-list-item-title>
<v-list-item-subtitle v-if="!dense">
{{ model.alignment }} {{ model.gender }} {{ model.race }}
</v-list-item-subtitle>
</v-list-item-content>
<v-list-item-action v-if="!dense">
<shared-icon :model="model" />
</v-list-item-action>
<v-list-item-action v-if="!selection && !dense">
<drag-handle
style="height: 100%; width: 40px;"
/>
</v-list-item-action>
</v-list-item>
<v-list-item-avatar
:color="isSelected ? 'red darken-1' : model.color || 'grey'"
:size="dense ? 30 : undefined"
class="white--text"
style="transition: background 0.3s;"
>
<v-fade-transition leave-absolute>
<v-icon v-if="isSelected">
mdi-check
</v-icon>
<img
v-else-if="model.avatarPicture"
:src="model.avatarPicture"
:alt="model.name"
>
<template v-else>
<span>
{{ model.initial }}
</span>
</template>
</v-fade-transition>
</v-list-item-avatar>
<v-list-item-content>
<v-list-item-title>
{{ model.name }}
</v-list-item-title>
<v-list-item-subtitle v-if="!dense">
{{ model.alignment }} {{ model.gender }} {{ model.race }}
</v-list-item-subtitle>
</v-list-item-content>
<v-list-item-action v-if="!dense">
<shared-icon :model="model" />
</v-list-item-action>
<v-list-item-action v-if="!selection && !dense">
<drag-handle
style="height: 100%; width: 40px;"
/>
</v-list-item-action>
</v-list-item>
</draggable>
</template>
<script lang="js">
import SharedIcon from '/imports/client/ui/components/SharedIcon.vue';
import draggable from 'vuedraggable';
import { organizeDoc } from '/imports/api/parenting/organizeMethods.js';
import { snackbar } from '/imports/client/ui/components/snackbars/SnackbarQueue.js';
export default {
components: {
SharedIcon,
draggable,
},
props: {
model: {
@@ -64,6 +83,65 @@ export default {
selection: Boolean,
isSelected: Boolean,
dense: Boolean,
},
data() {
return {
dataItems: [],
dragover: false,
};
},
methods: {
dropItem({ added }) {
const item = added?.element;
if (!item?._id) return;
const docRef = { collection: 'creatureProperties', id: item._id };
// Create the undo function
const oldOrder = item.order;
const oldParent = item.parent;
const undo = () => organizeDoc.call({
docRef,
parentRef: oldParent,
order: (oldOrder || 0) - 0.5,
skipClient: true, // The client no longer has the doc subscribed, so we can't simulate
}, (error) => {
if (error) {
console.error(error);
snackbar({ text: error.reason || error.message || error.toString() });
}
});
// Move the doc
organizeDoc.call({
docRef,
parentRef: { collection: 'creatures', id: this.model._id },
order: -0.5,
}, (error) => {
if (error) {
console.error(error);
snackbar({ text: error.reason || error.message || error.toString() });
} else {
snackbar({
text: `\u{1F36A} Moved ${item.name || 'item'} to ${this.model.name || 'another character'}`,
callbackName: 'undo',
callback: undo,
});
}
});
},
}
}
</script>
<style lang="css">
.item-to-creature-ghost {
position: absolute;
left: 50%;
top: 0;
bottom: 0;
right: 0;
}
.item-to-creature-ghost .v-btn {
display: none;
}
</style>

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