diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 00000000..502b5020 Binary files /dev/null and b/.DS_Store differ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..509c092d --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "app/packages/redis-oplog"] + path = app/packages/redis-oplog + url = https://github.com/ramezrafla/redis-oplog.git diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..e211d1a5 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,33 @@ +{ + "cSpell.words": [ + "alea", + "armor", + "autorun", + "blackbox", + "cantrip", + "Cantrips", + "Crit", + "Crits", + "cyrb", + "denormalize", + "denormalized", + "EJSON", + "healthbar", + "healthbars", + "jank", + "meteortesting", + "nearley", + "ngraph", + "ostrio", + "Ruleset", + "snackbars", + "Spellcasting", + "Subheaders", + "thumbhash", + "uncomputed", + "untarget", + "vuetify", + "Vuex", + "walkdown" + ] +} diff --git a/Dockerfile b/Dockerfile index 5f7ff99b..3c42def2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,14 +1,27 @@ -FROM ubuntu:latest -RUN apt-get update --quiet \ - && apt-get install --quiet --yes \ - bsdtar \ - curl \ - git -RUN ln --symbolic --force $(which bsdtar) $(which tar) -RUN useradd --create-home --shell /bin/bash dicecloud -USER dicecloud -WORKDIR /home/dicecloud -RUN curl https://install.meteor.com/?release=1.8.0.2 | sh -ENV PATH="${PATH}:/home/dicecloud/.meteor" -COPY dev.sh ./dev.sh -ENTRYPOINT ./dev.sh \ No newline at end of file +FROM ubuntu:jammy + +USER root +RUN adduser --system mt + +RUN apt-get update +RUN apt-get install --quiet --yes curl +RUN curl -fsSL https://deb.nodesource.com/setup_14.x | bash - +RUN apt-get update +RUN apt-get install --quiet --yes nodejs git + +USER mt + +RUN curl https://install.meteor.com/ | sh + +WORKDIR /home/mt +RUN git clone https://github.com/ThaumRystra/DiceCloud dicecloud +WORKDIR /home/mt/dicecloud/app +RUN npm install --production +ENV PATH=$PATH:/home/mt/.meteor +RUN meteor build --directory ~/dc/ --architecture os.linux.x86_64 +WORKDIR /home/mt/dc/bundle/programs/server +RUN npm install +WORKDIR /home/mt/dc/bundle +RUN rm -r /home/mt/dicecloud + +ENTRYPOINT node main.js diff --git a/README.md b/README.md index 3a50ea99..447d08b4 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/app/.gitignore b/app/.gitignore index da83c4f3..67d0a229 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -3,6 +3,10 @@ .demeteorized .cache .vscode +.coverage +.nyc_output +.DS_Store +fileStorage settings.json public/components public/_imports.html diff --git a/app/.meteor/packages b/app/.meteor/packages index dc62173e..41e7fd56 100644 --- a/app/.meteor/packages +++ b/app/.meteor/packages @@ -3,29 +3,27 @@ # 'meteor add' and 'meteor remove' will edit this file for you, # but you can also edit it by hand. -accounts-password@2.3.1 -random@1.2.0 -underscore@1.0.10 +accounts-password@2.4.0 +random@1.2.1 +underscore@1.6.1 dburles:mongo-collection-instances accounts-google@1.4.0 -email@2.2.1 +email@2.2.6 meteor-base@1.5.1 -mobile-experience@1.1.0 -mongo@1.16.0 -session@1.2.0 -tracker@1.2.0 -logging@1.3.1 +mobile-experience@1.1.1 +mongo@1.16.10 +session@1.2.1 +tracker@1.3.3 +logging@1.3.4 reload@1.3.1 -ejson@1.1.2 -check@1.3.1 +ejson@1.1.3 +check@1.4.1 standard-minifier-js@2.8.1 shell-server@0.5.0 -ecmascript@0.16.2 -es5-shim@4.8.0 -service-configuration@1.3.0 -dynamic-import@0.7.2 -ddp-rate-limiter@1.1.0 -rate-limit@1.0.9 +service-configuration@1.3.4 +dynamic-import@0.7.3 +ddp-rate-limiter@1.2.1 +rate-limit@1.1.1 mdg:validated-method static-html@1.3.2 aldeed:collection2 @@ -37,9 +35,8 @@ simple:rest simple:rest-method-mixin mikowals:batch-insert peerlibrary:subscription-data -seba:minifiers-autoprefixer zer0th:meteor-vuetify-loader -akryum:vue-component +akryum:vue-component@0.15.2 akryum:vue-router2 percolate:migrations meteortesting:mocha @@ -47,5 +44,12 @@ ostrio:files simple:rest-bearer-token-parser simple:rest-json-error-handler littledata:synced-cron -mdg:meteor-apm-agent -typescript@4.5.4 +#mdg:meteor-apm-agent +seba:minifiers-autoprefixer +#mixmax:smart-disconnect +zodern:types +zodern:fix-async-stubs +typescript@4.9.5 +ecmascript@0.16.8 +lmieulet:meteor-legacy-coverage +lmieulet:meteor-coverage diff --git a/app/.meteor/release b/app/.meteor/release index 1d2a6d0f..5152abe9 100644 --- a/app/.meteor/release +++ b/app/.meteor/release @@ -1 +1 @@ -METEOR@2.8.0 +METEOR@2.16 diff --git a/app/.meteor/versions b/app/.meteor/versions index b9f87f06..b1345cda 100644 --- a/app/.meteor/versions +++ b/app/.meteor/versions @@ -1,87 +1,86 @@ -accounts-base@2.2.4 +accounts-base@2.2.11 accounts-google@1.4.0 -accounts-oauth@1.4.1 -accounts-password@2.3.1 +accounts-oauth@1.4.4 +accounts-password@2.4.0 accounts-patreon@0.1.0 akryum:npm-check@0.1.2 -akryum:vue-component@0.15.2 +akryum:vue-component@0.16.0 akryum:vue-component-dev-client@0.4.7 akryum:vue-component-dev-server@0.1.4 akryum:vue-router2@0.2.3 aldeed:collection2@3.5.0 -aldeed:schema-index@3.0.0 +aldeed:schema-index@3.1.0 allow-deny@1.1.1 autoupdate@1.8.0 -babel-compiler@7.9.2 +babel-compiler@7.10.5 babel-runtime@1.5.1 base64@1.0.12 binary-heap@1.0.11 -blaze-tools@1.1.3 -boilerplate-generator@1.7.1 -bozhao:link-accounts@2.6.1 +blaze-tools@1.1.4 +boilerplate-generator@1.7.2 +bozhao:link-accounts@2.8.0 caching-compiler@1.2.2 -caching-html-compiler@1.2.1 -callback-hook@1.4.0 -check@1.3.1 -coffeescript@2.4.1 +caching-html-compiler@1.2.2 +callback-hook@1.5.1 +check@1.4.1 +coffeescript@2.7.0 coffeescript-compiler@2.4.1 -dburles:mongo-collection-instances@0.3.6 -ddp@1.4.0 -ddp-client@2.6.0 -ddp-common@1.4.0 -ddp-rate-limiter@1.1.0 -ddp-server@2.6.0 -diff-sequence@1.1.1 -dynamic-import@0.7.2 -ecmascript@0.16.2 -ecmascript-runtime@0.8.0 +dburles:mongo-collection-instances@0.4.0 +ddp@1.4.1 +ddp-client@2.6.2 +ddp-common@1.4.1 +ddp-rate-limiter@1.2.1 +ddp-server@2.7.1 +diff-sequence@1.1.2 +dynamic-import@0.7.3 +ecmascript@0.16.8 +ecmascript-runtime@0.8.1 ecmascript-runtime-client@0.12.1 ecmascript-runtime-server@0.11.0 -ejson@1.1.2 -email@2.2.1 +ejson@1.1.3 +email@2.2.6 es5-shim@4.8.0 -fetch@0.1.1 -geojson-utils@1.0.10 -google-oauth@1.4.2 +fetch@0.1.4 +geojson-utils@1.0.11 +google-oauth@1.4.4 hot-code-push@1.0.4 -html-tools@1.1.3 -htmljs@1.1.1 +html-tools@1.1.4 +htmljs@1.2.1 http@2.0.0 id-map@1.1.1 inter-process-messaging@0.1.1 -lai:collection-extensions@0.3.0 -launch-screen@1.3.0 +lai:collection-extensions@0.4.0 +launch-screen@2.0.0 littledata:synced-cron@1.5.1 -livedata@1.0.18 +lmieulet:meteor-coverage@4.1.0 +lmieulet:meteor-legacy-coverage@0.2.0 localstorage@1.2.0 -logging@1.3.1 -mdg:meteor-apm-agent@3.5.1 -mdg:validated-method@1.2.0 -meteor@1.10.1 +logging@1.3.4 +mdg:validated-method@1.3.0 +meteor@1.11.5 meteor-base@1.5.1 -meteortesting:browser-tests@1.3.5 -meteortesting:mocha@2.0.3 +meteortesting:browser-tests@1.5.3 +meteortesting:mocha@2.1.0 meteortesting:mocha-core@8.1.2 mikowals:batch-insert@1.3.0 -minifier-css@1.6.1 -minifier-js@2.7.5 -minimongo@1.9.0 -mobile-experience@1.1.0 +minifier-css@1.6.4 +minifier-js@2.8.0 +minimongo@1.9.4 +mobile-experience@1.1.1 mobile-status-bar@1.1.0 -modern-browsers@0.1.8 -modules@0.19.0 -modules-runtime@0.13.0 -mongo@1.16.0 +modern-browsers@0.1.10 +modules@0.20.0 +modules-runtime@0.13.1 +mongo@1.16.10 mongo-decimal@0.1.3 mongo-dev-server@1.1.0 mongo-id@1.0.8 -mongo-livedata@1.0.12 -npm-mongo@4.9.0 -oauth@2.1.2 -oauth2@1.3.1 +npm-mongo@4.17.2 +oauth@2.2.1 +oauth2@1.3.2 ordered-dict@1.1.0 -ostrio:cookies@2.7.2 -ostrio:files@2.3.0 +ostrio:cookies@2.8.1 +ostrio:files@2.3.3 patreon-oauth@0.1.0 peerlibrary:assert@0.3.0 peerlibrary:check-extension@0.7.0 @@ -93,37 +92,39 @@ peerlibrary:reactive-mongo@0.4.1 peerlibrary:reactive-publish@0.10.0 peerlibrary:server-autorun@0.8.0 peerlibrary:subscription-data@0.8.0 -percolate:migrations@1.0.3 -promise@0.12.0 +percolate:migrations@1.1.1 +promise@0.12.2 raix:eventemitter@1.0.0 -random@1.2.0 -rate-limit@1.0.9 -react-fast-refresh@0.2.3 -reactive-dict@1.3.0 -reactive-var@1.0.11 +random@1.2.1 +rate-limit@1.1.1 +react-fast-refresh@0.2.8 +reactive-dict@1.3.1 +reactive-var@1.0.12 reload@1.3.1 retry@1.1.0 routepolicy@1.1.1 seba:minifiers-autoprefixer@2.0.1 -service-configuration@1.3.0 -session@1.2.0 +service-configuration@1.3.4 +session@1.2.1 sha@1.0.9 shell-server@0.5.0 simple:json-routes@2.3.1 simple:rest@1.2.1 simple:rest-bearer-token-parser@1.1.1 -simple:rest-json-error-handler@1.1.1 +simple:rest-json-error-handler@1.1.3 simple:rest-method-mixin@1.1.0 -socket-stream-client@0.5.0 -spacebars-compiler@1.3.1 +socket-stream-client@0.5.2 +spacebars-compiler@1.3.2 standard-minifier-js@2.8.1 static-html@1.3.2 -templating-tools@1.2.2 +templating-tools@1.2.3 tmeasday:check-npm-versions@1.0.2 -tracker@1.2.0 -typescript@4.5.4 -underscore@1.0.10 +tracker@1.3.3 +typescript@4.9.5 +underscore@1.6.1 url@1.3.2 -webapp@1.13.1 -webapp-hashing@1.1.0 +webapp@1.13.8 +webapp-hashing@1.1.1 zer0th:meteor-vuetify-loader@0.1.41 +zodern:fix-async-stubs@1.0.2 +zodern:types@1.0.13 diff --git a/app/client/game-icons.css b/app/client/game-icons.css new file mode 100644 index 00000000..d694ff66 --- /dev/null +++ b/app/client/game-icons.css @@ -0,0 +1,12328 @@ +/* + https://github.com/seiyria/gameicons-font +*/ + +@font-face { + font-family: "game-icons"; + src: url("/fonts/game-icons.eot?817af6e52c83163eb30ece54d9f7d16d?#iefix") format("embedded-opentype"), + url("/fonts/game-icons.woff?817af6e52c83163eb30ece54d9f7d16d") format("woff"), + url("/fonts/game-icons.ttf?817af6e52c83163eb30ece54d9f7d16d") format("truetype"); +} + +.game-icon { + line-height: 1; +} + +.game-icon:before { + font-family: game-icons !important; + font-style: normal; + font-weight: normal !important; + vertical-align: top; +} + +.game-icon-3d-glasses:before { + content: "\f000"; +} +.game-icon-3d-hammer:before { + content: "\f001"; +} +.game-icon-3d-meeple:before { + content: "\f002"; +} +.game-icon-3d-stairs:before { + content: "\f003"; +} +.game-icon-abacus:before { + content: "\f004"; +} +.game-icon-abbot-meeple:before { + content: "\f005"; +} +.game-icon-abdominal-armor:before { + content: "\f006"; +} +.game-icon-abstract-001:before { + content: "\f007"; +} +.game-icon-abstract-002:before { + content: "\f008"; +} +.game-icon-abstract-003:before { + content: "\f009"; +} +.game-icon-abstract-004:before { + content: "\f00a"; +} +.game-icon-abstract-005:before { + content: "\f00b"; +} +.game-icon-abstract-006:before { + content: "\f00c"; +} +.game-icon-abstract-007:before { + content: "\f00d"; +} +.game-icon-abstract-008:before { + content: "\f00e"; +} +.game-icon-abstract-009:before { + content: "\f00f"; +} +.game-icon-abstract-010:before { + content: "\f010"; +} +.game-icon-abstract-011:before { + content: "\f011"; +} +.game-icon-abstract-012:before { + content: "\f012"; +} +.game-icon-abstract-013:before { + content: "\f013"; +} +.game-icon-abstract-014:before { + content: "\f014"; +} +.game-icon-abstract-015:before { + content: "\f015"; +} +.game-icon-abstract-016:before { + content: "\f016"; +} +.game-icon-abstract-017:before { + content: "\f017"; +} +.game-icon-abstract-018:before { + content: "\f018"; +} +.game-icon-abstract-019:before { + content: "\f019"; +} +.game-icon-abstract-020:before { + content: "\f01a"; +} +.game-icon-abstract-021:before { + content: "\f01b"; +} +.game-icon-abstract-022:before { + content: "\f01c"; +} +.game-icon-abstract-023:before { + content: "\f01d"; +} +.game-icon-abstract-024:before { + content: "\f01e"; +} +.game-icon-abstract-025:before { + content: "\f01f"; +} +.game-icon-abstract-026:before { + content: "\f020"; +} +.game-icon-abstract-027:before { + content: "\f021"; +} +.game-icon-abstract-028:before { + content: "\f022"; +} +.game-icon-abstract-029:before { + content: "\f023"; +} +.game-icon-abstract-030:before { + content: "\f024"; +} +.game-icon-abstract-031:before { + content: "\f025"; +} +.game-icon-abstract-032:before { + content: "\f026"; +} +.game-icon-abstract-033:before { + content: "\f027"; +} +.game-icon-abstract-034:before { + content: "\f028"; +} +.game-icon-abstract-035:before { + content: "\f029"; +} +.game-icon-abstract-036:before { + content: "\f02a"; +} +.game-icon-abstract-037:before { + content: "\f02b"; +} +.game-icon-abstract-038:before { + content: "\f02c"; +} +.game-icon-abstract-039:before { + content: "\f02d"; +} +.game-icon-abstract-040:before { + content: "\f02e"; +} +.game-icon-abstract-041:before { + content: "\f02f"; +} +.game-icon-abstract-042:before { + content: "\f030"; +} +.game-icon-abstract-043:before { + content: "\f031"; +} +.game-icon-abstract-044:before { + content: "\f032"; +} +.game-icon-abstract-045:before { + content: "\f033"; +} +.game-icon-abstract-046:before { + content: "\f034"; +} +.game-icon-abstract-047:before { + content: "\f035"; +} +.game-icon-abstract-048:before { + content: "\f036"; +} +.game-icon-abstract-049:before { + content: "\f037"; +} +.game-icon-abstract-050:before { + content: "\f038"; +} +.game-icon-abstract-051:before { + content: "\f039"; +} +.game-icon-abstract-052:before { + content: "\f03a"; +} +.game-icon-abstract-053:before { + content: "\f03b"; +} +.game-icon-abstract-054:before { + content: "\f03c"; +} +.game-icon-abstract-055:before { + content: "\f03d"; +} +.game-icon-abstract-056:before { + content: "\f03e"; +} +.game-icon-abstract-057:before { + content: "\f03f"; +} +.game-icon-abstract-058:before { + content: "\f040"; +} +.game-icon-abstract-059:before { + content: "\f041"; +} +.game-icon-abstract-060:before { + content: "\f042"; +} +.game-icon-abstract-061:before { + content: "\f043"; +} +.game-icon-abstract-062:before { + content: "\f044"; +} +.game-icon-abstract-063:before { + content: "\f045"; +} +.game-icon-abstract-064:before { + content: "\f046"; +} +.game-icon-abstract-065:before { + content: "\f047"; +} +.game-icon-abstract-066:before { + content: "\f048"; +} +.game-icon-abstract-067:before { + content: "\f049"; +} +.game-icon-abstract-068:before { + content: "\f04a"; +} +.game-icon-abstract-069:before { + content: "\f04b"; +} +.game-icon-abstract-070:before { + content: "\f04c"; +} +.game-icon-abstract-071:before { + content: "\f04d"; +} +.game-icon-abstract-072:before { + content: "\f04e"; +} +.game-icon-abstract-073:before { + content: "\f04f"; +} +.game-icon-abstract-074:before { + content: "\f050"; +} +.game-icon-abstract-075:before { + content: "\f051"; +} +.game-icon-abstract-076:before { + content: "\f052"; +} +.game-icon-abstract-077:before { + content: "\f053"; +} +.game-icon-abstract-078:before { + content: "\f054"; +} +.game-icon-abstract-079:before { + content: "\f055"; +} +.game-icon-abstract-080:before { + content: "\f056"; +} +.game-icon-abstract-081:before { + content: "\f057"; +} +.game-icon-abstract-082:before { + content: "\f058"; +} +.game-icon-abstract-083:before { + content: "\f059"; +} +.game-icon-abstract-084:before { + content: "\f05a"; +} +.game-icon-abstract-085:before { + content: "\f05b"; +} +.game-icon-abstract-086:before { + content: "\f05c"; +} +.game-icon-abstract-087:before { + content: "\f05d"; +} +.game-icon-abstract-088:before { + content: "\f05e"; +} +.game-icon-abstract-089:before { + content: "\f05f"; +} +.game-icon-abstract-090:before { + content: "\f060"; +} +.game-icon-abstract-091:before { + content: "\f061"; +} +.game-icon-abstract-092:before { + content: "\f062"; +} +.game-icon-abstract-093:before { + content: "\f063"; +} +.game-icon-abstract-094:before { + content: "\f064"; +} +.game-icon-abstract-095:before { + content: "\f065"; +} +.game-icon-abstract-096:before { + content: "\f066"; +} +.game-icon-abstract-097:before { + content: "\f067"; +} +.game-icon-abstract-098:before { + content: "\f068"; +} +.game-icon-abstract-099:before { + content: "\f069"; +} +.game-icon-abstract-100:before { + content: "\f06a"; +} +.game-icon-abstract-101:before { + content: "\f06b"; +} +.game-icon-abstract-102:before { + content: "\f06c"; +} +.game-icon-abstract-103:before { + content: "\f06d"; +} +.game-icon-abstract-104:before { + content: "\f06e"; +} +.game-icon-abstract-105:before { + content: "\f06f"; +} +.game-icon-abstract-106:before { + content: "\f070"; +} +.game-icon-abstract-107:before { + content: "\f071"; +} +.game-icon-abstract-108:before { + content: "\f072"; +} +.game-icon-abstract-109:before { + content: "\f073"; +} +.game-icon-abstract-110:before { + content: "\f074"; +} +.game-icon-abstract-111:before { + content: "\f075"; +} +.game-icon-abstract-112:before { + content: "\f076"; +} +.game-icon-abstract-113:before { + content: "\f077"; +} +.game-icon-abstract-114:before { + content: "\f078"; +} +.game-icon-abstract-115:before { + content: "\f079"; +} +.game-icon-abstract-116:before { + content: "\f07a"; +} +.game-icon-abstract-117:before { + content: "\f07b"; +} +.game-icon-abstract-118:before { + content: "\f07c"; +} +.game-icon-abstract-119:before { + content: "\f07d"; +} +.game-icon-abstract-120:before { + content: "\f07e"; +} +.game-icon-abstract-121:before { + content: "\f07f"; +} +.game-icon-accordion:before { + content: "\f080"; +} +.game-icon-ace:before { + content: "\f081"; +} +.game-icon-achievement:before { + content: "\f082"; +} +.game-icon-achilles-heel:before { + content: "\f083"; +} +.game-icon-acid-blob:before { + content: "\f084"; +} +.game-icon-acid-tube:before { + content: "\f085"; +} +.game-icon-acid:before { + content: "\f086"; +} +.game-icon-acorn:before { + content: "\f087"; +} +.game-icon-acoustic-megaphone:before { + content: "\f088"; +} +.game-icon-acrobatic:before { + content: "\f089"; +} +.game-icon-aerial-signal:before { + content: "\f08a"; +} +.game-icon-aerodynamic-harpoon:before { + content: "\f08b"; +} +.game-icon-aerosol:before { + content: "\f08c"; +} +.game-icon-africa:before { + content: "\f08d"; +} +.game-icon-afterburn:before { + content: "\f08e"; +} +.game-icon-agave:before { + content: "\f08f"; +} +.game-icon-ages:before { + content: "\f090"; +} +.game-icon-air-balloon:before { + content: "\f091"; +} +.game-icon-air-force:before { + content: "\f092"; +} +.game-icon-air-man:before { + content: "\f093"; +} +.game-icon-air-zigzag:before { + content: "\f094"; +} +.game-icon-airplane-arrival:before { + content: "\f095"; +} +.game-icon-airplane-departure:before { + content: "\f096"; +} +.game-icon-airplane:before { + content: "\f097"; +} +.game-icon-airtight-hatch:before { + content: "\f098"; +} +.game-icon-ak47:before { + content: "\f099"; +} +.game-icon-ak47u:before { + content: "\f09a"; +} +.game-icon-akhet:before { + content: "\f09b"; +} +.game-icon-alarm-clock:before { + content: "\f09c"; +} +.game-icon-algae:before { + content: "\f09d"; +} +.game-icon-algeria:before { + content: "\f09e"; +} +.game-icon-alien-bug:before { + content: "\f09f"; +} +.game-icon-alien-egg:before { + content: "\f0a0"; +} +.game-icon-alien-fire:before { + content: "\f0a1"; +} +.game-icon-alien-skull:before { + content: "\f0a2"; +} +.game-icon-alien-stare:before { + content: "\f0a3"; +} +.game-icon-all-for-one:before { + content: "\f0a4"; +} +.game-icon-all-seeing-eye:before { + content: "\f0a5"; +} +.game-icon-allied-star:before { + content: "\f0a6"; +} +.game-icon-alligator-clip:before { + content: "\f0a7"; +} +.game-icon-almond:before { + content: "\f0a8"; +} +.game-icon-amber-mosquito:before { + content: "\f0a9"; +} +.game-icon-ambulance:before { + content: "\f0aa"; +} +.game-icon-american-football-ball:before { + content: "\f0ab"; +} +.game-icon-american-football-helmet:before { + content: "\f0ac"; +} +.game-icon-american-football-player:before { + content: "\f0ad"; +} +.game-icon-american-shield:before { + content: "\f0ae"; +} +.game-icon-amethyst:before { + content: "\f0af"; +} +.game-icon-ammo-box:before { + content: "\f0b0"; +} +.game-icon-ammonite-fossil:before { + content: "\f0b1"; +} +.game-icon-ammonite:before { + content: "\f0b2"; +} +.game-icon-amphora:before { + content: "\f0b3"; +} +.game-icon-ample-dress:before { + content: "\f0b4"; +} +.game-icon-amplitude:before { + content: "\f0b5"; +} +.game-icon-amputation:before { + content: "\f0b6"; +} +.game-icon-anarchy:before { + content: "\f0b7"; +} +.game-icon-anatomy:before { + content: "\f0b8"; +} +.game-icon-anchor:before { + content: "\f0b9"; +} +.game-icon-ancient-columns:before { + content: "\f0ba"; +} +.game-icon-ancient-ruins:before { + content: "\f0bb"; +} +.game-icon-ancient-screw:before { + content: "\f0bc"; +} +.game-icon-ancient-sword:before { + content: "\f0bd"; +} +.game-icon-android-mask:before { + content: "\f0be"; +} +.game-icon-andromeda-chain:before { + content: "\f0bf"; +} +.game-icon-angel-outfit:before { + content: "\f0c0"; +} +.game-icon-angel-wings:before { + content: "\f0c1"; +} +.game-icon-angler-fish:before { + content: "\f0c2"; +} +.game-icon-angola:before { + content: "\f0c3"; +} +.game-icon-angry-eyes:before { + content: "\f0c4"; +} +.game-icon-angular-spider:before { + content: "\f0c5"; +} +.game-icon-animal-hide:before { + content: "\f0c6"; +} +.game-icon-animal-skull:before { + content: "\f0c7"; +} +.game-icon-ankh:before { + content: "\f0c8"; +} +.game-icon-annexation:before { + content: "\f0c9"; +} +.game-icon-ant:before { + content: "\f0ca"; +} +.game-icon-antarctica:before { + content: "\f0cb"; +} +.game-icon-anteater:before { + content: "\f0cc"; +} +.game-icon-anthem:before { + content: "\f0cd"; +} +.game-icon-anti-aircraft-gun:before { + content: "\f0ce"; +} +.game-icon-antibody:before { + content: "\f0cf"; +} +.game-icon-anticlockwise-rotation:before { + content: "\f0d0"; +} +.game-icon-ants:before { + content: "\f0d1"; +} +.game-icon-anubis:before { + content: "\f0d2"; +} +.game-icon-anvil-impact:before { + content: "\f0d3"; +} +.game-icon-anvil:before { + content: "\f0d4"; +} +.game-icon-apc:before { + content: "\f0d5"; +} +.game-icon-apollo-capsule:before { + content: "\f0d6"; +} +.game-icon-apothecary:before { + content: "\f0d7"; +} +.game-icon-apple-core:before { + content: "\f0d8"; +} +.game-icon-apple-maggot:before { + content: "\f0d9"; +} +.game-icon-apple-seeds:before { + content: "\f0da"; +} +.game-icon-aquarium:before { + content: "\f0db"; +} +.game-icon-aquarius:before { + content: "\f0dc"; +} +.game-icon-aqueduct:before { + content: "\f0dd"; +} +.game-icon-arabic-door:before { + content: "\f0de"; +} +.game-icon-arc-triomphe:before { + content: "\f0df"; +} +.game-icon-arch-bridge:before { + content: "\f0e0"; +} +.game-icon-archaeopteryx-fossil:before { + content: "\f0e1"; +} +.game-icon-archer:before { + content: "\f0e2"; +} +.game-icon-archery-target:before { + content: "\f0e3"; +} +.game-icon-architect-mask:before { + content: "\f0e4"; +} +.game-icon-archive-register:before { + content: "\f0e5"; +} +.game-icon-archive-research:before { + content: "\f0e6"; +} +.game-icon-arcing-bolt:before { + content: "\f0e7"; +} +.game-icon-arena:before { + content: "\f0e8"; +} +.game-icon-aries:before { + content: "\f0e9"; +} +.game-icon-arm-bandage:before { + content: "\f0ea"; +} +.game-icon-arm-sling:before { + content: "\f0eb"; +} +.game-icon-arm:before { + content: "\f0ec"; +} +.game-icon-armadillo-tail:before { + content: "\f0ed"; +} +.game-icon-armadillo:before { + content: "\f0ee"; +} +.game-icon-armor-downgrade:before { + content: "\f0ef"; +} +.game-icon-armor-punch:before { + content: "\f0f0"; +} +.game-icon-armor-upgrade:before { + content: "\f0f1"; +} +.game-icon-armor-vest:before { + content: "\f0f2"; +} +.game-icon-armored-boomerang:before { + content: "\f0f3"; +} +.game-icon-armored-pants:before { + content: "\f0f4"; +} +.game-icon-armoured-shell:before { + content: "\f0f5"; +} +.game-icon-arrest:before { + content: "\f0f6"; +} +.game-icon-arrow-cluster:before { + content: "\f0f7"; +} +.game-icon-arrow-cursor:before { + content: "\f0f8"; +} +.game-icon-arrow-dunk:before { + content: "\f0f9"; +} +.game-icon-arrow-flights:before { + content: "\f0fa"; +} +.game-icon-arrow-scope:before { + content: "\f0fb"; +} +.game-icon-arrow-wings:before { + content: "\f0fc"; +} +.game-icon-arrowed:before { + content: "\f0fd"; +} +.game-icon-arrowhead:before { + content: "\f0fe"; +} +.game-icon-arrows-shield:before { + content: "\f0ff"; +} +.game-icon-arson:before { + content: "\f100"; +} +.game-icon-artificial-hive:before { + content: "\f101"; +} +.game-icon-artificial-intelligence:before { + content: "\f102"; +} +.game-icon-artillery-shell:before { + content: "\f103"; +} +.game-icon-ascending-block:before { + content: "\f104"; +} +.game-icon-asian-lantern:before { + content: "\f105"; +} +.game-icon-asparagus:before { + content: "\f106"; +} +.game-icon-aspergillum:before { + content: "\f107"; +} +.game-icon-assassin-pocket:before { + content: "\f108"; +} +.game-icon-asteroid:before { + content: "\f109"; +} +.game-icon-astrolabe:before { + content: "\f10a"; +} +.game-icon-astronaut-helmet:before { + content: "\f10b"; +} +.game-icon-at-sea:before { + content: "\f10c"; +} +.game-icon-atlas:before { + content: "\f10d"; +} +.game-icon-atom-core:before { + content: "\f10e"; +} +.game-icon-atom:before { + content: "\f10f"; +} +.game-icon-atomic-slashes:before { + content: "\f110"; +} +.game-icon-attached-shield:before { + content: "\f111"; +} +.game-icon-aubergine:before { + content: "\f112"; +} +.game-icon-audio-cassette:before { + content: "\f113"; +} +.game-icon-aura:before { + content: "\f114"; +} +.game-icon-australia:before { + content: "\f115"; +} +.game-icon-auto-repair:before { + content: "\f116"; +} +.game-icon-autogun:before { + content: "\f117"; +} +.game-icon-automatic-sas:before { + content: "\f118"; +} +.game-icon-avocado:before { + content: "\f119"; +} +.game-icon-avoidance:before { + content: "\f11a"; +} +.game-icon-awareness:before { + content: "\f11b"; +} +.game-icon-axe-in-log:before { + content: "\f11c"; +} +.game-icon-axe-in-stump:before { + content: "\f11d"; +} +.game-icon-axe-swing:before { + content: "\f11e"; +} +.game-icon-axe-sword:before { + content: "\f11f"; +} +.game-icon-axolotl:before { + content: "\f120"; +} +.game-icon-aztec-calendar-sun:before { + content: "\f121"; +} +.game-icon-azul-flake:before { + content: "\f122"; +} +.game-icon-baby-bottle:before { + content: "\f123"; +} +.game-icon-baby-face:before { + content: "\f124"; +} +.game-icon-babyfoot-players:before { + content: "\f125"; +} +.game-icon-back-forth:before { + content: "\f126"; +} +.game-icon-back-pain:before { + content: "\f127"; +} +.game-icon-backbone-shell:before { + content: "\f128"; +} +.game-icon-backgammon:before { + content: "\f129"; +} +.game-icon-backpack:before { + content: "\f12a"; +} +.game-icon-backstab:before { + content: "\f12b"; +} +.game-icon-backup:before { + content: "\f12c"; +} +.game-icon-backward-time:before { + content: "\f12d"; +} +.game-icon-bacon:before { + content: "\f12e"; +} +.game-icon-bad-breath:before { + content: "\f12f"; +} +.game-icon-bad-gnome:before { + content: "\f130"; +} +.game-icon-bagpipes:before { + content: "\f131"; +} +.game-icon-bal-leth:before { + content: "\f132"; +} +.game-icon-balaclava:before { + content: "\f133"; +} +.game-icon-balkenkreuz:before { + content: "\f134"; +} +.game-icon-ball-glow:before { + content: "\f135"; +} +.game-icon-ball-heart:before { + content: "\f136"; +} +.game-icon-ball-pyramid:before { + content: "\f137"; +} +.game-icon-ballerina-shoes:before { + content: "\f138"; +} +.game-icon-ballista:before { + content: "\f139"; +} +.game-icon-balloon-dog:before { + content: "\f13a"; +} +.game-icon-balloons:before { + content: "\f13b"; +} +.game-icon-bamboo-fountain:before { + content: "\f13c"; +} +.game-icon-bamboo:before { + content: "\f13d"; +} +.game-icon-banana-bunch:before { + content: "\f13e"; +} +.game-icon-banana-peel:before { + content: "\f13f"; +} +.game-icon-banana-peeled:before { + content: "\f140"; +} +.game-icon-banana:before { + content: "\f141"; +} +.game-icon-bandage-roll:before { + content: "\f142"; +} +.game-icon-bandaged:before { + content: "\f143"; +} +.game-icon-bandana:before { + content: "\f144"; +} +.game-icon-bandit:before { + content: "\f145"; +} +.game-icon-banging-gavel:before { + content: "\f146"; +} +.game-icon-banjo:before { + content: "\f147"; +} +.game-icon-bank:before { + content: "\f148"; +} +.game-icon-banknote:before { + content: "\f149"; +} +.game-icon-baobab:before { + content: "\f14a"; +} +.game-icon-bar-stool:before { + content: "\f14b"; +} +.game-icon-barbarian:before { + content: "\f14c"; +} +.game-icon-barbecue:before { + content: "\f14d"; +} +.game-icon-barbed-arrow:before { + content: "\f14e"; +} +.game-icon-barbed-coil:before { + content: "\f14f"; +} +.game-icon-barbed-nails:before { + content: "\f150"; +} +.game-icon-barbed-spear:before { + content: "\f151"; +} +.game-icon-barbed-star:before { + content: "\f152"; +} +.game-icon-barbed-sun:before { + content: "\f153"; +} +.game-icon-barbed-wire:before { + content: "\f154"; +} +.game-icon-barbute:before { + content: "\f155"; +} +.game-icon-barefoot:before { + content: "\f156"; +} +.game-icon-barn:before { + content: "\f157"; +} +.game-icon-barracks-tent:before { + content: "\f158"; +} +.game-icon-barracks:before { + content: "\f159"; +} +.game-icon-barrel-leak:before { + content: "\f15a"; +} +.game-icon-barrel:before { + content: "\f15b"; +} +.game-icon-barricade:before { + content: "\f15c"; +} +.game-icon-barrier:before { + content: "\f15d"; +} +.game-icon-base-dome:before { + content: "\f15e"; +} +.game-icon-baseball-bat:before { + content: "\f15f"; +} +.game-icon-baseball-glove:before { + content: "\f160"; +} +.game-icon-basket:before { + content: "\f161"; +} +.game-icon-basketball-ball:before { + content: "\f162"; +} +.game-icon-basketball-basket:before { + content: "\f163"; +} +.game-icon-basketball-jersey:before { + content: "\f164"; +} +.game-icon-basset-hound-head:before { + content: "\f165"; +} +.game-icon-bassoon:before { + content: "\f166"; +} +.game-icon-bastet:before { + content: "\f167"; +} +.game-icon-bat-2:before { + content: "\f168"; +} +.game-icon-bat-blade:before { + content: "\f169"; +} +.game-icon-bat-leth:before { + content: "\f16a"; +} +.game-icon-bat-mask:before { + content: "\f16b"; +} +.game-icon-bat-wing:before { + content: "\f16c"; +} +.game-icon-bat:before { + content: "\f16d"; +} +.game-icon-bathtub:before { + content: "\f16e"; +} +.game-icon-baton:before { + content: "\f16f"; +} +.game-icon-battered-axe:before { + content: "\f170"; +} +.game-icon-batteries:before { + content: "\f171"; +} +.game-icon-battery-0:before { + content: "\f172"; +} +.game-icon-battery-100:before { + content: "\f173"; +} +.game-icon-battery-25:before { + content: "\f174"; +} +.game-icon-battery-50:before { + content: "\f175"; +} +.game-icon-battery-75:before { + content: "\f176"; +} +.game-icon-battery-minus:before { + content: "\f177"; +} +.game-icon-battery-pack-alt:before { + content: "\f178"; +} +.game-icon-battery-pack:before { + content: "\f179"; +} +.game-icon-battery-plus:before { + content: "\f17a"; +} +.game-icon-battle-axe:before { + content: "\f17b"; +} +.game-icon-battle-gear:before { + content: "\f17c"; +} +.game-icon-battle-mech:before { + content: "\f17d"; +} +.game-icon-battle-tank:before { + content: "\f17e"; +} +.game-icon-battleship:before { + content: "\f17f"; +} +.game-icon-batwing-emblem:before { + content: "\f180"; +} +.game-icon-bayonet:before { + content: "\f181"; +} +.game-icon-beach-bag:before { + content: "\f182"; +} +.game-icon-beach-ball:before { + content: "\f183"; +} +.game-icon-beach-bucket:before { + content: "\f184"; +} +.game-icon-beam-satellite:before { + content: "\f185"; +} +.game-icon-beam-wake:before { + content: "\f186"; +} +.game-icon-beams-aura:before { + content: "\f187"; +} +.game-icon-beanstalk:before { + content: "\f188"; +} +.game-icon-bear-face:before { + content: "\f189"; +} +.game-icon-bear-head:before { + content: "\f18a"; +} +.game-icon-beard:before { + content: "\f18b"; +} +.game-icon-beast-eye:before { + content: "\f18c"; +} +.game-icon-beaver:before { + content: "\f18d"; +} +.game-icon-bed-lamp:before { + content: "\f18e"; +} +.game-icon-bed:before { + content: "\f18f"; +} +.game-icon-bee:before { + content: "\f190"; +} +.game-icon-beech:before { + content: "\f191"; +} +.game-icon-beehive:before { + content: "\f192"; +} +.game-icon-beer-bottle:before { + content: "\f193"; +} +.game-icon-beer-horn:before { + content: "\f194"; +} +.game-icon-beer-stein:before { + content: "\f195"; +} +.game-icon-beet:before { + content: "\f196"; +} +.game-icon-beetle-shell:before { + content: "\f197"; +} +.game-icon-behold:before { + content: "\f198"; +} +.game-icon-belgium:before { + content: "\f199"; +} +.game-icon-bell-pepper:before { + content: "\f19a"; +} +.game-icon-bell-shield:before { + content: "\f19b"; +} +.game-icon-bellows:before { + content: "\f19c"; +} +.game-icon-belt-armor:before { + content: "\f19d"; +} +.game-icon-belt-buckles:before { + content: "\f19e"; +} +.game-icon-belt:before { + content: "\f19f"; +} +.game-icon-berries-bowl:before { + content: "\f1a0"; +} +.game-icon-berry-bush:before { + content: "\f1a1"; +} +.game-icon-bestial-fangs:before { + content: "\f1a2"; +} +.game-icon-beveled-star:before { + content: "\f1a3"; +} +.game-icon-biceps:before { + content: "\f1a4"; +} +.game-icon-big-diamond-ring:before { + content: "\f1a5"; +} +.game-icon-big-egg:before { + content: "\f1a6"; +} +.game-icon-big-gear:before { + content: "\f1a7"; +} +.game-icon-big-wave:before { + content: "\f1a8"; +} +.game-icon-billed-cap:before { + content: "\f1a9"; +} +.game-icon-bindle:before { + content: "\f1aa"; +} +.game-icon-binoculars:before { + content: "\f1ab"; +} +.game-icon-biohazard:before { + content: "\f1ac"; +} +.game-icon-biplane:before { + content: "\f1ad"; +} +.game-icon-birch-trees:before { + content: "\f1ae"; +} +.game-icon-bird-cage:before { + content: "\f1af"; +} +.game-icon-bird-claw:before { + content: "\f1b0"; +} +.game-icon-bird-house:before { + content: "\f1b1"; +} +.game-icon-bird-limb:before { + content: "\f1b2"; +} +.game-icon-bird-mask:before { + content: "\f1b3"; +} +.game-icon-bird-twitter:before { + content: "\f1b4"; +} +.game-icon-bison:before { + content: "\f1b5"; +} +.game-icon-black-bar:before { + content: "\f1b6"; +} +.game-icon-black-belt:before { + content: "\f1b7"; +} +.game-icon-black-book:before { + content: "\f1b8"; +} +.game-icon-black-bridge:before { + content: "\f1b9"; +} +.game-icon-black-cat:before { + content: "\f1ba"; +} +.game-icon-black-flag:before { + content: "\f1bb"; +} +.game-icon-black-hand-shield:before { + content: "\f1bc"; +} +.game-icon-black-hole-bolas:before { + content: "\f1bd"; +} +.game-icon-black-knight-helm:before { + content: "\f1be"; +} +.game-icon-black-sea:before { + content: "\f1bf"; +} +.game-icon-blackball:before { + content: "\f1c0"; +} +.game-icon-blackcurrant:before { + content: "\f1c1"; +} +.game-icon-blacksmith:before { + content: "\f1c2"; +} +.game-icon-blade-bite:before { + content: "\f1c3"; +} +.game-icon-blade-drag:before { + content: "\f1c4"; +} +.game-icon-blade-fall:before { + content: "\f1c5"; +} +.game-icon-blast:before { + content: "\f1c6"; +} +.game-icon-blaster:before { + content: "\f1c7"; +} +.game-icon-bleeding-eye:before { + content: "\f1c8"; +} +.game-icon-bleeding-heart:before { + content: "\f1c9"; +} +.game-icon-bleeding-wound:before { + content: "\f1ca"; +} +.game-icon-blender:before { + content: "\f1cb"; +} +.game-icon-blindfold:before { + content: "\f1cc"; +} +.game-icon-block-house:before { + content: "\f1cd"; +} +.game-icon-blood:before { + content: "\f1ce"; +} +.game-icon-bloody-stash:before { + content: "\f1cf"; +} +.game-icon-bloody-sword:before { + content: "\f1d0"; +} +.game-icon-blunderbuss:before { + content: "\f1d1"; +} +.game-icon-bo:before { + content: "\f1d2"; +} +.game-icon-boar-ensign:before { + content: "\f1d3"; +} +.game-icon-boar-tusks:before { + content: "\f1d4"; +} +.game-icon-boar:before { + content: "\f1d5"; +} +.game-icon-boat-engine:before { + content: "\f1d6"; +} +.game-icon-boat-fishing:before { + content: "\f1d7"; +} +.game-icon-boat-horizon:before { + content: "\f1d8"; +} +.game-icon-boat-propeller:before { + content: "\f1d9"; +} +.game-icon-boba:before { + content: "\f1da"; +} +.game-icon-body-balance:before { + content: "\f1db"; +} +.game-icon-body-height:before { + content: "\f1dc"; +} +.game-icon-body-swapping:before { + content: "\f1dd"; +} +.game-icon-boiling-bubbles:before { + content: "\f1de"; +} +.game-icon-bok-choy:before { + content: "\f1df"; +} +.game-icon-bolas:before { + content: "\f1e0"; +} +.game-icon-bolivia:before { + content: "\f1e1"; +} +.game-icon-bolt-bomb:before { + content: "\f1e2"; +} +.game-icon-bolt-cutter:before { + content: "\f1e3"; +} +.game-icon-bolt-drop:before { + content: "\f1e4"; +} +.game-icon-bolt-eye:before { + content: "\f1e5"; +} +.game-icon-bolt-saw:before { + content: "\f1e6"; +} +.game-icon-bolt-shield:before { + content: "\f1e7"; +} +.game-icon-bolt-spell-cast:before { + content: "\f1e8"; +} +.game-icon-bolter-gun:before { + content: "\f1e9"; +} +.game-icon-bomber:before { + content: "\f1ea"; +} +.game-icon-bombing-run:before { + content: "\f1eb"; +} +.game-icon-bone-gnawer:before { + content: "\f1ec"; +} +.game-icon-bone-knife-2:before { + content: "\f1ed"; +} +.game-icon-bone-knife:before { + content: "\f1ee"; +} +.game-icon-bone-mace:before { + content: "\f1ef"; +} +.game-icon-bonsai-tree:before { + content: "\f1f0"; +} +.game-icon-book-aura:before { + content: "\f1f1"; +} +.game-icon-book-cover-2:before { + content: "\f1f2"; +} +.game-icon-book-cover:before { + content: "\f1f3"; +} +.game-icon-book-pile:before { + content: "\f1f4"; +} +.game-icon-book-storm:before { + content: "\f1f5"; +} +.game-icon-bookmark:before { + content: "\f1f6"; +} +.game-icon-bookmarklet:before { + content: "\f1f7"; +} +.game-icon-bookshelf:before { + content: "\f1f8"; +} +.game-icon-boombox:before { + content: "\f1f9"; +} +.game-icon-boomerang-cross:before { + content: "\f1fa"; +} +.game-icon-boomerang-sun:before { + content: "\f1fb"; +} +.game-icon-boomerang:before { + content: "\f1fc"; +} +.game-icon-boot-kick:before { + content: "\f1fd"; +} +.game-icon-boot-prints:before { + content: "\f1fe"; +} +.game-icon-boot-stomp:before { + content: "\f1ff"; +} +.game-icon-boots:before { + content: "\f200"; +} +.game-icon-booze:before { + content: "\f201"; +} +.game-icon-bordered-shield:before { + content: "\f202"; +} +.game-icon-boss-key:before { + content: "\f203"; +} +.game-icon-bottle-cap:before { + content: "\f204"; +} +.game-icon-bottle-vapors:before { + content: "\f205"; +} +.game-icon-bottled-bolt:before { + content: "\f206"; +} +.game-icon-bottled-shadow:before { + content: "\f207"; +} +.game-icon-bottom-right-3d-arrow:before { + content: "\f208"; +} +.game-icon-boulder-dash:before { + content: "\f209"; +} +.game-icon-bouncing-spring:before { + content: "\f20a"; +} +.game-icon-bouncing-sword:before { + content: "\f20b"; +} +.game-icon-bow-arrow:before { + content: "\f20c"; +} +.game-icon-bow-string:before { + content: "\f20d"; +} +.game-icon-bow-tie-ribbon:before { + content: "\f20e"; +} +.game-icon-bow-tie:before { + content: "\f20f"; +} +.game-icon-bowels:before { + content: "\f210"; +} +.game-icon-bowen-knot:before { + content: "\f211"; +} +.game-icon-bowie-knife-2:before { + content: "\f212"; +} +.game-icon-bowie-knife:before { + content: "\f213"; +} +.game-icon-bowl-of-rice:before { + content: "\f214"; +} +.game-icon-bowl-spiral:before { + content: "\f215"; +} +.game-icon-bowling-alley:before { + content: "\f216"; +} +.game-icon-bowling-pin:before { + content: "\f217"; +} +.game-icon-bowling-propulsion:before { + content: "\f218"; +} +.game-icon-bowling-strike:before { + content: "\f219"; +} +.game-icon-bowman:before { + content: "\f21a"; +} +.game-icon-box-cutter:before { + content: "\f21b"; +} +.game-icon-box-trap:before { + content: "\f21c"; +} +.game-icon-box-unpacking:before { + content: "\f21d"; +} +.game-icon-boxing-glove-surprise:before { + content: "\f21e"; +} +.game-icon-boxing-glove:before { + content: "\f21f"; +} +.game-icon-boxing-ring:before { + content: "\f220"; +} +.game-icon-bracer:before { + content: "\f221"; +} +.game-icon-bracers:before { + content: "\f222"; +} +.game-icon-brain-dump:before { + content: "\f223"; +} +.game-icon-brain-freeze:before { + content: "\f224"; +} +.game-icon-brain-leak:before { + content: "\f225"; +} +.game-icon-brain-stem:before { + content: "\f226"; +} +.game-icon-brain-tentacle:before { + content: "\f227"; +} +.game-icon-brain:before { + content: "\f228"; +} +.game-icon-brainstorm:before { + content: "\f229"; +} +.game-icon-branch-arrow:before { + content: "\f22a"; +} +.game-icon-brandy-bottle:before { + content: "\f22b"; +} +.game-icon-brasero:before { + content: "\f22c"; +} +.game-icon-brass-eye:before { + content: "\f22d"; +} +.game-icon-brass-knuckles:before { + content: "\f22e"; +} +.game-icon-brazil-flag:before { + content: "\f22f"; +} +.game-icon-brazil:before { + content: "\f230"; +} +.game-icon-bread-slice:before { + content: "\f231"; +} +.game-icon-bread:before { + content: "\f232"; +} +.game-icon-breaking-chain:before { + content: "\f233"; +} +.game-icon-breastplate:before { + content: "\f234"; +} +.game-icon-brick-pile:before { + content: "\f235"; +} +.game-icon-brick-wall:before { + content: "\f236"; +} +.game-icon-bridge:before { + content: "\f237"; +} +.game-icon-briefcase:before { + content: "\f238"; +} +.game-icon-bright-explosion:before { + content: "\f239"; +} +.game-icon-broad-dagger:before { + content: "\f23a"; +} +.game-icon-broadhead-arrow:before { + content: "\f23b"; +} +.game-icon-broadsword:before { + content: "\f23c"; +} +.game-icon-broccoli:before { + content: "\f23d"; +} +.game-icon-brodie-helmet:before { + content: "\f23e"; +} +.game-icon-broken-arrow:before { + content: "\f23f"; +} +.game-icon-broken-axe:before { + content: "\f240"; +} +.game-icon-broken-bone:before { + content: "\f241"; +} +.game-icon-broken-bottle:before { + content: "\f242"; +} +.game-icon-broken-heart-zone:before { + content: "\f243"; +} +.game-icon-broken-heart:before { + content: "\f244"; +} +.game-icon-broken-pottery:before { + content: "\f245"; +} +.game-icon-broken-ribbon:before { + content: "\f246"; +} +.game-icon-broken-shield:before { + content: "\f247"; +} +.game-icon-broken-skull:before { + content: "\f248"; +} +.game-icon-broken-tablet:before { + content: "\f249"; +} +.game-icon-broken-wall:before { + content: "\f24a"; +} +.game-icon-broom:before { + content: "\f24b"; +} +.game-icon-brutal-helm:before { + content: "\f24c"; +} +.game-icon-brute:before { + content: "\f24d"; +} +.game-icon-bubble-field:before { + content: "\f24e"; +} +.game-icon-bubbles:before { + content: "\f24f"; +} +.game-icon-bubbling-beam:before { + content: "\f250"; +} +.game-icon-bubbling-bowl:before { + content: "\f251"; +} +.game-icon-bubbling-flask:before { + content: "\f252"; +} +.game-icon-bud:before { + content: "\f253"; +} +.game-icon-buffalo-head:before { + content: "\f254"; +} +.game-icon-bug-net:before { + content: "\f255"; +} +.game-icon-bugle-call:before { + content: "\f256"; +} +.game-icon-bulb:before { + content: "\f257"; +} +.game-icon-bulgaria:before { + content: "\f258"; +} +.game-icon-bull-horns:before { + content: "\f259"; +} +.game-icon-bull:before { + content: "\f25a"; +} +.game-icon-bulldozer:before { + content: "\f25b"; +} +.game-icon-bullet-bill:before { + content: "\f25c"; +} +.game-icon-bullet-impacts:before { + content: "\f25d"; +} +.game-icon-bullets:before { + content: "\f25e"; +} +.game-icon-bullseye:before { + content: "\f25f"; +} +.game-icon-bully-minion:before { + content: "\f260"; +} +.game-icon-bundle-grenade:before { + content: "\f261"; +} +.game-icon-bunk-beds:before { + content: "\f262"; +} +.game-icon-bunker-assault:before { + content: "\f263"; +} +.game-icon-bunker:before { + content: "\f264"; +} +.game-icon-bunny-slippers:before { + content: "\f265"; +} +.game-icon-buoy:before { + content: "\f266"; +} +.game-icon-burn:before { + content: "\f267"; +} +.game-icon-burning-blobs:before { + content: "\f268"; +} +.game-icon-burning-book:before { + content: "\f269"; +} +.game-icon-burning-dot:before { + content: "\f26a"; +} +.game-icon-burning-embers:before { + content: "\f26b"; +} +.game-icon-burning-eye:before { + content: "\f26c"; +} +.game-icon-burning-forest:before { + content: "\f26d"; +} +.game-icon-burning-meteor:before { + content: "\f26e"; +} +.game-icon-burning-passion:before { + content: "\f26f"; +} +.game-icon-burning-round-shot:before { + content: "\f270"; +} +.game-icon-burning-skull:before { + content: "\f271"; +} +.game-icon-burning-tree:before { + content: "\f272"; +} +.game-icon-burst-blob:before { + content: "\f273"; +} +.game-icon-bus-doors:before { + content: "\f274"; +} +.game-icon-bus-stop:before { + content: "\f275"; +} +.game-icon-bus:before { + content: "\f276"; +} +.game-icon-butter-toast:before { + content: "\f277"; +} +.game-icon-butter:before { + content: "\f278"; +} +.game-icon-butterfly-flower:before { + content: "\f279"; +} +.game-icon-butterfly-knife-2:before { + content: "\f27a"; +} +.game-icon-butterfly-knife:before { + content: "\f27b"; +} +.game-icon-butterfly-warning:before { + content: "\f27c"; +} +.game-icon-butterfly:before { + content: "\f27d"; +} +.game-icon-button-finger:before { + content: "\f27e"; +} +.game-icon-buy-card:before { + content: "\f27f"; +} +.game-icon-byzantin-temple:before { + content: "\f280"; +} +.game-icon-c96:before { + content: "\f281"; +} +.game-icon-cabbage:before { + content: "\f282"; +} +.game-icon-cable-stayed-bridge:before { + content: "\f283"; +} +.game-icon-cactus-pot:before { + content: "\f284"; +} +.game-icon-cactus-tap:before { + content: "\f285"; +} +.game-icon-cactus:before { + content: "\f286"; +} +.game-icon-cadillac-helm:before { + content: "\f287"; +} +.game-icon-caduceus:before { + content: "\f288"; +} +.game-icon-caesar:before { + content: "\f289"; +} +.game-icon-cage:before { + content: "\f28a"; +} +.game-icon-caged-ball:before { + content: "\f28b"; +} +.game-icon-cake-slice:before { + content: "\f28c"; +} +.game-icon-calavera:before { + content: "\f28d"; +} +.game-icon-calculator:before { + content: "\f28e"; +} +.game-icon-caldera:before { + content: "\f28f"; +} +.game-icon-calendar-half-year:before { + content: "\f290"; +} +.game-icon-calendar:before { + content: "\f291"; +} +.game-icon-caltrops:before { + content: "\f292"; +} +.game-icon-camargue-cross:before { + content: "\f293"; +} +.game-icon-cambodia:before { + content: "\f294"; +} +.game-icon-camel-head:before { + content: "\f295"; +} +.game-icon-camel:before { + content: "\f296"; +} +.game-icon-camp-cooking-pot:before { + content: "\f297"; +} +.game-icon-campfire:before { + content: "\f298"; +} +.game-icon-camping-tent:before { + content: "\f299"; +} +.game-icon-cancel:before { + content: "\f29a"; +} +.game-icon-cancer:before { + content: "\f29b"; +} +.game-icon-candle-flame:before { + content: "\f29c"; +} +.game-icon-candle-holder:before { + content: "\f29d"; +} +.game-icon-candle-light:before { + content: "\f29e"; +} +.game-icon-candle-skull:before { + content: "\f29f"; +} +.game-icon-candlebright:before { + content: "\f2a0"; +} +.game-icon-candles:before { + content: "\f2a1"; +} +.game-icon-candlestick-phone:before { + content: "\f2a2"; +} +.game-icon-candy-canes:before { + content: "\f2a3"; +} +.game-icon-canned-fish:before { + content: "\f2a4"; +} +.game-icon-cannister:before { + content: "\f2a5"; +} +.game-icon-cannon-ball:before { + content: "\f2a6"; +} +.game-icon-cannon-shot:before { + content: "\f2a7"; +} +.game-icon-cannon:before { + content: "\f2a8"; +} +.game-icon-canoe:before { + content: "\f2a9"; +} +.game-icon-cantua:before { + content: "\f2aa"; +} +.game-icon-cape-armor:before { + content: "\f2ab"; +} +.game-icon-cape:before { + content: "\f2ac"; +} +.game-icon-capitol:before { + content: "\f2ad"; +} +.game-icon-capricorn:before { + content: "\f2ae"; +} +.game-icon-captain-hat-profile:before { + content: "\f2af"; +} +.game-icon-capybara:before { + content: "\f2b0"; +} +.game-icon-car-battery:before { + content: "\f2b1"; +} +.game-icon-car-door:before { + content: "\f2b2"; +} +.game-icon-car-key:before { + content: "\f2b3"; +} +.game-icon-car-seat:before { + content: "\f2b4"; +} +.game-icon-car-wheel:before { + content: "\f2b5"; +} +.game-icon-carabiner:before { + content: "\f2b6"; +} +.game-icon-carambola:before { + content: "\f2b7"; +} +.game-icon-caravan:before { + content: "\f2b8"; +} +.game-icon-caravel:before { + content: "\f2b9"; +} +.game-icon-card-10-clubs:before { + content: "\f2ba"; +} +.game-icon-card-10-diamonds:before { + content: "\f2bb"; +} +.game-icon-card-10-hearts:before { + content: "\f2bc"; +} +.game-icon-card-10-spades:before { + content: "\f2bd"; +} +.game-icon-card-2-clubs:before { + content: "\f2be"; +} +.game-icon-card-2-diamonds:before { + content: "\f2bf"; +} +.game-icon-card-2-hearts:before { + content: "\f2c0"; +} +.game-icon-card-2-spades:before { + content: "\f2c1"; +} +.game-icon-card-3-clubs:before { + content: "\f2c2"; +} +.game-icon-card-3-diamonds:before { + content: "\f2c3"; +} +.game-icon-card-3-hearts:before { + content: "\f2c4"; +} +.game-icon-card-3-spades:before { + content: "\f2c5"; +} +.game-icon-card-4-clubs:before { + content: "\f2c6"; +} +.game-icon-card-4-diamonds:before { + content: "\f2c7"; +} +.game-icon-card-4-hearts:before { + content: "\f2c8"; +} +.game-icon-card-4-spades:before { + content: "\f2c9"; +} +.game-icon-card-5-clubs:before { + content: "\f2ca"; +} +.game-icon-card-5-diamonds:before { + content: "\f2cb"; +} +.game-icon-card-5-hearts:before { + content: "\f2cc"; +} +.game-icon-card-5-spades:before { + content: "\f2cd"; +} +.game-icon-card-6-clubs:before { + content: "\f2ce"; +} +.game-icon-card-6-diamonds:before { + content: "\f2cf"; +} +.game-icon-card-6-hearts:before { + content: "\f2d0"; +} +.game-icon-card-6-spades:before { + content: "\f2d1"; +} +.game-icon-card-7-clubs:before { + content: "\f2d2"; +} +.game-icon-card-7-diamonds:before { + content: "\f2d3"; +} +.game-icon-card-7-hearts:before { + content: "\f2d4"; +} +.game-icon-card-7-spades:before { + content: "\f2d5"; +} +.game-icon-card-8-clubs:before { + content: "\f2d6"; +} +.game-icon-card-8-diamonds:before { + content: "\f2d7"; +} +.game-icon-card-8-hearts:before { + content: "\f2d8"; +} +.game-icon-card-8-spades:before { + content: "\f2d9"; +} +.game-icon-card-9-clubs:before { + content: "\f2da"; +} +.game-icon-card-9-diamonds:before { + content: "\f2db"; +} +.game-icon-card-9-hearts:before { + content: "\f2dc"; +} +.game-icon-card-9-spades:before { + content: "\f2dd"; +} +.game-icon-card-ace-clubs:before { + content: "\f2de"; +} +.game-icon-card-ace-diamonds:before { + content: "\f2df"; +} +.game-icon-card-ace-hearts:before { + content: "\f2e0"; +} +.game-icon-card-ace-spades:before { + content: "\f2e1"; +} +.game-icon-card-burn:before { + content: "\f2e2"; +} +.game-icon-card-discard:before { + content: "\f2e3"; +} +.game-icon-card-draw:before { + content: "\f2e4"; +} +.game-icon-card-exchange:before { + content: "\f2e5"; +} +.game-icon-card-jack-clubs:before { + content: "\f2e6"; +} +.game-icon-card-jack-diamonds:before { + content: "\f2e7"; +} +.game-icon-card-jack-hearts:before { + content: "\f2e8"; +} +.game-icon-card-jack-spades:before { + content: "\f2e9"; +} +.game-icon-card-joker:before { + content: "\f2ea"; +} +.game-icon-card-king-clubs:before { + content: "\f2eb"; +} +.game-icon-card-king-diamonds:before { + content: "\f2ec"; +} +.game-icon-card-king-hearts:before { + content: "\f2ed"; +} +.game-icon-card-king-spades:before { + content: "\f2ee"; +} +.game-icon-card-pick:before { + content: "\f2ef"; +} +.game-icon-card-pickup:before { + content: "\f2f0"; +} +.game-icon-card-play:before { + content: "\f2f1"; +} +.game-icon-card-queen-clubs:before { + content: "\f2f2"; +} +.game-icon-card-queen-diamonds:before { + content: "\f2f3"; +} +.game-icon-card-queen-hearts:before { + content: "\f2f4"; +} +.game-icon-card-queen-spades:before { + content: "\f2f5"; +} +.game-icon-card-random:before { + content: "\f2f6"; +} +.game-icon-cardboard-box-closed:before { + content: "\f2f7"; +} +.game-icon-cardboard-box:before { + content: "\f2f8"; +} +.game-icon-cargo-crane:before { + content: "\f2f9"; +} +.game-icon-cargo-crate:before { + content: "\f2fa"; +} +.game-icon-cargo-ship:before { + content: "\f2fb"; +} +.game-icon-carillon:before { + content: "\f2fc"; +} +.game-icon-carnival-mask:before { + content: "\f2fd"; +} +.game-icon-carnivore-mouth:before { + content: "\f2fe"; +} +.game-icon-carnivorous-plant:before { + content: "\f2ff"; +} +.game-icon-carnyx:before { + content: "\f300"; +} +.game-icon-carousel:before { + content: "\f301"; +} +.game-icon-carpet-bombing:before { + content: "\f302"; +} +.game-icon-carrier:before { + content: "\f303"; +} +.game-icon-carrion:before { + content: "\f304"; +} +.game-icon-carrot:before { + content: "\f305"; +} +.game-icon-cartwheel:before { + content: "\f306"; +} +.game-icon-cash:before { + content: "\f307"; +} +.game-icon-cassowary-head:before { + content: "\f308"; +} +.game-icon-castle-2:before { + content: "\f309"; +} +.game-icon-castle-ruins:before { + content: "\f30a"; +} +.game-icon-castle:before { + content: "\f30b"; +} +.game-icon-cat:before { + content: "\f30c"; +} +.game-icon-catapult:before { + content: "\f30d"; +} +.game-icon-catch:before { + content: "\f30e"; +} +.game-icon-caterpillar:before { + content: "\f30f"; +} +.game-icon-cauldron-2:before { + content: "\f310"; +} +.game-icon-cauldron:before { + content: "\f311"; +} +.game-icon-cavalry:before { + content: "\f312"; +} +.game-icon-cave-entrance:before { + content: "\f313"; +} +.game-icon-caveman:before { + content: "\f314"; +} +.game-icon-cctv-camera:before { + content: "\f315"; +} +.game-icon-ceiling-barnacle:before { + content: "\f316"; +} +.game-icon-ceiling-light:before { + content: "\f317"; +} +.game-icon-celebration-fire:before { + content: "\f318"; +} +.game-icon-cellar-barrels:before { + content: "\f319"; +} +.game-icon-cement-shoes:before { + content: "\f31a"; +} +.game-icon-centaur-heart:before { + content: "\f31b"; +} +.game-icon-centaur:before { + content: "\f31c"; +} +.game-icon-centipede:before { + content: "\f31d"; +} +.game-icon-centurion-helmet:before { + content: "\f31e"; +} +.game-icon-ceremonial-mask:before { + content: "\f31f"; +} +.game-icon-chain-lightning:before { + content: "\f320"; +} +.game-icon-chain-mail:before { + content: "\f321"; +} +.game-icon-chained-arrow-heads:before { + content: "\f322"; +} +.game-icon-chained-heart:before { + content: "\f323"; +} +.game-icon-chaingun:before { + content: "\f324"; +} +.game-icon-chainsaw:before { + content: "\f325"; +} +.game-icon-chakram:before { + content: "\f326"; +} +.game-icon-chalice-drops:before { + content: "\f327"; +} +.game-icon-chalk-outline-murder:before { + content: "\f328"; +} +.game-icon-chameleon-glyph:before { + content: "\f329"; +} +.game-icon-champagne-cork:before { + content: "\f32a"; +} +.game-icon-champions:before { + content: "\f32b"; +} +.game-icon-chanterelles:before { + content: "\f32c"; +} +.game-icon-character:before { + content: "\f32d"; +} +.game-icon-charcuterie:before { + content: "\f32e"; +} +.game-icon-charged-arrow:before { + content: "\f32f"; +} +.game-icon-charging-bull:before { + content: "\f330"; +} +.game-icon-charging:before { + content: "\f331"; +} +.game-icon-chariot:before { + content: "\f332"; +} +.game-icon-charm:before { + content: "\f333"; +} +.game-icon-chart:before { + content: "\f334"; +} +.game-icon-chat-bubble:before { + content: "\f335"; +} +.game-icon-check-mark:before { + content: "\f336"; +} +.game-icon-checkbox-tree:before { + content: "\f337"; +} +.game-icon-checked-shield:before { + content: "\f338"; +} +.game-icon-checkered-diamond:before { + content: "\f339"; +} +.game-icon-checkered-flag:before { + content: "\f33a"; +} +.game-icon-checklist:before { + content: "\f33b"; +} +.game-icon-cheerful:before { + content: "\f33c"; +} +.game-icon-cheese-wedge:before { + content: "\f33d"; +} +.game-icon-chef-toque:before { + content: "\f33e"; +} +.game-icon-chelsea-boot:before { + content: "\f33f"; +} +.game-icon-chemical-arrow:before { + content: "\f340"; +} +.game-icon-chemical-bolt:before { + content: "\f341"; +} +.game-icon-chemical-drop:before { + content: "\f342"; +} +.game-icon-chemical-tank:before { + content: "\f343"; +} +.game-icon-cherish:before { + content: "\f344"; +} +.game-icon-cherry:before { + content: "\f345"; +} +.game-icon-chess-bishop:before { + content: "\f346"; +} +.game-icon-chess-king:before { + content: "\f347"; +} +.game-icon-chess-knight:before { + content: "\f348"; +} +.game-icon-chess-pawn:before { + content: "\f349"; +} +.game-icon-chess-queen:before { + content: "\f34a"; +} +.game-icon-chess-rook:before { + content: "\f34b"; +} +.game-icon-chest-armor:before { + content: "\f34c"; +} +.game-icon-chest:before { + content: "\f34d"; +} +.game-icon-chestnut-leaf:before { + content: "\f34e"; +} +.game-icon-chewed-heart:before { + content: "\f34f"; +} +.game-icon-chewed-skull:before { + content: "\f350"; +} +.game-icon-chicken-leg:before { + content: "\f351"; +} +.game-icon-chicken-oven:before { + content: "\f352"; +} +.game-icon-chicken:before { + content: "\f353"; +} +.game-icon-chili-pepper:before { + content: "\f354"; +} +.game-icon-chimney:before { + content: "\f355"; +} +.game-icon-chips-bag:before { + content: "\f356"; +} +.game-icon-chisel:before { + content: "\f357"; +} +.game-icon-chocolate-bar:before { + content: "\f358"; +} +.game-icon-choice:before { + content: "\f359"; +} +.game-icon-chopped-skull:before { + content: "\f35a"; +} +.game-icon-chopsticks:before { + content: "\f35b"; +} +.game-icon-church:before { + content: "\f35c"; +} +.game-icon-cigale:before { + content: "\f35d"; +} +.game-icon-cigar:before { + content: "\f35e"; +} +.game-icon-cigarette:before { + content: "\f35f"; +} +.game-icon-circle-cage:before { + content: "\f360"; +} +.game-icon-circle-claws:before { + content: "\f361"; +} +.game-icon-circle-forest:before { + content: "\f362"; +} +.game-icon-circle-sparks:before { + content: "\f363"; +} +.game-icon-circle:before { + content: "\f364"; +} +.game-icon-circling-fish:before { + content: "\f365"; +} +.game-icon-circuitry:before { + content: "\f366"; +} +.game-icon-circular-saw:before { + content: "\f367"; +} +.game-icon-circular-sawblade:before { + content: "\f368"; +} +.game-icon-city-car:before { + content: "\f369"; +} +.game-icon-clamp:before { + content: "\f36a"; +} +.game-icon-clapperboard:before { + content: "\f36b"; +} +.game-icon-clarinet:before { + content: "\f36c"; +} +.game-icon-classical-knowledge:before { + content: "\f36d"; +} +.game-icon-claw-hammer:before { + content: "\f36e"; +} +.game-icon-claw-slashes:before { + content: "\f36f"; +} +.game-icon-claw-string:before { + content: "\f370"; +} +.game-icon-claw:before { + content: "\f371"; +} +.game-icon-claws:before { + content: "\f372"; +} +.game-icon-clay-brick:before { + content: "\f373"; +} +.game-icon-claymore-explosive:before { + content: "\f374"; +} +.game-icon-cleaver:before { + content: "\f375"; +} +.game-icon-cleopatra:before { + content: "\f376"; +} +.game-icon-click:before { + content: "\f377"; +} +.game-icon-cliff-crossing:before { + content: "\f378"; +} +.game-icon-cloak-dagger:before { + content: "\f379"; +} +.game-icon-cloak:before { + content: "\f37a"; +} +.game-icon-cloaked-figure-on-horseback:before { + content: "\f37b"; +} +.game-icon-clockwise-rotation:before { + content: "\f37c"; +} +.game-icon-clockwork:before { + content: "\f37d"; +} +.game-icon-closed-barbute:before { + content: "\f37e"; +} +.game-icon-closed-doors:before { + content: "\f37f"; +} +.game-icon-cloth-jar:before { + content: "\f380"; +} +.game-icon-clothes:before { + content: "\f381"; +} +.game-icon-clothesline:before { + content: "\f382"; +} +.game-icon-clothespin:before { + content: "\f383"; +} +.game-icon-cloud-download:before { + content: "\f384"; +} +.game-icon-cloud-ring:before { + content: "\f385"; +} +.game-icon-cloud-upload:before { + content: "\f386"; +} +.game-icon-cloudy-fork:before { + content: "\f387"; +} +.game-icon-clout:before { + content: "\f388"; +} +.game-icon-clover-2:before { + content: "\f389"; +} +.game-icon-clover-spiked:before { + content: "\f38a"; +} +.game-icon-clover:before { + content: "\f38b"; +} +.game-icon-clown:before { + content: "\f38c"; +} +.game-icon-clownfish:before { + content: "\f38d"; +} +.game-icon-clubs:before { + content: "\f38e"; +} +.game-icon-cluster-bomb:before { + content: "\f38f"; +} +.game-icon-coa-de-jima:before { + content: "\f390"; +} +.game-icon-coal-pile:before { + content: "\f391"; +} +.game-icon-coal-wagon:before { + content: "\f392"; +} +.game-icon-cobra-2:before { + content: "\f393"; +} +.game-icon-cobra:before { + content: "\f394"; +} +.game-icon-cobweb:before { + content: "\f395"; +} +.game-icon-coconuts:before { + content: "\f396"; +} +.game-icon-coffee-beans:before { + content: "\f397"; +} +.game-icon-coffee-cup:before { + content: "\f398"; +} +.game-icon-coffee-mug:before { + content: "\f399"; +} +.game-icon-coffee-pot:before { + content: "\f39a"; +} +.game-icon-coffin:before { + content: "\f39b"; +} +.game-icon-cog-lock:before { + content: "\f39c"; +} +.game-icon-cog:before { + content: "\f39d"; +} +.game-icon-cogsplosion:before { + content: "\f39e"; +} +.game-icon-coiled-nail:before { + content: "\f39f"; +} +.game-icon-coiling-curl:before { + content: "\f3a0"; +} +.game-icon-coinflip:before { + content: "\f3a1"; +} +.game-icon-coins-pile:before { + content: "\f3a2"; +} +.game-icon-coins:before { + content: "\f3a3"; +} +.game-icon-cold-heart:before { + content: "\f3a4"; +} +.game-icon-coliseum:before { + content: "\f3a5"; +} +.game-icon-colombia:before { + content: "\f3a6"; +} +.game-icon-colombian-statue:before { + content: "\f3a7"; +} +.game-icon-colt-m1911:before { + content: "\f3a8"; +} +.game-icon-column-vase:before { + content: "\f3a9"; +} +.game-icon-coma:before { + content: "\f3aa"; +} +.game-icon-comb:before { + content: "\f3ab"; +} +.game-icon-combination-lock:before { + content: "\f3ac"; +} +.game-icon-comet-spark:before { + content: "\f3ad"; +} +.game-icon-commercial-airplane:before { + content: "\f3ae"; +} +.game-icon-compact-disc:before { + content: "\f3af"; +} +.game-icon-companion-cube:before { + content: "\f3b0"; +} +.game-icon-compass:before { + content: "\f3b1"; +} +.game-icon-computer-fan:before { + content: "\f3b2"; +} +.game-icon-computing:before { + content: "\f3b3"; +} +.game-icon-concentration-orb:before { + content: "\f3b4"; +} +.game-icon-concentric-crescents:before { + content: "\f3b5"; +} +.game-icon-concrete-bag:before { + content: "\f3b6"; +} +.game-icon-condor-emblem:before { + content: "\f3b7"; +} +.game-icon-condylura-skull:before { + content: "\f3b8"; +} +.game-icon-confirmed:before { + content: "\f3b9"; +} +.game-icon-confrontation:before { + content: "\f3ba"; +} +.game-icon-congress:before { + content: "\f3bb"; +} +.game-icon-conqueror:before { + content: "\f3bc"; +} +.game-icon-console-controller:before { + content: "\f3bd"; +} +.game-icon-contortionist:before { + content: "\f3be"; +} +.game-icon-contract:before { + content: "\f3bf"; +} +.game-icon-control-tower:before { + content: "\f3c0"; +} +.game-icon-convergence-target:before { + content: "\f3c1"; +} +.game-icon-conversation:before { + content: "\f3c2"; +} +.game-icon-converse-shoe:before { + content: "\f3c3"; +} +.game-icon-convict:before { + content: "\f3c4"; +} +.game-icon-convince:before { + content: "\f3c5"; +} +.game-icon-conway-life-glider:before { + content: "\f3c6"; +} +.game-icon-cook:before { + content: "\f3c7"; +} +.game-icon-cookie:before { + content: "\f3c8"; +} +.game-icon-cooking-glove:before { + content: "\f3c9"; +} +.game-icon-cooking-pot:before { + content: "\f3ca"; +} +.game-icon-cool-spices:before { + content: "\f3cb"; +} +.game-icon-cooler:before { + content: "\f3cc"; +} +.game-icon-cootie-catcher:before { + content: "\f3cd"; +} +.game-icon-coral:before { + content: "\f3ce"; +} +.game-icon-cork-hat:before { + content: "\f3cf"; +} +.game-icon-corked-tube:before { + content: "\f3d0"; +} +.game-icon-corkscrew:before { + content: "\f3d1"; +} +.game-icon-corn:before { + content: "\f3d2"; +} +.game-icon-corner-explosion:before { + content: "\f3d3"; +} +.game-icon-corner-flag:before { + content: "\f3d4"; +} +.game-icon-cornucopia:before { + content: "\f3d5"; +} +.game-icon-coronation:before { + content: "\f3d6"; +} +.game-icon-corporal:before { + content: "\f3d7"; +} +.game-icon-corset:before { + content: "\f3d8"; +} +.game-icon-corsica:before { + content: "\f3d9"; +} +.game-icon-cosmic-egg:before { + content: "\f3da"; +} +.game-icon-cotton-flower:before { + content: "\f3db"; +} +.game-icon-covered-jar:before { + content: "\f3dc"; +} +.game-icon-cow:before { + content: "\f3dd"; +} +.game-icon-cowboy-boot:before { + content: "\f3de"; +} +.game-icon-cowboy-holster:before { + content: "\f3df"; +} +.game-icon-cowled:before { + content: "\f3e0"; +} +.game-icon-cpu-shot:before { + content: "\f3e1"; +} +.game-icon-cpu:before { + content: "\f3e2"; +} +.game-icon-crab-claw:before { + content: "\f3e3"; +} +.game-icon-crab:before { + content: "\f3e4"; +} +.game-icon-cracked-alien-skull:before { + content: "\f3e5"; +} +.game-icon-cracked-ball-dunk:before { + content: "\f3e6"; +} +.game-icon-cracked-disc:before { + content: "\f3e7"; +} +.game-icon-cracked-glass:before { + content: "\f3e8"; +} +.game-icon-cracked-helm:before { + content: "\f3e9"; +} +.game-icon-cracked-mask:before { + content: "\f3ea"; +} +.game-icon-cracked-saber:before { + content: "\f3eb"; +} +.game-icon-cracked-shield:before { + content: "\f3ec"; +} +.game-icon-crafting:before { + content: "\f3ed"; +} +.game-icon-crags:before { + content: "\f3ee"; +} +.game-icon-crane:before { + content: "\f3ef"; +} +.game-icon-credits-currency:before { + content: "\f3f0"; +} +.game-icon-crenel-crown:before { + content: "\f3f1"; +} +.game-icon-crenulated-shield:before { + content: "\f3f2"; +} +.game-icon-crescent-blade-2:before { + content: "\f3f3"; +} +.game-icon-crescent-blade:before { + content: "\f3f4"; +} +.game-icon-crescent-staff:before { + content: "\f3f5"; +} +.game-icon-crested-helmet:before { + content: "\f3f6"; +} +.game-icon-cricket-bat:before { + content: "\f3f7"; +} +.game-icon-cricket:before { + content: "\f3f8"; +} +.game-icon-crime-scene-tape:before { + content: "\f3f9"; +} +.game-icon-croc-jaws:before { + content: "\f3fa"; +} +.game-icon-croc-sword:before { + content: "\f3fb"; +} +.game-icon-croissant:before { + content: "\f3fc"; +} +.game-icon-croissants-pupil:before { + content: "\f3fd"; +} +.game-icon-crook-flail:before { + content: "\f3fe"; +} +.game-icon-cross-flare:before { + content: "\f3ff"; +} +.game-icon-cross-mark:before { + content: "\f400"; +} +.game-icon-cross-shield:before { + content: "\f401"; +} +.game-icon-crossbow:before { + content: "\f402"; +} +.game-icon-crosscut-saw:before { + content: "\f403"; +} +.game-icon-crossed-air-flows:before { + content: "\f404"; +} +.game-icon-crossed-axes:before { + content: "\f405"; +} +.game-icon-crossed-bones:before { + content: "\f406"; +} +.game-icon-crossed-chains:before { + content: "\f407"; +} +.game-icon-crossed-claws:before { + content: "\f408"; +} +.game-icon-crossed-pistols:before { + content: "\f409"; +} +.game-icon-crossed-sabres:before { + content: "\f40a"; +} +.game-icon-crossed-slashes:before { + content: "\f40b"; +} +.game-icon-crossed-swords:before { + content: "\f40c"; +} +.game-icon-crosshair-arrow:before { + content: "\f40d"; +} +.game-icon-crosshair:before { + content: "\f40e"; +} +.game-icon-crossroad:before { + content: "\f40f"; +} +.game-icon-crow-dive:before { + content: "\f410"; +} +.game-icon-crow-nest:before { + content: "\f411"; +} +.game-icon-crowbar:before { + content: "\f412"; +} +.game-icon-crown-coin:before { + content: "\f413"; +} +.game-icon-crown-of-thorns:before { + content: "\f414"; +} +.game-icon-crown:before { + content: "\f415"; +} +.game-icon-crowned-explosion:before { + content: "\f416"; +} +.game-icon-crowned-heart:before { + content: "\f417"; +} +.game-icon-crowned-skull:before { + content: "\f418"; +} +.game-icon-crucifix:before { + content: "\f419"; +} +.game-icon-cruiser:before { + content: "\f41a"; +} +.game-icon-crumbling-ball:before { + content: "\f41b"; +} +.game-icon-crush:before { + content: "\f41c"; +} +.game-icon-cryo-chamber:before { + content: "\f41d"; +} +.game-icon-crypt-entrance:before { + content: "\f41e"; +} +.game-icon-crystal-ball:before { + content: "\f41f"; +} +.game-icon-crystal-bars:before { + content: "\f420"; +} +.game-icon-crystal-cluster:before { + content: "\f421"; +} +.game-icon-crystal-earrings:before { + content: "\f422"; +} +.game-icon-crystal-eye:before { + content: "\f423"; +} +.game-icon-crystal-growth:before { + content: "\f424"; +} +.game-icon-crystal-shine:before { + content: "\f425"; +} +.game-icon-crystal-shrine:before { + content: "\f426"; +} +.game-icon-crystal-wand:before { + content: "\f427"; +} +.game-icon-crystalize:before { + content: "\f428"; +} +.game-icon-cuauhtli:before { + content: "\f429"; +} +.game-icon-cube:before { + content: "\f42a"; +} +.game-icon-cubeforce:before { + content: "\f42b"; +} +.game-icon-cubes:before { + content: "\f42c"; +} +.game-icon-cuckoo-clock:before { + content: "\f42d"; +} +.game-icon-cultist-2:before { + content: "\f42e"; +} +.game-icon-cultist:before { + content: "\f42f"; +} +.game-icon-cupcake:before { + content: "\f430"; +} +.game-icon-cupidon-arrow:before { + content: "\f431"; +} +.game-icon-curled-leaf:before { + content: "\f432"; +} +.game-icon-curled-tentacle:before { + content: "\f433"; +} +.game-icon-curling-stone:before { + content: "\f434"; +} +.game-icon-curling-vines:before { + content: "\f435"; +} +.game-icon-curly-mask:before { + content: "\f436"; +} +.game-icon-curly-wing:before { + content: "\f437"; +} +.game-icon-cursed-star:before { + content: "\f438"; +} +.game-icon-curvy-knife:before { + content: "\f439"; +} +.game-icon-custodian-helmet:before { + content: "\f43a"; +} +.game-icon-cut-diamond:before { + content: "\f43b"; +} +.game-icon-cut-lemon:before { + content: "\f43c"; +} +.game-icon-cut-palm:before { + content: "\f43d"; +} +.game-icon-cyber-eye:before { + content: "\f43e"; +} +.game-icon-cyborg-face:before { + content: "\f43f"; +} +.game-icon-cycle:before { + content: "\f440"; +} +.game-icon-cycling:before { + content: "\f441"; +} +.game-icon-cyclops:before { + content: "\f442"; +} +.game-icon-cz-skorpion:before { + content: "\f443"; +} +.game-icon-d10:before { + content: "\f444"; +} +.game-icon-d12:before { + content: "\f445"; +} +.game-icon-d4:before { + content: "\f446"; +} +.game-icon-daemon-pull:before { + content: "\f447"; +} +.game-icon-daemon-skull:before { + content: "\f448"; +} +.game-icon-dagger-rose:before { + content: "\f449"; +} +.game-icon-daggers:before { + content: "\f44a"; +} +.game-icon-daisy:before { + content: "\f44b"; +} +.game-icon-dam:before { + content: "\f44c"; +} +.game-icon-damaged-house:before { + content: "\f44d"; +} +.game-icon-dandelion-flower:before { + content: "\f44e"; +} +.game-icon-dango:before { + content: "\f44f"; +} +.game-icon-dark-squad:before { + content: "\f450"; +} +.game-icon-dart:before { + content: "\f451"; +} +.game-icon-database:before { + content: "\f452"; +} +.game-icon-dead-eye:before { + content: "\f453"; +} +.game-icon-dead-head:before { + content: "\f454"; +} +.game-icon-dead-wood:before { + content: "\f455"; +} +.game-icon-deadly-strike:before { + content: "\f456"; +} +.game-icon-death-juice:before { + content: "\f457"; +} +.game-icon-death-note:before { + content: "\f458"; +} +.game-icon-death-skull:before { + content: "\f459"; +} +.game-icon-death-star:before { + content: "\f45a"; +} +.game-icon-death-zone:before { + content: "\f45b"; +} +.game-icon-deathcab:before { + content: "\f45c"; +} +.game-icon-decapitation:before { + content: "\f45d"; +} +.game-icon-deer-head:before { + content: "\f45e"; +} +.game-icon-deer-track:before { + content: "\f45f"; +} +.game-icon-deer:before { + content: "\f460"; +} +.game-icon-defense-satellite:before { + content: "\f461"; +} +.game-icon-defensive-wall:before { + content: "\f462"; +} +.game-icon-defibrilate:before { + content: "\f463"; +} +.game-icon-deku-tree:before { + content: "\f464"; +} +.game-icon-delicate-perfume:before { + content: "\f465"; +} +.game-icon-delighted:before { + content: "\f466"; +} +.game-icon-delivery-drone:before { + content: "\f467"; +} +.game-icon-demolish:before { + content: "\f468"; +} +.game-icon-dervish-swords:before { + content: "\f469"; +} +.game-icon-desert-camp:before { + content: "\f46a"; +} +.game-icon-desert-eagle:before { + content: "\f46b"; +} +.game-icon-desert-skull:before { + content: "\f46c"; +} +.game-icon-desert:before { + content: "\f46d"; +} +.game-icon-deshret-red-crown:before { + content: "\f46e"; +} +.game-icon-desk-lamp:before { + content: "\f46f"; +} +.game-icon-desk:before { + content: "\f470"; +} +.game-icon-despair:before { + content: "\f471"; +} +.game-icon-detonator:before { + content: "\f472"; +} +.game-icon-detour:before { + content: "\f473"; +} +.game-icon-devil-mask:before { + content: "\f474"; +} +.game-icon-dew:before { + content: "\f475"; +} +.game-icon-diablo-skull:before { + content: "\f476"; +} +.game-icon-diagram:before { + content: "\f477"; +} +.game-icon-dial-padlock:before { + content: "\f478"; +} +.game-icon-diamond-hard:before { + content: "\f479"; +} +.game-icon-diamond-hilt:before { + content: "\f47a"; +} +.game-icon-diamond-ring:before { + content: "\f47b"; +} +.game-icon-diamond-trophy:before { + content: "\f47c"; +} +.game-icon-diamonds-smile:before { + content: "\f47d"; +} +.game-icon-diamonds:before { + content: "\f47e"; +} +.game-icon-dice-eight-faces-eight:before { + content: "\f47f"; +} +.game-icon-dice-fire:before { + content: "\f480"; +} +.game-icon-dice-shield:before { + content: "\f481"; +} +.game-icon-dice-six-faces-five:before { + content: "\f482"; +} +.game-icon-dice-six-faces-four:before { + content: "\f483"; +} +.game-icon-dice-six-faces-one:before { + content: "\f484"; +} +.game-icon-dice-six-faces-six:before { + content: "\f485"; +} +.game-icon-dice-six-faces-three:before { + content: "\f486"; +} +.game-icon-dice-six-faces-two:before { + content: "\f487"; +} +.game-icon-dice-target:before { + content: "\f488"; +} +.game-icon-dice-twenty-faces-one:before { + content: "\f489"; +} +.game-icon-dice-twenty-faces-twenty:before { + content: "\f48a"; +} +.game-icon-dig-dug:before { + content: "\f48b"; +} +.game-icon-dig-hole:before { + content: "\f48c"; +} +.game-icon-digital-trace:before { + content: "\f48d"; +} +.game-icon-dimetrodon:before { + content: "\f48e"; +} +.game-icon-dinosaur-bones:before { + content: "\f48f"; +} +.game-icon-dinosaur-egg:before { + content: "\f490"; +} +.game-icon-dinosaur-rex:before { + content: "\f491"; +} +.game-icon-diplodocus:before { + content: "\f492"; +} +.game-icon-diploma:before { + content: "\f493"; +} +.game-icon-direction-sign:before { + content: "\f494"; +} +.game-icon-direction-signs:before { + content: "\f495"; +} +.game-icon-director-chair:before { + content: "\f496"; +} +.game-icon-direwolf:before { + content: "\f497"; +} +.game-icon-disc-golf-bag:before { + content: "\f498"; +} +.game-icon-disc-golf-basket:before { + content: "\f499"; +} +.game-icon-disc:before { + content: "\f49a"; +} +.game-icon-discobolus:before { + content: "\f49b"; +} +.game-icon-discussion:before { + content: "\f49c"; +} +.game-icon-disintegrate:before { + content: "\f49d"; +} +.game-icon-distraction-2:before { + content: "\f49e"; +} +.game-icon-distraction:before { + content: "\f49f"; +} +.game-icon-distress-signal:before { + content: "\f4a0"; +} +.game-icon-divergence:before { + content: "\f4a1"; +} +.game-icon-divert:before { + content: "\f4a2"; +} +.game-icon-divided-spiral:before { + content: "\f4a3"; +} +.game-icon-divided-square:before { + content: "\f4a4"; +} +.game-icon-diving-dagger:before { + content: "\f4a5"; +} +.game-icon-diving-helmet:before { + content: "\f4a6"; +} +.game-icon-djed-pillar:before { + content: "\f4a7"; +} +.game-icon-djembe:before { + content: "\f4a8"; +} +.game-icon-djinn:before { + content: "\f4a9"; +} +.game-icon-dna1:before { + content: "\f4aa"; +} +.game-icon-dna2:before { + content: "\f4ab"; +} +.game-icon-doctor-face:before { + content: "\f4ac"; +} +.game-icon-dodge:before { + content: "\f4ad"; +} +.game-icon-dodging:before { + content: "\f4ae"; +} +.game-icon-dog-bowl:before { + content: "\f4af"; +} +.game-icon-dog-house:before { + content: "\f4b0"; +} +.game-icon-dolmen:before { + content: "\f4b1"; +} +.game-icon-dolphin:before { + content: "\f4b2"; +} +.game-icon-domino-mask:before { + content: "\f4b3"; +} +.game-icon-domino-tiles:before { + content: "\f4b4"; +} +.game-icon-doner-kebab:before { + content: "\f4b5"; +} +.game-icon-donkey:before { + content: "\f4b6"; +} +.game-icon-donut:before { + content: "\f4b7"; +} +.game-icon-door-handle:before { + content: "\f4b8"; +} +.game-icon-door-ring-handle:before { + content: "\f4b9"; +} +.game-icon-door-watcher:before { + content: "\f4ba"; +} +.game-icon-door:before { + content: "\f4bb"; +} +.game-icon-doorway:before { + content: "\f4bc"; +} +.game-icon-dorsal-scales:before { + content: "\f4bd"; +} +.game-icon-double-diaphragm:before { + content: "\f4be"; +} +.game-icon-double-dragon:before { + content: "\f4bf"; +} +.game-icon-double-face-mask:before { + content: "\f4c0"; +} +.game-icon-double-fish:before { + content: "\f4c1"; +} +.game-icon-double-necklace:before { + content: "\f4c2"; +} +.game-icon-double-quaver:before { + content: "\f4c3"; +} +.game-icon-double-ringed-orb:before { + content: "\f4c4"; +} +.game-icon-double-shot:before { + content: "\f4c5"; +} +.game-icon-double-street-lights:before { + content: "\f4c6"; +} +.game-icon-doubled:before { + content: "\f4c7"; +} +.game-icon-dough-roller:before { + content: "\f4c8"; +} +.game-icon-dove:before { + content: "\f4c9"; +} +.game-icon-dozen:before { + content: "\f4ca"; +} +.game-icon-dragon-balls:before { + content: "\f4cb"; +} +.game-icon-dragon-breath:before { + content: "\f4cc"; +} +.game-icon-dragon-head-2:before { + content: "\f4cd"; +} +.game-icon-dragon-head:before { + content: "\f4ce"; +} +.game-icon-dragon-orb:before { + content: "\f4cf"; +} +.game-icon-dragon-shield:before { + content: "\f4d0"; +} +.game-icon-dragon-spiral:before { + content: "\f4d1"; +} +.game-icon-dragonfly:before { + content: "\f4d2"; +} +.game-icon-drakkar-dragon:before { + content: "\f4d3"; +} +.game-icon-drakkar:before { + content: "\f4d4"; +} +.game-icon-drama-masks:before { + content: "\f4d5"; +} +.game-icon-drawbridge:before { + content: "\f4d6"; +} +.game-icon-dread-skull:before { + content: "\f4d7"; +} +.game-icon-dread:before { + content: "\f4d8"; +} +.game-icon-dreadnought:before { + content: "\f4d9"; +} +.game-icon-dream-catcher:before { + content: "\f4da"; +} +.game-icon-dress:before { + content: "\f4db"; +} +.game-icon-drill-2:before { + content: "\f4dc"; +} +.game-icon-drill:before { + content: "\f4dd"; +} +.game-icon-drink-me:before { + content: "\f4de"; +} +.game-icon-drinking:before { + content: "\f4df"; +} +.game-icon-dripping-blade:before { + content: "\f4e0"; +} +.game-icon-dripping-goo:before { + content: "\f4e1"; +} +.game-icon-dripping-honey:before { + content: "\f4e2"; +} +.game-icon-dripping-knife:before { + content: "\f4e3"; +} +.game-icon-dripping-star:before { + content: "\f4e4"; +} +.game-icon-dripping-stone:before { + content: "\f4e5"; +} +.game-icon-dripping-sword:before { + content: "\f4e6"; +} +.game-icon-dripping-tube:before { + content: "\f4e7"; +} +.game-icon-drop-earrings:before { + content: "\f4e8"; +} +.game-icon-drop-weapon:before { + content: "\f4e9"; +} +.game-icon-drop:before { + content: "\f4ea"; +} +.game-icon-droplet-splash:before { + content: "\f4eb"; +} +.game-icon-droplets:before { + content: "\f4ec"; +} +.game-icon-drowning:before { + content: "\f4ed"; +} +.game-icon-drum-kit:before { + content: "\f4ee"; +} +.game-icon-drum:before { + content: "\f4ef"; +} +.game-icon-duality-mask:before { + content: "\f4f0"; +} +.game-icon-duality:before { + content: "\f4f1"; +} +.game-icon-duck-palm:before { + content: "\f4f2"; +} +.game-icon-duck:before { + content: "\f4f3"; +} +.game-icon-duel:before { + content: "\f4f4"; +} +.game-icon-duffel-bag:before { + content: "\f4f5"; +} +.game-icon-dumpling-bao:before { + content: "\f4f6"; +} +.game-icon-dumpling:before { + content: "\f4f7"; +} +.game-icon-dunce-cap:before { + content: "\f4f8"; +} +.game-icon-dungeon-gate:before { + content: "\f4f9"; +} +.game-icon-dungeon-light:before { + content: "\f4fa"; +} +.game-icon-duration:before { + content: "\f4fb"; +} +.game-icon-dust-cloud:before { + content: "\f4fc"; +} +.game-icon-dutch-bike:before { + content: "\f4fd"; +} +.game-icon-dwarf-face:before { + content: "\f4fe"; +} +.game-icon-dwarf-helmet:before { + content: "\f4ff"; +} +.game-icon-dwarf-king:before { + content: "\f500"; +} +.game-icon-dwennimmen:before { + content: "\f501"; +} +.game-icon-dynamite:before { + content: "\f502"; +} +.game-icon-eagle-emblem:before { + content: "\f503"; +} +.game-icon-eagle-head:before { + content: "\f504"; +} +.game-icon-earbuds:before { + content: "\f505"; +} +.game-icon-earrings:before { + content: "\f506"; +} +.game-icon-earth-africa-europe:before { + content: "\f507"; +} +.game-icon-earth-america:before { + content: "\f508"; +} +.game-icon-earth-asia-oceania:before { + content: "\f509"; +} +.game-icon-earth-crack:before { + content: "\f50a"; +} +.game-icon-earth-spit:before { + content: "\f50b"; +} +.game-icon-earth-worm:before { + content: "\f50c"; +} +.game-icon-earwig:before { + content: "\f50d"; +} +.game-icon-easel:before { + content: "\f50e"; +} +.game-icon-easter-egg:before { + content: "\f50f"; +} +.game-icon-eating-pelican:before { + content: "\f510"; +} +.game-icon-eating:before { + content: "\f511"; +} +.game-icon-echo-ripples:before { + content: "\f512"; +} +.game-icon-eclipse-flare:before { + content: "\f513"; +} +.game-icon-eclipse-saw:before { + content: "\f514"; +} +.game-icon-eclipse:before { + content: "\f515"; +} +.game-icon-ecology:before { + content: "\f516"; +} +.game-icon-edge-crack:before { + content: "\f517"; +} +.game-icon-edged-shield:before { + content: "\f518"; +} +.game-icon-eel:before { + content: "\f519"; +} +.game-icon-egg-clutch:before { + content: "\f51a"; +} +.game-icon-egg-defense:before { + content: "\f51b"; +} +.game-icon-egg-eye:before { + content: "\f51c"; +} +.game-icon-egg-pod:before { + content: "\f51d"; +} +.game-icon-egypt:before { + content: "\f51e"; +} +.game-icon-egyptian-bird:before { + content: "\f51f"; +} +.game-icon-egyptian-profile:before { + content: "\f520"; +} +.game-icon-egyptian-pyramids:before { + content: "\f521"; +} +.game-icon-egyptian-sphinx:before { + content: "\f522"; +} +.game-icon-egyptian-temple:before { + content: "\f523"; +} +.game-icon-egyptian-urns:before { + content: "\f524"; +} +.game-icon-egyptian-walk:before { + content: "\f525"; +} +.game-icon-eight-ball:before { + content: "\f526"; +} +.game-icon-elbow-pad:before { + content: "\f527"; +} +.game-icon-elderberry:before { + content: "\f528"; +} +.game-icon-electric-whip:before { + content: "\f529"; +} +.game-icon-electric:before { + content: "\f52a"; +} +.game-icon-electrical-crescent:before { + content: "\f52b"; +} +.game-icon-electrical-resistance:before { + content: "\f52c"; +} +.game-icon-electrical-socket:before { + content: "\f52d"; +} +.game-icon-elephant-head:before { + content: "\f52e"; +} +.game-icon-elephant:before { + content: "\f52f"; +} +.game-icon-elevator:before { + content: "\f530"; +} +.game-icon-elf-ear:before { + content: "\f531"; +} +.game-icon-elf-helmet:before { + content: "\f532"; +} +.game-icon-elven-castle:before { + content: "\f533"; +} +.game-icon-elysium-shade:before { + content: "\f534"; +} +.game-icon-ember-shot:before { + content: "\f535"; +} +.game-icon-embrassed-energy:before { + content: "\f536"; +} +.game-icon-embryo:before { + content: "\f537"; +} +.game-icon-emerald-necklace:before { + content: "\f538"; +} +.game-icon-emerald:before { + content: "\f539"; +} +.game-icon-empty-chessboard:before { + content: "\f53a"; +} +.game-icon-empty-hourglass:before { + content: "\f53b"; +} +.game-icon-empty-metal-bucket-handle:before { + content: "\f53c"; +} +.game-icon-empty-metal-bucket:before { + content: "\f53d"; +} +.game-icon-empty-wood-bucket-handle:before { + content: "\f53e"; +} +.game-icon-empty-wood-bucket:before { + content: "\f53f"; +} +.game-icon-encirclement:before { + content: "\f540"; +} +.game-icon-energise:before { + content: "\f541"; +} +.game-icon-energy-arrow:before { + content: "\f542"; +} +.game-icon-energy-breath:before { + content: "\f543"; +} +.game-icon-energy-shield:before { + content: "\f544"; +} +.game-icon-energy-sword:before { + content: "\f545"; +} +.game-icon-energy-tank:before { + content: "\f546"; +} +.game-icon-engagement-ring:before { + content: "\f547"; +} +.game-icon-enlightenment:before { + content: "\f548"; +} +.game-icon-enrage:before { + content: "\f549"; +} +.game-icon-ent-mouth:before { + content: "\f54a"; +} +.game-icon-entangled-typhoon:before { + content: "\f54b"; +} +.game-icon-entry-door:before { + content: "\f54c"; +} +.game-icon-envelope:before { + content: "\f54d"; +} +.game-icon-erlenmeyer:before { + content: "\f54e"; +} +.game-icon-ermine:before { + content: "\f54f"; +} +.game-icon-eruption:before { + content: "\f550"; +} +.game-icon-escalator:before { + content: "\f551"; +} +.game-icon-eskimo:before { + content: "\f552"; +} +.game-icon-eternal-love:before { + content: "\f553"; +} +.game-icon-european-flag:before { + content: "\f554"; +} +.game-icon-evasion:before { + content: "\f555"; +} +.game-icon-evil-bat:before { + content: "\f556"; +} +.game-icon-evil-book:before { + content: "\f557"; +} +.game-icon-evil-bud:before { + content: "\f558"; +} +.game-icon-evil-comet:before { + content: "\f559"; +} +.game-icon-evil-eyes:before { + content: "\f55a"; +} +.game-icon-evil-fork:before { + content: "\f55b"; +} +.game-icon-evil-hand:before { + content: "\f55c"; +} +.game-icon-evil-love:before { + content: "\f55d"; +} +.game-icon-evil-minion:before { + content: "\f55e"; +} +.game-icon-evil-moon:before { + content: "\f55f"; +} +.game-icon-evil-tower:before { + content: "\f560"; +} +.game-icon-evil-tree:before { + content: "\f561"; +} +.game-icon-evil-wings:before { + content: "\f562"; +} +.game-icon-executioner-hood:before { + content: "\f563"; +} +.game-icon-exit-door:before { + content: "\f564"; +} +.game-icon-expand:before { + content: "\f565"; +} +.game-icon-expanded-rays:before { + content: "\f566"; +} +.game-icon-expander:before { + content: "\f567"; +} +.game-icon-expense:before { + content: "\f568"; +} +.game-icon-exploding-planet:before { + content: "\f569"; +} +.game-icon-explosion-rays:before { + content: "\f56a"; +} +.game-icon-explosive-materials:before { + content: "\f56b"; +} +.game-icon-explosive-meeting:before { + content: "\f56c"; +} +.game-icon-extra-lucid:before { + content: "\f56d"; +} +.game-icon-extra-time:before { + content: "\f56e"; +} +.game-icon-extraction-orb:before { + content: "\f56f"; +} +.game-icon-eye-of-horus:before { + content: "\f570"; +} +.game-icon-eye-shield:before { + content: "\f571"; +} +.game-icon-eye-target:before { + content: "\f572"; +} +.game-icon-eyeball:before { + content: "\f573"; +} +.game-icon-eyedropper:before { + content: "\f574"; +} +.game-icon-eyelashes:before { + content: "\f575"; +} +.game-icon-eyepatch:before { + content: "\f576"; +} +.game-icon-eyestalk:before { + content: "\f577"; +} +.game-icon-f-clef:before { + content: "\f578"; +} +.game-icon-f1-car:before { + content: "\f579"; +} +.game-icon-face-to-face:before { + content: "\f57a"; +} +.game-icon-factory-arm:before { + content: "\f57b"; +} +.game-icon-factory:before { + content: "\f57c"; +} +.game-icon-fairy-2:before { + content: "\f57d"; +} +.game-icon-fairy-wand:before { + content: "\f57e"; +} +.game-icon-fairy-wings:before { + content: "\f57f"; +} +.game-icon-fairy:before { + content: "\f580"; +} +.game-icon-falcon-moon:before { + content: "\f581"; +} +.game-icon-fall-down:before { + content: "\f582"; +} +.game-icon-falling-blob:before { + content: "\f583"; +} +.game-icon-falling-bomb:before { + content: "\f584"; +} +.game-icon-falling-boulder:before { + content: "\f585"; +} +.game-icon-falling-eye:before { + content: "\f586"; +} +.game-icon-falling-leaf:before { + content: "\f587"; +} +.game-icon-falling-ovoid:before { + content: "\f588"; +} +.game-icon-falling-rocks:before { + content: "\f589"; +} +.game-icon-falling-star:before { + content: "\f58a"; +} +.game-icon-falling:before { + content: "\f58b"; +} +.game-icon-fallout-shelter:before { + content: "\f58c"; +} +.game-icon-famas:before { + content: "\f58d"; +} +.game-icon-family-house:before { + content: "\f58e"; +} +.game-icon-family-tree:before { + content: "\f58f"; +} +.game-icon-fanged-skull:before { + content: "\f590"; +} +.game-icon-fangs-circle:before { + content: "\f591"; +} +.game-icon-fangs:before { + content: "\f592"; +} +.game-icon-farm-tractor:before { + content: "\f593"; +} +.game-icon-farmer:before { + content: "\f594"; +} +.game-icon-fast-arrow:before { + content: "\f595"; +} +.game-icon-fast-backward-button:before { + content: "\f596"; +} +.game-icon-fast-forward-button:before { + content: "\f597"; +} +.game-icon-fast-noodles:before { + content: "\f598"; +} +.game-icon-fat:before { + content: "\f599"; +} +.game-icon-feather-necklace:before { + content: "\f59a"; +} +.game-icon-feather-wound:before { + content: "\f59b"; +} +.game-icon-feather:before { + content: "\f59c"; +} +.game-icon-feathered-wing:before { + content: "\f59d"; +} +.game-icon-fedora:before { + content: "\f59e"; +} +.game-icon-feline:before { + content: "\f59f"; +} +.game-icon-female-legs:before { + content: "\f5a0"; +} +.game-icon-female-vampire:before { + content: "\f5a1"; +} +.game-icon-female:before { + content: "\f5a2"; +} +.game-icon-fencer:before { + content: "\f5a3"; +} +.game-icon-fern:before { + content: "\f5a4"; +} +.game-icon-fertilizer-bag:before { + content: "\f5a5"; +} +.game-icon-fetus:before { + content: "\f5a6"; +} +.game-icon-fez:before { + content: "\f5a7"; +} +.game-icon-field-gun:before { + content: "\f5a8"; +} +.game-icon-field:before { + content: "\f5a9"; +} +.game-icon-figurehead:before { + content: "\f5aa"; +} +.game-icon-files:before { + content: "\f5ab"; +} +.game-icon-film-projector:before { + content: "\f5ac"; +} +.game-icon-film-spool:before { + content: "\f5ad"; +} +.game-icon-film-strip:before { + content: "\f5ae"; +} +.game-icon-finch:before { + content: "\f5af"; +} +.game-icon-finger-print:before { + content: "\f5b0"; +} +.game-icon-fingernail:before { + content: "\f5b1"; +} +.game-icon-fingers-crossed:before { + content: "\f5b2"; +} +.game-icon-finish-line:before { + content: "\f5b3"; +} +.game-icon-fire-ace:before { + content: "\f5b4"; +} +.game-icon-fire-axe:before { + content: "\f5b5"; +} +.game-icon-fire-bomb:before { + content: "\f5b6"; +} +.game-icon-fire-bottle:before { + content: "\f5b7"; +} +.game-icon-fire-bowl:before { + content: "\f5b8"; +} +.game-icon-fire-breath:before { + content: "\f5b9"; +} +.game-icon-fire-dash:before { + content: "\f5ba"; +} +.game-icon-fire-extinguisher:before { + content: "\f5bb"; +} +.game-icon-fire-flower:before { + content: "\f5bc"; +} +.game-icon-fire-gem:before { + content: "\f5bd"; +} +.game-icon-fire-punch:before { + content: "\f5be"; +} +.game-icon-fire-ray:before { + content: "\f5bf"; +} +.game-icon-fire-ring:before { + content: "\f5c0"; +} +.game-icon-fire-shield:before { + content: "\f5c1"; +} +.game-icon-fire-shrine:before { + content: "\f5c2"; +} +.game-icon-fire-silhouette:before { + content: "\f5c3"; +} +.game-icon-fire-spell-cast:before { + content: "\f5c4"; +} +.game-icon-fire-tail:before { + content: "\f5c5"; +} +.game-icon-fire-wave:before { + content: "\f5c6"; +} +.game-icon-fire-zone:before { + content: "\f5c7"; +} +.game-icon-fire:before { + content: "\f5c8"; +} +.game-icon-fireball:before { + content: "\f5c9"; +} +.game-icon-fireflake:before { + content: "\f5ca"; +} +.game-icon-fireplace:before { + content: "\f5cb"; +} +.game-icon-firewall:before { + content: "\f5cc"; +} +.game-icon-firework-rocket:before { + content: "\f5cd"; +} +.game-icon-first-aid-kit:before { + content: "\f5ce"; +} +.game-icon-fish-bucket:before { + content: "\f5cf"; +} +.game-icon-fish-cooked:before { + content: "\f5d0"; +} +.game-icon-fish-corpse:before { + content: "\f5d1"; +} +.game-icon-fish-eggs:before { + content: "\f5d2"; +} +.game-icon-fish-escape:before { + content: "\f5d3"; +} +.game-icon-fish-monster:before { + content: "\f5d4"; +} +.game-icon-fish-scales:before { + content: "\f5d5"; +} +.game-icon-fish-smoking:before { + content: "\f5d6"; +} +.game-icon-fishbone:before { + content: "\f5d7"; +} +.game-icon-fishhook-fork:before { + content: "\f5d8"; +} +.game-icon-fishing-boat:before { + content: "\f5d9"; +} +.game-icon-fishing-hook:before { + content: "\f5da"; +} +.game-icon-fishing-jig:before { + content: "\f5db"; +} +.game-icon-fishing-lure:before { + content: "\f5dc"; +} +.game-icon-fishing-net:before { + content: "\f5dd"; +} +.game-icon-fishing-pole:before { + content: "\f5de"; +} +.game-icon-fishing-spoon:before { + content: "\f5df"; +} +.game-icon-fishing:before { + content: "\f5e0"; +} +.game-icon-fission:before { + content: "\f5e1"; +} +.game-icon-fist-2:before { + content: "\f5e2"; +} +.game-icon-fist:before { + content: "\f5e3"; +} +.game-icon-fizzing-flask:before { + content: "\f5e4"; +} +.game-icon-flag-objective:before { + content: "\f5e5"; +} +.game-icon-flail:before { + content: "\f5e6"; +} +.game-icon-flake:before { + content: "\f5e7"; +} +.game-icon-flame-claws:before { + content: "\f5e8"; +} +.game-icon-flame-spin:before { + content: "\f5e9"; +} +.game-icon-flame-tunnel:before { + content: "\f5ea"; +} +.game-icon-flame:before { + content: "\f5eb"; +} +.game-icon-flamed-leaf:before { + content: "\f5ec"; +} +.game-icon-flamer:before { + content: "\f5ed"; +} +.game-icon-flamethrower-soldier:before { + content: "\f5ee"; +} +.game-icon-flamethrower:before { + content: "\f5ef"; +} +.game-icon-flaming-arrow:before { + content: "\f5f0"; +} +.game-icon-flaming-claw:before { + content: "\f5f1"; +} +.game-icon-flaming-sheet:before { + content: "\f5f2"; +} +.game-icon-flaming-trident:before { + content: "\f5f3"; +} +.game-icon-flamingo:before { + content: "\f5f4"; +} +.game-icon-flanged-mace:before { + content: "\f5f5"; +} +.game-icon-flash-grenade:before { + content: "\f5f6"; +} +.game-icon-flashlight:before { + content: "\f5f7"; +} +.game-icon-flat-hammer:before { + content: "\f5f8"; +} +.game-icon-flat-paw-print:before { + content: "\f5f9"; +} +.game-icon-flat-platform:before { + content: "\f5fa"; +} +.game-icon-flat-star:before { + content: "\f5fb"; +} +.game-icon-flat-tire:before { + content: "\f5fc"; +} +.game-icon-flatbed-covered:before { + content: "\f5fd"; +} +.game-icon-flatbed:before { + content: "\f5fe"; +} +.game-icon-flatfish:before { + content: "\f5ff"; +} +.game-icon-flax:before { + content: "\f600"; +} +.game-icon-fleshy-mass:before { + content: "\f601"; +} +.game-icon-fleur-de-lys:before { + content: "\f602"; +} +.game-icon-flexible-lamp:before { + content: "\f603"; +} +.game-icon-flexible-star:before { + content: "\f604"; +} +.game-icon-flint-spark:before { + content: "\f605"; +} +.game-icon-flip-flops:before { + content: "\f606"; +} +.game-icon-floating-crystal:before { + content: "\f607"; +} +.game-icon-floating-ghost:before { + content: "\f608"; +} +.game-icon-floating-platforms:before { + content: "\f609"; +} +.game-icon-floating-tentacles:before { + content: "\f60a"; +} +.game-icon-flood:before { + content: "\f60b"; +} +.game-icon-floor-hatch:before { + content: "\f60c"; +} +.game-icon-floor-polisher:before { + content: "\f60d"; +} +.game-icon-flour:before { + content: "\f60e"; +} +.game-icon-flower-emblem:before { + content: "\f60f"; +} +.game-icon-flower-hat:before { + content: "\f610"; +} +.game-icon-flower-pot:before { + content: "\f611"; +} +.game-icon-flower-star:before { + content: "\f612"; +} +.game-icon-flower-twirl:before { + content: "\f613"; +} +.game-icon-flowers:before { + content: "\f614"; +} +.game-icon-fluffy-cloud:before { + content: "\f615"; +} +.game-icon-fluffy-flame:before { + content: "\f616"; +} +.game-icon-fluffy-swirl:before { + content: "\f617"; +} +.game-icon-fluffy-trefoil:before { + content: "\f618"; +} +.game-icon-fluffy-wing:before { + content: "\f619"; +} +.game-icon-flute:before { + content: "\f61a"; +} +.game-icon-fly:before { + content: "\f61b"; +} +.game-icon-flying-beetle:before { + content: "\f61c"; +} +.game-icon-flying-dagger:before { + content: "\f61d"; +} +.game-icon-flying-flag:before { + content: "\f61e"; +} +.game-icon-flying-fox:before { + content: "\f61f"; +} +.game-icon-flying-shuriken:before { + content: "\f620"; +} +.game-icon-flying-target:before { + content: "\f621"; +} +.game-icon-flying-trout:before { + content: "\f622"; +} +.game-icon-fn-fal:before { + content: "\f623"; +} +.game-icon-foam:before { + content: "\f624"; +} +.game-icon-foamy-disc:before { + content: "\f625"; +} +.game-icon-focused-lightning:before { + content: "\f626"; +} +.game-icon-fog-light:before { + content: "\f627"; +} +.game-icon-fog:before { + content: "\f628"; +} +.game-icon-folded-paper:before { + content: "\f629"; +} +.game-icon-fomorian:before { + content: "\f62a"; +} +.game-icon-food-chain:before { + content: "\f62b"; +} +.game-icon-food-truck:before { + content: "\f62c"; +} +.game-icon-foot-plaster:before { + content: "\f62d"; +} +.game-icon-foot-trip:before { + content: "\f62e"; +} +.game-icon-footprint:before { + content: "\f62f"; +} +.game-icon-footsteps:before { + content: "\f630"; +} +.game-icon-footy-field:before { + content: "\f631"; +} +.game-icon-forearm:before { + content: "\f632"; +} +.game-icon-forest-camp:before { + content: "\f633"; +} +.game-icon-forest-entrance:before { + content: "\f634"; +} +.game-icon-forest:before { + content: "\f635"; +} +.game-icon-fork-knife-spoon:before { + content: "\f636"; +} +.game-icon-forklift:before { + content: "\f637"; +} +.game-icon-forward-field:before { + content: "\f638"; +} +.game-icon-forward-sun:before { + content: "\f639"; +} +.game-icon-fossil:before { + content: "\f63a"; +} +.game-icon-foundry-bucket:before { + content: "\f63b"; +} +.game-icon-fountain-pen:before { + content: "\f63c"; +} +.game-icon-fountain:before { + content: "\f63d"; +} +.game-icon-fox-head:before { + content: "\f63e"; +} +.game-icon-fox-tail:before { + content: "\f63f"; +} +.game-icon-fox:before { + content: "\f640"; +} +.game-icon-fragmented-meteor:before { + content: "\f641"; +} +.game-icon-fragmented-sword:before { + content: "\f642"; +} +.game-icon-fragrance:before { + content: "\f643"; +} +.game-icon-france:before { + content: "\f644"; +} +.game-icon-frankenstein-creature:before { + content: "\f645"; +} +.game-icon-frayed-arrow:before { + content: "\f646"; +} +.game-icon-freedom-dove:before { + content: "\f647"; +} +.game-icon-freemasonry:before { + content: "\f648"; +} +.game-icon-french-fries:before { + content: "\f649"; +} +.game-icon-french-horn:before { + content: "\f64a"; +} +.game-icon-fridge:before { + content: "\f64b"; +} +.game-icon-fried-eggs:before { + content: "\f64c"; +} +.game-icon-fried-fish:before { + content: "\f64d"; +} +.game-icon-frisbee:before { + content: "\f64e"; +} +.game-icon-froe-and-mallet:before { + content: "\f64f"; +} +.game-icon-frog-foot:before { + content: "\f650"; +} +.game-icon-frog-prince:before { + content: "\f651"; +} +.game-icon-frog:before { + content: "\f652"; +} +.game-icon-front-teeth:before { + content: "\f653"; +} +.game-icon-frontal-lobe:before { + content: "\f654"; +} +.game-icon-frostfire:before { + content: "\f655"; +} +.game-icon-frozen-arrow:before { + content: "\f656"; +} +.game-icon-frozen-block:before { + content: "\f657"; +} +.game-icon-frozen-body:before { + content: "\f658"; +} +.game-icon-frozen-orb:before { + content: "\f659"; +} +.game-icon-frozen-ring:before { + content: "\f65a"; +} +.game-icon-fruit-bowl:before { + content: "\f65b"; +} +.game-icon-fruit-tree:before { + content: "\f65c"; +} +.game-icon-fruiting:before { + content: "\f65d"; +} +.game-icon-fuel-tank:before { + content: "\f65e"; +} +.game-icon-fuji:before { + content: "\f65f"; +} +.game-icon-fulguro-punch:before { + content: "\f660"; +} +.game-icon-full-folder:before { + content: "\f661"; +} +.game-icon-full-metal-bucket-handle:before { + content: "\f662"; +} +.game-icon-full-metal-bucket:before { + content: "\f663"; +} +.game-icon-full-motorcycle-helmet:before { + content: "\f664"; +} +.game-icon-full-pizza:before { + content: "\f665"; +} +.game-icon-full-wood-bucket-handle:before { + content: "\f666"; +} +.game-icon-full-wood-bucket:before { + content: "\f667"; +} +.game-icon-funnel:before { + content: "\f668"; +} +.game-icon-fur-boot:before { + content: "\f669"; +} +.game-icon-fur-shirt:before { + content: "\f66a"; +} +.game-icon-furnace:before { + content: "\f66b"; +} +.game-icon-g-clef:before { + content: "\f66c"; +} +.game-icon-galaxy:before { + content: "\f66d"; +} +.game-icon-galea:before { + content: "\f66e"; +} +.game-icon-galleon:before { + content: "\f66f"; +} +.game-icon-galley:before { + content: "\f670"; +} +.game-icon-game-console:before { + content: "\f671"; +} +.game-icon-gamepad-cross:before { + content: "\f672"; +} +.game-icon-gamepad:before { + content: "\f673"; +} +.game-icon-gardening-shears:before { + content: "\f674"; +} +.game-icon-gargoyle:before { + content: "\f675"; +} +.game-icon-garlic:before { + content: "\f676"; +} +.game-icon-gas-mask-2:before { + content: "\f677"; +} +.game-icon-gas-mask:before { + content: "\f678"; +} +.game-icon-gas-pump:before { + content: "\f679"; +} +.game-icon-gas-stove:before { + content: "\f67a"; +} +.game-icon-gate:before { + content: "\f67b"; +} +.game-icon-gauls-helm:before { + content: "\f67c"; +} +.game-icon-gauntlet:before { + content: "\f67d"; +} +.game-icon-gavel:before { + content: "\f67e"; +} +.game-icon-gaze:before { + content: "\f67f"; +} +.game-icon-gear-hammer:before { + content: "\f680"; +} +.game-icon-gear-stick-pattern:before { + content: "\f681"; +} +.game-icon-gear-stick:before { + content: "\f682"; +} +.game-icon-gears:before { + content: "\f683"; +} +.game-icon-gecko:before { + content: "\f684"; +} +.game-icon-gem-chain:before { + content: "\f685"; +} +.game-icon-gem-necklace:before { + content: "\f686"; +} +.game-icon-gem-pendant:before { + content: "\f687"; +} +.game-icon-gemini:before { + content: "\f688"; +} +.game-icon-gems:before { + content: "\f689"; +} +.game-icon-ghost-ally:before { + content: "\f68a"; +} +.game-icon-ghost:before { + content: "\f68b"; +} +.game-icon-giant-squid:before { + content: "\f68c"; +} +.game-icon-giant:before { + content: "\f68d"; +} +.game-icon-gibbet:before { + content: "\f68e"; +} +.game-icon-gift-of-knowledge:before { + content: "\f68f"; +} +.game-icon-gift-trap:before { + content: "\f690"; +} +.game-icon-gingerbread-man:before { + content: "\f691"; +} +.game-icon-ginkgo-leaf:before { + content: "\f692"; +} +.game-icon-gladius:before { + content: "\f693"; +} +.game-icon-glaive:before { + content: "\f694"; +} +.game-icon-glass-ball:before { + content: "\f695"; +} +.game-icon-glass-celebration:before { + content: "\f696"; +} +.game-icon-glass-heart:before { + content: "\f697"; +} +.game-icon-glass-shot:before { + content: "\f698"; +} +.game-icon-glider:before { + content: "\f699"; +} +.game-icon-globe-ring:before { + content: "\f69a"; +} +.game-icon-globe:before { + content: "\f69b"; +} +.game-icon-glock:before { + content: "\f69c"; +} +.game-icon-gloop:before { + content: "\f69d"; +} +.game-icon-gloves:before { + content: "\f69e"; +} +.game-icon-glowing-artifact:before { + content: "\f69f"; +} +.game-icon-glowing-hands:before { + content: "\f6a0"; +} +.game-icon-gluttonous-smile:before { + content: "\f6a1"; +} +.game-icon-gluttony:before { + content: "\f6a2"; +} +.game-icon-goal-keeper:before { + content: "\f6a3"; +} +.game-icon-goat:before { + content: "\f6a4"; +} +.game-icon-goblin-camp:before { + content: "\f6a5"; +} +.game-icon-goblin-head:before { + content: "\f6a6"; +} +.game-icon-gold-bar:before { + content: "\f6a7"; +} +.game-icon-gold-mine:before { + content: "\f6a8"; +} +.game-icon-gold-nuggets:before { + content: "\f6a9"; +} +.game-icon-gold-scarab:before { + content: "\f6aa"; +} +.game-icon-gold-shell:before { + content: "\f6ab"; +} +.game-icon-gold-stack:before { + content: "\f6ac"; +} +.game-icon-golem-head:before { + content: "\f6ad"; +} +.game-icon-golf-flag:before { + content: "\f6ae"; +} +.game-icon-golf-tee:before { + content: "\f6af"; +} +.game-icon-gong:before { + content: "\f6b0"; +} +.game-icon-goo-explosion:before { + content: "\f6b1"; +} +.game-icon-goo-skull:before { + content: "\f6b2"; +} +.game-icon-goo-spurt:before { + content: "\f6b3"; +} +.game-icon-gooey-daemon:before { + content: "\f6b4"; +} +.game-icon-gooey-eyed-sun:before { + content: "\f6b5"; +} +.game-icon-gooey-impact:before { + content: "\f6b6"; +} +.game-icon-gooey-molecule:before { + content: "\f6b7"; +} +.game-icon-gooey-sword:before { + content: "\f6b8"; +} +.game-icon-goose:before { + content: "\f6b9"; +} +.game-icon-gorilla:before { + content: "\f6ba"; +} +.game-icon-gothic-cross:before { + content: "\f6bb"; +} +.game-icon-gps:before { + content: "\f6bc"; +} +.game-icon-grab:before { + content: "\f6bd"; +} +.game-icon-graduate-cap:before { + content: "\f6be"; +} +.game-icon-grain-bundle:before { + content: "\f6bf"; +} +.game-icon-grain:before { + content: "\f6c0"; +} +.game-icon-granary:before { + content: "\f6c1"; +} +.game-icon-grand-piano:before { + content: "\f6c2"; +} +.game-icon-grapes:before { + content: "\f6c3"; +} +.game-icon-grapple:before { + content: "\f6c4"; +} +.game-icon-grasping-claws:before { + content: "\f6c5"; +} +.game-icon-grasping-slug:before { + content: "\f6c6"; +} +.game-icon-grass-mushroom:before { + content: "\f6c7"; +} +.game-icon-grass:before { + content: "\f6c8"; +} +.game-icon-grave-flowers:before { + content: "\f6c9"; +} +.game-icon-graveyard:before { + content: "\f6ca"; +} +.game-icon-grease-trap:before { + content: "\f6cb"; +} +.game-icon-great-pyramid:before { + content: "\f6cc"; +} +.game-icon-great-war-tank:before { + content: "\f6cd"; +} +.game-icon-greaves:before { + content: "\f6ce"; +} +.game-icon-greek-sphinx:before { + content: "\f6cf"; +} +.game-icon-greek-temple:before { + content: "\f6d0"; +} +.game-icon-green-power:before { + content: "\f6d1"; +} +.game-icon-greenhouse:before { + content: "\f6d2"; +} +.game-icon-grenade-2:before { + content: "\f6d3"; +} +.game-icon-grenade:before { + content: "\f6d4"; +} +.game-icon-griffin-shield:before { + content: "\f6d5"; +} +.game-icon-griffin-symbol:before { + content: "\f6d6"; +} +.game-icon-grim-reaper:before { + content: "\f6d7"; +} +.game-icon-ground-sprout:before { + content: "\f6d8"; +} +.game-icon-groundbreaker:before { + content: "\f6d9"; +} +.game-icon-grouped-drops:before { + content: "\f6da"; +} +.game-icon-growth:before { + content: "\f6db"; +} +.game-icon-guarded-tower:before { + content: "\f6dc"; +} +.game-icon-guards:before { + content: "\f6dd"; +} +.game-icon-guatemala:before { + content: "\f6de"; +} +.game-icon-guillotine:before { + content: "\f6df"; +} +.game-icon-guitar-bass-head:before { + content: "\f6e0"; +} +.game-icon-guitar-head:before { + content: "\f6e1"; +} +.game-icon-guitar:before { + content: "\f6e2"; +} +.game-icon-gun-rose:before { + content: "\f6e3"; +} +.game-icon-gun-stock:before { + content: "\f6e4"; +} +.game-icon-gunshot:before { + content: "\f6e5"; +} +.game-icon-gym-bag:before { + content: "\f6e6"; +} +.game-icon-h2o:before { + content: "\f6e7"; +} +.game-icon-habitat-dome:before { + content: "\f6e8"; +} +.game-icon-hades-symbol:before { + content: "\f6e9"; +} +.game-icon-hair-strands:before { + content: "\f6ea"; +} +.game-icon-halberd-shuriken:before { + content: "\f6eb"; +} +.game-icon-halberd:before { + content: "\f6ec"; +} +.game-icon-half-body-crawling:before { + content: "\f6ed"; +} +.game-icon-half-dead:before { + content: "\f6ee"; +} +.game-icon-half-heart:before { + content: "\f6ef"; +} +.game-icon-half-log:before { + content: "\f6f0"; +} +.game-icon-half-tornado:before { + content: "\f6f1"; +} +.game-icon-halt:before { + content: "\f6f2"; +} +.game-icon-ham-shank:before { + content: "\f6f3"; +} +.game-icon-hamburger-menu:before { + content: "\f6f4"; +} +.game-icon-hamburger:before { + content: "\f6f5"; +} +.game-icon-hammer-break:before { + content: "\f6f6"; +} +.game-icon-hammer-drop:before { + content: "\f6f7"; +} +.game-icon-hammer-nails:before { + content: "\f6f8"; +} +.game-icon-hammer-sickle:before { + content: "\f6f9"; +} +.game-icon-hand-2:before { + content: "\f6fa"; +} +.game-icon-hand-bag:before { + content: "\f6fb"; +} +.game-icon-hand-bandage:before { + content: "\f6fc"; +} +.game-icon-hand-grip:before { + content: "\f6fd"; +} +.game-icon-hand-of-god:before { + content: "\f6fe"; +} +.game-icon-hand-ok:before { + content: "\f6ff"; +} +.game-icon-hand-saw:before { + content: "\f700"; +} +.game-icon-hand-truck:before { + content: "\f701"; +} +.game-icon-hand-wing:before { + content: "\f702"; +} +.game-icon-hand:before { + content: "\f703"; +} +.game-icon-handcuffed:before { + content: "\f704"; +} +.game-icon-handcuffs:before { + content: "\f705"; +} +.game-icon-handheld-fan:before { + content: "\f706"; +} +.game-icon-hang-glider-2:before { + content: "\f707"; +} +.game-icon-hang-glider:before { + content: "\f708"; +} +.game-icon-hanger:before { + content: "\f709"; +} +.game-icon-hanging-sign:before { + content: "\f70a"; +} +.game-icon-hanging-spider:before { + content: "\f70b"; +} +.game-icon-happy-skull:before { + content: "\f70c"; +} +.game-icon-harbor-dock:before { + content: "\f70d"; +} +.game-icon-harp:before { + content: "\f70e"; +} +.game-icon-harpoon-chain:before { + content: "\f70f"; +} +.game-icon-harpoon-trident:before { + content: "\f710"; +} +.game-icon-harpy:before { + content: "\f711"; +} +.game-icon-harry-potter-skull:before { + content: "\f712"; +} +.game-icon-hasty-grave:before { + content: "\f713"; +} +.game-icon-hatchet:before { + content: "\f714"; +} +.game-icon-hatchets:before { + content: "\f715"; +} +.game-icon-haunting:before { + content: "\f716"; +} +.game-icon-hawk-emblem:before { + content: "\f717"; +} +.game-icon-hazard-sign:before { + content: "\f718"; +} +.game-icon-hazmat-suit:before { + content: "\f719"; +} +.game-icon-head-shot:before { + content: "\f71a"; +} +.game-icon-headband-knot:before { + content: "\f71b"; +} +.game-icon-headphones:before { + content: "\f71c"; +} +.game-icon-headshot-2:before { + content: "\f71d"; +} +.game-icon-headshot:before { + content: "\f71e"; +} +.game-icon-healing-shield:before { + content: "\f71f"; +} +.game-icon-healing:before { + content: "\f720"; +} +.game-icon-health-capsule:before { + content: "\f721"; +} +.game-icon-health-decrease:before { + content: "\f722"; +} +.game-icon-health-increase:before { + content: "\f723"; +} +.game-icon-health-normal:before { + content: "\f724"; +} +.game-icon-health-potion:before { + content: "\f725"; +} +.game-icon-hearing-disabled:before { + content: "\f726"; +} +.game-icon-heart-armor:before { + content: "\f727"; +} +.game-icon-heart-battery:before { + content: "\f728"; +} +.game-icon-heart-beats:before { + content: "\f729"; +} +.game-icon-heart-bottle:before { + content: "\f72a"; +} +.game-icon-heart-drop:before { + content: "\f72b"; +} +.game-icon-heart-earrings:before { + content: "\f72c"; +} +.game-icon-heart-inside:before { + content: "\f72d"; +} +.game-icon-heart-key:before { + content: "\f72e"; +} +.game-icon-heart-minus:before { + content: "\f72f"; +} +.game-icon-heart-necklace:before { + content: "\f730"; +} +.game-icon-heart-organ:before { + content: "\f731"; +} +.game-icon-heart-plus:before { + content: "\f732"; +} +.game-icon-heart-shield:before { + content: "\f733"; +} +.game-icon-heart-stake:before { + content: "\f734"; +} +.game-icon-heart-tower:before { + content: "\f735"; +} +.game-icon-heart-wings:before { + content: "\f736"; +} +.game-icon-heartburn:before { + content: "\f737"; +} +.game-icon-hearts:before { + content: "\f738"; +} +.game-icon-heat-haze:before { + content: "\f739"; +} +.game-icon-heaven-gate:before { + content: "\f73a"; +} +.game-icon-heavy-arrow:before { + content: "\f73b"; +} +.game-icon-heavy-bullets:before { + content: "\f73c"; +} +.game-icon-heavy-collar:before { + content: "\f73d"; +} +.game-icon-heavy-fall:before { + content: "\f73e"; +} +.game-icon-heavy-fighter:before { + content: "\f73f"; +} +.game-icon-heavy-helm:before { + content: "\f740"; +} +.game-icon-heavy-lightning:before { + content: "\f741"; +} +.game-icon-heavy-rain:before { + content: "\f742"; +} +.game-icon-heavy-thorny-triskelion:before { + content: "\f743"; +} +.game-icon-heavy-timer:before { + content: "\f744"; +} +.game-icon-hedgehog:before { + content: "\f745"; +} +.game-icon-hedjet-white-crown:before { + content: "\f746"; +} +.game-icon-helicoprion:before { + content: "\f747"; +} +.game-icon-helicopter-tail:before { + content: "\f748"; +} +.game-icon-helicopter:before { + content: "\f749"; +} +.game-icon-hell-crosses:before { + content: "\f74a"; +} +.game-icon-helmet-head-shot:before { + content: "\f74b"; +} +.game-icon-helmet:before { + content: "\f74c"; +} +.game-icon-help:before { + content: "\f74d"; +} +.game-icon-hemp:before { + content: "\f74e"; +} +.game-icon-heptagram:before { + content: "\f74f"; +} +.game-icon-heraldic-sun:before { + content: "\f750"; +} +.game-icon-herbs-bundle:before { + content: "\f751"; +} +.game-icon-heron:before { + content: "\f752"; +} +.game-icon-hexagonal-nut:before { + content: "\f753"; +} +.game-icon-hexes:before { + content: "\f754"; +} +.game-icon-hidden:before { + content: "\f755"; +} +.game-icon-hieroglyph-legs:before { + content: "\f756"; +} +.game-icon-hieroglyph-y:before { + content: "\f757"; +} +.game-icon-high-five:before { + content: "\f758"; +} +.game-icon-high-grass:before { + content: "\f759"; +} +.game-icon-high-heel:before { + content: "\f75a"; +} +.game-icon-high-kick:before { + content: "\f75b"; +} +.game-icon-high-punch:before { + content: "\f75c"; +} +.game-icon-high-shot:before { + content: "\f75d"; +} +.game-icon-high-tide:before { + content: "\f75e"; +} +.game-icon-highlighter:before { + content: "\f75f"; +} +.game-icon-hiking:before { + content: "\f760"; +} +.game-icon-hill-conquest:before { + content: "\f761"; +} +.game-icon-hill-fort:before { + content: "\f762"; +} +.game-icon-hills:before { + content: "\f763"; +} +.game-icon-histogram:before { + content: "\f764"; +} +.game-icon-hive-mind:before { + content: "\f765"; +} +.game-icon-hive:before { + content: "\f766"; +} +.game-icon-hobbit-door:before { + content: "\f767"; +} +.game-icon-hobbit-dwelling:before { + content: "\f768"; +} +.game-icon-hockey:before { + content: "\f769"; +} +.game-icon-hole-ladder:before { + content: "\f76a"; +} +.game-icon-hole:before { + content: "\f76b"; +} +.game-icon-hollow-cat:before { + content: "\f76c"; +} +.game-icon-holosphere:before { + content: "\f76d"; +} +.game-icon-holy-grail:before { + content: "\f76e"; +} +.game-icon-holy-hand-grenade:before { + content: "\f76f"; +} +.game-icon-holy-oak:before { + content: "\f770"; +} +.game-icon-holy-symbol:before { + content: "\f771"; +} +.game-icon-holy-water:before { + content: "\f772"; +} +.game-icon-home-garage:before { + content: "\f773"; +} +.game-icon-honey-jar:before { + content: "\f774"; +} +.game-icon-honeycomb:before { + content: "\f775"; +} +.game-icon-honeypot:before { + content: "\f776"; +} +.game-icon-hood:before { + content: "\f777"; +} +.game-icon-hooded-assassin:before { + content: "\f778"; +} +.game-icon-hooded-figure:before { + content: "\f779"; +} +.game-icon-hoodie:before { + content: "\f77a"; +} +.game-icon-hoof:before { + content: "\f77b"; +} +.game-icon-hook:before { + content: "\f77c"; +} +.game-icon-hops:before { + content: "\f77d"; +} +.game-icon-horizon-road:before { + content: "\f77e"; +} +.game-icon-horizontal-flip:before { + content: "\f77f"; +} +.game-icon-horn-internal:before { + content: "\f780"; +} +.game-icon-horned-helm:before { + content: "\f781"; +} +.game-icon-horned-reptile:before { + content: "\f782"; +} +.game-icon-horned-skull:before { + content: "\f783"; +} +.game-icon-horse-head-2:before { + content: "\f784"; +} +.game-icon-horse-head:before { + content: "\f785"; +} +.game-icon-horseshoe:before { + content: "\f786"; +} +.game-icon-horus:before { + content: "\f787"; +} +.game-icon-hospital-cross:before { + content: "\f788"; +} +.game-icon-hospital:before { + content: "\f789"; +} +.game-icon-hot-dog:before { + content: "\f78a"; +} +.game-icon-hot-meal:before { + content: "\f78b"; +} +.game-icon-hot-spices:before { + content: "\f78c"; +} +.game-icon-hot-surface:before { + content: "\f78d"; +} +.game-icon-hound:before { + content: "\f78e"; +} +.game-icon-hourglass:before { + content: "\f78f"; +} +.game-icon-house-keys:before { + content: "\f790"; +} +.game-icon-house:before { + content: "\f791"; +} +.game-icon-human-cannonball:before { + content: "\f792"; +} +.game-icon-human-ear:before { + content: "\f793"; +} +.game-icon-human-pyramid:before { + content: "\f794"; +} +.game-icon-human-target:before { + content: "\f795"; +} +.game-icon-hummingbird:before { + content: "\f796"; +} +.game-icon-hungary:before { + content: "\f797"; +} +.game-icon-hunter-eyes:before { + content: "\f798"; +} +.game-icon-hunting-bolas:before { + content: "\f799"; +} +.game-icon-hunting-horn:before { + content: "\f79a"; +} +.game-icon-hut:before { + content: "\f79b"; +} +.game-icon-huts-village:before { + content: "\f79c"; +} +.game-icon-hydra-shot:before { + content: "\f79d"; +} +.game-icon-hydra:before { + content: "\f79e"; +} +.game-icon-hyena-head:before { + content: "\f79f"; +} +.game-icon-hypersonic-bolt:before { + content: "\f7a0"; +} +.game-icon-hypersonic-melon:before { + content: "\f7a1"; +} +.game-icon-hypodermic-test:before { + content: "\f7a2"; +} +.game-icon-i-beam:before { + content: "\f7a3"; +} +.game-icon-i-brick:before { + content: "\f7a4"; +} +.game-icon-ibis:before { + content: "\f7a5"; +} +.game-icon-icarus:before { + content: "\f7a6"; +} +.game-icon-ice-bolt:before { + content: "\f7a7"; +} +.game-icon-ice-bomb:before { + content: "\f7a8"; +} +.game-icon-ice-cream-cone:before { + content: "\f7a9"; +} +.game-icon-ice-cream-scoop:before { + content: "\f7aa"; +} +.game-icon-ice-cube:before { + content: "\f7ab"; +} +.game-icon-ice-cubes:before { + content: "\f7ac"; +} +.game-icon-ice-golem:before { + content: "\f7ad"; +} +.game-icon-ice-iris:before { + content: "\f7ae"; +} +.game-icon-ice-pop:before { + content: "\f7af"; +} +.game-icon-ice-shield:before { + content: "\f7b0"; +} +.game-icon-ice-skate:before { + content: "\f7b1"; +} +.game-icon-ice-spear:before { + content: "\f7b2"; +} +.game-icon-ice-spell-cast:before { + content: "\f7b3"; +} +.game-icon-iceberg:before { + content: "\f7b4"; +} +.game-icon-icebergs:before { + content: "\f7b5"; +} +.game-icon-iceland:before { + content: "\f7b6"; +} +.game-icon-icicles-aura:before { + content: "\f7b7"; +} +.game-icon-icicles-fence:before { + content: "\f7b8"; +} +.game-icon-id-card:before { + content: "\f7b9"; +} +.game-icon-idea:before { + content: "\f7ba"; +} +.game-icon-ifrit:before { + content: "\f7bb"; +} +.game-icon-igloo:before { + content: "\f7bc"; +} +.game-icon-imbricated-arrows:before { + content: "\f7bd"; +} +.game-icon-imp-laugh:before { + content: "\f7be"; +} +.game-icon-imp:before { + content: "\f7bf"; +} +.game-icon-impact-point:before { + content: "\f7c0"; +} +.game-icon-imperial-crown:before { + content: "\f7c1"; +} +.game-icon-implosion:before { + content: "\f7c2"; +} +.game-icon-imprisoned:before { + content: "\f7c3"; +} +.game-icon-inauguration:before { + content: "\f7c4"; +} +.game-icon-incense:before { + content: "\f7c5"; +} +.game-icon-incisors:before { + content: "\f7c6"; +} +.game-icon-incoming-rocket:before { + content: "\f7c7"; +} +.game-icon-incubator:before { + content: "\f7c8"; +} +.game-icon-india-gate:before { + content: "\f7c9"; +} +.game-icon-indian-palace:before { + content: "\f7ca"; +} +.game-icon-inferno-bomb:before { + content: "\f7cb"; +} +.game-icon-infested-mass:before { + content: "\f7cc"; +} +.game-icon-infinity:before { + content: "\f7cd"; +} +.game-icon-info:before { + content: "\f7ce"; +} +.game-icon-injustice:before { + content: "\f7cf"; +} +.game-icon-ink-swirl:before { + content: "\f7d0"; +} +.game-icon-inner-self:before { + content: "\f7d1"; +} +.game-icon-insect-jaws:before { + content: "\f7d2"; +} +.game-icon-inspiration:before { + content: "\f7d3"; +} +.game-icon-interceptor-ship:before { + content: "\f7d4"; +} +.game-icon-interdiction:before { + content: "\f7d5"; +} +.game-icon-interlaced-tentacles:before { + content: "\f7d6"; +} +.game-icon-interleaved-arrows:before { + content: "\f7d7"; +} +.game-icon-interleaved-claws:before { + content: "\f7d8"; +} +.game-icon-internal-injury:before { + content: "\f7d9"; +} +.game-icon-internal-organ:before { + content: "\f7da"; +} +.game-icon-interstellar-path:before { + content: "\f7db"; +} +.game-icon-intricate-necklace:before { + content: "\f7dc"; +} +.game-icon-inverted-dice-1:before { + content: "\f7dd"; +} +.game-icon-inverted-dice-2:before { + content: "\f7de"; +} +.game-icon-inverted-dice-3:before { + content: "\f7df"; +} +.game-icon-inverted-dice-4:before { + content: "\f7e0"; +} +.game-icon-inverted-dice-5:before { + content: "\f7e1"; +} +.game-icon-inverted-dice-6:before { + content: "\f7e2"; +} +.game-icon-invisible-face:before { + content: "\f7e3"; +} +.game-icon-invisible:before { + content: "\f7e4"; +} +.game-icon-ion-cannon-blast:before { + content: "\f7e5"; +} +.game-icon-ionic-column:before { + content: "\f7e6"; +} +.game-icon-iraq:before { + content: "\f7e7"; +} +.game-icon-iron-cross:before { + content: "\f7e8"; +} +.game-icon-iron-hulled-warship:before { + content: "\f7e9"; +} +.game-icon-iron-mask:before { + content: "\f7ea"; +} +.game-icon-island:before { + content: "\f7eb"; +} +.game-icon-italia:before { + content: "\f7ec"; +} +.game-icon-ivory-tusks:before { + content: "\f7ed"; +} +.game-icon-j-brick:before { + content: "\f7ee"; +} +.game-icon-jack-plug:before { + content: "\f7ef"; +} +.game-icon-james-bond-aperture:before { + content: "\f7f0"; +} +.game-icon-japan:before { + content: "\f7f1"; +} +.game-icon-japanese-bridge:before { + content: "\f7f2"; +} +.game-icon-jasmine:before { + content: "\f7f3"; +} +.game-icon-jason-mask:before { + content: "\f7f4"; +} +.game-icon-jawbone:before { + content: "\f7f5"; +} +.game-icon-jawless-cyclop:before { + content: "\f7f6"; +} +.game-icon-jeep-2:before { + content: "\f7f7"; +} +.game-icon-jeep:before { + content: "\f7f8"; +} +.game-icon-jelly-beans:before { + content: "\f7f9"; +} +.game-icon-jelly:before { + content: "\f7fa"; +} +.game-icon-jellyfish:before { + content: "\f7fb"; +} +.game-icon-jerrycan:before { + content: "\f7fc"; +} +.game-icon-jerusalem-cross:before { + content: "\f7fd"; +} +.game-icon-jester-hat:before { + content: "\f7fe"; +} +.game-icon-jet-fighter:before { + content: "\f7ff"; +} +.game-icon-jet-pack:before { + content: "\f800"; +} +.game-icon-jetpack:before { + content: "\f801"; +} +.game-icon-jewel-crown:before { + content: "\f802"; +} +.game-icon-jeweled-chalice:before { + content: "\f803"; +} +.game-icon-jigsaw-box:before { + content: "\f804"; +} +.game-icon-jigsaw-piece:before { + content: "\f805"; +} +.game-icon-join:before { + content: "\f806"; +} +.game-icon-joint:before { + content: "\f807"; +} +.game-icon-journey:before { + content: "\f808"; +} +.game-icon-joystick:before { + content: "\f809"; +} +.game-icon-jug:before { + content: "\f80a"; +} +.game-icon-juggler:before { + content: "\f80b"; +} +.game-icon-juggling-clubs:before { + content: "\f80c"; +} +.game-icon-juggling-seal:before { + content: "\f80d"; +} +.game-icon-jump-across:before { + content: "\f80e"; +} +.game-icon-jumping-dog:before { + content: "\f80f"; +} +.game-icon-jumping-rope:before { + content: "\f810"; +} +.game-icon-jungle:before { + content: "\f811"; +} +.game-icon-jupiter:before { + content: "\f812"; +} +.game-icon-justice-star:before { + content: "\f813"; +} +.game-icon-kaleidoscope-pearls:before { + content: "\f814"; +} +.game-icon-kangaroo:before { + content: "\f815"; +} +.game-icon-katana:before { + content: "\f816"; +} +.game-icon-kebab-spit:before { + content: "\f817"; +} +.game-icon-kenku-head:before { + content: "\f818"; +} +.game-icon-kenya:before { + content: "\f819"; +} +.game-icon-ketchup:before { + content: "\f81a"; +} +.game-icon-kevlar-vest:before { + content: "\f81b"; +} +.game-icon-kevlar:before { + content: "\f81c"; +} +.game-icon-key-2:before { + content: "\f81d"; +} +.game-icon-key-card:before { + content: "\f81e"; +} +.game-icon-key-lock:before { + content: "\f81f"; +} +.game-icon-key:before { + content: "\f820"; +} +.game-icon-keyboard:before { + content: "\f821"; +} +.game-icon-keyring:before { + content: "\f822"; +} +.game-icon-kick-scooter:before { + content: "\f823"; +} +.game-icon-kid-slide:before { + content: "\f824"; +} +.game-icon-kidneys:before { + content: "\f825"; +} +.game-icon-kimono:before { + content: "\f826"; +} +.game-icon-kindle:before { + content: "\f827"; +} +.game-icon-king-ju-mask:before { + content: "\f828"; +} +.game-icon-king:before { + content: "\f829"; +} +.game-icon-kitchen-knives:before { + content: "\f82a"; +} +.game-icon-kitchen-scale:before { + content: "\f82b"; +} +.game-icon-kitchen-tap:before { + content: "\f82c"; +} +.game-icon-kite:before { + content: "\f82d"; +} +.game-icon-kiwi-bird:before { + content: "\f82e"; +} +.game-icon-kiwi-fruit:before { + content: "\f82f"; +} +.game-icon-klingon:before { + content: "\f830"; +} +.game-icon-knapsack:before { + content: "\f831"; +} +.game-icon-knee-bandage:before { + content: "\f832"; +} +.game-icon-knee-cap:before { + content: "\f833"; +} +.game-icon-knee-pad:before { + content: "\f834"; +} +.game-icon-kneeling:before { + content: "\f835"; +} +.game-icon-knife-fork:before { + content: "\f836"; +} +.game-icon-knife-thrust:before { + content: "\f837"; +} +.game-icon-knight-banner:before { + content: "\f838"; +} +.game-icon-knocked-out-stars:before { + content: "\f839"; +} +.game-icon-knockout:before { + content: "\f83a"; +} +.game-icon-knot:before { + content: "\f83b"; +} +.game-icon-koala:before { + content: "\f83c"; +} +.game-icon-koholint-egg:before { + content: "\f83d"; +} +.game-icon-kraken-tentacle:before { + content: "\f83e"; +} +.game-icon-kusarigama:before { + content: "\f83f"; +} +.game-icon-l-brick:before { + content: "\f840"; +} +.game-icon-lab-coat:before { + content: "\f841"; +} +.game-icon-labrador-head:before { + content: "\f842"; +} +.game-icon-ladder:before { + content: "\f843"; +} +.game-icon-ladders-platform:before { + content: "\f844"; +} +.game-icon-ladle:before { + content: "\f845"; +} +.game-icon-ladybug:before { + content: "\f846"; +} +.game-icon-lamellar:before { + content: "\f847"; +} +.game-icon-lamprey-mouth:before { + content: "\f848"; +} +.game-icon-land-mine:before { + content: "\f849"; +} +.game-icon-lantern-flame:before { + content: "\f84a"; +} +.game-icon-lantern:before { + content: "\f84b"; +} +.game-icon-laptop:before { + content: "\f84c"; +} +.game-icon-large-dress:before { + content: "\f84d"; +} +.game-icon-large-paint-brush:before { + content: "\f84e"; +} +.game-icon-large-wound:before { + content: "\f84f"; +} +.game-icon-laser-blast:before { + content: "\f850"; +} +.game-icon-laser-burst:before { + content: "\f851"; +} +.game-icon-laser-gun:before { + content: "\f852"; +} +.game-icon-laser-precision:before { + content: "\f853"; +} +.game-icon-laser-sparks:before { + content: "\f854"; +} +.game-icon-laser-turret:before { + content: "\f855"; +} +.game-icon-laser-warning:before { + content: "\f856"; +} +.game-icon-laserburn:before { + content: "\f857"; +} +.game-icon-lasso:before { + content: "\f858"; +} +.game-icon-latvia:before { + content: "\f859"; +} +.game-icon-laurel-crown:before { + content: "\f85a"; +} +.game-icon-laurels-trophy:before { + content: "\f85b"; +} +.game-icon-laurels:before { + content: "\f85c"; +} +.game-icon-lava:before { + content: "\f85d"; +} +.game-icon-law-star:before { + content: "\f85e"; +} +.game-icon-layered-armor:before { + content: "\f85f"; +} +.game-icon-lead-pipe:before { + content: "\f860"; +} +.game-icon-leaf-skeleton:before { + content: "\f861"; +} +.game-icon-leaf-swirl:before { + content: "\f862"; +} +.game-icon-leak:before { + content: "\f863"; +} +.game-icon-leaky-skull:before { + content: "\f864"; +} +.game-icon-leapfrog:before { + content: "\f865"; +} +.game-icon-leather-armor:before { + content: "\f866"; +} +.game-icon-leather-boot:before { + content: "\f867"; +} +.game-icon-leather-vest:before { + content: "\f868"; +} +.game-icon-led:before { + content: "\f869"; +} +.game-icon-lee-enfield:before { + content: "\f86a"; +} +.game-icon-leeching-worm:before { + content: "\f86b"; +} +.game-icon-leek:before { + content: "\f86c"; +} +.game-icon-leg-armor:before { + content: "\f86d"; +} +.game-icon-leg:before { + content: "\f86e"; +} +.game-icon-lemon:before { + content: "\f86f"; +} +.game-icon-leo:before { + content: "\f870"; +} +.game-icon-letter-bomb:before { + content: "\f871"; +} +.game-icon-level-crossing:before { + content: "\f872"; +} +.game-icon-level-end-flag:before { + content: "\f873"; +} +.game-icon-level-four-advanced:before { + content: "\f874"; +} +.game-icon-level-four:before { + content: "\f875"; +} +.game-icon-level-three-advanced:before { + content: "\f876"; +} +.game-icon-level-three:before { + content: "\f877"; +} +.game-icon-level-two-advanced:before { + content: "\f878"; +} +.game-icon-level-two:before { + content: "\f879"; +} +.game-icon-lever:before { + content: "\f87a"; +} +.game-icon-liar:before { + content: "\f87b"; +} +.game-icon-liberty-wing:before { + content: "\f87c"; +} +.game-icon-libra:before { + content: "\f87d"; +} +.game-icon-libya:before { + content: "\f87e"; +} +.game-icon-life-bar:before { + content: "\f87f"; +} +.game-icon-life-buoy:before { + content: "\f880"; +} +.game-icon-life-in-the-balance:before { + content: "\f881"; +} +.game-icon-life-jacket:before { + content: "\f882"; +} +.game-icon-life-support:before { + content: "\f883"; +} +.game-icon-life-tap:before { + content: "\f884"; +} +.game-icon-lift:before { + content: "\f885"; +} +.game-icon-light-backpack:before { + content: "\f886"; +} +.game-icon-light-bulb:before { + content: "\f887"; +} +.game-icon-light-fighter:before { + content: "\f888"; +} +.game-icon-light-helm:before { + content: "\f889"; +} +.game-icon-light-projector:before { + content: "\f88a"; +} +.game-icon-light-sabers:before { + content: "\f88b"; +} +.game-icon-light-thorny-triskelion:before { + content: "\f88c"; +} +.game-icon-lighter:before { + content: "\f88d"; +} +.game-icon-lighthouse:before { + content: "\f88e"; +} +.game-icon-lightning-arc:before { + content: "\f88f"; +} +.game-icon-lightning-bow:before { + content: "\f890"; +} +.game-icon-lightning-branches:before { + content: "\f891"; +} +.game-icon-lightning-dissipation:before { + content: "\f892"; +} +.game-icon-lightning-dome:before { + content: "\f893"; +} +.game-icon-lightning-electron:before { + content: "\f894"; +} +.game-icon-lightning-flame:before { + content: "\f895"; +} +.game-icon-lightning-frequency:before { + content: "\f896"; +} +.game-icon-lightning-helix:before { + content: "\f897"; +} +.game-icon-lightning-mask:before { + content: "\f898"; +} +.game-icon-lightning-saber:before { + content: "\f899"; +} +.game-icon-lightning-shadow:before { + content: "\f89a"; +} +.game-icon-lightning-shield:before { + content: "\f89b"; +} +.game-icon-lightning-shout:before { + content: "\f89c"; +} +.game-icon-lightning-slashes:before { + content: "\f89d"; +} +.game-icon-lightning-spanner:before { + content: "\f89e"; +} +.game-icon-lightning-storm:before { + content: "\f89f"; +} +.game-icon-lightning-tear:before { + content: "\f8a0"; +} +.game-icon-lightning-tree:before { + content: "\f8a1"; +} +.game-icon-lightning-trio:before { + content: "\f8a2"; +} +.game-icon-lily-pads:before { + content: "\f8a3"; +} +.game-icon-linden-leaf:before { + content: "\f8a4"; +} +.game-icon-linked-rings:before { + content: "\f8a5"; +} +.game-icon-lion:before { + content: "\f8a6"; +} +.game-icon-lips:before { + content: "\f8a7"; +} +.game-icon-lipstick:before { + content: "\f8a8"; +} +.game-icon-liquid-soap:before { + content: "\f8a9"; +} +.game-icon-lit-candelabra:before { + content: "\f8aa"; +} +.game-icon-liver:before { + content: "\f8ab"; +} +.game-icon-lizard-tongue:before { + content: "\f8ac"; +} +.game-icon-lizardman:before { + content: "\f8ad"; +} +.game-icon-load:before { + content: "\f8ae"; +} +.game-icon-lob-arrow:before { + content: "\f8af"; +} +.game-icon-lock-picking:before { + content: "\f8b0"; +} +.game-icon-lock-spy:before { + content: "\f8b1"; +} +.game-icon-locked-box:before { + content: "\f8b2"; +} +.game-icon-locked-chest:before { + content: "\f8b3"; +} +.game-icon-locked-door:before { + content: "\f8b4"; +} +.game-icon-locked-fortress:before { + content: "\f8b5"; +} +.game-icon-locked-heart:before { + content: "\f8b6"; +} +.game-icon-lockers:before { + content: "\f8b7"; +} +.game-icon-lockpicks:before { + content: "\f8b8"; +} +.game-icon-log:before { + content: "\f8b9"; +} +.game-icon-logging:before { + content: "\f8ba"; +} +.game-icon-logic-gate-and:before { + content: "\f8bb"; +} +.game-icon-logic-gate-nand:before { + content: "\f8bc"; +} +.game-icon-logic-gate-nor:before { + content: "\f8bd"; +} +.game-icon-logic-gate-not:before { + content: "\f8be"; +} +.game-icon-logic-gate-nxor:before { + content: "\f8bf"; +} +.game-icon-logic-gate-or:before { + content: "\f8c0"; +} +.game-icon-logic-gate-xor:before { + content: "\f8c1"; +} +.game-icon-loincloth:before { + content: "\f8c2"; +} +.game-icon-long-antennae-bug:before { + content: "\f8c3"; +} +.game-icon-long-legged-spider:before { + content: "\f8c4"; +} +.game-icon-look-at:before { + content: "\f8c5"; +} +.game-icon-lorgnette:before { + content: "\f8c6"; +} +.game-icon-lost-limb:before { + content: "\f8c7"; +} +.game-icon-lotus-flower:before { + content: "\f8c8"; +} +.game-icon-lotus:before { + content: "\f8c9"; +} +.game-icon-louvre-pyramid:before { + content: "\f8ca"; +} +.game-icon-love-howl:before { + content: "\f8cb"; +} +.game-icon-love-injection:before { + content: "\f8cc"; +} +.game-icon-love-letter:before { + content: "\f8cd"; +} +.game-icon-love-mystery:before { + content: "\f8ce"; +} +.game-icon-love-song:before { + content: "\f8cf"; +} +.game-icon-lovers:before { + content: "\f8d0"; +} +.game-icon-low-tide:before { + content: "\f8d1"; +} +.game-icon-luchador:before { + content: "\f8d2"; +} +.game-icon-lucifer-cannon:before { + content: "\f8d3"; +} +.game-icon-lucky-fisherman:before { + content: "\f8d4"; +} +.game-icon-luger:before { + content: "\f8d5"; +} +.game-icon-lunar-module:before { + content: "\f8d6"; +} +.game-icon-lunar-wand:before { + content: "\f8d7"; +} +.game-icon-lungs:before { + content: "\f8d8"; +} +.game-icon-lynx-head:before { + content: "\f8d9"; +} +.game-icon-lyre:before { + content: "\f8da"; +} +.game-icon-m3-grease-gun:before { + content: "\f8db"; +} +.game-icon-mac-10:before { + content: "\f8dc"; +} +.game-icon-mace-head:before { + content: "\f8dd"; +} +.game-icon-machete:before { + content: "\f8de"; +} +.game-icon-machine-gun-magazine:before { + content: "\f8df"; +} +.game-icon-machine-gun:before { + content: "\f8e0"; +} +.game-icon-mad-scientist:before { + content: "\f8e1"; +} +.game-icon-maggot:before { + content: "\f8e2"; +} +.game-icon-magic-axe:before { + content: "\f8e3"; +} +.game-icon-magic-broom:before { + content: "\f8e4"; +} +.game-icon-magic-gate:before { + content: "\f8e5"; +} +.game-icon-magic-hat:before { + content: "\f8e6"; +} +.game-icon-magic-lamp:before { + content: "\f8e7"; +} +.game-icon-magic-palm:before { + content: "\f8e8"; +} +.game-icon-magic-portal:before { + content: "\f8e9"; +} +.game-icon-magic-potion:before { + content: "\f8ea"; +} +.game-icon-magic-shield:before { + content: "\f8eb"; +} +.game-icon-magic-swirl:before { + content: "\f8ec"; +} +.game-icon-magic-trident:before { + content: "\f8ed"; +} +.game-icon-magick-trick:before { + content: "\f8ee"; +} +.game-icon-magnet-blast:before { + content: "\f8ef"; +} +.game-icon-magnet-man:before { + content: "\f8f0"; +} +.game-icon-magnet:before { + content: "\f8f1"; +} +.game-icon-magnifying-glass:before { + content: "\f8f2"; +} +.game-icon-mail-shirt:before { + content: "\f8f3"; +} +.game-icon-mailbox:before { + content: "\f8f4"; +} +.game-icon-mailed-fist:before { + content: "\f8f5"; +} +.game-icon-male:before { + content: "\f8f6"; +} +.game-icon-mammoth:before { + content: "\f8f7"; +} +.game-icon-manacles:before { + content: "\f8f8"; +} +.game-icon-mandrill-head:before { + content: "\f8f9"; +} +.game-icon-manta-ray:before { + content: "\f8fa"; +} +.game-icon-mantrap:before { + content: "\f8fb"; +} +.game-icon-manual-juicer:before { + content: "\f8fc"; +} +.game-icon-manual-meat-grinder:before { + content: "\f8fd"; +} +.game-icon-maple-leaf:before { + content: "\f8fe"; +} +.game-icon-maracas:before { + content: "\f8ff"; +} +.game-icon-marble-tap:before { + content: "\f900"; +} +.game-icon-marbles:before { + content: "\f901"; +} +.game-icon-marrow-drain:before { + content: "\f902"; +} +.game-icon-mars-curiosity:before { + content: "\f903"; +} +.game-icon-mars-pathfinder:before { + content: "\f904"; +} +.game-icon-marshmallows:before { + content: "\f905"; +} +.game-icon-martini:before { + content: "\f906"; +} +.game-icon-martyr-memorial:before { + content: "\f907"; +} +.game-icon-masked-spider:before { + content: "\f908"; +} +.game-icon-mason-jar:before { + content: "\f909"; +} +.game-icon-mass-driver:before { + content: "\f90a"; +} +.game-icon-master-of-arms:before { + content: "\f90b"; +} +.game-icon-match-head:before { + content: "\f90c"; +} +.game-icon-match-tip:before { + content: "\f90d"; +} +.game-icon-matchbox:before { + content: "\f90e"; +} +.game-icon-materials-science:before { + content: "\f90f"; +} +.game-icon-matryoshka-dolls:before { + content: "\f910"; +} +.game-icon-matter-states:before { + content: "\f911"; +} +.game-icon-mayan-pyramid:before { + content: "\f912"; +} +.game-icon-maze-cornea:before { + content: "\f913"; +} +.game-icon-maze-saw:before { + content: "\f914"; +} +.game-icon-maze:before { + content: "\f915"; +} +.game-icon-meal:before { + content: "\f916"; +} +.game-icon-measure-tape:before { + content: "\f917"; +} +.game-icon-meat-cleaver:before { + content: "\f918"; +} +.game-icon-meat-hook:before { + content: "\f919"; +} +.game-icon-meat:before { + content: "\f91a"; +} +.game-icon-mecha-head:before { + content: "\f91b"; +} +.game-icon-mecha-mask:before { + content: "\f91c"; +} +.game-icon-mechanic-garage:before { + content: "\f91d"; +} +.game-icon-mechanical-arm:before { + content: "\f91e"; +} +.game-icon-medal-skull:before { + content: "\f91f"; +} +.game-icon-medal:before { + content: "\f920"; +} +.game-icon-medallist:before { + content: "\f921"; +} +.game-icon-medical-drip:before { + content: "\f922"; +} +.game-icon-medical-pack-alt:before { + content: "\f923"; +} +.game-icon-medical-pack:before { + content: "\f924"; +} +.game-icon-medical-thermometer:before { + content: "\f925"; +} +.game-icon-medicine-pills:before { + content: "\f926"; +} +.game-icon-medicines:before { + content: "\f927"; +} +.game-icon-medieval-barracks:before { + content: "\f928"; +} +.game-icon-medieval-gate:before { + content: "\f929"; +} +.game-icon-medieval-pavilion:before { + content: "\f92a"; +} +.game-icon-meditation:before { + content: "\f92b"; +} +.game-icon-medusa-head:before { + content: "\f92c"; +} +.game-icon-meeple-army:before { + content: "\f92d"; +} +.game-icon-meeple-circle:before { + content: "\f92e"; +} +.game-icon-meeple-group:before { + content: "\f92f"; +} +.game-icon-meeple-king:before { + content: "\f930"; +} +.game-icon-meeple:before { + content: "\f931"; +} +.game-icon-megabot:before { + content: "\f932"; +} +.game-icon-megaphone:before { + content: "\f933"; +} +.game-icon-melting-ice-cube:before { + content: "\f934"; +} +.game-icon-melting-metal:before { + content: "\f935"; +} +.game-icon-menhir:before { + content: "\f936"; +} +.game-icon-mermaid:before { + content: "\f937"; +} +.game-icon-mesh-ball:before { + content: "\f938"; +} +.game-icon-mesh-network:before { + content: "\f939"; +} +.game-icon-metal-bar:before { + content: "\f93a"; +} +.game-icon-metal-boot:before { + content: "\f93b"; +} +.game-icon-metal-detector:before { + content: "\f93c"; +} +.game-icon-metal-disc:before { + content: "\f93d"; +} +.game-icon-metal-golem-head:before { + content: "\f93e"; +} +.game-icon-metal-hand:before { + content: "\f93f"; +} +.game-icon-metal-plate:before { + content: "\f940"; +} +.game-icon-metal-scales:before { + content: "\f941"; +} +.game-icon-metal-skirt:before { + content: "\f942"; +} +.game-icon-meteor-impact:before { + content: "\f943"; +} +.game-icon-metroid:before { + content: "\f944"; +} +.game-icon-metronome:before { + content: "\f945"; +} +.game-icon-mexico:before { + content: "\f946"; +} +.game-icon-microchip:before { + content: "\f947"; +} +.game-icon-microphone:before { + content: "\f948"; +} +.game-icon-microscope-lens:before { + content: "\f949"; +} +.game-icon-microscope:before { + content: "\f94a"; +} +.game-icon-middle-arrow:before { + content: "\f94b"; +} +.game-icon-midnight-claw:before { + content: "\f94c"; +} +.game-icon-mighty-boosh:before { + content: "\f94d"; +} +.game-icon-mighty-force:before { + content: "\f94e"; +} +.game-icon-mighty-horn:before { + content: "\f94f"; +} +.game-icon-mighty-spanner:before { + content: "\f950"; +} +.game-icon-military-ambulance:before { + content: "\f951"; +} +.game-icon-military-fort:before { + content: "\f952"; +} +.game-icon-milk-carton:before { + content: "\f953"; +} +.game-icon-millenium-key:before { + content: "\f954"; +} +.game-icon-mimic-chest:before { + content: "\f955"; +} +.game-icon-mine-explosion:before { + content: "\f956"; +} +.game-icon-mine-truck:before { + content: "\f957"; +} +.game-icon-mine-wagon:before { + content: "\f958"; +} +.game-icon-minefield:before { + content: "\f959"; +} +.game-icon-miner:before { + content: "\f95a"; +} +.game-icon-mineral-heart:before { + content: "\f95b"; +} +.game-icon-mineral-pearls:before { + content: "\f95c"; +} +.game-icon-minerals:before { + content: "\f95d"; +} +.game-icon-mini-submarine:before { + content: "\f95e"; +} +.game-icon-minigun:before { + content: "\f95f"; +} +.game-icon-mining-helmet:before { + content: "\f960"; +} +.game-icon-mining:before { + content: "\f961"; +} +.game-icon-minions:before { + content: "\f962"; +} +.game-icon-minotaur:before { + content: "\f963"; +} +.game-icon-miracle-medecine:before { + content: "\f964"; +} +.game-icon-mirror-mirror:before { + content: "\f965"; +} +.game-icon-misdirection:before { + content: "\f966"; +} +.game-icon-missile-launcher:before { + content: "\f967"; +} +.game-icon-missile-mech:before { + content: "\f968"; +} +.game-icon-missile-pod:before { + content: "\f969"; +} +.game-icon-missile-swarm:before { + content: "\f96a"; +} +.game-icon-mite-alt:before { + content: "\f96b"; +} +.game-icon-mite:before { + content: "\f96c"; +} +.game-icon-moai:before { + content: "\f96d"; +} +.game-icon-modern-city:before { + content: "\f96e"; +} +.game-icon-moebius-star:before { + content: "\f96f"; +} +.game-icon-moebius-trefoil:before { + content: "\f970"; +} +.game-icon-moebius-triangle:before { + content: "\f971"; +} +.game-icon-moka-pot:before { + content: "\f972"; +} +.game-icon-moldova:before { + content: "\f973"; +} +.game-icon-molecule-2:before { + content: "\f974"; +} +.game-icon-molecule:before { + content: "\f975"; +} +.game-icon-molotov:before { + content: "\f976"; +} +.game-icon-mona-lisa:before { + content: "\f977"; +} +.game-icon-moncler-jacket:before { + content: "\f978"; +} +.game-icon-money-stack:before { + content: "\f979"; +} +.game-icon-mongolia:before { + content: "\f97a"; +} +.game-icon-monk-face:before { + content: "\f97b"; +} +.game-icon-monkey-wrench:before { + content: "\f97c"; +} +.game-icon-monkey:before { + content: "\f97d"; +} +.game-icon-mono-wheel-robot:before { + content: "\f97e"; +} +.game-icon-monster-grasp:before { + content: "\f97f"; +} +.game-icon-monstera-leaf:before { + content: "\f980"; +} +.game-icon-monument-valley:before { + content: "\f981"; +} +.game-icon-moon-bats:before { + content: "\f982"; +} +.game-icon-moon-claws:before { + content: "\f983"; +} +.game-icon-moon-orbit:before { + content: "\f984"; +} +.game-icon-moon:before { + content: "\f985"; +} +.game-icon-mooring-bollard:before { + content: "\f986"; +} +.game-icon-morbid-humour:before { + content: "\f987"; +} +.game-icon-morgue-feet:before { + content: "\f988"; +} +.game-icon-morph-ball:before { + content: "\f989"; +} +.game-icon-mortar:before { + content: "\f98a"; +} +.game-icon-mountain-cave:before { + content: "\f98b"; +} +.game-icon-mountain-climbing:before { + content: "\f98c"; +} +.game-icon-mountain-road:before { + content: "\f98d"; +} +.game-icon-mountains:before { + content: "\f98e"; +} +.game-icon-mountaintop:before { + content: "\f98f"; +} +.game-icon-mounted-knight:before { + content: "\f990"; +} +.game-icon-mouse-2:before { + content: "\f991"; +} +.game-icon-mouse:before { + content: "\f992"; +} +.game-icon-mouth-watering:before { + content: "\f993"; +} +.game-icon-move:before { + content: "\f994"; +} +.game-icon-movement-sensor:before { + content: "\f995"; +} +.game-icon-mp-40:before { + content: "\f996"; +} +.game-icon-mp5-2:before { + content: "\f997"; +} +.game-icon-mp5:before { + content: "\f998"; +} +.game-icon-mp5k:before { + content: "\f999"; +} +.game-icon-mucous-pillar:before { + content: "\f99a"; +} +.game-icon-mug-shot:before { + content: "\f99b"; +} +.game-icon-multi-directions:before { + content: "\f99c"; +} +.game-icon-multiple-targets:before { + content: "\f99d"; +} +.game-icon-mummy-head:before { + content: "\f99e"; +} +.game-icon-muscle-fat:before { + content: "\f99f"; +} +.game-icon-muscle-up:before { + content: "\f9a0"; +} +.game-icon-muscular-torso:before { + content: "\f9a1"; +} +.game-icon-mushroom-cloud:before { + content: "\f9a2"; +} +.game-icon-mushroom-gills:before { + content: "\f9a3"; +} +.game-icon-mushroom-house:before { + content: "\f9a4"; +} +.game-icon-mushroom:before { + content: "\f9a5"; +} +.game-icon-mushrooms-cluster:before { + content: "\f9a6"; +} +.game-icon-mushrooms:before { + content: "\f9a7"; +} +.game-icon-music-spell:before { + content: "\f9a8"; +} +.game-icon-musical-keyboard:before { + content: "\f9a9"; +} +.game-icon-musical-notes:before { + content: "\f9aa"; +} +.game-icon-musical-score:before { + content: "\f9ab"; +} +.game-icon-musket:before { + content: "\f9ac"; +} +.game-icon-mussel:before { + content: "\f9ad"; +} +.game-icon-mustache:before { + content: "\f9ae"; +} +.game-icon-mute:before { + content: "\f9af"; +} +.game-icon-nachos:before { + content: "\f9b0"; +} +.game-icon-nailed-foot:before { + content: "\f9b1"; +} +.game-icon-nailed-head:before { + content: "\f9b2"; +} +.game-icon-nails:before { + content: "\f9b3"; +} +.game-icon-nano-bot:before { + content: "\f9b4"; +} +.game-icon-nautilus-shell:before { + content: "\f9b5"; +} +.game-icon-neck-bite:before { + content: "\f9b6"; +} +.game-icon-necklace-display:before { + content: "\f9b7"; +} +.game-icon-necklace:before { + content: "\f9b8"; +} +.game-icon-nectar:before { + content: "\f9b9"; +} +.game-icon-needle-drill:before { + content: "\f9ba"; +} +.game-icon-needle-jaws:before { + content: "\f9bb"; +} +.game-icon-nefertiti:before { + content: "\f9bc"; +} +.game-icon-nest-birds:before { + content: "\f9bd"; +} +.game-icon-nest-eggs:before { + content: "\f9be"; +} +.game-icon-nested-eclipses:before { + content: "\f9bf"; +} +.game-icon-nested-hearts:before { + content: "\f9c0"; +} +.game-icon-nested-hexagons:before { + content: "\f9c1"; +} +.game-icon-network-bars:before { + content: "\f9c2"; +} +.game-icon-new-born:before { + content: "\f9c3"; +} +.game-icon-new-shoot:before { + content: "\f9c4"; +} +.game-icon-newspaper:before { + content: "\f9c5"; +} +.game-icon-next-button:before { + content: "\f9c6"; +} +.game-icon-nigeria:before { + content: "\f9c7"; +} +.game-icon-night-sky:before { + content: "\f9c8"; +} +.game-icon-night-sleep:before { + content: "\f9c9"; +} +.game-icon-night-vision:before { + content: "\f9ca"; +} +.game-icon-ninja-armor:before { + content: "\f9cb"; +} +.game-icon-ninja-head:before { + content: "\f9cc"; +} +.game-icon-ninja-heroic-stance:before { + content: "\f9cd"; +} +.game-icon-ninja-mask:before { + content: "\f9ce"; +} +.game-icon-ninja-star:before { + content: "\f9cf"; +} +.game-icon-ninja-velociraptor:before { + content: "\f9d0"; +} +.game-icon-nodular:before { + content: "\f9d1"; +} +.game-icon-noodle-ball:before { + content: "\f9d2"; +} +.game-icon-noodles:before { + content: "\f9d3"; +} +.game-icon-north-star-shuriken:before { + content: "\f9d4"; +} +.game-icon-nose-front:before { + content: "\f9d5"; +} +.game-icon-nose-side:before { + content: "\f9d6"; +} +.game-icon-notebook:before { + content: "\f9d7"; +} +.game-icon-nothing-to-say:before { + content: "\f9d8"; +} +.game-icon-nuclear-bomb:before { + content: "\f9d9"; +} +.game-icon-nuclear-plant:before { + content: "\f9da"; +} +.game-icon-nuclear-waste:before { + content: "\f9db"; +} +.game-icon-nuclear:before { + content: "\f9dc"; +} +.game-icon-nun-face:before { + content: "\f9dd"; +} +.game-icon-nunchaku:before { + content: "\f9de"; +} +.game-icon-nurse-female:before { + content: "\f9df"; +} +.game-icon-nurse-male:before { + content: "\f9e0"; +} +.game-icon-o-brick:before { + content: "\f9e1"; +} +.game-icon-oak-leaf:before { + content: "\f9e2"; +} +.game-icon-oak:before { + content: "\f9e3"; +} +.game-icon-oasis:before { + content: "\f9e4"; +} +.game-icon-oat:before { + content: "\f9e5"; +} +.game-icon-obelisk:before { + content: "\f9e6"; +} +.game-icon-observatory:before { + content: "\f9e7"; +} +.game-icon-ocarina:before { + content: "\f9e8"; +} +.game-icon-occupy:before { + content: "\f9e9"; +} +.game-icon-octogonal-eye:before { + content: "\f9ea"; +} +.game-icon-octoman:before { + content: "\f9eb"; +} +.game-icon-octopus:before { + content: "\f9ec"; +} +.game-icon-oden:before { + content: "\f9ed"; +} +.game-icon-office-chair:before { + content: "\f9ee"; +} +.game-icon-offshore-platform:before { + content: "\f9ef"; +} +.game-icon-ogre:before { + content: "\f9f0"; +} +.game-icon-oil-drum:before { + content: "\f9f1"; +} +.game-icon-oil-pump:before { + content: "\f9f2"; +} +.game-icon-oil-rig:before { + content: "\f9f3"; +} +.game-icon-oily-spiral:before { + content: "\f9f4"; +} +.game-icon-old-king:before { + content: "\f9f5"; +} +.game-icon-old-lantern:before { + content: "\f9f6"; +} +.game-icon-old-microphone:before { + content: "\f9f7"; +} +.game-icon-old-wagon:before { + content: "\f9f8"; +} +.game-icon-olive:before { + content: "\f9f9"; +} +.game-icon-omega:before { + content: "\f9fa"; +} +.game-icon-on-sight:before { + content: "\f9fb"; +} +.game-icon-on-target:before { + content: "\f9fc"; +} +.game-icon-one-eyed:before { + content: "\f9fd"; +} +.game-icon-oni:before { + content: "\f9fe"; +} +.game-icon-onigori:before { + content: "\f9ff"; +} +.game-icon-open-book:before { + content: "\fa00"; +} +.game-icon-open-chest:before { + content: "\fa01"; +} +.game-icon-open-folder:before { + content: "\fa02"; +} +.game-icon-open-gate:before { + content: "\fa03"; +} +.game-icon-open-palm:before { + content: "\fa04"; +} +.game-icon-open-treasure-chest:before { + content: "\fa05"; +} +.game-icon-open-wound:before { + content: "\fa06"; +} +.game-icon-opened-food-can:before { + content: "\fa07"; +} +.game-icon-opening-shell:before { + content: "\fa08"; +} +.game-icon-ophiuchus:before { + content: "\fa09"; +} +.game-icon-oppidum:before { + content: "\fa0a"; +} +.game-icon-opposite-hearts:before { + content: "\fa0b"; +} +.game-icon-oppression:before { + content: "\fa0c"; +} +.game-icon-orange-slice:before { + content: "\fa0d"; +} +.game-icon-orange:before { + content: "\fa0e"; +} +.game-icon-orb-direction:before { + content: "\fa0f"; +} +.game-icon-orb-wand:before { + content: "\fa10"; +} +.game-icon-orbit:before { + content: "\fa11"; +} +.game-icon-orbital-rays:before { + content: "\fa12"; +} +.game-icon-orbital:before { + content: "\fa13"; +} +.game-icon-orc-head:before { + content: "\fa14"; +} +.game-icon-ore:before { + content: "\fa15"; +} +.game-icon-organigram:before { + content: "\fa16"; +} +.game-icon-ostrich:before { + content: "\fa17"; +} +.game-icon-ouroboros:before { + content: "\fa18"; +} +.game-icon-outback-hat:before { + content: "\fa19"; +} +.game-icon-over-infinity:before { + content: "\fa1a"; +} +.game-icon-overdose:before { + content: "\fa1b"; +} +.game-icon-overdrive:before { + content: "\fa1c"; +} +.game-icon-overhead:before { + content: "\fa1d"; +} +.game-icon-overkill:before { + content: "\fa1e"; +} +.game-icon-overlord-helm:before { + content: "\fa1f"; +} +.game-icon-overmind:before { + content: "\fa20"; +} +.game-icon-owl:before { + content: "\fa21"; +} +.game-icon-oyster-pearl:before { + content: "\fa22"; +} +.game-icon-oyster:before { + content: "\fa23"; +} +.game-icon-p90:before { + content: "\fa24"; +} +.game-icon-packed-planks:before { + content: "\fa25"; +} +.game-icon-paddle-steamer:before { + content: "\fa26"; +} +.game-icon-paddles:before { + content: "\fa27"; +} +.game-icon-padlock-open:before { + content: "\fa28"; +} +.game-icon-padlock:before { + content: "\fa29"; +} +.game-icon-pagoda:before { + content: "\fa2a"; +} +.game-icon-paint-brush:before { + content: "\fa2b"; +} +.game-icon-paint-bucket:before { + content: "\fa2c"; +} +.game-icon-paint-roller:before { + content: "\fa2d"; +} +.game-icon-painted-pottery:before { + content: "\fa2e"; +} +.game-icon-palette:before { + content: "\fa2f"; +} +.game-icon-palisade:before { + content: "\fa30"; +} +.game-icon-palm-tree:before { + content: "\fa31"; +} +.game-icon-palm:before { + content: "\fa32"; +} +.game-icon-pan-flute:before { + content: "\fa33"; +} +.game-icon-panda:before { + content: "\fa34"; +} +.game-icon-pangolin:before { + content: "\fa35"; +} +.game-icon-panzerfaust:before { + content: "\fa36"; +} +.game-icon-paper-arrow:before { + content: "\fa37"; +} +.game-icon-paper-bag-crumpled:before { + content: "\fa38"; +} +.game-icon-paper-bag-folded:before { + content: "\fa39"; +} +.game-icon-paper-bag-open:before { + content: "\fa3a"; +} +.game-icon-paper-boat:before { + content: "\fa3b"; +} +.game-icon-paper-bomb:before { + content: "\fa3c"; +} +.game-icon-paper-clip:before { + content: "\fa3d"; +} +.game-icon-paper-crane:before { + content: "\fa3e"; +} +.game-icon-paper-frog:before { + content: "\fa3f"; +} +.game-icon-paper-lantern:before { + content: "\fa40"; +} +.game-icon-paper-plane:before { + content: "\fa41"; +} +.game-icon-paper-tray:before { + content: "\fa42"; +} +.game-icon-paper-windmill:before { + content: "\fa43"; +} +.game-icon-paper:before { + content: "\fa44"; +} +.game-icon-papers:before { + content: "\fa45"; +} +.game-icon-papyrus:before { + content: "\fa46"; +} +.game-icon-parachute:before { + content: "\fa47"; +} +.game-icon-paraguay:before { + content: "\fa48"; +} +.game-icon-paranoia:before { + content: "\fa49"; +} +.game-icon-parasaurolophus:before { + content: "\fa4a"; +} +.game-icon-park-bench:before { + content: "\fa4b"; +} +.game-icon-parmecia:before { + content: "\fa4c"; +} +.game-icon-parrot-head:before { + content: "\fa4d"; +} +.game-icon-party-flags:before { + content: "\fa4e"; +} +.game-icon-party-hat:before { + content: "\fa4f"; +} +.game-icon-party-popper:before { + content: "\fa50"; +} +.game-icon-passport:before { + content: "\fa51"; +} +.game-icon-path-distance:before { + content: "\fa52"; +} +.game-icon-path-tile:before { + content: "\fa53"; +} +.game-icon-pauldrons:before { + content: "\fa54"; +} +.game-icon-pause-button:before { + content: "\fa55"; +} +.game-icon-paw-front:before { + content: "\fa56"; +} +.game-icon-paw-heart:before { + content: "\fa57"; +} +.game-icon-paw-print:before { + content: "\fa58"; +} +.game-icon-paw:before { + content: "\fa59"; +} +.game-icon-pawn:before { + content: "\fa5a"; +} +.game-icon-pawprint:before { + content: "\fa5b"; +} +.game-icon-pay-money:before { + content: "\fa5c"; +} +.game-icon-pc:before { + content: "\fa5d"; +} +.game-icon-peace-dove:before { + content: "\fa5e"; +} +.game-icon-peach:before { + content: "\fa5f"; +} +.game-icon-peaks:before { + content: "\fa60"; +} +.game-icon-peanut:before { + content: "\fa61"; +} +.game-icon-pear:before { + content: "\fa62"; +} +.game-icon-pearl-earring:before { + content: "\fa63"; +} +.game-icon-pearl-necklace:before { + content: "\fa64"; +} +.game-icon-peas:before { + content: "\fa65"; +} +.game-icon-pegasus:before { + content: "\fa66"; +} +.game-icon-pelvis-bone:before { + content: "\fa67"; +} +.game-icon-pencil-brush:before { + content: "\fa68"; +} +.game-icon-pencil-ruler:before { + content: "\fa69"; +} +.game-icon-pencil:before { + content: "\fa6a"; +} +.game-icon-pendant-key:before { + content: "\fa6b"; +} +.game-icon-pendulum-swing:before { + content: "\fa6c"; +} +.game-icon-penguin:before { + content: "\fa6d"; +} +.game-icon-pentacle:before { + content: "\fa6e"; +} +.game-icon-pentagram-rose:before { + content: "\fa6f"; +} +.game-icon-pentarrows-tornado:before { + content: "\fa70"; +} +.game-icon-perfume-bottle:before { + content: "\fa71"; +} +.game-icon-periscope:before { + content: "\fa72"; +} +.game-icon-perpendicular-rings:before { + content: "\fa73"; +} +.game-icon-person-in-bed:before { + content: "\fa74"; +} +.game-icon-person:before { + content: "\fa75"; +} +.game-icon-perspective-dice-five:before { + content: "\fa76"; +} +.game-icon-perspective-dice-four:before { + content: "\fa77"; +} +.game-icon-perspective-dice-one:before { + content: "\fa78"; +} +.game-icon-perspective-dice-six-faces-five:before { + content: "\fa79"; +} +.game-icon-perspective-dice-six-faces-four:before { + content: "\fa7a"; +} +.game-icon-perspective-dice-six-faces-one:before { + content: "\fa7b"; +} +.game-icon-perspective-dice-six-faces-random:before { + content: "\fa7c"; +} +.game-icon-perspective-dice-six-faces-six:before { + content: "\fa7d"; +} +.game-icon-perspective-dice-six-faces-three:before { + content: "\fa7e"; +} +.game-icon-perspective-dice-six-faces-two:before { + content: "\fa7f"; +} +.game-icon-perspective-dice-six:before { + content: "\fa80"; +} +.game-icon-perspective-dice-three:before { + content: "\fa81"; +} +.game-icon-perspective-dice-two:before { + content: "\fa82"; +} +.game-icon-peru:before { + content: "\fa83"; +} +.game-icon-pestle-mortar:before { + content: "\fa84"; +} +.game-icon-pharoah:before { + content: "\fa85"; +} +.game-icon-philosopher-bust:before { + content: "\fa86"; +} +.game-icon-phone:before { + content: "\fa87"; +} +.game-icon-photo-camera:before { + content: "\fa88"; +} +.game-icon-phrygian-cap:before { + content: "\fa89"; +} +.game-icon-pianist:before { + content: "\fa8a"; +} +.game-icon-piano-keys:before { + content: "\fa8b"; +} +.game-icon-pick-of-destiny:before { + content: "\fa8c"; +} +.game-icon-pickelhaube:before { + content: "\fa8d"; +} +.game-icon-pickle:before { + content: "\fa8e"; +} +.game-icon-pie-chart:before { + content: "\fa8f"; +} +.game-icon-pie-slice:before { + content: "\fa90"; +} +.game-icon-piece-skull:before { + content: "\fa91"; +} +.game-icon-pierced-body:before { + content: "\fa92"; +} +.game-icon-pierced-heart:before { + content: "\fa93"; +} +.game-icon-piercing-sword:before { + content: "\fa94"; +} +.game-icon-pig-face:before { + content: "\fa95"; +} +.game-icon-pig:before { + content: "\fa96"; +} +.game-icon-piggy-bank:before { + content: "\fa97"; +} +.game-icon-pikeman:before { + content: "\fa98"; +} +.game-icon-pilgrim-hat:before { + content: "\fa99"; +} +.game-icon-pill-drop:before { + content: "\fa9a"; +} +.game-icon-pill:before { + content: "\fa9b"; +} +.game-icon-pillow:before { + content: "\fa9c"; +} +.game-icon-pimiento:before { + content: "\fa9d"; +} +.game-icon-pin:before { + content: "\fa9e"; +} +.game-icon-pinata:before { + content: "\fa9f"; +} +.game-icon-pinball-flipper:before { + content: "\faa0"; +} +.game-icon-pincers:before { + content: "\faa1"; +} +.game-icon-pine-tree:before { + content: "\faa2"; +} +.game-icon-pineapple:before { + content: "\faa3"; +} +.game-icon-ping-pong-bat:before { + content: "\faa4"; +} +.game-icon-pipe-organ:before { + content: "\faa5"; +} +.game-icon-pipes:before { + content: "\faa6"; +} +.game-icon-piranha:before { + content: "\faa7"; +} +.game-icon-pirate-cannon:before { + content: "\faa8"; +} +.game-icon-pirate-captain:before { + content: "\faa9"; +} +.game-icon-pirate-coat:before { + content: "\faaa"; +} +.game-icon-pirate-flag:before { + content: "\faab"; +} +.game-icon-pirate-grave:before { + content: "\faac"; +} +.game-icon-pirate-hat:before { + content: "\faad"; +} +.game-icon-pirate-hook:before { + content: "\faae"; +} +.game-icon-pirate-skull:before { + content: "\faaf"; +} +.game-icon-pisa-tower:before { + content: "\fab0"; +} +.game-icon-pisces:before { + content: "\fab1"; +} +.game-icon-pistol-gun:before { + content: "\fab2"; +} +.game-icon-pitchfork:before { + content: "\fab3"; +} +.game-icon-pizza-cutter:before { + content: "\fab4"; +} +.game-icon-pizza-slice:before { + content: "\fab5"; +} +.game-icon-plague-doctor-profile:before { + content: "\fab6"; +} +.game-icon-plain-arrow:before { + content: "\fab7"; +} +.game-icon-plain-circle:before { + content: "\fab8"; +} +.game-icon-plain-dagger:before { + content: "\fab9"; +} +.game-icon-plain-square:before { + content: "\faba"; +} +.game-icon-plane-pilot:before { + content: "\fabb"; +} +.game-icon-plane-wing:before { + content: "\fabc"; +} +.game-icon-planet-conquest:before { + content: "\fabd"; +} +.game-icon-planet-core:before { + content: "\fabe"; +} +.game-icon-planks:before { + content: "\fabf"; +} +.game-icon-plant-roots:before { + content: "\fac0"; +} +.game-icon-plant-seed:before { + content: "\fac1"; +} +.game-icon-plant-watering:before { + content: "\fac2"; +} +.game-icon-plants-and-animals:before { + content: "\fac3"; +} +.game-icon-plasma-bolt:before { + content: "\fac4"; +} +.game-icon-plastic-duck:before { + content: "\fac5"; +} +.game-icon-plastron:before { + content: "\fac6"; +} +.game-icon-plate-claw:before { + content: "\fac7"; +} +.game-icon-platform:before { + content: "\fac8"; +} +.game-icon-play-button:before { + content: "\fac9"; +} +.game-icon-player-base:before { + content: "\faca"; +} +.game-icon-player-next:before { + content: "\facb"; +} +.game-icon-player-previous:before { + content: "\facc"; +} +.game-icon-player-time:before { + content: "\facd"; +} +.game-icon-plesiosaurus:before { + content: "\face"; +} +.game-icon-plow:before { + content: "\facf"; +} +.game-icon-plug:before { + content: "\fad0"; +} +.game-icon-plunger:before { + content: "\fad1"; +} +.game-icon-pocket-bow:before { + content: "\fad2"; +} +.game-icon-pocket-radio:before { + content: "\fad3"; +} +.game-icon-pocket-watch:before { + content: "\fad4"; +} +.game-icon-podium-second:before { + content: "\fad5"; +} +.game-icon-podium-third:before { + content: "\fad6"; +} +.game-icon-podium-winner:before { + content: "\fad7"; +} +.game-icon-podium:before { + content: "\fad8"; +} +.game-icon-pointing:before { + content: "\fad9"; +} +.game-icon-pointy-hat:before { + content: "\fada"; +} +.game-icon-pointy-sword:before { + content: "\fadb"; +} +.game-icon-poison-bottle:before { + content: "\fadc"; +} +.game-icon-poison-cloud:before { + content: "\fadd"; +} +.game-icon-poison-gas:before { + content: "\fade"; +} +.game-icon-poison:before { + content: "\fadf"; +} +.game-icon-pokecog:before { + content: "\fae0"; +} +.game-icon-poker-hand:before { + content: "\fae1"; +} +.game-icon-poland:before { + content: "\fae2"; +} +.game-icon-polar-bear:before { + content: "\fae3"; +} +.game-icon-polar-star:before { + content: "\fae4"; +} +.game-icon-police-badge:before { + content: "\fae5"; +} +.game-icon-police-car:before { + content: "\fae6"; +} +.game-icon-police-officer-head:before { + content: "\fae7"; +} +.game-icon-police-target:before { + content: "\fae8"; +} +.game-icon-pollen-dust:before { + content: "\fae9"; +} +.game-icon-polo-shirt:before { + content: "\faea"; +} +.game-icon-poncho:before { + content: "\faeb"; +} +.game-icon-pool-dive:before { + content: "\faec"; +} +.game-icon-pool-table-corner:before { + content: "\faed"; +} +.game-icon-pool-triangle:before { + content: "\faee"; +} +.game-icon-popcorn:before { + content: "\faef"; +} +.game-icon-pope-crown:before { + content: "\faf0"; +} +.game-icon-poppy:before { + content: "\faf1"; +} +.game-icon-porcelain-vase:before { + content: "\faf2"; +} +.game-icon-porcupine:before { + content: "\faf3"; +} +.game-icon-porcupinefish:before { + content: "\faf4"; +} +.game-icon-portal:before { + content: "\faf5"; +} +.game-icon-portculis:before { + content: "\faf6"; +} +.game-icon-portrait:before { + content: "\faf7"; +} +.game-icon-portugal:before { + content: "\faf8"; +} +.game-icon-position-marker:before { + content: "\faf9"; +} +.game-icon-post-office:before { + content: "\fafa"; +} +.game-icon-post-stamp:before { + content: "\fafb"; +} +.game-icon-potato:before { + content: "\fafc"; +} +.game-icon-potion-ball:before { + content: "\fafd"; +} +.game-icon-potion-of-madness:before { + content: "\fafe"; +} +.game-icon-pounce:before { + content: "\faff"; +} +.game-icon-pouring-chalice:before { + content: "\fb00"; +} +.game-icon-pouring-pot:before { + content: "\fb01"; +} +.game-icon-powder-bag:before { + content: "\fb02"; +} +.game-icon-powder:before { + content: "\fb03"; +} +.game-icon-power-button:before { + content: "\fb04"; +} +.game-icon-power-generator:before { + content: "\fb05"; +} +.game-icon-power-lightning:before { + content: "\fb06"; +} +.game-icon-power-ring:before { + content: "\fb07"; +} +.game-icon-prayer-beads:before { + content: "\fb08"; +} +.game-icon-prayer:before { + content: "\fb09"; +} +.game-icon-praying-mantis:before { + content: "\fb0a"; +} +.game-icon-present:before { + content: "\fb0b"; +} +.game-icon-pressure-cooker:before { + content: "\fb0c"; +} +.game-icon-pretty-fangs:before { + content: "\fb0d"; +} +.game-icon-pretzel:before { + content: "\fb0e"; +} +.game-icon-previous-button:before { + content: "\fb0f"; +} +.game-icon-price-tag:before { + content: "\fb10"; +} +.game-icon-primitive-necklace:before { + content: "\fb11"; +} +.game-icon-primitive-torch:before { + content: "\fb12"; +} +.game-icon-prism:before { + content: "\fb13"; +} +.game-icon-prisoner:before { + content: "\fb14"; +} +.game-icon-private-first-class:before { + content: "\fb15"; +} +.game-icon-private:before { + content: "\fb16"; +} +.game-icon-processor:before { + content: "\fb17"; +} +.game-icon-profit:before { + content: "\fb18"; +} +.game-icon-progression:before { + content: "\fb19"; +} +.game-icon-propeller-beanie:before { + content: "\fb1a"; +} +.game-icon-protection-glasses:before { + content: "\fb1b"; +} +.game-icon-pschent-double-crown:before { + content: "\fb1c"; +} +.game-icon-psychic-waves:before { + content: "\fb1d"; +} +.game-icon-pterodactylus:before { + content: "\fb1e"; +} +.game-icon-pteruges:before { + content: "\fb1f"; +} +.game-icon-public-speaker:before { + content: "\fb20"; +} +.game-icon-pull:before { + content: "\fb21"; +} +.game-icon-pulley-hook:before { + content: "\fb22"; +} +.game-icon-pulse:before { + content: "\fb23"; +} +.game-icon-pummeled:before { + content: "\fb24"; +} +.game-icon-pumpkin-lantern:before { + content: "\fb25"; +} +.game-icon-pumpkin-mask:before { + content: "\fb26"; +} +.game-icon-pumpkin:before { + content: "\fb27"; +} +.game-icon-punch-blast:before { + content: "\fb28"; +} +.game-icon-punch:before { + content: "\fb29"; +} +.game-icon-punching-bag:before { + content: "\fb2a"; +} +.game-icon-puppet:before { + content: "\fb2b"; +} +.game-icon-purple-tentacle:before { + content: "\fb2c"; +} +.game-icon-push:before { + content: "\fb2d"; +} +.game-icon-puzzle:before { + content: "\fb2e"; +} +.game-icon-pylon:before { + content: "\fb2f"; +} +.game-icon-pyre:before { + content: "\fb30"; +} +.game-icon-pyromaniac:before { + content: "\fb31"; +} +.game-icon-qaitbay-citadel:before { + content: "\fb32"; +} +.game-icon-quake-stomp:before { + content: "\fb33"; +} +.game-icon-queen-crown:before { + content: "\fb34"; +} +.game-icon-quick-man:before { + content: "\fb35"; +} +.game-icon-quick-slash:before { + content: "\fb36"; +} +.game-icon-quicksand:before { + content: "\fb37"; +} +.game-icon-quill-ink:before { + content: "\fb38"; +} +.game-icon-quill:before { + content: "\fb39"; +} +.game-icon-quiver:before { + content: "\fb3a"; +} +.game-icon-rabbit-head:before { + content: "\fb3b"; +} +.game-icon-rabbit:before { + content: "\fb3c"; +} +.game-icon-raccoon-head:before { + content: "\fb3d"; +} +.game-icon-race-car:before { + content: "\fb3e"; +} +.game-icon-radar-cross-section:before { + content: "\fb3f"; +} +.game-icon-radar-dish:before { + content: "\fb40"; +} +.game-icon-radar-sweep:before { + content: "\fb41"; +} +.game-icon-raddish:before { + content: "\fb42"; +} +.game-icon-radial-balance:before { + content: "\fb43"; +} +.game-icon-radiations:before { + content: "\fb44"; +} +.game-icon-radio-tower:before { + content: "\fb45"; +} +.game-icon-radioactive:before { + content: "\fb46"; +} +.game-icon-raft:before { + content: "\fb47"; +} +.game-icon-ragged-wound:before { + content: "\fb48"; +} +.game-icon-rail-road:before { + content: "\fb49"; +} +.game-icon-railway:before { + content: "\fb4a"; +} +.game-icon-rainbow-star:before { + content: "\fb4b"; +} +.game-icon-raining:before { + content: "\fb4c"; +} +.game-icon-raise-skeleton:before { + content: "\fb4d"; +} +.game-icon-raise-zombie:before { + content: "\fb4e"; +} +.game-icon-rake:before { + content: "\fb4f"; +} +.game-icon-rally-the-troops:before { + content: "\fb50"; +} +.game-icon-ram-2:before { + content: "\fb51"; +} +.game-icon-ram-profile:before { + content: "\fb52"; +} +.game-icon-ram:before { + content: "\fb53"; +} +.game-icon-ranch-gate:before { + content: "\fb54"; +} +.game-icon-rank-1:before { + content: "\fb55"; +} +.game-icon-rank-2:before { + content: "\fb56"; +} +.game-icon-rank-3:before { + content: "\fb57"; +} +.game-icon-rapidshare-arrow:before { + content: "\fb58"; +} +.game-icon-raspberry:before { + content: "\fb59"; +} +.game-icon-rat:before { + content: "\fb5a"; +} +.game-icon-rattlesnake:before { + content: "\fb5b"; +} +.game-icon-raven:before { + content: "\fb5c"; +} +.game-icon-raw-egg:before { + content: "\fb5d"; +} +.game-icon-ray-gun:before { + content: "\fb5e"; +} +.game-icon-razor-blade:before { + content: "\fb5f"; +} +.game-icon-razor:before { + content: "\fb60"; +} +.game-icon-reactor:before { + content: "\fb61"; +} +.game-icon-read:before { + content: "\fb62"; +} +.game-icon-reaper-scythe:before { + content: "\fb63"; +} +.game-icon-rear-aura:before { + content: "\fb64"; +} +.game-icon-receive-money:before { + content: "\fb65"; +} +.game-icon-recycle:before { + content: "\fb66"; +} +.game-icon-red-carpet:before { + content: "\fb67"; +} +.game-icon-reed:before { + content: "\fb68"; +} +.game-icon-refinery:before { + content: "\fb69"; +} +.game-icon-regeneration:before { + content: "\fb6a"; +} +.game-icon-relationship-bounds:before { + content: "\fb6b"; +} +.game-icon-relic-blade:before { + content: "\fb6c"; +} +.game-icon-reload-gun-barrel:before { + content: "\fb6d"; +} +.game-icon-remedy:before { + content: "\fb6e"; +} +.game-icon-rempart:before { + content: "\fb6f"; +} +.game-icon-reptile-tail:before { + content: "\fb70"; +} +.game-icon-resize:before { + content: "\fb71"; +} +.game-icon-resonance:before { + content: "\fb72"; +} +.game-icon-resting-vampire:before { + content: "\fb73"; +} +.game-icon-reticule:before { + content: "\fb74"; +} +.game-icon-retro-controller:before { + content: "\fb75"; +} +.game-icon-return-arrow:before { + content: "\fb76"; +} +.game-icon-revolt-2:before { + content: "\fb77"; +} +.game-icon-revolt:before { + content: "\fb78"; +} +.game-icon-revolver-2:before { + content: "\fb79"; +} +.game-icon-revolver:before { + content: "\fb7a"; +} +.game-icon-rhinoceros-horn:before { + content: "\fb7b"; +} +.game-icon-rialto-bridge:before { + content: "\fb7c"; +} +.game-icon-ribbon-medal:before { + content: "\fb7d"; +} +.game-icon-ribbon-shield:before { + content: "\fb7e"; +} +.game-icon-ribbon:before { + content: "\fb7f"; +} +.game-icon-ribcage:before { + content: "\fb80"; +} +.game-icon-rice-cooker:before { + content: "\fb81"; +} +.game-icon-rifle:before { + content: "\fb82"; +} +.game-icon-ring-box:before { + content: "\fb83"; +} +.game-icon-ring-mould:before { + content: "\fb84"; +} +.game-icon-ring:before { + content: "\fb85"; +} +.game-icon-ringed-beam:before { + content: "\fb86"; +} +.game-icon-ringed-planet:before { + content: "\fb87"; +} +.game-icon-ringing-alarm:before { + content: "\fb88"; +} +.game-icon-ringing-bell:before { + content: "\fb89"; +} +.game-icon-ringmaster:before { + content: "\fb8a"; +} +.game-icon-riot-shield:before { + content: "\fb8b"; +} +.game-icon-riposte:before { + content: "\fb8c"; +} +.game-icon-river:before { + content: "\fb8d"; +} +.game-icon-road:before { + content: "\fb8e"; +} +.game-icon-roast-chicken:before { + content: "\fb8f"; +} +.game-icon-robber-hand:before { + content: "\fb90"; +} +.game-icon-robber-mask:before { + content: "\fb91"; +} +.game-icon-robber:before { + content: "\fb92"; +} +.game-icon-robe:before { + content: "\fb93"; +} +.game-icon-robin-hood-hat:before { + content: "\fb94"; +} +.game-icon-robot-antennas:before { + content: "\fb95"; +} +.game-icon-robot-golem:before { + content: "\fb96"; +} +.game-icon-robot-grab:before { + content: "\fb97"; +} +.game-icon-robot-helmet:before { + content: "\fb98"; +} +.game-icon-robot-leg:before { + content: "\fb99"; +} +.game-icon-rock-2:before { + content: "\fb9a"; +} +.game-icon-rock-golem:before { + content: "\fb9b"; +} +.game-icon-rock:before { + content: "\fb9c"; +} +.game-icon-rocket-flight:before { + content: "\fb9d"; +} +.game-icon-rocket-thruster:before { + content: "\fb9e"; +} +.game-icon-rocket:before { + content: "\fb9f"; +} +.game-icon-rocking-chair:before { + content: "\fba0"; +} +.game-icon-rod-of-asclepius:before { + content: "\fba1"; +} +.game-icon-rogue:before { + content: "\fba2"; +} +.game-icon-rolled-cloth:before { + content: "\fba3"; +} +.game-icon-roller-skate:before { + content: "\fba4"; +} +.game-icon-rolling-bomb:before { + content: "\fba5"; +} +.game-icon-rolling-dice-cup:before { + content: "\fba6"; +} +.game-icon-rolling-dices:before { + content: "\fba7"; +} +.game-icon-rolling-energy:before { + content: "\fba8"; +} +.game-icon-rolling-suitcase:before { + content: "\fba9"; +} +.game-icon-roman-shield:before { + content: "\fbaa"; +} +.game-icon-roman-toga:before { + content: "\fbab"; +} +.game-icon-rooster:before { + content: "\fbac"; +} +.game-icon-root-tip:before { + content: "\fbad"; +} +.game-icon-rope-bridge:before { + content: "\fbae"; +} +.game-icon-rope-coil:before { + content: "\fbaf"; +} +.game-icon-rope-dart:before { + content: "\fbb0"; +} +.game-icon-ropeway:before { + content: "\fbb1"; +} +.game-icon-rosa-shield:before { + content: "\fbb2"; +} +.game-icon-rose:before { + content: "\fbb3"; +} +.game-icon-rotary-phone:before { + content: "\fbb4"; +} +.game-icon-rough-wound:before { + content: "\fbb5"; +} +.game-icon-round-bottom-flask:before { + content: "\fbb6"; +} +.game-icon-round-knob:before { + content: "\fbb7"; +} +.game-icon-round-shield:before { + content: "\fbb8"; +} +.game-icon-round-silo:before { + content: "\fbb9"; +} +.game-icon-round-star:before { + content: "\fbba"; +} +.game-icon-round-straw-bale:before { + content: "\fbbb"; +} +.game-icon-round-struck:before { + content: "\fbbc"; +} +.game-icon-round-table:before { + content: "\fbbd"; +} +.game-icon-royal-love:before { + content: "\fbbe"; +} +.game-icon-rss-2:before { + content: "\fbbf"; +} +.game-icon-rss:before { + content: "\fbc0"; +} +.game-icon-rub-el-hizb:before { + content: "\fbc1"; +} +.game-icon-rubber-boot:before { + content: "\fbc2"; +} +.game-icon-rugby-conversion:before { + content: "\fbc3"; +} +.game-icon-rule-book:before { + content: "\fbc4"; +} +.game-icon-run:before { + content: "\fbc5"; +} +.game-icon-rune-stone:before { + content: "\fbc6"; +} +.game-icon-rune-sword:before { + content: "\fbc7"; +} +.game-icon-running-ninja:before { + content: "\fbc8"; +} +.game-icon-running-shoe:before { + content: "\fbc9"; +} +.game-icon-rupee:before { + content: "\fbca"; +} +.game-icon-rusty-sword:before { + content: "\fbcb"; +} +.game-icon-s-brick:before { + content: "\fbcc"; +} +.game-icon-saber-and-pistol:before { + content: "\fbcd"; +} +.game-icon-saber-slash:before { + content: "\fbce"; +} +.game-icon-saber-tooth:before { + content: "\fbcf"; +} +.game-icon-saber-toothed-cat-head:before { + content: "\fbd0"; +} +.game-icon-sabers-choc:before { + content: "\fbd1"; +} +.game-icon-sacrificial-dagger:before { + content: "\fbd2"; +} +.game-icon-sad-crab:before { + content: "\fbd3"; +} +.game-icon-saddle:before { + content: "\fbd4"; +} +.game-icon-safety-pin:before { + content: "\fbd5"; +} +.game-icon-sagittarius:before { + content: "\fbd6"; +} +.game-icon-sai:before { + content: "\fbd7"; +} +.game-icon-sail:before { + content: "\fbd8"; +} +.game-icon-sailboat:before { + content: "\fbd9"; +} +.game-icon-saint-basil-cathedral:before { + content: "\fbda"; +} +.game-icon-saiyan-suit:before { + content: "\fbdb"; +} +.game-icon-salamander:before { + content: "\fbdc"; +} +.game-icon-salmon:before { + content: "\fbdd"; +} +.game-icon-saloon-doors:before { + content: "\fbde"; +} +.game-icon-saloon:before { + content: "\fbdf"; +} +.game-icon-salt-shaker:before { + content: "\fbe0"; +} +.game-icon-samara-mosque:before { + content: "\fbe1"; +} +.game-icon-samurai-helmet:before { + content: "\fbe2"; +} +.game-icon-samus-helmet:before { + content: "\fbe3"; +} +.game-icon-sand-castle:before { + content: "\fbe4"; +} +.game-icon-sand-snake:before { + content: "\fbe5"; +} +.game-icon-sandal:before { + content: "\fbe6"; +} +.game-icon-sands-of-time:before { + content: "\fbe7"; +} +.game-icon-sandstorm:before { + content: "\fbe8"; +} +.game-icon-sandwich:before { + content: "\fbe9"; +} +.game-icon-santa-hat:before { + content: "\fbea"; +} +.game-icon-saphir:before { + content: "\fbeb"; +} +.game-icon-sarcophagus:before { + content: "\fbec"; +} +.game-icon-sasquatch:before { + content: "\fbed"; +} +.game-icon-satellite-communication:before { + content: "\fbee"; +} +.game-icon-sattelite:before { + content: "\fbef"; +} +.game-icon-saucepan:before { + content: "\fbf0"; +} +.game-icon-sauropod-head:before { + content: "\fbf1"; +} +.game-icon-sauropod-skeleton:before { + content: "\fbf2"; +} +.game-icon-sausage:before { + content: "\fbf3"; +} +.game-icon-sausages-ribbon:before { + content: "\fbf4"; +} +.game-icon-save-arrow:before { + content: "\fbf5"; +} +.game-icon-save:before { + content: "\fbf6"; +} +.game-icon-saw-claw:before { + content: "\fbf7"; +} +.game-icon-sawed-off-shotgun:before { + content: "\fbf8"; +} +.game-icon-saxophone:before { + content: "\fbf9"; +} +.game-icon-scabbard:before { + content: "\fbfa"; +} +.game-icon-scale-mail:before { + content: "\fbfb"; +} +.game-icon-scales:before { + content: "\fbfc"; +} +.game-icon-scallop:before { + content: "\fbfd"; +} +.game-icon-scalpel-strike:before { + content: "\fbfe"; +} +.game-icon-scalpel:before { + content: "\fbff"; +} +.game-icon-scar-wound:before { + content: "\fc00"; +} +.game-icon-scarab-beetle:before { + content: "\fc01"; +} +.game-icon-scarecrow:before { + content: "\fc02"; +} +.game-icon-school-bag:before { + content: "\fc03"; +} +.game-icon-school-of-fish:before { + content: "\fc04"; +} +.game-icon-scissors-2:before { + content: "\fc05"; +} +.game-icon-scissors:before { + content: "\fc06"; +} +.game-icon-scooter:before { + content: "\fc07"; +} +.game-icon-scorpio:before { + content: "\fc08"; +} +.game-icon-scorpion-tail:before { + content: "\fc09"; +} +.game-icon-scorpion:before { + content: "\fc0a"; +} +.game-icon-scout-ship:before { + content: "\fc0b"; +} +.game-icon-screaming:before { + content: "\fc0c"; +} +.game-icon-screen-impact:before { + content: "\fc0d"; +} +.game-icon-screw-2:before { + content: "\fc0e"; +} +.game-icon-screw:before { + content: "\fc0f"; +} +.game-icon-screwdriver:before { + content: "\fc10"; +} +.game-icon-scroll-quill:before { + content: "\fc11"; +} +.game-icon-scroll-unfurled:before { + content: "\fc12"; +} +.game-icon-scuba-mask:before { + content: "\fc13"; +} +.game-icon-scuba-tanks:before { + content: "\fc14"; +} +.game-icon-scythe:before { + content: "\fc15"; +} +.game-icon-sea-cliff:before { + content: "\fc16"; +} +.game-icon-sea-creature:before { + content: "\fc17"; +} +.game-icon-sea-dragon:before { + content: "\fc18"; +} +.game-icon-sea-serpent:before { + content: "\fc19"; +} +.game-icon-sea-star:before { + content: "\fc1a"; +} +.game-icon-sea-turtle:before { + content: "\fc1b"; +} +.game-icon-seagull:before { + content: "\fc1c"; +} +.game-icon-seahorse:before { + content: "\fc1d"; +} +.game-icon-seated-mouse:before { + content: "\fc1e"; +} +.game-icon-secret-book:before { + content: "\fc1f"; +} +.game-icon-secret-door:before { + content: "\fc20"; +} +.game-icon-security-gate:before { + content: "\fc21"; +} +.game-icon-seedling:before { + content: "\fc22"; +} +.game-icon-select:before { + content: "\fc23"; +} +.game-icon-self-love:before { + content: "\fc24"; +} +.game-icon-sell-card:before { + content: "\fc25"; +} +.game-icon-semi-closed-eye:before { + content: "\fc26"; +} +.game-icon-sensuousness:before { + content: "\fc27"; +} +.game-icon-sentry-gun:before { + content: "\fc28"; +} +.game-icon-sergeant:before { + content: "\fc29"; +} +.game-icon-serrated-slash:before { + content: "\fc2a"; +} +.game-icon-server-rack:before { + content: "\fc2b"; +} +.game-icon-sesame:before { + content: "\fc2c"; +} +.game-icon-settings-knobs:before { + content: "\fc2d"; +} +.game-icon-seven-pointed-star:before { + content: "\fc2e"; +} +.game-icon-severed-hand:before { + content: "\fc2f"; +} +.game-icon-sewed-shell:before { + content: "\fc30"; +} +.game-icon-sewing-machine:before { + content: "\fc31"; +} +.game-icon-sewing-needle:before { + content: "\fc32"; +} +.game-icon-sewing-string:before { + content: "\fc33"; +} +.game-icon-sextant:before { + content: "\fc34"; +} +.game-icon-shadow-follower:before { + content: "\fc35"; +} +.game-icon-shadow-grasp:before { + content: "\fc36"; +} +.game-icon-shaking-hands:before { + content: "\fc37"; +} +.game-icon-shambling-mound:before { + content: "\fc38"; +} +.game-icon-shambling-zombie:before { + content: "\fc39"; +} +.game-icon-shamrock:before { + content: "\fc3a"; +} +.game-icon-shard-sword:before { + content: "\fc3b"; +} +.game-icon-share:before { + content: "\fc3c"; +} +.game-icon-shark-bite:before { + content: "\fc3d"; +} +.game-icon-shark-fin:before { + content: "\fc3e"; +} +.game-icon-shark-jaws:before { + content: "\fc3f"; +} +.game-icon-sharp-axe:before { + content: "\fc40"; +} +.game-icon-sharp-crown:before { + content: "\fc41"; +} +.game-icon-sharp-halberd:before { + content: "\fc42"; +} +.game-icon-sharp-lips:before { + content: "\fc43"; +} +.game-icon-sharp-shuriken:before { + content: "\fc44"; +} +.game-icon-sharp-smile:before { + content: "\fc45"; +} +.game-icon-sharped-teeth-skull:before { + content: "\fc46"; +} +.game-icon-shatter:before { + content: "\fc47"; +} +.game-icon-shattered-glass:before { + content: "\fc48"; +} +.game-icon-shattered-heart:before { + content: "\fc49"; +} +.game-icon-shattered-sword:before { + content: "\fc4a"; +} +.game-icon-shears:before { + content: "\fc4b"; +} +.game-icon-sheep:before { + content: "\fc4c"; +} +.game-icon-sheikah-eye:before { + content: "\fc4d"; +} +.game-icon-shepherds-crook:before { + content: "\fc4e"; +} +.game-icon-sherlock-holmes:before { + content: "\fc4f"; +} +.game-icon-shield-bash:before { + content: "\fc50"; +} +.game-icon-shield-bounces:before { + content: "\fc51"; +} +.game-icon-shield-disabled:before { + content: "\fc52"; +} +.game-icon-shield-echoes:before { + content: "\fc53"; +} +.game-icon-shield-impact:before { + content: "\fc54"; +} +.game-icon-shield-opposition:before { + content: "\fc55"; +} +.game-icon-shield-reflect:before { + content: "\fc56"; +} +.game-icon-shield:before { + content: "\fc57"; +} +.game-icon-shieldcomb:before { + content: "\fc58"; +} +.game-icon-shining-claw:before { + content: "\fc59"; +} +.game-icon-shining-heart:before { + content: "\fc5a"; +} +.game-icon-shining-sword:before { + content: "\fc5b"; +} +.game-icon-shinto-shrine-mirror:before { + content: "\fc5c"; +} +.game-icon-shinto-shrine:before { + content: "\fc5d"; +} +.game-icon-shiny-apple:before { + content: "\fc5e"; +} +.game-icon-shiny-entrance:before { + content: "\fc5f"; +} +.game-icon-shiny-iris:before { + content: "\fc60"; +} +.game-icon-shiny-omega:before { + content: "\fc61"; +} +.game-icon-shiny-purse:before { + content: "\fc62"; +} +.game-icon-ship-bow:before { + content: "\fc63"; +} +.game-icon-ship-wheel:before { + content: "\fc64"; +} +.game-icon-ship-wreck:before { + content: "\fc65"; +} +.game-icon-shirt-button:before { + content: "\fc66"; +} +.game-icon-shirt:before { + content: "\fc67"; +} +.game-icon-shoebill-stork:before { + content: "\fc68"; +} +.game-icon-shooner-sailboat:before { + content: "\fc69"; +} +.game-icon-shop:before { + content: "\fc6a"; +} +.game-icon-shopping-bag:before { + content: "\fc6b"; +} +.game-icon-shopping-cart:before { + content: "\fc6c"; +} +.game-icon-shorts:before { + content: "\fc6d"; +} +.game-icon-shotgun-rounds:before { + content: "\fc6e"; +} +.game-icon-shotgun:before { + content: "\fc6f"; +} +.game-icon-shoulder-armor:before { + content: "\fc70"; +} +.game-icon-shoulder-bag:before { + content: "\fc71"; +} +.game-icon-shoulder-scales:before { + content: "\fc72"; +} +.game-icon-shouting:before { + content: "\fc73"; +} +.game-icon-shower:before { + content: "\fc74"; +} +.game-icon-shrimp:before { + content: "\fc75"; +} +.game-icon-shrug:before { + content: "\fc76"; +} +.game-icon-shuriken-2:before { + content: "\fc77"; +} +.game-icon-shuriken-aperture:before { + content: "\fc78"; +} +.game-icon-shuriken:before { + content: "\fc79"; +} +.game-icon-shut-rose:before { + content: "\fc7a"; +} +.game-icon-shuttlecock:before { + content: "\fc7b"; +} +.game-icon-sickle:before { + content: "\fc7c"; +} +.game-icon-sideswipe:before { + content: "\fc7d"; +} +.game-icon-siege-ram:before { + content: "\fc7e"; +} +.game-icon-siege-tower:before { + content: "\fc7f"; +} +.game-icon-sight-disabled:before { + content: "\fc80"; +} +.game-icon-silence:before { + content: "\fc81"; +} +.game-icon-silenced:before { + content: "\fc82"; +} +.game-icon-silex:before { + content: "\fc83"; +} +.game-icon-silver-bullet:before { + content: "\fc84"; +} +.game-icon-sinagot:before { + content: "\fc85"; +} +.game-icon-sing:before { + content: "\fc86"; +} +.game-icon-sinking-ship:before { + content: "\fc87"; +} +.game-icon-sinking-trap:before { + content: "\fc88"; +} +.game-icon-sinusoidal-beam:before { + content: "\fc89"; +} +.game-icon-siren:before { + content: "\fc8a"; +} +.game-icon-sitting-dog:before { + content: "\fc8b"; +} +.game-icon-six-eyes:before { + content: "\fc8c"; +} +.game-icon-skateboard:before { + content: "\fc8d"; +} +.game-icon-skeletal-hand:before { + content: "\fc8e"; +} +.game-icon-skeleton-inside:before { + content: "\fc8f"; +} +.game-icon-skeleton-key:before { + content: "\fc90"; +} +.game-icon-skeleton:before { + content: "\fc91"; +} +.game-icon-ski-boot:before { + content: "\fc92"; +} +.game-icon-skid-mark:before { + content: "\fc93"; +} +.game-icon-skier:before { + content: "\fc94"; +} +.game-icon-skills:before { + content: "\fc95"; +} +.game-icon-skipping-rope:before { + content: "\fc96"; +} +.game-icon-skirt:before { + content: "\fc97"; +} +.game-icon-skis:before { + content: "\fc98"; +} +.game-icon-skull-bolt:before { + content: "\fc99"; +} +.game-icon-skull-crack:before { + content: "\fc9a"; +} +.game-icon-skull-crossed-bones:before { + content: "\fc9b"; +} +.game-icon-skull-in-jar:before { + content: "\fc9c"; +} +.game-icon-skull-mask:before { + content: "\fc9d"; +} +.game-icon-skull-ring:before { + content: "\fc9e"; +} +.game-icon-skull-sabertooth:before { + content: "\fc9f"; +} +.game-icon-skull-shield:before { + content: "\fca0"; +} +.game-icon-skull-signet:before { + content: "\fca1"; +} +.game-icon-skull-slices:before { + content: "\fca2"; +} +.game-icon-skull-staff:before { + content: "\fca3"; +} +.game-icon-skull-with-syringe:before { + content: "\fca4"; +} +.game-icon-slalom:before { + content: "\fca5"; +} +.game-icon-slap:before { + content: "\fca6"; +} +.game-icon-slashed-shield:before { + content: "\fca7"; +} +.game-icon-slavery-whip:before { + content: "\fca8"; +} +.game-icon-sleeping-bag:before { + content: "\fca9"; +} +.game-icon-sleepy:before { + content: "\fcaa"; +} +.game-icon-sleeveless-jacket:before { + content: "\fcab"; +} +.game-icon-sleeveless-top:before { + content: "\fcac"; +} +.game-icon-sliced-bread:before { + content: "\fcad"; +} +.game-icon-sliced-mushroom:before { + content: "\fcae"; +} +.game-icon-sliced-sausage:before { + content: "\fcaf"; +} +.game-icon-slicing-arrow:before { + content: "\fcb0"; +} +.game-icon-slime:before { + content: "\fcb1"; +} +.game-icon-sling:before { + content: "\fcb2"; +} +.game-icon-slingshot:before { + content: "\fcb3"; +} +.game-icon-slipknot:before { + content: "\fcb4"; +} +.game-icon-slippers:before { + content: "\fcb5"; +} +.game-icon-slot-machine:before { + content: "\fcb6"; +} +.game-icon-sloth:before { + content: "\fcb7"; +} +.game-icon-slow-blob:before { + content: "\fcb8"; +} +.game-icon-slumbering-sanctuary:before { + content: "\fcb9"; +} +.game-icon-sly:before { + content: "\fcba"; +} +.game-icon-small-fire:before { + content: "\fcbb"; +} +.game-icon-small-fishing-sailboat:before { + content: "\fcbc"; +} +.game-icon-smart:before { + content: "\fcbd"; +} +.game-icon-smartphone-2:before { + content: "\fcbe"; +} +.game-icon-smartphone:before { + content: "\fcbf"; +} +.game-icon-smash-arrows:before { + content: "\fcc0"; +} +.game-icon-smitten:before { + content: "\fcc1"; +} +.game-icon-smoke-bomb:before { + content: "\fcc2"; +} +.game-icon-smoking-finger:before { + content: "\fcc3"; +} +.game-icon-smoking-orb:before { + content: "\fcc4"; +} +.game-icon-smoking-pipe:before { + content: "\fcc5"; +} +.game-icon-smoking-volcano:before { + content: "\fcc6"; +} +.game-icon-snail-eyes:before { + content: "\fcc7"; +} +.game-icon-snail:before { + content: "\fcc8"; +} +.game-icon-snake-bite:before { + content: "\fcc9"; +} +.game-icon-snake-egg:before { + content: "\fcca"; +} +.game-icon-snake-jar:before { + content: "\fccb"; +} +.game-icon-snake-spiral:before { + content: "\fccc"; +} +.game-icon-snake-tongue:before { + content: "\fccd"; +} +.game-icon-snake-totem:before { + content: "\fcce"; +} +.game-icon-snake:before { + content: "\fccf"; +} +.game-icon-snatch:before { + content: "\fcd0"; +} +.game-icon-sniffing-dog:before { + content: "\fcd1"; +} +.game-icon-snitch-quidditch-ball:before { + content: "\fcd2"; +} +.game-icon-snorkel:before { + content: "\fcd3"; +} +.game-icon-snout:before { + content: "\fcd4"; +} +.game-icon-snow-bottle:before { + content: "\fcd5"; +} +.game-icon-snowboard:before { + content: "\fcd6"; +} +.game-icon-snowflake-1:before { + content: "\fcd7"; +} +.game-icon-snowflake-2:before { + content: "\fcd8"; +} +.game-icon-snowing:before { + content: "\fcd9"; +} +.game-icon-snowman:before { + content: "\fcda"; +} +.game-icon-soap-experiment:before { + content: "\fcdb"; +} +.game-icon-soap:before { + content: "\fcdc"; +} +.game-icon-soccer-ball:before { + content: "\fcdd"; +} +.game-icon-soccer-field:before { + content: "\fcde"; +} +.game-icon-soccer-kick:before { + content: "\fcdf"; +} +.game-icon-socks:before { + content: "\fce0"; +} +.game-icon-soda-can:before { + content: "\fce1"; +} +.game-icon-sofa:before { + content: "\fce2"; +} +.game-icon-solar-power:before { + content: "\fce3"; +} +.game-icon-solar-system:before { + content: "\fce4"; +} +.game-icon-solar-time:before { + content: "\fce5"; +} +.game-icon-soldering-iron:before { + content: "\fce6"; +} +.game-icon-solid-leaf:before { + content: "\fce7"; +} +.game-icon-sombrero:before { + content: "\fce8"; +} +.game-icon-sonic-boom:before { + content: "\fce9"; +} +.game-icon-sonic-lightning:before { + content: "\fcea"; +} +.game-icon-sonic-screech:before { + content: "\fceb"; +} +.game-icon-sonic-shoes:before { + content: "\fcec"; +} +.game-icon-sonic-shout:before { + content: "\fced"; +} +.game-icon-soul-vessel:before { + content: "\fcee"; +} +.game-icon-sound-off:before { + content: "\fcef"; +} +.game-icon-sound-on:before { + content: "\fcf0"; +} +.game-icon-sound-waves:before { + content: "\fcf1"; +} +.game-icon-south-africa-flag:before { + content: "\fcf2"; +} +.game-icon-south-africa:before { + content: "\fcf3"; +} +.game-icon-south-america:before { + content: "\fcf4"; +} +.game-icon-south-korea:before { + content: "\fcf5"; +} +.game-icon-space-needle:before { + content: "\fcf6"; +} +.game-icon-space-shuttle:before { + content: "\fcf7"; +} +.game-icon-space-suit:before { + content: "\fcf8"; +} +.game-icon-spaceship:before { + content: "\fcf9"; +} +.game-icon-spade-skull:before { + content: "\fcfa"; +} +.game-icon-spade:before { + content: "\fcfb"; +} +.game-icon-spades:before { + content: "\fcfc"; +} +.game-icon-spain:before { + content: "\fcfd"; +} +.game-icon-spanner:before { + content: "\fcfe"; +} +.game-icon-spark-plug:before { + content: "\fcff"; +} +.game-icon-spark-spirit:before { + content: "\fd00"; +} +.game-icon-sparkles:before { + content: "\fd01"; +} +.game-icon-sparkling-sabre:before { + content: "\fd02"; +} +.game-icon-sparky-bomb:before { + content: "\fd03"; +} +.game-icon-sparrow:before { + content: "\fd04"; +} +.game-icon-spartan-helmet:before { + content: "\fd05"; +} +.game-icon-spartan:before { + content: "\fd06"; +} +.game-icon-spatter:before { + content: "\fd07"; +} +.game-icon-spawn-node:before { + content: "\fd08"; +} +.game-icon-speaker-off:before { + content: "\fd09"; +} +.game-icon-speaker:before { + content: "\fd0a"; +} +.game-icon-spear-feather:before { + content: "\fd0b"; +} +.game-icon-spear-hook:before { + content: "\fd0c"; +} +.game-icon-spearfishing:before { + content: "\fd0d"; +} +.game-icon-spears:before { + content: "\fd0e"; +} +.game-icon-spectacle-lenses:before { + content: "\fd0f"; +} +.game-icon-spectacles:before { + content: "\fd10"; +} +.game-icon-spectre-m4:before { + content: "\fd11"; +} +.game-icon-spectre:before { + content: "\fd12"; +} +.game-icon-speed-boat:before { + content: "\fd13"; +} +.game-icon-speedometer:before { + content: "\fd14"; +} +.game-icon-spell-book:before { + content: "\fd15"; +} +.game-icon-sperm-whale:before { + content: "\fd16"; +} +.game-icon-spider-alt:before { + content: "\fd17"; +} +.game-icon-spider-bot:before { + content: "\fd18"; +} +.game-icon-spider-eye:before { + content: "\fd19"; +} +.game-icon-spider-face:before { + content: "\fd1a"; +} +.game-icon-spider-mask:before { + content: "\fd1b"; +} +.game-icon-spider-web:before { + content: "\fd1c"; +} +.game-icon-spikeball:before { + content: "\fd1d"; +} +.game-icon-spiked-armor:before { + content: "\fd1e"; +} +.game-icon-spiked-ball:before { + content: "\fd1f"; +} +.game-icon-spiked-bat:before { + content: "\fd20"; +} +.game-icon-spiked-collar:before { + content: "\fd21"; +} +.game-icon-spiked-dragon-head:before { + content: "\fd22"; +} +.game-icon-spiked-fence:before { + content: "\fd23"; +} +.game-icon-spiked-halo:before { + content: "\fd24"; +} +.game-icon-spiked-mace:before { + content: "\fd25"; +} +.game-icon-spiked-shell:before { + content: "\fd26"; +} +.game-icon-spiked-shield:before { + content: "\fd27"; +} +.game-icon-spiked-shoulder-armor:before { + content: "\fd28"; +} +.game-icon-spiked-snail:before { + content: "\fd29"; +} +.game-icon-spiked-tail:before { + content: "\fd2a"; +} +.game-icon-spiked-tentacle:before { + content: "\fd2b"; +} +.game-icon-spiked-trunk:before { + content: "\fd2c"; +} +.game-icon-spiked-wall:before { + content: "\fd2d"; +} +.game-icon-spikes-full:before { + content: "\fd2e"; +} +.game-icon-spikes-half:before { + content: "\fd2f"; +} +.game-icon-spikes-init:before { + content: "\fd30"; +} +.game-icon-spikes:before { + content: "\fd31"; +} +.game-icon-spiky-eclipse:before { + content: "\fd32"; +} +.game-icon-spiky-explosion:before { + content: "\fd33"; +} +.game-icon-spiky-field:before { + content: "\fd34"; +} +.game-icon-spiky-pit:before { + content: "\fd35"; +} +.game-icon-spiky-wing:before { + content: "\fd36"; +} +.game-icon-spill:before { + content: "\fd37"; +} +.game-icon-spinal-coil:before { + content: "\fd38"; +} +.game-icon-spine-arrow:before { + content: "\fd39"; +} +.game-icon-spinning-blades:before { + content: "\fd3a"; +} +.game-icon-spinning-ribbons:before { + content: "\fd3b"; +} +.game-icon-spinning-sword:before { + content: "\fd3c"; +} +.game-icon-spinning-top:before { + content: "\fd3d"; +} +.game-icon-spinning-wheel:before { + content: "\fd3e"; +} +.game-icon-spiral-arrow:before { + content: "\fd3f"; +} +.game-icon-spiral-bloom:before { + content: "\fd40"; +} +.game-icon-spiral-bottle:before { + content: "\fd41"; +} +.game-icon-spiral-hilt:before { + content: "\fd42"; +} +.game-icon-spiral-lollipop:before { + content: "\fd43"; +} +.game-icon-spiral-shell:before { + content: "\fd44"; +} +.game-icon-spiral-tentacle:before { + content: "\fd45"; +} +.game-icon-spiral-thrust:before { + content: "\fd46"; +} +.game-icon-splash-2:before { + content: "\fd47"; +} +.game-icon-splash:before { + content: "\fd48"; +} +.game-icon-splashy-stream:before { + content: "\fd49"; +} +.game-icon-split-arrows:before { + content: "\fd4a"; +} +.game-icon-split-body:before { + content: "\fd4b"; +} +.game-icon-split-cross:before { + content: "\fd4c"; +} +.game-icon-splurt:before { + content: "\fd4d"; +} +.game-icon-spock-hand:before { + content: "\fd4e"; +} +.game-icon-spooky-house:before { + content: "\fd4f"; +} +.game-icon-spoon:before { + content: "\fd50"; +} +.game-icon-sport-medal:before { + content: "\fd51"; +} +.game-icon-spoted-flower:before { + content: "\fd52"; +} +.game-icon-spotted-arrowhead:before { + content: "\fd53"; +} +.game-icon-spotted-bug:before { + content: "\fd54"; +} +.game-icon-spotted-mushroom:before { + content: "\fd55"; +} +.game-icon-spotted-wound:before { + content: "\fd56"; +} +.game-icon-spoutnik:before { + content: "\fd57"; +} +.game-icon-spray:before { + content: "\fd58"; +} +.game-icon-spring:before { + content: "\fd59"; +} +.game-icon-sprint:before { + content: "\fd5a"; +} +.game-icon-sprout-disc:before { + content: "\fd5b"; +} +.game-icon-sprout:before { + content: "\fd5c"; +} +.game-icon-spy:before { + content: "\fd5d"; +} +.game-icon-spyglass:before { + content: "\fd5e"; +} +.game-icon-square-bottle:before { + content: "\fd5f"; +} +.game-icon-square:before { + content: "\fd60"; +} +.game-icon-squib:before { + content: "\fd61"; +} +.game-icon-squid-head:before { + content: "\fd62"; +} +.game-icon-squid:before { + content: "\fd63"; +} +.game-icon-squirrel:before { + content: "\fd64"; +} +.game-icon-sri-lanka:before { + content: "\fd65"; +} +.game-icon-stabbed-note:before { + content: "\fd66"; +} +.game-icon-stable:before { + content: "\fd67"; +} +.game-icon-stack:before { + content: "\fd68"; +} +.game-icon-stag-head:before { + content: "\fd69"; +} +.game-icon-stahlhelm:before { + content: "\fd6a"; +} +.game-icon-stairs-cake:before { + content: "\fd6b"; +} +.game-icon-stairs-goal:before { + content: "\fd6c"; +} +.game-icon-stairs:before { + content: "\fd6d"; +} +.game-icon-stake-hammer:before { + content: "\fd6e"; +} +.game-icon-stakes-fence:before { + content: "\fd6f"; +} +.game-icon-stalactites:before { + content: "\fd70"; +} +.game-icon-stalagtite:before { + content: "\fd71"; +} +.game-icon-stamper:before { + content: "\fd72"; +} +.game-icon-standing-potion:before { + content: "\fd73"; +} +.game-icon-stapler-heavy-duty:before { + content: "\fd74"; +} +.game-icon-stapler-pneumatic:before { + content: "\fd75"; +} +.game-icon-stapler:before { + content: "\fd76"; +} +.game-icon-star-altar:before { + content: "\fd77"; +} +.game-icon-star-cycle:before { + content: "\fd78"; +} +.game-icon-star-flag:before { + content: "\fd79"; +} +.game-icon-star-formation:before { + content: "\fd7a"; +} +.game-icon-star-gate:before { + content: "\fd7b"; +} +.game-icon-star-key:before { + content: "\fd7c"; +} +.game-icon-star-medal:before { + content: "\fd7d"; +} +.game-icon-star-prominences:before { + content: "\fd7e"; +} +.game-icon-star-pupil:before { + content: "\fd7f"; +} +.game-icon-star-sattelites:before { + content: "\fd80"; +} +.game-icon-star-shuriken:before { + content: "\fd81"; +} +.game-icon-star-skull:before { + content: "\fd82"; +} +.game-icon-star-struck:before { + content: "\fd83"; +} +.game-icon-star-swirl:before { + content: "\fd84"; +} +.game-icon-starfighter:before { + content: "\fd85"; +} +.game-icon-stars-stack:before { + content: "\fd86"; +} +.game-icon-staryu:before { + content: "\fd87"; +} +.game-icon-static-guard:before { + content: "\fd88"; +} +.game-icon-static-waves:before { + content: "\fd89"; +} +.game-icon-static:before { + content: "\fd8a"; +} +.game-icon-steak:before { + content: "\fd8b"; +} +.game-icon-stealth-bomber:before { + content: "\fd8c"; +} +.game-icon-steam-blast:before { + content: "\fd8d"; +} +.game-icon-steam-locomotive:before { + content: "\fd8e"; +} +.game-icon-steam:before { + content: "\fd8f"; +} +.game-icon-steampunk-goggles:before { + content: "\fd90"; +} +.game-icon-steamroller:before { + content: "\fd91"; +} +.game-icon-steel-claws-2:before { + content: "\fd92"; +} +.game-icon-steel-claws:before { + content: "\fd93"; +} +.game-icon-steel-door:before { + content: "\fd94"; +} +.game-icon-steeltoe-boots:before { + content: "\fd95"; +} +.game-icon-steelwing-emblem:before { + content: "\fd96"; +} +.game-icon-steering-wheel:before { + content: "\fd97"; +} +.game-icon-stegosaurus-scales:before { + content: "\fd98"; +} +.game-icon-stethoscope:before { + content: "\fd99"; +} +.game-icon-steyr-aug:before { + content: "\fd9a"; +} +.game-icon-stick-frame:before { + content: "\fd9b"; +} +.game-icon-stick-grenade:before { + content: "\fd9c"; +} +.game-icon-stick-splitting:before { + content: "\fd9d"; +} +.game-icon-sticking-plaster:before { + content: "\fd9e"; +} +.game-icon-sticky-boot:before { + content: "\fd9f"; +} +.game-icon-stigmata:before { + content: "\fda0"; +} +.game-icon-stiletto-2:before { + content: "\fda1"; +} +.game-icon-stiletto:before { + content: "\fda2"; +} +.game-icon-stitched-wound:before { + content: "\fda3"; +} +.game-icon-stockpiles:before { + content: "\fda4"; +} +.game-icon-stomach:before { + content: "\fda5"; +} +.game-icon-stomp-tornado:before { + content: "\fda6"; +} +.game-icon-stomp:before { + content: "\fda7"; +} +.game-icon-stone-axe:before { + content: "\fda8"; +} +.game-icon-stone-block:before { + content: "\fda9"; +} +.game-icon-stone-bridge:before { + content: "\fdaa"; +} +.game-icon-stone-bust:before { + content: "\fdab"; +} +.game-icon-stone-crafting:before { + content: "\fdac"; +} +.game-icon-stone-path:before { + content: "\fdad"; +} +.game-icon-stone-pile:before { + content: "\fdae"; +} +.game-icon-stone-spear:before { + content: "\fdaf"; +} +.game-icon-stone-sphere:before { + content: "\fdb0"; +} +.game-icon-stone-stack:before { + content: "\fdb1"; +} +.game-icon-stone-tablet:before { + content: "\fdb2"; +} +.game-icon-stone-throne:before { + content: "\fdb3"; +} +.game-icon-stone-tower:before { + content: "\fdb4"; +} +.game-icon-stone-wall:before { + content: "\fdb5"; +} +.game-icon-stone-wheel:before { + content: "\fdb6"; +} +.game-icon-stoned-skull:before { + content: "\fdb7"; +} +.game-icon-stop-sign:before { + content: "\fdb8"; +} +.game-icon-stopwatch-2:before { + content: "\fdb9"; +} +.game-icon-stopwatch:before { + content: "\fdba"; +} +.game-icon-stork-delivery:before { + content: "\fdbb"; +} +.game-icon-strafe:before { + content: "\fdbc"; +} +.game-icon-straight-pipe:before { + content: "\fdbd"; +} +.game-icon-strawberry:before { + content: "\fdbe"; +} +.game-icon-street-light:before { + content: "\fdbf"; +} +.game-icon-striking-arrows:before { + content: "\fdc0"; +} +.game-icon-striking-balls:before { + content: "\fdc1"; +} +.game-icon-striking-clamps:before { + content: "\fdc2"; +} +.game-icon-striking-diamonds:before { + content: "\fdc3"; +} +.game-icon-striking-splinter:before { + content: "\fdc4"; +} +.game-icon-striped-sun:before { + content: "\fdc5"; +} +.game-icon-striped-sword:before { + content: "\fdc6"; +} +.game-icon-strong-man:before { + content: "\fdc7"; +} +.game-icon-strong:before { + content: "\fdc8"; +} +.game-icon-strongbox:before { + content: "\fdc9"; +} +.game-icon-stump-regrowth:before { + content: "\fdca"; +} +.game-icon-stun-grenade:before { + content: "\fdcb"; +} +.game-icon-submarine-missile:before { + content: "\fdcc"; +} +.game-icon-submarine:before { + content: "\fdcd"; +} +.game-icon-subway:before { + content: "\fdce"; +} +.game-icon-suckered-tentacle:before { + content: "\fdcf"; +} +.game-icon-sugar-cane:before { + content: "\fdd0"; +} +.game-icon-suicide:before { + content: "\fdd1"; +} +.game-icon-suitcase:before { + content: "\fdd2"; +} +.game-icon-suits:before { + content: "\fdd3"; +} +.game-icon-summits:before { + content: "\fdd4"; +} +.game-icon-sun-cloud:before { + content: "\fdd5"; +} +.game-icon-sun-priest:before { + content: "\fdd6"; +} +.game-icon-sun-radiations:before { + content: "\fdd7"; +} +.game-icon-sun-spear:before { + content: "\fdd8"; +} +.game-icon-sun:before { + content: "\fdd9"; +} +.game-icon-sunbeams:before { + content: "\fdda"; +} +.game-icon-sundial:before { + content: "\fddb"; +} +.game-icon-sunflower:before { + content: "\fddc"; +} +.game-icon-sunglasses:before { + content: "\fddd"; +} +.game-icon-sunken-eye:before { + content: "\fdde"; +} +.game-icon-sunrise-2:before { + content: "\fddf"; +} +.game-icon-sunrise:before { + content: "\fde0"; +} +.game-icon-sunset:before { + content: "\fde1"; +} +.game-icon-super-mushroom:before { + content: "\fde2"; +} +.game-icon-supersonic-arrow:before { + content: "\fde3"; +} +.game-icon-supersonic-bullet:before { + content: "\fde4"; +} +.game-icon-surf-board:before { + content: "\fde5"; +} +.game-icon-surfer-van:before { + content: "\fde6"; +} +.game-icon-surprised-skull:before { + content: "\fde7"; +} +.game-icon-surprised:before { + content: "\fde8"; +} +.game-icon-surrounded-eye:before { + content: "\fde9"; +} +.game-icon-surrounded-shield:before { + content: "\fdea"; +} +.game-icon-sushis:before { + content: "\fdeb"; +} +.game-icon-suspension-bridge:before { + content: "\fdec"; +} +.game-icon-suspicious:before { + content: "\fded"; +} +.game-icon-sverd-i-fjell:before { + content: "\fdee"; +} +.game-icon-swallow-2:before { + content: "\fdef"; +} +.game-icon-swallow:before { + content: "\fdf0"; +} +.game-icon-swallower:before { + content: "\fdf1"; +} +.game-icon-swamp-bat:before { + content: "\fdf2"; +} +.game-icon-swamp:before { + content: "\fdf3"; +} +.game-icon-swan-breeze:before { + content: "\fdf4"; +} +.game-icon-swan:before { + content: "\fdf5"; +} +.game-icon-swap-bag:before { + content: "\fdf6"; +} +.game-icon-swimfins:before { + content: "\fdf7"; +} +.game-icon-swipe-card:before { + content: "\fdf8"; +} +.game-icon-swirl-ring:before { + content: "\fdf9"; +} +.game-icon-swirl-string:before { + content: "\fdfa"; +} +.game-icon-swirled-shell:before { + content: "\fdfb"; +} +.game-icon-swiss-army-knife:before { + content: "\fdfc"; +} +.game-icon-switch-weapon:before { + content: "\fdfd"; +} +.game-icon-switchblade:before { + content: "\fdfe"; +} +.game-icon-switzerland:before { + content: "\fdff"; +} +.game-icon-sword-altar:before { + content: "\fe00"; +} +.game-icon-sword-array:before { + content: "\fe01"; +} +.game-icon-sword-brandish:before { + content: "\fe02"; +} +.game-icon-sword-break:before { + content: "\fe03"; +} +.game-icon-sword-clash:before { + content: "\fe04"; +} +.game-icon-sword-hilt:before { + content: "\fe05"; +} +.game-icon-sword-in-stone:before { + content: "\fe06"; +} +.game-icon-sword-mold:before { + content: "\fe07"; +} +.game-icon-sword-slice:before { + content: "\fe08"; +} +.game-icon-sword-smithing:before { + content: "\fe09"; +} +.game-icon-sword-spade:before { + content: "\fe0a"; +} +.game-icon-sword-spin:before { + content: "\fe0b"; +} +.game-icon-sword-tie:before { + content: "\fe0c"; +} +.game-icon-sword-wound:before { + content: "\fe0d"; +} +.game-icon-swordman:before { + content: "\fe0e"; +} +.game-icon-swords-emblem:before { + content: "\fe0f"; +} +.game-icon-swords-power:before { + content: "\fe10"; +} +.game-icon-swordwoman:before { + content: "\fe11"; +} +.game-icon-sydney-opera-house:before { + content: "\fe12"; +} +.game-icon-syringe-2:before { + content: "\fe13"; +} +.game-icon-syringe:before { + content: "\fe14"; +} +.game-icon-t-brick:before { + content: "\fe15"; +} +.game-icon-t-rex-skull:before { + content: "\fe16"; +} +.game-icon-t-shirt:before { + content: "\fe17"; +} +.game-icon-tabi-boot:before { + content: "\fe18"; +} +.game-icon-table:before { + content: "\fe19"; +} +.game-icon-tablet:before { + content: "\fe1a"; +} +.game-icon-tabletop-players:before { + content: "\fe1b"; +} +.game-icon-tacos:before { + content: "\fe1c"; +} +.game-icon-tadpole:before { + content: "\fe1d"; +} +.game-icon-take-my-money:before { + content: "\fe1e"; +} +.game-icon-talk:before { + content: "\fe1f"; +} +.game-icon-tall-bridge:before { + content: "\fe20"; +} +.game-icon-tambourine:before { + content: "\fe21"; +} +.game-icon-tangerine:before { + content: "\fe22"; +} +.game-icon-tank-top:before { + content: "\fe23"; +} +.game-icon-tank-tread:before { + content: "\fe24"; +} +.game-icon-tank:before { + content: "\fe25"; +} +.game-icon-tanzania:before { + content: "\fe26"; +} +.game-icon-tap:before { + content: "\fe27"; +} +.game-icon-tapir:before { + content: "\fe28"; +} +.game-icon-target-arrows:before { + content: "\fe29"; +} +.game-icon-target-dummy:before { + content: "\fe2a"; +} +.game-icon-target-laser:before { + content: "\fe2b"; +} +.game-icon-target-poster:before { + content: "\fe2c"; +} +.game-icon-target-prize:before { + content: "\fe2d"; +} +.game-icon-target-shot:before { + content: "\fe2e"; +} +.game-icon-targeted:before { + content: "\fe2f"; +} +.game-icon-targeting:before { + content: "\fe30"; +} +.game-icon-tattered-banner:before { + content: "\fe31"; +} +.game-icon-taurus:before { + content: "\fe32"; +} +.game-icon-tavern-sign:before { + content: "\fe33"; +} +.game-icon-teacher:before { + content: "\fe34"; +} +.game-icon-team-downgrade:before { + content: "\fe35"; +} +.game-icon-team-idea:before { + content: "\fe36"; +} +.game-icon-team-upgrade:before { + content: "\fe37"; +} +.game-icon-teapot-leaves:before { + content: "\fe38"; +} +.game-icon-teapot:before { + content: "\fe39"; +} +.game-icon-tear-tracks:before { + content: "\fe3a"; +} +.game-icon-tearing:before { + content: "\fe3b"; +} +.game-icon-tec-9:before { + content: "\fe3c"; +} +.game-icon-techno-heart:before { + content: "\fe3d"; +} +.game-icon-tee-pipe:before { + content: "\fe3e"; +} +.game-icon-telefrag:before { + content: "\fe3f"; +} +.game-icon-telepathy:before { + content: "\fe40"; +} +.game-icon-teleport:before { + content: "\fe41"; +} +.game-icon-telescopic-baton:before { + content: "\fe42"; +} +.game-icon-teller-mine:before { + content: "\fe43"; +} +.game-icon-templar-eye:before { + content: "\fe44"; +} +.game-icon-templar-heart:before { + content: "\fe45"; +} +.game-icon-templar-shield:before { + content: "\fe46"; +} +.game-icon-temple-door:before { + content: "\fe47"; +} +.game-icon-temple-gate:before { + content: "\fe48"; +} +.game-icon-temporary-shield:before { + content: "\fe49"; +} +.game-icon-temptation:before { + content: "\fe4a"; +} +.game-icon-tennis-ball:before { + content: "\fe4b"; +} +.game-icon-tennis-court:before { + content: "\fe4c"; +} +.game-icon-tennis-racket:before { + content: "\fe4d"; +} +.game-icon-tension-snowflake:before { + content: "\fe4e"; +} +.game-icon-tentacle-heart:before { + content: "\fe4f"; +} +.game-icon-tentacle-strike:before { + content: "\fe50"; +} +.game-icon-tentacles-barrier:before { + content: "\fe51"; +} +.game-icon-tentacles-skull:before { + content: "\fe52"; +} +.game-icon-tentacurl:before { + content: "\fe53"; +} +.game-icon-terror:before { + content: "\fe54"; +} +.game-icon-tesla-coil:before { + content: "\fe55"; +} +.game-icon-tesla-turret:before { + content: "\fe56"; +} +.game-icon-tesla:before { + content: "\fe57"; +} +.game-icon-test-tubes:before { + content: "\fe58"; +} +.game-icon-texas:before { + content: "\fe59"; +} +.game-icon-theater-curtains:before { + content: "\fe5a"; +} +.game-icon-theater:before { + content: "\fe5b"; +} +.game-icon-thermometer-cold:before { + content: "\fe5c"; +} +.game-icon-thermometer-hot:before { + content: "\fe5d"; +} +.game-icon-thermometer-scale:before { + content: "\fe5e"; +} +.game-icon-think:before { + content: "\fe5f"; +} +.game-icon-third-eye:before { + content: "\fe60"; +} +.game-icon-thompson-m1:before { + content: "\fe61"; +} +.game-icon-thompson-m1928:before { + content: "\fe62"; +} +.game-icon-thor-fist:before { + content: "\fe63"; +} +.game-icon-thor-hammer:before { + content: "\fe64"; +} +.game-icon-thorn-helix:before { + content: "\fe65"; +} +.game-icon-thorned-arrow:before { + content: "\fe66"; +} +.game-icon-thorny-tentacle:before { + content: "\fe67"; +} +.game-icon-thorny-vine:before { + content: "\fe68"; +} +.game-icon-three-burning-balls:before { + content: "\fe69"; +} +.game-icon-three-friends:before { + content: "\fe6a"; +} +.game-icon-three-keys:before { + content: "\fe6b"; +} +.game-icon-three-leaves:before { + content: "\fe6c"; +} +.game-icon-three-pointed-shuriken:before { + content: "\fe6d"; +} +.game-icon-throne-king:before { + content: "\fe6e"; +} +.game-icon-throwing-ball:before { + content: "\fe6f"; +} +.game-icon-thrown-charcoal:before { + content: "\fe70"; +} +.game-icon-thrown-daggers:before { + content: "\fe71"; +} +.game-icon-thrown-knife:before { + content: "\fe72"; +} +.game-icon-thrown-spear:before { + content: "\fe73"; +} +.game-icon-thrust-bend:before { + content: "\fe74"; +} +.game-icon-thrust:before { + content: "\fe75"; +} +.game-icon-thumb-down:before { + content: "\fe76"; +} +.game-icon-thumb-up:before { + content: "\fe77"; +} +.game-icon-thunder-blade:before { + content: "\fe78"; +} +.game-icon-thunder-skull:before { + content: "\fe79"; +} +.game-icon-thunder-struck:before { + content: "\fe7a"; +} +.game-icon-thunderball:before { + content: "\fe7b"; +} +.game-icon-thwomp:before { + content: "\fe7c"; +} +.game-icon-tiara:before { + content: "\fe7d"; +} +.game-icon-tic-tac-toe:before { + content: "\fe7e"; +} +.game-icon-tick:before { + content: "\fe7f"; +} +.game-icon-ticket:before { + content: "\fe80"; +} +.game-icon-tie:before { + content: "\fe81"; +} +.game-icon-tied-scroll:before { + content: "\fe82"; +} +.game-icon-tiger-head:before { + content: "\fe83"; +} +.game-icon-tiger:before { + content: "\fe84"; +} +.game-icon-tightrope:before { + content: "\fe85"; +} +.game-icon-time-bomb:before { + content: "\fe86"; +} +.game-icon-time-dynamite:before { + content: "\fe87"; +} +.game-icon-time-synchronization:before { + content: "\fe88"; +} +.game-icon-time-trap:before { + content: "\fe89"; +} +.game-icon-tinker:before { + content: "\fe8a"; +} +.game-icon-tipi:before { + content: "\fe8b"; +} +.game-icon-tire-iron-cross:before { + content: "\fe8c"; +} +.game-icon-tire-iron:before { + content: "\fe8d"; +} +.game-icon-tire-tracks:before { + content: "\fe8e"; +} +.game-icon-tired-eye:before { + content: "\fe8f"; +} +.game-icon-toad-teeth:before { + content: "\fe90"; +} +.game-icon-toaster:before { + content: "\fe91"; +} +.game-icon-toggles:before { + content: "\fe92"; +} +.game-icon-token:before { + content: "\fe93"; +} +.game-icon-tomahawk:before { + content: "\fe94"; +} +.game-icon-tomato:before { + content: "\fe95"; +} +.game-icon-tombstone-2:before { + content: "\fe96"; +} +.game-icon-tombstone:before { + content: "\fe97"; +} +.game-icon-tongue:before { + content: "\fe98"; +} +.game-icon-toolbox:before { + content: "\fe99"; +} +.game-icon-tooth:before { + content: "\fe9a"; +} +.game-icon-toothbrush:before { + content: "\fe9b"; +} +.game-icon-top-hat:before { + content: "\fe9c"; +} +.game-icon-top-paw:before { + content: "\fe9d"; +} +.game-icon-topaz:before { + content: "\fe9e"; +} +.game-icon-torc:before { + content: "\fe9f"; +} +.game-icon-torch:before { + content: "\fea0"; +} +.game-icon-tornado-discs:before { + content: "\fea1"; +} +.game-icon-tornado:before { + content: "\fea2"; +} +.game-icon-torpedo:before { + content: "\fea3"; +} +.game-icon-tortoise:before { + content: "\fea4"; +} +.game-icon-totem-head:before { + content: "\fea5"; +} +.game-icon-totem-mask:before { + content: "\fea6"; +} +.game-icon-totem:before { + content: "\fea7"; +} +.game-icon-toucan:before { + content: "\fea8"; +} +.game-icon-tow-truck:before { + content: "\fea9"; +} +.game-icon-towel:before { + content: "\feaa"; +} +.game-icon-tower-bridge:before { + content: "\feab"; +} +.game-icon-tower-fall:before { + content: "\feac"; +} +.game-icon-tower-flag:before { + content: "\fead"; +} +.game-icon-toy-mallet:before { + content: "\feae"; +} +.game-icon-tracked-robot:before { + content: "\feaf"; +} +.game-icon-trade:before { + content: "\feb0"; +} +.game-icon-traffic-cone:before { + content: "\feb1"; +} +.game-icon-traffic-lights-green:before { + content: "\feb2"; +} +.game-icon-traffic-lights-orange:before { + content: "\feb3"; +} +.game-icon-traffic-lights-ready-to-go:before { + content: "\feb4"; +} +.game-icon-traffic-lights-red:before { + content: "\feb5"; +} +.game-icon-trail:before { + content: "\feb6"; +} +.game-icon-trample:before { + content: "\feb7"; +} +.game-icon-transform:before { + content: "\feb8"; +} +.game-icon-transfuse:before { + content: "\feb9"; +} +.game-icon-transparent-slime:before { + content: "\feba"; +} +.game-icon-transparent-tubes:before { + content: "\febb"; +} +.game-icon-transportation-rings:before { + content: "\febc"; +} +.game-icon-trap-mask:before { + content: "\febd"; +} +.game-icon-trash-can:before { + content: "\febe"; +} +.game-icon-travel-dress:before { + content: "\febf"; +} +.game-icon-tread:before { + content: "\fec0"; +} +.game-icon-treasure-map:before { + content: "\fec1"; +} +.game-icon-trebuchet:before { + content: "\fec2"; +} +.game-icon-tree-beehive:before { + content: "\fec3"; +} +.game-icon-tree-branch:before { + content: "\fec4"; +} +.game-icon-tree-door:before { + content: "\fec5"; +} +.game-icon-tree-face:before { + content: "\fec6"; +} +.game-icon-tree-growth:before { + content: "\fec7"; +} +.game-icon-tree-roots:before { + content: "\fec8"; +} +.game-icon-tree-swing:before { + content: "\fec9"; +} +.game-icon-treehouse:before { + content: "\feca"; +} +.game-icon-trefoil-lily:before { + content: "\fecb"; +} +.game-icon-trefoil-shuriken:before { + content: "\fecc"; +} +.game-icon-trench-assault:before { + content: "\fecd"; +} +.game-icon-trench-body-armor:before { + content: "\fece"; +} +.game-icon-trench-knife:before { + content: "\fecf"; +} +.game-icon-trench-spade:before { + content: "\fed0"; +} +.game-icon-triangle-target:before { + content: "\fed1"; +} +.game-icon-tribal-gear:before { + content: "\fed2"; +} +.game-icon-tribal-mask:before { + content: "\fed3"; +} +.game-icon-tribal-pendant:before { + content: "\fed4"; +} +.game-icon-tribal-shield:before { + content: "\fed5"; +} +.game-icon-tribunal-jury:before { + content: "\fed6"; +} +.game-icon-triceratops-head:before { + content: "\fed7"; +} +.game-icon-trident-shield:before { + content: "\fed8"; +} +.game-icon-trident:before { + content: "\fed9"; +} +.game-icon-triforce:before { + content: "\feda"; +} +.game-icon-trigger-hurt:before { + content: "\fedb"; +} +.game-icon-trilobite:before { + content: "\fedc"; +} +.game-icon-trinacria:before { + content: "\fedd"; +} +.game-icon-triorb:before { + content: "\fede"; +} +.game-icon-triple-beak:before { + content: "\fedf"; +} +.game-icon-triple-claws:before { + content: "\fee0"; +} +.game-icon-triple-corn:before { + content: "\fee1"; +} +.game-icon-triple-gate:before { + content: "\fee2"; +} +.game-icon-triple-lock:before { + content: "\fee3"; +} +.game-icon-triple-needle:before { + content: "\fee4"; +} +.game-icon-triple-plier:before { + content: "\fee5"; +} +.game-icon-triple-scratches:before { + content: "\fee6"; +} +.game-icon-triple-shells:before { + content: "\fee7"; +} +.game-icon-triple-skulls:before { + content: "\fee8"; +} +.game-icon-triple-yin:before { + content: "\fee9"; +} +.game-icon-tripwire:before { + content: "\feea"; +} +.game-icon-triquetra:before { + content: "\feeb"; +} +.game-icon-trireme:before { + content: "\feec"; +} +.game-icon-triton-head:before { + content: "\feed"; +} +.game-icon-troglodyte:before { + content: "\feee"; +} +.game-icon-trojan-horse:before { + content: "\feef"; +} +.game-icon-troll:before { + content: "\fef0"; +} +.game-icon-trombone:before { + content: "\fef1"; +} +.game-icon-tron-arrow:before { + content: "\fef2"; +} +.game-icon-trophies-shelf:before { + content: "\fef3"; +} +.game-icon-trophy-cup:before { + content: "\fef4"; +} +.game-icon-trophy:before { + content: "\fef5"; +} +.game-icon-tropical-fish:before { + content: "\fef6"; +} +.game-icon-trousers-2:before { + content: "\fef7"; +} +.game-icon-trousers:before { + content: "\fef8"; +} +.game-icon-trowel:before { + content: "\fef9"; +} +.game-icon-truce:before { + content: "\fefa"; +} +.game-icon-truck:before { + content: "\fefb"; +} +.game-icon-trumpet-flag:before { + content: "\fefc"; +} +.game-icon-trumpet:before { + content: "\fefd"; +} +.game-icon-trunk-mushroom:before { + content: "\fefe"; +} +.game-icon-tuba:before { + content: "\feff"; +} +.game-icon-tumbleweed:before { + content: "\ff00"; +} +.game-icon-tumor:before { + content: "\ff01"; +} +.game-icon-tumulus:before { + content: "\ff02"; +} +.game-icon-tune-pitch:before { + content: "\ff03"; +} +.game-icon-tunisia:before { + content: "\ff04"; +} +.game-icon-turban:before { + content: "\ff05"; +} +.game-icon-turbine:before { + content: "\ff06"; +} +.game-icon-turd:before { + content: "\ff07"; +} +.game-icon-turnstile:before { + content: "\ff08"; +} +.game-icon-turret:before { + content: "\ff09"; +} +.game-icon-turtle-shell:before { + content: "\ff0a"; +} +.game-icon-turtle:before { + content: "\ff0b"; +} +.game-icon-tusks-flag:before { + content: "\ff0c"; +} +.game-icon-tv-remote:before { + content: "\ff0d"; +} +.game-icon-tv-tower:before { + content: "\ff0e"; +} +.game-icon-tv:before { + content: "\ff0f"; +} +.game-icon-twin-shell:before { + content: "\ff10"; +} +.game-icon-twirl-center:before { + content: "\ff11"; +} +.game-icon-twirly-flower:before { + content: "\ff12"; +} +.game-icon-twister:before { + content: "\ff13"; +} +.game-icon-two-coins:before { + content: "\ff14"; +} +.game-icon-two-feathers:before { + content: "\ff15"; +} +.game-icon-two-handed-sword:before { + content: "\ff16"; +} +.game-icon-two-shadows:before { + content: "\ff17"; +} +.game-icon-tyre:before { + content: "\ff18"; +} +.game-icon-ubisoft-sun:before { + content: "\ff19"; +} +.game-icon-udder:before { + content: "\ff1a"; +} +.game-icon-ufo:before { + content: "\ff1b"; +} +.game-icon-ultrasound:before { + content: "\ff1c"; +} +.game-icon-uluru:before { + content: "\ff1d"; +} +.game-icon-umbrella-bayonet:before { + content: "\ff1e"; +} +.game-icon-umbrella:before { + content: "\ff1f"; +} +.game-icon-unbalanced:before { + content: "\ff20"; +} +.game-icon-uncertainty:before { + content: "\ff21"; +} +.game-icon-underground-cave:before { + content: "\ff22"; +} +.game-icon-underhand:before { + content: "\ff23"; +} +.game-icon-underwear-shorts:before { + content: "\ff24"; +} +.game-icon-underwear:before { + content: "\ff25"; +} +.game-icon-unfriendly-fire:before { + content: "\ff26"; +} +.game-icon-unicorn:before { + content: "\ff27"; +} +.game-icon-unicycle:before { + content: "\ff28"; +} +.game-icon-union-jack:before { + content: "\ff29"; +} +.game-icon-unlit-bomb:before { + content: "\ff2a"; +} +.game-icon-unlit-candelabra:before { + content: "\ff2b"; +} +.game-icon-unlocking:before { + content: "\ff2c"; +} +.game-icon-unplugged:before { + content: "\ff2d"; +} +.game-icon-unstable-orb:before { + content: "\ff2e"; +} +.game-icon-unstable-projectile:before { + content: "\ff2f"; +} +.game-icon-up-card:before { + content: "\ff30"; +} +.game-icon-upgrade:before { + content: "\ff31"; +} +.game-icon-uprising:before { + content: "\ff32"; +} +.game-icon-ursa-major:before { + content: "\ff33"; +} +.game-icon-uruguay:before { + content: "\ff34"; +} +.game-icon-usa-flag:before { + content: "\ff35"; +} +.game-icon-usable:before { + content: "\ff36"; +} +.game-icon-usb-key:before { + content: "\ff37"; +} +.game-icon-ushanka:before { + content: "\ff38"; +} +.game-icon-uzi:before { + content: "\ff39"; +} +.game-icon-vacuum-cleaner:before { + content: "\ff3a"; +} +.game-icon-valley:before { + content: "\ff3b"; +} +.game-icon-valve:before { + content: "\ff3c"; +} +.game-icon-vampire-cape:before { + content: "\ff3d"; +} +.game-icon-vampire-dracula:before { + content: "\ff3e"; +} +.game-icon-van-damme-split:before { + content: "\ff3f"; +} +.game-icon-vanilla-flower:before { + content: "\ff40"; +} +.game-icon-velocipede:before { + content: "\ff41"; +} +.game-icon-velociraptor-tracks:before { + content: "\ff42"; +} +.game-icon-velociraptor:before { + content: "\ff43"; +} +.game-icon-vending-machine:before { + content: "\ff44"; +} +.game-icon-venezuela:before { + content: "\ff45"; +} +.game-icon-venus-flytrap:before { + content: "\ff46"; +} +.game-icon-venus-of-willendorf:before { + content: "\ff47"; +} +.game-icon-vertical-banner:before { + content: "\ff48"; +} +.game-icon-vertical-flip:before { + content: "\ff49"; +} +.game-icon-vhs:before { + content: "\ff4a"; +} +.game-icon-vial:before { + content: "\ff4b"; +} +.game-icon-vibrating-ball:before { + content: "\ff4c"; +} +.game-icon-vibrating-shield:before { + content: "\ff4d"; +} +.game-icon-vibrating-smartphone:before { + content: "\ff4e"; +} +.game-icon-video-camera:before { + content: "\ff4f"; +} +.game-icon-video-conference:before { + content: "\ff50"; +} +.game-icon-viking-church:before { + content: "\ff51"; +} +.game-icon-viking-head:before { + content: "\ff52"; +} +.game-icon-viking-helmet:before { + content: "\ff53"; +} +.game-icon-viking-longhouse:before { + content: "\ff54"; +} +.game-icon-viking-shield:before { + content: "\ff55"; +} +.game-icon-vile-fluid:before { + content: "\ff56"; +} +.game-icon-village:before { + content: "\ff57"; +} +.game-icon-vine-flower:before { + content: "\ff58"; +} +.game-icon-vine-leaf:before { + content: "\ff59"; +} +.game-icon-vine-whip:before { + content: "\ff5a"; +} +.game-icon-vines:before { + content: "\ff5b"; +} +.game-icon-vintage-robot:before { + content: "\ff5c"; +} +.game-icon-viola:before { + content: "\ff5d"; +} +.game-icon-violin:before { + content: "\ff5e"; +} +.game-icon-virgo:before { + content: "\ff5f"; +} +.game-icon-virtual-marker:before { + content: "\ff60"; +} +.game-icon-virus:before { + content: "\ff61"; +} +.game-icon-visored-helm:before { + content: "\ff62"; +} +.game-icon-vitruvian-man:before { + content: "\ff63"; +} +.game-icon-volcano:before { + content: "\ff64"; +} +.game-icon-volleyball-ball:before { + content: "\ff65"; +} +.game-icon-vomiting:before { + content: "\ff66"; +} +.game-icon-voodoo-doll:before { + content: "\ff67"; +} +.game-icon-vortex:before { + content: "\ff68"; +} +.game-icon-vote:before { + content: "\ff69"; +} +.game-icon-vr-headset:before { + content: "\ff6a"; +} +.game-icon-vulture:before { + content: "\ff6b"; +} +.game-icon-vuvuzelas:before { + content: "\ff6c"; +} +.game-icon-walk:before { + content: "\ff6d"; +} +.game-icon-walkie-talkie:before { + content: "\ff6e"; +} +.game-icon-walking-boot:before { + content: "\ff6f"; +} +.game-icon-walking-scout:before { + content: "\ff70"; +} +.game-icon-walking-turret:before { + content: "\ff71"; +} +.game-icon-wall-light:before { + content: "\ff72"; +} +.game-icon-wallet:before { + content: "\ff73"; +} +.game-icon-walrus-head:before { + content: "\ff74"; +} +.game-icon-walther-ppk:before { + content: "\ff75"; +} +.game-icon-wanted-reward:before { + content: "\ff76"; +} +.game-icon-war-axe:before { + content: "\ff77"; +} +.game-icon-war-bonnet:before { + content: "\ff78"; +} +.game-icon-war-pick:before { + content: "\ff79"; +} +.game-icon-warhammer:before { + content: "\ff7a"; +} +.game-icon-warlock-eye:before { + content: "\ff7b"; +} +.game-icon-warlock-hood:before { + content: "\ff7c"; +} +.game-icon-warp-pipe:before { + content: "\ff7d"; +} +.game-icon-washing-machine:before { + content: "\ff7e"; +} +.game-icon-wasp-sting:before { + content: "\ff7f"; +} +.game-icon-watch:before { + content: "\ff80"; +} +.game-icon-watchtower:before { + content: "\ff81"; +} +.game-icon-water-bolt:before { + content: "\ff82"; +} +.game-icon-water-bottle:before { + content: "\ff83"; +} +.game-icon-water-diviner-stick:before { + content: "\ff84"; +} +.game-icon-water-drop:before { + content: "\ff85"; +} +.game-icon-water-flask:before { + content: "\ff86"; +} +.game-icon-water-fountain:before { + content: "\ff87"; +} +.game-icon-water-gallon:before { + content: "\ff88"; +} +.game-icon-water-gun:before { + content: "\ff89"; +} +.game-icon-water-mill:before { + content: "\ff8a"; +} +.game-icon-water-polo:before { + content: "\ff8b"; +} +.game-icon-water-recycling:before { + content: "\ff8c"; +} +.game-icon-water-splash:before { + content: "\ff8d"; +} +.game-icon-water-tank:before { + content: "\ff8e"; +} +.game-icon-water-tower:before { + content: "\ff8f"; +} +.game-icon-waterfall:before { + content: "\ff90"; +} +.game-icon-watering-can:before { + content: "\ff91"; +} +.game-icon-watermelon:before { + content: "\ff92"; +} +.game-icon-wave-crest:before { + content: "\ff93"; +} +.game-icon-wave-strike:before { + content: "\ff94"; +} +.game-icon-wave-surfer:before { + content: "\ff95"; +} +.game-icon-waves:before { + content: "\ff96"; +} +.game-icon-wavy-chains:before { + content: "\ff97"; +} +.game-icon-wavy-itinerary:before { + content: "\ff98"; +} +.game-icon-wax-seal:before { + content: "\ff99"; +} +.game-icon-wax-tablet:before { + content: "\ff9a"; +} +.game-icon-web-spit:before { + content: "\ff9b"; +} +.game-icon-weight-crush:before { + content: "\ff9c"; +} +.game-icon-weight-lifting-down:before { + content: "\ff9d"; +} +.game-icon-weight-lifting-up:before { + content: "\ff9e"; +} +.game-icon-weight-scale:before { + content: "\ff9f"; +} +.game-icon-weight:before { + content: "\ffa0"; +} +.game-icon-well:before { + content: "\ffa1"; +} +.game-icon-werewolf:before { + content: "\ffa2"; +} +.game-icon-western-hat:before { + content: "\ffa3"; +} +.game-icon-whale-tail:before { + content: "\ffa4"; +} +.game-icon-wheat:before { + content: "\ffa5"; +} +.game-icon-wheelbarrow:before { + content: "\ffa6"; +} +.game-icon-whip:before { + content: "\ffa7"; +} +.game-icon-whiplash:before { + content: "\ffa8"; +} +.game-icon-whirlpool-shuriken:before { + content: "\ffa9"; +} +.game-icon-whirlwind:before { + content: "\ffaa"; +} +.game-icon-whisk:before { + content: "\ffab"; +} +.game-icon-whistle:before { + content: "\ffac"; +} +.game-icon-white-book:before { + content: "\ffad"; +} +.game-icon-white-cat:before { + content: "\ffae"; +} +.game-icon-white-tower:before { + content: "\ffaf"; +} +.game-icon-wide-arrow-dunk:before { + content: "\ffb0"; +} +.game-icon-wifi-router:before { + content: "\ffb1"; +} +.game-icon-wildfires:before { + content: "\ffb2"; +} +.game-icon-william-tell-skull:before { + content: "\ffb3"; +} +.game-icon-william-tell:before { + content: "\ffb4"; +} +.game-icon-willow-tree:before { + content: "\ffb5"; +} +.game-icon-winchester-rifle:before { + content: "\ffb6"; +} +.game-icon-wind-hole:before { + content: "\ffb7"; +} +.game-icon-wind-slap:before { + content: "\ffb8"; +} +.game-icon-wind-turbine:before { + content: "\ffb9"; +} +.game-icon-windchimes:before { + content: "\ffba"; +} +.game-icon-windmill:before { + content: "\ffbb"; +} +.game-icon-window-bars:before { + content: "\ffbc"; +} +.game-icon-window:before { + content: "\ffbd"; +} +.game-icon-windpump:before { + content: "\ffbe"; +} +.game-icon-windsock:before { + content: "\ffbf"; +} +.game-icon-windy-stripes:before { + content: "\ffc0"; +} +.game-icon-wine-bottle:before { + content: "\ffc1"; +} +.game-icon-wine-glass:before { + content: "\ffc2"; +} +.game-icon-wing-cloak:before { + content: "\ffc3"; +} +.game-icon-winged-arrow:before { + content: "\ffc4"; +} +.game-icon-winged-emblem:before { + content: "\ffc5"; +} +.game-icon-winged-leg:before { + content: "\ffc6"; +} +.game-icon-winged-scepter:before { + content: "\ffc7"; +} +.game-icon-winged-shield:before { + content: "\ffc8"; +} +.game-icon-winged-sword:before { + content: "\ffc9"; +} +.game-icon-wingfoot:before { + content: "\ffca"; +} +.game-icon-winter-gloves:before { + content: "\ffcb"; +} +.game-icon-winter-hat:before { + content: "\ffcc"; +} +.game-icon-wire-coil:before { + content: "\ffcd"; +} +.game-icon-wireframe-globe:before { + content: "\ffce"; +} +.game-icon-wisdom:before { + content: "\ffcf"; +} +.game-icon-witch-face:before { + content: "\ffd0"; +} +.game-icon-witch-flight:before { + content: "\ffd1"; +} +.game-icon-wizard-face:before { + content: "\ffd2"; +} +.game-icon-wizard-staff:before { + content: "\ffd3"; +} +.game-icon-wok:before { + content: "\ffd4"; +} +.game-icon-wolf-head:before { + content: "\ffd5"; +} +.game-icon-wolf-howl:before { + content: "\ffd6"; +} +.game-icon-wolf-trap:before { + content: "\ffd7"; +} +.game-icon-wolverine-claws-2:before { + content: "\ffd8"; +} +.game-icon-wolverine-claws:before { + content: "\ffd9"; +} +.game-icon-woman-elf-face:before { + content: "\ffda"; +} +.game-icon-wood-axe:before { + content: "\ffdb"; +} +.game-icon-wood-beam:before { + content: "\ffdc"; +} +.game-icon-wood-cabin:before { + content: "\ffdd"; +} +.game-icon-wood-canoe:before { + content: "\ffde"; +} +.game-icon-wood-club:before { + content: "\ffdf"; +} +.game-icon-wood-frame:before { + content: "\ffe0"; +} +.game-icon-wood-pile:before { + content: "\ffe1"; +} +.game-icon-wood-stick:before { + content: "\ffe2"; +} +.game-icon-wooden-chair:before { + content: "\ffe3"; +} +.game-icon-wooden-clogs:before { + content: "\ffe4"; +} +.game-icon-wooden-crate:before { + content: "\ffe5"; +} +.game-icon-wooden-door:before { + content: "\ffe6"; +} +.game-icon-wooden-fence:before { + content: "\ffe7"; +} +.game-icon-wooden-helmet:before { + content: "\ffe8"; +} +.game-icon-wooden-pegleg:before { + content: "\ffe9"; +} +.game-icon-wooden-pier:before { + content: "\ffea"; +} +.game-icon-wooden-sign:before { + content: "\ffeb"; +} +.game-icon-wool:before { + content: "\ffec"; +} +.game-icon-world:before { + content: "\ffed"; +} +.game-icon-worm-mouth:before { + content: "\ffee"; +} +.game-icon-worms:before { + content: "\ffef"; +} +.game-icon-worried-eyes:before { + content: "\fff0"; +} +.game-icon-wrapped-heart:before { + content: "\fff1"; +} +.game-icon-wrapped-sweet:before { + content: "\fff2"; +} +.game-icon-wrapping-star:before { + content: "\fff3"; +} +.game-icon-wrecking-ball:before { + content: "\fff4"; +} +.game-icon-wrench:before { + content: "\fff5"; +} +.game-icon-wyvern:before { + content: "\fff6"; +} +.game-icon-xylophone:before { + content: "\fff7"; +} +.game-icon-yarn:before { + content: "\fff8"; +} +.game-icon-yin-yang:before { + content: "\fff9"; +} +.game-icon-yunluo:before { + content: "\fffa"; +} +.game-icon-z-brick:before { + content: "\fffb"; +} +.game-icon-zat-gun:before { + content: "\fffc"; +} +.game-icon-zebra-shield:before { + content: "\fffd"; +} +.game-icon-zeppelin:before { + content: "\fffe"; +} +.game-icon-zeus-sword:before { + content: "\ffff"; +} +.game-icon-zig-arrow:before { + content: "\00000"; +} +.game-icon-zigzag-cage:before { + content: "\00001"; +} +.game-icon-zigzag-hieroglyph:before { + content: "\00002"; +} +.game-icon-zigzag-leaf:before { + content: "\00003"; +} +.game-icon-zigzag-tune:before { + content: "\00004"; +} +.game-icon-zipper:before { + content: "\00005"; +} diff --git a/app/client/main.js b/app/client/main.js index d9d770b7..7bb201aa 100644 --- a/app/client/main.js +++ b/app/client/main.js @@ -1,7 +1,7 @@ -import '/imports/api/simpleSchemaConfig.js'; -import '/imports/ui/vueSetup.js'; -import '/imports/ui/styles/stylesIndex.js'; -import '/imports/client/config.js'; -import '/imports/client/serviceWorker.js'; +import '/imports/api/simpleSchemaConfig'; +import '/imports/client/ui/vueSetup'; +import '/imports/client/ui/styles/stylesIndex'; +import '/imports/client/config'; +import '/imports/client/serviceWorker'; import 'ngraph.graph'; diff --git a/app/exampleMeteorSettings.json b/app/exampleMeteorSettings.json index 76d4108c..56083303 100644 --- a/app/exampleMeteorSettings.json +++ b/app/exampleMeteorSettings.json @@ -1,6 +1,7 @@ { "public": { "environment": "production", - "disablePatreon": true + "disablePatreon": true, + "disallowCreatureApiImport": false } -} +} \ No newline at end of file diff --git a/app/imports/@types/ddp.d.ts b/app/imports/@types/ddp.d.ts new file mode 100644 index 00000000..483f0cc0 --- /dev/null +++ b/app/imports/@types/ddp.d.ts @@ -0,0 +1,3 @@ +declare namespace DDP { + function randomStream(seed: string): typeof Random; +} \ No newline at end of file diff --git a/app/imports/@types/meteor-ostrio-files.d.ts b/app/imports/@types/meteor-ostrio-files.d.ts new file mode 100644 index 00000000..09a7bd18 --- /dev/null +++ b/app/imports/@types/meteor-ostrio-files.d.ts @@ -0,0 +1,244 @@ +declare module 'meteor/ostrio:files' { + import { Meteor } from 'meteor/meteor'; + import { Mongo } from 'meteor/mongo'; + import { ReactiveVar } from 'meteor/reactive-var'; + import { SimpleSchemaDefinition } from 'simpl-schema'; + import * as http from 'http'; + import { IncomingMessage } from 'connect'; + + interface Params { + _id: string; + query: { [key: string]: string }; + name: string; + version: string; + } + + interface ContextHTTP { + request: IncomingMessage; + response: http.ServerResponse; + params: Params; + } + + interface ContextUser { + userId: string; + user: () => Meteor.User; + } + + interface ContextUpload { + file: object; + /** On server only. */ + chunkId?: number; + /** On server only. */ + eof?: boolean; + } + + interface Version { + extension: string; + meta: MetadataType; + path: string; + size: number; + type: string; + } + + class FileObj { + _id: string; + size: number; + name: string; + type: string; + path: string; + isVideo: boolean; + isAudio: boolean; + isImage: boolean; + isText: boolean; + isJSON: boolean; + isPDF: boolean; + ext?: string; + extension?: string; + extensionWithDot: string; + _storagePath: string; + _downloadRoute: string; + _collectionName: string; + public?: boolean; + meta?: MetadataType; + userId?: string; + updatedAt?: Date; + versions: { + [propName: string]: Version; + }; + mime: string; + 'mime-type': string; + } + + class FileRef extends FileObj { + remove: (callback?: (error: Meteor.Error) => void) => void; + link: (version?: string, location?: string) => string; + get: (property?: string) => any; + fetch: () => Array>; + with: () => FileCursor; + } + + interface FileData { + size: number; + type: string; + mime: string; + 'mime-type': string; + ext: string; + extension: string; + name: string; + meta: MetadataType; + } + + interface FilesCollectionConfig { + storagePath?: string | ((fileObj: FileObj) => string); + collection?: Mongo.Collection>; + collectionName?: string; + continueUploadTTL?: string; + ddp?: object; + cacheControl?: string; + responseHeaders?: { [x: string]: string } | ((responseCode?: string, fileRef?: FileRef, versionRef?: Version, version?: string) => { [x: string]: string }); + throttle?: number | boolean; + downloadRoute?: string; + schema?: SimpleSchemaDefinition; + chunkSize?: number; + namingFunction?: (fileObj: FileObj) => string; + permissions?: number; + parentDirPermissions?: number; + integrityCheck?: boolean; + strict?: boolean; + downloadCallback?: (this: ContextHTTP & ContextUser, fileObj: FileObj) => boolean; + protected?: boolean | ((this: ContextHTTP & ContextUser, fileObj: FileObj) => boolean | number); + public?: boolean; + onBeforeUpload?: (this: ContextUpload & ContextUser, fileData: FileData) => boolean | string; + onBeforeRemove?: (this: ContextUser, cursor: Mongo.Cursor>) => boolean; + onInitiateUpload?: (this: ContextUpload & ContextUser, fileData: FileData) => void; + onAfterUpload?: (fileRef: FileRef) => any; + onAfterRemove?: (files: ReadonlyArray>) => any; + onbeforeunloadMessage?: string | (() => string); + allowClientCode?: boolean; + debug?: boolean; + interceptDownload?: (http: object, fileRef: FileRef, version: string) => boolean; + } + + interface SearchOptions { + sort?: Mongo.SortSpecifier; + skip?: number; + limit?: number; + fields?: Mongo.FieldSpecifier; + reactive?: boolean; + transform?: (fileObj: FileObj) => FileObj & TransformAdditions; + } + + interface InsertOptions { + file: File | object | string; + fileId?: string; + fileName?: string; + isBase64?: boolean; + meta?: MetadataType; + transport?: 'ddp' | 'http'; + ddp?: object; + onStart?: (error: Meteor.Error, fileData: FileData) => any; + onUploaded?: (error: Meteor.Error, fileRef: FileRef) => any; + onAbort?: (fileData: FileData) => any; + onError?: (error: Meteor.Error, fileData: FileData) => any; + onProgress?: (progress: number, fileData: FileData) => any; + onBeforeUpload?: (fileData: FileData) => any; + chunkSize?: number | 'dynamic'; + allowWebWorkers?: boolean; + type?: string; + } + + interface LoadOptions { + fileName: string; + meta?: MetadataType; + type?: string; + size?: number; + userId?: string; + fileId?: string; + } + + class FileUpload { + file: File; + onPause: ReactiveVar; + progress: ReactiveVar; + estimateTime: ReactiveVar; + estimateSpeed: ReactiveVar; + state: ReactiveVar<'active' | 'paused' | 'aborted' | 'completed'>; + pause(): void; + continue(): void; + toggle(): void; + pipe(): void; + start(): void; + on(event: string, callback: () => void): void; + } + + class FileCursor extends FileRef { } + + class FilesCursor extends Mongo.Cursor> { + cursor: Mongo.Cursor>; // Refers to base cursor? Why is this existing? + + get(): Array & TransformAdditions>; + hasNext(): boolean; + next(): FileCursor & TransformAdditions; + hasPrevious(): boolean; + previous(): FileCursor & TransformAdditions; + first(): FileCursor & TransformAdditions; + last(): FileCursor & TransformAdditions; + remove(callback?: (err: object) => void): void; + each(callback: (cursor: FileCursor & TransformAdditions) => void): void; + current(): object | undefined; + } + + class FilesCollection { + collection: Mongo.Collection>; + schema: SimpleSchemaDefinition; + + constructor(config: FilesCollectionConfig) + + /** + * Find and return Cursor for matching documents. + * + * @param selector [[http://docs.meteor.com/api/collections.html#selectors | Mongo-Style selector]] + * @param options [[http://docs.meteor.com/api/collections.html#sortspecifiers | Mongo-Style selector Options]] + * + * @template TransformAdditions Additional properties provided by transforming a document with options.tranform(). + * Note that removing fields with a transform function is not currently supported as this may break + * functions defined on a FileRef or FileCursor. + */ + find( + selector?: Mongo.Selector>>, + options?: SearchOptions + ): FilesCursor; + + /** + * Finds the first document that matches the selector, as ordered by sort and skip options. + * + * @param selector [[http://docs.meteor.com/api/collections.html#selectors | Mongo-Style selector]] + * @param options [[http://docs.meteor.com/api/collections.html#sortspecifiers | Mongo-Style selector Options]] + * + * @template TransformAdditions Additional properties provided by transforming a document with options.tranform(). + * Note that removing fields with a transform function is not currently supported as this may break + * functions defined on a FileRef or FileCursor. + */ + findOne( + selector?: Mongo.Selector>> | string, + options?: SearchOptions + ): FileCursor & TransformAdditions; + + insert(settings: InsertOptions, autoStart?: boolean): FileUpload; + remove(select: Mongo.Selector> | string, callback?: (error: Meteor.Error) => void): FilesCollection; + update(select: Mongo.Selector> | string, modifier: Mongo.Modifier>, options?: { + multi?: boolean; + upsert?: boolean; + arrayFilters?: Array<{ [identifier: string]: any }>; + }, callback?: (error: Meteor.Error, insertedCount: number) => void): FilesCollection; + link(fileRef: FileRef, version?: string): string; + allow(options: Mongo.AllowDenyOptions): void; + deny(options: Mongo.AllowDenyOptions): void; + denyClient(): void; + on(event: string, callback: (fileRef: FileRef) => void): void; + unlink(fileRef: FileRef, version?: string): FilesCollection; + addFile(path: string, opts: LoadOptions, callback?: (err: any, fileRef: FileRef) => any, proceedAfterUpload?: boolean): FilesCollection; + load(url: string, opts: LoadOptions, callback?: (err: object, fileRef: FileRef) => any, proceedAfterUpload?: boolean): FilesCollection; + write(buffer: Buffer, opts: LoadOptions, callback?: (err: object, fileRef: FileRef) => any, proceedAfterUpload?: boolean): FilesCollection; + } +} \ No newline at end of file diff --git a/app/imports/@types/meteor.d.ts b/app/imports/@types/meteor.d.ts new file mode 100644 index 00000000..d590b723 --- /dev/null +++ b/app/imports/@types/meteor.d.ts @@ -0,0 +1,7 @@ +declare module 'meteor/meteor' { + namespace Meteor { + interface User { + roles?: string[]; + } + } +} \ No newline at end of file diff --git a/app/imports/@types/mongo.d.ts b/app/imports/@types/mongo.d.ts new file mode 100644 index 00000000..a5623ebc --- /dev/null +++ b/app/imports/@types/mongo.d.ts @@ -0,0 +1,7 @@ +declare namespace Mongo { + interface CollectionStatic { + get: ( + collectionName: string, options?: { connection: Meteor.Connection } + ) => Mongo.Collection; + } +} diff --git a/app/imports/@types/validated-method.d.ts b/app/imports/@types/validated-method.d.ts new file mode 100644 index 00000000..4c26e353 --- /dev/null +++ b/app/imports/@types/validated-method.d.ts @@ -0,0 +1,27 @@ +declare module 'meteor/mdg:validated-method' { + interface ValidatedMethodOptionsMixinFields { + rateLimit: { + numRequests: number, + timeInterval: number, + }; + } + type Return = TFunc extends (...args: any[]) => infer TReturn ? TReturn : never; + type Argument = TFunc extends (...args: infer TArgs) => any ? TArgs extends [infer TArg] ? TArg + : NoArguments + : never; + interface ValidatedMethod any> { + callAsync: Argument extends NoArguments + // methods with no argument can be called with () or just a callback + ? + & ((unusedArg: any, callback: (error: Meteor.Error, result: Return) => void) => void) + & ((callback: (error: Meteor.Error | undefined, result: Return) => void) => void) + & (() => Return) + // methods with arguments require those arguments to be called + : + & (( + arg: Argument, + callback: (error: Meteor.Error | undefined, result: Return) => void, + ) => void) + & ((arg: Argument) => Return); + } +} \ No newline at end of file diff --git a/app/imports/@types/vue-meteor.d.ts b/app/imports/@types/vue-meteor.d.ts new file mode 100644 index 00000000..744b2dd5 --- /dev/null +++ b/app/imports/@types/vue-meteor.d.ts @@ -0,0 +1,15 @@ +import Vue from 'vue'; + +declare module 'vue/types/options' { + interface ComponentOptions { + meteor?: any; + } +} + +declare module 'vue/types/vue' { + interface Vue { + $subscribe: (name: string, params: any[]) => void; + $autorun: (fn: () => void) => number; + $subReady: Record; + } +} \ No newline at end of file diff --git a/app/imports/api/creature/archive/ArchiveCreatureFiles.js b/app/imports/api/creature/archive/ArchiveCreatureFiles.js index 1b699c0b..10280566 100644 --- a/app/imports/api/creature/archive/ArchiveCreatureFiles.js +++ b/app/imports/api/creature/archive/ArchiveCreatureFiles.js @@ -1,18 +1,28 @@ -import { createS3FilesCollection } from '/imports/api/files/s3FileStorage.js'; + import SimpleSchema from 'simpl-schema'; -import { incrementFileStorageUsed } from '/imports/api/users/methods/updateFileStorageUsed.js'; -import { CreaturePropertySchema } from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import { CreatureSchema } from '/imports/api/creature/creatures/Creatures.js'; +import { incrementFileStorageUsed } from '/imports/api/users/methods/updateFileStorageUsed'; +import { CreaturePropertySchema } from '/imports/api/creature/creatureProperties/CreatureProperties'; +import { CreatureSchema } from '/imports/api/creature/creatures/Creatures'; +import assertUserHasFileSpace from '/imports/api/files/assertUserHasFileSpace'; +let createS3FilesCollection; +if (Meteor.isServer) { + createS3FilesCollection = require('/imports/api/files/server/s3FileStorage').createS3FilesCollection +} else { + createS3FilesCollection = require('/imports/api/files/client/s3FileStorage').createS3FilesCollection +} const ArchiveCreatureFiles = createS3FilesCollection({ collectionName: 'archiveCreatureFiles', - storagePath: Meteor.isDevelopment ? '/DiceCloud/archiveCreatures/' : 'assets/app/archiveCreatures', + storagePath: Meteor.isDevelopment ? '../../../../../fileStorage/archiveCreatures' : 'assets/app/archiveCreatures', onBeforeUpload(file) { // Allow upload files under 10MB, and only in json format if (file.size > 10485760) { return 'Please upload with size equal or less than 10MB'; } - if (!/json/i.test(file.extension)){ + // Make sure the user has enough space + assertUserHasFileSpace(Meteor.userId(), file.size); + // Only accept JSON + if (!/json/i.test(file.extension)) { return 'Please upload only a JSON file'; } return true; diff --git a/app/imports/api/creature/archive/methods/archiveCreatureToFile.js b/app/imports/api/creature/archive/methods/archiveCreatureToFile.js index 92b6f3bd..a58ddb47 100644 --- a/app/imports/api/creature/archive/methods/archiveCreatureToFile.js +++ b/app/imports/api/creature/archive/methods/archiveCreatureToFile.js @@ -1,21 +1,24 @@ -import SCHEMA_VERSION from '/imports/constants/SCHEMA_VERSION.js'; +import { Meteor } from 'meteor/meteor'; +import SCHEMA_VERSION from '/imports/constants/SCHEMA_VERSION'; import SimpleSchema from 'simpl-schema'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import { assertOwnership } from '/imports/api/creature/creatures/creaturePermissions.js'; -import Creatures from '/imports/api/creature/creatures/Creatures.js'; -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js'; -import Experiences from '/imports/api/creature/experience/Experiences.js'; -import { removeCreatureWork } from '/imports/api/creature/creatures/methods/removeCreature.js'; -import ArchiveCreatureFiles from '/imports/api/creature/archive/ArchiveCreatureFiles.js'; +import { assertOwnership } from '/imports/api/creature/creatures/creaturePermissions'; +import Creatures from '/imports/api/creature/creatures/Creatures'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; +import CreatureLogs from '/imports/api/creature/log/CreatureLogs'; +import Experiences from '/imports/api/creature/experience/Experiences'; +import { removeCreatureWork } from '/imports/api/creature/creatures/methods/removeCreature'; +import ArchiveCreatureFiles from '/imports/api/creature/archive/ArchiveCreatureFiles'; +import { getFilter } from '/imports/api/parenting/parentingFunctions'; -export function getArchiveObj(creatureId){ +export function getArchiveObj(creatureId) { // Build the archive document const creature = Creatures.findOne(creatureId); - const properties = CreatureProperties.find({'ancestors.id': creatureId}).fetch(); - const experiences = Experiences.find({creatureId}).fetch(); - const logs = CreatureLogs.find({creatureId}).fetch(); + if (!creature) throw new Meteor.Error('creature-not-found', 'Creature not found'); + const properties = CreatureProperties.find({ ...getFilter.descendantsOfRoot(creatureId) }).fetch(); + const experiences = Experiences.find({ creatureId }).fetch(); + const logs = CreatureLogs.find({ creatureId }).fetch(); let archiveCreature = { meta: { type: 'DiceCloud V2 Creature Archive', @@ -31,7 +34,7 @@ export function getArchiveObj(creatureId){ return archiveCreature; } -export function archiveCreature(creatureId){ +export const archiveCreature = Meteor.wrapAsync(function archiveCreatureFn(creatureId, callback) { const archive = getArchiveObj(creatureId); const buffer = Buffer.from(JSON.stringify(archive, null, 2)); ArchiveCreatureFiles.write(buffer, { @@ -43,14 +46,33 @@ export function archiveCreature(creatureId){ creatureId: archive.creature._id, creatureName: archive.creature.name, }, - }, (error) => { - if (error){ - throw error; - } else { + }, (error, fileRef) => { + if (error) { + // If there is an error already, just call the callback + callback(error); + } else if (!Meteor.settings.useS3) { + // If we aren't using s3, remove the creature and call the callback removeCreatureWork(creatureId); + callback(); + } else { + // Wait for s3Result event that occurs when the s3 attempt to write ends. + // If it's successful, remove the creature, otherwise callback with error + const resultHandler = (s3Error, resultRef) => { + // This event is for a different file, ignore it + if (resultRef._id !== fileRef._id) return; + // Remove this handler, we are only running it once for this fileId + ArchiveCreatureFiles.off('s3Result', resultHandler); + // Remove the creature if there was no error + if (!s3Error) { + removeCreatureWork(creatureId); + } + // Alert the callback that we're done + callback(s3Error); + } + ArchiveCreatureFiles.on('s3Result', resultHandler); } }, true); -} +}); const archiveCreatureToFile = new ValidatedMethod({ name: 'Creatures.methods.archiveCreatureToFile', @@ -65,10 +87,10 @@ const archiveCreatureToFile = new ValidatedMethod({ numRequests: 10, timeInterval: 5000, }, - async run({creatureId}) { + async run({ creatureId }) { assertOwnership(creatureId, this.userId); - if (Meteor.isServer){ - archiveCreature(creatureId, this.userId); + if (Meteor.isServer) { + archiveCreature(creatureId); } else { removeCreatureWork(creatureId); } diff --git a/app/imports/api/creature/archive/methods/index.js b/app/imports/api/creature/archive/methods/index.js index 491ba038..abb0d9b6 100644 --- a/app/imports/api/creature/archive/methods/index.js +++ b/app/imports/api/creature/archive/methods/index.js @@ -1,3 +1,3 @@ -import '/imports/api/creature/archive/methods/archiveCreatureToFile.js'; -import '/imports/api/creature/archive/methods/restoreCreatureFromFile.js'; -import '/imports/api/creature/archive/methods/removeArchiveCreature.js'; +import '/imports/api/creature/archive/methods/archiveCreatureToFile'; +import '/imports/api/creature/archive/methods/restoreCreatureFromFile'; +import '/imports/api/creature/archive/methods/removeArchiveCreature'; diff --git a/app/imports/api/creature/archive/methods/removeArchiveCreature.js b/app/imports/api/creature/archive/methods/removeArchiveCreature.js index f4ba608b..c7a20fb2 100644 --- a/app/imports/api/creature/archive/methods/removeArchiveCreature.js +++ b/app/imports/api/creature/archive/methods/removeArchiveCreature.js @@ -1,8 +1,8 @@ import SimpleSchema from 'simpl-schema'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import ArchiveCreatureFiles from '/imports/api/creature/archive/ArchiveCreatureFiles.js'; -import { incrementFileStorageUsed } from '/imports/api/users/methods/updateFileStorageUsed.js'; +import ArchiveCreatureFiles from '/imports/api/creature/archive/ArchiveCreatureFiles'; +import { incrementFileStorageUsed } from '/imports/api/users/methods/updateFileStorageUsed'; const removeArchiveCreature = new ValidatedMethod({ name: 'ArchiveCreatureFiles.methods.removeArchiveCreature', diff --git a/app/imports/api/creature/archive/methods/restoreCreatureFromFile.js b/app/imports/api/creature/archive/methods/restoreCreatureFromFile.js index 9479b0c7..416782a0 100644 --- a/app/imports/api/creature/archive/methods/restoreCreatureFromFile.js +++ b/app/imports/api/creature/archive/methods/restoreCreatureFromFile.js @@ -1,24 +1,24 @@ -import SCHEMA_VERSION from '/imports/constants/SCHEMA_VERSION.js'; +import SCHEMA_VERSION from '/imports/constants/SCHEMA_VERSION'; import SimpleSchema from 'simpl-schema'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import Creatures from '/imports/api/creature/creatures/Creatures.js'; -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js'; -import Experiences from '/imports/api/creature/experience/Experiences.js'; -import { removeCreatureWork } from '/imports/api/creature/creatures/methods/removeCreature.js'; -import ArchiveCreatureFiles from '/imports/api/creature/archive/ArchiveCreatureFiles.js'; -import assertHasCharactersSlots from '/imports/api/creature/creatures/methods/assertHasCharacterSlots.js'; -import { incrementFileStorageUsed } from '/imports/api/users/methods/updateFileStorageUsed.js'; -import verifyArchiveSafety from '/imports/api/creature/archive/methods/verifyArchiveSafety.js'; +import Creatures from '/imports/api/creature/creatures/Creatures'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; +import CreatureLogs from '/imports/api/creature/log/CreatureLogs'; +import Experiences from '/imports/api/creature/experience/Experiences'; +import { removeCreatureWork } from '/imports/api/creature/creatures/methods/removeCreature'; +import ArchiveCreatureFiles from '/imports/api/creature/archive/ArchiveCreatureFiles'; +import assertHasCharactersSlots from '/imports/api/creature/creatures/methods/assertHasCharacterSlots'; +import { incrementFileStorageUsed } from '/imports/api/users/methods/updateFileStorageUsed'; +import verifyArchiveSafety from '/imports/api/creature/archive/methods/verifyArchiveSafety'; let migrateArchive; -if (Meteor.isServer){ - migrateArchive = require('/imports/migrations/server/migrateArchive.js').default; +if (Meteor.isServer) { + migrateArchive = require('/imports/migrations/archive/migrateArchive').default; } -function restoreCreature(archive, userId){ - if (SCHEMA_VERSION < archive.meta.schemaVersion){ +function restoreCreature(archive, userId) { + if (SCHEMA_VERSION < archive.meta.schemaVersion) { throw new Meteor.Error('Incompatible', 'The archive file is from a newer version. Update required to read.') } @@ -35,7 +35,7 @@ function restoreCreature(archive, userId){ }); if (existingCreature) throw new Meteor.Error('Already exists', 'The creature you are trying to restore already exists.') - + // Ensure the user owns the restored creature archive.creature.owner = userId; @@ -44,13 +44,13 @@ function restoreCreature(archive, userId){ Creatures.insert(archive.creature); try { // Add all the properties - if (archive.properties && archive.properties.length){ + if (archive.properties && archive.properties.length) { CreatureProperties.batchInsert(archive.properties); } - if (archive.experiences && archive.experiences.length){ + if (archive.experiences && archive.experiences.length) { Experiences.batchInsert(archive.experiences); } - if (archive.logs && archive.logs.length){ + if (archive.logs && archive.logs.length) { CreatureLogs.batchInsert(archive.logs); } } catch (e) { @@ -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); diff --git a/app/imports/api/creature/archive/methods/verifyArchiveSafety.js b/app/imports/api/creature/archive/methods/verifyArchiveSafety.js index 734b2582..a7836db7 100644 --- a/app/imports/api/creature/archive/methods/verifyArchiveSafety.js +++ b/app/imports/api/creature/archive/methods/verifyArchiveSafety.js @@ -1,7 +1,7 @@ import { slice } from 'lodash'; -import PER_CREATURE_LOG_LIMIT from '/imports/api/creature/log/CreatureLogs.js'; +import { PER_CREATURE_LOG_LIMIT } from '/imports/api/creature/log/CreatureLogs'; -export default function verifyArchiveSafety({ meta, creature, properties, experiences, logs }){ +export default function verifyArchiveSafety({ creature, properties, experiences, logs }) { const creatureId = creature._id; // Check lengths of arrays @@ -21,7 +21,7 @@ export default function verifyArchiveSafety({ meta, creature, properties, experi } }); properties.forEach(prop => { - if (prop.ancestors[0].id !== creatureId) { + if (prop.root?.id !== creatureId) { throw new Meteor.Error('Malicious prop', 'Properties contains an entry for the wrong creature'); } }); diff --git a/app/imports/api/creature/creatureFolders/CreatureFolders.js b/app/imports/api/creature/creatureFolders/CreatureFolders.js index e0e784cc..6339014a 100644 --- a/app/imports/api/creature/creatureFolders/CreatureFolders.js +++ b/app/imports/api/creature/creatureFolders/CreatureFolders.js @@ -1,5 +1,5 @@ import SimpleSchema from 'simpl-schema'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; let CreatureFolders = new Mongo.Collection('creatureFolders'); @@ -35,5 +35,5 @@ let creatureFolderSchema = new SimpleSchema({ CreatureFolders.attachSchema(creatureFolderSchema); -import '/imports/api/creature/creatureFolders/methods.js/index.js'; +import '/imports/api/creature/creatureFolders/methods.js/index'; export default CreatureFolders; diff --git a/app/imports/api/creature/creatureFolders/methods.js/index.js b/app/imports/api/creature/creatureFolders/methods.js/index.js index 26782576..94a5bbc7 100644 --- a/app/imports/api/creature/creatureFolders/methods.js/index.js +++ b/app/imports/api/creature/creatureFolders/methods.js/index.js @@ -1,4 +1,4 @@ -import '/imports/api/creature/creatureFolders/methods.js/insertCreatureFolder.js'; -import '/imports/api/creature/creatureFolders/methods.js/updateCreatureFolderName.js'; -import '/imports/api/creature/creatureFolders/methods.js/removeCreatureFolder.js'; -import '/imports/api/creature/creatureFolders/methods.js/moveCreatureToFolder.js'; +import '/imports/api/creature/creatureFolders/methods.js/insertCreatureFolder'; +import '/imports/api/creature/creatureFolders/methods.js/updateCreatureFolderName'; +import '/imports/api/creature/creatureFolders/methods.js/removeCreatureFolder'; +import '/imports/api/creature/creatureFolders/methods.js/moveCreatureToFolder'; diff --git a/app/imports/api/creature/creatureFolders/methods.js/insertCreatureFolder.js b/app/imports/api/creature/creatureFolders/methods.js/insertCreatureFolder.js index 8df44d40..ebae17f4 100644 --- a/app/imports/api/creature/creatureFolders/methods.js/insertCreatureFolder.js +++ b/app/imports/api/creature/creatureFolders/methods.js/insertCreatureFolder.js @@ -1,4 +1,4 @@ -import CreatureFolders from '/imports/api/creature/creatureFolders/CreatureFolders.js'; +import CreatureFolders from '/imports/api/creature/creatureFolders/CreatureFolders'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; @@ -15,23 +15,23 @@ const insertCreatureFolder = new ValidatedMethod({ let userId = this.userId; if (!userId) { throw new Meteor.Error('creatureFolders.methods.insert.denied', - 'You need to be logged in to insert a folder'); + 'You need to be logged in to insert a folder'); } // Limit folders to 50 per user let existingFolders = CreatureFolders.find({ owner: userId }, { - fields: {order: 1}, - sort: {order :-1} + fields: { order: 1 }, + sort: { left: -1 } }); - if (existingFolders.count() >= 50){ + if (existingFolders.count() >= 50) { throw new Meteor.Error('creatureFolders.methods.insert.denied', - 'You can not have more than 50 folders'); + 'You can not have more than 50 folders'); } // Make the new folder the last in the order let order = 0; let lastFolder = existingFolders.fetch()[0]; - if (lastFolder){ + if (lastFolder) { order = (lastFolder.order || 0) + 1; } // Insert diff --git a/app/imports/api/creature/creatureFolders/methods.js/moveCreatureToFolder.js b/app/imports/api/creature/creatureFolders/methods.js/moveCreatureToFolder.js index 46f9b696..2fa90c49 100644 --- a/app/imports/api/creature/creatureFolders/methods.js/moveCreatureToFolder.js +++ b/app/imports/api/creature/creatureFolders/methods.js/moveCreatureToFolder.js @@ -1,4 +1,4 @@ -import CreatureFolders from '/imports/api/creature/creatureFolders/CreatureFolders.js'; +import CreatureFolders from '/imports/api/creature/creatureFolders/CreatureFolders'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; @@ -10,33 +10,33 @@ const moveCreatureToFolder = new ValidatedMethod({ numRequests: 5, timeInterval: 5000, }, - run({creatureId, folderId}) { + run({ creatureId, folderId }) { // Ensure logged in let userId = this.userId; if (!userId) { throw new Meteor.Error('creatureFolders.methods.updateName.denied', - 'You need to be logged in to remove a folder'); + 'You need to be logged in to remove a folder'); } // Check that this folder is owned by the user - if (folderId){ + if (folderId) { let existingFolder = CreatureFolders.findOne(folderId); - if (existingFolder.owner !== userId){ + if (existingFolder.owner !== userId) { throw new Meteor.Error('creatureFolders.methods.updateName.denied', - 'This folder does not belong to you'); + 'This folder does not belong to you'); } } // Remove from other folders CreatureFolders.update({ owner: userId }, { - $pull: {creatures: creatureId}, + $pull: { creatures: creatureId }, }, { multi: true, }); - if (folderId){ + if (folderId) { // Add to this folder CreatureFolders.update(folderId, { - $addToSet: {creatures: creatureId}, + $addToSet: { creatures: creatureId }, }); } }, diff --git a/app/imports/api/creature/creatureFolders/methods.js/removeCreatureFolder.js b/app/imports/api/creature/creatureFolders/methods.js/removeCreatureFolder.js index 71006cb7..4502fcb6 100644 --- a/app/imports/api/creature/creatureFolders/methods.js/removeCreatureFolder.js +++ b/app/imports/api/creature/creatureFolders/methods.js/removeCreatureFolder.js @@ -1,4 +1,4 @@ -import CreatureFolders from '/imports/api/creature/creatureFolders/CreatureFolders.js'; +import CreatureFolders from '/imports/api/creature/creatureFolders/CreatureFolders'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; @@ -10,18 +10,18 @@ const removeCreatureFolder = new ValidatedMethod({ numRequests: 5, timeInterval: 5000, }, - run({_id}) { + run({ _id }) { // Ensure logged in let userId = this.userId; if (!userId) { throw new Meteor.Error('creatureFolders.methods.updateName.denied', - 'You need to be logged in to remove a folder'); + 'You need to be logged in to remove a folder'); } // Check that this folder is owned by the user let existingFolder = CreatureFolders.findOne(_id); - if (existingFolder.owner !== userId){ + if (existingFolder.owner !== userId) { throw new Meteor.Error('creatureFolders.methods.updateName.denied', - 'This folder does not belong to you'); + 'This folder does not belong to you'); } // Remove return CreatureFolders.remove(_id); diff --git a/app/imports/api/creature/creatureFolders/methods.js/reorderCreatureFolder.js b/app/imports/api/creature/creatureFolders/methods.js/reorderCreatureFolder.js index 92ecb418..4b309819 100644 --- a/app/imports/api/creature/creatureFolders/methods.js/reorderCreatureFolder.js +++ b/app/imports/api/creature/creatureFolders/methods.js/reorderCreatureFolder.js @@ -1,4 +1,4 @@ -import CreatureFolders from '/imports/api/creature/creatureFolders/CreatureFolders.js'; +import CreatureFolders from '/imports/api/creature/creatureFolders/CreatureFolders'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; @@ -10,31 +10,31 @@ const reorderCreatureFolder = new ValidatedMethod({ numRequests: 5, timeInterval: 5000, }, - run({_id, order}) { + run({ _id, order }) { // Ensure logged in let userId = this.userId; if (!userId) { throw new Meteor.Error('creatureFolders.methods.reorder.denied', - 'You need to be logged in to reorder a folder'); + 'You need to be logged in to reorder a folder'); } // Check that this folder is owned by the user let existingFolder = CreatureFolders.findOne(_id); - if (existingFolder.owner !== userId){ + if (existingFolder.owner !== userId) { throw new Meteor.Error('creatureFolders.methods.reorder.denied', - 'This folder does not belong to you'); + 'This folder does not belong to you'); } // First give it the new order, it should end in 0.5 putting it between two other docs - CreatureFolders.update(_id, {$set: {order}}); + CreatureFolders.update(_id, { $set: { order } }); this.unblock(); // Reorder all the folders with integer numbers in this new order CreatureFolders.find({ owner: userId }, { - fields: {order: 1,}, - sort: {order: -1} + fields: { order: 1, }, + sort: { order: 1 } }).forEach((folder, index) => { - if (folder.order !== index){ - CreatureFolders.update(_id, {$set: {order: index}}) + if (folder.order !== index) { + CreatureFolders.update(_id, { $set: { order: index } }) } }); }, diff --git a/app/imports/api/creature/creatureFolders/methods.js/updateCreatureFolderName.js b/app/imports/api/creature/creatureFolders/methods.js/updateCreatureFolderName.js index d798e59a..4fbb2a93 100644 --- a/app/imports/api/creature/creatureFolders/methods.js/updateCreatureFolderName.js +++ b/app/imports/api/creature/creatureFolders/methods.js/updateCreatureFolderName.js @@ -1,4 +1,4 @@ -import CreatureFolders from '/imports/api/creature/creatureFolders/CreatureFolders.js'; +import CreatureFolders from '/imports/api/creature/creatureFolders/CreatureFolders'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; @@ -10,21 +10,21 @@ const updateCreatureFolderName = new ValidatedMethod({ numRequests: 5, timeInterval: 5000, }, - run({_id, name}) { + run({ _id, name }) { // Ensure logged in let userId = this.userId; if (!userId) { throw new Meteor.Error('creatureFolders.methods.updateName.denied', - 'You need to be logged in to update a folder'); + 'You need to be logged in to update a folder'); } // Check that this folder is owned by the user let existingFolder = CreatureFolders.findOne(_id); - if (existingFolder.owner !== userId){ + if (existingFolder.owner !== userId) { throw new Meteor.Error('creatureFolders.methods.updateName.denied', - 'This folder does not belong to you'); + 'This folder does not belong to you'); } // Update - return CreatureFolders.update(_id, {$set: {name}}); + return CreatureFolders.update(_id, { $set: { name } }); }, }); diff --git a/app/imports/api/creature/creatureProperties/CreatureProperties.js b/app/imports/api/creature/creatureProperties/CreatureProperties.ts similarity index 53% rename from app/imports/api/creature/creatureProperties/CreatureProperties.js rename to app/imports/api/creature/creatureProperties/CreatureProperties.ts index ff301a79..c346e8c6 100644 --- a/app/imports/api/creature/creatureProperties/CreatureProperties.js +++ b/app/imports/api/creature/creatureProperties/CreatureProperties.ts @@ -1,15 +1,36 @@ import { Mongo } from 'meteor/mongo'; import SimpleSchema from 'simpl-schema'; -import ColorSchema from '/imports/api/properties/subSchemas/ColorSchema.js'; -import ChildSchema from '/imports/api/parenting/ChildSchema.js'; -import SoftRemovableSchema from '/imports/api/parenting/SoftRemovableSchema.js'; -import propertySchemasIndex from '/imports/api/properties/computedPropertySchemasIndex.js'; -import { storedIconsSchema } from '/imports/api/icons/Icons.js'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; +import ColorSchema from '/imports/api/properties/subSchemas/ColorSchema'; +import ChildSchema, { TreeDoc } from '/imports/api/parenting/ChildSchema'; +import SoftRemovableSchema from '/imports/api/parenting/SoftRemovableSchema'; +import propertySchemasIndex from '/imports/api/properties/computedPropertySchemasIndex'; +import { storedIconsSchema } from '/imports/api/icons/Icons'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; -let CreatureProperties = new Mongo.Collection('creatureProperties'); +// TODO make this a union type of all CreatureProperty types +const CreatureProperties: Mongo.Collection = new Mongo.Collection('creatureProperties'); -let CreaturePropertySchema = new SimpleSchema({ +export interface CreatureProperty extends TreeDoc { + _id: string + _migrationError?: string + tags: string[] + type: string + disabled?: boolean + icon?: { + name: string + shape: string + }, + libraryNodeId?: string + slotQuantityFilled?: number + inactive?: boolean + deactivatedByAncestor?: boolean + deactivatedBySelf?: boolean + deactivatedByToggle?: boolean + deactivatingToggleId?: boolean + dirty?: boolean +} + +const CreaturePropertySchema = new SimpleSchema({ _id: { type: String, regEx: SimpleSchema.RegEx.Id, @@ -46,11 +67,17 @@ 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({ // Denormalised flag if this property is inactive on the sheet for any reason - // Including being disabled, or a decendent of a disabled property + // Including being disabled, or a descendant of a disabled property inactive: { type: Boolean, optional: true, @@ -82,6 +109,42 @@ const DenormalisedOnlyCreaturePropertySchema = new SimpleSchema({ index: 1, removeBeforeCompute: true, }, + deactivatingToggleId: { + type: String, + regEx: SimpleSchema.RegEx.Id, + optional: true, + removeBeforeCompute: true, + }, + // Triggers that fire when this property is applied + 'triggerIds': { + type: Object, + optional: true, + removeBeforeCompute: true, + }, + 'triggerIds.before': { + type: Array, + optional: true, + }, + 'triggerIds.before.$': { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, + 'triggerIds.after': { + type: Array, + optional: true, + }, + 'triggerIds.after.$': { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, + 'triggerIds.afterChildren': { + type: Array, + optional: true, + }, + 'triggerIds.afterChildren.$': { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, // When this is true on any property, the creature needs to be recomputed dirty: { type: Boolean, @@ -93,13 +156,20 @@ const DenormalisedOnlyCreaturePropertySchema = new SimpleSchema({ CreaturePropertySchema.extend(DenormalisedOnlyCreaturePropertySchema); -for (let key in propertySchemasIndex) { - let schema = new SimpleSchema({}); +for (const key in propertySchemasIndex) { + const schema = new SimpleSchema({}); schema.extend(propertySchemasIndex[key]); schema.extend(CreaturePropertySchema); schema.extend(ColorSchema); schema.extend(ChildSchema); schema.extend(SoftRemovableSchema); + // Use the any schema as a default schema for the collection + if (key === 'any') { + // @ts-expect-error don't have types for .attachSchema + CreatureProperties.attachSchema(schema); + } + // TODO make this an else branch and remove all {selector: {type: any}} options + // @ts-expect-error don't have types for .attachSchema CreatureProperties.attachSchema(schema, { selector: { type: key } }); diff --git a/app/imports/api/creature/creatureProperties/getRootCreatureAncestor.js b/app/imports/api/creature/creatureProperties/getRootCreatureAncestor.js index c4e2c862..d1e62ab3 100644 --- a/app/imports/api/creature/creatureProperties/getRootCreatureAncestor.js +++ b/app/imports/api/creature/creatureProperties/getRootCreatureAncestor.js @@ -1,5 +1,5 @@ -import Creatures from '/imports/api/creature/creatures/Creatures.js'; +import { getCreature } from '/imports/api/engine/loadCreatures'; -export default function getRootCreatureAncestor(property){ - return Creatures.findOne(property.ancestors[0].id); +export default function getRootCreatureAncestor(property) { + return getCreature(property.root.id); } diff --git a/app/imports/api/creature/creatureProperties/methods/adjustQuantity.js b/app/imports/api/creature/creatureProperties/methods/adjustQuantity.js index d08c43bb..8ed733c3 100644 --- a/app/imports/api/creature/creatureProperties/methods/adjustQuantity.js +++ b/app/imports/api/creature/creatureProperties/methods/adjustQuantity.js @@ -1,9 +1,9 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import SimpleSchema from 'simpl-schema'; -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js'; -import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; +import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor'; +import { assertEditPermission } from '/imports/api/sharing/sharingPermissions'; const adjustQuantity = new ValidatedMethod({ name: 'creatureProperties.adjustQuantity', diff --git a/app/imports/api/creature/creatureProperties/methods/copyPropertyToLibrary.js b/app/imports/api/creature/creatureProperties/methods/copyPropertyToLibrary.js new file mode 100644 index 00000000..838e14c0 --- /dev/null +++ b/app/imports/api/creature/creatureProperties/methods/copyPropertyToLibrary.js @@ -0,0 +1,182 @@ +import SimpleSchema from 'simpl-schema'; +import { ValidatedMethod } from 'meteor/mdg:validated-method'; +import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; +import LibraryNodes from '/imports/api/library/LibraryNodes'; +import { RefSchema } from '/imports/api/parenting/ChildSchema'; +import { + assertEditPermission, + assertDocEditPermission, + assertCopyPermission +} from '/imports/api/sharing/sharingPermissions'; +import { + fetchDocByRef, + getFilter, + renewDocIds +} from '/imports/api/parenting/parentingFunctions'; +import { rebuildNestedSets } from '/imports/api/parenting/parentingFunctions'; +import Libraries from '/imports/api/library/Libraries'; +const DUPLICATE_CHILDREN_LIMIT = 500; + +const copyPropertyToLibrary = new ValidatedMethod({ + name: 'creatureProperties.copyPropertyToLibrary', + validate: new SimpleSchema({ + propId: { + type: String, + 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 + const parentDoc = fetchDocByRef(parentRef); + + // Check permission to edit the destination + let rootLibrary; + if (parentRef.collection === 'libraries') { + rootLibrary = parentDoc; + } else if (parentRef.collection === 'libraryNodes') { + rootLibrary = Libraries.findOne(parentDoc.root.id) + } else { + throw `${parentRef.collection} is not a valid parent collection` + } + assertEditPermission(rootLibrary, this.userId); + + const insertedRootNode = insertNodeFromProperty(propId, order, this); + + // Tree structure changed by inserts, reorder the tree + rebuildNestedSets(LibraryNodes, rootLibrary._id); + + // Return the docId of the inserted root property + return insertedRootNode?._id; + }, +}); + +function insertNodeFromProperty(propId, order, method) { + // Fetch the property and its descendants, provided they have not been + // removed + let prop = CreatureProperties.findOne({ + _id: propId, + removed: { $ne: true }, + }); + if (!prop) { + if (Meteor.isClient) return; + else { + throw new Meteor.Error( + 'Insert property from library failed', + `No property with id '${propId}' was found` + ); + } + } + + // Make sure we can edit this property + assertDocEditPermission(prop, method.userId); + + let oldParentId = prop.parentId; + const propCursor = CreatureProperties.find({ + ...getFilter.descendants(prop), + removed: { $ne: true }, + }); + + // Make sure there aren't too many descendants + if (propCursor.count() > DUPLICATE_CHILDREN_LIMIT) { + throw new Meteor.Error('Copy children limit', + `The property has over ${DUPLICATE_CHILDREN_LIMIT} descendants and cannot be copied`); + } + + let props = propCursor.fetch(); + + // The root prop is first in the array of props + // It must get the first generated ID to prevent flickering + props = [prop, ...props]; + + // If the docs came from a library, that library must consent to this user copying their + // properties + assertSourceLibraryCopyPermission(props, method); + + // Give the docs new IDs without breaking internal references + renewDocIds({ + docArray: props, + collectionMap: { 'creatureProperties': 'libraryNodes' } + }); + + // Order the root node + prop.left = Number.MAX_SAFE_INTEGER - 1; + prop.right = Number.MAX_SAFE_INTEGER; + + // Clean the props + props = cleanProps(props); + + // Insert the props as library nodes + LibraryNodes.batchInsert(props); + return prop; +} + +/** + * + * @param props The properties to check + * @param userId The userId trying to copy these properties to a library + * Checks that every property can be copied out of the library that originated it by this user + */ +function assertSourceLibraryCopyPermission(props, method) { + // Skip on the client + if (method.isSimulation) return; + + // Get all the library node ids that are sources for these properties + const libraryNodeIds = []; + props.forEach(prop => { + if (prop.libraryNodeId) libraryNodeIds.push(prop.libraryNodeId); + }); + if (!libraryNodeIds.length) return; + + // Get the actual library Ids that each of these source nodes came from + const sourceLibIds = new Set(); + LibraryNodes.find({ + _id: { $in: libraryNodeIds } + }, { + fields: { root: 1 } + }).forEach(node => { + sourceLibIds.add(node.root.id); + }); + + // Assert copy permission on each of those libraries + Libraries.find({ + _id: { $in: Array.from(sourceLibIds) } + }, { + fields: { + name: 1, + owner: 1, + readers: 1, + writers: 1, + public: 1, + readersCanCopy: 1, + } + }).forEach(lib => { + try { + assertCopyPermission(lib, method.userId); + } catch (e) { + throw new Meteor.Error('Copy permission denied', + `One of the properties you are copying comes from ${lib.name}, which you do not have permission to copy from`); + } + }); +} + +export function cleanProps(props) { + return props.map(prop => { + let schema = LibraryNodes.simpleSchema(prop); + return schema.clean(prop); + }); +} + +export default copyPropertyToLibrary; diff --git a/app/imports/api/creature/creatureProperties/methods/damageProperty.js b/app/imports/api/creature/creatureProperties/methods/damageProperty.js deleted file mode 100644 index 507d82bd..00000000 --- a/app/imports/api/creature/creatureProperties/methods/damageProperty.js +++ /dev/null @@ -1,139 +0,0 @@ -import { ValidatedMethod } from 'meteor/mdg:validated-method'; -import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import SimpleSchema from 'simpl-schema'; -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; -import { applyTriggers } from '/imports/api/engine/actions/applyTriggers.js'; -import ActionContext from '/imports/api/engine/actions/ActionContext.js'; - -const damageProperty = new ValidatedMethod({ - name: 'creatureProperties.damage', - validate: new SimpleSchema({ - _id: SimpleSchema.RegEx.Id, - operation: { - type: String, - allowedValues: ['set', 'increment'] - }, - value: Number, - }).validator(), - mixins: [RateLimiterMixin], - rateLimit: { - numRequests: 20, - timeInterval: 5000, - }, - run({ _id, operation, value }) { - - // Get action context - let prop = CreatureProperties.findOne(_id); - if (!prop) throw new Meteor.Error( - 'Damage property failed', 'Property doesn\'t exist' - ); - const creatureId = prop.ancestors[0].id; - const actionContext = new ActionContext(creatureId, [creatureId], this); - - // Check permissions - assertEditPermission(actionContext.creature, this.userId); - - // Check if property can take damage - let schema = CreatureProperties.simpleSchema(prop); - if (!schema.allowsKey('damage')) { - throw new Meteor.Error( - 'Damage property failed', - `Property of type "${prop.type}" can't be damaged` - ); - } - - // Replace the prop by its actionContext counterpart if possible - if (prop.variableName) { - const actionContextProp = actionContext.scope[prop.variableName]; - if (actionContextProp?._id === prop._id) { - prop = actionContextProp; - } - } - - const result = damagePropertyWork({ prop, operation, value, actionContext }); - - // Insert the log - actionContext.writeLog(); - return result; - }, -}); - -export function damagePropertyWork({ prop, operation, value, actionContext }) { - - // Save the value to the scope before applying the before triggers - if (operation === 'increment') { - if (value >= 0) { - actionContext.scope['$damage'] = value; - } else { - actionContext.scope['$healing'] = -value; - } - } else { - actionContext.scope['$set'] = value; - } - - applyTriggers(actionContext.triggers?.damageProperty?.before, prop, actionContext); - - // fetch the value from the scope after the before triggers, in case they changed them - if (operation === 'increment') { - if (value >= 0) { - value = actionContext.scope['$damage']; - } else { - value = -actionContext.scope['$healing']; - } - } else { - value = actionContext.scope['$set']; - } - - let damage, newValue, increment; - if (operation === 'set') { - const total = prop.total || 0; - // Set represents what we want the value to be after damage - // So we need the actual damage to get to that value - damage = total - value; - // Damage can't exceed total value - if (damage > total && !prop.ignoreLowerLimit) damage = total; - // Damage must be positive - if (damage < 0 && !prop.ignoreUpperLimit) damage = 0; - newValue = prop.total - damage; - // Write the results - CreatureProperties.update(prop._id, { - $set: { damage, value: newValue, dirty: true } - }, { - selector: prop - }); - // Also write it straight to the prop so that it is updated in the actionContext - prop.damage = damage; - prop.value = newValue; - } else if (operation === 'increment') { - let currentValue = prop.value || 0; - let currentDamage = prop.damage || 0; - increment = value; - // Can't increase damage above the remaining value - if (increment > currentValue && !prop.ignoreLowerLimit) increment = currentValue; - // Can't decrease damage below zero - if (-increment > currentDamage && !prop.ignoreUpperLimit) increment = -currentDamage; - damage = currentDamage + increment; - newValue = prop.total - damage; - // Write the results - CreatureProperties.update(prop._id, { - $inc: { damage: increment, value: -increment }, - $set: { dirty: true }, - }, { - selector: prop - }); - // Also write it straight to the prop so that it is updated in the actionContext - prop.damage += increment; - prop.value -= increment; - } - - applyTriggers(actionContext.triggers?.damageProperty?.after, prop, actionContext); - - if (operation === 'set') { - return damage; - } else if (operation === 'increment') { - return increment; - } -} - -export default damageProperty; diff --git a/app/imports/api/creature/creatureProperties/methods/duplicateProperty.js b/app/imports/api/creature/creatureProperties/methods/duplicateProperty.js index 5a8c84fe..8cd7c52b 100644 --- a/app/imports/api/creature/creatureProperties/methods/duplicateProperty.js +++ b/app/imports/api/creature/creatureProperties/methods/duplicateProperty.js @@ -1,18 +1,18 @@ import SimpleSchema from 'simpl-schema'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; -import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; +import { assertEditPermission } from '/imports/api/sharing/sharingPermissions'; +import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor'; import { - setLineageOfDocs, + getFilter, renewDocIds -} from '/imports/api/parenting/parenting.js'; -import { reorderDocs } from '/imports/api/parenting/order.js'; +} from '/imports/api/parenting/parentingFunctions'; +import { rebuildNestedSets } from '/imports/api/parenting/parentingFunctions'; var snackbar; if (Meteor.isClient) { snackbar = require( - '/imports/ui/components/snackbars/SnackbarQueue.js' + '/imports/client/ui/components/snackbars/SnackbarQueue' ).snackbar } @@ -33,22 +33,29 @@ const duplicateProperty = new ValidatedMethod({ }, run({ _id }) { let property = CreatureProperties.findOne(_id); - let creature = getRootCreatureAncestor(property); + if (!property) throw new Meteor.Error('not-found', 'The source property was not found'); + + const creature = getRootCreatureAncestor(property); assertEditPermission(creature, this.userId); // Renew the doc ID - let randomSrc = DDP.randomStream('duplicateProperty'); - let propertyId = randomSrc.id(); + const randomSrc = DDP.randomStream('duplicateProperty'); + const propertyId = randomSrc.id(); property._id = propertyId; + // Change the variableName so it isn't immediately overridden + if (property.variableName) { + property.variableName += 'Copy' + } + // Get all the descendants - let nodes = CreatureProperties.find({ - 'ancestors.id': _id, + const nodes = CreatureProperties.find({ + ...getFilter.descendants(property), removed: { $ne: true }, }, { limit: DUPLICATE_CHILDREN_LIMIT + 1, - sort: { order: 1 }, + sort: { left: 1 }, }).fetch(); // Alert the user if the limit was hit @@ -61,33 +68,28 @@ const duplicateProperty = new ValidatedMethod({ } } - // re-map all the ancestors - setLineageOfDocs({ - docArray: nodes, - newAncestry: [ - ...property.ancestors, - { id: propertyId, collection: 'creatureProperties' } - ], - oldParent: { id: _id, collection: 'creatureProperties' }, + // Give the docs new IDs without breaking internal references + const allNodes = [property, ...nodes]; + renewDocIds({ + docArray: allNodes, + idMap: { + [_id]: propertyId, + [propertyId]: propertyId, + }, }); - // Give the docs new IDs without breaking internal references - renewDocIds({ docArray: nodes }); - // Order the root node - property.order += 0.5; + property.left = Number.MAX_SAFE_INTEGER - 1; + property.right = Number.MAX_SAFE_INTEGER; // Mark the sheet as needing recompute property.dirty = true; // Insert the properties - CreatureProperties.batchInsert([property, ...nodes]); + CreatureProperties.batchInsert(allNodes); // Tree structure changed by inserts, reorder the tree - reorderDocs({ - collection: CreatureProperties, - ancestorId: property.ancestors[0].id, - }); + rebuildNestedSets(CreatureProperties, property.root.id); return propertyId; }, diff --git a/app/imports/api/creature/creatureProperties/methods/equipItem.js b/app/imports/api/creature/creatureProperties/methods/equipItem.js index 671908b3..27810d95 100644 --- a/app/imports/api/creature/creatureProperties/methods/equipItem.js +++ b/app/imports/api/creature/creatureProperties/methods/equipItem.js @@ -1,11 +1,11 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method'; -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; -import { organizeDoc } from '/imports/api/parenting/organizeMethods.js'; -import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js'; -import BUILT_IN_TAGS from '/imports/constants/BUILT_IN_TAGS.js'; -import getParentRefByTag from '/imports/api/creature/creatureProperties/methods/getParentRefByTag.js'; +import { assertEditPermission } from '/imports/api/sharing/sharingPermissions'; +import { organizeDoc } from '/imports/api/parenting/organizeMethods'; +import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor'; +import BUILT_IN_TAGS from '/imports/constants/BUILT_IN_TAGS'; +import getParentRefByTag from '/imports/api/creature/creatureProperties/methods/getParentRefByTag'; // Equipping or unequipping an item will also change its parent const equipItem = new ValidatedMethod({ @@ -36,7 +36,7 @@ const equipItem = new ValidatedMethod({ let parentRef = getParentRefByTag(creature._id, tag); if (!parentRef) parentRef = { id: creature._id, collection: 'creatures' }; - organizeDoc.call({ + organizeDoc.callAsync({ docRef: { id: _id, collection: 'creatureProperties', diff --git a/app/imports/api/creature/creatureProperties/methods/flipToggle.js b/app/imports/api/creature/creatureProperties/methods/flipToggle.js index dbd814ff..c97b3ac9 100644 --- a/app/imports/api/creature/creatureProperties/methods/flipToggle.js +++ b/app/imports/api/creature/creatureProperties/methods/flipToggle.js @@ -1,8 +1,8 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; -import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; +import { assertEditPermission } from '/imports/api/sharing/sharingPermissions'; +import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor'; const flipToggle = new ValidatedMethod({ name: 'creatureProperties.flipToggle', @@ -17,7 +17,7 @@ const flipToggle = new ValidatedMethod({ run({ _id }) { // Permission let property = CreatureProperties.findOne(_id, { - fields: { type: 1, ancestors: 1, enabled: 1, disabled: 1 } + fields: { type: 1, root: 1, enabled: 1, disabled: 1 } }); if (property.type !== 'toggle') { throw new Meteor.Error('wrong property', diff --git a/app/imports/api/creature/creatureProperties/methods/getParentRefByTag.js b/app/imports/api/creature/creatureProperties/methods/getParentRefByTag.js index f5236c9f..cda68469 100644 --- a/app/imports/api/creature/creatureProperties/methods/getParentRefByTag.js +++ b/app/imports/api/creature/creatureProperties/methods/getParentRefByTag.js @@ -1,13 +1,14 @@ -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; +import { getFilter } from '/imports/api/parenting/parentingFunctions'; -export default function getParentRefByTag(creatureId, tag){ +export default function getParentRefByTag(creatureId, tag) { let prop = CreatureProperties.findOne({ - 'ancestors.id': creatureId, - removed: {$ne: true}, - inactive: {$ne: true}, + ...getFilter.descendantsOfRoot(creatureId), + removed: { $ne: true }, + inactive: { $ne: true }, tags: tag, }, { - sort: {order: 1}, + sort: { left: 1 }, }); - return prop && {id: prop._id, collection: 'creatureProperties'}; + return prop && { id: prop._id, collection: 'creatureProperties' }; } diff --git a/app/imports/api/creature/creatureProperties/methods/getSlotFillFilter.js b/app/imports/api/creature/creatureProperties/methods/getSlotFillFilter.js index d4670c95..08446124 100644 --- a/app/imports/api/creature/creatureProperties/methods/getSlotFillFilter.js +++ b/app/imports/api/creature/creatureProperties/methods/getSlotFillFilter.js @@ -1,62 +1,78 @@ -export default function getSlotFillFilter({slot, libraryIds}){ +import { getFilter } from '/imports/api/parenting/parentingFunctions'; + +export default function getSlotFillFilter({ slot, libraryIds }) { + + if (!slot) throw 'Slot is required for getSlotFillFilter'; + if (!libraryIds) throw 'LibraryIds is required for getSlotFillFilter'; + let filter = { - removed: {$ne: true}, - $and: [] + fillSlots: true, + removed: { $ne: true }, + $and: [], }; - if (libraryIds){ - filter['ancestors.id'] = {$in: libraryIds}; + if (libraryIds.length) { + Object.assign( + filter, + getFilter.descendantsOfAllRoots(libraryIds) + ); } - if (slot.slotType){ + if (slot.slotType) { filter.$and.push({ $or: [{ type: slot.slotType - },{ - type: 'slotFiller', + }, { slotFillerType: slot.slotType, }] }); } else if (slot.type === 'class') { - filter.$and.push({ - $or: [{ - type: 'classLevel', - },{ - type: 'slotFiller', - slotFillerType: 'classLevel', - }] - }); - if (slot.variableName) { - filter.variableName = slot.variableName; + const classLevelFilter = { + type: 'classLevel', + }; + const slotFillerFilter = { + slotFillerType: 'classLevel', + }; + + // Match variable name or tags + if (slot.variableName) { + classLevelFilter.variableName = slot.variableName; + slotFillerFilter.libraryTags = slot.variableName; } // Only search for levels the class needs if (slot.missingLevels && slot.missingLevels.length) { - filter.level = {$in: slot.missingLevels}; + classLevelFilter.level = { $in: slot.missingLevels }; + slotFillerFilter['cache.node.level'] = { $in: slot.missingLevels }; } else { - filter.level = (slot.level || 0) + 1; + classLevelFilter.level = { $gt: slot.level || 0 }; + slotFillerFilter['cache.node.level'] = { $gt: slot.level || 0 }; } + + filter.$and.push({ + $or: [classLevelFilter, slotFillerFilter] + }); } let tagsOr = []; let tagsNin = []; - if (slot.slotTags && slot.slotTags.length){ - tagsOr.push({tags: {$all: slot.slotTags}}); + if (slot.slotTags && slot.slotTags.length) { + tagsOr.push({ libraryTags: { $all: slot.slotTags } }); } - if (slot.extraTags && slot.extraTags.length){ + if (slot.extraTags && slot.extraTags.length) { slot.extraTags.forEach(extra => { if (!extra.tags || !extra.tags.length) return; - if (extra.operation === 'OR'){ - tagsOr.push({tags: {$all: extra.tags}}); - } else if (extra.operation === 'NOT'){ + if (extra.operation === 'OR') { + tagsOr.push({ libraryTags: { $all: extra.tags } }); + } else if (extra.operation === 'NOT') { tagsNin.push(...extra.tags); } }); } - if (tagsOr.length){ + if (tagsOr.length) { filter.$or = tagsOr; } - if (tagsNin.length){ - filter.$and.push({tags: {$nin: tagsNin}}); + if (tagsNin.length) { + filter.$and.push({ libraryTags: { $nin: tagsNin } }); } - if (!filter.$and.length){ + if (!filter.$and.length) { delete filter.$and; } return filter; diff --git a/app/imports/api/creature/creatureProperties/methods/getSlotFillFilter.test.js b/app/imports/api/creature/creatureProperties/methods/getSlotFillFilter.test.js new file mode 100644 index 00000000..cd8f1c5a --- /dev/null +++ b/app/imports/api/creature/creatureProperties/methods/getSlotFillFilter.test.js @@ -0,0 +1,85 @@ +import { assert } from 'chai'; +import getSlotFillFilter from '/imports/api/creature/creatureProperties/methods/getSlotFillFilter'; + +describe('Slot fill filter', function () { + + it('Gives error if arguments aren\'t provided', function () { + assert.throws( + () => getSlotFillFilter(undefined), + null, null, 'Passing undefined should give an error' + ); + assert.throws( + () => getSlotFillFilter({ + slot: { slotTags: ['tag1'] }, + }), + null, null, 'Passing no libraryIds should give an error' + ); + assert.throws( + () => getSlotFillFilter({ + libraryIds: ['libraryId1'], + }), + null, null, 'Passing no slot should give an error' + ); + }); + + it('filters using basic slot tags', function () { + const filter = getSlotFillFilter({ + slot: { + slotTags: ['tag1', 'tag2'] + }, + libraryIds: ['libraryId1', 'libraryId2'], + }); + assert.deepStrictEqual(filter, { + $or: [{ + libraryTags: { $all: ['tag1', 'tag2'] } + }], + 'root.id': { $in: ['libraryId1', 'libraryId2'] }, + removed: { $ne: true }, + fillSlots: true, + }); + }); + + it('filters using slot type', function () { + const filter = getSlotFillFilter({ + slot: { + slotTags: ['tag1', 'tag2'], + slotType: 'feature', + }, + libraryIds: ['libraryId1', 'libraryId2'] + }); + assert.deepStrictEqual(filter.$and, [{ + $or: [{ + type: 'feature' + }, { + slotFillerType: 'feature', + }], + }]); + }); + + it('filters using extra tags', function () { + const filter = getSlotFillFilter({ + slot: { + slotTags: ['tag1', 'tag2'], + extraTags: [ + { operation: 'OR', tags: ['tag3', 'tag4'] }, + { operation: 'NOT', tags: ['tag5', 'tag6'] }, + { operation: 'NOT', tags: ['tag7', 'tag8'] }, + ], + }, + libraryIds: ['libraryId1', 'libraryId2'], + }); + assert.deepStrictEqual(filter, { + $or: [ + { libraryTags: { $all: ['tag1', 'tag2'] } }, + { libraryTags: { $all: ['tag3', 'tag4'] } }, + ], + $and: [ + { libraryTags: { $nin: ['tag5', 'tag6', 'tag7', 'tag8'] } }, + ], + 'root.id': { $in: ['libraryId1', 'libraryId2'] }, + removed: { $ne: true }, + fillSlots: true, + }); + }); + +}); \ No newline at end of file diff --git a/app/imports/api/creature/creatureProperties/methods/index.js b/app/imports/api/creature/creatureProperties/methods/index.js index b71515e9..05aba1ce 100644 --- a/app/imports/api/creature/creatureProperties/methods/index.js +++ b/app/imports/api/creature/creatureProperties/methods/index.js @@ -1,13 +1,13 @@ -import '/imports/api/creature/creatureProperties/methods/adjustQuantity.js'; -import '/imports/api/creature/creatureProperties/methods/damageProperty.js'; -import '/imports/api/creature/creatureProperties/methods/duplicateProperty.js'; -import '/imports/api/creature/creatureProperties/methods/equipItem.js'; -import '/imports/api/creature/creatureProperties/methods/insertProperty.js'; -import '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js'; -import '/imports/api/creature/creatureProperties/methods/pullFromProperty.js'; -import '/imports/api/creature/creatureProperties/methods/pushToProperty.js'; -import '/imports/api/creature/creatureProperties/methods/restoreProperty.js'; -import '/imports/api/creature/creatureProperties/methods/selectAmmoItem.js'; -import '/imports/api/creature/creatureProperties/methods/softRemoveProperty.js'; -import '/imports/api/creature/creatureProperties/methods/updateCreatureProperty.js'; -import '/imports/api/creature/creatureProperties/methods/flipToggle.js'; +import '/imports/api/creature/creatureProperties/methods/adjustQuantity'; +import '/imports/api/creature/creatureProperties/methods/copyPropertyToLibrary'; +import '/imports/api/creature/creatureProperties/methods/duplicateProperty'; +import '/imports/api/creature/creatureProperties/methods/equipItem'; +import '/imports/api/creature/creatureProperties/methods/insertProperty'; +import '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode'; +import '/imports/api/creature/creatureProperties/methods/pullFromProperty'; +import '/imports/api/creature/creatureProperties/methods/pushToProperty'; +import '/imports/api/creature/creatureProperties/methods/restoreProperty'; +import '/imports/api/creature/creatureProperties/methods/selectAmmoItem'; +import '/imports/api/creature/creatureProperties/methods/softRemoveProperty'; +import '/imports/api/creature/creatureProperties/methods/updateCreatureProperty'; +import '/imports/api/creature/creatureProperties/methods/flipToggle'; diff --git a/app/imports/api/creature/creatureProperties/methods/insertProperty.js b/app/imports/api/creature/creatureProperties/methods/insertProperty.js index 2d872868..570a301d 100644 --- a/app/imports/api/creature/creatureProperties/methods/insertProperty.js +++ b/app/imports/api/creature/creatureProperties/methods/insertProperty.js @@ -1,14 +1,12 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; +import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor'; import SimpleSchema from 'simpl-schema'; -import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; -import { reorderDocs } from '/imports/api/parenting/order.js'; -import { getAncestry } from '/imports/api/parenting/parenting.js'; -import getParentRefByTag from '/imports/api/creature/creatureProperties/methods/getParentRefByTag.js'; -import { RefSchema } from '/imports/api/parenting/ChildSchema.js'; -import { getHighestOrder } from '/imports/api/parenting/order.js'; +import { assertEditPermission } from '/imports/api/sharing/sharingPermissions'; +import { fetchDocByRef, rebuildNestedSets } from '/imports/api/parenting/parentingFunctions'; +import getParentRefByTag from '/imports/api/creature/creatureProperties/methods/getParentRefByTag'; +import { RefSchema } from '/imports/api/parenting/ChildSchema'; const insertProperty = new ValidatedMethod({ name: 'creatureProperties.insert', @@ -25,27 +23,23 @@ const insertProperty = new ValidatedMethod({ timeInterval: 5000, }, run({ creatureProperty, parentRef }) { - // get the new ancestry for the properties - let { parentDoc, ancestors } = getAncestry({ parentRef }); + let rootCreature; + const parentDoc = fetchDocByRef(parentRef); // Check permission to edit - let rootCreature; if (parentRef.collection === 'creatures') { rootCreature = parentDoc; } else if (parentRef.collection === 'creatureProperties') { rootCreature = getRootCreatureAncestor(parentDoc); + creatureProperty.parentId = parentDoc._id; } else { throw `${parentRef.collection} is not a valid parent collection` } assertEditPermission(rootCreature, this.userId); - creatureProperty.parent = parentRef; - creatureProperty.ancestors = ancestors; + creatureProperty.root = { collection: 'creatures', id: rootCreature._id }; - return insertPropertyWork({ - property: creatureProperty, - creature: rootCreature, - }); + return insertPropertyWork(creatureProperty); }, }); @@ -77,18 +71,17 @@ const insertPropertyAsChildOfTag = new ValidatedMethod({ }, run({ creatureProperty, creatureId, tag, tagDefaultName }) { let parentRef = getParentRefByTag(creatureId, tag); + let insertFolderFirst = false; if (!parentRef) { // Use the creature as the parent and mark that we need to insert the folder first later - var insertFolderFirst = true; + insertFolderFirst = true; parentRef = { id: creatureId, collection: 'creatures' }; } - // get the new ancestry for the properties - let { parentDoc, ancestors } = getAncestry({ parentRef }); - // Check permission to edit let rootCreature; + const parentDoc = fetchDocByRef(parentRef); if (parentRef.collection === 'creatures') { rootCreature = parentDoc; } else if (parentRef.collection === 'creatureProperties') { @@ -98,46 +91,34 @@ const insertPropertyAsChildOfTag = new ValidatedMethod({ } assertEditPermission(rootCreature, this.userId); + const root = { collection: 'creatures', id: rootCreature._id }; + // Add the folder first if we need to if (insertFolderFirst) { - let order = getHighestOrder({ - collection: CreatureProperties, - ancestorId: parentRef.id, - }) + 1; let id = CreatureProperties.insert({ type: 'folder', name: tagDefaultName || (tag.charAt(0).toUpperCase() + tag.slice(1)), tags: [tag], - parent: parentRef, - ancestors: [parentRef], - order, + // parentId: undefined, + root, }); // Make the folder our new parent - let newParentRef = { id, collection: 'creatureProperties' }; - ancestors = [parentRef, newParentRef]; - parentRef = newParentRef; - creatureProperty.order = order + 1; + parentRef = { id, collection: 'creatureProperties' }; } - creatureProperty.parent = parentRef; - creatureProperty.ancestors = ancestors; + creatureProperty.root = root; + creatureProperty.parentId = parentRef.id; - return insertPropertyWork({ - property: creatureProperty, - creature: rootCreature, - }); + return insertPropertyWork(creatureProperty); }, }); -export function insertPropertyWork({ property, creature }) { +export function insertPropertyWork(property) { delete property._id; property.dirty = true; let _id = CreatureProperties.insert(property); // Tree structure changed by insert, reorder the tree - reorderDocs({ - collection: CreatureProperties, - ancestorId: creature._id, - }); + rebuildNestedSets(CreatureProperties, property.root.id); return _id; } diff --git a/app/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js b/app/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js index 641a9cfb..b4d833c8 100644 --- a/app/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js +++ b/app/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js @@ -1,19 +1,18 @@ import SimpleSchema from 'simpl-schema'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import LibraryNodes from '/imports/api/library/LibraryNodes.js'; -import { RefSchema } from '/imports/api/parenting/ChildSchema.js'; -import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js'; -import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; +import LibraryNodes from '/imports/api/library/LibraryNodes'; +import { RefSchema } from '/imports/api/parenting/ChildSchema'; +import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor'; +import { assertEditPermission } from '/imports/api/sharing/sharingPermissions'; import { - setLineageOfDocs, - getAncestry, - renewDocIds -} from '/imports/api/parenting/parenting.js'; -import { reorderDocs } from '/imports/api/parenting/order.js'; -import { setDocToLastOrder } from '/imports/api/parenting/order.js'; -import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js'; + renewDocIds, + fetchDocByRef, + rebuildNestedSets, + getFilter +} from '/imports/api/parenting/parentingFunctions'; +import { union } from 'lodash'; const insertPropertyFromLibraryNode = new ValidatedMethod({ name: 'creatureProperties.insertPropertyFromLibraryNode', @@ -29,19 +28,15 @@ const insertPropertyFromLibraryNode = new ValidatedMethod({ parentRef: { type: RefSchema, }, - order: { - type: Number, - optional: true, - }, }).validator(), mixins: [RateLimiterMixin], rateLimit: { numRequests: 5, timeInterval: 5000, }, - run({ nodeIds, parentRef, order }) { + run({ nodeIds, parentRef }) { // get the new ancestry for the properties - let { parentDoc, ancestors } = getAncestry({ parentRef }); + const parentDoc = fetchDocByRef(parentRef); // Check permission to edit let rootCreature; @@ -54,37 +49,32 @@ const insertPropertyFromLibraryNode = new ValidatedMethod({ } assertEditPermission(rootCreature, this.userId); - // {libraryId: hasViewPermission} - //let libraryPermissionMemoir = {}; + const root = { collection: 'creatures', id: rootCreature._id }; + const parentId = parentRef.id; + let node; nodeIds.forEach(nodeId => { - // TODO: Check library view permission for each node before starting - node = insertPropertyFromNode(nodeId, ancestors, order); + node = insertPropertyFromNode(nodeId, root, parentId); }); - // get one of the root inserted docs - let rootId = node._id; - // Tree structure changed by inserts, reorder the tree - reorderDocs({ - collection: CreatureProperties, - ancestorId: rootCreature._id, - }); - // Return the docId of the last property, the inserted root property - return rootId; + rebuildNestedSets(CreatureProperties, rootCreature._id); + + // get one of the root inserted docs + const lastInsertedId = node?._id; + return lastInsertedId; }, }); -function insertPropertyFromNode(nodeId, ancestors, order) { - // Fetch the library node and its decendents, provided they have not been +function insertPropertyFromNode(nodeId, root, parentId) { + // Fetch the library node and its descendants, provided they have not been // removed - // TODO: Check permission to read the library this node is in let node = LibraryNodes.findOne({ _id: nodeId, removed: { $ne: true }, }); if (!node) { - if (Meteor.isClient) return; + if (Meteor.isClient) return {}; else { throw new Meteor.Error( 'Insert property from library failed', @@ -92,69 +82,51 @@ function insertPropertyFromNode(nodeId, ancestors, order) { ); } } - let oldParent = node.parent; + let nodes = LibraryNodes.find({ - 'ancestors.id': nodeId, + ...getFilter.descendants(node), removed: { $ne: true }, }).fetch(); - // Convert all references into actual nodes - nodes = reifyNodeReferences(nodes); - // The root node is first in the array of nodes // It must get the first generated ID to prevent flickering nodes = [node, ...nodes]; + // Convert all references into actual nodes + nodes = reifyNodeReferences(nodes); + // Refetch the root node, it might have been reified + node = nodes[0] || node; + // set libraryNodeIds storeLibraryNodeReferences(nodes); - // re-map all the ancestors - setLineageOfDocs({ - docArray: nodes, - newAncestry: ancestors, - oldParent, - }); - // Give the docs new IDs without breaking internal references renewDocIds({ docArray: nodes, collectionMap: { 'libraryNodes': 'creatureProperties' } }); - // Order the root node - if (order === undefined) { - setDocToLastOrder({ - collection: CreatureProperties, - doc: node, - }); - } else { - node.order = order; - } + // Mark root node as dirty + node.dirty = true; - // Mark all nodes as dirty - dirtyNodes(nodes); + // Move the root node to the end of the order + node.left = Number.MAX_SAFE_INTEGER; // Insert the creature properties CreatureProperties.batchInsert(nodes); return node; } -function storeLibraryNodeReferences(nodes) { +export function storeLibraryNodeReferences(nodes) { nodes.forEach(node => { if (node.libraryNodeId) return; node.libraryNodeId = node._id; }); } -function dirtyNodes(nodes) { - nodes.forEach(node => { - node.dirty = true; - }); -} - // Covert node references into actual nodes // TODO: check permissions for each library a reference node references -function reifyNodeReferences(nodes, visitedRefs = new Set(), depth = 0) { +export function reifyNodeReferences(nodes, visitedRefs = new Set(), depth = 0) { depth += 1; // New nodes added this function let newNodes = []; @@ -174,7 +146,7 @@ function reifyNodeReferences(nodes, visitedRefs = new Set(), depth = 0) { let referencedNode try { referencedNode = fetchDocByRef(node.ref); - referencedNode.order = node.order; + referencedNode.tags = union(node.tags, referencedNode.tags); // We are definitely replacing this node, so add it to the list visitedRefs.add(node._id); } catch (e) { @@ -183,23 +155,15 @@ function reifyNodeReferences(nodes, visitedRefs = new Set(), depth = 0) { } // Get all the descendants of the referenced node - let descendents = LibraryNodes.find({ - 'ancestors.id': referencedNode._id, + let descendants = LibraryNodes.find({ + ...getFilter.descendants(referencedNode), removed: { $ne: true }, }, { - sort: { order: 1 }, + sort: { left: 1 }, }).fetch(); // We are adding the referenced node and its descendants - let addedNodes = [referencedNode, ...descendents]; - - // re-map all the ancestors to parent the new sub-tree into our existing - // node tree - setLineageOfDocs({ - docArray: addedNodes, - newAncestry: node.ancestors, - oldParent: referencedNode.parent, - }); + let addedNodes = [referencedNode, ...descendants]; // Filter all the looped references addedNodes = addedNodes.filter(addedNode => { diff --git a/app/imports/api/creature/creatureProperties/methods/pullFromProperty.js b/app/imports/api/creature/creatureProperties/methods/pullFromProperty.js index 4dd8245e..86303ab6 100644 --- a/app/imports/api/creature/creatureProperties/methods/pullFromProperty.js +++ b/app/imports/api/creature/creatureProperties/methods/pullFromProperty.js @@ -1,8 +1,8 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; -import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; +import { assertEditPermission } from '/imports/api/sharing/sharingPermissions'; +import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor'; const pullFromProperty = new ValidatedMethod({ name: 'creatureProperties.pull', diff --git a/app/imports/api/creature/creatureProperties/methods/pushToProperty.js b/app/imports/api/creature/creatureProperties/methods/pushToProperty.js index 95735d9b..e064273e 100644 --- a/app/imports/api/creature/creatureProperties/methods/pushToProperty.js +++ b/app/imports/api/creature/creatureProperties/methods/pushToProperty.js @@ -1,8 +1,8 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; -import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; +import { assertEditPermission } from '/imports/api/sharing/sharingPermissions'; +import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor'; import { get } from 'lodash'; const pushToProperty = new ValidatedMethod({ diff --git a/app/imports/api/creature/creatureProperties/methods/restoreProperty.js b/app/imports/api/creature/creatureProperties/methods/restoreProperty.js index 9e66448e..05e0a316 100644 --- a/app/imports/api/creature/creatureProperties/methods/restoreProperty.js +++ b/app/imports/api/creature/creatureProperties/methods/restoreProperty.js @@ -1,10 +1,10 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import SimpleSchema from 'simpl-schema'; -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; -import { restore } from '/imports/api/parenting/softRemove.js'; -import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; +import { assertEditPermission } from '/imports/api/sharing/sharingPermissions'; +import { restore } from '/imports/api/parenting/softRemove'; +import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor'; const restoreProperty = new ValidatedMethod({ name: 'creatureProperties.restore', @@ -23,13 +23,7 @@ const restoreProperty = new ValidatedMethod({ assertEditPermission(rootCreature, this.userId); // Do work - restore({ - _id, - collection: CreatureProperties, - extraUpdates: { - $set: { dirty: true } - }, - }); + restore(CreatureProperties, property, { $set: { dirty: true } }); } }); diff --git a/app/imports/api/creature/creatureProperties/methods/selectAmmoItem.js b/app/imports/api/creature/creatureProperties/methods/selectAmmoItem.js index 57bbe6bd..b6578898 100644 --- a/app/imports/api/creature/creatureProperties/methods/selectAmmoItem.js +++ b/app/imports/api/creature/creatureProperties/methods/selectAmmoItem.js @@ -1,9 +1,9 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import SimpleSchema from 'simpl-schema'; -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js'; -import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; +import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor'; +import { assertEditPermission } from '/imports/api/sharing/sharingPermissions'; const selectAmmoItem = new ValidatedMethod({ name: 'creatureProperties.selectAmmoItem', diff --git a/app/imports/api/creature/creatureProperties/methods/softRemoveProperty.js b/app/imports/api/creature/creatureProperties/methods/softRemoveProperty.js index a4240ac7..3d62655a 100644 --- a/app/imports/api/creature/creatureProperties/methods/softRemoveProperty.js +++ b/app/imports/api/creature/creatureProperties/methods/softRemoveProperty.js @@ -1,10 +1,10 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import SimpleSchema from 'simpl-schema'; -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; -import { softRemove } from '/imports/api/parenting/softRemove.js'; -import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; +import { assertEditPermission } from '/imports/api/sharing/sharingPermissions'; +import { softRemove } from '/imports/api/parenting/softRemove'; +import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor'; const softRemoveProperty = new ValidatedMethod({ name: 'creatureProperties.softRemove', @@ -23,7 +23,7 @@ const softRemoveProperty = new ValidatedMethod({ assertEditPermission(rootCreature, this.userId); // Do work - softRemove({ _id, collection: CreatureProperties }); + softRemove(CreatureProperties, property); } }); diff --git a/app/imports/api/creature/creatureProperties/methods/updateCreatureProperty.js b/app/imports/api/creature/creatureProperties/methods/updateCreatureProperty.js index 4b606114..12c90905 100644 --- a/app/imports/api/creature/creatureProperties/methods/updateCreatureProperty.js +++ b/app/imports/api/creature/creatureProperties/methods/updateCreatureProperty.js @@ -1,8 +1,8 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; -import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; +import { assertEditPermission } from '/imports/api/sharing/sharingPermissions'; +import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor'; const updateCreatureProperty = new ValidatedMethod({ name: 'creatureProperties.update', @@ -14,6 +14,10 @@ const updateCreatureProperty = new ValidatedMethod({ case 'order': case 'parent': case 'ancestors': + case 'root': + case 'left': + case 'right': + case 'parentId': case 'damage': throw new Meteor.Error('Permission denied', 'This property can\'t be updated directly'); @@ -21,13 +25,13 @@ const updateCreatureProperty = new ValidatedMethod({ }, mixins: [RateLimiterMixin], rateLimit: { - numRequests: 5, + numRequests: 12, timeInterval: 5000, }, run({ _id, path, value }) { // Permission let property = CreatureProperties.findOne(_id, { - fields: { type: 1, ancestors: 1 } + fields: { type: 1, root: 1 } }); let rootCreature = getRootCreatureAncestor(property); assertEditPermission(rootCreature, this.userId); diff --git a/app/imports/api/creature/creatureProperties/recomputeCreaturesByProperty.js b/app/imports/api/creature/creatureProperties/recomputeCreaturesByProperty.js index a2190860..06fe42c7 100644 --- a/app/imports/api/creature/creatureProperties/recomputeCreaturesByProperty.js +++ b/app/imports/api/creature/creatureProperties/recomputeCreaturesByProperty.js @@ -1,12 +1,9 @@ -import computeCreature from '/imports/api/engine/computeCreature.js'; +import computeCreature from '/imports/api/engine/computeCreature'; /** * Recomputes all ancestor creatures of this property + * @deprecated */ -export default function recomputeCreaturesByProperty(property){ - for (let ref of property.ancestors){ - if (ref.collection === 'creatures') { - computeCreature.call(ref.id); - } - } +export default function recomputeCreaturesByProperty(property) { + computeCreature.call(property.root.id); } diff --git a/app/imports/api/creature/creatures/CreatureVariables.js b/app/imports/api/creature/creatures/CreatureVariables.js deleted file mode 100644 index 27fe0af7..00000000 --- a/app/imports/api/creature/creatures/CreatureVariables.js +++ /dev/null @@ -1,21 +0,0 @@ -//set up the collection for creature variables -let CreatureVariables = new Mongo.Collection('creatureVariables'); - -// Unique index on _creatureId -if (Meteor.isServer) { - CreatureVariables._ensureIndex({ _creatureId: 1 }, { unique: true }) -} - -/** No schema because the structure isn't known until compute time - * Expect documents to looke like: - * { - * _id: "nE8Ngd6K4L4jSxLY2", - * _creatureId: "nE8Ngd6K4L4jSxLY2", // indexed reference to the creature - * explicitlyDefinedVariableName: {...some creatureProperty} - * implicitVariableName: {value: 10}, - * undefinedVariableName: {}, - * } - * Where top level fields that don't start with `_` are variables on the sheet -**/ - -export default CreatureVariables; diff --git a/app/imports/api/creature/creatures/CreatureVariables.ts b/app/imports/api/creature/creatures/CreatureVariables.ts new file mode 100644 index 00000000..c0ff7739 --- /dev/null +++ b/app/imports/api/creature/creatures/CreatureVariables.ts @@ -0,0 +1,103 @@ +import { getSingleProperty } from '/imports/api/engine/loadCreatures'; +import ParseNode from '/imports/parser/parseTree/ParseNode'; +import array from '/imports/parser/parseTree/array'; +import constant, { isFiniteNode } from '/imports/parser/parseTree/constant'; + +//set up the collection for creature variables +const CreatureVariables = new Mongo.Collection('creatureVariables'); + +// Unique index on _creatureId +if (Meteor.isServer) { + CreatureVariables._ensureIndex({ _creatureId: 1 }, { unique: true }) +} + +/** No schema because the structure isn't known until compute time + * Expect documents to look like: + * { + * _id: "nE8Ngd6K4L4jSxLY2", + * _creatureId: "nE8Ngd6K4L4jSxLY2", // indexed reference to the creature + * explicitlyDefinedVariableName: {...some creatureProperty}, + * // Must be found in CreatureProperties before using: + * linkedProperty: { _propId: "nE8Ngd6K1234SxLY2" } + * implicitVariableName: {value: 10}, + * undefinedVariableName: {}, + * } + * Where top level fields that don't start with `_` are variables on the sheet +**/ + +/** + * Get the property from the given scope, respecting properties that are just a link to the actual + * property document + */ +export function getFromScope(name: string, scope) { + let value = scope?.[name]; + if (value?._propId) { + const [propId, rowIdentifier, rowNumber] = value._propId.split('_'); + value = getSingleProperty(scope._creatureId, propId); + if (rowIdentifier === 'row' && value?.type === 'pointBuy') { + value = value.values[rowNumber]; + } + } + return value; +} + +export function getNumberFromScope(name, scope) { + const parseNode = getParseNodeFromScope(name, scope); + if (!parseNode || !isFiniteNode(parseNode)) { + return undefined; + } + return parseNode.value; +} + +export async function getConstantValueFromScope( + name, scope +) { + const parseNode = getParseNodeFromScope(name, scope); + if (!parseNode) return; + if (parseNode.parseType !== 'constant') return; + return parseNode.value; +} + +export function getParseNodeFromScope(name, scope): ParseNode | undefined { + let value = getFromScope(name, scope); + if (!value) return; + let valueType = getType(value); + // Iterate into object.values + while (valueType === 'object') { + // Prefer the valueNode over the value + if (value.valueNode) { + value = value.valueNode; + } else { + value = value.value; + } + valueType = getType(value); + } + // Return a discovered parse node + if (valueType === 'parseNode') { + return value; + } + // Return a parse node based on the constant type returned + if (valueType === 'string' || valueType === 'number' || valueType === 'boolean') { + return constant.create({ value }); + } + // Return a parser array + if (valueType === 'array') { + // If the first value is a parse node, assume all the values are + if (getType(value[0]) === 'parseNode') { + return array.create({ + values: value, + }); + } + // Create the array from js primitives instead + return array.fromConstantArray(value); + } +} + +function getType(val) { + if (!val) return typeof val; + if (Array.isArray(val)) return 'array'; + if (val.parseType) return 'parseNode'; + return typeof val; +} + +export default CreatureVariables; diff --git a/app/imports/api/creature/creatures/Creatures.js b/app/imports/api/creature/creatures/Creatures.ts similarity index 63% rename from app/imports/api/creature/creatures/Creatures.js rename to app/imports/api/creature/creatures/Creatures.ts index b94fe7e4..af8121d6 100644 --- a/app/imports/api/creature/creatures/Creatures.js +++ b/app/imports/api/creature/creatures/Creatures.ts @@ -1,13 +1,59 @@ import SimpleSchema from 'simpl-schema'; -import deathSaveSchema from '/imports/api/properties/subSchemas/DeathSavesSchema.js' -import ColorSchema from '/imports/api/properties/subSchemas/ColorSchema.js'; -import SharingSchema from '/imports/api/sharing/SharingSchema.js'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; +import ColorSchema, { Colored } from '/imports/api/properties/subSchemas/ColorSchema'; +import SharingSchema, { Shared } from '/imports/api/sharing/SharingSchema'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; + +export type Creature = Colored & Shared & { + // Strings + _id: string, + name?: string, + alignment?: string, + gender?: string, + picture?: string, + avatarPicture?: string, + + // Libraries + allowedLibraries?: string[], + allowedLibraryCollections?: string[], + + // Stats that are computed and denormalized outside of recomputation + denormalizedStats?: { + xp: number, + milestoneLevels: number, + }, + propCount?: number, + // Does the character need a recompute? + dirty?: boolean, + // Version of computation engine that was last used to compute this creature + computeVersion?: string, + type: 'pc' | 'npc' | 'monster', + computeErrors?: { + type: string, + details?: any, + }[], + + // Tabletop + tabletopId?: string, + initiativeRoll?: number, + + settings: { + useVariantEncumbrance?: true, + hideSpellcasting?: true, + hideRestButtons?: true, + swapStatAndModifier?: true, + hideUnusedStats?: true, + showTreeTab?: true, + hideSpellsTab?: true, + hideCalculationErrors?: true, + hitDiceResetMultiplier?: number, + discordWebhook?: string, + }, +}; //set up the collection for creatures -let Creatures = new Mongo.Collection('creatures'); +const Creatures = new Mongo.Collection('creatures'); -let CreatureSettingsSchema = new SimpleSchema({ +const CreatureSettingsSchema = new SimpleSchema({ //slowed down by carrying too much? useVariantEncumbrance: { type: Boolean, @@ -62,7 +108,24 @@ let CreatureSettingsSchema = new SimpleSchema({ }, }); -let CreatureSchema = new SimpleSchema({ +const IconGroupSchema = new SimpleSchema({ + name: { + type: String, + max: STORAGE_LIMITS.name, + optional: true, + }, + iconIds: { + type: Array, + max: 4, + defaultValue: [], + }, + 'iconIds.$': { + type: String, + max: STORAGE_LIMITS.variableName, + }, +}); + +const CreatureSchema = new SimpleSchema({ // Strings name: { type: String, @@ -111,11 +174,6 @@ let CreatureSchema = new SimpleSchema({ regEx: SimpleSchema.RegEx.Id, }, - // Mechanics - deathSave: { - type: deathSaveSchema, - defaultValue: {}, - }, // Stats that are computed and denormalised outside of recomputation denormalizedStats: { type: Object, @@ -131,6 +189,10 @@ let CreatureSchema = new SimpleSchema({ type: SimpleSchema.Integer, defaultValue: 0, }, + propCount: { + type: SimpleSchema.Integer, + defaultValue: 0, + }, // Does the character need a recompute? dirty: { type: Boolean, @@ -146,16 +208,6 @@ let CreatureSchema = new SimpleSchema({ defaultValue: 'pc', allowedValues: ['pc', 'npc', 'monster'], }, - damageMultipliers: { - type: Object, - blackbox: true, - defaultValue: {} - }, - variables: { - type: Object, - blackbox: true, - defaultValue: {} - }, computeErrors: { type: Array, optional: true, @@ -173,9 +225,10 @@ let CreatureSchema = new SimpleSchema({ }, // Tabletop - tabletop: { + tabletopId: { + index: 1, type: String, - regEx: SimpleSchema.RegEx.id, + regEx: SimpleSchema.RegEx.Id, optional: true, }, initiativeRoll: { @@ -193,11 +246,10 @@ let CreatureSchema = new SimpleSchema({ CreatureSchema.extend(ColorSchema); CreatureSchema.extend(SharingSchema); +//@ts-expect-error attachSchema not defined Creatures.attachSchema(CreatureSchema); export default Creatures; export { CreatureSchema }; - -import '/imports/api/engine/actions/doAction.js'; diff --git a/app/imports/api/creature/creatures/creaturePermissions.js b/app/imports/api/creature/creatures/creaturePermissions.js index 380a5258..1e8c4d2c 100644 --- a/app/imports/api/creature/creatures/creaturePermissions.js +++ b/app/imports/api/creature/creatures/creaturePermissions.js @@ -1,29 +1,29 @@ -import Creatures from '/imports/api/creature/creatures/Creatures.js'; +import Creatures from '/imports/api/creature/creatures/Creatures'; import { assertEditPermission as editPermission, assertViewPermission as viewPermission, assertOwnership as ownership -} from '/imports/api/sharing/sharingPermissions.js'; +} from '/imports/api/sharing/sharingPermissions'; -function getCreature(creature, fields){ - if (typeof creature === 'string'){ - return Creatures.findOne(creature, {fields}); +function getCreature(creature, fields) { + if (typeof creature === 'string') { + return Creatures.findOne(creature, { fields }); } else { return creature; } } -export function assertOwnership(creature, userId){ - creature = getCreature(creature, {owner: 1}); +export function assertOwnership(creature, userId) { + creature = getCreature(creature, { owner: 1 }); ownership(creature, userId); } export function assertEditPermission(creature, userId) { - creature = getCreature(creature, {owner: 1, writers: 1}); + creature = getCreature(creature, { owner: 1, writers: 1 }); editPermission(creature, userId); } export function assertViewPermission(creature, userId) { - creature = getCreature(creature, {owner: 1, readers:1, writers: 1, public: 1}); + creature = getCreature(creature, { owner: 1, readers: 1, writers: 1, public: 1 }); viewPermission(creature, userId); } diff --git a/app/imports/api/creature/creatures/defaultCharacterProperties.js b/app/imports/api/creature/creatures/defaultCharacterProperties.js index e7f05a11..dc94cd96 100644 --- a/app/imports/api/creature/creatures/defaultCharacterProperties.js +++ b/app/imports/api/creature/creatures/defaultCharacterProperties.js @@ -1,47 +1,50 @@ -import BUILT_IN_TAGS from '/imports/constants/BUILT_IN_TAGS.js'; +import BUILT_IN_TAGS from '/imports/constants/BUILT_IN_TAGS'; -export default function defaultCharacterProperties(creatureId){ +export default function defaultCharacterProperties(creatureId) { if (!creatureId) throw 'creatureId is required'; - const creatureRef = {collection: 'creatures', id: creatureId}; + const creatureRef = { collection: 'creatures', id: creatureId }; let randomSrc = DDP.randomStream('defaultProperties'); const inventoryId = randomSrc.id(); - const inventoryRef = {collection: 'creatureProperties', id: inventoryId}; return [ { type: 'propertySlot', name: 'Ruleset', - description: {text: 'Choose a starting point for your character, this will define the basic setup of your character sheet. Without a base ruleset, your sheet will be empty.'}, + description: { text: 'Choose a starting point for your character, this will define the basic setup of your character sheet. Without a base ruleset, your sheet will be empty.' }, slotTags: ['base'], tags: [], - quantityExpected: {calculation: '1'}, + quantityExpected: { calculation: '1' }, hideWhenFull: true, spaceLeft: 1, totalFilled: 0, - order: 0, - parent: creatureRef, - ancestors: [creatureRef], + left: 1, + right: 2, + parentId: creatureId, + root: creatureRef, }, { _id: inventoryId, type: 'folder', name: 'Inventory', tags: [BUILT_IN_TAGS.inventory], - order: 1, - parent: creatureRef, - ancestors: [creatureRef], + left: 3, + right: 8, + parentId: creatureId, + root: creatureRef, }, { type: 'folder', name: 'Equipment', tags: [BUILT_IN_TAGS.equipment], - order: 2, - parent: inventoryRef, - ancestors: [creatureRef, inventoryRef], + left: 4, + right: 5, + parentId: inventoryId, + root: creatureRef, }, { type: 'folder', name: 'Carried', tags: [BUILT_IN_TAGS.carried], - order: 3, - parent: inventoryRef, - ancestors: [creatureRef, inventoryRef], + left: 6, + right: 7, + parent: inventoryId, + root: creatureRef, }, ]; } diff --git a/app/imports/api/creature/creatures/methods/assertHasCharacterSlots.js b/app/imports/api/creature/creatures/methods/assertHasCharacterSlots.js index 5b368e5f..2dc31832 100644 --- a/app/imports/api/creature/creatures/methods/assertHasCharacterSlots.js +++ b/app/imports/api/creature/creatures/methods/assertHasCharacterSlots.js @@ -1,5 +1,5 @@ -import { getUserTier } from '/imports/api/users/patreon/tiers.js'; -import Creatures from '/imports/api/creature/creatures/Creatures.js'; +import { getUserTier } from '/imports/api/users/patreon/tiers'; +import Creatures from '/imports/api/creature/creatures/Creatures'; export default function assertHasCharactersSlots(userId) { if (characterSlotsRemaining(userId) <= 0) { diff --git a/app/imports/api/creature/creatures/methods/changeAllowedLibraries.js b/app/imports/api/creature/creatures/methods/changeAllowedLibraries.js index 05a8e9d6..c95aa533 100644 --- a/app/imports/api/creature/creatures/methods/changeAllowedLibraries.js +++ b/app/imports/api/creature/creatures/methods/changeAllowedLibraries.js @@ -1,9 +1,9 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import Creatures from '/imports/api/creature/creatures/Creatures.js'; -import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; +import Creatures from '/imports/api/creature/creatures/Creatures'; +import { assertEditPermission } from '/imports/api/sharing/sharingPermissions'; import SimpleSchema from 'simpl-schema'; -import simpleSchemaMixin from '/imports/api/creature/mixins/simpleSchemaMixin.js'; +import simpleSchemaMixin from '/imports/api/creature/mixins/simpleSchemaMixin'; const changeAllowedLibraries = new ValidatedMethod({ name: 'creatures.changeAllowedLibraries', diff --git a/app/imports/api/creature/creatures/methods/importCharacterFromDiceCloudInstance.js b/app/imports/api/creature/creatures/methods/importCharacterFromDiceCloudInstance.js new file mode 100644 index 00000000..6901ef04 --- /dev/null +++ b/app/imports/api/creature/creatures/methods/importCharacterFromDiceCloudInstance.js @@ -0,0 +1,99 @@ +import SCHEMA_VERSION from '/imports/constants/SCHEMA_VERSION'; +import { ValidatedMethod } from 'meteor/mdg:validated-method'; +import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; +import Creatures from '/imports/api/creature/creatures/Creatures'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; +import CreatureLogs from '/imports/api/creature/log/CreatureLogs'; +import Experiences from '/imports/api/creature/experience/Experiences'; +import { removeCreatureWork } from '/imports/api/creature/creatures/methods/removeCreature'; +import assertHasCharactersSlots from '/imports/api/creature/creatures/methods/assertHasCharacterSlots'; +import verifyArchiveSafety from '/imports/api/creature/archive/methods/verifyArchiveSafety'; + +let migrateApiCreature; +if (Meteor.isServer) { + migrateApiCreature = require('/imports/migrations/apiCreature/migrateApiCreature.js').default; +} + +function importApiCreature(apiCreature, userId) { + const apiVersion = apiCreature.meta?.schemaVersion ?? 2; + const creatureId = apiCreature.creatures[0]._id; + if (SCHEMA_VERSION < apiVersion) { + throw new Meteor.Error('Incompatible', + 'The creature on the remote server is from a newer version of DiceCloud') + } + + // Migrate and verify the archive meets the current schema + migrateApiCreature(apiCreature); + + // Asset that the api creature is (mildly) safe + verifyArchiveSafety({ + creature: apiCreature.creatures[0], + properties: apiCreature.creatureProperties ?? [], + experiences: apiCreature.experiences ?? [], + logs: apiCreature.logs ?? [], + }); + + // Don't upload creatures twice + const existingCreature = Creatures.findOne(apiCreature.creatures[0]._id, { + fields: { _id: 1 } + }); + + if (existingCreature) throw new Meteor.Error('Already exists', + 'The creature you are trying to import already exists in this database.') + + // Ensure the user owns the restored creature + apiCreature.creatures[0].owner = userId; + + // Ensure there is only 1 creature being imported + if (apiCreature.creatures.length !== 1) { + throw new Meteor.Error('invalid-import', + 'One and only one creature must be imported at a time' + ) + } + + // Insert the creature sub documents + // They still have their original _id's + Creatures.insert(apiCreature.creatures[0]); + try { + // Add all the properties + if (apiCreature.creatureProperties && apiCreature.creatureProperties.length) { + CreatureProperties.batchInsert(apiCreature.creatureProperties); + } + if (apiCreature.experiences && apiCreature.experiences.length) { + Experiences.batchInsert(apiCreature.experiences); + } + if (apiCreature.logs && apiCreature.logs.length) { + CreatureLogs.batchInsert(apiCreature.logs); + } + } catch (e) { + // If the above fails, delete the inserted creature + removeCreatureWork(creatureId); + throw e; + } + return creatureId; +} + +const importCharacterFromDiceCloudInstance = new ValidatedMethod({ + name: 'Creatures.methods.importFromInstance', + validate: null, + mixins: [RateLimiterMixin], + rateLimit: { + numRequests: 10, + timeInterval: 5000, + }, + async run({ characterData }) { + if (Meteor.settings.public.disallowCreatureApiImport) throw new Meteor.Error('not-allowed', + 'This instance of DiceCloud has disallowed creature imports') + // fetch the file + if (!characterData) { + throw new Meteor.Error('no-input', + 'No character data was provided'); + } + assertHasCharactersSlots(this.userId); + if (Meteor.isServer) { + return importApiCreature(characterData, this.userId) + } + }, +}); + +export default importCharacterFromDiceCloudInstance; diff --git a/app/imports/api/creature/creatures/methods/index.js b/app/imports/api/creature/creatures/methods/index.js index 85aefc77..9d0cf4b2 100644 --- a/app/imports/api/creature/creatures/methods/index.js +++ b/app/imports/api/creature/creatures/methods/index.js @@ -1,5 +1,5 @@ -import '/imports/api/creature/creatures/methods/insertCreature.js'; -import '/imports/api/creature/creatures/methods/removeCreature.js'; -import '/imports/api/creature/creatures/methods/restCreature.js'; -import '/imports/api/creature/creatures/methods/updateCreature.js'; -import '/imports/api/creature/creatures/methods/changeAllowedLibraries.js'; +import '/imports/api/creature/creatures/methods/changeAllowedLibraries'; +import '/imports/api/creature/creatures/methods/importCharacterFromDiceCloudInstance.js'; +import '/imports/api/creature/creatures/methods/insertCreature'; +import '/imports/api/creature/creatures/methods/removeCreature'; +import '/imports/api/creature/creatures/methods/updateCreature'; diff --git a/app/imports/api/creature/creatures/methods/insertCreature.js b/app/imports/api/creature/creatures/methods/insertCreature.js index c978785b..cf0e8845 100644 --- a/app/imports/api/creature/creatures/methods/insertCreature.js +++ b/app/imports/api/creature/creatures/methods/insertCreature.js @@ -1,21 +1,21 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import simpleSchemaMixin from '/imports/api/creature/mixins/simpleSchemaMixin.js'; -import Creatures, { CreatureSchema } from '/imports/api/creature/creatures/Creatures.js'; -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import defaultCharacterProperties from '/imports/api/creature/creatures/defaultCharacterProperties.js'; -import insertPropertyFromLibraryNode from '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js'; -import assertHasCharactersSlots from '/imports/api/creature/creatures/methods/assertHasCharacterSlots.js'; -import getSlotFillFilter from '/imports/api/creature/creatureProperties/methods/getSlotFillFilter.js'; -import getCreatureLibraryIds from '/imports/api/library/getCreatureLibraryIds.js'; -import LibraryNodes from '/imports/api/library/LibraryNodes.js'; -import { insertExperienceForCreature } from '/imports/api/creature/experience/Experiences.js'; +import simpleSchemaMixin from '/imports/api/creature/mixins/simpleSchemaMixin'; +import Creatures, { CreatureSchema } from '/imports/api/creature/creatures/Creatures'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; +import defaultCharacterProperties from '/imports/api/creature/creatures/defaultCharacterProperties'; +import insertPropertyFromLibraryNode from '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode'; +import assertHasCharactersSlots from '/imports/api/creature/creatures/methods/assertHasCharacterSlots'; +import getSlotFillFilter from '/imports/api/creature/creatureProperties/methods/getSlotFillFilter'; +import getCreatureLibraryIds from '/imports/api/library/getCreatureLibraryIds'; +import LibraryNodes from '/imports/api/library/LibraryNodes'; +import { insertExperienceForCreature } from '/imports/api/creature/experience/Experiences'; import SimpleSchema from 'simpl-schema'; const insertCreature = new ValidatedMethod({ name: 'creatures.insertCreature', mixins: [RateLimiterMixin, simpleSchemaMixin], - schema: CreatureSchema.pick( + validate: CreatureSchema.pick( 'name', 'gender', 'alignment', @@ -26,7 +26,7 @@ const insertCreature = new ValidatedMethod({ type: SimpleSchema.Integer, min: 0, }, - }), + }).validator(), rateLimit: { numRequests: 5, timeInterval: 5000, @@ -48,8 +48,13 @@ const insertCreature = new ValidatedMethod({ name, gender, alignment, + type: 'pc', allowedLibraries, allowedLibraryCollections, + settings: {}, + readers: [], + writers: [], + public: false, }); // Insert experience to get character to starting level @@ -61,7 +66,6 @@ const insertCreature = new ValidatedMethod({ creatureId }, creatureId, - userId, }); } @@ -96,7 +100,6 @@ function insertDefaultRuleset(creatureId, baseId, userId, slot) { insertPropertyFromLibraryNode.call({ nodeIds: [ruleset._id], parentRef: { id: baseId, collection: 'creatureProperties' }, - order: 0.5, }); } } diff --git a/app/imports/api/creature/creatures/methods/removeCreature.js b/app/imports/api/creature/creatures/methods/removeCreature.js index d157f5d7..646a6bc8 100644 --- a/app/imports/api/creature/creatures/methods/removeCreature.js +++ b/app/imports/api/creature/creatures/methods/removeCreature.js @@ -1,18 +1,19 @@ import SimpleSchema from 'simpl-schema'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import { assertOwnership } from '/imports/api/creature/creatures/creaturePermissions.js'; -import Creatures from '/imports/api/creature/creatures/Creatures.js'; -import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables.js'; -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js'; -import Experiences from '/imports/api/creature/experience/Experiences.js'; +import { assertOwnership } from '/imports/api/creature/creatures/creaturePermissions'; +import Creatures from '/imports/api/creature/creatures/Creatures'; +import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; +import CreatureLogs from '/imports/api/creature/log/CreatureLogs'; +import Experiences from '/imports/api/creature/experience/Experiences'; +import { getFilter } from '/imports/api/parenting/parentingFunctions'; -function removeRelatedDocuments(creatureId){ - CreatureVariables.remove({_creatureId: creatureId}); - CreatureProperties.remove({'ancestors.id': creatureId}); - CreatureLogs.remove({creatureId}); - Experiences.remove({creatureId}); +function removeRelatedDocuments(creatureId) { + CreatureVariables.remove({ _creatureId: creatureId }); + CreatureProperties.remove(getFilter.descendantsOfRoot(creatureId)); + CreatureLogs.remove({ creatureId }); + Experiences.remove({ creatureId }); } const removeCreature = new ValidatedMethod({ @@ -28,14 +29,14 @@ const removeCreature = new ValidatedMethod({ numRequests: 5, timeInterval: 5000, }, - run({charId}) { + run({ charId }) { assertOwnership(charId, this.userId) this.unblock(); removeCreatureWork(charId) }, }); -export function removeCreatureWork(creatureId){ +export function removeCreatureWork(creatureId) { Creatures.remove(creatureId); removeRelatedDocuments(creatureId); } diff --git a/app/imports/api/creature/creatures/methods/restCreature.js b/app/imports/api/creature/creatures/methods/restCreature.js deleted file mode 100644 index 7ca7c16c..00000000 --- a/app/imports/api/creature/creatures/methods/restCreature.js +++ /dev/null @@ -1,180 +0,0 @@ -import SimpleSchema from 'simpl-schema'; -import { ValidatedMethod } from 'meteor/mdg:validated-method'; -import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js'; -import { union } from 'lodash'; -import ActionContext from '/imports/api/engine/actions/ActionContext.js'; -import { applyTriggers } from '/imports/api/engine/actions/applyTriggers.js'; - -const restCreature = new ValidatedMethod({ - name: 'creature.methods.rest', - validate: new SimpleSchema({ - creatureId: { - type: String, - regEx: SimpleSchema.RegEx.Id, - }, - restType: { - type: String, - allowedValues: ['shortRest', 'longRest'], - }, - }).validator(), - mixins: [RateLimiterMixin], - rateLimit: { - numRequests: 5, - timeInterval: 5000, - }, - run({ creatureId, restType }) { - // Get action context - const actionContext = new ActionContext(creatureId, [creatureId], this); - // Check permissions - assertEditPermission(actionContext.creature, this.userId); - - // Join, sort, and apply before triggers - const beforeTriggers = union( - actionContext.triggers.anyRest?.before, actionContext.triggers[restType]?.before - ).sort((a, b) => a.order - b.order); - applyTriggers(beforeTriggers, null, actionContext); - - // Rest - actionContext.addLog({ - name: restType === 'shortRest' ? 'Short rest' : 'Long rest', - }); - doRestWork(restType, actionContext); - - // Join, sort, and apply after triggers - const afterTriggers = union( - actionContext.triggers.anyRest?.after, actionContext.triggers[restType]?.after - ).sort((a, b) => a.order - b.order); - applyTriggers(afterTriggers, null, actionContext); - - // Insert log - actionContext.writeLog(); - }, -}); - -function doRestWork(restType, actionContext) { - const creatureId = actionContext.creature._id; - // Long rests reset short rest properties as well - let resetFilter; - if (restType === 'shortRest') { - resetFilter = 'shortRest' - } else { - resetFilter = { $in: ['shortRest', 'longRest'] } - } - resetProperties(creatureId, resetFilter, actionContext); - - // Reset half hit dice on a long rest, starting with the highest dice - if (restType === 'longRest') { - resetHitDice(creatureId, actionContext); - } -} - -export function resetProperties(creatureId, resetFilter, actionContext) { - // Only apply to active properties - const filter = { - 'ancestors.id': creatureId, - reset: resetFilter, - removed: { $ne: true }, - inactive: { $ne: true }, - }; - // update all attribute's damage - const attributeFilter = { - ...filter, - type: 'attribute', - damage: { $ne: 0 }, - } - CreatureProperties.find(attributeFilter, { - fields: { name: 1, damage: 1 } - }).forEach(prop => { - actionContext.addLog({ - name: prop.name, - value: prop.damage >= 0 ? `Restored ${prop.damage}` : `Removed ${-prop.damage}` - }); - }); - CreatureProperties.update(attributeFilter, { - $set: { - damage: 0, - dirty: true, - } - }, { - selector: { type: 'attribute' }, - multi: true, - }); - // Update all action-like properties' usesUsed - const actionFilter = { - ...filter, - type: { - $in: ['action', 'spell'] - }, - usesUsed: { $ne: 0 }, - }; - CreatureProperties.find(actionFilter, { - fields: { name: 1, usesUsed: 1 } - }).forEach(prop => { - actionContext.addLog({ - name: prop.name, - value: prop.usesUsed >= 0 ? `Restored ${prop.usesUsed} uses` : `Removed ${-prop.usesUsed} uses` - }); - }); - CreatureProperties.update(actionFilter, { - $set: { - usesUsed: 0, - dirty: true, - } - }, { - selector: { type: 'action' }, - multi: true, - }); -} - -function resetHitDice(creatureId, actionContext) { - let hitDice = CreatureProperties.find({ - 'ancestors.id': creatureId, - type: 'attribute', - attributeType: 'hitDice', - removed: { $ne: true }, - inactive: { $ne: true }, - }, { - fields: { - name: 1, - hitDiceSize: 1, - damage: 1, - total: 1, - } - }).fetch(); - // Use a collator to do sorting in natural order - let collator = new Intl.Collator('en', { - numeric: true, sensitivity: 'base' - }); - // Get the hit dice in decending order of hitDiceSize - let compare = (a, b) => collator.compare(b.hitDiceSize, a.hitDiceSize) - hitDice.sort(compare); - // Get the total number of hit dice that can be recovered this rest - let totalHd = hitDice.reduce((sum, hd) => sum + (hd.total || 0), 0); - let resetMultiplier = actionContext.creature.settings.hitDiceResetMultiplier || 0.5; - let recoverableHd = Math.max(Math.floor(totalHd * resetMultiplier), 1); - // recover each hit dice in turn until the recoverable amount is used up - let amountToRecover, resultingDamage; - hitDice.forEach(hd => { - if (!recoverableHd) return; - amountToRecover = Math.min(recoverableHd, hd.damage || 0); - if (!amountToRecover) return; - recoverableHd -= amountToRecover; - resultingDamage = hd.damage - amountToRecover; - actionContext.addLog({ - name: hd.name, - value: amountToRecover >= 0 ? `Restored ${amountToRecover} hit dice` : `Removed ${-amountToRecover} hit dice` - }); - CreatureProperties.update(hd._id, { - $set: { - damage: resultingDamage, - dirty: true, - } - }, { - selector: { type: 'attribute' }, - }); - }); -} - -export default restCreature; diff --git a/app/imports/api/creature/creatures/methods/updateCreature.js b/app/imports/api/creature/creatures/methods/updateCreature.js index 4da290bc..58efc7a6 100644 --- a/app/imports/api/creature/creatures/methods/updateCreature.js +++ b/app/imports/api/creature/creatures/methods/updateCreature.js @@ -1,7 +1,7 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import Creatures from '/imports/api/creature/creatures/Creatures.js'; -import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; +import Creatures from '/imports/api/creature/creatures/Creatures'; +import { assertEditPermission } from '/imports/api/sharing/sharingPermissions'; const updateCreature = new ValidatedMethod({ name: 'creatures.update', diff --git a/app/imports/api/creature/experience/Experiences.js b/app/imports/api/creature/experience/Experiences.js index d1c5c527..26ee5739 100644 --- a/app/imports/api/creature/experience/Experiences.js +++ b/app/imports/api/creature/experience/Experiences.js @@ -1,9 +1,9 @@ import SimpleSchema from 'simpl-schema'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js'; -import Creatures from '/imports/api/creature/creatures/Creatures.js'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; +import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions'; +import Creatures from '/imports/api/creature/creatures/Creatures'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; let Experiences = new Mongo.Collection('experiences'); @@ -26,7 +26,7 @@ let ExperienceSchema = new SimpleSchema({ min: 0, index: 1, }, - // The real-world date that it occured, usually sorted by date + // The real-world date that it occurred, usually sorted by date date: { type: Date, autoValue: function () { @@ -93,7 +93,7 @@ const insertExperience = new ValidatedMethod({ let insertedIds = []; creatureIds.forEach(creatureId => { assertEditPermission(creatureId, userId); - let id = insertExperienceForCreature({ experience, creatureId, userId }); + let id = insertExperienceForCreature({ experience, creatureId }); insertedIds.push(id); }); return insertedIds; diff --git a/app/imports/api/creature/journal/JournalEntry.js b/app/imports/api/creature/journal/JournalEntry.js index 74b885ea..bb1a5059 100644 --- a/app/imports/api/creature/journal/JournalEntry.js +++ b/app/imports/api/creature/journal/JournalEntry.js @@ -1,5 +1,5 @@ import SimpleSchema from 'simpl-schema'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; let ExperienceSchema = new SimpleSchema({ title: { diff --git a/app/imports/api/creature/log/CreatureLogs.js b/app/imports/api/creature/log/CreatureLogs.js index 5b858b29..6ba45991 100644 --- a/app/imports/api/creature/log/CreatureLogs.js +++ b/app/imports/api/creature/log/CreatureLogs.js @@ -1,17 +1,19 @@ import SimpleSchema from 'simpl-schema'; -import Creatures from '/imports/api/creature/creatures/Creatures.js'; -import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables.js'; -import LogContentSchema from '/imports/api/creature/log/LogContentSchema.js'; +import Creatures from '/imports/api/creature/creatures/Creatures'; +import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables'; +import LogContentSchema from '/imports/api/creature/log/LogContentSchema'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js'; -import { parse, prettifyParseError } from '/imports/parser/parser.js'; -import resolve, { toString } from '/imports/parser/resolve.js'; +import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions'; +import { parse, prettifyParseError } from '/imports/parser/parser'; +import resolve from '/imports/parser/resolve'; +import toString from '/imports/parser/toString'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; + const PER_CREATURE_LOG_LIMIT = 100; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; if (Meteor.isServer) { - var sendWebhookAsCreature = require('/imports/server/discord/sendWebhook.js').sendWebhookAsCreature; + var sendWebhookAsCreature = require('/imports/server/discord/sendWebhook').sendWebhookAsCreature; } let CreatureLogs = new Mongo.Collection('creatureLogs'); @@ -36,9 +38,15 @@ let CreatureLogSchema = new SimpleSchema({ }, index: 1, }, + // The acting creature initiating the logged events creatureId: { type: String, - regEx: SimpleSchema.RegEx.Id, + index: 1, + }, + // The tabletops this log is associated with + tabletopId: { + type: String, + optional: true, index: 1, }, creatureName: { @@ -50,11 +58,17 @@ let CreatureLogSchema = new SimpleSchema({ CreatureLogs.attachSchema(CreatureLogSchema); -function removeOldLogs(creatureId) { +function removeOldLogs({ creatureId, tabletopId }) { + let filter; + if (creatureId && tabletopId || (!creatureId && !tabletopId)) { + throw Error('Provide either creatureId or tabletopId') + } else if (creatureId) { + filter = { creatureId }; + } else if (tabletopId) { + filter = { tabletopId } + } // Find the first log that is over the limit - let firstExpiredLog = CreatureLogs.find({ - creatureId - }, { + let firstExpiredLog = CreatureLogs.find(filter, { sort: { date: -1 }, skip: PER_CREATURE_LOG_LIMIT, }); @@ -70,10 +84,21 @@ function logToMessageData(log) { let embed = { fields: [], }; - log.content.forEach(field => { + log.content.forEach((field, index) => { + // Empty character for blank names if (!field.name) field.name = '\u200b'; if (!field.value) field.value = '\u200b'; - embed.fields.push(field); + // Enforce Discord field character limits + if (field.name?.length > 256) { + field.name = field.name.substring(0, 255); + } + if (field.value?.length > 1024) { + field.value = field.value.substring(0, 1024 - 3) + '...'; + } + // Enforce Discord 25 field limit + if (index < 25) { + embed.fields.push(field); + } }); return { embeds: [embed] }; } @@ -107,6 +132,7 @@ const insertCreatureLog = new ValidatedMethod({ 'settings.discordWebhook': 1, name: 1, avatarPicture: 1, + tabletop: 1, } }); assertEditPermission(creature, this.userId); @@ -122,13 +148,27 @@ export function insertCreatureLogWork({ log, creature, method }) { log = { content: [{ value: log }] }; } if (!log.content?.length) return; + + // Truncate the string lengths to fit the log content schema + log.content.forEach((logItem) => { + if (logItem.value?.length > STORAGE_LIMITS.summary) { + logItem.value = logItem.value.substring(0, STORAGE_LIMITS.summary - 3) + '...'; + } + }); log.date = new Date(); + if (creature && creature.tabletop) log.tabletopId = creature.tabletop; // Insert it let id = CreatureLogs.insert(log); if (Meteor.isServer) { method?.unblock(); - removeOldLogs(creature._id); - logWebhook({ log, creature }); + if (creature) { + logWebhook({ log, creature }); + } + if (log.tabletopId) { + removeOldLogs({ tabletopId: log.tabletopId }); + } else { + removeOldLogs({ creatureId: creature._id }); + } } return id; } @@ -153,21 +193,28 @@ const logRoll = new ValidatedMethod({ creatureId: { type: String, regEx: SimpleSchema.RegEx.Id, + optional: true, }, }).validator(), - run({ roll, creatureId }) { - const creature = Creatures.findOne(creatureId, { - fields: { - readers: 1, - writers: 1, - owner: 1, - 'settings.discordWebhook': 1, - name: 1, - avatarPicture: 1, - } - }); - assertEditPermission(creature, this.userId); - const variables = CreatureVariables.findOne({ _creatureId: creatureId }); + async run({ roll, creatureId }) { + if (!creatureId) throw new Meteor.Error('no-id', + 'A creature id must be given' + ); + let creature; + if (creatureId) { + creature = Creatures.findOne(creatureId, { + fields: { + readers: 1, + writers: 1, + owner: 1, + 'settings.discordWebhook': 1, + name: 1, + avatarPicture: 1, + } + }); + assertEditPermission(creature, this.userId); + } + const variables = CreatureVariables.findOne({ _creatureId: creatureId }) || {}; let logContent = [] let parsedResult = undefined; try { @@ -180,7 +227,7 @@ const logRoll = new ValidatedMethod({ let { result: compiled, context - } = resolve('compile', parsedResult, variables); + } = await resolve('compile', parsedResult, variables); const compiledString = toString(compiled); if (!equalIgnoringWhitespace(compiledString, roll)) logContent.push({ value: roll @@ -188,12 +235,12 @@ const logRoll = new ValidatedMethod({ logContent.push({ value: compiledString }); - let { result: rolled } = resolve('roll', compiled, variables, context); + let { result: rolled } = await resolve('roll', compiled, variables, context); let rolledString = toString(rolled); if (rolledString !== compiledString) logContent.push({ value: rolledString }); - let { result } = resolve('reduce', rolled, variables, context); + let { result } = await resolve('reduce', rolled, variables, context); let resultString = toString(result); if (resultString !== rolledString) logContent.push({ value: resultString diff --git a/app/imports/api/creature/log/LogContentSchema.js b/app/imports/api/creature/log/LogContentSchema.ts similarity index 69% rename from app/imports/api/creature/log/LogContentSchema.js rename to app/imports/api/creature/log/LogContentSchema.ts index 05d9ce87..2599bb1a 100644 --- a/app/imports/api/creature/log/LogContentSchema.js +++ b/app/imports/api/creature/log/LogContentSchema.ts @@ -1,7 +1,18 @@ import SimpleSchema from 'simpl-schema'; -import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js'; -import RollDetailsSchema from '/imports/api/properties/subSchemas/RollDetailsSchema.js'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; +import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema'; +import RollDetailsSchema from '/imports/api/properties/subSchemas/RollDetailsSchema'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; + +export interface LogContent { + name?: string + value?: string + inline?: boolean + context?: { + errors: any[] + rolls: any[] + doubleRolls?: boolean + } +} let LogContentSchema = new SimpleSchema({ // The name of the field, included in discord webhook message @@ -22,11 +33,16 @@ let LogContentSchema = new SimpleSchema({ type: Boolean, optional: true, }, + // This log entry was silenced + silenced: { + type: Boolean, + optional: true, + }, context: { type: Object, optional: true, }, - 'context.errors':{ + 'context.errors': { type: Array, defaultValue: [], maxCount: STORAGE_LIMITS.errorCount, @@ -46,6 +62,13 @@ let LogContentSchema = new SimpleSchema({ type: Boolean, optional: true, }, + targetIds: { + type: Array, + optional: true, + }, + 'targetIds.$': { + type: String, + } }); export default LogContentSchema; diff --git a/app/imports/api/creature/mixins/creaturePermissionMixin.js b/app/imports/api/creature/mixins/creaturePermissionMixin.js index 18b96fbd..de419e82 100644 --- a/app/imports/api/creature/mixins/creaturePermissionMixin.js +++ b/app/imports/api/creature/mixins/creaturePermissionMixin.js @@ -2,7 +2,7 @@ import { assertEditPermission, assertViewPermission, assertOwnership, -} from '/imports/api/creature/creatures/creaturePermissions.js'; +} from '/imports/api/creature/creatures/creaturePermissions'; // Checks if the method has permission to run on the document. If the document // has a charId, that creature is checked, otherwise if it has an _id and the @@ -13,36 +13,36 @@ import { // Because this mixin injects the charId into argument objects that don't // already contain it, it should always come last in the mixin list, so that it // the outermost wrapper of the run function -export default function creaturePermissionMixin(methodOptions){ +export default function creaturePermissionMixin(methodOptions) { let assertPermission; - if (methodOptions.permission === 'owner'){ + if (methodOptions.permission === 'owner') { assertPermission = assertOwnership; - } else if (methodOptions.permission === 'edit'){ + } else if (methodOptions.permission === 'edit') { assertPermission = assertEditPermission; - } else if (methodOptions.permission === 'view'){ + } else if (methodOptions.permission === 'view') { assertPermission = assertViewPermission; } else { throw "`permission` missing in method options"; } let getCharId; - if (methodOptions.getCharId){ + if (methodOptions.getCharId) { getCharId = methodOptions.getCharId; } else if (methodOptions.collection) { - getCharId = function({_id}){ + getCharId = function ({ _id }) { return methodOptions.collection.findOne(_id, { - fields: {charId: 1} + fields: { charId: 1 } }).charId; }; } else { - getCharId = function(){ + getCharId = function () { throw "`getCharId` or `collection` missing in method options," + " or {charId} missing in call"; }; } let runFunc = methodOptions.run; - methodOptions.run = function(doc, ...rest){ + methodOptions.run = function (doc, ...rest) { // Store the charId on the doc for other mixins if it had to be fetched doc.charId = doc.charId || getCharId.apply(this, arguments); assertPermission(doc.charId, this.userId); diff --git a/app/imports/api/creature/mixins/propagateInheritanceUpdateMixin.js b/app/imports/api/creature/mixins/propagateInheritanceUpdateMixin.js deleted file mode 100644 index 0ed6ee9f..00000000 --- a/app/imports/api/creature/mixins/propagateInheritanceUpdateMixin.js +++ /dev/null @@ -1,59 +0,0 @@ -import { - updateChildren, - updateDescendants, -} from '/imports/api/parenting/parenting.js'; -import { inheritedFields } from '/imports/api/parenting/ChildSchema.js'; -import MONGO_OPERATORS from '/imports/constants/MONGO_OPERATORS.js'; - -// This mixin can be safely applied to all update methods which are validated -// with the updateSchemaMixin. It will propagate updates to fields which -// are inherited and normalised on the parent or ancestor docs -// It should have neglible performance impact for updates that aren't inherited -function propagateInheritanceUpdate({_id, update}){ - let childModifier = {}; - let descendantModifier = {}; - // For each operator - for (let operator of MONGO_OPERATORS){ - // If the operator is in the update, for each field - if (update[operator]) for (let field in update[operator]){ - // Get the top level field that was actually modified - const indexOfDot = field.indexOf('.'); - let modifiedField; - if (indexOfDot !== -1) { - modifiedField = field.substring(0, indexOfDot); - } else { - modifiedField = field; - } - // If that field is updated and inherited - if (inheritedFields.has(modifiedField)){ - // Perform the same update on the descendants - if (!childModifier[operator]) childModifier[operator] = {}; - if (!descendantModifier[operator]) descendantModifier[operator] = {}; - childModifier[operator][`parent.${field}`] = update[operator][field]; - descendantModifier[operator][`ancestors.$.${field}`] = update[operator][field]; - } - } - } - - // Update the parent object of its children - updateChildren({ - parentId: _id, - modifier: childModifier, - }); - - // Update the ancestors object of its descendants - updateDescendants({ - ancestorId: _id, - modifier: descendantModifier, - }); -} - -export default function propagateInheritanceUpdateMixin(methodOptions){ - let runFunc = methodOptions.run; - methodOptions.run = function({_id, update}){ - const result = runFunc.apply(this, arguments); - propagateInheritanceUpdate({_id, update}); - return result; - }; - return methodOptions; -} diff --git a/app/imports/api/creature/mixins/recomputeCreatureMixin.js b/app/imports/api/creature/mixins/recomputeCreatureMixin.js index a6b991a7..32caa2f6 100644 --- a/app/imports/api/creature/mixins/recomputeCreatureMixin.js +++ b/app/imports/api/creature/mixins/recomputeCreatureMixin.js @@ -1,8 +1,8 @@ -import computeCreature from '/imports/api/engine/computeCreature.js'; +import computeCreature from '/imports/api/engine/computeCreature'; -export default function recomputeCreatureMixin(methodOptions){ +export default function recomputeCreatureMixin(methodOptions) { let runFunc = methodOptions.run; - methodOptions.run = function({charId}){ + methodOptions.run = function ({ charId }) { const result = runFunc.apply(this, arguments); if ( methodOptions.skipRecompute && diff --git a/app/imports/api/creature/mixins/setDocToLastMixin.js b/app/imports/api/creature/mixins/setDocToLastMixin.js deleted file mode 100644 index 53a4a0f9..00000000 --- a/app/imports/api/creature/mixins/setDocToLastMixin.js +++ /dev/null @@ -1,27 +0,0 @@ -import SimpleSchema from 'simpl-schema'; -import { setDocToLastOrder } from '/imports/api/parenting/order.js'; - -export function setDocToLastMixin(methodOptions){ - // Make sure the doc has a charId - // This mixin should come before simpleSchemaMixin so that it can extend the - // schema before it is turned into a validate function - if (methodOptions.validate){ - throw new Meteor.Error(`setDocToLastMixin should come before simpleSchemaMixin`); - } - methodOptions.schema = new SimpleSchema({ - charId: { - type: String, - regEx: SimpleSchema.RegEx.Id, - }, - }).extend(methodOptions.schema); - let collection = methodOptions.collection; - if (!collection){ - throw new Meteor.Error("`collection` required in method options for setDocToLastMixin"); - } - let runFunc = methodOptions.run; - methodOptions.run = function(doc){ - setDocToLastOrder({collection, doc}); - return runFunc.apply(this, arguments); - }; - return methodOptions; -} diff --git a/app/imports/api/docs/Docs.js b/app/imports/api/docs/Docs.js deleted file mode 100644 index 91961b16..00000000 --- a/app/imports/api/docs/Docs.js +++ /dev/null @@ -1,3 +0,0 @@ -if (Meteor.isServer) throw 'Client side only collection, don\'t import on server'; -const Docs = new Mongo.Collection('docs'); -export default Docs; diff --git a/app/imports/api/docs/Docs.ts b/app/imports/api/docs/Docs.ts new file mode 100644 index 00000000..bb7de8f6 --- /dev/null +++ b/app/imports/api/docs/Docs.ts @@ -0,0 +1,287 @@ +import { Meteor } from 'meteor/meteor'; +import { ValidatedMethod } from 'meteor/mdg:validated-method'; +import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; +import SimpleSchema from 'simpl-schema'; +import { softRemove } from '/imports/api/parenting/softRemove'; +import SoftRemovableSchema from '/imports/api/parenting/SoftRemovableSchema'; +import { storedIconsSchema } from '/imports/api/icons/Icons'; +import '/imports/api/library/methods/index'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; +import { restore } from '/imports/api/parenting/softRemove'; +import { getFilter, rebuildNestedSets, moveDocWithinRoot } from '/imports/api/parenting/parentingFunctions'; +import ChildSchema, { TreeDoc } from '/imports/api/parenting/ChildSchema'; + +// Give the docs a common root, so they can share parenting logic +export const DOC_ROOT_ID = 'DDDDDDDDDDDDDDDDD' + +type Doc = { + _id: string, + name: string, + urlName: string, + href: string, + description?: string, + published?: true, + icon?: { + name: string, + shape: string, + }, +} & TreeDoc; + +const Docs: Mongo.Collection & { + getJsonDocs?: () => string +} = new Mongo.Collection('docs'); + +const DocSchema = new SimpleSchema({ + _id: { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, + name: { + type: String, + max: STORAGE_LIMITS.description, + }, + urlName: { + type: String, + regEx: /[a-z]+(?:[a-z]|-)+/, + min: 2, + max: STORAGE_LIMITS.variableName, + }, + href: { + type: String, + }, + description: { + type: String, + optional: true, + }, + published: { + type: Boolean, + optional: true, + }, + icon: { + type: storedIconsSchema, + optional: true, + max: STORAGE_LIMITS.icon, + }, +}); + +const schema = new SimpleSchema({}); +schema.extend(DocSchema); +schema.extend(ChildSchema); +schema.extend(SoftRemovableSchema); +// @ts-expect-error No attach schema in types +Docs.attachSchema(schema); + +function assertDocsEditPermission(userId) { + if (!userId || typeof userId !== 'string') throw new Meteor.Error('No user id provided'); + const user = Meteor.users.findOne(userId); + if (!user) throw new Meteor.Error('User does not exist'); + if (!user?.roles?.includes?.('docsWriter')) throw ('Permission denied') +} + +function getDocLink(doc: Doc, urlName?: string) { + if (!urlName) urlName = doc.urlName; + const address = ['/docs']; + const ancestorDocs = Docs.find(getFilter.ancestors(doc)); + ancestorDocs?.forEach(a => { + address.push(a.urlName); + }); + address.push(urlName); + return address.join('/'); +} + +// Add a means of seeding new servers with documentation +if (Meteor.isClient) { + Docs.getJsonDocs = function () { + return JSON.stringify(Docs.find({}).fetch(), null, 2); + } +} else if (Meteor.isServer) { + Meteor.startup(() => { + if (!Docs.findOne()) { + console.log('No docs found, filling documentation with defaults'); + Assets.getText('docs/defaultDocs.json', (error, string) => { + const docs = JSON.parse(string) + docs.forEach(doc => Docs.insert(doc)); + rebuildNestedSets(Docs, DOC_ROOT_ID); + }); + } + }); +} + +const insertDoc = new ValidatedMethod({ + name: 'docs.insert', + validate: null, + mixins: [RateLimiterMixin], + rateLimit: { + numRequests: 5, + timeInterval: 5000, + }, + run({ doc, parentId }) { + delete doc._id; + assertDocsEditPermission(this.userId); + + doc.parentId = parentId; + doc.root = { + collection: 'docs', + id: DOC_ROOT_ID, + }; + + const lastOrder = Docs.find({}, { sort: { left: -1 }, limit: 1 }).fetch()[0]?.left || 0; + doc.urlName = 'new-doc-' + (lastOrder + 1); + doc.href = getDocLink(doc); + if (Docs.findOne({ href: doc.href })) { + throw new Meteor.Error('Link collision', 'A document with the same URL already exists'); + } + + const docId = Docs.insert(doc); + rebuildNestedSets(Docs, DOC_ROOT_ID); + return docId; + }, +}); + +const updateDoc = new ValidatedMethod({ + name: 'docs.update', + validate({ _id, path }) { + if (!_id) return false; + // We cannot change these fields with a simple update + switch (path[0]) { + case '_is': + return false; + } + }, + mixins: [RateLimiterMixin], + rateLimit: { + numRequests: 5, + timeInterval: 5000, + }, + run({ _id, path, value }) { + assertDocsEditPermission(this.userId); + const pathString = path.join('.'); + let modifier; + // unset empty values + if (value === null || value === undefined) { + modifier = { $unset: { [pathString]: 1 } }; + } else { + modifier = { $set: { [pathString]: value } }; + } + if (pathString === 'urlName') { + const doc = Docs.findOne(_id); + if (!doc) throw new Meteor.Error('Not Found', 'The document you are trying to edit was not found'); + const newLink = getDocLink(doc, value); + if (Docs.findOne({ href: newLink })) { + throw new Meteor.Error('Link collision', 'A document with the same URL already exists'); + } + modifier.$set = modifier.$set || {}; + modifier.$set.href = newLink; + } + const updates = Docs.update(_id, modifier); + rebuildNestedSets(Docs, DOC_ROOT_ID); + return updates; + }, +}); + +const pushToDoc = new ValidatedMethod({ + name: 'docs.push', + validate: null, + mixins: [RateLimiterMixin], + rateLimit: { + numRequests: 5, + timeInterval: 5000, + }, + run({ _id, path, value }) { + assertDocsEditPermission(this.userId); + return Docs.update(_id, { + $push: { [path.join('.')]: value }, + }); + } +}); + +const pullFromDoc = new ValidatedMethod({ + name: 'docs.pull', + validate: null, + mixins: [RateLimiterMixin], + rateLimit: { + numRequests: 5, + timeInterval: 5000, + }, + run({ _id, path, itemId }) { + assertDocsEditPermission(this.userId); + return Docs.update(_id, { + $pull: { [path.join('.')]: { _id: itemId } }, + }); + } +}); + +const softRemoveDoc = new ValidatedMethod({ + name: 'docs.softRemove', + validate: new SimpleSchema({ + _id: SimpleSchema.RegEx.Id + }).validator(), + mixins: [RateLimiterMixin], + rateLimit: { + numRequests: 5, + timeInterval: 5000, + }, + run({ _id }) { + assertDocsEditPermission(this.userId); + softRemove(Docs, _id); + rebuildNestedSets(Docs, DOC_ROOT_ID); + } +}); + +const restoreDoc = new ValidatedMethod({ + name: 'docs.restore', + validate: new SimpleSchema({ + _id: SimpleSchema.RegEx.Id + }).validator(), + mixins: [RateLimiterMixin], + rateLimit: { + numRequests: 5, + timeInterval: 5000, + }, + run({ _id }) { + assertDocsEditPermission(this.userId); + restore('docs', _id); + rebuildNestedSets(Docs, DOC_ROOT_ID); + } +}); + +const organizeDoc = new ValidatedMethod({ + name: 'docs.organizeDoc', + validate: new SimpleSchema({ + docId: String, + newPosition: Number, + skipClient: { + type: Boolean, + optional: true, + } + }).validator(), + mixins: [RateLimiterMixin], + rateLimit: { + numRequests: 5, + timeInterval: 5000, + }, + async run({ docId, newPosition, skipClient }: { docId: string, newPosition: number, skipClient?: boolean }) { + if (skipClient && this.isSimulation) { + return; + } + assertDocsEditPermission(this.userId); + + const doc = Docs.findOne(docId); + if (!doc) throw new Meteor.Error('not found', 'The doc you are moving was not found'); + // Move the doc + await moveDocWithinRoot(doc, Docs, newPosition); + }, +}); + +export { + DocSchema, + insertDoc, + updateDoc, + pushToDoc, + pullFromDoc, + softRemoveDoc, + restoreDoc, + organizeDoc, +}; + +export default Docs; diff --git a/app/imports/api/engine/action/ActionEngine.test.ts b/app/imports/api/engine/action/ActionEngine.test.ts new file mode 100644 index 00000000..31041ba8 --- /dev/null +++ b/app/imports/api/engine/action/ActionEngine.test.ts @@ -0,0 +1,448 @@ +import { assert } from 'chai'; +import '/imports/api/simpleSchemaConfig.js'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; +import { propsFromForest } from '/imports/api/properties/tests/propTestBuilder.testFn'; +import Creatures from '/imports/api/creature/creatures/Creatures'; +import computeCreature from '/imports/api/engine/computeCreature'; +import { loadCreature } from '/imports/api/engine/loadCreatures'; +import EngineActions, { EngineAction } from '/imports/api/engine/action/EngineActions'; +import applyAction from '/imports/api/engine/action/functions/applyAction'; +import { LogContent, Removal, Update } from '/imports/api/engine/action/tasks/TaskResult'; +import inputProvider from './functions/userInput/inputProviderForTests.testFn'; +import { removeAllCreaturesAndProps } from '/imports/api/engine/action/functions/actionEngineTest.testFn'; + +const creatureId = Random.id(); +const targetId = Random.id(); + +describe('Interrupt action system', function () { + const dummySubscription = Tracker.autorun(() => undefined) + this.timeout(8000); + before(async function () { + // Remove old data + await removeAllCreaturesAndProps(); + + // Add creatures + await Promise.all([ + Creatures.insertAsync({ + _id: creatureId, + name: 'action test creature', + owner: Random.id(), + dirty: true, + type: 'pc', + readers: [], + writers: [], + public: false, + settings: {}, + }), + Creatures.insertAsync({ + _id: targetId, + name: 'action test creature', + owner: Random.id(), + dirty: true, + type: 'pc', + readers: [], + writers: [], + public: false, + settings: {}, + }) + ]); + // Add test props + await insertActionTestProps(); + // Compute before load or we might run tests before the computation changes reflect in the cache + computeCreature(creatureId); + computeCreature(targetId); + loadCreature(creatureId, dummySubscription); + }); + after(function () { + dummySubscription.stop(); + }); + it('writes notes to the log', async function () { + const action = await runActionById(note1Id); + assert.deepEqual( + allLogContent(action), + [{ value: 'Note 1 summary. 1 + 1 = 2' }] + ); + }); + it('Applies children of folders', async function () { + const action = await runActionById(folderId); + assert.deepEqual( + allLogContent(action), + [{ value: 'child of folder' }] + ); + }); + it('Applies the children of if branches', async function () { + let action = await runActionById(ifTruthyBranchId); + assert.deepEqual( + allLogContent(action), + [{ value: 'child of if branch' }] + ); + action = await runActionById(ifFalsyBranchId); + assert.deepEqual( + allLogContent(action), + [] + ); + }); + it('Applies the children of index branches', async function () { + const action = await runActionById(indexBranchId); + assert.deepEqual( + allLogContent(action), + [{ value: 'child 2 of index branch' }] + ); + }); + it('Gets choices from choice branches', async function () { + const action = await runActionById(choiceBranchId); + assert.deepEqual( + allLogContent(action), + [{ value: 'child 1 of choice branch' }] + ); + }); + it('Applies adjustments', async function () { + let action = await runActionById(adjustmentSetId); + assert.deepEqual( + allUpdates(action), + [{ + propId: adjustedStatId, + type: 'attribute', + set: { damage: 5, value: 3 }, + }], + 'Applying set adjustments should return the correct updates' + ); + action = await runActionById(adjustmentIncrementId) + assert.deepEqual( + allUpdates(action), + [{ + propId: adjustedStatId, + type: 'attribute', + inc: { damage: 2, value: -2 }, // damage goes up by 2, value down by 2 + }], + 'Applying increment adjustments should return the correct updates' + ); + }); + it('Applies rolls', async function () { + const action = await runActionById(rollId); + assert.deepEqual(allLogContent(action), [ + { + name: 'New Roll', + value: '7d1 [1, 1, 1, 1, 1, 1, 1] + 9\n**16**', + inline: true, + }, { + value: 'rollVar: 16' + } + ]); + }); + it('Applies buffs', async function () { + const action = await runActionById(buffId); + const inserts = allInserts(action); + const newIds = inserts.map(p => p._id); + assert.notEqual(buffId, newIds[0]); + assert.deepEqual(inserts, [ + { + _id: newIds[0], + left: 43, + parentId: null, + right: 48, + root: { + collection: 'creatures', + id: creatureId, + }, + tags: [], + target: 'self', + type: 'buff', + }, { + _id: newIds[1], + attributeType: 'stat', + baseValue: { + calculation: '13 + buffSourceStat + 7', + }, + left: 44, + parentId: newIds[0], + right: 45, + root: { + collection: 'creatures', + id: creatureId, + }, + tags: [], + type: 'attribute', + variableName: 'buffStat', + }, { + _id: newIds[2], + left: 46, + parentId: newIds[0], + removeAll: true, + right: 47, + root: { + collection: 'creatures', + id: creatureId, + }, + tags: [], + target: 'self', + targetParentBuff: true, + type: 'buffRemover', + } + ]); + }); + it('Removes parent buffs', async function () { + const action = await runActionById(removeParentBuffId); + assert.deepEqual(allRemovals(action), [ + { propId: buffId } + ]); + }); + it('Removes all buffs by tag', async function () { + const action = await runActionById(removeTaggedBuffsId); + assert.deepEqual(allRemovals(action), [ + { propId: taggedBuffId }, + { propId: secondTaggedBuffId }, + ]); + }); + it('Removes a single buff by tag', async function () { + const action = await runActionById(removeOneTaggedBuffId); + assert.deepEqual(allRemovals(action), [ + { propId: taggedBuffId }, + ]); + }); +}); + +function createAction(prop, targetIds?) { + const action: EngineAction = { + creatureId: prop.root.id, + results: [], + taskCount: 0, + task: { + prop, + targetIds, + } + }; + return EngineActions.insertAsync(action); +} + +async function runActionById(propId) { + const prop = await CreatureProperties.findOneAsync(propId); + const actionId = await createAction(prop); + const action = await EngineActions.findOneAsync(actionId); + if (!action) throw 'Action is expected to exist'; + await applyAction(action, inputProvider, { simulate: true }); + return action; +} + +function allUpdates(action: EngineAction) { + const updates: Update[] = []; + action.results.forEach(result => { + result.mutations.forEach(mutation => { + mutation.updates?.forEach(update => { + updates.push(update); + }); + }); + }); + return updates; +} + +function allInserts(action: EngineAction) { + const inserts: any[] = []; + action.results.forEach(result => { + result.mutations.forEach(mutation => { + mutation.inserts?.forEach(update => { + inserts.push(update); + }); + }); + }); + return inserts; +} + +function allRemovals(action: EngineAction) { + const removals: Removal[] = []; + action.results.forEach(result => { + result.mutations.forEach(mutation => { + mutation.removals?.forEach(update => { + removals.push(update); + }); + }); + }); + return removals +} + +function allLogContent(action: EngineAction) { + const contents: LogContent[] = []; + action.results.forEach(result => { + result.mutations.forEach(mutation => { + mutation.contents?.forEach(logContent => { + contents.push(logContent); + }); + }); + }); + return contents; +} + +let note1Id, folderId, ifTruthyBranchId, ifFalsyBranchId, indexBranchId, choiceBranchId, + adjustedStatId, adjustmentIncrementId, adjustmentSetId, rollId, buffId, + removeParentBuffId, removeTaggedBuffsId, removeOneTaggedBuffId, taggedBuffId, secondTaggedBuffId; + +const propForest = [ + // Apply a simple note + { + _id: note1Id = Random.id(), + type: 'note', + summary: { + text: 'Note 1 summary. 1 + 1 = {1 + 1}' + }, + }, + // Apply a folder with a note inside + { + _id: folderId = Random.id(), + type: 'folder', + children: [{ type: 'note', summary: { text: 'child of folder' } }], + }, + // Apply an if branch with a truthy condition + { + _id: ifTruthyBranchId = Random.id(), + type: 'branch', + branchType: 'if', + condition: { calculation: '1 + 1' }, + children: [{ type: 'note', summary: { text: 'child of if branch' } }], + }, + // Apply an if branch with a falsy condition + { + _id: ifFalsyBranchId = Random.id(), + type: 'branch', + branchType: 'if', + condition: { calculation: '1 - 1' }, + children: [{ type: 'note', summary: { text: 'child of if branch' } }], + }, + // Apply an index branch + { + _id: indexBranchId = Random.id(), + type: 'branch', + branchType: 'index', + condition: { calculation: '1 + 1' }, + children: [ + { type: 'note', summary: { text: 'child 1 of index branch' } }, + { type: 'note', summary: { text: 'child 2 of index branch' } }, + { type: 'note', summary: { text: 'child 3 of index branch' } }, + ], + }, + // Apply a choice branch + { + _id: choiceBranchId = Random.id(), + type: 'branch', + branchType: 'choice', + children: [ + { type: 'note', summary: { text: 'child 1 of choice branch' } }, + { type: 'note', summary: { text: 'child 2 of choice branch' } }, + { type: 'note', summary: { text: 'child 3 of choice branch' } }, + ], + }, + // Apply adjustments + { + _id: adjustedStatId = Random.id(), + type: 'attribute', + attributeType: 'stat', + variableName: 'adjustedStat', + baseValue: { calculation: '8' }, + }, { + _id: adjustmentSetId = Random.id(), + type: 'adjustment', + stat: 'adjustedStat', + operation: 'set', + amount: { calculation: '3' }, + target: 'self', + children: [ + { type: 'note', summary: { text: 'adjustment set applied' } }, + ], + }, { + _id: adjustmentIncrementId = Random.id(), + type: 'adjustment', + stat: 'adjustedStat', + operation: 'increment', + amount: { calculation: '2' }, + target: 'self', + children: [ + { type: 'note', summary: { text: 'adjustment increment applied' } }, + ], + }, + // Apply buffs + { + _id: Random.id(), + type: 'attribute', + attributeType: 'stat', + variableName: 'buffSourceStat', + baseValue: { calculation: '13' }, + }, { + _id: buffId = Random.id(), + type: 'buff', + target: 'self', + children: [ + { + _id: Random.id(), + type: 'attribute', + attributeType: 'stat', + variableName: 'buffStat', + baseValue: { calculation: 'buffSourceStat + ~target.buffSourceStat + 7' }, + }, { + _id: removeParentBuffId = Random.id(), + type: 'buffRemover', + target: 'self', + targetParentBuff: true, + }, + ], + }, + // Extra buffs with and without tags + { + _id: taggedBuffId = Random.id(), + name: 'Tagged Buff', + type: 'buff', + tags: ['buff tag', 'other tag'] + }, { + _id: secondTaggedBuffId = Random.id(), + name: 'Tagged buff 2', + type: 'buff', + tags: ['buff tag', 'yet another tag'] + }, { + _id: Random.id(), + name: 'Untagged buff', + type: 'buff', + tags: ['other tag'] + }, + // Remove buffs by tag + { + _id: removeTaggedBuffsId = Random.id(), + type: 'buffRemover', + target: 'self', + removeAll: true, + targetTags: 'buff tag', + }, { + _id: removeOneTaggedBuffId = Random.id(), + type: 'buffRemover', + target: 'self', + removeAll: false, + targetTags: 'buff tag', + }, + // Apply rolls + { + _id: rollId = Random.id(), + type: 'roll', + // Roll d1's because it's a pain to test random numbers + roll: { calculation: '1 + 3 + 7d1 + 5' }, + variableName: 'rollVar', + children: [ + { type: 'note', summary: { text: 'rollVar: {rollVar}' } } + ] + } +]; + +const targetPropForest = [ + { + type: 'attribute', + attributeType: 'stat', + variableName: 'armor', + baseValue: { calculation: '10' }, + } +]; + +function insertActionTestProps() { + const promises = propsFromForest(propForest, creatureId).map(prop => { + return CreatureProperties.insertAsync(prop); + }); + propsFromForest(targetPropForest, targetId).forEach(prop => { + promises.push(CreatureProperties.insertAsync(prop)); + }); + return Promise.all(promises); +} diff --git a/app/imports/api/engine/action/EngineActions.ts b/app/imports/api/engine/action/EngineActions.ts new file mode 100644 index 00000000..966e5a87 --- /dev/null +++ b/app/imports/api/engine/action/EngineActions.ts @@ -0,0 +1,137 @@ +import SimpleSchema from 'simpl-schema'; +import TaskResult from './tasks/TaskResult'; +import LogContentSchema from '/imports/api/creature/log/LogContentSchema'; +import Task from './tasks/Task'; + +const EngineActions = new Mongo.Collection('actions'); + +export interface EngineAction { + _id?: string; + _isSimulation?: boolean; + _stepThrough?: boolean; + _decisions?: any[], + task: Task; + creatureId: string; + tabletopId?: string; + results: TaskResult[]; + taskCount: number; +} + +const ActionSchema = new SimpleSchema({ + creatureId: { + type: String, + regEx: SimpleSchema.RegEx.Id, + // @ts-expect-error index not defined + index: 1, + }, + rootPropId: { + type: String, + regEx: SimpleSchema.RegEx.Id, + optional: true, + }, + tabletopId: { + type: String, + regEx: SimpleSchema.RegEx.Id, + optional: true, + // @ts-expect-error index not defined + index: 1, + }, + task: { + type: Object, + blackbox: true, + }, + // Applied properties + results: { + type: Array, + defaultValue: [], + }, + 'results.$': { + type: Object, + }, + // The property and target ids popped off the task stack + // Pushing these to the top of the stack and deleting the results from this point onwards + // Should re-run the action identically from this point + 'results.$.propId': { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, + 'results.$.targetIds': { + type: Array, + defaultValue: [], + }, + 'results.$.targetIds.$': { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, + // Changes that override the local scope + 'results.$.scope': { + type: Object, + optional: true, + blackbox: true, + }, + // Changes that consume pushed values from the local scope + 'results.$.popScope': { + type: Object, + optional: true, + blackbox: true, + }, + // Changes that push values to the local scope + 'results.$.pushScope': { + type: Object, + optional: true, + blackbox: true, + }, + // database changes + 'results.$.mutations': { + type: Array, + optional: true, + }, + 'results.$.mutations.$': { + type: Object, + }, + 'results.$.mutations.$.targetIds': { + type: Array, + }, + 'results.$.mutations.$.targetIds.$': { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, + 'results.$.mutations.$.updates': { + type: Array, + optional: true, + }, + 'results.$.mutations.$.updates.$': { + type: Object, + }, + 'results.$.mutations.$.updates.$.propId': { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, + // Required, because CreatureProperties.update requires a selector of { type } + 'results.$.mutations.$.updates.$.type': { + type: String, + }, + 'results.$.mutations.$.updates.$.set': { + type: Object, + optional: true, + blackbox: true, + }, + 'results.$.mutations.$.updates.$.inc': { + type: Object, + optional: true, + blackbox: true, + }, + 'results.$.mutations.$.contents': { + type: Array, + optional: true, + }, + 'results.$.mutations.$.contents.$': { + type: LogContentSchema, + }, +}); + +// @ts-expect-error Collections2 lacks TypeScript support +EngineActions.attachSchema(ActionSchema); + +export default EngineActions; +export { ActionSchema } diff --git a/app/imports/api/engine/action/applyProperties/applyActionProperty.test.ts b/app/imports/api/engine/action/applyProperties/applyActionProperty.test.ts new file mode 100644 index 00000000..7f03a562 --- /dev/null +++ b/app/imports/api/engine/action/applyProperties/applyActionProperty.test.ts @@ -0,0 +1,458 @@ +import { assert } from 'chai'; +import { + allLogContent, + allMutations, + allUpdates, + createTestCreature, + getRandomIds, + removeAllCreaturesAndProps, + runActionById +} from '/imports/api/engine/action/functions/actionEngineTest.testFn'; +import { LogContent, Mutation, Update } from '/imports/api/engine/action/tasks/TaskResult'; +import Alea from 'alea'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; + +const [ + creatureId, targetCreatureId, targetCreature2Id, emptyActionId, selfActionId, attackActionId, + usesActionId, attackMissId, attackNoTargetId, usesResourcesActionId, ammoId, resourceAttId, + consumeAmmoId, consumeResourceId, noUsesActionId, insufficientResourcesActionId, + attributeResetByEventId, eventActionId, advantageAttackId, advantageEffectId, disadvantageAttackId, disadvantageEffectId, +] = getRandomIds(100); + +const actionTestCreature = { + _id: creatureId, + props: [ + // Empty + { + _id: emptyActionId, + type: 'action', + summary: { text: 'Summary text 1 + 1 = {1 + 1}' } + }, + // Attack that targets self + { + _id: selfActionId, + type: 'action', + target: 'self', + }, + // Attack that hits + { + _id: attackActionId, + type: 'action', + attackRoll: { calculation: '10' }, + }, + // Attack that misses + { + _id: attackMissId, + type: 'action', + attackRoll: { calculation: '-5' }, + }, + // Attack that has Advantage + { + _id: advantageAttackId, + type: 'action', + attackRoll: { calculation: '0' }, + tags: ['hasAdvantage'], + }, + { + _id: advantageEffectId, + type: 'effect', + operation: 'advantage', + targetByTags: true, + targetTags: ['hasAdvantage'], + }, + // Attack that has Disadvantage + { + _id: disadvantageAttackId, + type: 'action', + attackRoll: { calculation: '0' }, + tags: ['hasDisadvantage'], + }, + { + _id: disadvantageEffectId, + type: 'effect', + operation: 'disadvantage', + targetByTags: true, + targetTags: ['hasDisadvantage'], + }, + // Attack that has no target + { + _id: attackNoTargetId, + type: 'action', + attackRoll: { calculation: '1' }, + }, + // Disable crits + { + type: 'attribute', + attributeType: 'stat', + variableName: '~criticalHitTarget', + baseValue: { calculation: '21' }, + }, + { + type: 'attribute', + attributeType: 'stat', + variableName: '~criticalMissTarget', + baseValue: { calculation: '0' }, + }, + // Has uses + { + _id: usesActionId, + type: 'action', + uses: { calculation: '3' }, + usesUsed: 1, + reset: 'longRest', + }, + // Not enough uses + { + _id: noUsesActionId, + type: 'action', + uses: { calculation: '5' }, + usesUsed: 5, + reset: 'longRest', + }, + // Uses Resources + { + _id: ammoId, + type: 'item', + quantity: 12, + tags: ['ammo'] + }, + { + _id: resourceAttId, + type: 'attribute', + name: 'Resource Name', + attributeType: 'stat', + baseValue: { calculation: '7' }, + variableName: 'resourceVar', + }, + { + _id: usesResourcesActionId, + type: 'action', + resources: { + itemsConsumed: [{ + _id: consumeAmmoId, + tag: 'ammo', + quantity: { calculation: '3' }, + itemId: ammoId, + }], + attributesConsumed: [{ + _id: consumeResourceId, + variableName: 'resourceVar', + quantity: { calculation: '2' }, + }] + } + }, + { + _id: insufficientResourcesActionId, + type: 'action', + resources: { + attributesConsumed: [{ + _id: consumeResourceId, + variableName: 'resourceVar', + quantity: { calculation: '9001' }, + }] + } + }, + // Events and resetting attributes + { + _id: attributeResetByEventId, + type: 'attribute', + name: 'Attribute Reset By testEvent Event', + attributeType: 'stat', + baseValue: { calculation: '27' }, + damage: 13, + variableName: 'resetByEventAtt', + reset: 'testEvent' + }, + { + _id: eventActionId, + type: 'action', + actionType: 'event', + variableName: 'testEvent', + }, + ], +} + +const actionTargetCreature = { + _id: targetCreatureId, + props: [ + { + type: 'attribute', + attributeType: 'stat', + variableName: 'armor', + baseValue: { calculation: '10' }, + } + ] +} + +const actionTargetCreature2 = { + _id: targetCreature2Id, + props: [ + { + type: 'attribute', + attributeType: 'stat', + variableName: 'armor', + baseValue: { calculation: '10' }, + } + ] +} + +describe('Apply Action Properties', function () { + // Increase timeout + this.timeout(8000); + + before(async function () { + await removeAllCreaturesAndProps(); + await createTestCreature(actionTestCreature); + await createTestCreature(actionTargetCreature); + await createTestCreature(actionTargetCreature2); + }); + + it('should generate random numbers reliably given consistent seeds', function () { + const aleaFraction = Alea('test', 'seeds'); + const randomNumbers = [aleaFraction(), aleaFraction(), aleaFraction()]; + assert.deepEqual(randomNumbers, [ + 0.19889510236680508, 0.9176857066340744, 0.042551583144813776 + ]); + }); + + it('should run empty actions', async function () { + const action = await runActionById(emptyActionId); + assert.exists(action); + assert.deepEqual(allMutations(action), [{ + contents: [{ + name: 'Action', + value: 'Summary text 1 + 1 = 2', + }], + targetIds: [], + }]); + }); + + it('should target self when set', async function () { + const action = await runActionById(selfActionId); + assert.exists(action); + assert.deepEqual(allMutations(action), [{ + contents: [{ + name: 'Action', + }], + targetIds: [creatureId], + }]); + }); + + it('should make attack rolls against no targets', async function () { + const action = await runActionById(attackNoTargetId, []); + const expectedMutations: Mutation[] = [ + { + contents: [{ name: 'Action' }], + targetIds: [], + }, { + contents: [{ + name: 'To Hit', + value: '1d20 [10] + 1\n**11**', + inline: true, + }], + targetIds: [], + } + ]; + assert.deepEqual(allMutations(action), expectedMutations); + }) + + it('should make attack rolls against multiple creatures', async function () { + const action = await runActionById(attackActionId, [ + targetCreatureId, + targetCreature2Id, + ]); + const expectedMutations: Mutation[] = [ + { + contents: [{ name: 'Action' }], + targetIds: [targetCreatureId, targetCreature2Id] + }, { + contents: [{ + inline: true, + name: 'Hit!', + value: '1d20 [10] + 10\n**20**', + }], + targetIds: [targetCreatureId], + }, { + contents: [{ + inline: true, + name: 'Hit!', + value: '1d20 [10] + 10\n**20**', + }], + targetIds: [targetCreature2Id], + }, + ]; + assert.deepEqual(allMutations(action), expectedMutations); + }); + + it('should make attack rolls that use uses', async function () { + const action = await runActionById(usesActionId, [targetCreatureId]); + const expectedUpdates: Update[] = [ + { + propId: usesActionId, + type: 'action', + inc: { usesUsed: 1, usesLeft: -1 }, + } + ] + assert.deepEqual(allUpdates(action), expectedUpdates); + }); + + it('should fail to make attacks that have no uses left', async function () { + const action = await runActionById(noUsesActionId, [targetCreatureId]); + const expectedContent: LogContent[] = [ + { + name: 'Action' + }, { + name: 'Error', + value: 'Action does not have enough uses left' + } + ] + assert.deepEqual(allLogContent(action), expectedContent); + }); + + it('should make attack rolls that miss', async function () { + const action = await runActionById(attackMissId, [targetCreatureId]); + const expectedMutations: Mutation[] = [ + { + contents: [{ name: 'Action' }], + targetIds: [targetCreatureId], + }, { + contents: [{ + inline: true, + name: 'Miss!', + value: '1d20 [10] − 5\n**5**', // DiceCloud uses unicode minus + }], + targetIds: [targetCreatureId], + } + ]; + assert.deepEqual(allMutations(action), expectedMutations); + }); + + it('should make attack rolls that roll with advantage', async function () { + const prop = await CreatureProperties.findOneAsync(advantageAttackId); + assert.equal(prop.attackRoll.advantage, 1, 'The attack roll should have advantage'); + const action = await runActionById(advantageAttackId, [targetCreatureId]); + const expectedMutations: Mutation[] = [ + { + contents: [{ name: 'Action' }], + targetIds: [targetCreatureId], + }, { + contents: [{ + inline: true, + name: 'Hit! (Advantage)', + value: '1d20 [ ~~10~~, 11 ] + 0\n**11**', + }], + targetIds: [targetCreatureId], + } + ]; + assert.deepEqual(allMutations(action), expectedMutations); + }); + + it('should make attack rolls that roll with disadvantage', async function () { + const prop = await CreatureProperties.findOneAsync(disadvantageAttackId); + assert.equal(prop.attackRoll.disadvantage, 1, 'The attack roll should have disadvantage'); + const action = await runActionById(disadvantageAttackId, [targetCreatureId]); + const expectedMutations: Mutation[] = [ + { + contents: [{ name: 'Action' }], + targetIds: [targetCreatureId], + }, { + contents: [{ + inline: true, + name: 'Hit! (Disadvantage)', + value: '1d20 [ 10, ~~11~~ ] + 0\n**10**', + }], + targetIds: [targetCreatureId], + } + ]; + assert.deepEqual(allMutations(action), expectedMutations); + }); + + it('actions should consume resources', async function () { + const action = await runActionById(usesResourcesActionId, []); + const expectedMutations: Mutation[] = [ + { + contents: [{ name: 'Action' }], + targetIds: [] + }, + { + contents: [{ + inline: true, + name: 'Attribute damaged', + value: '−2 Resource Name', + }], + targetIds: [creatureId], + updates: [{ + inc: { + damage: 2, + value: -2 + }, + propId: resourceAttId, + type: 'attribute' + }], + }, + { + targetIds: [], + updates: [ + { + inc: { + quantity: -3 + }, + propId: ammoId, + type: 'item', + } + ] + } + ]; + assert.deepEqual(allMutations(action), expectedMutations); + }); + + it('should handle insufficient resources', async function () { + const action = await runActionById(insufficientResourcesActionId, []); + const expectedMutations: Mutation[] = [ + { + contents: [{ + name: 'Action' + }, { + name: 'Error', + value: 'This creature doesn\'t have sufficient resources to perform this action', + }], + targetIds: [], + }, + ]; + assert.deepEqual(allMutations(action), expectedMutations); + }); + + it('should reset attributes when events happen', async function () { + const action = await runActionById(eventActionId, []); + const expectedMutations: Mutation[] = [ + { + contents: [{ + name: 'Action' + }], + targetIds: [], + }, + { + contents: [ + { + inline: true, + name: 'Attribute restored', + value: '+13 Attribute Reset By testEvent Event', + }, + ], + targetIds: [creatureId], + updates: [ + { + inc: { + damage: -13, + value: 13, + }, + propId: attributeResetByEventId, + type: 'attribute', + }, + ], + } + ]; + assert.deepEqual(allMutations(action), expectedMutations); + }); + +}); diff --git a/app/imports/api/engine/action/applyProperties/applyActionProperty.ts b/app/imports/api/engine/action/applyProperties/applyActionProperty.ts new file mode 100644 index 00000000..d38dbddb --- /dev/null +++ b/app/imports/api/engine/action/applyProperties/applyActionProperty.ts @@ -0,0 +1,251 @@ +import { EngineAction } from '/imports/api/engine/action/EngineActions'; +import { PropTask } from '../tasks/Task'; +import TaskResult, { LogContent } from '../tasks/TaskResult'; +import { getVariables } from '/imports/api/engine/loadCreatures'; +import getPropertyTitle from '/imports/api/utility/getPropertyTitle'; +import recalculateInlineCalculations from '/imports/api/engine/action/functions/recalculateInlineCalculations'; +import spendResources from '/imports/api/engine/action/functions/spendResources'; +import { applyAfterChildrenTriggers, applyAfterTriggers, applyChildren } from '/imports/api/engine/action/functions/applyTaskGroups'; +import recalculateCalculation from '/imports/api/engine/action/functions/recalculateCalculation'; +import { getEffectiveActionScope } from '/imports/api/engine/action/functions/getEffectiveActionScope'; +import numberToSignedString from '/imports/api/utility/numberToSignedString'; +import { getNumberFromScope } from '/imports/api/creature/creatures/CreatureVariables'; +import InputProvider from '/imports/api/engine/action/functions/userInput/InputProvider'; +import { CalculatedField } from '/imports/api/properties/subSchemas/computedField'; +import applyResetTask from '/imports/api/engine/action/tasks/applyResetTask'; + +export default async function applyActionProperty( + task: PropTask, action: EngineAction, result: TaskResult, userInput: InputProvider +): Promise { + const prop = task.prop; + const targetIds = prop.target === 'self' ? [action.creatureId] : task.targetIds; + + //Log the name and summary, check that the property has enough resources to fire + if (prop.summary?.text) { + await recalculateInlineCalculations(prop.summary, action, 'reduce', userInput); + } + result.appendLog({ + name: getPropertyTitle(prop), + ...prop.summary && { value: prop.summary.value }, + silenced: prop.silent, + }, targetIds); + + // Check Uses + if (prop.usesLeft <= 0) { + result.appendLog({ + name: 'Error', + value: `${getPropertyTitle(prop)} does not have enough uses left`, + silenced: prop.silent, + }, targetIds); + return; + } + + // Check Resources + if (prop.insufficientResources) { + result.appendLog({ + name: 'Error', + value: 'This creature doesn\'t have sufficient resources to perform this action', + silenced: prop.silent, + }, targetIds); + return; + } + + await spendResources(action, prop, targetIds, result, userInput); + + const attack: CalculatedField = prop.attackRoll || prop.attackRollBonus; + + // Attack if there is an attack roll + if (attack && attack.calculation) { + if (targetIds.length) { + for (const targetId of targetIds) { + await applyAttackToTarget(task, action, attack, targetId, result, userInput); + await applyAfterTriggers(action, prop, [targetId], userInput); + await applyChildren(action, prop, [targetId], userInput); + } + } else { + await applyAttackWithoutTarget(action, prop, attack, result, userInput); + await applyAfterTriggers(action, prop, targetIds, userInput); + await applyChildren(action, prop, targetIds, userInput); + } + } else { + await applyAfterTriggers(action, prop, targetIds, userInput); + await applyChildren(action, prop, targetIds, userInput); + } + if (prop.actionType === 'event' && prop.variableName) { + await applyResetTask({ + subtaskFn: 'reset', + eventName: prop.variableName, + targetIds: [action.creatureId], + }, action, result, userInput); + } + + // Finish + return await applyAfterChildrenTriggers(action, prop, targetIds, userInput); +} + +async function applyAttackToTarget( + task: PropTask, action: EngineAction, attack: CalculatedField, targetId: string, + taskResult: TaskResult, userInput: InputProvider +) { + taskResult.pushScope = { + '~attackHit': {}, + '~attackMiss': {}, + '~criticalHit': {}, + '~criticalMiss': {}, + '~attackRoll': {}, + } + + await recalculateCalculation(attack, action, 'reduce', userInput); + const scope = await getEffectiveActionScope(action); + const contents: LogContent[] = []; + + const { + resultPrefix, + result, + criticalHit, + criticalMiss, + advantage + } = await rollAttack(attack, scope, taskResult.pushScope, userInput); + + const targetScope = getVariables(targetId); + const targetArmor = getNumberFromScope('armor', targetScope) + + if (targetArmor !== undefined) { + let name = criticalHit ? 'Critical Hit!' : + criticalMiss ? 'Critical Miss!' : + result >= targetArmor ? 'Hit!' : 'Miss!'; + if (advantage === 1) { + name += ' (Advantage)'; + } else if (advantage === -1) { + name += ' (Disadvantage)'; + } + + contents.push({ + name, + value: `${resultPrefix}\n**${result}**`, + inline: true, + ...task.prop.silent && { silenced: true }, + }); + + if (criticalMiss || result < targetArmor) { + taskResult.pushScope['~attackMiss'] = { value: true }; + } else { + taskResult.pushScope['~attackHit'] = { value: true }; + } + } else { + contents.push({ + name: 'Error', + value: 'Target has no `armor`', + inline: true, + ...task.prop.silent && { silenced: true }, + }, { + name: criticalHit ? 'Critical Hit!' : criticalMiss ? 'Critical Miss!' : 'To Hit', + value: `${resultPrefix}\n**${result}**`, + inline: true, + ...task.prop.silent && { silenced: true }, + }); + } + if (contents.length) { + taskResult.mutations.push({ + contents, + targetIds: [targetId], + }); + } +} + +async function applyAttackWithoutTarget(action, prop, attack, taskResult: TaskResult, userInput: InputProvider) { + taskResult.pushScope = { + '~attackHit': {}, + '~attackMiss': {}, + '~criticalHit': {}, + '~criticalMiss': {}, + '~attackRoll': {}, + } + await recalculateCalculation(attack, action, 'reduce', userInput); + const scope = await getEffectiveActionScope(action); + const { + resultPrefix, + result, + criticalHit, + criticalMiss, + advantage, + } = await rollAttack(attack, scope, taskResult.pushScope, userInput); + let name = criticalHit ? 'Critical Hit!' : criticalMiss ? 'Critical Miss!' : 'To Hit'; + if (advantage === 1) { + name += ' (Advantage)'; + } else if (advantage === -1) { + name += ' (Disadvantage)'; + } + if (!criticalMiss) { + taskResult.pushScope['~attackHit'] = { value: true } + } + if (!criticalHit) { + taskResult.pushScope['~attackMiss'] = { value: true }; + } + taskResult.mutations.push({ + contents: [{ + name, + value: `${resultPrefix}\n**${result}**`, + inline: true, + ...prop.silent && { silenced: true }, + }], + targetIds: [], + }); +} + +async function rollAttack(attack, scope: any, resultPushScope, userInput: InputProvider) { + const advantage: 0 | 1 | -1 = await userInput.advantage( + (!!attack.advantage && !attack.disadvantage) ? 1 : + (!attack.advantage && !!attack.disadvantage) ? -1 : + 0 + ); + const rollModifierText = numberToSignedString(attack.value, true); + let value, resultPrefix; + + if (advantage === 1) { + const [[a, b]] = await userInput.rollDice([{ number: 2, diceSize: 20 }]); + if (a >= b) { + value = a; + resultPrefix = `1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText}`; + } else { + value = b; + resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`; + } + } else if (advantage === -1) { + const [[a, b]] = await userInput.rollDice([{ number: 2, diceSize: 20 }]); + if (a <= b) { + value = a; + resultPrefix = `1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText}`; + } else { + value = b; + resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`; + } + } else { + [[value]] = await userInput.rollDice([{ number: 1, diceSize: 20 }]); + resultPrefix = `1d20 [${value}] ${rollModifierText}` + } + resultPushScope['~attackDiceRoll'] = { value }; + const result = value + attack.value; + resultPushScope['~attackRoll'] = { value: result }; + const { criticalHit, criticalMiss } = applyCrits(value, scope, resultPushScope); + return { resultPrefix, result, value, criticalHit, criticalMiss, advantage }; +} + +function applyCrits(value, scope, resultPushScope) { + const scopeCritTarget = getNumberFromScope('~criticalHitTarget', scope); + const criticalHitTarget = scopeCritTarget !== undefined && + Number.isFinite(scopeCritTarget) ? scopeCritTarget : 20; + + const scopeCritMissTarget = getNumberFromScope('~criticalMissTarget', scope); + const criticalMissTarget = scopeCritMissTarget !== undefined && + Number.isFinite(scopeCritMissTarget) ? scopeCritMissTarget : 1; + + const criticalHit = value >= criticalHitTarget; + const criticalMiss = value <= criticalMissTarget; + if (criticalHit) { + resultPushScope['~criticalHit'] = { value: true }; + } else if (criticalMiss) { + resultPushScope['~criticalMiss'] = { value: true }; + } + return { criticalHit, criticalMiss }; +} diff --git a/app/imports/api/engine/action/applyProperties/applyAdjustmentProperty.test.ts b/app/imports/api/engine/action/applyProperties/applyAdjustmentProperty.test.ts new file mode 100644 index 00000000..d4063fe2 --- /dev/null +++ b/app/imports/api/engine/action/applyProperties/applyAdjustmentProperty.test.ts @@ -0,0 +1,164 @@ +import { assert } from 'chai'; +import { + allMutations, + createTestCreature, + getRandomIds, + removeAllCreaturesAndProps, + runActionById +} from '/imports/api/engine/action/functions/actionEngineTest.testFn'; + +const [ + creatureId, targetCreatureId, targetCreature2Id, adjustmentToTargetId, adjustmentToSelfId, targetCreatureStrengthId, targetCreature2StrengthId, selfDexterityId +] = getRandomIds(100); + +const actionTestCreature = { + _id: creatureId, + props: [ + { + _id: adjustmentToTargetId, + type: 'adjustment', + target: 'target', + stat: 'strength', + operation: 'increment', + amount: { calculation: '2' } + }, + { + _id: adjustmentToSelfId, + type: 'adjustment', + target: 'self', + stat: 'dexterity', + operation: 'set', + amount: { calculation: '11' } + }, + { + _id: selfDexterityId, + type: 'attribute', + name: 'Dexterity', + attributeType: 'ability', + variableName: 'dexterity', + baseValue: { calculation: '13' }, + }, + ], +} + +const actionTargetCreature = { + _id: targetCreatureId, + props: [ + { + _id: targetCreatureStrengthId, + type: 'attribute', + attributeType: 'ability', + variableName: 'strength', + baseValue: { calculation: '12' }, + } + ] +} + +const actionTargetCreature2 = { + _id: targetCreature2Id, + props: [ + { + _id: targetCreature2StrengthId, + type: 'attribute', + attributeType: 'ability', + variableName: 'strength', + baseValue: { calculation: '18' }, + } + ] +} + +describe('Apply Adjustment Properties', function () { + // Increase timeout + this.timeout(8000); + + before(async function () { + await removeAllCreaturesAndProps(); + await createTestCreature(actionTestCreature); + await createTestCreature(actionTargetCreature); + await createTestCreature(actionTargetCreature2); + }); + + it('Adjusts the attributes of self', async function () { + const action = await runActionById(adjustmentToSelfId); + assert.exists(action); + assert.deepEqual(allMutations(action), [{ + contents: [ + { + inline: true, + name: 'Attribute damage', + value: 'Dexterity set from 13 to 11', + } + ], + targetIds: [creatureId], + updates: [ + { + propId: selfDexterityId, + type: 'attribute', + set: { damage: 2, value: 11 }, + }, + ], + }]); + }); + + it('Adjusts the attributes of a single target', async function () { + const action = await runActionById(adjustmentToTargetId, [targetCreatureId]); + assert.exists(action); + assert.deepEqual(allMutations(action), [{ + contents: [ + { + inline: true, + name: 'Attribute damaged', + value: '−2 Attribute', + } + ], + targetIds: [targetCreatureId], + updates: [ + { + propId: targetCreatureStrengthId, + type: 'attribute', + inc: { damage: 2, value: -2 }, + }, + ], + }]); + }); + + it('Adjusts the attributes of multiple targets', async function () { + const action = await runActionById(adjustmentToTargetId, [ + targetCreatureId, targetCreature2Id + ]); + assert.exists(action); + assert.deepEqual(allMutations(action), [{ + contents: [ + { + inline: true, + name: 'Attribute damaged', + value: '−2 Attribute', + } + ], + targetIds: [targetCreatureId], + updates: [ + { + propId: targetCreatureStrengthId, + type: 'attribute', + inc: { damage: 2, value: -2 }, + }, + ], + }, { + contents: [ + { + inline: true, + name: 'Attribute damaged', + value: '−2 Attribute', + } + ], + targetIds: [targetCreature2Id], + updates: [ + { + propId: targetCreature2StrengthId, + type: 'attribute', + inc: { damage: 2, value: -2 }, + }, + ], + }]); + }); +}); diff --git a/app/imports/api/engine/action/applyProperties/applyAdjustmentProperty.ts b/app/imports/api/engine/action/applyProperties/applyAdjustmentProperty.ts new file mode 100644 index 00000000..aee14885 --- /dev/null +++ b/app/imports/api/engine/action/applyProperties/applyAdjustmentProperty.ts @@ -0,0 +1,62 @@ +import { EngineAction } from '/imports/api/engine/action/EngineActions'; +import InputProvider from '/imports/api/engine/action/functions/userInput/InputProvider'; +import { applyDefaultAfterPropTasks, applyTaskToEachTarget } from '/imports/api/engine/action/functions/applyTaskGroups'; +import recalculateCalculation from '/imports/api/engine/action/functions/recalculateCalculation'; +import { PropTask } from '/imports/api/engine/action/tasks/Task'; +import TaskResult from '/imports/api/engine/action/tasks/TaskResult'; +import applyTask from '/imports/api/engine/action/tasks/applyTask'; +import { getSingleProperty, getVariables } from '/imports/api/engine/loadCreatures'; +import getPropertyTitle from '/imports/api/utility/getPropertyTitle'; + +export default async function applyAdjustmentProperty( + task: PropTask, action: EngineAction, result: TaskResult, userInput: InputProvider +): Promise { + const prop = task.prop; + const damageTargetIds = prop.target === 'self' ? [action.creatureId] : task.targetIds; + + if (damageTargetIds.length > 1) { + return applyTaskToEachTarget(action, task, damageTargetIds, userInput); + } + + // Get the operation and value and push the damage hooks to the queue + if (!prop.amount) { + return; + } + + // Evaluate the amount + await recalculateCalculation(prop.amount, action, 'reduce', userInput); + const value = +prop.amount.value; + if (!isFinite(value)) { + return; + } + + if (!damageTargetIds?.length) { + return; + } + + if (damageTargetIds.length !== 1) { + throw 'At this step, only a single target is supported' + } + const targetId = damageTargetIds[0]; + const statId = getVariables(targetId)?.[prop.stat]?._propId; + const stat = statId && getSingleProperty(targetId, statId); + if (!stat?.type) { + result.appendLog({ + name: 'Error', + value: `Could not apply attribute damage, creature does not have \`${prop.stat}\` set`, + silenced: prop.silent, + }, damageTargetIds); + return; + } + await applyTask(action, { + targetIds: damageTargetIds, + subtaskFn: 'damageProp', + params: { + title: getPropertyTitle(prop), + operation: prop.operation, + value, + targetProp: stat, + }, + }, userInput); + return applyDefaultAfterPropTasks(action, prop, damageTargetIds, userInput); +} diff --git a/app/imports/api/engine/action/applyProperties/applyBranchProperty.test.ts b/app/imports/api/engine/action/applyProperties/applyBranchProperty.test.ts new file mode 100644 index 00000000..db59fc1e --- /dev/null +++ b/app/imports/api/engine/action/applyProperties/applyBranchProperty.test.ts @@ -0,0 +1,376 @@ +import { assert } from 'chai'; +import { + allMutations, + createTestCreature, + getRandomIds, + removeAllCreaturesAndProps, + runActionById +} from '/imports/api/engine/action/functions/actionEngineTest.testFn'; + +const [ + creatureId, targetCreatureId, ifTrueBranchId, ifFalseBranchId, indexBranchId, attackHitId, attackMissId, saveSucceedId, saveFailId, randomBranchId, targetCreature2Id, eachTargetBranchId, choiceBranchId, +] = getRandomIds(100); + +const actionTestCreature = { + _id: creatureId, + props: [ + // If branch + { + _id: ifTrueBranchId, + type: 'branch', + branchType: 'if', + condition: { calculation: 'true' }, + children: [ + { + type: 'note', + summary: { text: 'this should run' }, + }, + ], + }, + { + _id: ifFalseBranchId, + type: 'branch', + branchType: 'if', + condition: { calculation: 'false' }, + children: [ + { + type: 'note', + summary: { text: 'this should not run' }, + }, + ], + }, + // index branch + { + _id: indexBranchId, + type: 'branch', + branchType: 'index', + condition: { calculation: '1 + 1' }, + children: [ + { + type: 'note', + summary: { text: 'FAIL: index child 1 should not run' }, + }, + { + type: 'note', + summary: { text: 'Child 2 should run' }, + }, + { + type: 'note', + summary: { text: 'FAIL: index child 3 should not run' }, + }, + ], + }, + // Hit and miss branches + { + _id: attackHitId, + type: 'action', + attackRoll: { calculation: '1' }, + children: [ + { + type: 'branch', + branchType: 'hit', + children: [{ + type: 'note', + summary: { text: 'attack hit branch' } + }], + }, + { + type: 'branch', + branchType: 'miss', + children: [{ + type: 'note', + summary: { text: 'attack miss branch' } + }], + }, + ] + }, + { + _id: attackMissId, + type: 'action', + attackRoll: { calculation: '-1' }, + children: [ + { + type: 'branch', + branchType: 'hit', + children: [{ + type: 'note', + summary: { text: 'attack hit branch' } + }], + }, + { + type: 'branch', + branchType: 'miss', + children: [{ + type: 'note', + summary: { text: 'attack miss branch' } + }], + }, + ] + }, + + // Save and fail save branch + { + _id: saveSucceedId, + type: 'savingThrow', + dc: { calculation: '10' }, + target: 'target', + stat: 'strengthSave', + children: [ + { + type: 'branch', + branchType: 'successfulSave', + children: [{ + type: 'note', + summary: { text: 'made save branch' } + }], + }, + { + type: 'branch', + branchType: 'failedSave', + children: [{ + type: 'note', + summary: { text: 'failed save branch' } + }], + }, + ] + }, + { + _id: saveFailId, + type: 'savingThrow', + dc: { calculation: '15' }, + target: 'target', + stat: 'strengthSave', + children: [ + { + type: 'branch', + branchType: 'successfulSave', + children: [{ + type: 'note', + summary: { text: 'made save branch' } + }], + }, + { + type: 'branch', + branchType: 'failedSave', + children: [{ + type: 'note', + summary: { text: 'failed save branch' } + }], + }, + ] + }, + + // Random branch + { + _id: randomBranchId, + type: 'branch', + branchType: 'random', + children: [ + { + type: 'note', + summary: { text: 'FAIL: random child 1 should not run' }, + }, + { + type: 'note', + summary: { text: 'Random child 2 should run' }, + }, + { + type: 'note', + summary: { text: 'FAIL: random child 3 should not run' }, + }, + ], + }, + + // Each target branch + { + _id: eachTargetBranchId, + type: 'branch', + branchType: 'eachTarget', + children: [ + { + type: 'note', + summary: { text: 'some note' } + } + ] + }, + + // Choice branch + { + _id: choiceBranchId, + type: 'branch', + branchType: 'choice', + children: [ + { + type: 'note', + summary: { text: 'Choice child 1 should run' }, + }, + { + type: 'note', + summary: { text: 'Fail: choice child 2 should not run' }, + }, + { + type: 'note', + summary: { text: 'Fail: choice child 3 should not run' }, + }, + ], + }, + ], +} + +const actionTargetCreature = { + _id: targetCreatureId, + props: [ + { + type: 'attribute', + attributeType: 'stat', + variableName: 'armor', + baseValue: { calculation: '10' }, + }, + { + type: 'skill', + skillType: 'save', + variableName: 'strengthSave', + baseValue: { calculation: '3' }, + }, + ] +} + +const actionTargetCreature2 = { + _id: targetCreature2Id, + props: [ + { + type: 'attribute', + attributeType: 'stat', + variableName: 'armor', + baseValue: { calculation: '15' }, + }, + ] +} + +describe('Apply Branch Properties', function () { + // Increase timeout + this.timeout(8000); + + before(async function () { + await removeAllCreaturesAndProps(); + await createTestCreature(actionTestCreature); + await createTestCreature(actionTargetCreature); + await createTestCreature(actionTargetCreature2); + }); + + // If branch + it('Runs an if branch with a true condition', async function () { + const action = await runActionById(ifTrueBranchId); + assert.deepEqual(allMutations(action), [{ + contents: [{ value: 'this should run' }], + targetIds: [], + }]); + }); + it('runs an if branch with a false condition', async function () { + const action = await runActionById(ifFalseBranchId); + assert.deepEqual(allMutations(action), []); + }); + it('runs an if branch and chooses the correct child', async function () { + const action = await runActionById(indexBranchId); + assert.deepEqual(allMutations(action), [{ + contents: [{ value: 'Child 2 should run' }], + targetIds: [], + }]); + }); + + // Hit and miss branch + it('Runs only hit branches on an attack that hits', async function () { + const action = await runActionById(attackHitId, [targetCreatureId]); + assert.deepEqual(allMutations(action), [{ + contents: [{ name: 'Action' }], + targetIds: [targetCreatureId], + }, { + contents: [{ inline: true, name: 'Hit!', value: '1d20 [10] + 1\n**11**' }], + targetIds: [targetCreatureId], + }, { + contents: [{ value: 'attack hit branch' }], + targetIds: [targetCreatureId], + }]); + }); + it('Runs only miss branches on an attack that misses', async function () { + const action = await runActionById(attackMissId, [targetCreatureId]); + assert.deepEqual(allMutations(action), [{ + contents: [{ name: 'Action' }], + targetIds: [targetCreatureId], + }, { + contents: [{ inline: true, name: 'Miss!', value: '1d20 [10] − 1\n**9**' }], + targetIds: [targetCreatureId], + }, { + contents: [{ value: 'attack miss branch' }], + targetIds: [targetCreatureId], + }]); + }); + + // Save succeed and fail branches + it('Runs only miss branches on an attack that misses', async function () { + const action = await runActionById(saveSucceedId, [targetCreatureId]); + assert.deepEqual(allMutations(action), [{ + contents: [{ + name: 'Saving throw', + value: 'DC **10**', + inline: true + }, { + name: 'Successful save', + value: '1d20 [ 10 ] + 3\n**13**', + inline: true + }], + targetIds: [targetCreatureId], + }, { + contents: [{ value: 'made save branch' }], + targetIds: [targetCreatureId], + }]); + }); + it('Runs only miss branches on an attack that misses', async function () { + const action = await runActionById(saveFailId, [targetCreatureId]); + assert.deepEqual(allMutations(action), [{ + contents: [{ + name: 'Saving throw', + value: 'DC **15**', + inline: true + }, { + name: 'Failed save', + value: '1d20 [ 10 ] + 3\n**13**', + inline: true + }], + targetIds: [targetCreatureId], + }, { + contents: [{ value: 'failed save branch' }], + targetIds: [targetCreatureId], + }]); + }); + + // Random branches, RNG is fixed at average for testing, so child 2 should run + it('runs a random branch and chooses the correct child', async function () { + const action = await runActionById(randomBranchId); + assert.deepEqual(allMutations(action), [{ + contents: [{ value: 'Random child 2 should run' }], + targetIds: [], + }]); + }); + + // Branches can split actions across targets + it('Can split actions to targets using a branch', async function () { + const action = await runActionById(eachTargetBranchId, [targetCreatureId, targetCreature2Id]); + assert.deepEqual(allMutations(action), [{ + contents: [{ value: 'some note' }], + targetIds: [targetCreatureId], + }, { + contents: [{ value: 'some note' }], + targetIds: [targetCreature2Id], + }]); + }); + + // Choice branches, choices are fixed to first option for testing + it('runs a choice branch and chooses the correct child', async function () { + const action = await runActionById(choiceBranchId); + assert.deepEqual(allMutations(action), [{ + contents: [{ value: 'Choice child 1 should run' }], + targetIds: [], + }]); + }); +}); diff --git a/app/imports/api/engine/action/applyProperties/applyBranchProperty.ts b/app/imports/api/engine/action/applyProperties/applyBranchProperty.ts new file mode 100644 index 00000000..86e7ba68 --- /dev/null +++ b/app/imports/api/engine/action/applyProperties/applyBranchProperty.ts @@ -0,0 +1,131 @@ +import { filter } from 'lodash'; +import { EngineAction } from '/imports/api/engine/action/EngineActions'; +import InputProvider from '/imports/api/engine/action/functions/userInput/InputProvider'; +import { applyAfterPropTasksForSingleChild, applyAfterPropTasksForSomeChildren, applyAfterTasksSkipChildren, applyDefaultAfterPropTasks, applyTaskToEachTarget } from '/imports/api/engine/action/functions/applyTaskGroups'; +import { getEffectiveActionScope } from '/imports/api/engine/action/functions/getEffectiveActionScope'; +import recalculateCalculation from '/imports/api/engine/action/functions/recalculateCalculation'; +import { PropTask } from '/imports/api/engine/action/tasks/Task'; +import TaskResult from '/imports/api/engine/action/tasks/TaskResult'; +import { getPropertyChildren } from '/imports/api/engine/loadCreatures'; + +export default async function applyBranchProperty( + task: PropTask, action: EngineAction, result: TaskResult, userInput: InputProvider +): Promise { + const prop = task.prop; + const targets = task.targetIds; + + switch (prop.branchType) { + case 'if': { + await recalculateCalculation(prop.condition, action, 'reduce', userInput); + if (prop.condition?.value) { + return applyDefaultAfterPropTasks(action, prop, targets, userInput); + } else { + return applyAfterTasksSkipChildren(action, prop, targets, userInput); + } + } + case 'index': { + const children = await getPropertyChildren(action.creatureId, prop); + if (!children.length) { + return applyAfterTasksSkipChildren(action, prop, targets, userInput); + } + await recalculateCalculation(prop.condition, action, 'reduce', userInput); + if (!isFinite(prop.condition?.value)) { + result.appendLog({ + name: 'Branch Error', + value: `Index did not resolve into a valid number, got \`${prop.condition?.value}\` instead`, + silenced: prop.silent, + }, targets); + return applyAfterTasksSkipChildren(action, prop, targets, userInput); + } + let index = Math.floor(prop.condition?.value); + if (index < 1) index = 1; + if (index > children.length) index = children.length; + const child = children[index - 1]; + return applyAfterPropTasksForSingleChild(action, prop, child, targets, userInput); + } + case 'hit': { + const scope = await getEffectiveActionScope(action); + if (scope['~attackHit']?.value) { + if (!targets.length && !prop.silent) { + result.appendLog({ + value: '**On hit**', + silenced: prop.silent, + }, targets); + } + return applyDefaultAfterPropTasks(action, prop, targets, userInput); + } else { + return applyAfterTasksSkipChildren(action, prop, targets, userInput); + } + } + case 'miss': { + const scope = await getEffectiveActionScope(action); + if (scope['~attackMiss']?.value) { + if (!targets.length && !prop.silent) { + result.appendLog({ + value: '**On miss**', + silenced: prop.silent, + }, targets); + } + return applyDefaultAfterPropTasks(action, prop, targets, userInput); + } else { + return applyAfterTasksSkipChildren(action, prop, targets, userInput); + } + } + case 'failedSave': { + const scope = await getEffectiveActionScope(action); + if (scope['~saveFailed']?.value) { + if (!targets.length && !prop.silent) { + result.appendLog({ + value: '**On failed save**', + silenced: prop.silent, + }, targets); + } + return applyDefaultAfterPropTasks(action, prop, targets, userInput); + } else { + return applyAfterTasksSkipChildren(action, prop, targets, userInput); + } + } + case 'successfulSave': { + const scope = await getEffectiveActionScope(action); + if (scope['~saveSucceeded']?.value) { + if (!targets.length && !prop.silent) { + result.appendLog({ + value: '**On save**', + silenced: prop.silent, + }, targets); + } + return applyDefaultAfterPropTasks(action, prop, targets, userInput); + } else { + return applyAfterTasksSkipChildren(action, prop, targets, userInput); + } + } + case 'random': { + const children = await getPropertyChildren(action.creatureId, prop); + if (children.length) { + const index = (await userInput.rollDice([{ number: 1, diceSize: children.length }]))[0][0]; + const child = children[index - 1]; + return applyAfterPropTasksForSingleChild(action, prop, child, targets, userInput); + } else { + return applyAfterTasksSkipChildren(action, prop, targets, userInput); + } + } + case 'eachTarget': + if (targets.length > 1) { + return applyTaskToEachTarget(action, task, targets, userInput); + } + return applyDefaultAfterPropTasks(action, prop, targets, userInput); + case 'choice': { + const children = await getPropertyChildren(action.creatureId, prop); + let choices: string[]; + let chosenChildren: typeof children = []; + if (children.length) { + choices = await userInput.choose(children); + chosenChildren = filter(children, child => choices.includes(child._id)); + } + if (!children.length || !chosenChildren.length) { + return applyAfterTasksSkipChildren(action, prop, targets, userInput); + } + return applyAfterPropTasksForSomeChildren(action, prop, chosenChildren, targets, userInput); + } + } +} diff --git a/app/imports/api/engine/action/applyProperties/applyBuffProperty.test.ts b/app/imports/api/engine/action/applyProperties/applyBuffProperty.test.ts new file mode 100644 index 00000000..f1109e2e --- /dev/null +++ b/app/imports/api/engine/action/applyProperties/applyBuffProperty.test.ts @@ -0,0 +1,115 @@ +import { assert } from 'chai'; +import { + allMutations, + createTestCreature, + getRandomIds, + removeAllCreaturesAndProps, + runActionById +} from '/imports/api/engine/action/functions/actionEngineTest.testFn'; + +const [ + creatureId, targetCreatureId, buffId +] = getRandomIds(100); + +const actionTestCreature = { + _id: creatureId, + props: [ + { + _id: buffId, + type: 'buff', + description: { text: 'This buff reduces AC of target by difference between the strength of caster {strength} and the target {~target.strength}' }, + children: [ + { + type: 'effect', + stats: ['armor'], + operation: 'add', + amount: { calculation: '~target.strength - strength' }, + }, + ], + }, + { + type: 'attribute', + attributeType: 'stat', + variableName: 'strength', + baseValue: { calculation: '18' }, + }, + ], +}; + +const actionTargetCreature = { + _id: targetCreatureId, + props: [ + { + type: 'attribute', + attributeType: 'stat', + variableName: 'armor', + baseValue: { calculation: '10' }, + }, + { + type: 'attribute', + attributeType: 'ability', + variableName: 'strength', + baseValue: { calculation: '12' }, + }, + ], +}; + +describe('Apply Buff Properties', function () { + // Increase timeout + this.timeout(8000); + + before(async function () { + await removeAllCreaturesAndProps(); + await createTestCreature(actionTestCreature); + await createTestCreature(actionTargetCreature); + }); + + it('Applies a buff and freezes some variables', async function () { + const action = await runActionById(buffId, [targetCreatureId]); + const mutations = allMutations(action); + // Get random Ids of inserted props + const insertedBuffId = mutations?.[1]?.inserts?.[0]?._id; + const insertedEffectId = mutations?.[1]?.inserts?.[1]?._id; + assert.deepEqual(mutations, [{ + contents: [{ + name: 'Buff', + // TODO Make target strength available in action scope to fix: 'target 0' -> 'target 12' + value: 'This buff reduces AC of target by difference between the strength of caster 18 and the target 0', + }], + targetIds: [targetCreatureId], + }, { + contents: [], + inserts: [{ + _id: insertedBuffId, + type: 'buff', + description: { + text: 'This buff reduces AC of target by difference between the strength of caster {18} and the target {strength}' + }, + left: 1, + right: 4, + parentId: null, + root: { + collection: 'creatures', + id: targetCreatureId, + }, + tags: [], + target: 'target', + }, { + _id: insertedEffectId, + type: 'effect', + stats: ['armor'], + operation: 'add', + amount: { calculation: 'strength - 18' }, + left: 2, + right: 3, + parentId: insertedBuffId, + root: { + collection: 'creatures', + id: targetCreatureId, + }, + tags: [], + }], + targetIds: [targetCreatureId], + }]); + }); +}); diff --git a/app/imports/api/engine/action/applyProperties/applyBuffProperty.ts b/app/imports/api/engine/action/applyProperties/applyBuffProperty.ts new file mode 100644 index 00000000..241912f6 --- /dev/null +++ b/app/imports/api/engine/action/applyProperties/applyBuffProperty.ts @@ -0,0 +1,165 @@ +import { get } from 'lodash'; + +import { EngineAction } from '/imports/api/engine/action/EngineActions'; +import { PropTask } from '/imports/api/engine/action/tasks/Task'; +import { getPropertyDescendants } from '/imports/api/engine/loadCreatures'; +import resolve from '/imports/parser/resolve'; +import map from '/imports/parser/map'; +import toString from '/imports/parser/toString'; +import computedSchemas from '/imports/api/properties/computedOnlyPropertySchemasIndex.js'; +import applyFnToKey, { applyFnToKeyAsync } from '/imports/api/engine/computation/utility/applyFnToKey'; +import accessor from '/imports/parser/parseTree/accessor'; +import TaskResult, { Mutation } from '/imports/api/engine/action/tasks/TaskResult'; +import { getEffectiveActionScope } from '/imports/api/engine/action/functions/getEffectiveActionScope'; +import cyrb53 from '/imports/api/engine/computation/utility/cyrb53'; +import { renewDocIds } from '/imports/api/parenting/parentingFunctions'; +import { cleanProps } from '/imports/api/creature/creatureProperties/methods/copyPropertyToLibrary'; +import recalculateInlineCalculations from '/imports/api/engine/action/functions/recalculateInlineCalculations'; +import getPropertyTitle from '/imports/api/utility/getPropertyTitle'; +import INLINE_CALCULATION_REGEX from '/imports/constants/INLINE_CALCULATION_REGEX'; +import { applyAfterTasksSkipChildren } from '/imports/api/engine/action/functions/applyTaskGroups'; +import InputProvider from '/imports/api/engine/action/functions/userInput/InputProvider'; + +export default async function applyBuffProperty( + task: PropTask, action: EngineAction, result: TaskResult, userInput: InputProvider +) { + const prop = EJSON.clone(task.prop); + const targetIds = prop.target === 'self' ? [action.creatureId] : task.targetIds; + + // Get the buff and its descendants + const propList = [ + EJSON.clone(prop), + ...getPropertyDescendants(action.creatureId, prop._id), + ]; + + // Crystalize the variables + if (!prop.skipCrystalization) { + await crystalizeVariables(action, propList, task, result); + } + + targetIds.forEach(target => { + // Create a per-target mutation + const mutation: Mutation = { targetIds: [target], contents: [] }; + + // Create a per-target copy of the propList + let targetPropList = EJSON.clone(propList); + + // Give the properties new IDs as descendants of the target + renewDocIds({ + docArray: targetPropList, + idMap: { + [prop.parentId]: null, + [prop.root.id]: target, + }, + collectionMap: { [prop.root.collection]: 'creatures' } + }); + + //Log the buff + let logValue = prop.description?.value + if (prop.description?.text) { + recalculateInlineCalculations(prop.description, action, 'reduce', userInput); + logValue = prop.description?.value; + } + result.appendLog({ + name: getPropertyTitle(prop), + value: logValue, + silenced: prop.silent, + }, [target]); + + // remove all the computed fields + targetPropList = cleanProps(targetPropList); + + // Insert the props in the mutation + mutation.inserts = targetPropList; + + // Add the mutation to the results + result.mutations.push(mutation); + }); + applyAfterTasksSkipChildren(action, prop, targetIds, userInput); +} + +/** + * Replaces all variables with their resolved values + * except variables of the form `~target.thing.total` become `thing.total` + */ +async function crystalizeVariables( + action: EngineAction, propList: any[], task: PropTask, result: TaskResult +) { + const scope = await getEffectiveActionScope(action); + for (const prop of propList) { + if (prop._skipCrystalize) { + delete prop._skipCrystalize; + return; + } + // Iterate through all the calculations and crystalize them + for (const calcKey of computedSchemas[prop.type].computedFields()) { + await applyFnToKeyAsync(prop, calcKey, async (prop, key) => { + const calcObj = get(prop, key); + if (!calcObj?.parseNode) return; + calcObj.parseNode = await map(calcObj.parseNode, async node => { + // Skip nodes that aren't symbols or accessors + if ( + node.parseType !== 'accessor' + ) return node; + // Handle variables + if (node.parseType === 'accessor' && node.name === '~target') { + // strip ~target + if (node.path?.length > 0) { + const name = node.path.shift(); + return accessor.create({ + name, + path: node.path?.length ? node.path : undefined, + }); + } else { + // Can't strip if there isn't anything in the path after ~target + result.appendLog({ + name: 'Error', + value: 'Variable `~target` should not be used without a property: ~target.property', + silenced: prop.silent, + }, task.targetIds); + } + return node; + } else { + // Resolve all other variables + const { result: nodeResult, context } = await resolve('reduce', node, scope); + result.appendParserContextErrors(context, task.targetIds); + return nodeResult; + } + }); + calcObj.calculation = toString(calcObj.parseNode); + calcObj.hash = cyrb53(calcObj.calculation); + }); + } + // For each key in the schema + for (const calcKey of computedSchemas[prop.type].inlineCalculationFields()) { + // That ends in .inlineCalculations + applyFnToKey(prop, calcKey, (prop, key) => { + const inlineCalcObj = get(prop, key); + if (!inlineCalcObj) return; + + // If there is no text, skip + if (!inlineCalcObj.text) { + return; + } + + // Replace all the existing calculations + let index = -1; + inlineCalcObj.text = inlineCalcObj.text.replace(INLINE_CALCULATION_REGEX, () => { + index += 1; + return `{${inlineCalcObj.inlineCalculations[index].calculation}}`; + }); + + // Set the value to the uncomputed string + inlineCalcObj.value = inlineCalcObj.text; + + // Write a new hash + const inlineCalcHash = cyrb53(inlineCalcObj.text); + if (inlineCalcHash === inlineCalcObj.hash) { + // Skip if nothing changed + return; + } + inlineCalcObj.hash = inlineCalcHash; + }); + } + } +} diff --git a/app/imports/api/engine/action/applyProperties/applyBuffRemoverProperty.test.ts b/app/imports/api/engine/action/applyProperties/applyBuffRemoverProperty.test.ts new file mode 100644 index 00000000..49d32e75 --- /dev/null +++ b/app/imports/api/engine/action/applyProperties/applyBuffRemoverProperty.test.ts @@ -0,0 +1,97 @@ +import { assert } from 'chai'; +import { + allMutations, + createTestCreature, + getRandomIds, + removeAllCreaturesAndProps, + runActionById +} from '/imports/api/engine/action/functions/actionEngineTest.testFn'; + +const [ + creatureId, otherCreatureId, buffId, removeParentBuffId, removeTargetBuffsId, +] = getRandomIds(100); + +const actionTestCreature = { + _id: creatureId, + props: [ + { + _id: buffId, + type: 'buff', + description: { text: 'This buff reduces AC of target by difference between the strength of caster {strength} and the target {~target.strength}' }, + tags: ['some buff'], + children: [ + { + type: 'effect', + stats: ['armor'], + operation: 'add', + amount: { calculation: '~target.strength - strength' }, + }, + { + _id: removeParentBuffId, + type: 'buffRemover', + targetParentBuff: true, + target: 'self', + }, + ], + }, + { + type: 'attribute', + attributeType: 'stat', + variableName: 'strength', + baseValue: { calculation: '18' }, + }, + ], +}; + +const actionOtherCreature = { + _id: otherCreatureId, + props: [ + { + _id: removeTargetBuffsId, + type: 'buffRemover', + target: 'target', + targetTags: ['some buff'] + }, + ], +}; + +describe('Apply Buff Remover Properties', function () { + // Increase timeout + this.timeout(8000); + + beforeEach(async function () { + await removeAllCreaturesAndProps(); + await createTestCreature(actionTestCreature); + await createTestCreature(actionOtherCreature); + }); + + it('removes a parent buff', async function () { + const action = await runActionById(removeParentBuffId); + const mutations = allMutations(action); + assert.deepEqual(mutations, [{ + contents: [{ + name: 'Removed', + value: 'Buff', + }], + removals: [{ + propId: buffId, + }], + targetIds: [] + }]); + }); + + it('removes a tag targeted buff', async function () { + const action = await runActionById(removeTargetBuffsId, [creatureId]); + const mutations = allMutations(action); + assert.deepEqual(mutations, [{ + contents: [{ + name: 'Removed', + value: 'Buff', + }], + removals: [{ + propId: buffId, + }], + targetIds: [creatureId] + }]); + }); +}); diff --git a/app/imports/api/engine/action/applyProperties/applyBuffRemoverProperty.ts b/app/imports/api/engine/action/applyProperties/applyBuffRemoverProperty.ts new file mode 100644 index 00000000..791f0b7a --- /dev/null +++ b/app/imports/api/engine/action/applyProperties/applyBuffRemoverProperty.ts @@ -0,0 +1,117 @@ +import { PropTask } from '/imports/api/engine/action/tasks/Task'; +import TaskResult from 'imports/api/engine/action/tasks/TaskResult'; +import getPropertyTitle from '/imports/api/utility/getPropertyTitle'; +import { findLast, filter, difference, intersection } from 'lodash'; +import { getPropertiesOfType, getPropertyAncestors } from '/imports/api/engine/loadCreatures'; +import getEffectivePropTags from '/imports/api/engine/computation/utility/getEffectivePropTags'; +import { applyDefaultAfterPropTasks, applyTaskToEachTarget } from '/imports/api/engine/action/functions/applyTaskGroups'; +import { EngineAction } from '/imports/api/engine/action/EngineActions'; +import InputProvider from '/imports/api/engine/action/functions/userInput/InputProvider'; + +export default async function applyBuffRemoverProperty( + task: PropTask, action: EngineAction, result: TaskResult, userInput: InputProvider +) { + const prop = task.prop; + + const targetIds = prop.target === 'self' ? [action.creatureId] : task.targetIds; + + if (prop.name) { + // Log Name + result.appendLog({ + name: getPropertyTitle(prop), + silenced: prop.silent, + }, task.targetIds) + } + + if (targetIds.length > 1) { + return applyTaskToEachTarget(action, task, targetIds, userInput); + } + + if (targetIds.length !== 1) { + throw 'At this step, only a single target is supported' + } + const targetId = targetIds[0]; + + // Remove buffs + if (prop.targetParentBuff) { + // Remove nearest ancestor buff + const ancestors = getPropertyAncestors(action.creatureId, prop._id); + const nearestBuff = findLast(ancestors, ancestor => ancestor.type === 'buff'); + if (!nearestBuff) { + result.appendLog({ + name: 'Error', + value: 'Buff remover does not have a parent buff to remove', + silenced: prop.silent, + }, [targetId]); + return; + } + removeBuff(nearestBuff, prop, result); + } else { + // Get all the buffs targeted by tags + const allBuffs = getPropertiesOfType(targetId, 'buff'); + const targetedBuffs = filter(allBuffs, buff => { + if (buff.inactive) return false; + if (buffRemoverMatchTags(prop, buff)) return true; + }); + // Remove the buffs + if (prop.removeAll) { + // Remove all matching buffs + targetedBuffs.forEach(buff => { + removeBuff(buff, prop, result); + }); + } else { + // Sort in reverse order + targetedBuffs.sort((a, b) => b.order - a.order); + // Remove the one with the highest order + const buff = targetedBuffs[0]; + if (buff) { + removeBuff(buff, prop, result); + } + } + } + return applyDefaultAfterPropTasks(action, prop, task.targetIds, userInput); +} + +function removeBuff(buff: any, prop, result: TaskResult) { + result.mutations.push({ + targetIds: result.targetIds, + removals: [{ propId: buff._id }], + contents: [{ + name: 'Removed', + value: `${buff.name || 'Buff'}`, + ...prop.silent && { silenced: true }, + }], + }); +} + +function buffRemoverMatchTags(buffRemover, prop) { + let matched = false; + const propTags = getEffectivePropTags(prop); + // Check the target tags + if ( + !buffRemover.targetTags?.length || + difference(buffRemover.targetTags, propTags).length === 0 + ) { + matched = true; + } + // Check the extra tags + buffRemover.extraTags?.forEach(extra => { + if (extra.operation === 'OR') { + if (matched) return; + if ( + !extra.tags.length || + difference(extra.tags, propTags).length === 0 + ) { + matched = true; + } + } else if (extra.operation === 'NOT') { + if ( + extra.tags.length && + intersection(extra.tags, propTags) + ) { + return false; + } + } + }); + return matched; +} diff --git a/app/imports/api/engine/action/applyProperties/applyCreatureTemplateProperty.ts b/app/imports/api/engine/action/applyProperties/applyCreatureTemplateProperty.ts new file mode 100644 index 00000000..ed4dbe33 --- /dev/null +++ b/app/imports/api/engine/action/applyProperties/applyCreatureTemplateProperty.ts @@ -0,0 +1,31 @@ +import { EngineAction } from '/imports/api/engine/action/EngineActions'; +import { PropTask } from '/imports/api/engine/action/tasks/Task'; +import recalculateInlineCalculations from '/imports/api/engine/action/functions/recalculateInlineCalculations'; +import getPropertyTitle from '/imports/api/utility/getPropertyTitle'; + +export default async function applyCreatureTemplateProperty( + task: PropTask, action: EngineAction, result, userInput +): Promise { + const prop = task.prop; + //Log the Creature that is about to be summoned + let logValue = prop.description?.value + if (prop.description?.text) { + recalculateInlineCalculations(prop.description, action, 'reduce', userInput); + logValue = prop.description?.value; + } + // There are no targets for creature templates + // Creatures are always summoned as children of the action's creature + result.appendLog({ + name: getPropertyTitle(prop), + value: logValue, + silenced: prop.silent, + }, []); + + result.appendLog({ + name: 'Warning', + value: 'Creature summoning is not yet implemented...', + silenced: prop.silent, + }, []); + + return; +} diff --git a/app/imports/api/engine/action/applyProperties/applyDamageProperty.test.ts b/app/imports/api/engine/action/applyProperties/applyDamageProperty.test.ts new file mode 100644 index 00000000..146f56a0 --- /dev/null +++ b/app/imports/api/engine/action/applyProperties/applyDamageProperty.test.ts @@ -0,0 +1,244 @@ +import { assert } from 'chai'; +import { + allMutations, + createTestCreature, + getRandomIds, + removeAllCreaturesAndProps, + runActionById +} from '/imports/api/engine/action/functions/actionEngineTest.testFn'; + +const [ + creatureId, targetCreatureId, targetCreature2Id, damageTargetId, damageSelfId, targetCreatureHitPointsId, targetCreature2HitPointsId, selfHitPointsId, damageWithEffectsId, effectId, effect2Id, +] = getRandomIds(20); + +const actionTestCreature = { + _id: creatureId, + props: [ + { + _id: damageTargetId, + type: 'damage', + target: 'target', + amount: { calculation: '2d6 + 7' } + }, + { + _id: damageSelfId, + type: 'damage', + target: 'self', + amount: { calculation: '1d12 + 7' } + }, + { + _id: selfHitPointsId, + type: 'attribute', + name: 'Hit Points', + attributeType: 'healthBar', + variableName: 'hitPoints', + baseValue: { calculation: '20' }, + }, + { + _id: damageWithEffectsId, + type: 'damage', + target: 'target', + amount: { calculation: '1d13 + 3' }, + tags: ['tag'] + }, + { + _id: effectId, + type: 'effect', + operation: 'add', + amount: { calculation: '1' }, + targetByTags: true, + targetTags: ['tag'], + }, + { + _id: effect2Id, + type: 'effect', + operation: 'mul', + amount: { calculation: '2' }, + targetByTags: true, + targetTags: ['tag'], + }, + ], +} + +const actionTargetCreature = { + _id: targetCreatureId, + props: [ + { + _id: targetCreatureHitPointsId, + type: 'attribute', + name: 'Hit Points', + attributeType: 'healthBar', + variableName: 'hitPoints', + baseValue: { calculation: '33' }, + } + ] +} + +const actionTargetCreature2 = { + _id: targetCreature2Id, + props: [ + { + _id: targetCreature2HitPointsId, + type: 'attribute', + name: 'Hit Points', + attributeType: 'healthBar', + variableName: 'hitPoints', + baseValue: { calculation: '47' }, + } + ] +} + +describe('Apply Damage Properties', function () { + // Increase timeout + this.timeout(8000); + + before(async function () { + await removeAllCreaturesAndProps(); + await createTestCreature(actionTestCreature); + await createTestCreature(actionTargetCreature); + await createTestCreature(actionTargetCreature2); + }); + + it('Damages self', async function () { + const action = await runActionById(damageSelfId); + assert.exists(action); + assert.deepEqual(allMutations(action), [{ + contents: [ + { + inline: true, + name: 'Damage', + value: '1d12 [6] + 7', + } + ], + targetIds: [creatureId], + }, { + contents: [{ + inline: true, + name: 'Attribute damaged', + value: '−13 Hit Points', + }], + updates: [ + { + propId: selfHitPointsId, + type: 'attribute', + inc: { damage: 13, value: -13 }, + }, + ], + targetIds: [creatureId], + }]); + }); + + it('Damages a single target', async function () { + const action = await runActionById(damageTargetId, [targetCreatureId]); + assert.exists(action); + assert.deepEqual(allMutations(action), [{ + contents: [ + { + inline: true, + name: 'Damage', + value: '2d6 [3, 4] + 7', + } + ], + targetIds: [targetCreatureId], + }, { + contents: [ + { + inline: true, + name: 'Attribute damaged', + value: '−14 Hit Points', + } + ], + targetIds: [targetCreatureId], + updates: [ + { + propId: targetCreatureHitPointsId, + type: 'attribute', + inc: { damage: 14, value: -14 }, + }, + ], + }]); + }); + + it('Damages multiple targets', async function () { + const action = await runActionById(damageTargetId, [ + targetCreatureId, targetCreature2Id + ]); + assert.exists(action); + assert.deepEqual(allMutations(action), [{ + contents: [ + { + inline: true, + name: 'Damage', + value: '2d6 [3, 4] + 7', + } + ], + targetIds: [ + targetCreatureId, + targetCreature2Id, + ], + }, { + contents: [ + { + inline: true, + name: 'Attribute damaged', + value: '−14 Hit Points', + } + ], + targetIds: [targetCreatureId], + updates: [ + { + propId: targetCreatureHitPointsId, + type: 'attribute', + inc: { damage: 14, value: -14 }, + }, + ], + }, { + contents: [ + { + inline: true, + name: 'Attribute damaged', + value: '−14 Hit Points', + } + ], + targetIds: [targetCreature2Id], + updates: [ + { + propId: targetCreature2HitPointsId, + type: 'attribute', + inc: { damage: 14, value: -14 }, + }, + ], + }]); + }); + + it('Applies effects when doing damage', async function () { + const action = await runActionById(damageWithEffectsId, [targetCreatureId]); + assert.exists(action); + assert.deepEqual(allMutations(action), [{ + contents: [ + { + inline: true, + name: 'Damage', + value: '(1d13 [7] + 4) * 2', + } + ], + targetIds: [targetCreatureId], + }, { + contents: [ + { + inline: true, + name: 'Attribute damaged', + value: '−22 Hit Points', + } + ], + targetIds: [targetCreatureId], + updates: [ + { + propId: targetCreatureHitPointsId, + type: 'attribute', + inc: { damage: 22, value: -22 }, + }, + ], + }]); + }); +}); diff --git a/app/imports/api/engine/action/applyProperties/applyDamageProperty.ts b/app/imports/api/engine/action/applyProperties/applyDamageProperty.ts new file mode 100644 index 00000000..c38c53e5 --- /dev/null +++ b/app/imports/api/engine/action/applyProperties/applyDamageProperty.ts @@ -0,0 +1,322 @@ +import { some, includes, difference, intersection } from 'lodash'; + +import { getConstantValueFromScope } from '/imports/api/creature/creatures/CreatureVariables'; +import { EngineAction } from '/imports/api/engine/action/EngineActions'; +import { applyDefaultAfterPropTasks } from '/imports/api/engine/action/functions/applyTaskGroups'; +import { getEffectiveActionScope } from '/imports/api/engine/action/functions/getEffectiveActionScope'; +import recalculateCalculation from '/imports/api/engine/action/functions/recalculateCalculation'; +import { PropTask } from '/imports/api/engine/action/tasks/Task'; +import TaskResult from '/imports/api/engine/action/tasks/TaskResult'; +import { isFiniteNode } from '/imports/parser/parseTree/constant'; +import resolve from '/imports/parser/resolve'; +import toString from '/imports/parser/toString'; +import { getPropertiesOfType } from '/imports/api/engine/loadCreatures'; +import applyTask from '/imports/api/engine/action/tasks/applyTask'; +import InputProvider from '/imports/api/engine/action/functions/userInput/InputProvider'; +import getEffectivePropTags from '/imports/api/engine/computation/utility/getEffectivePropTags'; +import Context from '/imports/parser/types/Context'; +import applySavingThrowProperty from '/imports/api/engine/action/applyProperties/applySavingThrowProperty'; + +export default async function applyDamageProperty( + task: PropTask, action: EngineAction, result: TaskResult, inputProvider: InputProvider +) { + const prop = task.prop; + const scope = getEffectiveActionScope(action); + + // Skip if there is no parse node to work with + if (!prop.amount?.parseNode) return; + + // Choose target + const damageTargets = prop.target === 'self' ? [action.creatureId] : task.targetIds; + // Determine if the hit is critical + const criticalHit = await getConstantValueFromScope('~criticalHit', scope) + && prop.damageType !== 'healing'; // Can't critically heal + // Double the damage rolls if the hit is critical + const context = new Context({ + options: { doubleRolls: criticalHit }, + }); + + // Gather all the lines we need to log into an array + const logValue: string[] = []; + const logName = prop.damageType === 'healing' ? 'Healing' : 'Damage'; + + // roll the dice only and store that string + await recalculateCalculation(prop.amount, action, 'compile', inputProvider); + const { result: rolled } = await resolve('roll', prop.amount.valueNode, scope, context, inputProvider); + if (rolled.parseType !== 'constant') { + logValue.push(toString(rolled)); + } + result.appendParserContextErrors(context, damageTargets); + + // Reset the errors so we don't log the same errors twice + context.errors = []; + + // Resolve the roll to a final value + const { result: reduced } = await resolve('reduce', rolled, scope, context, inputProvider); + result.appendParserContextErrors(context, damageTargets); + + // Store the result + let damage: number | undefined = undefined; + if (reduced.parseType === 'constant') { + prop.amount.value = reduced.value; + if (typeof reduced.value === 'number') { + damage = reduced.value; + } + } else if (reduced.parseType === 'error') { + prop.amount.value = null; + } else { + prop.amount.value = toString(reduced); + } + + // If we didn't end up with damage of finite amount, give up + if ( + typeof damage !== 'number' + || !isFinite(damage) + ) { + return applyDefaultAfterPropTasks(action, prop, damageTargets, inputProvider); + } + + // Round the damage to a whole number + damage = Math.floor(damage); + scope['~damage'] = { value: damage }; + + // Convert extra damage into the stored type + const lastDamageType = await getConstantValueFromScope('~lastDamageType', scope); + if (prop.damageType === 'extra' && typeof lastDamageType === 'string') { + prop.damageType = lastDamageType; + } + // Store current damage type + if (prop.damageType !== 'healing') { + scope['~lastDamageType'] = { value: prop.damageType }; + } + + // Memoise the damage suffix for the log + const suffix = (criticalHit ? ' critical ' : ' ') + + prop.damageType + + (prop.damageType !== 'healing' ? ' damage ' : ''); + + // If there is a save, calculate the save damage + let damageOnSave, saveProp, saveRoll; + if (prop.save) { + if (prop.save.damageFunction?.calculation) { + await recalculateCalculation(prop.save.damageFunction, action, 'compile', inputProvider); + context.errors = []; + const { result: saveDamageRolled } = await resolve( + 'roll', prop.save.damageFunction.valueNode, scope, context, inputProvider + ); + saveRoll = toString(saveDamageRolled); + const { result: saveDamageResult } = await resolve( + 'reduce', saveDamageRolled, scope, context, inputProvider + ); + result.appendParserContextErrors(context, damageTargets); + // If we didn't end up with a constant of finite amount, give up + if ( + !isFiniteNode(saveDamageResult) + ) { + return applyDefaultAfterPropTasks(action, prop, damageTargets, inputProvider); + } + // Round the damage to a whole number + damageOnSave = Math.floor(saveDamageResult.value); + } else { + damageOnSave = Math.floor(damage / 2); + } + saveProp = { + node: { + ...prop.save, + name: prop.save.stat, + silent: prop.silent, + }, + children: [], + } + } + + if (damageTargets && damageTargets.length) { + // Iterate through all the targets + for (const target of damageTargets) { + let damageToApply = damage || 0; + + // If there is a saving throw, apply that first + if (prop.save) { + await applySavingThrowProperty({ + prop: saveProp, + targetIds: task.targetIds, + }, action, result, inputProvider); + if (await getConstantValueFromScope('~saveSucceeded', scope)) { + // Log the total damage + logValue.push(toString(reduced)); + // Log the save damage + const damageText = damageFunctionText(prop.save); + if (damageText) { + logValue.push(damageText); + } else { + logValue.push( + '**Damage on successful save**', + prop.save.damageFunction.calculation, + saveRoll + ); + } + damageToApply = damageOnSave; + } + } + + // Apply weaknesses/resistances/immunities + damageToApply = applyDamageMultipliers({ + target, + damage: damageToApply, + damageProp: prop, + logValue + }); + + // Deal the damage to the target + await dealDamage( + action, prop, result, inputProvider, target, prop.damageType, damageToApply + ); + } + } else { + // There are no targets, just log the result + logValue.push(`**${damage}** ${suffix}`); + if (prop.save) { + await applySavingThrowProperty(saveProp, action, result, inputProvider); + await applySavingThrowProperty({ + prop: saveProp, + targetIds: task.targetIds, + }, action, result, inputProvider); + logValue.push(`**${damageOnSave}** ${suffix} on a successful save`); + } + } + if (logValue.length) result.appendLog({ + name: logName, + value: logValue.join('\n'), + inline: true, + silenced: prop.silent, + }, damageTargets); + return applyDefaultAfterPropTasks(action, prop, damageTargets, inputProvider); +} + +function damageFunctionText(save) { + if (!save) return; + if (!save.damageFunction) { + return '**Half damage on successful save**'; + } + if (save.damageFunction.calculation == '0' || save.damageFunction.value === 0) { + return '**No damage on successful save**' + } +} + +function applyDamageMultipliers({ target, damage, damageProp, logValue }) { + const damageType = damageProp?.damageType; + if (!damageType) return damage; + + const multiplier = target?.variables?.[damageType]; + if (!multiplier) return damage; + + const damageTypeText = damageType == 'healing' ? 'healing' : `${damageType} damage`; + + if ( + multiplier.immunity && + some(multiplier.immunities, multiplierAppliesTo(damageProp, 'immunity')) + ) { + logValue.push(`Immune to ${damageTypeText}`); + return 0; + } else { + if ( + multiplier.resistance && + some(multiplier.resistances, multiplierAppliesTo(damageProp, 'resistance')) + ) { + logValue.push(`Resistant to ${damageTypeText}`); + damage = Math.floor(damage / 2); + } + if ( + multiplier.vulnerability && + some(multiplier.vulnerabilities, multiplierAppliesTo(damageProp, 'vulnerability')) + ) { + logValue.push(`Vulnerable to ${damageTypeText}`); + damage = Math.floor(damage * 2); + } + } + return damage; +} + +function multiplierAppliesTo(damageProp, multiplierType) { + return multiplier => { + // Apply the default 'ignore x' tags + const effectiveTags = getEffectivePropTags(damageProp); + if (includes(effectiveTags, `ignore ${multiplierType}`)) return false; + + const hasRequiredTags = difference( + multiplier.includeTags, effectiveTags + ).length === 0; + + const hasNoExcludedTags = intersection( + multiplier.excludeTags, effectiveTags + ).length === 0; + + return hasRequiredTags && hasNoExcludedTags; + } +} + +async function dealDamage( + action: EngineAction, prop: any, result: TaskResult, userInput: InputProvider, + targetId: string, damageType: string, amount: number +) { + // Get all the health bars and do damage to them + let healthBars = getPropertiesOfType(targetId, 'attribute'); + + // Keep only the healthbars that can take damage/healing + healthBars = healthBars.filter((bar) => { + if (bar.attributeType !== 'healthBar' || bar.inactive || bar.removed || bar.overridden) { + return false; + } + if (damageType === 'healing' && bar.healthBarNoHealing) { + return false; + } + if (damageType !== 'healing' && amount >= 0 && bar.healthBarNoDamage) { + return false; + } + return true; + }); + + // Sort healthbars by damage/healing order or tree order as a fallback + healthBars.sort((a, b) => { + let diff; + if (amount >= 0) { + diff = a.healthBarDamageOrder - b.healthBarDamageOrder; + } else { + diff = a.healthBarHealingOrder - b.healthBarHealingOrder; + } + if (Number.isFinite(diff)) { + return diff; + } else { + return a.order - b.order; + } + }); + + // Deal the damage to each healthbar in order until all damage is done + const totalDamage = amount; + let damageLeft = totalDamage; + if (damageType === 'healing') damageLeft = -totalDamage; + for (const healthBar of healthBars) { + if (damageLeft === 0) return; + // Do the damage + const damageAdded = await applyTask(action, { + targetIds: [targetId], + subtaskFn: 'damageProp', + params: { + operation: 'increment', + value: +damageLeft || 0, + targetProp: healthBar, + }, + }, userInput); + + damageLeft -= damageAdded; + // Prevent overflow + if ( + damageType === 'healing' ? + healthBar.healthBarNoHealingOverflow : + healthBar.healthBarNoDamageOverflow + ) { + damageLeft = 0; + } + } + return totalDamage; +} diff --git a/app/imports/api/engine/action/applyProperties/applyFolderProperty.test.ts b/app/imports/api/engine/action/applyProperties/applyFolderProperty.test.ts new file mode 100644 index 00000000..3eec1b2c --- /dev/null +++ b/app/imports/api/engine/action/applyProperties/applyFolderProperty.test.ts @@ -0,0 +1,49 @@ +import { assert } from 'chai'; +import { + allMutations, + createTestCreature, + getRandomIds, + removeAllCreaturesAndProps, + runActionById +} from '/imports/api/engine/action/functions/actionEngineTest.testFn'; + +const [ + creatureId, folderId +] = getRandomIds(100); + +const actionTestCreature = { + _id: creatureId, + props: [ + { + _id: folderId, + type: 'folder', + children: [{ + type: 'note', + summary: { text: 'this should run' }, + }], + }, + ], +} + +describe('Apply folder properties', function () { + // Increase timeout + this.timeout(8000); + + before(async function () { + await removeAllCreaturesAndProps(); + await createTestCreature(actionTestCreature); + }); + + it('Applies the children of the folder', async function () { + const action = await runActionById(folderId); + assert.exists(action); + assert.deepEqual(allMutations(action), [{ + contents: [ + { + value: 'this should run' + } + ], + targetIds: [], + }]); + }); +}); diff --git a/app/imports/api/engine/action/applyProperties/applyFolderProperty.ts b/app/imports/api/engine/action/applyProperties/applyFolderProperty.ts new file mode 100644 index 00000000..ca1b3c68 --- /dev/null +++ b/app/imports/api/engine/action/applyProperties/applyFolderProperty.ts @@ -0,0 +1,11 @@ +import { EngineAction } from '/imports/api/engine/action/EngineActions'; +import { applyDefaultAfterPropTasks } from '/imports/api/engine/action/functions/applyTaskGroups'; +import { PropTask } from '/imports/api/engine/action/tasks/Task'; + + +export default async function applyFolderProperty( + task: PropTask, action: EngineAction, result, userInput +): Promise { + const prop = task.prop; + return applyDefaultAfterPropTasks(action, prop, task.targetIds, userInput); +} diff --git a/app/imports/api/engine/action/applyProperties/applyNoteProperty.test.ts b/app/imports/api/engine/action/applyProperties/applyNoteProperty.test.ts new file mode 100644 index 00000000..e7ec36e0 --- /dev/null +++ b/app/imports/api/engine/action/applyProperties/applyNoteProperty.test.ts @@ -0,0 +1,48 @@ +import { assert } from 'chai'; +import { + allMutations, + createTestCreature, + getRandomIds, + removeAllCreaturesAndProps, + runActionById +} from '/imports/api/engine/action/functions/actionEngineTest.testFn'; + +const [ + creatureId, noteId +] = getRandomIds(2); + +const actionTestCreature = { + _id: creatureId, + props: [ + { + _id: noteId, + type: 'note', + name: 'Note Name', + summary: { text: 'Note summary {1 + 2}' } + }, + ], +} + +describe('Apply note properties', function () { + // Increase timeout + this.timeout(8000); + + before(async function () { + await removeAllCreaturesAndProps(); + await createTestCreature(actionTestCreature); + }); + + it('Applies the note text', async function () { + const action = await runActionById(noteId); + assert.exists(action); + assert.deepEqual(allMutations(action), [{ + contents: [ + { + name: 'Note Name', + value: 'Note summary 3' + } + ], + targetIds: [], + }]); + }); +}); diff --git a/app/imports/api/engine/action/applyProperties/applyNoteProperty.ts b/app/imports/api/engine/action/applyProperties/applyNoteProperty.ts new file mode 100644 index 00000000..41baa4dd --- /dev/null +++ b/app/imports/api/engine/action/applyProperties/applyNoteProperty.ts @@ -0,0 +1,33 @@ +import { EngineAction } from '/imports/api/engine/action/EngineActions'; +import InputProvider from '/imports/api/engine/action/functions/userInput/InputProvider'; +import { applyDefaultAfterPropTasks } from '/imports/api/engine/action/functions/applyTaskGroups'; +import recalculateInlineCalculations from '/imports/api/engine/action/functions/recalculateInlineCalculations'; +import { PropTask } from '/imports/api/engine/action/tasks/Task'; +import TaskResult, { LogContent } from '/imports/api/engine/action/tasks/TaskResult'; + +export default async function applyNoteProperty( + task: PropTask, action: EngineAction, result: TaskResult, inputProvider: InputProvider +): Promise { + const prop = task.prop; + const logContent: LogContent & { silenced: boolean } = { + silenced: prop.silent, + }; + if (prop.name) logContent.name = prop.name; + if (prop.summary?.text) { + await recalculateInlineCalculations(prop.summary, action, 'reduce', inputProvider); + logContent.value = prop.summary.value; + } + + if (logContent.name || logContent.value) { + result.appendLog(logContent, task.targetIds); + } + // Log description + if (prop.description?.text) { + await recalculateInlineCalculations(prop.description, action, 'reduce', inputProvider); + result.appendLog({ + value: prop.description.value, + silenced: prop.silent, + }, task.targetIds); + } + return applyDefaultAfterPropTasks(action, prop, task.targetIds, inputProvider); +} diff --git a/app/imports/api/engine/action/applyProperties/applyRollProperty.test.ts b/app/imports/api/engine/action/applyProperties/applyRollProperty.test.ts new file mode 100644 index 00000000..26faa70d --- /dev/null +++ b/app/imports/api/engine/action/applyProperties/applyRollProperty.test.ts @@ -0,0 +1,53 @@ +import { assert } from 'chai'; +import { + allLogContent, + createTestCreature, + getRandomIds, + removeAllCreaturesAndProps, + runActionById +} from '/imports/api/engine/action/functions/actionEngineTest.testFn'; + +const [ + creatureId, rollId, +] = getRandomIds(2); + +const actionTestCreature = { + _id: creatureId, + props: [ + { + _id: rollId, + type: 'roll', + name: 'Roll Name', + variableName: 'roll1', + roll: { calculation: '7 + 15' }, + children: [ + { + type: 'note', + summary: { text: 'roll: {roll1}' }, + }, + ], + }, + ], +}; + +describe('Apply roll properties', function () { + // Increase timeout + this.timeout(8000); + + before(async function () { + await removeAllCreaturesAndProps(); + await createTestCreature(actionTestCreature); + }); + + it('Saves the value of the roll into the variable name', async function () { + const action = await runActionById(rollId); + assert.exists(action); + assert.deepEqual(allLogContent(action), [{ + inline: true, + name: 'Roll Name', + value: '**22**', + }, { + value: 'roll: 22', + }]); + }); +}); diff --git a/app/imports/api/engine/action/applyProperties/applyRollProperty.ts b/app/imports/api/engine/action/applyProperties/applyRollProperty.ts new file mode 100644 index 00000000..a27f2149 --- /dev/null +++ b/app/imports/api/engine/action/applyProperties/applyRollProperty.ts @@ -0,0 +1,64 @@ +import { EngineAction } from '/imports/api/engine/action/EngineActions'; +import InputProvider from '/imports/api/engine/action/functions/userInput/InputProvider'; +import { applyDefaultAfterPropTasks } from '/imports/api/engine/action/functions/applyTaskGroups'; +import { rollAndReduceCalculation } from '/imports/api/engine/action/functions/recalculateCalculation'; +import { PropTask } from '/imports/api/engine/action/tasks/Task'; +import TaskResult from '/imports/api/engine/action/tasks/TaskResult'; +import { isFiniteNode } from '/imports/parser/parseTree/constant'; +import toString from '/imports/parser/toString'; + +export default async function applyRollProperty( + task: PropTask, action: EngineAction, result: TaskResult, inputProvider: InputProvider +): Promise { + const prop = task.prop; + // If there isn't a calculation, just apply the children instead + if (!prop.roll?.calculation) { + return applyDefaultAfterPropTasks(action, prop, task.targetIds, inputProvider); + } + + const logValue: string[] = []; + + // roll the dice only and store that string + const { + rolled, reduced, errors + } = await rollAndReduceCalculation(prop.roll, action, inputProvider); + + if (rolled.parseType !== 'constant') { + logValue.push(toString(rolled)); + } + errors?.forEach(error => { + result.appendLog({ + name: 'Error', + value: error.message, + silenced: prop.silent, + }, task.targetIds); + }); + + // Store the result + if (reduced.parseType === 'constant') { + prop.roll.value = reduced.value; + } else if (reduced.parseType === 'error') { + prop.roll.value = null; + } else { + prop.roll.value = toString(reduced); + } + + // If we didn't end up with a constant or a number of finite value, give up + if (reduced?.parseType !== 'constant' || !isFiniteNode(reduced)) { + return applyDefaultAfterPropTasks(action, prop, task.targetIds, inputProvider); + } + const value = reduced.value; + + result.scope[prop.variableName] = { value }; + logValue.push(`**${value}**`); + + result.appendLog({ + name: prop.name, + value: logValue.join('\n'), + inline: true, + silenced: prop.silent, + }, task.targetIds); + + // Apply children + return applyDefaultAfterPropTasks(action, prop, task.targetIds, inputProvider); +} diff --git a/app/imports/api/engine/action/applyProperties/applySavingThrowProperty.test.ts b/app/imports/api/engine/action/applyProperties/applySavingThrowProperty.test.ts new file mode 100644 index 00000000..ef44505f --- /dev/null +++ b/app/imports/api/engine/action/applyProperties/applySavingThrowProperty.test.ts @@ -0,0 +1,114 @@ +import { assert } from 'chai'; +import { + allMutations, + createTestCreature, + getRandomIds, + removeAllCreaturesAndProps, + runActionById +} from '/imports/api/engine/action/functions/actionEngineTest.testFn'; + +const [ + creatureId, savingThrowId, targetCreatureId, targetCreature2Id +] = getRandomIds(4); + +const actionTestCreature = { + _id: creatureId, + props: [ + { + _id: savingThrowId, + type: 'savingThrow', + name: 'Strength Save', + dc: { calculation: '10 + 3' }, + stat: 'strengthSave', + children: [{ + type: 'branch', + branchType: 'successfulSave', + children: [{ + type: 'note', + summary: { text: 'note to apply on save' }, + }], + }, { + type: 'branch', + branchType: 'failedSave', + children: [{ + type: 'note', + summary: { text: 'note to apply on failed save' }, + }], + }], + }, + ], +} + +const actionTargetCreature = { + _id: targetCreatureId, + props: [ + { + type: 'skill', + variableName: 'strengthSave', + baseValue: { calculation: '3' }, + }, + ], +} +const actionTargetCreature2 = { + _id: targetCreature2Id, + props: [ + { + type: 'skill', + variableName: 'strengthSave', + baseValue: { calculation: '2' }, + }, + ], +} + +describe('Apply saving throw properties', function () { + // Increase timeout + this.timeout(8000); + + before(async function () { + await removeAllCreaturesAndProps(); + await createTestCreature(actionTestCreature); + await createTestCreature(actionTargetCreature); + await createTestCreature(actionTargetCreature2); + }); + + it('Makes multiple creatures make saves', async function () { + const action = await runActionById(savingThrowId, [targetCreatureId, targetCreature2Id]); + assert.exists(action); + assert.deepEqual(allMutations(action), [ + { + 'contents': [{ + 'inline': true, + 'name': 'Strength Save', + 'value': 'DC **13**', + }, { + 'inline': true, + 'name': 'Successful save', + 'value': '1d20 [ 10 ] + 3\n**13**', + }], + 'targetIds': [targetCreatureId], + }, { + 'contents': [{ + 'value': 'note to apply on save', + }], + 'targetIds': [targetCreatureId], + }, { + 'contents': [{ + 'inline': true, + 'name': 'Strength Save', + 'value': 'DC **13**', + }, { + 'inline': true, + 'name': 'Failed save', + 'value': '1d20 [ 10 ] + 2\n**12**', + }], + 'targetIds': [targetCreature2Id], + }, { + 'contents': [{ + 'value': 'note to apply on failed save', + }], + 'targetIds': [targetCreature2Id], + }, + ], + ); + }); +}); diff --git a/app/imports/api/engine/action/applyProperties/applySavingThrowProperty.ts b/app/imports/api/engine/action/applyProperties/applySavingThrowProperty.ts new file mode 100644 index 00000000..95688331 --- /dev/null +++ b/app/imports/api/engine/action/applyProperties/applySavingThrowProperty.ts @@ -0,0 +1,115 @@ +import { getFromScope } from '/imports/api/creature/creatures/CreatureVariables'; +import { EngineAction } from '/imports/api/engine/action/EngineActions'; +import InputProvider from '/imports/api/engine/action/functions/userInput/InputProvider'; +import { applyDefaultAfterPropTasks, applyTaskToEachTarget } from '/imports/api/engine/action/functions/applyTaskGroups'; +import getPropertyTitle from '/imports/api/utility/getPropertyTitle'; +import recalculateCalculation from '/imports/api/engine/action/functions/recalculateCalculation'; +import { PropTask } from '/imports/api/engine/action/tasks/Task'; +import TaskResult from '/imports/api/engine/action/tasks/TaskResult'; +import { getVariables } from '/imports/api/engine/loadCreatures'; +import numberToSignedString from '/imports/api/utility/numberToSignedString'; +import { isFiniteNode } from '/imports/parser/parseTree/constant'; + +export default async function applySavingThrowProperty( + task: PropTask, action: EngineAction, result: TaskResult, inputProvider: InputProvider +): Promise { + + const prop = task.prop; + + const saveTargetIds = prop.target === 'self' ? [action.creatureId] : task.targetIds; + + if (saveTargetIds.length > 1) { + return applyTaskToEachTarget(action, task, saveTargetIds, inputProvider); + } + + recalculateCalculation(prop.dc, action, 'reduce', inputProvider); + + if (!isFiniteNode(prop.dc?.valueNode)) { + result.appendLog({ + name: 'Error', + value: 'Saving throw requires a DC', + silenced: prop.silent, + }, saveTargetIds); + return applyDefaultAfterPropTasks(action, prop, saveTargetIds, inputProvider); + } + + const dc = (prop.dc?.value); + result.appendLog({ + name: getPropertyTitle(prop), + value: `DC **${dc}**`, + inline: true, + silenced: prop.silent, + }, saveTargetIds); + + const targetId = saveTargetIds[0]; + + // If there are no save targets, apply all children as if the save both + // succeeded and failed + if (!targetId) { + console.warn('no target, returning early'); + result.pushScope = { + ['~saveFailed']: { value: true }, + ['~saveSucceeded']: { value: true }, + } + return applyDefaultAfterPropTasks(action, prop, saveTargetIds, inputProvider); + } + + // Each target makes the saving throw + const save = getFromScope(prop.stat, getVariables(targetId)); + + if (!save) { + result.appendLog({ + name: 'Saving throw error', + value: 'No saving throw found: ' + prop.stat, + silenced: prop.silent, + }, [targetId]); + return applyDefaultAfterPropTasks(action, prop, [targetId], inputProvider); + } + + const rollModifierText = numberToSignedString(save.value, true); + const rollModifier = save.value; + + let value, resultPrefix; + if (save.advantage === 1) { + const [[a, b]] = await inputProvider.rollDice([{ number: 2, diceSize: 20 }]); + if (a >= b) { + value = a; + resultPrefix = `Advantage\n1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText}`; + } else { + value = b; + resultPrefix = `Advantage\n1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`; + } + } else if (save.advantage === -1) { + const [[a, b]] = await inputProvider.rollDice([{ number: 2, diceSize: 20 }]); + if (a <= b) { + value = a; + resultPrefix = `Disadvantage\n1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText}`; + } else { + value = b; + resultPrefix = `Disadvantage\n1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`; + } + } else { + const [[rolledValue]] = await inputProvider.rollDice([{ number: 1, diceSize: 20 }]); + value = rolledValue; + resultPrefix = `1d20 [ ${value} ] ${rollModifierText}` + } + result.pushScope = {}; + result.pushScope['~saveDiceRoll'] = { value }; + const resultValue = value + rollModifier || 0; + result.pushScope['~saveRoll'] = { value: resultValue }; + const saveSuccess = resultValue >= dc; + if (saveSuccess) { + result.pushScope['~saveSucceeded'] = { value: true }; + result.pushScope['~saveFailed'] = { value: false }; + } else { + result.pushScope['~saveFailed'] = { value: true }; + result.pushScope['~saveSucceeded'] = { value: false }; + } + result.appendLog({ + name: saveSuccess ? 'Successful save' : 'Failed save', + value: resultPrefix + '\n**' + resultValue + '**', + inline: true, + silenced: prop.silent, + }, [targetId]); + return applyDefaultAfterPropTasks(action, prop, [targetId], inputProvider); +} diff --git a/app/imports/api/engine/action/applyProperties/applyToggleProperty.test.ts b/app/imports/api/engine/action/applyProperties/applyToggleProperty.test.ts new file mode 100644 index 00000000..7d59c303 --- /dev/null +++ b/app/imports/api/engine/action/applyProperties/applyToggleProperty.test.ts @@ -0,0 +1,63 @@ +import { assert } from 'chai'; +import { + allMutations, + createTestCreature, + getRandomIds, + removeAllCreaturesAndProps, + runActionById +} from '/imports/api/engine/action/functions/actionEngineTest.testFn'; + +const [ + creatureId, trueToggleId, falseToggleId, +] = getRandomIds(3); + +const actionTestCreature = { + _id: creatureId, + props: [ + { + _id: trueToggleId, + type: 'toggle', + condition: { calculation: 'true' }, + children: [ + { + type: 'note', + summary: { text: 'this should run' }, + }, + ], + }, + { + _id: falseToggleId, + type: 'toggle', + condition: { calculation: 'false' }, + children: [ + { + type: 'note', + summary: { text: 'this should not run' }, + }, + ], + }, + ], +} + +describe('Apply Toggle Properties', function () { + // Increase timeout + this.timeout(8000); + + before(async function () { + await removeAllCreaturesAndProps(); + await createTestCreature(actionTestCreature); + }); + + // If branch + it('Runs a toggle with a true condition', async function () { + const action = await runActionById(trueToggleId); + assert.deepEqual(allMutations(action), [{ + contents: [{ value: 'this should run' }], + targetIds: [], + }]); + }); + it('runs a toggle with a false condition', async function () { + const action = await runActionById(falseToggleId); + assert.deepEqual(allMutations(action), []); + }); +}); diff --git a/app/imports/api/engine/action/applyProperties/applyToggleProperty.ts b/app/imports/api/engine/action/applyProperties/applyToggleProperty.ts new file mode 100644 index 00000000..f348e8b3 --- /dev/null +++ b/app/imports/api/engine/action/applyProperties/applyToggleProperty.ts @@ -0,0 +1,19 @@ +import { EngineAction } from '/imports/api/engine/action/EngineActions'; +import InputProvider from '/imports/api/engine/action/functions/userInput/InputProvider'; +import { applyAfterTasksSkipChildren, applyDefaultAfterPropTasks } from '/imports/api/engine/action/functions/applyTaskGroups'; +import recalculateCalculation from '/imports/api/engine/action/functions/recalculateCalculation'; +import { PropTask } from '/imports/api/engine/action/tasks/Task'; +import TaskResult from '/imports/api/engine/action/tasks/TaskResult'; + +export default async function applyToggle( + task: PropTask, action: EngineAction, result: TaskResult, inputProvider: InputProvider +): Promise { + + const prop = task.prop; + await recalculateCalculation(prop.condition, action, 'reduce', inputProvider); + if (prop.condition?.value) { + return applyDefaultAfterPropTasks(action, prop, task.targetIds, inputProvider); + } else { + return applyAfterTasksSkipChildren(action, prop, task.targetIds, inputProvider); + } +} diff --git a/app/imports/api/engine/action/applyProperties/applyTriggerProperty.test.ts b/app/imports/api/engine/action/applyProperties/applyTriggerProperty.test.ts new file mode 100644 index 00000000..b92f1c1d --- /dev/null +++ b/app/imports/api/engine/action/applyProperties/applyTriggerProperty.test.ts @@ -0,0 +1,119 @@ +import { assert } from 'chai'; +import { + allLogContent, + createTestCreature, + getRandomIds, + removeAllCreaturesAndProps, + runActionById +} from '/imports/api/engine/action/functions/actionEngineTest.testFn'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; + +const [ + creatureId, targetCreatureId, targetCreature2Id, actionWithTriggerId, triggerBeforeActionId, + triggerAfterActionId, triggerAfterActionChildrenId +] = getRandomIds(100); + +const actionTestCreature = { + _id: creatureId, + props: [ + // Action with triggers + { + _id: actionWithTriggerId, + type: 'action', + tags: ['trigger tag'], + children: [ + { + type: 'note', + name: 'Action Child' + } + ], + }, + { + _id: triggerBeforeActionId, + type: 'trigger', + targetTags: ['trigger tag'], + name: 'Before Action Trigger', + event: 'doActionProperty', + actionPropertyType: 'action', + timing: 'before', + }, + { + _id: triggerAfterActionId, + type: 'trigger', + targetTags: ['trigger tag'], + name: 'After Action Trigger', + event: 'doActionProperty', + actionPropertyType: 'action', + timing: 'after', + }, + { + _id: triggerAfterActionChildrenId, + type: 'trigger', + targetTags: ['trigger tag'], + name: 'After Action Children Trigger', + event: 'doActionProperty', + actionPropertyType: 'action', + timing: 'afterChildren', + }, + ], +} + +const actionTargetCreature = { + _id: targetCreatureId, + props: [ + { + type: 'attribute', + attributeType: 'stat', + variableName: 'armor', + baseValue: { calculation: '10' }, + } + ] +} + +const actionTargetCreature2 = { + _id: targetCreature2Id, + props: [ + { + type: 'attribute', + attributeType: 'stat', + variableName: 'armor', + baseValue: { calculation: '10' }, + } + ] +} + +describe('Triggers', function () { + // Increase timeout + this.timeout(8000); + + before(async function () { + await removeAllCreaturesAndProps(); + await createTestCreature(actionTestCreature); + await createTestCreature(actionTargetCreature); + await createTestCreature(actionTargetCreature2); + }); + + it('should run triggers on actions', async function () { + const actionProp = CreatureProperties.findOne(actionWithTriggerId); + assert.deepEqual(actionProp.triggerIds, { + before: [triggerBeforeActionId], + after: [triggerAfterActionId], + afterChildren: [triggerAfterActionChildrenId], + }, 'Prop\'s triggerIds should be set'); + const action = await runActionById(actionWithTriggerId); + assert.exists(action); + assert.deepEqual(allLogContent(action), [ + { + name: 'Before Action Trigger', + }, { + name: 'Action', + }, { + name: 'After Action Trigger', + }, { + name: 'Action Child', + }, { + name: 'After Action Children Trigger', + }, + ]); + }); +}); diff --git a/app/imports/api/engine/action/applyProperties/applyTriggerProperty.ts b/app/imports/api/engine/action/applyProperties/applyTriggerProperty.ts new file mode 100644 index 00000000..a7c86268 --- /dev/null +++ b/app/imports/api/engine/action/applyProperties/applyTriggerProperty.ts @@ -0,0 +1,27 @@ +import TaskResult, { LogContent } from '../tasks/TaskResult'; +import { EngineAction } from '/imports/api/engine/action/EngineActions'; +import { applyDefaultAfterPropTasks } from '/imports/api/engine/action/functions/applyTaskGroups'; +import recalculateInlineCalculations from '/imports/api/engine/action/functions/recalculateInlineCalculations'; +import { PropTask } from '/imports/api/engine/action/tasks/Task'; +import getPropertyTitle from '/imports/api/utility/getPropertyTitle'; + +export default async function applyTriggerProperty( + task: PropTask, action: EngineAction, result: TaskResult, userInput +): Promise { + const prop = task.prop; + const logContent: LogContent & { silenced: boolean } = { + name: getPropertyTitle(prop), + silenced: prop.silent, + } + + // Add the trigger description to the log + if (prop.description?.text) { + await recalculateInlineCalculations(prop.description, action, 'reduce', userInput); + if (prop.description.value) { + logContent.value = prop.description.value; + } + } + + result.appendLog(logContent, task.targetIds); + return applyDefaultAfterPropTasks(action, prop, task.targetIds, userInput); +} diff --git a/app/imports/api/engine/action/applyProperties/index.ts b/app/imports/api/engine/action/applyProperties/index.ts new file mode 100644 index 00000000..458c5bb6 --- /dev/null +++ b/app/imports/api/engine/action/applyProperties/index.ts @@ -0,0 +1,39 @@ +import { EngineAction } from '/imports/api/engine/action/EngineActions'; +import InputProvider from '/imports/api/engine/action/functions/userInput/InputProvider'; +import { PropTask } from '/imports/api/engine/action/tasks/Task'; +import TaskResult from '/imports/api/engine/action/tasks/TaskResult'; + +import action from './applyActionProperty'; +import adjustment from './applyAdjustmentProperty'; +import branch from './applyBranchProperty'; +import buff from './applyBuffProperty'; +import buffRemover from './applyBuffRemoverProperty'; +import creature from './applyCreatureTemplateProperty'; +import damage from './applyDamageProperty'; +import folder from './applyFolderProperty'; +import note from './applyNoteProperty'; +import roll from './applyRollProperty'; +import savingThrow from './applySavingThrowProperty'; +import toggle from './applyToggleProperty'; +import trigger from './applyTriggerProperty'; + +const applyPropertyByType: { + [key: string]: (task: PropTask, action: EngineAction, result: TaskResult, input: InputProvider) => Promise +} = { + action, + adjustment, + branch, + buff, + buffRemover, + creature, + damage, + folder, + note, + roll, + savingThrow, + propertySlot: folder, + toggle, + trigger, +} + +export default applyPropertyByType; \ No newline at end of file diff --git a/app/imports/api/engine/action/applySilencedProps.test.ts b/app/imports/api/engine/action/applySilencedProps.test.ts new file mode 100644 index 00000000..05254012 --- /dev/null +++ b/app/imports/api/engine/action/applySilencedProps.test.ts @@ -0,0 +1,50 @@ +import { assert } from 'chai'; +import { + allMutations, + createTestCreature, + getRandomIds, + removeAllCreaturesAndProps, + runActionById +} from '/imports/api/engine/action/functions/actionEngineTest.testFn'; + +const [ + creatureId, silencedNoteId +] = getRandomIds(2); + +const actionTestCreature = { + _id: creatureId, + props: [ + { + _id: silencedNoteId, + type: 'note', + name: 'Note Name', + summary: { text: 'Note summary {1 + 2}' }, + silent: true, + }, + ], +} + +describe('Apply silenced properties', function () { + // Increase timeout + this.timeout(8000); + + before(async function () { + await removeAllCreaturesAndProps(); + await createTestCreature(actionTestCreature); + }); + + it('Hides the note text', async function () { + const action = await runActionById(silencedNoteId); + assert.exists(action); + assert.deepEqual(allMutations(action), [{ + contents: [ + { + name: 'Note Name', + value: 'Note summary 3', + silenced: true, + }, + ], + targetIds: [], + }]); + }); +}); diff --git a/app/imports/api/engine/action/functions/actionEngineTest.testFn.ts b/app/imports/api/engine/action/functions/actionEngineTest.testFn.ts new file mode 100644 index 00000000..4c389c19 --- /dev/null +++ b/app/imports/api/engine/action/functions/actionEngineTest.testFn.ts @@ -0,0 +1,157 @@ +import '/imports/api/simpleSchemaConfig.js'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; +import { propsFromForest } from '/imports/api/properties/tests/propTestBuilder.testFn'; +import Creatures from '/imports/api/creature/creatures/Creatures'; +import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables'; +import computeCreature from '/imports/api/engine/computeCreature'; +import { loadCreature } from '/imports/api/engine/loadCreatures'; +import EngineActions, { EngineAction } from '/imports/api/engine/action/EngineActions'; +import applyAction from '/imports/api/engine/action/functions/applyAction'; +import { LogContent, Mutation, Removal, Update } from '/imports/api/engine/action/tasks/TaskResult'; +import inputProvider from './userInput/inputProviderForTests.testFn'; +/** + * Removes all creatures, properties, and creatureVariable documents from the database + */ +export async function removeAllCreaturesAndProps() { + if (Meteor.isServer) { + return Promise.all([ + CreatureProperties.removeAsync({}), + Creatures.removeAsync({}), + CreatureVariables.removeAsync({}), + ]); + } else { + CreatureProperties.find({}).forEach(doc => CreatureProperties.remove(doc._id)); + Creatures.find({}).forEach(doc => Creatures.remove(doc._id)); + CreatureVariables.find({}).forEach((doc: any) => CreatureVariables.remove(doc._id)); + } +} + +/** + * Creates a test creature with all its props computed and loaded into memory + * You may need to set the timeout of a test higher for this function to conclude + * as it is inserting and reading potentially many database documents + */ +export async function createTestCreature(creature: TestCreature) { + const dummySubscription = Tracker.autorun(() => undefined) + await Creatures.insertAsync({ + _id: creature._id, + name: creature.name || 'Test Creature', + owner: Random.id(), + dirty: true, + }); + const propsInserted = propsFromForest(creature.props, creature._id).map(prop => { + return CreatureProperties.insertAsync(prop); + }); + await Promise.all(propsInserted); + loadCreature(creature._id, dummySubscription); + await computeCreature(creature._id,); +} + +type TestCreature = { + _id: string; + name?: string; + props: any[]; +} + +/** + * get a list of random Ids + */ +export const getRandomIds = (count) => new Array(count).fill(undefined).map(() => Random.id()); + +/** + * Creates a new Engine Action and applies the specified creature property + * @param propId The _id of the property, any property that the engine can apply will work + * @param userInputFn A function that simulates user input + * @returns The Engine Action with mutations resulting from running the action + */ +export async function runActionById(propId, targetIds?, userInput = inputProvider) { + const prop = await CreatureProperties.findOneAsync(propId); + const actionId = await createAction(prop, targetIds); + const action = await EngineActions.findOneAsync(actionId); + if (!action) throw 'Action is expected to exist'; + await applyAction(action, userInput, { simulate: true }); + return action; +} + +/** + * Creates and inserts a new Engine Action into the database + * @param prop The property to start applying + * @param targetIds A list of target ids + * @returns Promise< id of the inserted Engine Action > + */ +function createAction(prop: any, targetIds?: string[]) { + const action: EngineAction = { + creatureId: prop.root.id, + results: [], + taskCount: 0, + task: { + prop, + targetIds: targetIds || [], + } + }; + return EngineActions.insertAsync(action); +} + +/** + * Get all the mutations in the results of an engineAction + */ +export function allMutations(action: EngineAction) { + const mutations: Mutation[] = []; + action.results.forEach(result => { + result.mutations.forEach(mutation => { + mutations.push(mutation); + }); + }); + return mutations; +} + +/** + * Get all the updates in all mutations in the result of an Engine Action + */ +export function allUpdates(action: EngineAction) { + const updates: Update[] = []; + allMutations(action).forEach(mutation => { + mutation.updates?.forEach(update => { + updates.push(update); + }); + }); + return updates; +} +/** + * Get all the inserts in all mutations in the result of an Engine Action + */ +export function allInserts(action: EngineAction) { + const inserts: any[] = []; + allMutations(action).forEach(mutation => { + mutation.inserts?.forEach(update => { + inserts.push(update); + }); + }); + return inserts; +} + +/** + * Get all the removals in all mutations in the result of an Engine Action + */ +export function allRemovals(action: EngineAction) { + const removals: Removal[] = []; + allMutations(action).forEach(mutation => { + mutation.removals?.forEach(update => { + removals.push(update); + }); + }); + return removals +} + +/** + * Get all the log content in all mutations in the result of an Engine Action + */ +export function allLogContent(action: EngineAction) { + const contents: LogContent[] = []; + allMutations(action).forEach(mutation => { + mutation.contents?.forEach(logContent => { + contents.push(logContent); + }); + }); + return contents; +} diff --git a/app/imports/api/engine/action/functions/applyAction.ts b/app/imports/api/engine/action/functions/applyAction.ts new file mode 100644 index 00000000..60b31b15 --- /dev/null +++ b/app/imports/api/engine/action/functions/applyAction.ts @@ -0,0 +1,53 @@ +import { EngineAction } from '/imports/api/engine/action/EngineActions'; +import applyTask from '/imports/api/engine/action/tasks/applyTask' +import InputProvider from '/imports/api/engine/action/functions/userInput/InputProvider'; +import saveInputChoices from './userInput/saveInputChoices'; + +// TODO create a function to get the effective value of a property, +// simulating all the result updates in the action so far + +/** + * Apply an action + * This is run once as a simulation on the client awaiting all the various inputs or step through + * clicks from the user, then it is run as part of the runAction method, where it is expected to + * complete instantly on the client, and sent to the server as a method call + * @param action The action to apply + * @param userInput The input provider + * @param { Object } options + */ +export default async function applyAction(action: EngineAction, userInput: InputProvider, options?: { + simulate?: boolean, stepThrough?: boolean, +}) { + const { simulate, stepThrough } = options || {}; + if (!simulate && stepThrough) throw 'Cannot step through unless simulating'; + if (simulate && !userInput) throw 'Must provide a function to get user input when simulating'; + + if (action._isSimulation || action._stepThrough) { + console.error('_isSimulation and _stepThrough should not be set on the action, rather call\ + applyAction with the appropriate options'); + } + + // If we are simulating, save the user input choices + if (simulate) { + userInput = saveInputChoices(action, userInput); + } + + action._stepThrough = stepThrough; + action._isSimulation = simulate; + action.taskCount = 0; + // Get the target Ids from the user input if they are expected and not found + if ( + !action.task.targetIds?.length + && action.tabletopId + && 'prop' in action.task + && ( + action.task.prop?.target === 'singleTarget' || + action.task.prop?.target === 'multipleTargets' + ) + ) { + action.task.targetIds = await (userInput.targetIds(action.task.prop.target)); + } + + await applyTask(action, action.task, userInput); + return action; +} diff --git a/app/imports/api/engine/action/functions/applyTaskGroups.ts b/app/imports/api/engine/action/functions/applyTaskGroups.ts new file mode 100644 index 00000000..098afd0b --- /dev/null +++ b/app/imports/api/engine/action/functions/applyTaskGroups.ts @@ -0,0 +1,160 @@ +import { get } from 'lodash'; + +import { getPropertyChildren, getSingleProperty } from '/imports/api/engine/loadCreatures'; +import { EngineAction } from '/imports/api/engine/action/EngineActions'; +import applyTask from '../tasks/applyTask'; +import { PropTask } from '../tasks/Task'; +import InputProvider from '/imports/api/engine/action/functions/userInput/InputProvider'; + +/** + * Get all the child tasks of a given property + * @param action + * @param prop + * @param targetIds + * @returns + */ +export async function applyChildren( + action: EngineAction, prop, targetIds: string[], inputProvider: InputProvider +) { + const children = await getPropertyChildren(action.creatureId, prop); + for (const childProp of children) { + await applyTask(action, { prop: childProp, targetIds }, inputProvider); + } +} + +/** + * Get the afterChildren triggers for a given property + * @param prop + * @param targetIds + * @returns + */ +export async function applyAfterChildrenTriggers( + action: EngineAction, prop, targetIds: string[], inputProvider: InputProvider +) { + if (!prop.triggerIds?.afterChildren) return; + for (const triggerId of prop.triggerIds.afterChildren) { + const trigger = await getSingleProperty(action.creatureId, triggerId); + if (!trigger) continue; + await applyTask(action, { prop: trigger, targetIds }, inputProvider); + } +} + +export async function applyAfterTriggers( + action: EngineAction, prop, targetIds: string[], inputProvider: InputProvider +) { + if (!prop.triggerIds?.after) return; + for (const triggerId of prop.triggerIds.after) { + const trigger = await getSingleProperty(action.creatureId, triggerId); + if (!trigger) continue; + await applyTask(action, { prop: trigger, targetIds }, inputProvider); + } +} + +/** + * Applies the following: + * After triggers + * Children of the prop + * After-children triggers + * @param action + * @param prop + * @param targetIds + * @returns + */ +export async function applyDefaultAfterPropTasks( + action: EngineAction, prop, targetIds: string[], inputProvider: InputProvider +) { + await applyAfterTriggers(action, prop, targetIds, inputProvider); + await applyChildren(action, prop, targetIds, inputProvider); + await applyAfterChildrenTriggers(action, prop, targetIds, inputProvider); +} + +/** + * Applies the following: + * After triggers + * After-children triggers + * @param action + * @param prop + * @param targetIds + * @returns + */ +export async function applyAfterTasksSkipChildren( + action: EngineAction, prop, targetIds: string[], inputProvider: InputProvider +) { + await applyAfterTriggers(action, prop, targetIds, inputProvider); + await applyAfterChildrenTriggers(action, prop, targetIds, inputProvider); +} + +/** + * Returns a list of tasks containing the following: + * After triggers + * After-children triggers + * @param action + * @param prop + * @param targetIds + * @returns + */ +export async function applyAfterPropTasksForSingleChild( + action: EngineAction, prop, childProp, targetIds: string[], inputProvider: InputProvider +) { + await applyAfterTriggers(action, prop, targetIds, inputProvider); + await applyTask(action, { prop: childProp, targetIds }, inputProvider); + await applyAfterChildrenTriggers(action, prop, targetIds, inputProvider); +} + +/** + * Returns a list of tasks containing the following: + * After triggers + * After-children triggers + * @param action + * @param prop + * @param targetIds + * @returns + */ +export async function applyAfterPropTasksForSomeChildren( + action: EngineAction, prop, children, targetIds: string[], inputProvider: InputProvider +) { + await applyAfterTriggers(action, prop, targetIds, inputProvider); + for (const childProp of children) { + await applyTask(action, { prop: childProp, targetIds }, inputProvider); + } + await applyAfterChildrenTriggers(action, prop, targetIds, inputProvider); +} + +/** + * Get all the trigger tasks for a given trigger path + * @param action + * @param prop + * @param targetIds + * @param triggerPath + * @returns + */ +export async function applyTriggers( + action: EngineAction, prop, targetIds: string[], triggerPath: string, inputProvider: InputProvider +) { + const triggerIds = get(prop, triggerPath); + if (!triggerIds) return; + for (const triggerId of triggerIds) { + const trigger = await getSingleProperty(action.creatureId, triggerId); + if (!trigger) continue; + await applyTask(action, { prop: trigger, targetIds }, inputProvider); + } +} + +/** + * Split a task over its targets, incrementing task step by 1 + * @param task + * @param targetIds + * @returns Copies of the task, but with a single target each + */ +export async function applyTaskToEachTarget( + action: EngineAction, task: PropTask, targetIds: string[] = task.targetIds, inputProvider: InputProvider +) { + if (targetIds.length <= 1) throw 'Must have multiple targets to split a task'; + // If there are targets, apply a new task to each target + for (const targetId of targetIds) { + await applyTask(action, { + ...task, + targetIds: [targetId] + }, inputProvider); + } +} diff --git a/app/imports/api/engine/action/functions/getEffectiveActionScope.ts b/app/imports/api/engine/action/functions/getEffectiveActionScope.ts new file mode 100644 index 00000000..1508d6a9 --- /dev/null +++ b/app/imports/api/engine/action/functions/getEffectiveActionScope.ts @@ -0,0 +1,60 @@ +import { EngineAction } from '/imports/api/engine/action/EngineActions'; +import { getVariables } from '/imports/api/engine/loadCreatures'; + +// Combine all the action results into the scope at present +export async function getEffectiveActionScope(action: EngineAction) { + const scope = await getVariables(action.creatureId); + // delete scope._id; + // delete scope._creatureId; + // Combine the applied results + for (const result of action.results) { + // Pop keys that are not longer used by a busy property + if (result.popScope) { + for (const key in result.popScope) { + if (!result.popScope[key]) continue; + // If the popped keys have previous results, return to them + if (scope[key]?.previous) { + scope[key] = scope[key]?.previous; + } else { + // just remove the busy flag, the prop has been consumed + delete scope[key]?._busy + } + } + } + // For keys that have just started being used by a busy property + if (result.pushScope) { + for (const key in result.pushScope) { + // If the pushed keys already exist and are busy, + // save the previous results and overwrite + // the key + if (scope[key]?._busy) { + scope[key] = { + ...result.pushScope[key], + previous: scope[key], + _busy: true, + }; + } else { + scope[key] = { + ...result.pushScope[key], + _busy: true, + }; + } + } + } + // Assign other scope changes without bashing the scope[key].previous field + if (result.scope) { + for (const key in result.scope) { + if (scope[key]?.previous || scope[key]?._busy) { + scope[key] = { + ...result.scope[key], + previous: scope[key].previous, + _busy: scope[key]._busy, + }; + } else { + scope[key] = result.scope[key]; + } + } + } + } + return scope; +} \ No newline at end of file diff --git a/app/imports/api/engine/action/functions/mutationToLogUpdates.ts b/app/imports/api/engine/action/functions/mutationToLogUpdates.ts new file mode 100644 index 00000000..1a9fdfa0 --- /dev/null +++ b/app/imports/api/engine/action/functions/mutationToLogUpdates.ts @@ -0,0 +1,13 @@ +import { Mutation } from '/imports/api/engine/action/tasks/TaskResult'; + +export default function mutationToLogUpdates(mutation: Mutation) { + if (!mutation.contents) return []; + const contents: any[] = []; + for (const content of mutation.contents) { + contents.push({ + ...content, + targetIds: mutation.targetIds, + }); + } + return contents; +} diff --git a/app/imports/api/engine/action/functions/mutationToPropUpdates.ts b/app/imports/api/engine/action/functions/mutationToPropUpdates.ts new file mode 100644 index 00000000..d500e17c --- /dev/null +++ b/app/imports/api/engine/action/functions/mutationToPropUpdates.ts @@ -0,0 +1,48 @@ +import { Mutation } from '/imports/api/engine/action/tasks/TaskResult'; +import { newOperation } from '/imports/api/engine/shared/bulkWrite'; + +export default function mutationToPropUpdates(mutation: Mutation) { + const bulkWriteOps: any[] = []; + // Updates to creature properties + if (mutation.updates) { + const propUpdatesById: Record = {}; + for (const update of mutation.updates) { + if (!propUpdatesById[update.propId]) { + propUpdatesById[update.propId] = newOperation(update.propId); + } + if (update.set) { + propUpdatesById[update.propId].updateOne.update.$set = { + ...propUpdatesById[update.propId].updateOne.update.$set, + ...update.set, + }; + } + if (update.inc) { + propUpdatesById[update.propId].updateOne.update.$inc = { + ...propUpdatesById[update.propId].updateOne.update.$inc, + ...update.inc, + }; + } + } + for (const id in propUpdatesById) { + bulkWriteOps.push(propUpdatesById[id]); + } + } + // Insert creature properties + if (mutation.inserts) for (const insertOne of mutation.inserts) { + bulkWriteOps.push({ + insertOne + }); + } + // Remove creature properties + if (mutation.removals) for (const removeOne of mutation.removals) { + console.log(removeOne); + bulkWriteOps.push({ + deleteOne: { + filter: { + _id: removeOne.propId + }, + }, + }); + } + return bulkWriteOps; +} diff --git a/app/imports/api/engine/action/functions/recalculateCalculation.ts b/app/imports/api/engine/action/functions/recalculateCalculation.ts new file mode 100644 index 00000000..1258cfd2 --- /dev/null +++ b/app/imports/api/engine/action/functions/recalculateCalculation.ts @@ -0,0 +1,79 @@ +import Context from '../../../../parser/types/Context'; +import toPrimitiveOrString from '/imports/parser/toPrimitiveOrString'; +import { + aggregateCalculationEffects, + aggregateCalculationProficiencies, +} from '/imports/api/engine/computation/computeComputation/computeByType/computeCalculation'; +import { getSingleProperty } from '/imports/api/engine/loadCreatures'; +import resolve from '/imports/parser/resolve'; +import { getEffectiveActionScope } from '/imports/api/engine/action/functions/getEffectiveActionScope'; +import { CalculatedField } from '/imports/api/properties/subSchemas/computedField'; +import { ResolveLevel } from '/imports/parser/parseTree/NodeFactory'; +import InputProvider from '/imports/api/engine/action/functions/userInput/InputProvider'; +import { EngineAction } from '/imports/api/engine/action/EngineActions'; + +// TODO Redo the work of +// imports/api/engine/computation/computeComputation/computeByType/computeCalculation.js +// But in the action scope +export default async function recalculateCalculation( + calcObj: CalculatedField, + action, + parseLevel: ResolveLevel = 'reduce', + userInput: InputProvider, +) { + if (!calcObj?.parseNode) return; + const scope = await getEffectiveActionScope(action); + // Re-resolve the parse node before effects and proficiencies + const { + result: unaffectedResult, + context + } = await resolve(parseLevel, calcObj.parseNode, scope); + calcObj.valueNode = unaffectedResult; + + // store the unaffected value + if (calcObj.effectIds || calcObj.proficiencyIds) { + calcObj.unaffected = toPrimitiveOrString(calcObj.valueNode); + } + // Apply all the effects and proficiencies + aggregateCalculationEffects( + calcObj, + id => getSingleProperty(action.creatureId, id) + ); + aggregateCalculationProficiencies( + calcObj, + id => getSingleProperty(action.creatureId, id), + scope['proficiencyBonus']?.value || 0 + ); + + // Resolve the modified valueNode, use the same context + const { + result: finalResult + } = await resolve(parseLevel, calcObj.valueNode, scope, context); + + // Store the errors + calcObj.errors = context.errors; + + // Store the value and its primitive + calcObj.value = toPrimitiveOrString(finalResult); + calcObj.valueNode = finalResult; +} + +export async function rollAndReduceCalculation( + calcObj: CalculatedField, action: EngineAction, userInput: InputProvider +) { + if (!calcObj) throw new Error('calcObj is required'); + const context = new Context(); + const scope = await getEffectiveActionScope(action); + // Compile + recalculateCalculation(calcObj, action, 'compile', userInput); + const compiled = calcObj.valueNode; + + // Roll + const { result: rolled } = await resolve('roll', calcObj.valueNode, scope, context, userInput); + + // Reduce + const { result: reduced } = await resolve('reduce', rolled, scope, context, userInput); + + // Return + return { compiled, rolled, reduced, errors: context.errors }; +} diff --git a/app/imports/api/engine/action/functions/recalculateInlineCalculations.ts b/app/imports/api/engine/action/functions/recalculateInlineCalculations.ts new file mode 100644 index 00000000..02d7e5e1 --- /dev/null +++ b/app/imports/api/engine/action/functions/recalculateInlineCalculations.ts @@ -0,0 +1,20 @@ +import embedInlineCalculations from '/imports/api/engine/computation/utility/embedInlineCalculations'; +import recalculateCalculation from './recalculateCalculation' +import { InlineCalculation } from '/imports/api/properties/subSchemas/inlineCalculationField'; +import { EngineAction } from '/imports/api/engine/action/EngineActions'; +import ResolveLevel from '/imports/parser/types/ResolveLevel'; +import InputProvider from '/imports/api/engine/action/functions/userInput/InputProvider'; + +export default async function recalculateInlineCalculations( + inlineCalcObj: InlineCalculation, action: EngineAction, + parseLevel: ResolveLevel, userInput: InputProvider +) { + // Skip if there are no calculations + if (!inlineCalcObj?.inlineCalculations?.length) return; + // Recalculate each calculation with the current scope + for (const calc of inlineCalcObj.inlineCalculations) { + await recalculateCalculation(calc, action, undefined, userInput); + } + // Embed the new calculated values + embedInlineCalculations(inlineCalcObj); +} diff --git a/app/imports/api/engine/action/functions/spendResources.ts b/app/imports/api/engine/action/functions/spendResources.ts new file mode 100644 index 00000000..7879cba6 --- /dev/null +++ b/app/imports/api/engine/action/functions/spendResources.ts @@ -0,0 +1,81 @@ +import { getFromScope } from '/imports/api/creature/creatures/CreatureVariables'; +import { EngineAction } from '/imports/api/engine/action/EngineActions'; +import { getEffectiveActionScope } from '/imports/api/engine/action/functions/getEffectiveActionScope'; +import recalculateCalculation from '/imports/api/engine/action/functions/recalculateCalculation'; +import TaskResult from '/imports/api/engine/action/tasks/TaskResult'; +import applyTask from '/imports/api/engine/action/tasks/applyTask'; +import { getSingleProperty } from '/imports/api/engine/loadCreatures'; +import { hasAncestorRelationship } from '/imports/api/parenting/parentingFunctions'; + +export default async function spendResources( + action: EngineAction, prop, targetIds: string[], result: TaskResult, userInput +) { + // Use uses + if (prop.usesLeft) { + result.mutations.push({ + targetIds, + updates: [{ + propId: prop._id, + inc: { usesUsed: 1, usesLeft: -1 }, + type: prop.type, + }], + contents: [{ + name: 'Uses left', + value: `${prop.usesLeft - 1}`, + inline: true, + silenced: prop.silent, + }] + }); + } + + // Iterate through all the resources consumed and damage them + if (prop.resources?.attributesConsumed?.length) { + for (const att of prop.resources.attributesConsumed) { + const scope = await getEffectiveActionScope(action); + const statToDamage = await getFromScope(att.variableName, scope); + await recalculateCalculation(att.quantity, action, 'reduce', userInput); + await applyTask(action, { + prop, + targetIds: [action.creatureId], + subtaskFn: 'damageProp', + params: { + operation: 'increment', + value: +att.quantity?.value || 0, + targetProp: statToDamage, + }, + }, userInput); + } + } + + // Iterate through all the items consumed and consume them + if (prop.resources?.itemsConsumed?.length) { + for (const itemConsumed of prop.resources.itemsConsumed) { + await recalculateCalculation(itemConsumed.quantity, action, 'reduce', userInput); + if (!itemConsumed.itemId) { + throw 'No ammo was selected'; + } + const item = getSingleProperty(action.creatureId, itemConsumed.itemId); + if (!item || item.root.id !== prop.root.id) { + throw 'The prop\'s ammo was not found on the creature'; + } + const quantity = +itemConsumed?.quantity?.value; + if ( + !quantity || + !isFinite(quantity) + ) continue; + + await applyTask(action, { + prop, + targetIds, + subtaskFn: 'consumeItemAsAmmo', + params: { + value: quantity, + item, + // If the item is an ancestor or descendant of this prop, skip the item's children to avoid + // an infinite loop + skipChildren: hasAncestorRelationship(item, prop), + }, + }, userInput); + } + } +} diff --git a/app/imports/api/engine/action/functions/userInput/InputProvider.ts b/app/imports/api/engine/action/functions/userInput/InputProvider.ts new file mode 100644 index 00000000..4da07d59 --- /dev/null +++ b/app/imports/api/engine/action/functions/userInput/InputProvider.ts @@ -0,0 +1,62 @@ +import Task from '/imports/api/engine/action/tasks/Task'; + +type InputProvider = { + /** + * Get the ids of the creatures being targeted + */ + targetIds(target: 'singleTarget' | 'multipleTargets', currentTargetIds?: string[]): Promise; + /** + * Show the user the next property or task to apply and wait for input to continue + */ + nextStep?(task: Task): Promise; + /** + * Roll dice + * @param dice How many dice + * @param diceSize How many faces per die + */ + rollDice( + dice: { number: number, diceSize: number }[] + ): Promise; + /** + * Choose from a provided selection + * @param action + * @param choices Options to choose from + * @param quantity Number of choices to make [min, max] inclusive, where -1 means no limit + */ + choose( + choices: ({ _id: string } & Record)[], + quantity?: [min: number, max: number], + ): Promise; + /** + * Get advantage, natural, or disadvantage for a d20 roll + */ + advantage(suggestedAdvantage: Advantage): Promise; + /** + * Get the details of a check or save + */ + check(suggestedParams: CheckParams): Promise; + /** + * Get the details of casting a spell + */ + castSpell(suggestedParams: Partial): Promise; +} + +export type Advantage = 0 | 1 | -1; + +export type CheckParams = { + advantage: Advantage; + skillVariableName?: string; + abilityVariableName?: string; + dc: number | null; + contest?: true; + targetSkillVariableName?: string; + targetAbilityVariableName?: string; +} + +export type CastSpellParams = { + spellId: string, + slotId: string | undefined, + ritual: boolean, +} + +export default InputProvider; diff --git a/app/imports/api/engine/action/functions/userInput/getDeterministicDiceRoller.ts b/app/imports/api/engine/action/functions/userInput/getDeterministicDiceRoller.ts new file mode 100644 index 00000000..b8532d83 --- /dev/null +++ b/app/imports/api/engine/action/functions/userInput/getDeterministicDiceRoller.ts @@ -0,0 +1,28 @@ +import Alea from 'alea'; + +/** + * Return a function that can be be used as InputProvider.rollDice + * this function instance must be used for the entire action + */ +export default function getDeterministicDiceRoller( + actionId: string +): (dice: { number: number, diceSize: number }[]) => Promise { + // Create a random number generator seeded on the ID of the action + if (!actionId) throw new Meteor.Error('Id Required', 'action ID can not be ' + actionId) + const randFrac = Alea(actionId); + return (dice) => { + const results: number[][] = []; + for (const diceRoll of dice) { + const values: number[] = []; + if (diceRoll.number > 100) { + throw new Meteor.Error('Too many dice', 'can only roll up to 100 dice at once'); + } + for (let i = 0; i < diceRoll.number; i++) { + const rolledValue = ~~(randFrac() * diceRoll.diceSize) + 1 + values.push(rolledValue); + } + results.push(values); + } + return Promise.resolve(results); + } +} diff --git a/app/imports/api/engine/action/functions/userInput/getReplayChoicesInputProvider.ts b/app/imports/api/engine/action/functions/userInput/getReplayChoicesInputProvider.ts new file mode 100644 index 00000000..f67de791 --- /dev/null +++ b/app/imports/api/engine/action/functions/userInput/getReplayChoicesInputProvider.ts @@ -0,0 +1,36 @@ +import InputProvider from '/imports/api/engine/action/functions/userInput/InputProvider'; +import getDeterministicDiceRoller from '/imports/api/engine/action/functions/userInput/getDeterministicDiceRoller'; + +// This assumes the user's choices are in exactly the order they will be requested +// Dice rolls are done fresh, no cheating +export default function getReplayChoicesInputProvider(actionId: string, decisions: any[]): + InputProvider { + const decisionStack = [...decisions].reverse(); + const dRoller = getDeterministicDiceRoller(actionId); + const replaySavedInput: InputProvider = { + targetIds() { + return Promise.resolve(decisionStack.pop()); + }, + nextStep() { + return Promise.resolve(); + }, + // To roll dice, ignore the user and use the deterministic dice roller again + rollDice(dice) { + decisionStack.pop(); + return dRoller(dice); + }, + choose() { + return Promise.resolve(decisionStack.pop()); + }, + advantage() { + return Promise.resolve(decisionStack.pop()); + }, + check() { + return Promise.resolve(decisionStack.pop()); + }, + castSpell() { + return Promise.resolve(decisionStack.pop()); + }, + } + return replaySavedInput; +} diff --git a/app/imports/api/engine/action/functions/userInput/inputProviderForTests.testFn.ts b/app/imports/api/engine/action/functions/userInput/inputProviderForTests.testFn.ts new file mode 100644 index 00000000..dec48a5d --- /dev/null +++ b/app/imports/api/engine/action/functions/userInput/inputProviderForTests.testFn.ts @@ -0,0 +1,50 @@ +import InputProvider from '/imports/api/engine/action/functions/userInput/InputProvider'; + +const inputProviderForTests: InputProvider = { + async targetIds(target, currentTargetIds = []) { + return currentTargetIds; + }, + /** + * For testing, randomness is hard to deal with + * rollDice function returns the average roll for every dice rolled, but increasing by one each time + * [6d10, 1d4] => [[6,7,8,9,10,1], [3]] + */ + async rollDice(dice = []) { + const result: number[][] = []; + for (const diceRoll of dice) { + const averageRoll = Math.round(diceRoll.diceSize / 2); + // Return an array full of averagely rolled dice, increasing by 1 for every dice + result.push( + new Array(diceRoll.number) + .fill(averageRoll) + .map((value, index) => (value + index - 1) % diceRoll.diceSize + 1) + ) + } + return result; + }, + /** + * For testing, always return the minimum number of choices, always choosing the first options + */ + async choose(choices, quantity = [1, 1]) { + const chosen: string[] = []; + const choiceQuantity = quantity[0] <= 0 ? 1 : quantity[0]; + for (let i = 0; i < choiceQuantity && i < choices.length; i += 1) { + chosen.push(choices[i]._id); + } + return chosen; + }, + /** + * For testing, always return the suggested advantage, as if the user never chose differently + */ + async advantage(suggestedAdvantage) { + return suggestedAdvantage; + }, + async check(suggestedParams) { + return suggestedParams; + }, + async castSpell(suggestedParams) { + return suggestedParams; + }, +} + +export default inputProviderForTests; diff --git a/app/imports/api/engine/action/functions/userInput/saveInputChoices.ts b/app/imports/api/engine/action/functions/userInput/saveInputChoices.ts new file mode 100644 index 00000000..ad1baa45 --- /dev/null +++ b/app/imports/api/engine/action/functions/userInput/saveInputChoices.ts @@ -0,0 +1,28 @@ +import { EngineAction } from '/imports/api/engine/action/EngineActions'; +import InputProvider from '/imports/api/engine/action/functions/userInput/InputProvider'; + +/** + * Create a new version of the user input function, that saves the user's choices to an array + * before returning them + */ +export default function saveInputChoices(action: EngineAction, userInput: InputProvider): InputProvider { + const newInputProvider: Partial = {}; + + if (!action._decisions) { + action._decisions = []; + } + + // For every function in the given input provider + for (const key in userInput) { + const oldFn = userInput[key]; + // Make a new function that does the same thing, but saves the result to action._decisions + const newFn = async (...args) => { + const result = await oldFn(...args); + action._decisions?.push(result); + return result; + } + newInputProvider[key] = newFn; + } + + return newInputProvider as InputProvider; +} diff --git a/app/imports/api/engine/action/functions/writeActionResults.ts b/app/imports/api/engine/action/functions/writeActionResults.ts new file mode 100644 index 00000000..b8f08309 --- /dev/null +++ b/app/imports/api/engine/action/functions/writeActionResults.ts @@ -0,0 +1,45 @@ +import EngineActions, { EngineAction } from '/imports/api/engine/action/EngineActions'; +import mutationToPropUpdates from './mutationToPropUpdates'; +import mutationToLogUpdates from '/imports/api/engine/action/functions/mutationToLogUpdates'; +import { union } from 'lodash'; +import CreatureLogs from '/imports/api/creature/log/CreatureLogs'; +import bulkWrite from '/imports/api/engine/shared/bulkWrite'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; +import Creatures from '/imports/api/creature/creatures/Creatures'; + +export default async function writeActionResults(action: EngineAction) { + if (!action._id) throw new Meteor.Error('type-error', 'Action does not have an _id'); + const engineActionPromise = EngineActions.removeAsync(action._id); + const creaturePropUpdates: any[] = []; + const logContents: any[] = []; + + // Collect all the updates and log content + action.results.forEach(result => { + result.mutations.forEach(mutation => { + creaturePropUpdates.push(...mutationToPropUpdates(mutation)); + logContents.push(...mutationToLogUpdates(mutation)); + }); + }); + const allTargetIds: string[] = union(...logContents.map(c => c.targetIds)); + + // Write the log + const logPromise = CreatureLogs.insertAsync({ + content: logContents, + creatureId: action.creatureId, + tabletopId: action.tabletopId, + }); + + // Write the bulk updates + const bulkWritePromise = bulkWrite(creaturePropUpdates, CreatureProperties); + + // Mark the creatures as dirty + const creaturePromise = Creatures.updateAsync({ + _id: { $in: [action.creatureId, ...allTargetIds] }, + }, { + $set: { dirty: true }, + }, { + multi: true, + }); + + return Promise.all([engineActionPromise, logPromise, bulkWritePromise, creaturePromise]); +} diff --git a/app/imports/api/engine/action/methods/index.ts b/app/imports/api/engine/action/methods/index.ts new file mode 100644 index 00000000..d498cbc4 --- /dev/null +++ b/app/imports/api/engine/action/methods/index.ts @@ -0,0 +1,3 @@ +import './insertAction'; +import './runAction'; +import './updateAction'; diff --git a/app/imports/api/engine/action/methods/insertAction.ts b/app/imports/api/engine/action/methods/insertAction.ts new file mode 100644 index 00000000..ffc973a4 --- /dev/null +++ b/app/imports/api/engine/action/methods/insertAction.ts @@ -0,0 +1,42 @@ +import { ValidatedMethod } from 'meteor/mdg:validated-method'; +import SimpleSchema from 'simpl-schema'; +import EngineActions, { EngineAction, ActionSchema } from '/imports/api/engine/action/EngineActions'; +import { assertEditPermission } from '/imports/api/sharing/sharingPermissions'; +import { getCreature } from '/imports/api/engine/loadCreatures'; + +export const insertAction = new ValidatedMethod({ + name: 'actions.insertAction', + validate: new SimpleSchema({ + action: ActionSchema + }).validator({ clean: true }), + rateLimit: { + numRequests: 5, + timeInterval: 1000, + }, + run: function ({ action }: { action: EngineAction }) { + const creature = getCreature(action.creatureId); + assertEditPermission(getCreature(creature), this.userId); + // Make sure the action shares the creature's tabletopId + // It is assumed that if a character you control is in a tabletop, you have the rights + // to do actions in that tabletop + action.tabletopId = creature.tabletopId; + + // Ensure that all the targeted creatures exist and share a tabletop + if (action.task.targetIds) for (const targetId of action.task.targetIds) { + const target = getCreature(targetId); + if (!target) { + throw new Meteor.Error('not-found', 'Target creature does not exist'); + } + if (target.tabletopId !== action.tabletopId) { + throw new Meteor.Error('permission-denied', 'Target creature does not share a tabletop with the acting creature'); + } + } + + // First remove all other actions on this creature + // only do one action at a time, don't wait for this to finish + EngineActions.remove({ creatureId: action.creatureId }); + // Force a random id even if one was provided, we may use it later as the seed for PRNG + delete action._id; + return EngineActions.insert(action); + }, +}); diff --git a/app/imports/api/engine/action/methods/runAction.ts b/app/imports/api/engine/action/methods/runAction.ts new file mode 100644 index 00000000..6c415de8 --- /dev/null +++ b/app/imports/api/engine/action/methods/runAction.ts @@ -0,0 +1,37 @@ +import { ValidatedMethod } from 'meteor/mdg:validated-method'; +import SimpleSchema from 'simpl-schema'; +import EngineActions from '/imports/api/engine/action/EngineActions'; +import { assertEditPermission } from '/imports/api/sharing/sharingPermissions'; +import { getCreature } from '/imports/api/engine/loadCreatures'; +import applyAction from '/imports/api/engine/action/functions/applyAction'; +import writeActionResults from '../functions/writeActionResults'; +import getReplayChoicesInputProvider from '/imports/api/engine/action/functions/userInput/getReplayChoicesInputProvider'; +import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; + +export const runAction = new ValidatedMethod({ + name: 'actions.runAction', + validate: null, + mixins: [RateLimiterMixin], + rateLimit: { + numRequests: 10, + timeInterval: 5000, + }, + run: async function ({ actionId, decisions = [] }: { actionId: string, decisions?: any[] }) { + // Get the action + const action = await EngineActions.findOneAsync(actionId); + if (!action) throw new Meteor.Error('not-found', 'Action not found'); + + // Permissions + assertEditPermission(getCreature(action.creatureId), this.userId); + + // Replay the user's decisions as user input + const userInput = getReplayChoicesInputProvider(actionId, decisions); + + // Apply the action + await applyAction(action, userInput); + + // Persist changes + const writePromise = writeActionResults(action); + return writePromise; + }, +}); diff --git a/app/imports/api/engine/action/methods/updateAction.ts b/app/imports/api/engine/action/methods/updateAction.ts new file mode 100644 index 00000000..e4101f00 --- /dev/null +++ b/app/imports/api/engine/action/methods/updateAction.ts @@ -0,0 +1,28 @@ +import { ValidatedMethod } from 'meteor/mdg:validated-method'; +import EngineActions from '/imports/api/engine/action/EngineActions'; +import { assertEditPermission } from '/imports/api/sharing/sharingPermissions'; +import { getCreature } from '/imports/api/engine/loadCreatures'; +import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; + +export const updateAction = new ValidatedMethod({ + name: 'actions.updateAction', + validate({ _id, path, value }) { + if (!_id) throw new Meteor.Error('No _id', '_id is required'); + // We cannot change these fields with a simple update + if (path !== 'targetIds') throw new Meteor.Error('Can only update target ids'); + if (!Array.isArray(value)) throw new Meteor.Error('TargetIds must be an array'); + }, + mixins: [RateLimiterMixin], + rateLimit: { + numRequests: 10, + timeInterval: 5000, + }, + run: async function ({ _id, path, value }: { _id: string, path: 'targetIds', value: string[] }) { + const action = await EngineActions.findOneAsync(_id); + if (!action) { + throw new Meteor.Error('not found', 'The given action was not found'); + } + assertEditPermission(getCreature(action.creatureId), this.userId); + return EngineActions.updateAsync(_id, { $set: { [path]: value } }); + }, +}); diff --git a/app/imports/api/engine/action/tasks/Task.ts b/app/imports/api/engine/action/tasks/Task.ts new file mode 100644 index 00000000..0b008815 --- /dev/null +++ b/app/imports/api/engine/action/tasks/Task.ts @@ -0,0 +1,66 @@ +import { CheckParams } from '/imports/api/engine/action/functions/userInput/InputProvider'; + +type Task = PropTask | DamagePropTask | ItemAsAmmoTask | CheckTask | ResetTask | CastSpellTask; + +export default Task; + +type BaseTask = { + targetIds: string[]; + silent?: boolean | undefined; +} + +type Prop = { + _id: string; + type: string; + [key: string]: any, +} + +export type PropTask = BaseTask & { + prop: Prop; + subtaskFn?: undefined; + silent?: undefined; +} + +export type DamagePropTask = BaseTask & { + subtaskFn: 'damageProp'; + params: { + /** + * Use getPropertyTitle(prop) to set the title + */ + title?: string; + operation: 'increment' | 'set'; + value: number; + targetProp: Prop; + }; +} + +export type ItemAsAmmoTask = BaseTask & { + subtaskFn: 'consumeItemAsAmmo'; + prop: Prop; + silent?: undefined; + params: { + value: number; + item: any; + skipChildren: boolean; + }; +} + +export type CheckTask = BaseTask & CheckParams & { + subtaskFn: 'check'; +} + +export type ResetTask = BaseTask & { + subtaskFn: 'reset'; + eventName: string; + // One and only one target + targetIds: [string]; +} + +export type CastSpellTask = BaseTask & { + prop?: Prop | undefined; + silent?: undefined; + subtaskFn: 'castSpell'; + params: { + spellId: string | undefined; + }; +} diff --git a/app/imports/api/engine/action/tasks/TaskResult.ts b/app/imports/api/engine/action/tasks/TaskResult.ts new file mode 100644 index 00000000..a9426fbe --- /dev/null +++ b/app/imports/api/engine/action/tasks/TaskResult.ts @@ -0,0 +1,95 @@ +import Context from '../../../../parser/types/Context'; + +/** + * The result of running a task containing all the changes that need to be made to the listed + * targets + * Each mutation may apply to a different subset of targets + */ +export default class TaskResult { + // The targets of the original task + targetIds: string[]; + scope: any; + // Consume pushed changes from the local scope, every change pushed must be popped later + popScope?: any; + // Push changes to the scope if the same task intends to consume them in later steps + // These changes will be marked as _busy until they are consumed + // This allows a property to run in between steps of the same property type without + // bashing the variables used to maintain state between steps while still exposing + // those variables to triggers that need to change them + // If multiple properties use the same variable at once, the values used by outer + // properties can be found on variable.previous + pushScope?: any; + mutations: Mutation[]; + constructor(targetIds: string[]) { + this.targetIds = targetIds; + this.mutations = []; + this.scope = {}; + } + // Appends the log content to the latest mutation + appendLog(content: LogContent & { silenced: boolean }, targetIds: string[]) { + // Create a shallow copy of the content + const logContent: LogContent = { ...content }; + // remove false silenced properties + if (!logContent.silenced) { + delete logContent.silenced; + } + if (!this.mutations.length) { + this.mutations.push({ targetIds, contents: [] }); + } + const latestMutation = this.mutations[this.mutations.length - 1] + if (!latestMutation.contents) { + latestMutation.contents = []; + } + latestMutation.contents.push(logContent); + } + appendParserContextErrors(context: Context, targetIds) { + if (!context.errors?.length) return; + if (!this.mutations.length) { + this.mutations.push({ targetIds, contents: [] }); + } + const latestMutation = this.mutations[this.mutations.length - 1] + if (!latestMutation.contents) { + latestMutation.contents = []; + } + context.errors?.forEach(error => { + latestMutation.contents?.push({ + name: 'Error', + value: error.message, + }); + }); + } +} + +export type Mutation = { + // Which creatures the mutation is applied to + // A mutation may apply to all, or a subset of, the result's targets and the acting creature + targetIds: string[]; + // What changes in the database + updates?: Update[]; + // What properties get added + // TODO make these properties a LibraryNode type + inserts?: any[]; + // What properties get deleted + removals?: Removal[]; + // Logged when this is applied + contents?: LogContent[]; +} + +export type Update = { + propId: string; + type: string, + set?: any; + inc?: any; +} + +export type Removal = { + propId: string; +} + +export type LogContent = { + name?: string; + value?: string; + inline?: boolean; + context?: any; + silenced?: boolean; +} diff --git a/app/imports/api/engine/action/tasks/applyCastSpellTask.ts b/app/imports/api/engine/action/tasks/applyCastSpellTask.ts new file mode 100644 index 00000000..0907cd12 --- /dev/null +++ b/app/imports/api/engine/action/tasks/applyCastSpellTask.ts @@ -0,0 +1,102 @@ +import { EngineAction } from '/imports/api/engine/action/EngineActions'; +import { CastSpellTask } from '/imports/api/engine/action/tasks/Task'; +import TaskResult from './TaskResult'; +import InputProvider from '/imports/api/engine/action/functions/userInput/InputProvider'; +import { getPropertiesOfType, getSingleProperty } from '/imports/api/engine/loadCreatures'; +import applyTask from '/imports/api/engine/action/tasks/applyTask'; +import applyActionProperty from '../applyProperties/applyActionProperty'; + +export default async function applySpellProperty( + task: CastSpellTask, action: EngineAction, result: TaskResult, userInput: InputProvider +): Promise { + let prop = task.prop; + // Ask the user how this spell is being cast + const castOptions = await userInput.castSpell({ + spellId: task.params.spellId, + slotId: prop?.castWithoutSpellSlots + ? undefined + : getSuggestedSpellSlotId(action.creatureId, prop), + ritual: false, + }); + if (!castOptions.spellId) { + result.appendLog({ + name: 'Error casting spell', + value: 'No spell was selected', + silenced: false, + }, [action.creatureId]); + return; + } + // If the user changed the spell they are casting, use that as the prop + prop = getSingleProperty(action.creatureId, castOptions.spellId); + + if (!prop) { + result.appendLog({ + name: 'Error casting spell', + value: 'The chosen spell was not found', + silenced: false, + }, [action.creatureId]); + return; + } + let slotLevel = prop.level || 0; + // Get the slot being cast with + const slot = castOptions.slotId && getSingleProperty(action.creatureId, castOptions.slotId); + // Log casting method + logCastingMessage(slot?.spellSlotLevel?.value, castOptions, result, prop, task.targetIds); + // Spend the spell slot and change the spell's casting level if a slot is used + if (slot) { + await spendSpellSlot(action, castOptions, userInput); + slotLevel = slot.spellSlotLevel?.value || 0; + } + // Add the slot level to the scope + result.pushScope = { + '~slotLevel': { value: slotLevel }, + 'slotLevel': { value: slotLevel }, + }; + // Run the rest of the spell as if it were an action + return applyActionProperty({ + prop, + targetIds: task.targetIds, + }, action, result, userInput); +} + +function getSuggestedSpellSlotId(creatureId, prop) { + if (!prop) return; + const slots = getPropertiesOfType(creatureId, 'spellSlot') + .sort((a, b) => a.spellSlotLevel?.value - b.spellSlotLevel?.value) + .filter(slot => slot.spellSlotLevel.value > prop.level); + return slots[0]?._id; +} + +function logCastingMessage(slotLevel: number, castOptions, result: TaskResult, prop, targetIds: string[]) { + let message = ''; + // Determine which message to post + if (slotLevel) { + message = `Casting using a level ${slotLevel} spell slot` + } else if (prop.level) { + if (castOptions.ritual) { + message = `Ritual casting at level ${slotLevel}` + } else { + message = `Casting at level ${slotLevel}` + } + } + // Post the message + if (message) { + result.appendLog({ + name: `Casting at level ${slotLevel}`, + silenced: prop.silent, + }, targetIds); + } +} + +function spendSpellSlot(action, castOptions, userInput) { + const slot = getSingleProperty(action.creatureId, castOptions.slotId); + return applyTask(action, { + targetIds: [action.creatureId], + subtaskFn: 'damageProp', + params: { + operation: 'increment', + value: 1, + targetProp: slot, + }, + }, userInput); +} diff --git a/app/imports/api/engine/action/tasks/applyCheckTask.ts b/app/imports/api/engine/action/tasks/applyCheckTask.ts new file mode 100644 index 00000000..7d3a1595 --- /dev/null +++ b/app/imports/api/engine/action/tasks/applyCheckTask.ts @@ -0,0 +1,131 @@ +import { applyTriggers } from '/imports/api/engine/action/functions/applyTaskGroups'; +import { CheckTask } from '/imports/api/engine/action/tasks/Task'; +import { EngineAction } from '/imports/api/engine/action/EngineActions'; +import { getEffectiveActionScope } from '/imports/api/engine/action/functions/getEffectiveActionScope'; +import { getFromScope } from '/imports/api/creature/creatures/CreatureVariables'; +import { getVariables } from '/imports/api/engine/loadCreatures'; +import InputProvider from '/imports/api/engine/action/functions/userInput/InputProvider'; +import numberToSignedString from '/imports/api/utility/numberToSignedString'; +import TaskResult from '/imports/api/engine/action/tasks/TaskResult'; + +/** + * A skill property is applied as a check or a saving throw + */ +export default async function applyCheckTask( + task: CheckTask, action: EngineAction, result: TaskResult, userInput: InputProvider +): Promise { + const targetIds = task.targetIds; + + if (task.contest) { + throw new Meteor.Error('not-implemented', 'This functionality is not implemented yet'); + } + + for (const targetId of targetIds) { + let scope; + if (targetId === action.creatureId) { + scope = await getEffectiveActionScope(action); + } else { + scope = getVariables(targetId); + } + // Get the updated parameters from user input + const checkParams = await userInput.check(task); + const advantage = checkParams.advantage; + + const skill = checkParams.skillVariableName && getFromScope(checkParams.skillVariableName, scope) || null; + const skillBonus = (skill?.value || 0) - (skill?.abilityMod || 0); + + const ability = checkParams.abilityVariableName && getFromScope(checkParams.abilityVariableName, scope) || null; + const abilityModifier = ability?.modifier || 0; + + + // Run the before triggers which may change scope properties + if (skill) await applyTriggers(action, skill, [targetId], 'checkTriggerIds.before', userInput); + if (ability) await applyTriggers(action, ability, [targetId], 'checkTriggerIds.before', userInput); + + if (skill || ability) { + // Create a new result after before triggers have run + result = new TaskResult(task.targetIds); + action.results.push(result); + } + + const totalModifier = skillBonus + abilityModifier; + const rollModifierText = numberToSignedString(totalModifier); + + // Get the name of the check + let checkName = 'Check'; + if (ability?.name && skill?.name) { + checkName = `${ability.name} (${skill.name})` + } else if (ability?.name || skill?.name) { + checkName = `${ability?.name || skill?.name}`; + } + + let rollName = 'Roll' + + // Append advantage/disadvantage to the check name + if (advantage === 1) { + rollName += ' (Advantage)' + } else if (advantage === -1) { + rollName += ' (Disadvantage)' + } + + // Print check name and DC if present + const dc = checkParams.dc; + result.appendLog({ + name: checkName, + inline: true, + ...dc !== null && { value: `DC **${dc}**` }, + silenced: task.silent ?? false, + }, [targetId]); + + // Roll the dice + let rolledValue, resultPrefix; + if (advantage === 1) { + const [[a, b]] = await userInput.rollDice([{ number: 2, diceSize: 20 }]); + if (a >= b) { + rolledValue = a; + resultPrefix = `1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText}`; + } else { + rolledValue = b; + resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`; + } + } else if (advantage === -1) { + const [[a, b]] = await userInput.rollDice([{ number: 2, diceSize: 20 }]); + if (a <= b) { + rolledValue = a; + resultPrefix = `1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText}`; + } else { + rolledValue = b; + resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`; + } + } else { + [[rolledValue]] = await userInput.rollDice([{ number: 1, diceSize: 20 }]); + resultPrefix = `1d20 [${rolledValue}] ${rollModifierText}` + } + + const totalValue = rolledValue + totalModifier; + + result.appendLog({ + name: rollName, + value: `${resultPrefix}\n**${totalValue}**`, + inline: true, + silenced: task.silent ?? false, + }, [targetId]); + + // After check triggers + if (skill) await applyTriggers(action, skill, [targetId], 'checkTriggerIds.after', userInput); + if (ability) await applyTriggers(action, ability, [targetId], 'checkTriggerIds.after', userInput); + + // After children check triggers + if (skill) await applyTriggers(action, skill, [targetId], 'checkTriggerIds.afterChildren', userInput); + if (ability) await applyTriggers(action, ability, [targetId], 'checkTriggerIds.afterChildren', userInput); + } +} + +// TODO set these and potentially read them again if triggers can change them +/* +'~checkAdvantage' +'~checkAdvantage' +'~checkDiceRoll' +'~checkRoll' +'~checkModifier' +*/ diff --git a/app/imports/api/engine/action/tasks/applyDamagePropTask.ts b/app/imports/api/engine/action/tasks/applyDamagePropTask.ts new file mode 100644 index 00000000..6d3fa735 --- /dev/null +++ b/app/imports/api/engine/action/tasks/applyDamagePropTask.ts @@ -0,0 +1,160 @@ +import { EngineAction } from '/imports/api/engine/action/EngineActions'; +import { DamagePropTask } from '/imports/api/engine/action/tasks/Task'; +import TaskResult from '/imports/api/engine/action/tasks/TaskResult'; +import { applyTriggers } from '/imports/api/engine/action/functions/applyTaskGroups'; +import { getEffectiveActionScope } from '/imports/api/engine/action/functions/getEffectiveActionScope'; +import getPropertyTitle from '/imports/api/utility/getPropertyTitle'; +import { getSingleProperty } from '/imports/api/engine/loadCreatures'; +import numberToSignedString from '/imports/api/utility/numberToSignedString'; + +export default async function applyDamagePropTask( + task: DamagePropTask, action: EngineAction, result: TaskResult, userInput +): Promise { + if (task.targetIds.length > 1) { + throw 'This subtask can only be called on a single target'; + } + const targetId = task.targetIds[0]; + + let { value } = task.params; + const { title, operation } = task.params; + let targetProp = task.params.targetProp; + + if (!targetProp) throw new Meteor.Error('not-found', 'Target property is required') + + // Set the scope properties + result.pushScope = {}; + if (operation === 'increment') { + if (value >= 0) { + result.pushScope['~damage'] = { value }; + } else { + result.pushScope['~healing'] = { value: -value }; + } + } else { + result.pushScope['~set'] = { value }; + } + // Store which property we're targeting + if (targetId === action.creatureId) { + result.pushScope['~attributeDamaged'] = { _propId: targetProp._id }; + } else { + result.pushScope['~attributeDamaged'] = targetProp; + } + + // Run the before triggers which may change scope properties + await applyTriggers(action, targetProp, [targetId], 'damageTriggerIds.before', userInput); + + // Create a new result after triggers have run + result = new TaskResult(task.targetIds); + action.results.push(result); + + // Refetch the scope properties + const scope = await getEffectiveActionScope(action); + result.popScope = { + '~damage': 1, '~healing': 1, '~set': 1, '~attributeDamaged': 1, + }; + value = +value; + if (operation === 'increment') { + if (value >= 0) { + value = scope['~damage']?.value; + } else { + value = -scope['~healing']?.value; + } + } else { + value = scope['~set']?.value; + } + const targetPropId = scope['~attributeDamaged']?._propId ?? + scope['~attributeDamaged']?._id; + + // If there are no targets, just log the result that would apply and end + if (!task.targetIds?.length) { + // Get the locally equivalent stat with the same variable name + const statName = getPropertyTitle(targetProp); + result.appendLog({ + name: title, + value: `${statName}${operation === 'set' ? ' set to' : ''}` + + ` ${value}`, + inline: true, + silenced: task.silent ?? false, + }, task.targetIds); + } + + let damage, newValue, increment; + targetProp = await getSingleProperty(targetId, targetPropId); + + if (!targetProp) return value; + + if (operation === 'set') { + const total = targetProp.total || 0; + // Set represents what we want the value to be after damage + // So we need the actual damage to get to that value + damage = total - value; + // Damage can't exceed total value + if (damage > total && !targetProp.ignoreLowerLimit) damage = total; + // Damage must be positive + if (damage < 0 && !targetProp.ignoreUpperLimit) damage = 0; + newValue = targetProp.total - damage; + // Write the results + result.mutations.push({ + targetIds: [targetId], + updates: [{ + propId: targetProp._id, + set: { damage, value: newValue }, + type: targetProp.type, + }], + contents: [{ + name: title, + value: `${getPropertyTitle(targetProp)} set from ${targetProp.value} to ${value}`, + inline: true, + ...task.silent && { silenced: true }, + }] + }); + if (targetId === action.creatureId) setScope(result, targetProp, newValue, damage); + } else if (operation === 'increment') { + const currentValue = targetProp.value || 0; + const currentDamage = targetProp.damage || 0; + increment = value; + // Can't increase damage above the remaining value + if (increment > currentValue && !targetProp.ignoreLowerLimit) increment = currentValue; + // Can't decrease damage below zero + if (-increment > currentDamage && !targetProp.ignoreUpperLimit) increment = -currentDamage; + damage = currentDamage + increment; + newValue = targetProp.total - damage; + // Write the results + result.mutations.push({ + targetIds: [targetId], + updates: [{ + propId: targetProp._id, + inc: { damage: increment, value: -increment }, + type: targetProp.type, + }], + contents: [{ + name: increment >= 0 ? 'Attribute damaged' : 'Attribute restored', + value: `${numberToSignedString(-increment)} ${getPropertyTitle(targetProp)}`, + inline: true, + ...task.silent && { silenced: true }, + }] + }); + if (targetId === action.creatureId) setScope(result, targetProp, newValue, damage); + } + await applyTriggers(action, targetProp, [targetId], 'damageTriggerIds.after', userInput); + await applyTriggers(action, targetProp, [targetId], 'damageTriggerIds.afterChildren', userInput); + return increment; +} + +// Update the scope with the attribute, but updated to the new value +// TODO ideally we re-write the getEffectiveActionScope code to be more +// getSomethingFromScope which does the same work, but for a single key, and includes all +// updates to the doc returned that are already applied in the result array +function setScope(result, targetProp, newValue, damage) { + // This isn't the defining property, don't bother + if (targetProp.overridden) return; + const key = targetProp.variableName; + // No variable name can't set scope + if (!key) return; + // Make sure scope is defined + if (!result.scope) result.scope = {}; + result.scope[key] = { + ...EJSON.clone(targetProp), + value: newValue, + damage, + }; +} diff --git a/app/imports/api/engine/action/tasks/applyItemAsAmmoTask.ts b/app/imports/api/engine/action/tasks/applyItemAsAmmoTask.ts new file mode 100644 index 00000000..6b6dde63 --- /dev/null +++ b/app/imports/api/engine/action/tasks/applyItemAsAmmoTask.ts @@ -0,0 +1,67 @@ +import { EngineAction } from '/imports/api/engine/action/EngineActions'; +import { + applyDefaultAfterPropTasks, applyAfterTasksSkipChildren, applyTriggers +} from '/imports/api/engine/action/functions/applyTaskGroups'; +import { + getEffectiveActionScope +} from '/imports/api/engine/action/functions/getEffectiveActionScope'; +import { ItemAsAmmoTask } from '/imports/api/engine/action/tasks/Task'; +import TaskResult from '/imports/api/engine/action/tasks/TaskResult'; +import { getPropertyChildren } from '/imports/api/engine/loadCreatures'; +import getPropertyTitle from '/imports/api/utility/getPropertyTitle'; + +export default async function applyItemAsAmmoTask(task: ItemAsAmmoTask, action: EngineAction, result: TaskResult, userInput): Promise { + const prop = task.prop; + const { item } = task.params + let { value } = task.params; + + if (item.type !== 'item') throw 'Must use an item as ammo'; + + // Store the ammo item and value in the scope + result.scope['#ammo'] = { propId: item._id }; + result.pushScope = { ['~ammoConsumed']: { value } }; + + // Apply the before triggers + await applyTriggers(action, item, task.targetIds, 'ammoTriggerIds.before', userInput); + + // Create a new result after before triggers have run + result = new TaskResult(task.targetIds); + action.results.push(result); + + // Refetch the scope properties + const scope = await getEffectiveActionScope(action); + result.popScope = { + '~ammoConsumed': 1, + }; + value = scope['~ammoConsumed']?.value || 0; + + const itemChildren = task.params.skipChildren ? [] : await getPropertyChildren(action.creatureId, item); + + // Do the quantity adjustment + // Check if property has quantity + result.mutations.push({ + targetIds: task.targetIds, + updates: [{ + propId: item._id, + inc: { quantity: -value }, + type: 'item', + }], + // Log the item name as a heading if it has child properties to apply + ...itemChildren.length && !task.params.skipChildren && { + contents: [{ + name: getPropertyTitle(item) || 'Ammo', + inline: false, + ...prop?.silent && { silenced: true }, + }] + }, + }); + + await applyTriggers(action, item, task.targetIds, 'ammoTriggerIds.after', userInput); + + if (task.params.skipChildren) { + await applyAfterTasksSkipChildren(action, item, task.targetIds, userInput); + } else { + await applyDefaultAfterPropTasks(action, item, task.targetIds, userInput); + } + return applyTriggers(action, item, task.targetIds, 'ammoTriggerIds.afterChildren', userInput); +} diff --git a/app/imports/api/engine/action/tasks/applyResetTask.ts b/app/imports/api/engine/action/tasks/applyResetTask.ts new file mode 100644 index 00000000..e6ddf0a5 --- /dev/null +++ b/app/imports/api/engine/action/tasks/applyResetTask.ts @@ -0,0 +1,171 @@ +import { EngineAction } from '/imports/api/engine/action/EngineActions'; +import InputProvider from '/imports/api/engine/action/functions/userInput/InputProvider'; +import { ResetTask } from '/imports/api/engine/action/tasks/Task'; +import TaskResult from '/imports/api/engine/action/tasks/TaskResult'; +import applyTask from '/imports/api/engine/action/tasks/applyTask'; +import { getCreature, getPropertiesByFilter, getPropertiesOfType } from '/imports/api/engine/loadCreatures'; +import getPropertyTitle from '/imports/api/utility/getPropertyTitle'; + +export default async function applyResetTask( + task: ResetTask, action: EngineAction, result: TaskResult, userInput: InputProvider +): Promise { + // Event name must be defined + if (!task.eventName) return; + + // This task can only be applied to a single target + if (task.targetIds.length !== 1) { + throw new Meteor.Error('wrong-number-of-targets', `Must reset the properties of a single creature at a time, ${task.targetIds.length} targets were provided`) + } + + // Print a title for rest events + switch (task.eventName) { + case 'shortRest': + result.appendLog({ + name: 'Short Rest', + silenced: task.silent ?? false, + }, task.targetIds); + break; + case 'longRest': + result.appendLog({ + name: 'Long Rest', + silenced: task.silent ?? false, + }, task.targetIds); + break; + } + + // Reset the properties by this event name + await resetProperties(task, action, result, userInput); + + // Reset hit dice on a long rest, starting with the highest dice + if (task.eventName === 'longRest') { + await resetHitDice(task, action, result, userInput); + } +} + +export async function resetProperties(task: ResetTask, action: EngineAction, result: TaskResult, userInput: InputProvider) { + const creatureId = task.targetIds[0]; + + // Long rests reset short rest properties as well + let mongoFilter: Mongo.Selector + if (task.eventName === 'longRest') { + mongoFilter = { reset: { $in: ['shortRest', 'longRest'] } } + } else { + mongoFilter = { reset: task.eventName }; + } + + const filterFn = (prop) => { + if (task.eventName === 'longRest') { + if (prop.reset !== 'longRest' && prop.reset !== 'shortRest') return false; + } else { + if (prop.reset !== task.eventName) return false; + } + return true; + } + + // Attributes + + const attributeFilter: Mongo.Selector = { + ...mongoFilter, + type: 'attribute', + damage: { $nin: [0, undefined] }, + } + + const attributeFilterFunction = (att) => { + if (att.type !== 'attribute') return false; + if (!filterFn(att)) return false; + if (att.damage === 0 || att.damage === undefined) return false; + return true; + } + + const attributes = getPropertiesByFilter(creatureId, attributeFilterFunction, attributeFilter); + + for (const prop of attributes) { + await applyTask(action, { + targetIds: [action.creatureId], + subtaskFn: 'damageProp', + params: { + title: getPropertyTitle(prop), + operation: 'increment', + value: -prop.damage || 0, + targetProp: prop, + }, + }, userInput); + } + + // Action-like properties + + const actionFilter = { + ...mongoFilter, + type: { + $in: ['action', 'spell'] + }, + usesUsed: { $nin: [0, undefined] }, + }; + + const actionFilterFunction = (prop) => { + if (prop.type !== 'action' && prop.type !== 'spell') return false; + if (!filterFn(prop)) return false; + if (prop.usesUsed === 0 || prop.usesUsed === undefined) return false; + return true; + } + + const actionProps = getPropertiesByFilter(creatureId, actionFilterFunction, actionFilter); + + for (const prop of actionProps) { + result.mutations.push({ + targetIds: [creatureId], + updates: [{ + propId: prop._id, + type: prop.type, + set: { usesUsed: 0 }, + }], + contents: [{ + name: prop.name, + value: prop.usesUsed >= 0 ? `Restored ${prop.usesUsed} uses` : `Removed ${-prop.usesUsed} uses` + }], + }); + } +} + +async function resetHitDice(task: ResetTask, action: EngineAction, result: TaskResult, userInput: InputProvider) { + const creatureId = task.targetIds[0]; + + const hitDice = getPropertiesOfType(creatureId, 'hitDice'); + + // Use a collator to do sorting in natural order + const collator = new Intl.Collator('en', { + numeric: true, sensitivity: 'base' + }); + + // Get the hit dice in decending order of hitDiceSize + const compare = (a, b) => collator.compare(b.hitDiceSize, a.hitDiceSize) + hitDice.sort(compare); + + // Get the total number of hit dice that can be recovered this rest + const totalHd = hitDice.reduce((sum, hd) => sum + (hd.total || 0), 0); + const creature = getCreature(creatureId); + const resetMultiplier = creature.settings.hitDiceResetMultiplier || 0.5; + let recoverableHd = Math.max(Math.floor(totalHd * resetMultiplier), 1); + + // recover each hit dice in turn until the recoverable amount is used up + let amountToRecover; + for (const hd of hitDice) { + if (!recoverableHd) return; + amountToRecover = Math.min(recoverableHd, hd.damage ?? 0); + if (!amountToRecover) return; + recoverableHd -= amountToRecover; + + // Apply the damage prop task + await applyTask(action, { + targetIds: [creatureId], + subtaskFn: 'damageProp', + params: { + title: getPropertyTitle(hd), + operation: 'increment', + value: -amountToRecover, + targetProp: hd, + }, + }, userInput); + + } +} diff --git a/app/imports/api/engine/action/tasks/applyTask.ts b/app/imports/api/engine/action/tasks/applyTask.ts new file mode 100644 index 00000000..494c1bf9 --- /dev/null +++ b/app/imports/api/engine/action/tasks/applyTask.ts @@ -0,0 +1,84 @@ +import { EngineAction } from '/imports/api/engine/action/EngineActions'; +import Task, { CheckTask, DamagePropTask, ItemAsAmmoTask, PropTask } from './Task'; +import TaskResult from '/imports/api/engine/action/tasks/TaskResult'; +import applyDamagePropTask from '/imports/api/engine/action/tasks/applyDamagePropTask'; +import applyItemAsAmmoTask from '/imports/api/engine/action/tasks/applyItemAsAmmoTask'; +import { getSingleProperty } from '/imports/api/engine/loadCreatures'; +import applyProperties from '/imports/api/engine/action/applyProperties'; +import InputProvider from '/imports/api/engine/action/functions/userInput/InputProvider'; +import applyCheckTask from '/imports/api/engine/action/tasks/applyCheckTask'; +import applyResetTask from '/imports/api/engine/action/tasks/applyResetTask'; +import applyCastSpellTask from '/imports/api/engine/action/tasks/applyCastSpellTask'; + +// DamagePropTask promises a number of actual damage done +export default async function applyTask( + action: EngineAction, task: DamagePropTask, inputProvider: InputProvider +): Promise + +// Other tasks promise nothing +export default async function applyTask( + action: EngineAction, task: PropTask | ItemAsAmmoTask | CheckTask, inputProvider: InputProvider +): Promise + +export default async function applyTask( + action: EngineAction, task: Task, inputProvider: InputProvider +): Promise + +export default async function applyTask( + action: EngineAction, task: Task, inputProvider: InputProvider +): Promise { + + // Pause and wait for the user if the action is being stepped through + if (action._isSimulation && action._stepThrough && inputProvider.nextStep) { + await inputProvider.nextStep(task); + } + + // Ensure no more than 100 tasks are performed by a single action + action.taskCount += 1; + if (action.taskCount > 100) throw 'Only 100 properties can be applied at once'; + + if (task.subtaskFn) { + const result = new TaskResult(task.targetIds); + action.results.push(result); + switch (task.subtaskFn) { + case 'damageProp': + return applyDamagePropTask(task, action, result, inputProvider); + case 'consumeItemAsAmmo': + return applyItemAsAmmoTask(task, action, result, inputProvider); + case 'check': + return applyCheckTask(task, action, result, inputProvider); + case 'reset': + return applyResetTask(task, action, result, inputProvider); + case 'castSpell': + return applyCastSpellTask(task, action, result, inputProvider); + default: + throw 'No case defined for the given subtaskFn'; + } + } else { + // Get property + const prop = task.prop; + + // Ensure the prop exists + if (!prop) throw new Meteor.Error('Not found', 'Property could not be found'); + + // If the property is deactivated by a toggle, skip it + if (prop.deactivatedByToggle) return; + + // Before triggers + if (prop.triggerIds?.before?.length) { + for (const triggerId of prop.triggerIds.before) { + const trigger = await getSingleProperty(action.creatureId, triggerId); + if (!trigger) continue; + await applyTask(action, { prop: trigger, targetIds: task.targetIds }, inputProvider); + } + } + + // Create a result an push it to the action results, pass it to the apply function to modify + const result = new TaskResult(task.targetIds); + result.scope[`#${prop.type}`] = { _propId: prop._id }; + action.results.push(result); + + // Apply the property + return applyProperties[prop.type]?.(task, action, result, inputProvider); + } +} diff --git a/app/imports/api/engine/actions/ActionContext.js b/app/imports/api/engine/actions/ActionContext.js deleted file mode 100644 index 9c5e2e50..00000000 --- a/app/imports/api/engine/actions/ActionContext.js +++ /dev/null @@ -1,78 +0,0 @@ -import { CreatureLogSchema, insertCreatureLogWork } from '/imports/api/creature/log/CreatureLogs.js'; -import { - getCreature, getVariables, getPropertiesOfType -} from '/imports/api/engine/loadCreatures.js'; -import { groupBy, remove } from 'lodash'; - -export default class ActionContext{ - constructor(creatureId, targetIds = [], method) { - // Get the creature - this.creature = getCreature(creatureId) - - if (!this.creature) { - throw new Meteor.Error('No Creature', `No creature could be found with id: ${creatureId}`) - } - // Create a log - this.log = CreatureLogSchema.clean({ - creatureId: creatureId, - creatureName: this.creature.name, - }); - - // Get the variables of the acting creature - this.creature.variables = getVariables(creatureId); - delete this.creature.variables._id; - delete this.creature.variables._creatureId; - // Alias as scope - this.scope = this.creature.variables; - - // Get the targets and their variables - this.targets = []; - targetIds.forEach(targetId => { - let target; - if (targetId === creatureId) { - target = this.creature; - } else { - target = getCreature(targetId); - target.variables = getVariables(targetId); - delete target.variables._id; - delete target.variables._creatureId; - } - this.targets.push(target); - }); - - // Store a reference to the method for inserting the log - this.method = method; - - // Get triggers - this.triggers = getPropertiesOfType(creatureId, 'trigger'); - // Remove deleted or inactive triggers - remove(this.triggers, trigger => trigger.removed || trigger.inactive); - // Sort triggers by order - this.triggers.sort((a, b) => a.order - b.order); - // Group the triggers into triggers.. or - // triggers.doActionProperty.. - this.triggers = groupBy(this.triggers, 'event'); - for (let event in this.triggers) { - if (event === 'doActionProperty') { - this.triggers[event] = groupBy(this.triggers[event], 'actionPropertyType'); - for (let propertyType in this.triggers[event]) { - this.triggers[event][propertyType] = groupBy(this.triggers[event][propertyType], 'timing'); - } - } else { - this.triggers[event] = groupBy(this.triggers[event], 'timing'); - } - } - } - addLog(content) { - if (content.name || content.value){ - this.log.content.push(content); - } - } - writeLog() { - insertCreatureLogWork({ - log: this.log, - creature: this.creature, - method: this.method, - }); - } -} \ No newline at end of file diff --git a/app/imports/api/engine/actions/applyProperty.js b/app/imports/api/engine/actions/applyProperty.js deleted file mode 100644 index 4dc1cb79..00000000 --- a/app/imports/api/engine/actions/applyProperty.js +++ /dev/null @@ -1,31 +0,0 @@ -import action from './applyPropertyByType/applyAction.js'; -import adjustment from './applyPropertyByType/applyAdjustment.js'; -import branch from './applyPropertyByType/applyBranch.js'; -import buff from './applyPropertyByType/applyBuff.js'; -import buffRemover from './applyPropertyByType/applyBuffRemover.js'; -import damage from './applyPropertyByType/applyDamage.js'; -import folder from './applyPropertyByType/applyFolder.js'; -import note from './applyPropertyByType/applyNote.js'; -import roll from './applyPropertyByType/applyRoll.js'; -import savingThrow from './applyPropertyByType/applySavingThrow.js'; -import toggle from './applyPropertyByType/applyToggle.js'; - -const applyPropertyByType = { - action, - adjustment, - branch, - buff, - buffRemover, - damage, - folder, - note, - roll, - savingThrow, - spell: action, - toggle, -}; - -export default function applyProperty(node, actionContext, ...rest) { - actionContext.scope[`#${node.node.type}`] = node.node; - applyPropertyByType[node.node.type]?.(node, actionContext, ...rest); -} diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyAction.js b/app/imports/api/engine/actions/applyPropertyByType/applyAction.js deleted file mode 100644 index becf0775..00000000 --- a/app/imports/api/engine/actions/applyPropertyByType/applyAction.js +++ /dev/null @@ -1,305 +0,0 @@ -import recalculateInlineCalculations from './shared/recalculateInlineCalculations.js'; -import recalculateCalculation from './shared/recalculateCalculation.js'; -import rollDice from '/imports/parser/rollDice.js'; -import applyProperty from '../applyProperty.js'; -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import { adjustQuantityWork } from '/imports/api/creature/creatureProperties/methods/adjustQuantity.js'; -import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js'; -import numberToSignedString from '/imports/ui/utility/numberToSignedString.js'; -import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js'; -import { resetProperties } from '/imports/api/creature/creatures/methods/restCreature.js'; - -export default function applyAction(node, actionContext) { - applyNodeTriggers(node, 'before', actionContext); - const prop = node.node; - if (prop.target === 'self') actionContext.targets = [actionContext.creature]; - const targets = actionContext.targets; - - // Log the name and summary - let content = { name: prop.name }; - if (prop.summary?.text) { - recalculateInlineCalculations(prop.summary, actionContext); - content.value = prop.summary.value; - } - if (!prop.silent) actionContext.addLog(content); - - // Spend the resources - const failed = spendResources(prop, actionContext); - if (failed) return; - - const attack = prop.attackRoll || prop.attackRollBonus; - - // Attack if there is an attack roll - if (attack && attack.calculation) { - if (targets.length) { - targets.forEach(target => { - applyAttackToTarget({ attack, target, actionContext }); - // Apply the children, but only to the current target - actionContext.targets = [target]; - applyChildren(node, actionContext); - }); - } else { - applyAttackWithoutTarget({ attack, actionContext }); - applyChildren(node, actionContext); - } - } else { - applyChildren(node, actionContext); - } - if (prop.actionType === 'event' && prop.variableName) { - resetProperties(actionContext.creature._id, prop.variableName, actionContext); - } -} - -function applyAttackWithoutTarget({ attack, actionContext }) { - delete actionContext.scope['$attackHit']; - delete actionContext.scope['$attackMiss']; - delete actionContext.scope['$criticalHit']; - delete actionContext.scope['$criticalMiss']; - delete actionContext.scope['$attackRoll']; - - recalculateCalculation(attack, actionContext); - const scope = actionContext.scope; - let { - resultPrefix, - result, - criticalHit, - criticalMiss, - } = rollAttack(attack, scope); - let name = criticalHit ? 'Critical Hit!' : criticalMiss ? 'Critical Miss!' : 'To Hit'; - if (scope['$attackAdvantage'] === 1) { - name += ' (Advantage)'; - } else if (scope['$attackAdvantage'] === -1) { - name += ' (Disadvantage)'; - } - if (!criticalMiss) { - scope['$attackHit'] = { value: true } - } - if (!criticalHit) { - scope['$attackMiss'] = { value: true }; - } - - actionContext.addLog({ - name, - value: `${resultPrefix}\n**${result}**`, - inline: true, - }); -} - -function applyAttackToTarget({ attack, target, actionContext }) { - const scope = actionContext.scope; - delete scope['$attackHit']; - delete scope['$attackMiss']; - delete scope['$criticalHit']; - delete scope['$criticalMiss']; - delete scope['$attackDiceRoll']; - delete scope['$attackRoll']; - - recalculateCalculation(attack, actionContext); - - let { - resultPrefix, - result, - criticalHit, - criticalMiss, - } = rollAttack(attack, scope); - - if (target.variables.armor) { - const armor = target.variables.armor.value; - - let name = criticalHit ? 'Critical Hit!' : - criticalMiss ? 'Critical Miss!' : - result > armor ? 'Hit!' : 'Miss!'; - if (scope['$attackAdvantage'] === 1) { - name += ' (Advantage)'; - } else if (scope['$attackAdvantage'] === -1) { - name += ' (Disadvantage)'; - } - - actionContext.addLog({ - name, - value: `${resultPrefix}\n**${result}**`, - inline: true, - }); - if (criticalMiss || result < armor) { - scope['$attackMiss'] = { value: true }; - } else { - scope['$attackHit'] = { value: true }; - } - } else { - actionContext.addLog({ - name: 'Error', - value: 'Target has no `armor`', - }); - actionContext.addLog({ - name: criticalHit ? 'Critical Hit!' : criticalMiss ? 'Critical Miss!' : 'To Hit', - value: `${resultPrefix}\n**${result}**`, - inline: true, - }); - } -} - -function rollAttack(attack, scope) { - const rollModifierText = numberToSignedString(attack.value, true); - let value, resultPrefix; - if (scope['$attackAdvantage'] === 1) { - const [a, b] = rollDice(2, 20); - if (a >= b) { - value = a; - resultPrefix = `1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText}`; - } else { - value = b; - resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`; - } - } else if (scope['$attackAdvantage'] === -1) { - const [a, b] = rollDice(2, 20); - if (a <= b) { - value = a; - resultPrefix = `1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText}`; - } else { - value = b; - resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`; - } - } else { - value = rollDice(1, 20)[0]; - resultPrefix = `1d20 [${value}] ${rollModifierText}` - } - scope['$attackRoll'] = { value }; - const result = value + attack.value; - const { criticalHit, criticalMiss } = applyCrits(value, scope); - return { resultPrefix, result, value, criticalHit, criticalMiss }; -} - -function applyCrits(value, scope) { - let criticalHitTarget = scope.criticalHitTarget?.value || 20; - let criticalHit = value >= criticalHitTarget; - let criticalMiss; - if (criticalHit) { - scope['$criticalHit'] = { value: true }; - } else { - criticalMiss = value === 1; - if (criticalMiss) { - scope['$criticalMiss'] = { value: true }; - } - } - return { criticalHit, criticalMiss }; -} - -function applyChildren(node, actionContext) { - applyNodeTriggers(node, 'after', actionContext); - node.children.forEach(child => applyProperty(child, actionContext)); -} - -function spendResources(prop, actionContext) { - // Check Uses - if (prop.usesLeft <= 0) { - if (!prop.silent) actionContext.addLog({ - name: 'Error', - value: `${prop.name || 'action'} does not have enough uses left`, - }); - return true; - } - // Resources - if (prop.insufficientResources) { - if (!prop.silent) actionContext.addLog({ - name: 'Error', - value: 'This creature doesn\'t have sufficient resources to perform this action', - }); - return true; - } - // Items - let itemQuantityAdjustments = []; - let spendLog = []; - let gainLog = []; - try { - prop.resources.itemsConsumed.forEach(itemConsumed => { - recalculateCalculation(itemConsumed.quantity, actionContext); - if (!itemConsumed.itemId) { - throw 'No ammo was selected for this prop'; - } - let item = CreatureProperties.findOne(itemConsumed.itemId); - if (!item || item.ancestors[0].id !== prop.ancestors[0].id) { - throw 'The prop\'s ammo was not found on the creature'; - } - if (!item.equipped) { - throw 'The selected ammo is not equipped'; - } - if ( - !itemConsumed.quantity.value || - !isFinite(itemConsumed.quantity.value) - ) return; - itemQuantityAdjustments.push({ - property: item, - operation: 'increment', - value: itemConsumed.quantity.value, - }); - let logName = item.name; - if (itemConsumed.quantity.value > 1 || itemConsumed.quantity.value < -1) { - logName = item.plural || logName; - } - if (itemConsumed.quantity.value > 0) { - spendLog.push(logName + ': ' + itemConsumed.quantity.value); - } else if (itemConsumed.quantity.value < 0) { - gainLog.push(logName + ': ' + -itemConsumed.quantity.value); - } - }); - } catch (e) { - actionContext.addLog({ - name: 'Error', - value: e, - }); - return true; - } - // No more errors should be thrown after this line - // Now that we have confirmed that there are no errors, do actual work - //Items - itemQuantityAdjustments.forEach(adjustQuantityWork); - - // Use uses - if (prop.usesLeft) { - CreatureProperties.update(prop._id, { - $inc: { usesUsed: 1 } - }, { - selector: prop - }); - if (!prop.silent) actionContext.addLog({ - name: 'Uses left', - value: prop.usesLeft - 1, - inline: true, - }); - } - - // Damage stats - prop.resources.attributesConsumed.forEach(attConsumed => { - recalculateCalculation(attConsumed.quantity, actionContext); - - if (!attConsumed.quantity?.value) return; - let stat = actionContext.scope[attConsumed.variableName]; - if (!stat) { - spendLog.push(stat.name + ': ' + ' not found'); - return; - } - damagePropertyWork({ - prop: stat, - operation: 'increment', - value: attConsumed.quantity.value, - actionContext, - }); - if (attConsumed.quantity.value > 0) { - spendLog.push(stat.name + ': ' + attConsumed.quantity.value); - } else if (attConsumed.quantity.value < 0) { - gainLog.push(stat.name + ': ' + -attConsumed.quantity.value); - } - }); - - // Log all the spending - if (gainLog.length && !prop.silent) actionContext.addLog({ - name: 'Gained', - value: gainLog.join('\n'), - inline: true, - }); - if (spendLog.length && !prop.silent) actionContext.addLog({ - name: 'Spent', - value: spendLog.join('\n'), - inline: true, - }); -} diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyAdjustment.js b/app/imports/api/engine/actions/applyPropertyByType/applyAdjustment.js deleted file mode 100644 index ede94665..00000000 --- a/app/imports/api/engine/actions/applyPropertyByType/applyAdjustment.js +++ /dev/null @@ -1,61 +0,0 @@ -import applyProperty from '../applyProperty.js'; -import recalculateCalculation from './shared/recalculateCalculation.js'; -import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js'; -import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js'; - -export default function applyAdjustment(node, actionContext){ - applyNodeTriggers(node, 'before', actionContext); - const prop = node.node; - const damageTargets = prop.target === 'self' ? [actionContext.creature] : actionContext.targets; - - if (!prop.amount) { - return applyChildren(node, actionContext); - } - - // Evaluate the amount - recalculateCalculation(prop.amount, actionContext); - - const value = +prop.amount.value; - if (!isFinite(value)) { - return applyChildren(node, actionContext); - } - - if (damageTargets?.length) { - damageTargets.forEach(target => { - let stat = target.variables[prop.stat]; - if (!stat?.type) { - if (!prop.silent) actionContext.addLog({ - name: 'Error', - value: `Could not apply attribute damage, creature does not have \`${prop.stat}\` set` - }); - return applyChildren(node, actionContext); - } - damagePropertyWork({ - prop: stat, - operation: prop.operation, - value, - actionContext, - }); - if (!prop.silent) actionContext.addLog({ - name: 'Attribute damage', - value: `${prop.stat}${prop.operation === 'set' ? ' set to' : ''}` + - ` ${value}`, - inline: true, - }); - }); - } else { - if (!prop.silent) actionContext.addLog({ - name: 'Attribute damage', - value: `${prop.stat}${prop.operation === 'set' ? ' set to' : ''}` + - ` ${value}`, - inline: true, - }); - } - - return applyChildren(node, actionContext); -} - -function applyChildren(node, actionContext){ - applyNodeTriggers(node, 'after', actionContext); - node.children.forEach(child => applyProperty(child, actionContext)); -} diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyBranch.js b/app/imports/api/engine/actions/applyPropertyByType/applyBranch.js deleted file mode 100644 index ce918c4a..00000000 --- a/app/imports/api/engine/actions/applyPropertyByType/applyBranch.js +++ /dev/null @@ -1,80 +0,0 @@ -import applyProperty from '../applyProperty.js'; -import recalculateCalculation from './shared/recalculateCalculation.js'; -import rollDice from '/imports/parser/rollDice.js'; -import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js'; - -export default function applyBranch(node, actionContext){ - applyNodeTriggers(node, 'before', actionContext); - const applyChildren = function(){ - applyNodeTriggers(node, 'after', actionContext); - node.children.forEach(child => applyProperty(child, actionContext)); - }; - const scope = actionContext.scope; - const targets = actionContext.targets; - const prop = node.node; - switch(prop.branchType){ - case 'if': - recalculateCalculation(prop.condition, actionContext); - if (prop.condition?.value) applyChildren(); - break; - case 'index': - if (node.children.length){ - recalculateCalculation(prop.condition, actionContext); - if (!isFinite(prop.condition?.value)) { - actionContext.addLog({ - name: 'Branch Error', - value: 'Index did not resolve into a valid number' - }); - break; - } - let index = Math.floor(prop.condition?.value); - if (index < 1) index = 1; - if (index > node.children.length) index = node.children.length; - applyNodeTriggers(node, 'after', actionContext); - applyProperty(node.children[index - 1], actionContext); - } - break; - case 'hit': - if (scope['$attackHit']?.value){ - 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**'}); - applyChildren(); - } - break; - case 'failedSave': - 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**',}); - applyChildren(); - } - break; - case 'random': - if (node.children.length){ - let index = rollDice(1, node.children.length)[0] - 1; - applyNodeTriggers(node, 'after', actionContext); - applyProperty(node.children[index], actionContext); - } - break; - case 'eachTarget': - if (targets.length) { - targets.forEach(target => { - applyNodeTriggers(node, 'after', actionContext); - actionContext.targets = [target] - node.children.forEach(child => applyProperty(child, actionContext)); - }); - } else { - applyChildren(); - } - break; - } -} diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyBuff.js b/app/imports/api/engine/actions/applyPropertyByType/applyBuff.js deleted file mode 100644 index 4e68eabe..00000000 --- a/app/imports/api/engine/actions/applyPropertyByType/applyBuff.js +++ /dev/null @@ -1,171 +0,0 @@ -import { - setLineageOfDocs, - renewDocIds -} from '/imports/api/parenting/parenting.js'; -import { setDocToLastOrder } from '/imports/api/parenting/order.js'; -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import computedSchemas from '/imports/api/properties/computedPropertySchemasIndex.js'; -import applyFnToKey from '/imports/api/engine/computation/utility/applyFnToKey.js'; -import { get } from 'lodash'; -import resolve, { map, toString } from '/imports/parser/resolve.js'; -import symbol from '/imports/parser/parseTree/symbol.js'; -import logErrors from './shared/logErrors.js'; -import { insertCreatureLog } from '/imports/api/creature/log/CreatureLogs.js'; -import cyrb53 from '/imports/api/engine/computation/utility/cyrb53.js'; -import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js'; -import INLINE_CALCULATION_REGEX from '/imports/constants/INLINE_CALCULTION_REGEX.js'; - -export default function applyBuff(node, actionContext) { - applyNodeTriggers(node, 'before', actionContext); - const prop = node.node; - let buffTargets = prop.target === 'self' ? [actionContext.creature] : actionContext.targets; - - // Then copy the decendants of the buff to the targets - let propList = [prop]; - function addChildrenToPropList(children, { skipCrystalize } = {}) { - children.forEach(child => { - if (skipCrystalize) child.node._skipCrystalize = true; - propList.push(child.node); - // recursively add the child's children, but don't crystalize nested buffs - addChildrenToPropList(child.children, { - skipCrystalize: skipCrystalize || child.node.type === 'buff' - }); - }); - } - addChildrenToPropList(node.children); - if (!prop.skipCrystalization) { - crystalizeVariables({ propList, actionContext }); - } - - let oldParent = { - id: prop.parent.id, - collection: prop.parent.collection, - }; - buffTargets.forEach(target => { - // Apply the buff - copyNodeListToTarget(propList, target, oldParent); - - //Log the buff - if ((prop.name || prop.description?.value) && !prop.silent) { - if (target._id === actionContext.creature._id) { - // Targeting self - actionContext.addLog({ - name: prop.name, - value: prop.description?.value, - }); - } else { - // Targeting other - insertCreatureLog.call({ - log: { - creatureId: target._id, - content: [{ - name: prop.name, - value: prop.description?.value, - }], - } - }); - } - } - }); - applyNodeTriggers(node, 'after', actionContext); - - // Don't apply the children of the buff, they get copied to the target instead -} - -function copyNodeListToTarget(propList, target, oldParent) { - let ancestry = [{ collection: 'creatures', id: target._id }]; - setLineageOfDocs({ - docArray: propList, - newAncestry: ancestry, - oldParent, - }); - renewDocIds({ - docArray: propList, - }); - setDocToLastOrder({ - collection: CreatureProperties, - doc: propList[0], - }); - CreatureProperties.batchInsert(propList); -} - -/** - * Replaces all variables with their resolved values - * except variables of the form `$target.thing.total` become `thing.total` - */ -function crystalizeVariables({ propList, actionContext }) { - propList.forEach(prop => { - if (prop._skipCrystalize) { - delete prop._skipCrystalize; - return; - } - // Iterate through all the calculations and crystalize them - computedSchemas[prop.type].computedFields().forEach(calcKey => { - applyFnToKey(prop, calcKey, (prop, key) => { - const calcObj = get(prop, key); - if (!calcObj?.parseNode) return; - calcObj.parseNode = map(calcObj.parseNode, node => { - // Skip nodes that aren't symbols or accessors - if ( - node.parseType !== 'accessor' && node.parseType !== 'symbol' - ) return node; - // Handle variables - if (node.name === '$target') { - // strip $target - if (node.parseType === 'accessor') { - node.name = node.path.shift(); - if (!node.path.length) { - return symbol.create({ name: node.name }) - } - } else { - // Can't strip symbols - actionContext.addLog({ - name: 'Error', - value: 'Variable `$target` should not be used without a property: $target.property', - }); - } - return node; - } else { - // Resolve all other variables - const { result, context } = resolve('reduce', node, actionContext.scope); - logErrors(context.errors, actionContext); - return result; - } - }); - calcObj.calculation = toString(calcObj.parseNode); - calcObj.hash = cyrb53(calcObj.calculation); - }); - }); - // For each key in the schema - computedSchemas[prop.type].inlineCalculationFields().forEach(calcKey => { - // That ends in .inlineCalculations - applyFnToKey(prop, calcKey, (prop, key) => { - const inlineCalcObj = get(prop, key); - if (!inlineCalcObj) return; - - // If there is no text, skip - if (!inlineCalcObj.text) { - return; - } - - // Replace all the existing calculations - let index = -1; - inlineCalcObj.text = inlineCalcObj.text.replace(INLINE_CALCULATION_REGEX, () => { - index += 1; - return `{${inlineCalcObj.inlineCalculations[index].calculation}}`; - }); - - // Set the value to the uncomputed string - inlineCalcObj.value = inlineCalcObj.text; - - // Write a new hash - const inlineCalcHash = cyrb53(inlineCalcObj.text); - if (inlineCalcHash === inlineCalcObj.hash) { - // Skip if nothing changed - return; - } - inlineCalcObj.hash = inlineCalcHash; - }); - }); - }); -} diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyBuffRemover.js b/app/imports/api/engine/actions/applyPropertyByType/applyBuffRemover.js deleted file mode 100644 index 86b60949..00000000 --- a/app/imports/api/engine/actions/applyPropertyByType/applyBuffRemover.js +++ /dev/null @@ -1,101 +0,0 @@ -import { findLast, difference, intersection, filter } from 'lodash'; -import applyProperty from '../applyProperty.js'; -import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js'; -import { getProperyAncestors, getPropertiesOfType } from '/imports/api/engine/loadCreatures.js'; -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import { softRemove } from '/imports/api/parenting/softRemove.js'; -import getEffectivePropTags from '/imports/api/engine/computation/utility/getEffectivePropTags.js'; - -export default function applyBuffRemover(node, actionContext) { - // Apply triggers - applyNodeTriggers(node, 'before', actionContext); - - const prop = node.node; - - // Log Name - if (prop.name && !prop.silent){ - actionContext.addLog({ name: prop.name }); - } - - // Remove buffs - if (prop.targetParentBuff) { - // Remove nearest ancestor buff - const ancestors = getProperyAncestors(actionContext.creature._id, prop._id); - const nearestBuff = findLast(ancestors, ancestor => ancestor.type === 'buff'); - if (!nearestBuff) { - actionContext.addLog({ - name: 'Error', - value: 'Buff remover does not have a parent buff to remove', - }); - return; - } - removeBuff(nearestBuff, actionContext, prop); - } else { - // Get all the buffs targeted by tags - const allBuffs = getPropertiesOfType(actionContext.creature._id, 'buff'); - const targetedBuffs = filter(allBuffs, buff => { - if (buff.inactive) return false; - if (buffRemoverMatchTags(prop, buff)) return true; - }); - // Remove the buffs - if (prop.removeAll) { - // Remove all matching buffs - targetedBuffs.forEach(buff => { - removeBuff(buff, actionContext, prop); - }); - } else { - // Sort in reverse order - targetedBuffs.sort((a, b) => b.order - a.order); - // Remove the one with the highest order - const buff = targetedBuffs[0]; - if (buff) { - removeBuff(buff, actionContext, prop); - } - } - } - - // Apply triggers - applyNodeTriggers(node, 'after', actionContext); - // Apply children - node.children.forEach(child => applyProperty(child, actionContext)); -} - -function removeBuff(buff, actionContext, prop) { - if (!prop.silent) actionContext.addLog({ - name: 'Removed', - value: `${buff.name || 'Buff'}` - }); - softRemove({ _id: buff._id, collection: CreatureProperties }); -} - -function buffRemoverMatchTags(buffRemover, prop) { - let matched = false; - const propTags = getEffectivePropTags(prop); - // Check the target tags - if ( - !buffRemover.targetTags?.length || - difference(buffRemover.targetTags, propTags).length === 0 - ) { - matched = true; - } - // Check the extra tags - buffRemover.extraTags?.forEach(extra => { - if (extra.operation === 'OR') { - if (matched) return; - if ( - !extra.tags.length || - difference(extra.tags, propTags).length === 0 - ) { - matched = true; - } - } else if (extra.operation === 'NOT') { - if ( - extra.tags.length && - intersection(extra.tags, propTags) - ) { - return false; - } - } - }); - return matched; -} diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyDamage.js b/app/imports/api/engine/actions/applyPropertyByType/applyDamage.js deleted file mode 100644 index f1c0c832..00000000 --- a/app/imports/api/engine/actions/applyPropertyByType/applyDamage.js +++ /dev/null @@ -1,252 +0,0 @@ -import { some, intersection, difference, remove, includes } from 'lodash'; -import applyProperty from '../applyProperty.js'; -import { insertCreatureLog } from '/imports/api/creature/log/CreatureLogs.js'; -import resolve, { Context, toString } from '/imports/parser/resolve.js'; -import logErrors from './shared/logErrors.js'; -import applyEffectsToCalculationParseNode from '/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js'; -import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js'; -import { - getPropertiesOfType -} from '/imports/api/engine/loadCreatures.js'; -import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js'; - -export default function applyDamage(node, actionContext) { - applyNodeTriggers(node, 'before', actionContext); - const applyChildren = function () { - applyNodeTriggers(node, 'after', actionContext); - node.children.forEach(child => applyProperty(child, actionContext)); - }; - - const prop = node.node; - const scope = actionContext.scope; - - // Skip if there is no parse node to work with - if (!prop.amount?.parseNode) return; - - // Choose target - let damageTargets = prop.target === 'self' ? [actionContext.creature] : actionContext.targets; - // Determine if the hit is critical - let criticalHit = scope['$criticalHit']?.value && - prop.damageType !== 'healing' // Can't critically heal - ; - // Double the damage rolls if the hit is critical - let context = new Context({ - options: { doubleRolls: criticalHit }, - }); - - // Gather all the lines we need to log into an array - const logValue = []; - const logName = prop.damageType === 'healing' ? 'Healing' : 'Damage'; - - // roll the dice only and store that string - applyEffectsToCalculationParseNode(prop.amount, actionContext.log); - const { result: rolled } = resolve('roll', prop.amount.parseNode, scope, context); - if (rolled.parseType !== 'constant') { - logValue.push(toString(rolled)); - } - logErrors(context.errors, actionContext); - - // Reset the errors so we don't log the same errors twice - context.errors = []; - - // Resolve the roll to a final value - const { result: reduced } = resolve('reduce', rolled, scope, context); - logErrors(context.errors, actionContext); - - // Store the result - if (reduced.parseType === 'constant') { - prop.amount.value = reduced.value; - } else if (reduced.parseType === 'error') { - prop.amount.value = null; - } else { - prop.amount.value = toString(reduced); - } - let damage = +reduced.value; - - // If we didn't end up with a constant of finite amount, give up - if (reduced?.parseType !== 'constant' || !isFinite(reduced.value)) { - return applyChildren(); - } - - // Round the damage to a whole number - damage = Math.floor(damage); - - // Convert extra damage into the stored type - if (prop.damageType === 'extra' && scope['$lastDamageType']) { - prop.damageType = scope['$lastDamageType']; - } - // Store current damage type - if (prop.damageType !== 'healing') { - scope['$lastDamageType'] = prop.damageType; - } - - // Memoise the damage suffix for the log - let suffix = (criticalHit ? ' critical ' : ' ') + - prop.damageType + - (prop.damageType !== 'healing' ? ' damage ' : ''); - - if (damageTargets && damageTargets.length) { - // Iterate through all the targets - damageTargets.forEach(target => { - - // Apply weaknesses/resistances/immunities - damage = applyDamageMultipliers({ - target, - damage, - damageProp: prop, - logValue - }); - - actionContext.target = [target]; - // Deal the damage to the target - let damageDealt = dealDamage({ - target, - damageType: prop.damageType, - amount: damage, - actionContext - }); - - // Log the damage done - if (target._id === actionContext.creature._id) { - // Target is same as self, log damage as such - logValue.push(`**${damageDealt}** ${suffix} to self`); - } else { - logValue.push(`Dealt **${damageDealt}** ${suffix} ${target.name && ' to '}${target.name}`); - // Log the damage received on that creature's log as well - insertCreatureLog.call({ - log: { - creatureId: target._id, - content: [{ - name, - value: `Recieved **${damageDealt}** ${suffix}`, - }], - } - }); - } - }); - } else { - // There are no targets, just log the result - logValue.push(`**${damage}** ${suffix}`); - } - if (!prop.silent) actionContext.addLog({ - name: logName, - value: logValue.join('\n'), - inline: true, - }); - return applyChildren(); -} - -function applyDamageMultipliers({ target, damage, damageProp, logValue }) { - const damageType = damageProp?.damageType; - if (!damageType) return damage; - - const multiplier = target?.variables?.[damageType]; - if (!multiplier) return damage; - - const damageTypeText = damageType == 'healing' ? 'healing' : `${damageType} damage`; - - if ( - multiplier.immunity && - some(multiplier.immunities, multiplierAppliesTo(damageProp, 'immunity')) - ) { - logValue.push(`Immune to ${damageTypeText}`); - return 0; - } else { - if ( - multiplier.resistance && - some(multiplier.resistances, multiplierAppliesTo(damageProp, 'resistance')) - ) { - logValue.push(`Resistant to ${damageTypeText}`); - damage = Math.floor(damage / 2); - } - if ( - multiplier.vulnerability && - some(multiplier.vulnerabilities, multiplierAppliesTo(damageProp, 'vulnerability')) - ) { - logValue.push(`Vulnerable to ${damageTypeText}`); - damage = Math.floor(damage * 2); - } - } - return damage; -} - -function multiplierAppliesTo(damageProp, multiplierType) { - return multiplier => { - // Apply the default 'ignore x' tags - if (includes(damageProp.tags, `ignore ${multiplierType}`)) return false; - - const hasRequiredTags = difference( - multiplier.includeTags, damageProp.tags - ).length === 0; - - const hasNoExcludedTags = intersection( - multiplier.excludeTags, damageProp.tags - ).length === 0; - - return hasRequiredTags && hasNoExcludedTags; - } -} - -function dealDamage({ target, damageType, amount, actionContext }) { - // Get all the health bars and do damage to them - let healthBars = getPropertiesOfType(target._id, 'attribute'); - - // Keep only the healthbars that can take damage/healing - remove(healthBars, (bar) => - bar.attributeType !== 'healthBar' || - bar.inactive || - bar.removed || - bar.overridden || - (amount >= 0 && bar.healthBarNoDamage) || - (amount < 0 && bar.healthBarNoHealing) - ); - - // Sort healthbars by damage/healing order or tree order as a fallback - healthBars.sort((a, b) => { - let diff; - if (amount >= 0) { - diff = a.healthBarDamageOrder - b.healthBarDamageOrder; - } else { - diff = a.healthBarHealingOrder - b.healthBarHealingOrder; - } - if (Number.isFinite(diff)) { - return diff; - } else { - return a.order - b.order; - } - }); - - // Deal the damage to each healthbar in order until all damage is done - const totalDamage = amount; - let damageLeft = totalDamage; - if (damageType === 'healing') damageLeft = -totalDamage; - healthBars.forEach(healthBar => { - if (damageLeft === 0) return; - // Replace the healthbar by the one in the action context if we can - // The damagePropertyWork function bashes the prop with the damage - // So we can use the new value in later action properties - if (healthBar.variableName) { - const targetHealthBar = target.variables[healthBar.variableName]; - if (targetHealthBar?._id === healthBar._id) { - healthBar = targetHealthBar; - } - } - // Do the damage - let damageAdded = damagePropertyWork({ - prop: healthBar, - operation: 'increment', - value: damageLeft, - actionContext - }); - damageLeft -= damageAdded; - // Prevent overflow - if ( - damageType === 'healing' ? - healthBar.healthBarNoHealingOverflow : - healthBar.healthBarNoDamageOverflow - ) { - damageLeft = 0; - } - }); - return totalDamage; -} diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyFolder.js b/app/imports/api/engine/actions/applyPropertyByType/applyFolder.js deleted file mode 100644 index 0965f56d..00000000 --- a/app/imports/api/engine/actions/applyPropertyByType/applyFolder.js +++ /dev/null @@ -1,11 +0,0 @@ -import recalculateInlineCalculations from './shared/recalculateInlineCalculations.js'; -import applyProperty from '../applyProperty.js'; -import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js'; - -export default function applyFolder(node, actionContext) { - // Apply triggers - applyNodeTriggers(node, 'before', actionContext); - applyNodeTriggers(node, 'after', actionContext); - // Apply children - node.children.forEach(child => applyProperty(child, actionContext)); -} diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyNote.js b/app/imports/api/engine/actions/applyPropertyByType/applyNote.js deleted file mode 100644 index 0d5f9e84..00000000 --- a/app/imports/api/engine/actions/applyPropertyByType/applyNote.js +++ /dev/null @@ -1,27 +0,0 @@ -import recalculateInlineCalculations from './shared/recalculateInlineCalculations.js'; -import applyProperty from '../applyProperty.js'; -import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js'; - -export default function applyNote(node, actionContext){ - applyNodeTriggers(node, 'before', actionContext); - const prop = node.node; - - // Log Name, summary - let content = { name: prop.name }; - if (prop.summary?.text){ - recalculateInlineCalculations(prop.summary, actionContext); - content.value = prop.summary.value; - } - if (content.name || content.value){ - actionContext.addLog(content); - } - // Log description - if (prop.description?.text){ - recalculateInlineCalculations(prop.description, actionContext); - actionContext.addLog({value: prop.description.value}); - } - // Apply triggers - applyNodeTriggers(node, 'after', actionContext); - // Apply children - node.children.forEach(child => applyProperty(child, actionContext)); -} diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyRoll.js b/app/imports/api/engine/actions/applyPropertyByType/applyRoll.js deleted file mode 100644 index 7d860948..00000000 --- a/app/imports/api/engine/actions/applyPropertyByType/applyRoll.js +++ /dev/null @@ -1,61 +0,0 @@ -import applyProperty from '../applyProperty.js'; -import logErrors from './shared/logErrors.js'; -import applyEffectsToCalculationParseNode from '/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js'; -import resolve, { toString } from '/imports/parser/resolve.js'; -import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js'; - -export default function applyRoll(node, actionContext){ - applyNodeTriggers(node, 'before', actionContext); - const prop = node.node; - - const applyChildren = function(){ - applyNodeTriggers(node, 'after', actionContext); - node.children.forEach(child => applyProperty(child, actionContext)); - }; - - if (prop.roll?.calculation){ - const logValue = []; - - // roll the dice only and store that string - applyEffectsToCalculationParseNode(prop.roll, actionContext); - const {result: rolled, context} = resolve('roll', prop.roll.parseNode, actionContext.scope); - if (rolled.parseType !== 'constant'){ - logValue.push(toString(rolled)); - } - logErrors(context.errors, actionContext); - - // Reset the errors so we don't log the same errors twice - context.errors = []; - - // Resolve the roll to a final value - const {result: reduced} = resolve('reduce', rolled, actionContext.scope, context); - logErrors(context.errors, actionContext); - - // Store the result - if (reduced.parseType === 'constant'){ - prop.roll.value = reduced.value; - } else if (reduced.parseType === 'error'){ - prop.roll.value = null; - } else { - prop.roll.value = toString(reduced); - } - - // If we didn't end up with a constant of finite amount, give up - if (reduced?.parseType !== 'constant' || !isFinite(reduced.value)){ - return applyChildren(); - } - const value = reduced.value; - - actionContext.scope[prop.variableName] = value; - logValue.push(`**${value}**`); - - if (!prop.silent){ - actionContext.addLog({ - name: prop.name, - value: logValue.join('\n'), - inline: true, - }); - } - } - return applyChildren(); -} diff --git a/app/imports/api/engine/actions/applyPropertyByType/applySavingThrow.js b/app/imports/api/engine/actions/applyPropertyByType/applySavingThrow.js deleted file mode 100644 index a5aa4bae..00000000 --- a/app/imports/api/engine/actions/applyPropertyByType/applySavingThrow.js +++ /dev/null @@ -1,104 +0,0 @@ -import rollDice from '/imports/parser/rollDice.js'; -import recalculateCalculation from './shared/recalculateCalculation.js'; -import applyProperty from '../applyProperty.js'; -import numberToSignedString from '/imports/ui/utility/numberToSignedString.js'; -import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js'; - -export default function applySavingThrow(node, actionContext){ - applyNodeTriggers(node, 'before', actionContext); - const prop = node.node; - - let saveTargets = prop.target === 'self' ? [actionContext.creature] : actionContext.targets; - - recalculateCalculation(prop.dc, actionContext); - - const dc = (prop.dc?.value); - if (!isFinite(dc)){ - actionContext.addLog({ - name: 'Error', - value: 'Saving throw requires a DC', - }); - return node.children.forEach(child => applyProperty(child, actionContext)); - } - if (!prop.silent) actionContext.addLog({ - name: prop.name, - value: `DC **${dc}**`, - inline: true, - }); - const scope = actionContext.scope; - - // If there are no save targets, apply all children as if the save both - // succeeeded and failed - if (!saveTargets?.length){ - scope['$saveFailed'] = {value: true}; - scope['$saveSucceeded'] = { value: true }; - applyNodeTriggers(node, 'after', actionContext); - return node.children.forEach(child => applyProperty(child, actionContext)); - } - - // Each target makes the saving throw - saveTargets.forEach(target => { - delete scope['$saveFailed']; - delete scope['$saveSucceeded']; - delete scope['$saveDiceRoll']; - delete scope['$saveRoll']; - - const applyChildren = function () { - applyNodeTriggers(node, 'after', actionContext); - actionContext.targets = [target] - node.children.forEach(child => applyProperty(child, actionContext)); - }; - - const save = target.variables[prop.stat]; - - if (!save){ - actionContext.addLog({ - name: 'Saving throw error', - value: 'No saving throw found: ' + prop.stat, - }); - return applyChildren(); - } - - const rollModifierText = numberToSignedString(save.value, true); - - let value, values, resultPrefix; - if (save.advantage === 1){ - const [a, b] = rollDice(2, 20); - if (a >= b) { - value = a; - resultPrefix = `Advantage\n1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText}`; - } else { - value = b; - resultPrefix = `Advantage\n1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`; - } - } else if (save.advantage === -1){ - const [a, b] = rollDice(2, 20); - if (a <= b) { - value = a; - resultPrefix = `Disadvantage\n1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText}`; - } else { - value = b; - resultPrefix = `Disadvantage\n1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`; - } - } else { - values = rollDice(1, 20); - value = values[0]; - resultPrefix = `1d20 [ ${value} ] ${rollModifierText}` - } - scope['$saveDiceRoll'] = {value}; - const result = value + save.value || 0; - scope['$saveRoll'] = {value: result}; - const saveSuccess = result >= dc; - if (saveSuccess){ - scope['$saveSucceeded'] = {value: true}; - } else { - scope['$saveFailed'] = {value: true}; - } - if (!prop.silent) actionContext.addLog({ - name: saveSuccess ? 'Successful save' : 'Failed save', - value: resultPrefix + '\n**' + result + '**', - inline: true, - }); - return applyChildren(); - }); -} diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyToggle.js b/app/imports/api/engine/actions/applyPropertyByType/applyToggle.js deleted file mode 100644 index be80b012..00000000 --- a/app/imports/api/engine/actions/applyPropertyByType/applyToggle.js +++ /dev/null @@ -1,13 +0,0 @@ -import applyProperty from '../applyProperty.js'; -import recalculateCalculation from './shared/recalculateCalculation.js'; -import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js'; - -export default function applyToggle(node, actionContext){ - applyNodeTriggers(node, 'before', actionContext); - const prop = node.node; - recalculateCalculation(prop.condition, actionContext); - if (prop.condition?.value) { - applyNodeTriggers(node, 'after', actionContext); - return node.children.forEach(child => applyProperty(child, actionContext)); - } -} diff --git a/app/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js b/app/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js deleted file mode 100644 index 0ada4ea1..00000000 --- a/app/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js +++ /dev/null @@ -1,24 +0,0 @@ -import operator from '/imports/parser/parseTree/operator.js'; -import { parse } from '/imports/parser/parser.js'; -import logErrors from './logErrors.js'; - -export default function applyEffectsToCalculationParseNode(calcObj, actionContext){ - if (!calcObj.effects) return; - calcObj.effects.forEach(effect => { - if (effect.operation !== 'add') return; - if (!effect.amount) return; - if (effect.amount.value === null) return; - let effectParseNode; - try { - effectParseNode = parse(effect.amount.value.toString()); - calcObj.parseNode = operator.create({ - left: calcObj.parseNode, - right: effectParseNode, - operator: '+', - fn: 'add' - }); - } catch (e){ - logErrors([e], actionContext) - } - }); -} diff --git a/app/imports/api/engine/actions/applyPropertyByType/shared/logErrors.js b/app/imports/api/engine/actions/applyPropertyByType/shared/logErrors.js deleted file mode 100644 index 9ea760e6..00000000 --- a/app/imports/api/engine/actions/applyPropertyByType/shared/logErrors.js +++ /dev/null @@ -1,7 +0,0 @@ -export default function logErrors(errors, actionContext){ - errors?.forEach(error => { - if (error.type !== 'info'){ - actionContext.addLog({name: 'Error', value: error.message}); - } - }); -} diff --git a/app/imports/api/engine/actions/applyPropertyByType/shared/recalculateCalculation.js b/app/imports/api/engine/actions/applyPropertyByType/shared/recalculateCalculation.js deleted file mode 100644 index 2f484567..00000000 --- a/app/imports/api/engine/actions/applyPropertyByType/shared/recalculateCalculation.js +++ /dev/null @@ -1,11 +0,0 @@ -import evaluateCalculation from '/imports/api/engine/computation/utility/evaluateCalculation.js'; -import applyEffectsToCalculationParseNode from '/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js'; -import logErrors from './logErrors.js'; - -export default function recalculateCalculation(calc, actionContext, context){ - if (!calc?.parseNode) return; - calc._parseLevel = 'reduce'; - applyEffectsToCalculationParseNode(calc, actionContext); - evaluateCalculation(calc, actionContext.scope, context); - logErrors(calc.errors, actionContext); -} diff --git a/app/imports/api/engine/actions/applyPropertyByType/shared/recalculateInlineCalculations.js b/app/imports/api/engine/actions/applyPropertyByType/shared/recalculateInlineCalculations.js deleted file mode 100644 index 1a2b1cb7..00000000 --- a/app/imports/api/engine/actions/applyPropertyByType/shared/recalculateInlineCalculations.js +++ /dev/null @@ -1,13 +0,0 @@ -import embedInlineCalculations from '/imports/api/engine/computation/utility/embedInlineCalculations.js'; -import recalculateCalculation from './recalculateCalculation.js' - -export default function recalculateInlineCalculations(inlineCalcObj, actionContext){ - // Skip if there are no calculations - if (!inlineCalcObj?.inlineCalculations?.length) return; - // Recalculate each calculation with the current scope - inlineCalcObj.inlineCalculations.forEach(calc => { - recalculateCalculation(calc, actionContext); - }); - // Embed the new calculated values - embedInlineCalculations(inlineCalcObj); -} diff --git a/app/imports/api/engine/actions/applyTriggers.js b/app/imports/api/engine/actions/applyTriggers.js deleted file mode 100644 index 8fd6a536..00000000 --- a/app/imports/api/engine/actions/applyTriggers.js +++ /dev/null @@ -1,111 +0,0 @@ -import recalculateCalculation from '/imports/api/engine/actions/applyPropertyByType/shared/recalculateCalculation.js'; -import recalculateInlineCalculations from '/imports/api/engine/actions/applyPropertyByType/shared/recalculateInlineCalculations.js'; -import { getPropertyDecendants } from '/imports/api/engine/loadCreatures.js'; -import { nodeArrayToTree } from '/imports/api/parenting/nodesToTree.js'; -import applyProperty from '/imports/api/engine/actions/applyProperty.js'; -import { difference, intersection } from 'lodash'; -import getEffectivePropTags from '/imports/api/engine/computation/utility/getEffectivePropTags.js'; - -export function applyNodeTriggers(node, timing, actionContext) { - const prop = node.node; - const type = prop.type; - const triggers = actionContext.triggers?.doActionProperty?.[type]?.[timing]; - if (triggers) { - triggers.forEach(trigger => { - applyTrigger(trigger, prop, actionContext); - }); - } -} - -export function applyTriggers(triggers = [], prop, actionContext) { - // Apply the triggers - triggers.forEach(trigger => { - applyTrigger(trigger, prop, actionContext) - }); -} - -export function applyTrigger(trigger, prop, actionContext) { - // If there is a prop we are applying the trigger from, - // don't fire if the tags don't match - if (prop && !triggerMatchTags(trigger, prop)) { - return; - } - - // Prevent trigger from firing if it's inactive - if (trigger.inactive) { - return; - } - - // Prevent triggers from firing if their condition is false - if (trigger.condition?.parseNode) { - recalculateCalculation(trigger.condition, actionContext); - if (!trigger.condition.value) return; - } - - // Prevent triggers from firing themselves in a loop - if (trigger.firing) { - /* - log.content.push({ - name: trigger.name || 'Trigger', - value: 'Trigger can\'t fire itself', - inline: true, - }); - */ - return; - } - trigger.firing = true; - - // Fire the trigger - const content = { - name: trigger.name || 'Trigger', - value: trigger.description, - inline: false, - } - if (trigger.description?.text){ - recalculateInlineCalculations(trigger.description, actionContext); - content.value = trigger.description.value; - } - if(!trigger.silent) actionContext.addLog(content); - - // Get all the trigger's properties and apply them - const properties = getPropertyDecendants(actionContext.creature._id, trigger._id); - properties.sort((a, b) => a.order - b.order); - const propertyForest = nodeArrayToTree(properties); - propertyForest.forEach(node => { - applyProperty(node, actionContext); - }); - - trigger.firing = false; -} - -function triggerMatchTags(trigger, prop) { - let matched = false; - const propTags = getEffectivePropTags(prop); - // Check the target tags - if ( - !trigger.targetTags?.length || - difference(trigger.targetTags, propTags).length === 0 - ) { - matched = true; - } - // Check the extra tags - 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; - } - } - }); - return matched; -} diff --git a/app/imports/api/engine/actions/doAction.js b/app/imports/api/engine/actions/doAction.js deleted file mode 100644 index 57c7beac..00000000 --- a/app/imports/api/engine/actions/doAction.js +++ /dev/null @@ -1,100 +0,0 @@ -import SimpleSchema from 'simpl-schema'; -import { ValidatedMethod } from 'meteor/mdg:validated-method'; -import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js'; -import { nodeArrayToTree } from '/imports/api/parenting/nodesToTree.js'; -import { - getProperyAncestors, getPropertyDecendants -} from '/imports/api/engine/loadCreatures.js'; -import Creatures from '/imports/api/creature/creatures/Creatures.js'; -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import applyProperty from './applyProperty.js'; -import ActionContext from '/imports/api/engine/actions/ActionContext.js'; - -const doAction = new ValidatedMethod({ - name: 'creatureProperties.doAction', - validate: new SimpleSchema({ - actionId: SimpleSchema.RegEx.Id, - targetIds: { - type: Array, - defaultValue: [], - maxCount: 20, - optional: true, - }, - 'targetIds.$': { - type: String, - regEx: SimpleSchema.RegEx.Id, - }, - scope: { - type: Object, - blackbox: true, - optional: true, - }, - }).validator(), - mixins: [RateLimiterMixin], - rateLimit: { - numRequests: 10, - timeInterval: 5000, - }, - run({ actionId, targetIds = [], scope }) { - // Get action context - let action = CreatureProperties.findOne(actionId); - const creatureId = action.ancestors[0].id; - const actionContext = new ActionContext(creatureId, targetIds, this); - - // Check permissions - assertEditPermission(actionContext.creature, this.userId); - actionContext.targets.forEach(target => { - assertEditPermission(target, this.userId); - }); - - const ancestors = getProperyAncestors(creatureId, action._id); - ancestors.sort((a, b) => a.order - b.order); - - const properties = getPropertyDecendants(creatureId, action._id); - properties.push(action); - properties.sort((a, b) => a.order - b.order); - - // Do the action - doActionWork({ properties, ancestors, actionContext, methodScope: scope }); - - // Recompute all involved creatures - Creatures.update({ - _id: { $in: [creatureId, ...targetIds] } - }, { - $set: { dirty: true }, - }); - }, -}); - -export default doAction; - -export function doActionWork({ - properties, ancestors, actionContext, methodScope = {}, -}) { - // get the docs - const ancestorScope = getAncestorScope(ancestors); - const propertyForest = nodeArrayToTree(properties); - if (propertyForest.length !== 1) { - throw new Meteor.Error(`The action has ${propertyForest.length} top level properties, expected 1`); - } - - // Include the ancestry and method scope in the context scope - Object.assign(actionContext.scope, ancestorScope, methodScope); - - // Apply the top level property, it is responsible for applying its children - // recursively - applyProperty(propertyForest[0], actionContext); - - // Insert the log - actionContext.writeLog(); -} - -// Assumes ancestors are in tree order already -function getAncestorScope(ancestors) { - let scope = {}; - ancestors.forEach(prop => { - scope[`#${prop.type}`] = prop; - }); - return scope; -} diff --git a/app/imports/api/engine/actions/doAction.test.js b/app/imports/api/engine/actions/doAction.test.js deleted file mode 100644 index 56c365cb..00000000 --- a/app/imports/api/engine/actions/doAction.test.js +++ /dev/null @@ -1,53 +0,0 @@ -import '/imports/api/simpleSchemaConfig.js'; -//import testTypes from './testTypes/index.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){ - let schema = CreatureProperties.simpleSchema(prop); - return schema.clean(prop); -} - -function cleanCreature(creature){ - let schema = Creatures.simpleSchema(creature); - return schema.clean(creature); -} - -// Fake ActionContext to test actions with -const creatureId = 'actionTestCreatureId'; -const creatureName = 'Action Test Creature'; -const testActionContext = { - creature: cleanCreature({ - _id: creatureId, - }), - log: CreatureLogSchema.clean({ - creatureId: creatureId, - creatureName: creatureName, - }), - scope: {}, - addLog(content) { - if (content.name || content.value){ - this.log.content.push(content); - } - }, - writeLog: () => { }, -} - -const action = cleanProp({ - type: 'action', -}); -const actionAncestors = []; - -describe('Do Action', function(){ - it('Does an empty action', function(){ - doActionWork({ - properties: [action], - ancestors: actionAncestors, - actionContext: testActionContext, - methodScope: {}, - }); - }); - //testTypes.forEach(test => it(test.text, test.fn)); -}); diff --git a/app/imports/api/engine/actions/doCastSpell.js b/app/imports/api/engine/actions/doCastSpell.js deleted file mode 100644 index f512e4ed..00000000 --- a/app/imports/api/engine/actions/doCastSpell.js +++ /dev/null @@ -1,136 +0,0 @@ -import SimpleSchema from 'simpl-schema'; -import { ValidatedMethod } from 'meteor/mdg:validated-method'; -import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import Creatures from '/imports/api/creature/creatures/Creatures.js'; -import { - getProperyAncestors, getPropertyDecendants -} from '/imports/api/engine/loadCreatures.js'; -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js'; -import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js'; -import { doActionWork } from '/imports/api/engine/actions/doAction.js'; -import ActionContext from '/imports/api/engine/actions/ActionContext.js'; - -const doAction = new ValidatedMethod({ - name: 'creatureProperties.doCastSpell', - validate: new SimpleSchema({ - spellId: SimpleSchema.RegEx.Id, - slotId: { - type: String, - regEx: SimpleSchema.RegEx.Id, - optional: true, - }, - ritual: { - type: Boolean, - optional: true, - }, - targetIds: { - type: Array, - defaultValue: [], - maxCount: 20, - optional: true, - }, - 'targetIds.$': { - type: String, - regEx: SimpleSchema.RegEx.Id, - }, - scope: { - type: Object, - blackbox: true, - optional: true, - }, - }).validator(), - mixins: [RateLimiterMixin], - rateLimit: { - numRequests: 10, - timeInterval: 5000, - }, - run({ spellId, slotId, ritual, targetIds = [], scope = {} }) { - // Get action context - let spell = CreatureProperties.findOne(spellId); - const creatureId = spell.ancestors[0].id; - const actionContext = new ActionContext(creatureId, targetIds, this); - - // Check permissions - assertEditPermission(actionContext.creature, this.userId); - actionContext.targets.forEach(target => { - assertEditPermission(target, this.userId); - }); - - const ancestors = getProperyAncestors(creatureId, spell._id); - ancestors.sort((a, b) => a.order - b.order); - - const properties = getPropertyDecendants(creatureId, spell._id); - properties.push(spell); - properties.sort((a, b) => a.order - b.order); - - // Spend the appropriate slot - let slotLevel = spell.level || 0; - let slot; - - // If a spell requires a slot, make sure a slot is spent - if (spell.level && !spell.castWithoutSpellSlots && !(ritual && spell.ritual)) { - slot = CreatureProperties.findOne(slotId); - if (!slot) { - throw new Meteor.Error('No slot', - 'Slot not found to cast spell'); - } - if (!slot.value) { - throw new Meteor.Error('No slot', - 'Slot depleted'); - } - if (slot.attributeType !== 'spellSlot') { - throw new Meteor.Error('Not a slot', - 'The given property is not a valid spell slot'); - } - if (!slot.spellSlotLevel?.value) { - throw new Meteor.Error('No slot level', - 'Slot does not have a spell slot level'); - } - if (slot.spellSlotLevel.value < spell.level) { - throw new Meteor.Error('Slot too small', - 'Slot is not large enough to cast spell'); - } - slotLevel = slot.spellSlotLevel.value; - damagePropertyWork({ - prop: slot, - operation: 'increment', - value: 1, - actionContext, - }); - } - - // Post the slot level spent to the log - if (slot?.spellSlotLevel?.value) { - actionContext.addLog({ - name: `Casting using a level ${slotLevel} spell slot` - }); - } else if (slotLevel) { - if (ritual) { - actionContext.addLog({ - name: `Ritual casting at level ${slotLevel}` - }); - } else { - actionContext.addLog({ - name: `Casting at level ${slotLevel}` - }); - } - } - - actionContext.scope['slotLevel'] = slotLevel; - - // Do the action - doActionWork({ - properties, ancestors, actionContext, methodScope: scope, - }); - - // Force the characters involved to recalculate - Creatures.update({ - _id: { $in: [creatureId, ...targetIds] } - }, { - $set: { dirty: true }, - }); - }, -}); - -export default doAction; diff --git a/app/imports/api/engine/actions/doCheck.js b/app/imports/api/engine/actions/doCheck.js deleted file mode 100644 index a5bc653b..00000000 --- a/app/imports/api/engine/actions/doCheck.js +++ /dev/null @@ -1,134 +0,0 @@ -import SimpleSchema from 'simpl-schema'; -import { ValidatedMethod } from 'meteor/mdg:validated-method'; -import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js'; -import rollDice from '/imports/parser/rollDice.js'; -import numberToSignedString from '/imports/ui/utility/numberToSignedString.js'; -import { applyTriggers } from '/imports/api/engine/actions/applyTriggers.js'; -import ActionContext from '/imports/api/engine/actions/ActionContext.js'; -import evaluateCalculation from '/imports/api/engine/computation/utility/evaluateCalculation.js'; - -const doCheck = new ValidatedMethod({ - name: 'creatureProperties.doCheck', - validate: new SimpleSchema({ - propId: SimpleSchema.RegEx.Id, - scope: { - type: Object, - blackbox: true, - }, - }).validator(), - mixins: [RateLimiterMixin], - rateLimit: { - numRequests: 10, - timeInterval: 5000, - }, - run({ propId, scope }) { - const prop = CreatureProperties.findOne(propId); - const creatureId = prop.ancestors[0].id; - const actionContext = new ActionContext(creatureId, [creatureId], this); - Object.assign(actionContext.scope, scope); - - // Check permissions - assertEditPermission(actionContext.creature, this.userId); - - // Do the check - doCheckWork({ prop, actionContext }); - }, -}); - -export default doCheck; - -export function doCheckWork({ prop, actionContext }) { - - applyTriggers(actionContext.triggers.check?.before, prop, actionContext); - rollCheck(prop, actionContext); - applyTriggers(actionContext.triggers.check?.after, prop, actionContext); - - // Insert the log - actionContext.writeLog(); -} - -function rollCheck(prop, actionContext) { - const scope = actionContext.scope; - // get the modifier for the roll - let rollModifier; - let logName = `${prop.name} check`; - if (prop.type === 'skill') { - rollModifier = prop.value; - if (prop.skillType === 'save') { - if (prop.name.match(/save/i)) { - logName = prop.name; - } else { - logName = prop.name ? `${prop.name} save` : 'Saving Throw'; - } - } - } else if (prop.type === 'attribute') { - if (prop.attributeType === 'ability') { - rollModifier = prop.modifier; - } else { - rollModifier = prop.value; - } - } else { - throw (`${prop.type} not supported for checks`); - } - - let rollModifierText = numberToSignedString(rollModifier, true); - - const { effectBonus, effectString } = applyUnresolvedEffects(prop, scope) - rollModifierText += effectString; - rollModifier += effectBonus; - - let value, values, resultPrefix; - if (scope['$checkAdvantage'] === 1) { - logName += ' (Advantage)'; - const [a, b] = rollDice(2, 20); - if (a >= b) { - value = a; - resultPrefix = `1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText} = `; - } else { - value = b; - resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText} = `; - } - } else if (scope['$checkAdvantage'] === -1) { - logName += ' (Disadvantage)'; - const [a, b] = rollDice(2, 20); - if (a <= b) { - value = a; - resultPrefix = `1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText} = `; - } else { - value = b; - resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText} = `; - } - } else { - values = rollDice(1, 20); - value = values[0]; - resultPrefix = `1d20 [ ${value} ] ${rollModifierText} = ` - } - const result = (value + rollModifier) || 0; - scope['$checkDiceRoll'] = value; - scope['$checkRoll'] = result; - scope['$checkModifier'] = rollModifier; - actionContext.addLog({ - name: logName, - value: `${resultPrefix} **${result}**`, - }); -} - -function applyUnresolvedEffects(prop, scope) { - let effectBonus = 0; - let effectString = ''; - if (!prop.effects) { - return { effectBonus, effectString }; - } - prop.effects.forEach(effect => { - if (!effect.amount?.parseNode) return; - if (effect.operation !== 'add') return; - effect.amount._parseLevel = 'reduce'; - evaluateCalculation(effect.amount, scope); - if (typeof effect.amount?.value !== 'number') return; - effectBonus += effect.amount.value; - effectString += ` ${effect.amount.value < 0 ? '-' : '+'} [${effect.amount.calculation}] ${Math.abs(effect.amount.value)}` - }); - return { effectBonus, effectString }; -} diff --git a/app/imports/api/engine/actions/index.js b/app/imports/api/engine/actions/index.js deleted file mode 100644 index 5cd7a8b4..00000000 --- a/app/imports/api/engine/actions/index.js +++ /dev/null @@ -1,2 +0,0 @@ -import './doCastSpell.js'; -import './doCheck.js'; diff --git a/app/imports/api/engine/computation/CreatureComputation.ts b/app/imports/api/engine/computation/CreatureComputation.ts index 75ec51b3..b4b4fa0c 100644 --- a/app/imports/api/engine/computation/CreatureComputation.ts +++ b/app/imports/api/engine/computation/CreatureComputation.ts @@ -1,6 +1,6 @@ import { EJSON } from 'meteor/ejson'; import createGraph, { Graph } from 'ngraph.graph'; -import getEffectivePropTags from '/imports/api/engine/computation/utility/getEffectivePropTags.js'; +import getEffectivePropTags from '/imports/api/engine/computation/utility/getEffectivePropTags'; interface CreatureProperty { _id: string; @@ -8,12 +8,12 @@ interface CreatureProperty { } export default class CreatureComputation { - originalPropsById: object; - propsById: object; - propsWithTag: object; - scope: object; + originalPropsById: Record; + propsById: Record; + propsWithTag: Record; + scope: Record; props: Array; - dependencyGraph: Graph; + dependencyGraph: Graph; errors: Array; creature: object; variables: object; diff --git a/app/imports/api/engine/computation/buildComputation/computeInactiveStatus.js b/app/imports/api/engine/computation/buildComputation/computeInactiveStatus.ts similarity index 52% rename from app/imports/api/engine/computation/buildComputation/computeInactiveStatus.js rename to app/imports/api/engine/computation/buildComputation/computeInactiveStatus.ts index 5282e93e..4d8772f1 100644 --- a/app/imports/api/engine/computation/buildComputation/computeInactiveStatus.js +++ b/app/imports/api/engine/computation/buildComputation/computeInactiveStatus.ts @@ -1,42 +1,42 @@ -import walkDown from '/imports/api/engine/computation/utility/walkdown.js'; +import { CreatureProperty } from '/imports/api/creature/creatureProperties/CreatureProperties'; +import walkDown from '/imports/api/engine/computation/utility/walkdown'; +import { TreeNode } from '/imports/api/parenting/parentingFunctions'; +import { isSpell } from '/imports/api/properties/Spells'; -export default function computeInactiveStatus(node){ - const prop = node.node; - if (!isActive(prop)){ +export default function computeInactiveStatus(node: TreeNode): void { + const prop = node.doc; + if (!isActive(prop)) { // Mark prop inactive due to self prop.inactive = true; prop.deactivatedBySelf = true; } - if(!childrenActive(prop)){ + if (!childrenActive(prop)) { // Mark children as inactive due to ancestor walkDown(node.children, child => { - child.node.inactive = true; - child.node.deactivatedByAncestor = true; + child.doc.inactive = true; + child.doc.deactivatedByAncestor = true; }); } } -function isActive(prop){ +function isActive(prop: CreatureProperty): boolean { if (prop.disabled) return false; - switch (prop.type){ - // Unprepared spells are inactive - case 'spell': return !!prop.prepared || !!prop.alwaysPrepared; - default: return true; + if (isSpell(prop)) { + return !!prop.prepared || !!prop.alwaysPrepared; } + return true; } -function childrenActive(prop){ +function childrenActive(prop: CreatureProperty): boolean { // Children of disabled properties are always inactive if (prop.disabled) return false; - switch (prop.type){ + switch (prop.type) { // Only equipped items with non-zero quantity have active children case 'item': return !!prop.equipped && prop.quantity !== 0; // The children of actions, spells, and triggers are always inactive case 'action': return false; case 'spell': return false; case 'trigger': return false; - // The children of notes are always inactive - case 'note': return false; // Other children are active default: return true; } diff --git a/app/imports/api/engine/computation/buildComputation/computeSlotQuantityFilled.js b/app/imports/api/engine/computation/buildComputation/computeSlotQuantityFilled.js index e3adabc8..e7485c11 100644 --- a/app/imports/api/engine/computation/buildComputation/computeSlotQuantityFilled.js +++ b/app/imports/api/engine/computation/buildComputation/computeSlotQuantityFilled.js @@ -2,17 +2,16 @@ * Only computes `totalFilled`, need to compute `quantityExpected.value` * before `spacesLeft` can be computed */ -export default function computeSlotQuantityFilled(node, dependencyGraph){ - let slot = node.node; +export default function computeSlotQuantityFilled(node, dependencyGraph) { + let slot = node.doc; if (slot.type !== 'propertySlot') return; slot.totalFilled = 0; node.children.forEach(child => { - let childProp = child.node; + let childProp = child.doc; dependencyGraph.addLink(slot._id, childProp._id, 'slotFill'); if ( - childProp.type === 'slotFiller' && Number.isFinite(childProp.slotQuantityFilled) - ){ + ) { slot.totalFilled += childProp.slotQuantityFilled; } else { slot.totalFilled++; diff --git a/app/imports/api/engine/computation/buildComputation/computeToggleDependencies.js b/app/imports/api/engine/computation/buildComputation/computeToggleDependencies.js deleted file mode 100644 index b7a98595..00000000 --- a/app/imports/api/engine/computation/buildComputation/computeToggleDependencies.js +++ /dev/null @@ -1,17 +0,0 @@ -import walkDown from '/imports/api/engine/computation/utility/walkdown.js'; - -export default function computeToggleDependencies(node, dependencyGraph){ - 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; - walkDown(node.children, child => { - // The child nodes depend on the toggle condition compuation - child.node._computationDetails.toggleAncestors.push(prop); - dependencyGraph.addLink(child.node._id, prop._id, 'toggle'); - }); -} diff --git a/app/imports/api/engine/computation/buildComputation/computeToggleDependencies.ts b/app/imports/api/engine/computation/buildComputation/computeToggleDependencies.ts new file mode 100644 index 00000000..06361748 --- /dev/null +++ b/app/imports/api/engine/computation/buildComputation/computeToggleDependencies.ts @@ -0,0 +1,37 @@ +import walkDown from '/imports/api/engine/computation/utility/walkdown'; +import { getEffectTagTargets } from '/imports/api/engine/computation/buildComputation/linkTypeDependencies'; +import { Forest, TreeNode } from '/imports/api/parenting/parentingFunctions'; +import { CreatureProperty } from '/imports/api/creature/creatureProperties/CreatureProperties'; +import CreatureComputation from '/imports/api/engine/computation/CreatureComputation'; + +export default function computeToggleDependencies( + node: TreeNode, computation: CreatureComputation, forest: Forest +) { + const prop = node.doc + // 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.doc._computationDetails.toggleAncestors.push(prop); + computation.dependencyGraph.addLink(target.doc._id, prop._id, 'toggle'); + walkDown(target.children, child => { + // The child nodes depend on the toggle + child.doc._computationDetails.toggleAncestors.push(prop); + computation.dependencyGraph.addLink(child.doc._id, prop._id, 'toggle'); + }); + }); + } + + // We don't need to link direct children of static toggles, it's already done + if (prop.disabled || prop.enabled) return; + + walkDown(node.children, child => { + // The child nodes depend on the toggle + child.doc._computationDetails.toggleAncestors.push(prop); + computation.dependencyGraph.addLink(child.doc._id, prop._id, 'toggle'); + }); +} diff --git a/app/imports/api/engine/computation/buildComputation/linkCalculationDependencies.js b/app/imports/api/engine/computation/buildComputation/linkCalculationDependencies.js index 67e4452e..d07c13a6 100644 --- a/app/imports/api/engine/computation/buildComputation/linkCalculationDependencies.js +++ b/app/imports/api/engine/computation/buildComputation/linkCalculationDependencies.js @@ -1,7 +1,6 @@ -import findAncestorByType from '/imports/api/engine/computation/utility/findAncestorByType.js'; -import { traverse } from '/imports/parser/resolve.js'; +import traverse from '/imports/parser/traverse'; -export default function linkCalculationDependencies(dependencyGraph, prop, {propsById}){ +export default function linkCalculationDependencies(dependencyGraph, prop, { propsById }) { prop._computationDetails.calculations.forEach(calcObj => { // Store resolved ancestors const memo = { @@ -9,19 +8,27 @@ export default function linkCalculationDependencies(dependencyGraph, prop, {prop }; // Add this calculation to the dependency graph const calcNodeId = `${prop._id}.${calcObj._key}`; - dependencyGraph.addNode(calcNodeId, calcObj); + // Skip empty calculations that aren't targeted by anything + if ( + !calcObj.calculation + && !calcObj.effectIds + && !calcObj.proficiencyIds + ) return; + + dependencyGraph.addNode(calcNodeId, calcObj); // Traverse the parsed calculation looking for variable names traverse(calcObj.parseNode, node => { // Skip nodes that aren't symbols or accessors if (node.parseType !== 'symbol' && node.parseType !== 'accessor') return; // Link ancestor references as direct property dependencies - if (node.name[0] === '#'){ + if (node.name[0] === '#') { let ancestorProp = getAncestorProp( node.name.slice(1), memo, prop, propsById ); if (!ancestorProp) return; // Link the ancestor prop as a direct dependency + // TODO: we might be referencing a calculation sub-field, depend on that instead dependencyGraph.addLink( calcNodeId, ancestorProp._id, 'ancestorReference' ); @@ -34,18 +41,29 @@ export default function linkCalculationDependencies(dependencyGraph, prop, {prop }); // Store the resolved ancestors in this calculation's local scope if (memo.ancestors) { - calcObj._localScope = { ...calcObj._localScope, ...memo.ancestors}; + calcObj._localScope = { ...calcObj._localScope, ...memo.ancestors }; } }); } -function getAncestorProp(type, memo, prop, propsById){ - if (memo.ancestors && memo.ancestors['#' + type]){ +function getAncestorProp(type, memo, prop, propsById) { + if (memo.ancestors && memo.ancestors['#' + type]) { return memo.ancestors['#' + type]; } else { - var ancestorProp = findAncestorByType( prop, type, propsById ); + var ancestorProp = findAncestorByType(prop, type, propsById); if (!memo.ancestors) memo.ancestors = {}; memo.ancestors['#' + type] = ancestorProp; return ancestorProp; } } + +function findAncestorByType(prop, type, propsById) { + if (!prop || !prop.parentId) return; + let parentProp = prop; + while (parentProp) { + parentProp = propsById[parentProp.parentId]; + if (parentProp?.type === type) { + return parentProp; + } + } +} diff --git a/app/imports/api/engine/computation/buildComputation/linkInventory.js b/app/imports/api/engine/computation/buildComputation/linkInventory.js index ae092bd5..6213834b 100644 --- a/app/imports/api/engine/computation/buildComputation/linkInventory.js +++ b/app/imports/api/engine/computation/buildComputation/linkInventory.js @@ -2,23 +2,23 @@ * Performs a depth first traversal of the character tree, summing the container * and inventory contents on the way up the tree */ -export default function linkInventory(forest, dependencyGraph){ +export default function linkInventory(forest, dependencyGraph) { // The stack of properties to still navigate - const stack = [...forest]; + const stack = [...forest.trees]; // The current containers we are inside of const containerStack = []; - while(stack.length){ + while (stack.length) { const top = stack[stack.length - 1]; - const prop = top.node; - if (prop._computationDetails.inventoryChildrenVisited){ + const prop = top.doc; + if (prop._computationDetails.inventoryChildrenVisited) { if (prop.type === 'container') containerStack.pop(); stack.pop(); handleProp(prop, containerStack, dependencyGraph); } else { // Add all containers to the stack when we first visit them - if (prop.type === 'container'){ - containerStack.push(top.node); + if (prop.type === 'container') { + containerStack.push(top.doc); } // Push children onto the stack and mark this as children are visited stack.push(...top.children); @@ -27,18 +27,18 @@ export default function linkInventory(forest, dependencyGraph){ } } -function handleProp(prop, containerStack, dependencyGraph){ +function handleProp(prop, containerStack, dependencyGraph) { // Skip props that aren't part of the inventory if (prop.type !== 'item' && prop.type !== 'container') return; // Determine if this property is carried, items are carried by default let carried = prop.type === 'container' ? prop.carried : true; // Item-specific links - if (prop.type === 'item'){ - if (prop.attuned){ + if (prop.type === 'item') { + if (prop.attuned) { dependencyGraph.addLink('itemsAttuned', prop._id, 'attunedItem'); } - if (prop.equipped){ + if (prop.equipped) { dependencyGraph.addLink('weightEquipment', prop._id, 'equippedItem'); dependencyGraph.addLink('valueEquipment', prop._id, 'equippedItem'); } @@ -47,14 +47,14 @@ function handleProp(prop, containerStack, dependencyGraph){ // Get the parent container const container = containerStack[containerStack.length - 1]; - if (container){ + if (container) { // The container depends on this prop for its contents data dependencyGraph.addLink(container._id, prop._id, 'containerContents'); } else { // There is no parent container, the character totals depend on this prop dependencyGraph.addLink('weightTotal', prop._id, 'inventoryStats'); dependencyGraph.addLink('valueTotal', prop._id, 'inventoryStats'); - if (carried){ + if (carried) { dependencyGraph.addLink('weightCarried', prop._id, 'inventoryStats'); dependencyGraph.addLink('valueCarried', prop._id, 'inventoryStats'); } diff --git a/app/imports/api/engine/computation/buildComputation/linkTypeDependencies.js b/app/imports/api/engine/computation/buildComputation/linkTypeDependencies.js index 44fc93e9..f1cbd104 100644 --- a/app/imports/api/engine/computation/buildComputation/linkTypeDependencies.js +++ b/app/imports/api/engine/computation/buildComputation/linkTypeDependencies.js @@ -23,23 +23,26 @@ const linkDependenciesByType = { toggle: linkToggle, } -export default function linkTypeDependencies(dependencyGraph, prop, computation){ +export default function linkTypeDependencies(dependencyGraph, prop, computation) { linkDependenciesByType[prop.type]?.(dependencyGraph, prop, computation); } -function dependOnCalc({dependencyGraph, prop, key}){ +function dependOnCalc({ dependencyGraph, prop, key }) { let calc = get(prop, key); - if (!calc) return; - if (calc.type !== '_calculation'){ - throw `Expected calculation got ${calc.type}` + if (!calc?.type) return; + if (calc.type !== '_calculation') { + throw `Failed to dependOnCal for prop: ${prop._id}, key: ${key}. Expected calculation got ${calc.type}` } dependencyGraph.addLink(prop._id, `${prop._id}.${key}`, 'calculation'); } -function linkAction(dependencyGraph, prop, {propsById}){ +function linkAction(dependencyGraph, prop, { propsById }) { + if (prop.variableName) { + dependencyGraph.addLink(prop.variableName, prop._id, 'eventDefinition'); + } // The action depends on its attack roll and uses calculations - dependOnCalc({dependencyGraph, prop, key: 'attackRoll'}); - dependOnCalc({dependencyGraph, prop, key: 'uses'}); + dependOnCalc({ dependencyGraph, prop, key: 'attackRoll' }); + dependOnCalc({ dependencyGraph, prop, key: 'uses' }); // Link the resources the action uses if (!prop.resources) return; @@ -47,7 +50,7 @@ function linkAction(dependencyGraph, prop, {propsById}){ prop.resources.itemsConsumed.forEach((itemConsumed, index) => { if (!itemConsumed.itemId) return; const item = propsById[itemConsumed.itemId]; - if (!item || item.inactive){ + if (!item || item.inactive) { // Unlink if the item doesn't exist or is inactive itemConsumed.itemId = undefined; return; @@ -77,50 +80,61 @@ function linkAction(dependencyGraph, prop, {propsById}){ key: `resources.attributesConsumed[${index}].quantity`, }); }); + // Link conditions + prop.resources.conditions?.forEach((con, index) => { + // Link the property to its condition calculation + dependOnCalc({ + dependencyGraph, + prop, + key: `resources.conditions[${index}].condition`, + }); + }); } -function linkAdjustment(dependencyGraph, prop){ +function linkAdjustment(dependencyGraph, prop) { // Adjustment depends on its amount - dependOnCalc({dependencyGraph, prop, key: 'amount'}); + dependOnCalc({ dependencyGraph, prop, key: 'amount' }); } -function linkAttribute(dependencyGraph, prop){ +function linkAttribute(dependencyGraph, prop) { linkVariableName(dependencyGraph, prop); - // Depends on spellSlotLevel - dependOnCalc({dependencyGraph, prop, key: 'spellSlotLevel'}); + // Spell slots depend on spellSlotLevel + if (prop.type === 'spellSlot') { + dependOnCalc({ dependencyGraph, prop, key: 'spellSlotLevel' }); + } // Depends on base value - dependOnCalc({dependencyGraph, prop, key: 'baseValue'}); + dependOnCalc({ dependencyGraph, prop, key: 'baseValue' }); // hit dice depend on constitution - if (prop.attributeType === 'hitDice'){ + if (prop.attributeType === 'hitDice') { dependencyGraph.addLink(prop._id, 'constitution', 'hitDiceConMod'); } } -function linkBranch(dependencyGraph, prop){ - dependOnCalc({dependencyGraph, prop, key: 'condition'}); +function linkBranch(dependencyGraph, prop) { + dependOnCalc({ dependencyGraph, prop, key: 'condition' }); } -function linkBuff(dependencyGraph, prop){ - dependOnCalc({dependencyGraph, prop, key: 'duration'}); +function linkBuff(dependencyGraph, prop) { + dependOnCalc({ dependencyGraph, prop, key: 'duration' }); } function linkClassLevel(dependencyGraph, prop) { if (prop.inactive) return; // The variableName of the prop depends on the prop - if (prop.variableName && prop.level){ + if (prop.variableName && prop.level) { dependencyGraph.addLink(prop.variableName, prop._id, 'classLevel'); // The level variable depends on the class variableName variable let existingLevelLink = dependencyGraph.getLink('level', prop.variableName); - if (!existingLevelLink){ + if (!existingLevelLink) { dependencyGraph.addLink('level', prop.variableName, 'level'); } } } -function linkDamage(dependencyGraph, prop){ - dependOnCalc({dependencyGraph, prop, key: 'amount'}); +function linkDamage(dependencyGraph, prop) { + dependOnCalc({ dependencyGraph, prop, key: 'amount' }); } function linkEffects(dependencyGraph, prop, computation) { @@ -132,7 +146,7 @@ function linkEffects(dependencyGraph, prop, computation) { if (prop.inactive) { // Inactive effects apply to no stats return; - } else if (prop.targetByTags){ + } else if (prop.targetByTags) { getEffectTagTargets(prop, computation).forEach(targetId => { const targetProp = computation.propsById[targetId]; if ( @@ -147,8 +161,8 @@ function linkEffects(dependencyGraph, prop, computation) { // Otherwise target a field on that property const key = prop.targetField || getDefaultCalculationField(targetProp); const calcObj = get(targetProp, key); - if (calcObj && calcObj.calculation){ - dependencyGraph.addLink(`${targetProp._id}.${key}`, prop._id , 'effect'); + if (calcObj) { + dependencyGraph.addLink(`${targetProp._id}.${key}`, prop._id, 'effect'); } } }); @@ -161,14 +175,14 @@ 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){ + let notIds = [effect._id]; // Can't target itself + if (effect.extraTags) { effect.extraTags.forEach(ex => { if (ex.operation === 'OR') { targets = union(targets, getTargetListFromTags(ex.tags, computation)); - } else if (ex.operation === 'NOT'){ + } else if (ex.operation === 'NOT') { ex.tags.forEach(tag => { const idList = computation.propsWithTag[tag]; if (idList) { @@ -181,7 +195,7 @@ function getEffectTagTargets(effect, computation){ return difference(targets, notIds); } -function getTargetListFromTags(tags, computation){ +function getTargetListFromTags(tags, computation) { const targetTagIdLists = []; if (!tags) return []; tags.forEach(tag => { @@ -192,8 +206,8 @@ function getTargetListFromTags(tags, computation){ return targets; } -function getDefaultCalculationField(prop){ - switch (prop.type){ +function getDefaultCalculationField(prop) { + switch (prop.type) { case 'action': return 'attackRoll'; case 'adjustment': return 'amount'; case 'attribute': return 'baseValue'; @@ -215,7 +229,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; @@ -223,13 +236,13 @@ function getDefaultCalculationField(prop){ } } -function linkRoll(dependencyGraph, prop){ - dependOnCalc({dependencyGraph, prop, key: 'roll'}); +function linkRoll(dependencyGraph, prop) { + dependOnCalc({ dependencyGraph, prop, key: 'roll' }); } -function linkVariableName(dependencyGraph, prop){ +function linkVariableName(dependencyGraph, prop) { // The variableName of the prop depends on the prop if the prop is active - if (prop.variableName && !prop.inactive){ + if (prop.variableName && !prop.inactive) { dependencyGraph.addLink(prop.variableName, prop._id, 'definition'); } } @@ -243,67 +256,108 @@ function linkDamageMultiplier(dependencyGraph, prop) { }); } -function linkPointBuy(dependencyGraph, prop){ +function linkPointBuy(dependencyGraph, prop) { dependOnCalc({ dependencyGraph, prop, key: 'min' }); dependOnCalc({ dependencyGraph, prop, key: 'max' }); - dependOnCalc({ dependencyGraph, prop, key: 'cost' }); dependOnCalc({ dependencyGraph, prop, key: 'total' }); - prop.values?.forEach(row => { + + prop.values?.forEach((row, index) => { + // Get a unique id for the row because it might be shared among duplicated point buy tables + // prop._id is forced unique by the database, so it can be used instead + const uniqueRowId = prop._id + '_row_' + index; // Wrap the document in a new object so we don't bash it unintentionally const pointBuyRow = { ...row, + _id: uniqueRowId, type: 'pointBuyRow', tableName: prop.name, tableId: prop._id, + rowIndex: index, } - dependencyGraph.addNode(row._id, pointBuyRow); + dependencyGraph.addNode(pointBuyRow._id, pointBuyRow); linkVariableName(dependencyGraph, pointBuyRow); - dependOnCalc({ dependencyGraph, pointBuyRow, key: 'row.min' }); - dependOnCalc({ dependencyGraph, pointBuyRow, key: 'row.max' }); - dependOnCalc({ dependencyGraph, pointBuyRow, key: 'row.cost' }); + dependencyGraph.addLink(pointBuyRow._id, prop._id, 'pointBuyRow'); }); - 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) { + dependencyGraph.addLink(`${targetProp._id}.${key}`, prop._id, 'proficiency'); + } + } + }); + } else { + prop.stats.forEach(statName => { + if (!statName) return; + dependencyGraph.addLink(statName, prop._id, 'proficiency'); + }); + } } -function linkSavingThrow(dependencyGraph, prop){ - dependOnCalc({dependencyGraph, prop, key: 'dc'}); +function linkSavingThrow(dependencyGraph, prop) { + dependOnCalc({ dependencyGraph, prop, key: 'dc' }); } -function linkSkill(dependencyGraph, prop){ +function linkSkill(dependencyGraph, prop, computation) { // Depends on base value dependOnCalc({ dependencyGraph, prop, key: 'baseValue' }); // Link dependents if (prop.inactive) return; linkVariableName(dependencyGraph, prop); // The prop depends on the variable references as the ability - if (prop.ability){ + if (prop.ability) { dependencyGraph.addLink(prop._id, prop.ability, 'skillAbilityScore'); } // Skills depend on the creature's proficiencyBonus dependencyGraph.addLink(prop._id, 'proficiencyBonus', 'skillProficiencyBonus'); + + // 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) { + dependencyGraph.addLink(`${targetProp._id}.${key}`, prop._id, 'proficiency'); + } + }); + } } -function linkSlot(dependencyGraph, prop){ - dependOnCalc({dependencyGraph, prop, key: 'quantityExpected'}); - dependOnCalc({dependencyGraph, prop, key: 'slotCondition'}); +function linkSlot(dependencyGraph, prop) { + dependOnCalc({ dependencyGraph, prop, key: 'quantityExpected' }); + dependOnCalc({ dependencyGraph, prop, key: 'slotCondition' }); } -function linkSpellList(dependencyGraph, prop){ - dependOnCalc({dependencyGraph, prop, key: 'maxPrepared'}); - dependOnCalc({dependencyGraph, prop, key: 'attackRollBonus'}); - dependOnCalc({dependencyGraph, prop, key: 'dc'}); +function linkSpellList(dependencyGraph, prop) { + dependOnCalc({ dependencyGraph, prop, key: 'maxPrepared' }); + dependOnCalc({ dependencyGraph, prop, key: 'attackRollBonus' }); + dependOnCalc({ dependencyGraph, prop, key: 'dc' }); } -function linkToggle(dependencyGraph, prop){ +function linkToggle(dependencyGraph, prop) { linkVariableName(dependencyGraph, prop); - dependOnCalc({dependencyGraph, prop, key: 'condition'}); + dependOnCalc({ dependencyGraph, prop, key: 'condition' }); } diff --git a/app/imports/api/engine/computation/buildComputation/parseCalculationFields.js b/app/imports/api/engine/computation/buildComputation/parseCalculationFields.js index 3de657fe..7cfb333b 100644 --- a/app/imports/api/engine/computation/buildComputation/parseCalculationFields.js +++ b/app/imports/api/engine/computation/buildComputation/parseCalculationFields.js @@ -1,18 +1,18 @@ -import INLINE_CALCULATION_REGEX from '/imports/constants/INLINE_CALCULTION_REGEX.js'; -import { prettifyParseError, parse } from '/imports/parser/parser.js'; -import applyFnToKey from '/imports/api/engine/computation/utility/applyFnToKey.js'; -import { get, unset } from 'lodash'; -import errorNode from '/imports/parser/parseTree/error.js'; -import cyrb53 from '/imports/api/engine/computation/utility/cyrb53.js'; +import INLINE_CALCULATION_REGEX from '/imports/constants/INLINE_CALCULATION_REGEX'; +import { prettifyParseError, parse } from '/imports/parser/parser'; +import applyFnToKey from '/imports/api/engine/computation/utility/applyFnToKey'; +import { get, set, unset } from 'lodash'; +import errorNode from '/imports/parser/parseTree/error'; +import cyrb53 from '/imports/api/engine/computation/utility/cyrb53'; -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,11 +51,11 @@ 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'; + let parseLevel = schemas[prop.type].getDefinition(calcKey).parseLevel || 'compile'; // Special case of effects, when targeting by tags compile if (prop.type === 'effect' && prop.targetByTags) parseLevel = 'compile'; @@ -63,12 +63,21 @@ function parseAllCalculationFields(prop, schemas){ // For all fields matching they keys // supports `keys.$.with.$.arrays` applyFnToKey(prop, calcKey, (prop, key) => { - const calcObj = get(prop, key); + let calcObj = get(prop, key); + // Create a calculation object if one doesn't exist, it will get deleted again later if + // it's not used, but if an effect targets a calculated field, we should have one to target + if ( + !calcObj + && subDocsExist(prop, key) + ) { + calcObj = {}; + set(prop, key, calcObj); + } + // Sub document didn't exist, skip this field if (!calcObj) return; - // Delete the whole calculation object if the calculation string isn't set - if (!calcObj.calculation){ - unset(prop, calcKey); - return; + // Keep a list of empty calculations for potential deletion if they aren't used + if (!calcObj.calculation) { + prop._computationDetails.emptyCalculations.push(calcObj); } // Store a reference to all the calculations prop._computationDetails.calculations.push(calcObj); @@ -84,15 +93,31 @@ function parseAllCalculationFields(prop, schemas){ }); } -function parseCalculation(calcObj){ - const calcHash = cyrb53(calcObj.calculation); +function subDocsExist(prop, key) { + const path = key.split('.'); + if (path.length < 2) return !!prop; + path.pop(); + const subPath = path.join('.'); + return !!get(prop, subPath); +} + +export function removeEmptyCalculations(prop) { + prop._computationDetails.emptyCalculations.forEach(calcObj => { + if (!calcObj.effectIds?.length && !calcObj.proficiencyIds?.length) { + unset(prop, calcObj._key); + } + }); +} + +function parseCalculation(calcObj) { + const calcHash = cyrb53(calcObj.calculation || '0'); // If the cached parse calculation is equal to the calculation, skip - if (calcHash === calcObj.hash){ + if (calcHash === calcObj.hash) { return; } calcObj.hash = calcHash; try { - calcObj.parseNode = parse(calcObj.calculation); + calcObj.parseNode = parse(calcObj.calculation || '0'); calcObj.parseError = null; } catch (e) { let error = { @@ -100,6 +125,6 @@ function parseCalculation(calcObj){ message: prettifyParseError(e), }; calcObj.parseError = error; - calcObj.parseNode = errorNode.create({error}); + calcObj.parseNode = errorNode.create({ error: error.message }); } } diff --git a/app/imports/api/engine/computation/buildComputation/removeSchemaFields.js b/app/imports/api/engine/computation/buildComputation/removeSchemaFields.js index 9c783df0..f0813250 100644 --- a/app/imports/api/engine/computation/buildComputation/removeSchemaFields.js +++ b/app/imports/api/engine/computation/buildComputation/removeSchemaFields.js @@ -1,9 +1,9 @@ -import applyFnToKey from '../utility/applyFnToKey.js'; +import applyFnToKey from '../utility/applyFnToKey'; 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) ); }); diff --git a/app/imports/api/engine/computation/buildComputation/tests/computeInactiveStatus.testFn.js b/app/imports/api/engine/computation/buildComputation/tests/computeInactiveStatus.testFn.js index c7565156..c4119e2d 100644 --- a/app/imports/api/engine/computation/buildComputation/tests/computeInactiveStatus.testFn.js +++ b/app/imports/api/engine/computation/buildComputation/tests/computeInactiveStatus.testFn.js @@ -1,9 +1,11 @@ -import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation.js'; +import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation'; import { assert } from 'chai'; -import clean from '../../utility/cleanProp.testFn.js'; +import clean from '../../utility/cleanProp.testFn'; +import { applyNestedSetProperties } from '/imports/api/parenting/parentingFunctions'; -export default function(){ +export default function () { let computation = buildComputationFromProps(testProperties); + const bySelf = (propId, note) => assertDeactivatedBySelf(computation, propId, note); const byAncestor = (propId, note) => assertDeactivatedByAncestor(computation, propId, note); const active = (propId, note) => assertActive(computation, propId, note); @@ -24,22 +26,22 @@ export default function(){ // Notes active('NoteId', 'Notes should be active'); - byAncestor('NoteChildId', 'children of notes should always be inactive'); + active('NoteChildId', 'children of notes should be active'); } -function assertDeactivatedBySelf(computation, propId, note){ +function assertDeactivatedBySelf(computation, propId, note) { const prop = computation.propsById[propId]; assert.isTrue(prop.deactivatedBySelf, note); assert.isTrue(prop.inactive, note + '. The property should be inactive'); } -function assertDeactivatedByAncestor(computation, propId, note){ +function assertDeactivatedByAncestor(computation, propId, note) { const prop = computation.propsById[propId]; assert.isTrue(prop.deactivatedByAncestor, note); assert.isTrue(prop.inactive, 'The property should be inactive'); } -function assertActive(computation, propId, note){ +function assertActive(computation, propId, note) { const prop = computation.propsById[propId]; assert.isNotTrue(prop.inactive, note); assert.isNotTrue(prop.deactivatedBySelf, note); @@ -51,66 +53,68 @@ var testProperties = [ clean({ _id: 'itemUnequippedId', type: 'item', - ancestors: [{id: 'charId'}], + parentId: 'charId', }), clean({ _id: 'itemUnequippedChildId', type: 'folder', - ancestors: [{id: 'charId'}, {id: 'itemUnequippedId'}], + parentId: 'itemUnequippedId', }), clean({ _id: 'itemEquippedId', type: 'item', equipped: true, - ancestors: [{id: 'charId'}], + parentId: 'charId', }), clean({ _id: 'itemEquippedChildId', type: 'folder', - ancestors: [{id: 'charId'}, {id: 'itemEquippedId'}], + parentId: 'itemEquippedId', }), // Spells clean({ _id: 'spellPreparedId', type: 'spell', - ancestors: [{id: 'charId'}], + parentId: 'charId', prepared: true, }), clean({ _id: 'spellPreparedChildId', type: 'folder', - ancestors: [{id: 'charId'}, {id: 'spellPreparedId'}], + parentId: 'spellPreparedId', }), clean({ _id: 'spellAlwaysPreparedId', type: 'spell', - ancestors: [{id: 'charId'}], + parentId: 'charId', alwaysPrepared: true, }), clean({ _id: 'spellAlwaysPreparedChildId', type: 'folder', - ancestors: [{id: 'charId'}, {id: 'spellAlwaysPreparedId'}], + parentId: 'spellAlwaysPreparedId', }), clean({ _id: 'spellUnpreparedId', type: 'spell', - ancestors: [{id: 'charId'}], + parentId: 'charId', }), clean({ _id: 'spellUnpreparedChildId', type: 'folder', - ancestors: [{id: 'charId'}, {id: 'spellUnpreparedId'}], + parentId: 'spellUnpreparedId', }), // Notes clean({ _id: 'NoteId', type: 'note', - ancestors: [{id: 'charId'}], + parentId: 'charId', }), clean({ _id: 'NoteChildId', type: 'folder', - ancestors: [{id: 'charId'}, {id: 'NoteId'}], + parentId: 'NoteId', }), ]; + +applyNestedSetProperties(testProperties); diff --git a/app/imports/api/engine/computation/buildComputation/tests/computeSlotQuantityFilled.testFn.js b/app/imports/api/engine/computation/buildComputation/tests/computeSlotQuantityFilled.testFn.js index 8a8af817..5ee0ee23 100644 --- a/app/imports/api/engine/computation/buildComputation/tests/computeSlotQuantityFilled.testFn.js +++ b/app/imports/api/engine/computation/buildComputation/tests/computeSlotQuantityFilled.testFn.js @@ -1,8 +1,9 @@ -import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation.js'; +import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation'; import { assert } from 'chai'; -import clean from '../../utility/cleanProp.testFn.js'; +import clean from '../../utility/cleanProp.testFn'; +import { applyNestedSetProperties } from '/imports/api/parenting/parentingFunctions'; -export default function(){ +export default function () { const computation = buildComputationFromProps(testProperties); const totalFilled = computation.propsById['slotId'].totalFilled; assert.equal(totalFilled, 4); @@ -13,24 +14,25 @@ var testProperties = [ clean({ _id: 'slotId', type: 'propertySlot', - ancestors: [{id: 'charId'}], }), // Children clean({ _id: 'slotFillerId', - type: 'slotFiller', + type: 'folder', slotQuantityFilled: 3, slotFillerType: 'item', - ancestors: [{id: 'charId'}, {id: 'slotId'}], + parentId: 'slotId', }), clean({ _id: 'slotChildId', type: 'item', - ancestors: [{id: 'charId'}, {id: 'slotId'}], + parentId: 'slotId', }), clean({ _id: 'slotGrandchildId', type: 'effect', - ancestors: [{id: 'charId'}, {id: 'slotId'}, {id: 'slotChildId'}], + parentId: 'slotChildId', }), ]; + +applyNestedSetProperties(testProperties); diff --git a/app/imports/api/engine/computation/buildComputation/tests/computeToggleDependencies.testFn.js b/app/imports/api/engine/computation/buildComputation/tests/computeToggleDependencies.testFn.js index 268ea2ef..5ffe3fb3 100644 --- a/app/imports/api/engine/computation/buildComputation/tests/computeToggleDependencies.testFn.js +++ b/app/imports/api/engine/computation/buildComputation/tests/computeToggleDependencies.testFn.js @@ -1,8 +1,9 @@ -import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation.js'; +import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation'; import { assert } from 'chai'; -import clean from '../../utility/cleanProp.testFn.js'; +import clean from '../../utility/cleanProp.testFn'; +import { applyNestedSetProperties } from '/imports/api/parenting/parentingFunctions'; -export default function(){ +export default function () { const computation = buildComputationFromProps(testProperties); const hasLink = computation.dependencyGraph.hasLink; assert.include( @@ -37,38 +38,37 @@ var testProperties = [ _id: 'enabledToggleId', type: 'toggle', enabled: true, - ancestors: [{id: 'charId'}], }), clean({ _id: 'disabledToggleId', type: 'toggle', disabled: true, - ancestors: [{id: 'charId'}], }), clean({ _id: 'conditionToggleId', type: 'toggle', - ancestors: [{id: 'charId'}], }), // Children clean({ _id: 'enabledToggleChildId', type: 'folder', - ancestors: [{id: 'charId'}, {id: 'enabledToggleId'}], + parentId: 'enabledToggleId', }), clean({ _id: 'disabledToggleChildId', type: 'folder', - ancestors: [{id: 'charId'}, {id: 'disabledToggleId'}], + parentId: 'disabledToggleId', }), clean({ _id: 'conditionToggleChildId', type: 'folder', - ancestors: [{id: 'charId'}, {id: 'conditionToggleId'}], + parentId: 'conditionToggleId', }), clean({ _id: 'conditionToggleGrandChildId', type: 'folder', - ancestors: [{id: 'charId'}, {id: 'conditionToggleId'}, {id: 'conditionToggleChildId'}], + parentId: 'conditionToggleChildId', }), ]; + +applyNestedSetProperties(testProperties); diff --git a/app/imports/api/engine/computation/buildComputation/tests/linkCalculationDependencies.testFn.js b/app/imports/api/engine/computation/buildComputation/tests/linkCalculationDependencies.testFn.js index 29fd101a..60c5eb22 100644 --- a/app/imports/api/engine/computation/buildComputation/tests/linkCalculationDependencies.testFn.js +++ b/app/imports/api/engine/computation/buildComputation/tests/linkCalculationDependencies.testFn.js @@ -1,8 +1,9 @@ -import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation.js'; +import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation'; import { assert } from 'chai'; -import clean from '../../utility/cleanProp.testFn.js'; +import clean from '../../utility/cleanProp.testFn'; +import { applyNestedSetProperties } from '/imports/api/parenting/parentingFunctions'; -export default function(){ +export default function () { const computation = buildComputationFromProps(testProperties); const hasLink = computation.dependencyGraph.hasLink; const prop = (id) => computation.propsById[id]; @@ -32,7 +33,6 @@ var testProperties = [ clean({ _id: 'spellListId', type: 'spellList', - ancestors: [{id: 'charId'}], }), clean({ _id: 'childId', @@ -40,7 +40,7 @@ var testProperties = [ description: { text: 'DC {#spellList.dc} save or suck' }, - ancestors: [{id: 'charId'}, {id: 'spellListId'}], + parentId: 'spellListId', }), clean({ _id: 'grandchildId', @@ -48,7 +48,7 @@ var testProperties = [ dc: { calculation: '#spellList.dc + strength + wisdom.modifier' }, - ancestors: [{id: 'charId'}, {id: 'spellListId'}, {id: 'childId'}], + parentId: 'childId', }), clean({ _id: 'strengthId', @@ -57,6 +57,7 @@ var testProperties = [ baseValue: { calculation: '15 + ', }, - ancestors: [{id: 'charId'}], }), ]; + +applyNestedSetProperties(testProperties); diff --git a/app/imports/api/engine/computation/buildComputation/tests/linkInventory.testFn.js b/app/imports/api/engine/computation/buildComputation/tests/linkInventory.testFn.js index cb22544a..e412c8fa 100644 --- a/app/imports/api/engine/computation/buildComputation/tests/linkInventory.testFn.js +++ b/app/imports/api/engine/computation/buildComputation/tests/linkInventory.testFn.js @@ -1,8 +1,9 @@ -import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation.js'; +import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation'; import { assert } from 'chai'; -import clean from '../../utility/cleanProp.testFn.js'; +import clean from '../../utility/cleanProp.testFn'; +import { applyNestedSetProperties } from '/imports/api/parenting/parentingFunctions'; -export default function(){ +export default function () { const computation = buildComputationFromProps(testProperties); const hasLink = computation.dependencyGraph.hasLink; @@ -62,28 +63,28 @@ var testProperties = [ type: 'item', equipped: true, attuned: true, - ancestors: [{id: 'charId'}], }), clean({ _id: 'containerId', type: 'container', carried: true, - ancestors: [{id: 'charId'}], }), clean({ _id: 'childContainerId', type: 'container', carried: true, - ancestors: [{id: 'charId'}, {id: 'containerId'}], - }), - clean({ - _id: 'childItemId', - type: 'item', - ancestors: [{id: 'charId'}, {id: 'containerId'}], + parentId: 'containerId', }), clean({ _id: 'grandchildItemId', type: 'item', - ancestors: [{id: 'charId'}, {id: 'containerId'}, {id: 'childContainerId'}], + parentId: 'childContainerId', + }), + clean({ + _id: 'childItemId', + type: 'item', + parentId: 'containerId', }), ]; + +applyNestedSetProperties(testProperties); diff --git a/app/imports/api/engine/computation/buildComputation/tests/linkTypeDependencies.testfn.js b/app/imports/api/engine/computation/buildComputation/tests/linkTypeDependencies.testfn.js index b8f9d4f1..8b1d5f1d 100644 --- a/app/imports/api/engine/computation/buildComputation/tests/linkTypeDependencies.testfn.js +++ b/app/imports/api/engine/computation/buildComputation/tests/linkTypeDependencies.testfn.js @@ -1,8 +1,8 @@ -import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation.js'; +import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation'; import { assert } from 'chai'; -import clean from '../../utility/cleanProp.testFn.js'; +import clean from '../../utility/cleanProp.testFn'; -export default function(){ +export default function () { const computation = buildComputationFromProps(testProperties); const getLink = computation.dependencyGraph.hasLink; const getNode = computation.dependencyGraph.getNode; @@ -22,6 +22,5 @@ var testProperties = [ _id: 'strengthId', type: 'attribute', variableName: 'strength', - ancestors: [{id: 'charId'}], }), ]; diff --git a/app/imports/api/engine/computation/buildCreatureComputation.test.js b/app/imports/api/engine/computation/buildCreatureComputation.test.js index 33b3830b..08d18107 100644 --- a/app/imports/api/engine/computation/buildCreatureComputation.test.js +++ b/app/imports/api/engine/computation/buildCreatureComputation.test.js @@ -1,16 +1,16 @@ -import '/imports/api/simpleSchemaConfig.js'; -import { buildComputationFromProps } from './buildCreatureComputation.js'; +import '/imports/api/simpleSchemaConfig'; +import { buildComputationFromProps } from './buildCreatureComputation'; import { assert } from 'chai'; -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import computeInactiveStatus from './buildComputation/tests/computeInactiveStatus.testFn.js'; -import computeSlotQuantityFilled from './buildComputation/tests/computeSlotQuantityFilled.testFn.js'; -import computeToggleDependencies from './buildComputation/tests/computeToggleDependencies.testFn.js'; -import linkCalculationDependencies from './buildComputation/tests/linkCalculationDependencies.testFn.js'; -import linkInventory from './buildComputation/tests/linkInventory.testFn.js'; -import linkTypeDependencies from './buildComputation/tests/linkTypeDependencies.testFn.js'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; +import computeInactiveStatus from './buildComputation/tests/computeInactiveStatus.testFn'; +import computeSlotQuantityFilled from './buildComputation/tests/computeSlotQuantityFilled.testFn'; +import computeToggleDependencies from './buildComputation/tests/computeToggleDependencies.testFn'; +import linkCalculationDependencies from './buildComputation/tests/linkCalculationDependencies.testFn'; +import linkInventory from './buildComputation/tests/linkInventory.testFn'; +import linkTypeDependencies from '/imports/api/engine/computation/buildComputation/tests/linkTypeDependencies.testfn'; -describe('buildComputation', function(){ - it('Builds something at all', function(){ +describe('buildComputation', function () { + it('Builds something at all', function () { let computation = buildComputationFromProps(testProperties); assert.exists(computation); }); @@ -37,7 +37,7 @@ var testProperties = [ }), ]; -function clean(prop){ +function clean(prop) { let schema = CreatureProperties.simpleSchema(prop); return schema.clean(prop); } diff --git a/app/imports/api/engine/computation/buildCreatureComputation.js b/app/imports/api/engine/computation/buildCreatureComputation.ts similarity index 74% rename from app/imports/api/engine/computation/buildCreatureComputation.js rename to app/imports/api/engine/computation/buildCreatureComputation.ts index a736354c..d9c4bd2f 100644 --- a/app/imports/api/engine/computation/buildCreatureComputation.js +++ b/app/imports/api/engine/computation/buildCreatureComputation.ts @@ -1,19 +1,19 @@ -import { nodeArrayToTree } from '/imports/api/parenting/nodesToTree.js'; -import { DenormalisedOnlyCreaturePropertySchema as denormSchema } - from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import { getProperties, getCreature, getVariables } from '/imports/api/engine/loadCreatures.js'; -import computedOnlySchemas from '/imports/api/properties/computedOnlyPropertySchemasIndex.js'; -import computedSchemas from '/imports/api/properties/computedPropertySchemasIndex.js'; -import linkInventory from './buildComputation/linkInventory.js'; -import walkDown from './utility/walkdown.js'; -import parseCalculationFields from './buildComputation/parseCalculationFields.js'; -import computeInactiveStatus from './buildComputation/computeInactiveStatus.js'; -import computeToggleDependencies from './buildComputation/computeToggleDependencies.js'; -import linkCalculationDependencies from './buildComputation/linkCalculationDependencies.js'; -import linkTypeDependencies from './buildComputation/linkTypeDependencies.js'; -import computeSlotQuantityFilled from './buildComputation/computeSlotQuantityFilled.js'; -import CreatureComputation from './CreatureComputation.ts'; -import removeSchemaFields from './buildComputation/removeSchemaFields.js'; +import { applyNestedSetProperties } from '/imports/api/parenting/parentingFunctions'; +import { CreatureProperty, DenormalisedOnlyCreaturePropertySchema as denormSchema } + from '/imports/api/creature/creatureProperties/CreatureProperties'; +import { getProperties, getCreature, getVariables } from '/imports/api/engine/loadCreatures'; +import computedOnlySchemas from '/imports/api/properties/computedOnlyPropertySchemasIndex'; +import computedSchemas from '/imports/api/properties/computedPropertySchemasIndex'; +import linkInventory from './buildComputation/linkInventory'; +import walkDown from './utility/walkdown'; +import parseCalculationFields from './buildComputation/parseCalculationFields'; +import computeInactiveStatus from './buildComputation/computeInactiveStatus'; +import computeToggleDependencies from './buildComputation/computeToggleDependencies'; +import linkCalculationDependencies from './buildComputation/linkCalculationDependencies'; +import linkTypeDependencies from './buildComputation/linkTypeDependencies'; +import computeSlotQuantityFilled from './buildComputation/computeSlotQuantityFilled'; +import CreatureComputation from './CreatureComputation'; +import removeSchemaFields from './buildComputation/removeSchemaFields'; /** * Store index of properties @@ -29,7 +29,7 @@ import removeSchemaFields from './buildComputation/removeSchemaFields.js'; * computed toggles */ -export default function buildCreatureComputation(creatureId){ +export default function buildCreatureComputation(creatureId: string) { const creature = getCreature(creatureId); const variables = getVariables(creatureId); const properties = getProperties(creatureId); @@ -37,7 +37,9 @@ export default function buildCreatureComputation(creatureId){ return computation; } -export function buildComputationFromProps(properties, creature, variables){ +export function buildComputationFromProps( + properties: CreatureProperty[], creature, variables +) { const computation = new CreatureComputation(properties, creature, variables); // Dependency graph where edge(a, b) means a depends on b @@ -49,14 +51,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' @@ -75,6 +77,7 @@ export function buildComputationFromProps(properties, creature, variables){ // Add a place to store all the computation details prop._computationDetails = { calculations: [], + emptyCalculations: [], inlineCalculations: [], toggleAncestors: [], }; @@ -84,16 +87,17 @@ export function buildComputationFromProps(properties, creature, variables){ }); - // Get all the properties as trees based on their ancestors - let forest = nodeArrayToTree(properties); + // Get all the properties as a forest, with their nested set properties set + const forest = applyNestedSetProperties(properties); + // Walk the property trees computing things that need to be inherited - walkDown(forest, node => { + walkDown(forest.trees, node => { computeInactiveStatus(node); }); // Inactive status must be complete for the whole tree before toggle deps // are calculated - walkDown(forest, node => { - computeToggleDependencies(node, dependencyGraph); + walkDown(forest.trees, node => { + computeToggleDependencies(node, computation, forest); computeSlotQuantityFilled(node, dependencyGraph); }); diff --git a/app/imports/api/engine/computation/computeComputation/computeByType.js b/app/imports/api/engine/computation/computeComputation/computeByType.js index 0b5633e3..3affc066 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType.js @@ -1,12 +1,14 @@ -import _variable from './computeByType/computeVariable.js'; -import action from './computeByType/computeAction.js'; -import attribute from './computeByType/computeAttribute.js'; -import skill from './computeByType/computeSkill.js'; -import pointBuy from './computeByType/computePointBuy.js'; -import propertySlot from './computeByType/computeSlot.js'; -import container from './computeByType/computeContainer.js'; -import spellList from './computeByType/computeSpellList.js'; -import _calculation from './computeByType/computeCalculation.js'; +import _variable from './computeByType/computeVariable'; +import action from './computeByType/computeAction'; +import attribute from './computeByType/computeAttribute'; +import skill from './computeByType/computeSkill'; +import pointBuy from './computeByType/computePointBuy'; +import propertySlot from './computeByType/computeSlot'; +import container from './computeByType/computeContainer'; +import spellList from './computeByType/computeSpellList'; +import toggle from './computeByType/computeToggle'; +import trigger from './computeByType/computeTrigger'; +import _calculation from './computeByType/computeCalculation'; export default Object.freeze({ _variable, @@ -19,4 +21,6 @@ export default Object.freeze({ propertySlot, spell: action, spellList, + toggle, + trigger, }); diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeAction.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeAction.js deleted file mode 100644 index c52dec00..00000000 --- a/app/imports/api/engine/computation/computeComputation/computeByType/computeAction.js +++ /dev/null @@ -1,35 +0,0 @@ -export default function computeAction(computation, node){ - const prop = node.data; - if (prop.uses){ - prop.usesLeft = prop.uses.value - (prop.usesUsed || 0); - if (!prop.usesLeft){ - prop.insufficientResources = true; - } - } - computeResources(computation, node); - if (!prop.resources) return; - prop.resources.itemsConsumed.forEach(itemConsumed => { - if (!itemConsumed.itemId) return; - if (itemConsumed.available < itemConsumed.quantity?.value){ - prop.insufficientResources = true; - } - }); - prop.resources.attributesConsumed.forEach(attConsumed => { - if (!attConsumed.variableName) return; - if (attConsumed.available < attConsumed.quantity?.value){ - prop.insufficientResources = true; - } - }); -} - -function computeResources(computation, node){ - const resources = node.data?.resources; - if (!resources) return; - resources.attributesConsumed.forEach(attConsumed => { - if (!attConsumed.variableName) return; - const att = computation.scope[attConsumed.variableName]; - if (!att._id) return; - attConsumed.available = att.value; - attConsumed.statName = att.name; - }); -} diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeAction.ts b/app/imports/api/engine/computation/computeComputation/computeByType/computeAction.ts new file mode 100644 index 00000000..ea51f905 --- /dev/null +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeAction.ts @@ -0,0 +1,44 @@ +import CreatureComputation from '/imports/api/engine/computation/CreatureComputation'; +import { Node } from 'ngraph.graph'; + +export default function computeAction(computation: CreatureComputation, node: Node) { + const prop = node.data; + if (Number.isFinite(prop.uses?.value)) { + prop.usesLeft = prop.uses.value - (prop.usesUsed || 0); + if (!prop.usesLeft) { + prop.insufficientResources = true; + } + } + computeResources(computation, node); + if (!prop.resources) return; + prop.resources.conditions?.forEach(conObj => { + const condition = conObj.condition; + if (!condition) return; + if (condition.calculation && !condition.value) { + prop.insufficientResources = true; + } + }); + prop.resources.itemsConsumed?.forEach(itemConsumed => { + if (!itemConsumed?.itemId || itemConsumed.available < itemConsumed.quantity?.value) { + prop.insufficientResources = true; + } + }); + prop.resources.attributesConsumed?.forEach(attConsumed => { + if (!attConsumed?.variableName) return; + if (!(attConsumed.available >= attConsumed.quantity?.value)) { + prop.insufficientResources = true; + } + }); +} + +function computeResources(computation, node) { + const resources = node.data?.resources; + if (!resources) return; + resources.attributesConsumed.forEach(attConsumed => { + if (!attConsumed.variableName) return; + const att = computation.scope[attConsumed.variableName]; + if (!att?._id) return; + attConsumed.available = att.value; + attConsumed.statName = att.name; + }); +} diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeCalculation.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeCalculation.js index 054fc7f9..09ba91d4 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType/computeCalculation.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeCalculation.js @@ -1,14 +1,52 @@ -import evaluateCalculation from '../../utility/evaluateCalculation.js'; +import call from '/imports/parser/parseTree/call'; +import constant from '/imports/parser/parseTree/constant'; +import operator from '/imports/parser/parseTree/operator'; +import parenthesis from '/imports/parser/parseTree/parenthesis'; +import resolve from '/imports/parser/resolve'; +import toPrimitiveOrString from '/imports/parser/toPrimitiveOrString'; -export default function computeCalculation(computation, node){ +export default async function computeCalculation(computation, node) { const calcObj = node.data; - evaluateCalculation(calcObj, computation.scope); - aggregateCalculationEffects(node, computation); + if (!calcObj) return; + // resolve the parse node into the initial value + await resolveCalculationNode(calcObj, calcObj.parseNode, computation.scope); + + // link the effects and proficiencies + linkCalculationEffects(node, computation); + linkCalculationProficiencies(node, computation) + + // Store the unaffected value + if (calcObj.effectIds || calcObj.proficiencyIds) { + calcObj.unaffected = toPrimitiveOrString(calcObj.valueNode); + } + + // Aggregate the effects and proficiencies + aggregateCalculationEffects(calcObj, id => computation.propsById[id]); + aggregateCalculationProficiencies(calcObj, id => computation.propsById[id], computation.scope['proficiencyBonus']?.value || 0); + + // Resolve the valueNode after effects and proficiencies have been applied to it + await resolveCalculationNode(calcObj, calcObj.valueNode, computation.scope); + + // Store the value as a primitive + calcObj.value = toPrimitiveOrString(calcObj.valueNode); + + // remove the working fields + delete calcObj._parseLevel; + delete calcObj._localScope; } -export function aggregateCalculationEffects(node, computation){ +export async function resolveCalculationNode(calculation, parseNode, scope, givenContext) { + if (!parseNode) throw new Error('parseNode is required'); + const fn = calculation._parseLevel; + const calculationScope = { ...calculation._localScope, ...scope }; + const { result: resultNode, context } = await resolve(fn, parseNode, calculationScope, givenContext); + calculation.errors = context.errors; + calculation.valueNode = resultNode; +} + +function linkCalculationEffects(node, computation) { const calcObj = node.data; - delete calcObj.effects; + delete calcObj.effectIds; computation.dependencyGraph.forEachLinkedNode( node.id, (linkedNode, link) => { @@ -20,29 +58,172 @@ export function aggregateCalculationEffects(node, computation){ if (linkedNode.data.inactive) return; // Collate effects - calcObj.effects = calcObj.effects || []; - calcObj.effects.push({ - _id: linkedNode.data._id, - name: linkedNode.data.name, - operation: linkedNode.data.operation, - amount: linkedNode.data.amount && { - value: linkedNode.data.amount.value, - //parseNode: linkedNode.data.amount.parseNode, - }, - // ancestors: linkedNode.data.ancestors, - }); + calcObj.effectIds = calcObj.effectIds || []; + calcObj.effectIds.push(linkedNode.data._id); }, true // enumerate only outbound links ); - if (calcObj.effects && typeof calcObj.value === 'number'){ - calcObj.baseValue = calcObj.value; - calcObj.effects.forEach(effect => { - if ( - effect.operation === 'add' && - effect.amount && typeof effect.amount.value === 'number' - ){ - calcObj.value += effect.amount.value - } +} + +export function aggregateCalculationEffects(calcObj, getEffectFromId) { + // dictionary of {[operation]: parseNode} + const aggregator = {}; + // Store all effect values + calcObj.effectIds?.forEach(effectId => { + const effect = getEffectFromId(effectId); + const op = effect.operation; + switch (op) { + case undefined: + break; + // Conditionals stored as a list of text + case 'conditional': + if (!aggregator[op]) aggregator[op] = []; + aggregator[op].push(effect.text); + break; + // Adv/Dis and Fails just count instances + case 'advantage': + case 'disadvantage': + case 'fail': + if (calcObj[op] === undefined) calcObj[op] = 0; + calcObj[op]++; + break; + // Math functions store value parseNodes + case 'base': + case 'add': + case 'mul': + case 'min': + case 'max': + case 'set': + if (!aggregator[op]) aggregator[op] = []; + aggregator[op].push(effect.amount.valueNode); + break; + // No case for passiveAdd, it doesn't make sense in this context + } + }); + /** + * Aggregate the effects in a parse tree like so + * x = max(...base, unaffectedValue) + * x = x + sum(...add) + * x = x * mul(...mul) + * x = min(...min, x) + * x = max(...max, x) + * x = set(last(...set))a + */ + // Set + // If we do set, return early, nothing else matters + if (aggregator.set) { + calcObj.valueNode = aggregator.set[aggregator.set.length - 1]; + return; + } + // Base value + if (aggregator.base) { + calcObj.valueNode = call.create({ + functionName: 'max', + args: [calcObj.valueNode, aggregator.base] + }); + } + // Add + aggregator.add?.forEach(node => { + calcObj.valueNode = operator.create({ + left: calcObj.valueNode, + right: node, + operator: '+' + }); + }); + // Multiply + if (aggregator.mul) { + // Wrap the previous node in brackets if it's another operator + if (calcObj.valueNode.parseType === 'operator') { + calcObj.valueNode = parenthesis.create({ + content: calcObj.valueNode + }); + } + // Append all multiplications + aggregator.mul.forEach(node => { + calcObj.valueNode = operator.create({ + left: calcObj.valueNode, + right: node, + operator: '*' + }); + }); + } + // Min + if (aggregator.min) { + calcObj.valueNode = call.create({ + functionName: 'max', + args: [calcObj.valueNode, ...aggregator.min] + }); + } + // Max + if (aggregator.max) { + calcObj.valueNode = call.create({ + functionName: 'min', + args: [calcObj.valueNode, ...aggregator.max] }); } } + +function linkCalculationProficiencies(node, computation) { + const calcObj = node.data; + delete calcObj.proficiencyIds; + delete calcObj.proficiency; + + // 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; + // Collate proficiencies + calcObj.proficiencyIds = calcObj.proficiencyIds || []; + calcObj.proficiencyIds.push(linkedNode.data._id); + }, + true // enumerate only outbound links + ); +} + +export function aggregateCalculationProficiencies(calcObj, getProficiencyFromId, profBonus) { + if (!calcObj.proficiencyIds) return; + // Apply the highest proficiency, marking all others as overridden + calcObj.proficiency = 0; + calcObj.proficiencyBonus = 0; + let currentProf; + calcObj.proficiencyIds.forEach(profId => { + const profProp = getProficiencyFromId(profId) + if (!profProp) { + console.warn('proficiency linked but not found ', profId); + } + // Compute the proficiency and value + let proficiency, value; + if (profProp.type === 'proficiency') { + proficiency = profProp.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 (profProp.type === 'skill') { + value = profProp.value || 0; + proficiency = profProp.proficiency || 0; + } + if (value > calcObj.proficiencyBonus) { + if (currentProf) currentProf.overridden = true; + calcObj.proficiencyBonus = value; + calcObj.proficiency = proficiency; + currentProf = profProp; + } else { + profProp.overridden = true; + } + }); + calcObj.valueNode = operator.create({ + left: calcObj.valueNode, + right: constant.create({ value: calcObj.proficiencyBonus }), + operator: '+' + }); +} diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeContainer.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeContainer.js index 61b4b79a..c77611cf 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType/computeContainer.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeContainer.js @@ -1,11 +1,17 @@ -import aggregate from './computeVariable/aggregate/index.js'; +import aggregate from './computeVariable/aggregate/index'; +import { safeStrip } from '/imports/api/engine/computation/utility/stripFloatingPointOddities'; -export default function computeContainer(computation, node){ +export default function computeContainer(computation, node) { if (!node.data) node.data = {}; aggregateLinks(computation, node); + + // Clean up floating points + const prop = node.data; + prop.contentsWeight = safeStrip(prop.contentsWeight); + prop.carriedWeight = safeStrip(prop.carriedWeight); } -function aggregateLinks(computation, node){ +function aggregateLinks(computation, node) { computation.dependencyGraph.forEachLinkedNode( node.id, (linkedNode, link) => { @@ -13,7 +19,7 @@ function aggregateLinks(computation, node){ // Ignore inactive props if (linkedNode.data.inactive) return; // Aggregate inventory links - aggregate.inventory({node, linkedNode, link}); + aggregate.inventory({ node, linkedNode, link }); }, true // enumerate only outbound links ); diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computePointBuy.js b/app/imports/api/engine/computation/computeComputation/computeByType/computePointBuy.js index c2cc7606..47f83925 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType/computePointBuy.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computePointBuy.js @@ -1,22 +1,15 @@ import { has } from 'lodash'; -import evaluateCalculation from '../../utility/evaluateCalculation.js'; +import { resolveCalculationNode } from '/imports/api/engine/computation/computeComputation/computeByType/computeCalculation'; -export default function computePointBuy(computation, node) { +export default async function computePointBuy(computation, node) { const prop = node.data; - const tableMin = prop.min?.value || null; - const tableMax = prop.max?.value || null; + const min = has(prop, 'min.value') ? prop.min.value : null; + const max = has(prop, 'max.value') ? prop.max.value : null; prop.spent = 0; - prop.values?.forEach(row => { - // Clean up added properties - // delete row.tableId; - // delete row.tableName; - // delete row.type; - + for (const row of prop.values || []) { row.spent = 0; if (row.value === undefined) return; - const min = has(row, 'min.value') ? row.min.value : tableMin; - const max = has(row, 'max.value') ? row.max.value : tableMax; - const costFunction = EJSON.clone(row.cost || prop.cost); + const costFunction = EJSON.clone(prop.cost); if (costFunction) costFunction.parseLevel = 'reduce'; // Check min and max @@ -28,7 +21,9 @@ export default function computePointBuy(computation, node) { } // Evaluate the cost function if (!costFunction) return; - evaluateCalculation(costFunction, { ...computation.scope, value: row.value }); + await resolveCalculationNode(costFunction, costFunction.parseNode, { + ...computation.scope, value: row.value + }); // Write calculation errors costFunction.errors?.forEach(error => { if (error?.message) { @@ -41,7 +36,7 @@ export default function computePointBuy(computation, node) { row.spent = costFunction.value; prop.spent += costFunction.value; } - }); + } prop.pointsLeft = (prop.total?.value || 0) - (prop.spent || 0); if (prop.spent > prop.total?.value) { prop.errors = prop.errors || []; diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeSlot.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeSlot.js index 9e9dc07b..8128719a 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType/computeSlot.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeSlot.js @@ -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; } } diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeSpellList.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeSpellList.js index 63dc6003..499ddb97 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType/computeSpellList.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeSpellList.js @@ -2,5 +2,9 @@ export default function computeSpelllist(computation, node) { const prop = node.data; const ability = computation.scope[prop.ability]; - prop.abilityMod = ability?.modifier || 0; -} \ No newline at end of file + if (Number.isFinite(ability?.modifier)) { + prop.abilityMod = ability.modifier; + } else if (Number.isFinite(ability?.value)) { + prop.abilityMod = ability.value; + } +} diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeToggle.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeToggle.js new file mode 100644 index 00000000..bba5c4ff --- /dev/null +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeToggle.js @@ -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; + } +} diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeTrigger.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeTrigger.js new file mode 100644 index 00000000..f80e6f13 --- /dev/null +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeTrigger.js @@ -0,0 +1,48 @@ +import { get, set } from 'lodash'; +import { getEffectTagTargets } from '/imports/api/engine/computation/buildComputation/linkTypeDependencies'; + +export default function computeTrigger(computation, node) { + const prop = node.data; + + // Triggers that aren't active aren't linked to properties + if (prop.inactive) return; + + // Link triggers to all the properties that would fire them when applied + const tagTargets = getEffectTagTargets(prop, computation); + for (const targetId of tagTargets) { + const targetProp = computation.propsById[targetId]; + switch (prop.event) { + case 'doActionProperty': + // Only apply if the trigger matches this property type + if (targetProp.type === prop.actionPropertyType) { + setTrigger(prop, targetProp, 'triggerIds'); + } + // Or on an item used as ammo + else if (prop.actionPropertyType === 'ammo' && targetProp.type === 'item') { + setTrigger(prop, targetProp, 'ammoTriggerIds'); + } + break; + case 'damageProperty': + // Only apply to attributes + if (targetProp.type === 'attribute') { + setTrigger(prop, targetProp, 'damageTriggerIds'); + } + break; + case 'check': + // Only apply to attributes and skills + if (targetProp.type === 'attribute' || targetProp.type === 'skill') { + setTrigger(prop, targetProp, 'checkTriggerIds'); + } + break; + } + } +} + +function setTrigger(prop, targetProp, field = 'triggerIds') { + let triggerIdArray = get(targetProp, `${field}.${prop.timing}`); + if (!triggerIdArray) { + triggerIdArray = []; + set(targetProp, `${field}.${prop.timing}`, triggerIdArray); + } + triggerIdArray.push(prop._id); +} \ No newline at end of file diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable.js index 279ad3d6..19e06a33 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable.js @@ -1,13 +1,13 @@ -import aggregate from './computeVariable/aggregate/index.js'; -import computeVariableAsAttribute from './computeVariable/computeVariableAsAttribute.js'; -import computeVariableAsSkill from './computeVariable/computeVariableAsSkill.js'; -import computeVariableAsConstant from './computeVariable/computeVariableAsConstant.js'; -import computeVariableAsClass from './computeVariable/computeVariableAsClass.js'; -import computeVariableAsToggle from './computeVariable/computeVariableAsToggle.js'; -import computeImplicitVariable from './computeVariable/computeImplicitVariable.js'; -import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js'; +import aggregate from './computeVariable/aggregate/index'; +import computeVariableAsAttribute from './computeVariable/computeVariableAsAttribute'; +import computeVariableAsSkill from './computeVariable/computeVariableAsSkill'; +import computeVariableAsConstant from './computeVariable/computeVariableAsConstant'; +import computeVariableAsClass from './computeVariable/computeVariableAsClass'; +import computeVariableAsToggle from './computeVariable/computeVariableAsToggle'; +import computeImplicitVariable from './computeVariable/computeImplicitVariable'; +import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX'; -export default function computeVariable(computation, node){ +export default function computeVariable(computation, node) { const scope = computation.scope; if (!node.data) node.data = {}; aggregateLinks(computation, node); @@ -15,7 +15,7 @@ export default function computeVariable(computation, node){ // Don't add to the scope if the node id is not a legitimate variable name // Without this `some.thing` could break the entire sheet as a database key if (!VARIABLE_NAME_REGEX.test(node.id)) return; - if (node.data.definingProp){ + if (node.data.definingProp) { // Add the defining variable to the scope scope[node.id] = node.data.definingProp } else { @@ -24,19 +24,25 @@ export default function computeVariable(computation, node){ } } -function aggregateLinks(computation, node){ +function aggregateLinks(computation, node) { computation.dependencyGraph.forEachLinkedNode( node.id, (linkedNode, link) => { if (!linkedNode.data) linkedNode.data = {}; // Ignore inactive props if (linkedNode.data.inactive) return; + // Ignore point buy rows if their base table is inactive + if ( + linkedNode.data.tableId + && computation.propsById[linkedNode.data.tableId]?.inactive + ) return; // Apply all the aggregations - let arg = {node, linkedNode, link, computation}; + let arg = { node, linkedNode, link, computation }; aggregate.classLevel(arg); aggregate.damageMultiplier(arg); aggregate.definition(arg); aggregate.effect(arg); + aggregate.eventDefinition(arg); aggregate.inventory(arg); aggregate.proficiency(arg); }, @@ -44,7 +50,7 @@ function aggregateLinks(computation, node){ ); } -function combineAggregations(computation, node){ +function combineAggregations(computation, node) { combineMultiplierAggregator(node); node.data.overridenProps?.forEach(prop => { computeVariableProp(computation, node, prop); @@ -52,51 +58,51 @@ function combineAggregations(computation, node){ computeVariableProp(computation, node, node.data.definingProp); } -function computeVariableProp(computation, node, prop){ +function computeVariableProp(computation, node, prop) { if (!prop) return; // Combine damage multipliers in all props so that they can't be overridden - if (node.data.immunity){ + if (node.data.immunity) { prop.immunity = node.data.immunity; prop.immunities = node.data.immunities; } - if (node.data.resistance){ + if (node.data.resistance) { prop.resistance = node.data.resistance; prop.resistances = node.data.resistances; } - if (node.data.vulnerability){ + if (node.data.vulnerability) { prop.vulnerability = node.data.vulnerability; prop.vulnerabilities = node.data.vulnerabilities; } - if (prop.type === 'attribute'){ + if (prop.type === 'attribute') { computeVariableAsAttribute(computation, node, prop); - } else if (prop.type === 'skill'){ + } else if (prop.type === 'skill') { computeVariableAsSkill(computation, node, prop); - } else if (prop.type === 'constant'){ + } else if (prop.type === 'constant') { computeVariableAsConstant(computation, node, prop); - } else if (prop.type === 'class'){ + } else if (prop.type === 'class') { computeVariableAsClass(computation, node, prop); - } else if (prop.type === 'toggle'){ + } else if (prop.type === 'toggle') { computeVariableAsToggle(computation, node, prop); } } -function combineMultiplierAggregator(node){ +function combineMultiplierAggregator(node) { // get a reference to the aggregator const aggregator = node.data.multiplierAggregator; if (!aggregator) return; // Combine - if (aggregator.immunities?.length){ + if (aggregator.immunities?.length) { node.data.immunity = true; node.data.immunities = aggregator.immunities; } - if (aggregator.resistances?.length){ + if (aggregator.resistances?.length) { node.data.resistance = true; node.data.resistances = aggregator.resistances; } - if (aggregator.vulnerabilities?.length){ + if (aggregator.vulnerabilities?.length) { node.data.vulnerability = true; node.data.vulnerabilities = aggregator.vulnerabilities; } diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateDefinition.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateDefinition.js index d665b284..ebd6a989 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateDefinition.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateDefinition.js @@ -1,5 +1,5 @@ -export default function aggregateDefinition({node, linkedNode, link}){ +export default function aggregateDefinition({ node, linkedNode, link }) { // Look at all definition links if (link.data !== 'definition') return; @@ -12,7 +12,7 @@ export default function aggregateDefinition({node, linkedNode, link}){ !definingProp || prop.type !== 'pointBuyRow' && ( definingProp.type === 'pointBuyRow' || - prop.order > definingProp.order + prop.left > definingProp.left ) ) { // override the current defining prop @@ -32,30 +32,19 @@ export default function aggregateDefinition({node, linkedNode, link}){ if (propBaseValue === undefined) return; // Store a summary of the definition as a base value effect - node.data.effects = node.data.effects || []; + node.data.definitions = node.data.definitions || []; + if (prop.type === 'pointBuyRow') { - node.data.effects.push({ - _id: prop.tableId, - name: prop.tableName, - operation: 'base', - amount: { value: propBaseValue }, - type: 'pointBuy', - }); + node.data.definitions.push({ _id: prop.tableId, type: 'pointBuy', row: prop.index }); } else { - node.data.effects.push({ - _id: prop._id, - name: prop.name, - operation: 'base', - amount: { value: propBaseValue }, - type: prop.type, - }); + node.data.definitions.push({ _id: prop._id, type: node.data.type }); } - if (node.data.baseValue === undefined || propBaseValue > node.data.baseValue){ + if (node.data.baseValue === undefined || propBaseValue > node.data.baseValue) { node.data.baseValue = propBaseValue; } } -function overrideProp(prop, node){ +function overrideProp(prop, node) { if (!prop) return; prop.overridden = true; if (!node.data.overriddenProps) node.data.overriddenProps = []; diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateEffect.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateEffect.js index ae609923..a06d3872 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateEffect.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateEffect.js @@ -1,5 +1,3 @@ -import { pick } from 'lodash'; - export default function aggregateEffect({ node, linkedNode, link }) { if (link.data !== 'effect') return; // store the effect aggregator, its presence indicates that the variable is @@ -19,33 +17,15 @@ export default function aggregateEffect({ node, linkedNode, link }) { rollBonus: [], }; - // Store a summary of the effect itself - node.data.effects = node.data.effects || []; - // Store either just - let effectAmount; - if (!linkedNode.data.amount) { - effectAmount = undefined; - } else if (typeof linkedNode.data.amount.value === 'string') { - effectAmount = pick(linkedNode.data.amount, [ - 'calculation', 'parseNode', 'parseError', 'value' - ]); - } else { - effectAmount = pick(linkedNode.data.amount, ['value']); - } - node.data.effects.push({ - _id: linkedNode.data._id, - name: linkedNode.data.name, - operation: linkedNode.data.operation, - amount: effectAmount, - type: linkedNode.data.type, - text: linkedNode.data.text, - // ancestors: linkedNode.data.ancestors, - }); + // Store a link to the effect + node.data.effectIds = node.data.effectIds || []; + node.data.effectIds.push(linkedNode.data._id); // get a shorter reference to the aggregator document const aggregator = node.data.effectAggregator; // Get the result of the effect - const result = linkedNode.data.amount?.value; + let result = linkedNode.data.amount?.value; + if (typeof result !== 'number') result = undefined; // Aggregate the effect based on its operation switch (linkedNode.data.operation) { diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateEventDefinition.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateEventDefinition.js new file mode 100644 index 00000000..8e1dfda6 --- /dev/null +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateEventDefinition.js @@ -0,0 +1,22 @@ + +export default function aggregateEventDefinition({ node, linkedNode, link }) { + // Look at all event definition links + if (link.data !== 'eventDefinition') return; + + // Store which property is THE defining event and which are overridden + const prop = linkedNode.data; + // get current defining event + const definingEvent = node.data.definingEvent; + // Find the last defining event + if ( + !definingEvent || + prop.left > definingEvent.left + ) { + // override the current defining prop + if (definingEvent) definingEvent.overridden = true; + // set this prop as the new defining prop + node.data.definingEvent = prop; + } else { + prop.overridden = true; + } +} diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateInventory.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateInventory.js index 6dd01ba2..a7291388 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateInventory.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateInventory.js @@ -1,25 +1,25 @@ -export default function aggregateInventory({node, linkedNode, link}){ +export default function aggregateInventory({ node, linkedNode, link }) { let linkedProp = linkedNode.data || {}; const prop = node.data; - switch (link.data){ + switch (link.data) { case 'attunedItem': prop.baseValue = (prop.baseValue || 0) + 1; return; case 'equippedItem': - if (node.id === 'weightEquipment'){ + if (node.id === 'weightEquipment') { prop.baseValue = (prop.baseValue || 0) + weight(linkedProp); - } else if (node.id === 'valueEquipment'){ + } else if (node.id === 'valueEquipment') { prop.baseValue = (prop.baseValue || 0) + value(linkedProp); } return; case 'containerContents': // Add this property's weights and values to the container - if (!prop.weightless){ + if (!prop.weightless) { prop.contentsWeight = (prop.contentsWeight || 0) + weight(linkedProp); - if (prop.carried){ + if (prop.carried && !prop.contentsWeightless) { prop.carriedWeight = (prop.carriedWeight || 0) + carriedWeight(linkedProp); } } @@ -30,39 +30,39 @@ export default function aggregateInventory({node, linkedNode, link}){ return; case 'inventoryStats': - if (node.id === 'weightTotal'){ + if (node.id === 'weightTotal') { prop.baseValue = (prop.baseValue || 0) + weight(linkedProp); - } else if (node.id === 'valueTotal'){ + } else if (node.id === 'valueTotal') { prop.baseValue = (prop.baseValue || 0) + value(linkedProp); - } else if (node.id === 'weightCarried'){ + } else if (node.id === 'weightCarried') { prop.baseValue = (prop.baseValue || 0) + carriedWeight(linkedProp); - } else if (node.id === 'valueCarried'){ + } else if (node.id === 'valueCarried') { prop.baseValue = (prop.baseValue || 0) + carriedValue(linkedProp); } return; } } -function quantity(prop){ - if (typeof prop.quantity === 'number'){ +function quantity(prop) { + if (typeof prop.quantity === 'number') { return prop.quantity; } else { return 1; } } -function weight(prop){ +function weight(prop) { return (prop.weight || 0) * quantity(prop) + (prop.contentsWeight || 0); } -function carriedWeight(prop){ +function carriedWeight(prop) { return (prop.weight || 0) * quantity(prop) + (prop.carriedWeight || 0); } -function value (prop){ +function value(prop) { return (prop.value || 0) * quantity(prop) + (prop.contentsValue || 0); } -function carriedValue (prop){ +function carriedValue(prop) { return (prop.value || 0) * quantity(prop) + (prop.carriedValue || 0); } diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateProficiency.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateProficiency.js index ea4383e6..a89cfa08 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateProficiency.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateProficiency.js @@ -1,21 +1,28 @@ -export default function aggregateProficiency({node, linkedNode, link}){ +export default function aggregateProficiency({ node, linkedNode, link }) { if ( link.data !== 'proficiency' && !(link.data === 'definition' && linkedNode.data.type === 'skill') ) return; let proficiency; - if (link.data === 'proficiency'){ + if (link.data === 'proficiency') { proficiency = linkedNode.data.value || 0; - } else if (link.data === 'definition' && linkedNode.data.type === 'skill'){ + } else if (link.data === 'definition' && linkedNode.data.type === 'skill') { proficiency = linkedNode.data.baseProficiency || 0; } else { return; } + + if (proficiency) { + // Store a link to the proficiency + node.data.proficiencyIds = node.data.proficiencyIds || []; + node.data.proficiencyIds.push(linkedNode.data._id); + } + // Store the highest proficiency if ( node.data.proficiency === undefined || proficiency > node.data.proficiency - ){ + ) { node.data.proficiency = proficiency; } } diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/index.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/index.js index 4b455afa..c0e32be6 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/index.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/index.js @@ -1,15 +1,17 @@ -import definition from './aggregateDefinition.js'; -import damageMultiplier from './aggregateDamageMultiplier.js'; -import effect from './aggregateEffect.js'; -import proficiency from './aggregateProficiency.js'; -import classLevel from './aggregateClassLevel.js'; -import inventory from './aggregateInventory.js'; +import definition from './aggregateDefinition'; +import damageMultiplier from './aggregateDamageMultiplier'; +import effect from './aggregateEffect'; +import eventDefinition from './aggregateEventDefinition'; +import proficiency from './aggregateProficiency'; +import classLevel from './aggregateClassLevel'; +import inventory from './aggregateInventory'; export default Object.freeze({ classLevel, damageMultiplier, definition, effect, + eventDefinition, inventory, proficiency, }); diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeImplicitVariable.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeImplicitVariable.js index 13dcc7f2..bc757c85 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeImplicitVariable.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeImplicitVariable.js @@ -1,61 +1,61 @@ -import getAggregatorResult from './getAggregatorResult.js'; +import getAggregatorResult from './getAggregatorResult'; /* * Variables with effects, proficiencies, or damage multipliers but no defining * properties are added to the scope as implicit variables */ - export default function computeImplicitVariable(node){ - const prop = {}; +export default function computeImplicitVariable(node) { + const prop = {}; // Combine damage multipliers - if (node.data.immunity){ + if (node.data.immunity) { prop.immunity = node.data.immunity; prop.immunities = node.data.immunities; } - if (node.data.resistance){ + if (node.data.resistance) { prop.resistance = node.data.resistance; prop.resistances = node.data.resistances; } - if (node.data.vulnerability){ + if (node.data.vulnerability) { prop.vulnerability = node.data.vulnerability; prop.vulnerabilities = node.data.vulnerabilities; } - const result = getAggregatorResult(node); - if (result !== undefined){ - prop.value = result; - } - if (node.data.proficiency !== undefined){ - prop.proficiency = node.data.proficiency; - } + const result = getAggregatorResult(node); + if (result !== undefined) { + prop.value = result; + } + if (node.data.proficiency !== undefined) { + prop.proficiency = node.data.proficiency; + } - // denormalise class level aggregator - let classLevelAgg = node.data.classLevelAggregator; - if (classLevelAgg){ - prop.level = classLevelAgg.level; - } + // denormalise class level aggregator + let classLevelAgg = node.data.classLevelAggregator; + if (classLevelAgg) { + prop.level = classLevelAgg.level; + } - // denormalise the effect aggregator fields - 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; - } - // Passive bonus - prop.passiveBonus = aggregator.passiveAdd; - // conditional benefits - prop.conditionalBenefits = aggregator.conditional; - // Roll bonuses - prop.rollBonus = aggregator.rollBonus; - // Forced to fail - prop.fail = aggregator.fail; - // Rollbonus - prop.rollBonuses = aggregator.rollBonus; - } + // denormalise the effect aggregator fields + 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; + } + // Passive bonus + prop.passiveBonus = aggregator.passiveAdd; + // conditional benefits + prop.conditionalBenefits = aggregator.conditional; + // Roll bonuses + prop.rollBonus = aggregator.rollBonus; + // Forced to fail + prop.fail = aggregator.fail; + // Rollbonus + prop.rollBonuses = aggregator.rollBonus; + } - return prop; - } + return prop; +} diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsAttribute.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsAttribute.js index 6ec5df2d..b2b5902a 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsAttribute.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsAttribute.js @@ -1,16 +1,38 @@ -import getAggregatorResult from './getAggregatorResult.js'; +import getAggregatorResult from './getAggregatorResult'; -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) { + prop.damage = prop.total; + } + if (!prop.ignoreUpperLimit && prop.damage < 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); } @@ -24,6 +46,8 @@ export default function computeVariableAsAttribute(computation, node, prop){ prop.baseValue === undefined || undefined - // Store effects - prop.effects = node.data.effects; + // Store effects and proficiencies + prop.effectIds = node.data.effectIds; + prop.definitions = node.data.definitions; + prop.proficiencyIds = node.data.proficiencyIds; } diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsConstant.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsConstant.js index 898d2c5e..9f2aa184 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsConstant.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsConstant.js @@ -1,12 +1,13 @@ -import { parse } from '/imports/parser/parser.js'; +import { parse } from '/imports/parser/parser'; -export default function computeVariableAsConstant(computation, node, prop){ +export default function computeVariableAsConstant(computation, node, prop) { let string = prop.calculation; if (!string) return; let parseNode; try { parseNode = parse(string); } catch (e) { + console.error(e); return; } prop.value = parseNode; diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsSkill.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsSkill.js index 01d34101..d57d3692 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsSkill.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsSkill.js @@ -1,12 +1,12 @@ -import aggregate from './aggregate/index.js'; +import aggregate from './aggregate/index'; -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 { @@ -33,11 +33,13 @@ export default function computeVariableAsSkill(computation, node, prop){ const aggregator = node.data.effectAggregator; const aggregatorBase = aggregator?.base || 0; - // Store effects - prop.effects = node.data.effects; + // Store effects and proficiencies + prop.effectIds = node.data.effectIds; + prop.definitions = node.data.definitions; + prop.proficiencyIds = node.data.proficiencyIds; // 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 +54,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 +90,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 +100,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); }, diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsToggle.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsToggle.js index 0c3c41a9..f38ecc60 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsToggle.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsToggle.js @@ -1,7 +1,7 @@ -import getAggregatorResult from './getAggregatorResult.js'; +import getAggregatorResult from './getAggregatorResult'; -export default function computeVariableAsToggle(computation, node, prop){ - let result = getAggregatorResult(node, prop) || 0; +export default function computeVariableAsToggle(computation, node, prop) { + let result = getAggregatorResult(node) || 0; prop.value = !!result || !!prop.enabled || !!prop.condition?.value; } diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/getAggregatorResult.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/getAggregatorResult.js index 3a9f7316..5d3519ce 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/getAggregatorResult.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/getAggregatorResult.js @@ -1,6 +1,6 @@ -import stripFloatingPointOddities from '/imports/api/engine/computation/utility/stripFloatingPointOddities.js'; +import stripFloatingPointOddities from '/imports/api/engine/computation/utility/stripFloatingPointOddities'; -export default function getAggregatorResult(node){ +export default function getAggregatorResult(node) { // Work out the base value as the greater of the deining stat value // This baseValue comes from aggregating definitions let statBase = node.data.baseValue; @@ -12,9 +12,9 @@ export default function getAggregatorResult(node){ if (!aggregator) return statBase; let base; - if (!Number.isFinite(aggregator.base)){ + if (!Number.isFinite(aggregator.base)) { base = statBase || 0; - } else if (!Number.isFinite(statBase)){ + } else if (!Number.isFinite(statBase)) { base = aggregator.base || 0; } else { base = Math.max(aggregator.base, statBase); @@ -29,9 +29,9 @@ export default function getAggregatorResult(node){ if (aggregator.set !== undefined) { result = aggregator.set; } - if (!node.data.definingProp?.decimal && Number.isFinite(result)){ + if (!node.data.definingProp?.decimal && Number.isFinite(result)) { result = Math.floor(result); - } else if (Number.isFinite(result)){ + } else if (Number.isFinite(result)) { result = stripFloatingPointOddities(result); } diff --git a/app/imports/api/engine/computation/computeComputation/computeToggles.js b/app/imports/api/engine/computation/computeComputation/computeToggles.js index caa310d2..a39e8dc4 100644 --- a/app/imports/api/engine/computation/computeComputation/computeToggles.js +++ b/app/imports/api/engine/computation/computeComputation/computeToggles.js @@ -1,13 +1,34 @@ -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 isn't set to constantly enabled or disabled + !toggle.enabled && + !toggle.disabled && + // Toggle is not disabled by another toggle targeting it + // Ancestor toggles would've handled this child anyway, + // and tag targeted toggles break the link + !toggle.deactivatedByToggle && + !toggle.deactivatedByAncestor && + // Toggle has a condition with a falsy value + toggle.condition && + !toggle.condition.value + ) + || ( + // Toggle is disabled manually + toggle.disabled && + // Toggle isn't deactivated by something else + !toggle.deactivatedByToggle && + !toggle.deactivatedByAncestor + ) + ) { prop.inactive = true; prop.deactivatedByToggle = true; + prop.deactivatingToggleId = toggle._id; } }); } diff --git a/app/imports/api/engine/computation/computeComputation/tests/computeAction.testFn.js b/app/imports/api/engine/computation/computeComputation/tests/computeAction.testFn.js index 47c78f49..6bec5740 100644 --- a/app/imports/api/engine/computation/computeComputation/tests/computeAction.testFn.js +++ b/app/imports/api/engine/computation/computeComputation/tests/computeAction.testFn.js @@ -1,11 +1,11 @@ -import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation.js'; +import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation'; import { assert } from 'chai'; -import computeCreatureComputation from '../../computeCreatureComputation.js'; -import clean from '../../utility/cleanProp.testFn.js'; +import computeCreatureComputation from '../../computeCreatureComputation'; +import clean from '../../utility/cleanProp.testFn'; -export default function(){ +export default async function () { const computation = buildComputationFromProps(testProperties); - computeCreatureComputation(computation); + await computeCreatureComputation(computation); const prop = computation.propsById['actionId']; assert.equal(prop.summary.value, 'test summary 3 without referencing anything 7'); @@ -33,7 +33,6 @@ var testProperties = [ clean({ _id: 'actionId', type: 'action', - ancestors: [{id: 'charId'}], summary: { text: 'test summary {1 + 2} without referencing anything {3 + 4}', }, @@ -58,17 +57,20 @@ var testProperties = [ }], }, uses: { - calculation: 'nonExistantProperty + 7', + calculation: 'nonExistentProperty + 7', }, usesUsed: 5, + left: 1, + right: 2, }), clean({ _id: 'rolledDescriptionId', type: 'action', - ancestors: [{id: 'charId'}], summary: { text: 'test roll gets compiled {4 + (2 + 2)} properly', }, + left: 3, + right: 4, }), clean({ _id: 'numItemsConumedId', @@ -77,6 +79,8 @@ var testProperties = [ baseValue: { calculation: '3', }, + left: 5, + right: 6, }), clean({ _id: 'numResourceConumedId', @@ -85,6 +89,8 @@ var testProperties = [ baseValue: { calculation: '4', }, + left: 7, + right: 8, }), clean({ _id: 'resourceVarId', @@ -94,6 +100,8 @@ var testProperties = [ baseValue: { calculation: '9', }, + left: 9, + right: 10, }), clean({ _id: 'inlineRefResourceId', @@ -102,6 +110,8 @@ var testProperties = [ baseValue: { calculation: '1 + 5', }, + left: 11, + right: 12, }), clean({ _id: 'arrowId', @@ -110,5 +120,7 @@ var testProperties = [ quantity: 27, icon: 'itemIcon', color: 'itemColor', + left: 13, + right: 14, }), ]; diff --git a/app/imports/api/engine/computation/computeComputation/tests/computeAttribute.testFn.js b/app/imports/api/engine/computation/computeComputation/tests/computeAttribute.testFn.js index dfc9fb86..6ea411a0 100644 --- a/app/imports/api/engine/computation/computeComputation/tests/computeAttribute.testFn.js +++ b/app/imports/api/engine/computation/computeComputation/tests/computeAttribute.testFn.js @@ -1,11 +1,11 @@ -import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation.js'; +import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation'; import { assert } from 'chai'; -import computeCreatureComputation from '../../computeCreatureComputation.js'; -import clean from '../../utility/cleanProp.testFn.js'; +import computeCreatureComputation from '../../computeCreatureComputation'; +import clean from '../../utility/cleanProp.testFn'; -export default function(){ +export default async function () { const computation = buildComputationFromProps(testProperties); - computeCreatureComputation(computation); + await computeCreatureComputation(computation); const prop = id => computation.propsById[id]; const scope = variableName => computation.scope[variableName]; assert.equal(prop('emptyId').value, 0, 'calculates empty props to zero'); @@ -27,6 +27,8 @@ var testProperties = [ _id: 'emptyId', type: 'attribute', attributeType: 'ability', + left: 1, + right: 2, }), clean({ _id: 'noVariableNameId', @@ -35,6 +37,8 @@ var testProperties = [ baseValue: { calculation: '8' }, + left: 3, + right: 4, }), clean({ _id: 'strengthId', @@ -44,6 +48,8 @@ var testProperties = [ baseValue: { calculation: '12' }, + left: 5, + right: 6, }), clean({ _id: 'overriddenDexId', @@ -54,6 +60,8 @@ var testProperties = [ baseValue: { calculation: '15' }, + left: 7, + right: 8, }), clean({ _id: 'dexterityId', @@ -64,6 +72,8 @@ var testProperties = [ baseValue: { calculation: '15' }, + left: 9, + right: 10, }), clean({ _id: 'constitutionId', @@ -73,6 +83,8 @@ var testProperties = [ baseValue: { calculation: '21' }, + left: 11, + right: 12, }), clean({ _id: 'referencesDexId', @@ -81,6 +93,8 @@ var testProperties = [ baseValue: { calculation: 'dexterity.modifier + 2' }, + left: 13, + right: 14, }), clean({ _id: 'hitDiceId', @@ -91,6 +105,8 @@ var testProperties = [ baseValue: { calculation: '4' }, + left: 15, + right: 16, }), clean({ _id: 'parseErrorId', @@ -100,5 +116,7 @@ var testProperties = [ baseValue: { calculation: '12 +' }, + left: 17, + right: 18, }), ]; diff --git a/app/imports/api/engine/computation/computeComputation/tests/computeCalculations.testFn.js b/app/imports/api/engine/computation/computeComputation/tests/computeCalculations.testFn.js new file mode 100644 index 00000000..ee749e4d --- /dev/null +++ b/app/imports/api/engine/computation/computeComputation/tests/computeCalculations.testFn.js @@ -0,0 +1,113 @@ +import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation'; +import { assert } from 'chai'; +import computeCreatureComputation from '../../computeCreatureComputation'; +import clean from '../../utility/cleanProp.testFn'; + +export default async function () { + const computation = buildComputationFromProps(testProperties); + await computeCreatureComputation(computation); + const prop = id => computation.propsById[id]; + // Tag targeted effects make complicated parse trees + assert.equal(prop('attackAction2').attackRoll.value, 'min(3 + d4, d100)', 'Tag targeted effects change the attack roll correctly'); + // Tags target effects on attributes + assert.equal(prop('taggedCon').value, 26, 'Tagged targeted effects affect attribute values'); + assert.equal(prop('taggedCon').baseValue.value, 10, 'Tag targeted effects target the attribute itself, not the base value'); + // Tag target effects on a calculation + assert.equal(prop('attackAction').attackRoll.value, 20, 'Tag targeted effects change the attack roll correctly'); + // Tag target effects can deal with rolls + assert.equal(prop('attackAction').attackRoll.value, 20, 'Tag targeted effects change the attack roll correctly'); +} + +var testProperties = [ + // Constitution plus some effects that target it by tag + clean({ + _id: 'taggedCon', + variableName: 'constitution', + type: 'attribute', + attributeType: 'ability', + baseValue: { + calculation: '10' + }, + tags: ['tag'] + }), + clean({ + _id: 'add3ToCon', + type: 'effect', + operation: 'add', + amount: { + calculation: '3' + }, + targetByTags: true, + targetTags: ['tag'], + }), + clean({ + _id: 'mulConBy2', + type: 'effect', + operation: 'mul', + amount: { + calculation: '2' + }, + targetByTags: true, + targetTags: ['tag'], + }), + + // Attack action plus some effects that target it by tag + clean({ + _id: 'attackAction', + type: 'action', + attackRoll: { + calculation: '3' + }, + tags: ['attackTag'] + }), + clean({ + _id: 'add1ToAttack', + type: 'effect', + operation: 'add', + amount: { + calculation: '1' + }, + targetByTags: true, + targetTags: ['attackTag'], + }), + clean({ + _id: 'mulAttackBy5', + type: 'effect', + operation: 'mul', + amount: { + calculation: '5' + }, + targetByTags: true, + targetTags: ['attackTag'], + }), + + // Attack action plus some effects that target it by tag but have rolled values + clean({ + _id: 'attackAction2', + type: 'action', + attackRoll: { + calculation: '3' + }, + tags: ['attackTag2'] + }), + clean({ + _id: 'addD4ToAttackRoll', + type: 'effect', + operation: 'add', + amount: { + calculation: 'd4' + }, + targetByTags: true, + targetTags: ['attackTag2'], + }), + clean({ + _id: 'MaxAttackByD100', + type: 'effect', + operation: 'max', + amount: { + calculation: 'd100' + }, + targetByTags: true, + targetTags: ['attackTag2'], + }), +]; diff --git a/app/imports/api/engine/computation/computeComputation/tests/computeClasses.testFn.js b/app/imports/api/engine/computation/computeComputation/tests/computeClasses.testFn.js index 2c6bdc8f..a1d50ed3 100644 --- a/app/imports/api/engine/computation/computeComputation/tests/computeClasses.testFn.js +++ b/app/imports/api/engine/computation/computeComputation/tests/computeClasses.testFn.js @@ -1,11 +1,12 @@ -import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation.js'; +import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation'; import { assert } from 'chai'; -import computeCreatureComputation from '../../computeCreatureComputation.js'; -import clean from '../../utility/cleanProp.testFn.js'; +import computeCreatureComputation from '../../computeCreatureComputation'; +import clean from '../../utility/cleanProp.testFn'; +import { applyNestedSetProperties } from '/imports/api/parenting/parentingFunctions'; -export default function(){ +export default async function () { const computation = buildComputationFromProps(testProperties); - computeCreatureComputation(computation); + await computeCreatureComputation(computation); const scope = id => computation.scope[id]; const prop = id => computation.propsById[id]; assert.equal(scope('level').value, 5); @@ -20,41 +21,37 @@ var testProperties = [ type: 'class', variableName: 'wizard', classType: 'startingClass', - ancestors: [{id: 'charId'}], }), clean({ _id: 'rangerId', type: 'class', variableName: 'ranger', classType: 'multiClass', - ancestors: [{id: 'charId'}], }), clean({ _id: 'wiz1Id', type: 'classLevel', variableName: 'wizard', level: 1, - ancestors: [{id: 'charId'}], }), clean({ _id: 'wiz2Id', type: 'classLevel', variableName: 'wizard', level: 2, - ancestors: [{id: 'charId'}], }), clean({ _id: 'wiz4Id', type: 'classLevel', variableName: 'wizard', level: 4, - ancestors: [{id: 'charId'}], }), clean({ _id: 'rang1Id', type: 'classLevel', variableName: 'ranger', level: 1, - ancestors: [{id: 'charId'}], }), ]; + +applyNestedSetProperties(testProperties); diff --git a/app/imports/api/engine/computation/computeComputation/tests/computeConstants.testFn.js b/app/imports/api/engine/computation/computeComputation/tests/computeConstants.testFn.js index 94fd414e..793924b7 100644 --- a/app/imports/api/engine/computation/computeComputation/tests/computeConstants.testFn.js +++ b/app/imports/api/engine/computation/computeComputation/tests/computeConstants.testFn.js @@ -1,11 +1,12 @@ -import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation.js'; +import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation'; import { assert } from 'chai'; -import computeCreatureComputation from '../../computeCreatureComputation.js'; -import clean from '../../utility/cleanProp.testFn.js'; +import computeCreatureComputation from '../../computeCreatureComputation'; +import clean from '../../utility/cleanProp.testFn'; +import { applyNestedSetProperties } from '/imports/api/parenting/parentingFunctions'; -export default function(){ +export default async function () { const computation = buildComputationFromProps(testProperties); - computeCreatureComputation(computation); + await computeCreatureComputation(computation); const prop = id => computation.propsById[id]; assert.equal(prop('attId').value, 6); } @@ -23,6 +24,7 @@ var testProperties = [ baseValue: { calculation: 'arrayConstant[3]', }, - ancestors: [{id: 'charId'}], }), ]; + +applyNestedSetProperties(testProperties); diff --git a/app/imports/api/engine/computation/computeComputation/tests/computeDamageMultipliers.testFn.js b/app/imports/api/engine/computation/computeComputation/tests/computeDamageMultipliers.testFn.js index ee777627..86d984b7 100644 --- a/app/imports/api/engine/computation/computeComputation/tests/computeDamageMultipliers.testFn.js +++ b/app/imports/api/engine/computation/computeComputation/tests/computeDamageMultipliers.testFn.js @@ -1,11 +1,11 @@ -import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation.js'; +import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation'; import { assert } from 'chai'; -import computeCreatureComputation from '../../computeCreatureComputation.js'; -import clean from '../../utility/cleanProp.testFn.js'; +import computeCreatureComputation from '../../computeCreatureComputation'; +import clean from '../../utility/cleanProp.testFn'; -export default function(){ +export default async function () { const computation = buildComputationFromProps(testProperties); - computeCreatureComputation(computation); + await computeCreatureComputation(computation); const scope = id => computation.scope[id]; assert.isTrue(scope('blugeoning').vulnerability); assert.isTrue(scope('customDamage').resistance); diff --git a/app/imports/api/engine/computation/computeComputation/tests/computeEffects.testFn.js b/app/imports/api/engine/computation/computeComputation/tests/computeEffects.testFn.js index 8650a9e2..9ef3d6ef 100644 --- a/app/imports/api/engine/computation/computeComputation/tests/computeEffects.testFn.js +++ b/app/imports/api/engine/computation/computeComputation/tests/computeEffects.testFn.js @@ -1,59 +1,53 @@ -import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation.js'; +import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation'; import { assert } from 'chai'; -import computeCreatureComputation from '../../computeCreatureComputation.js'; -import clean from '../../utility/cleanProp.testFn.js'; +import computeCreatureComputation from '../../computeCreatureComputation'; +import clean from '../../utility/cleanProp.testFn'; +import { propsFromForest } from '/imports/api/properties/tests/propTestBuilder.testFn'; -export default function(){ +export default async function () { const computation = buildComputationFromProps(testProperties); - computeCreatureComputation(computation); + await computeCreatureComputation(computation); const prop = id => computation.propsById[id]; assert.equal(prop('strengthId').value, 26); } -var testProperties = [ - clean({ - _id: 'strengthId', +var testProperties = propsFromForest([ + { variableName: 'strength', type: 'attribute', attributeType: 'ability', baseValue: { calculation: '8' }, - }), - clean({ - _id: 'strength2Id', + }, { + // This strength is later in order, so it will override the other + _id: 'strengthId', variableName: 'strength', type: 'attribute', attributeType: 'ability', baseValue: { calculation: '10' }, - }), - clean({ - _id: 'strengthBaseId', + }, { type: 'effect', operation: 'base', amount: { calculation: '10 + 2' }, stats: ['strength'], - }), - clean({ - _id: 'strengthAddId', + }, { type: 'effect', operation: 'add', amount: { calculation: '1' }, stats: ['strength'], - }), - clean({ - _id: 'strengthMulId', + }, { type: 'effect', operation: 'mul', amount: { calculation: '2' }, stats: ['strength'], - }), -]; + }, +]); diff --git a/app/imports/api/engine/computation/computeComputation/tests/computeInventory.testFn.js b/app/imports/api/engine/computation/computeComputation/tests/computeInventory.testFn.js index 13b76e4a..98878cb2 100644 --- a/app/imports/api/engine/computation/computeComputation/tests/computeInventory.testFn.js +++ b/app/imports/api/engine/computation/computeComputation/tests/computeInventory.testFn.js @@ -1,11 +1,12 @@ -import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation.js'; +import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation'; import { assert } from 'chai'; -import computeCreatureComputation from '../../computeCreatureComputation.js'; -import clean from '../../utility/cleanProp.testFn.js'; +import computeCreatureComputation from '../../computeCreatureComputation'; +import clean from '../../utility/cleanProp.testFn'; +import { applyNestedSetProperties, compareOrder } from '/imports/api/parenting/parentingFunctions'; -export default function(){ +export default async function () { const computation = buildComputationFromProps(testProperties); - computeCreatureComputation(computation); + await computeCreatureComputation(computation); const prop = id => computation.propsById[id]; const scope = id => computation.scope[id].value; @@ -13,9 +14,8 @@ export default function(){ assert.equal(scope('valueEquipment'), 3); assert.equal(scope('itemsAttuned'), 1); - - assert.equal(prop('childContainerId').carriedWeight, 69); - assert.equal(prop('childContainerId').contentsWeight, 69); + assert.equal(prop('childContainerId').carriedWeight, 69, 'Calculates container carried weight correctly'); + assert.equal(prop('childContainerId').contentsWeight, 69, 'Calculates container contents weight correctly'); assert.equal(scope('weightCarried'), 104); assert.equal(scope('valueCarried'), 129); @@ -32,7 +32,6 @@ var testProperties = [ attuned: true, weight: 2, value: 3, - ancestors: [{id: 'charId'}], }), clean({ _id: 'containerId', @@ -40,22 +39,13 @@ var testProperties = [ carried: true, weight: 5, value: 7, - ancestors: [{id: 'charId'}], - }), - clean({ - _id: 'childContainerId', - type: 'container', - carried: true, - weight: 11, - value: 13, - ancestors: [{id: 'charId'}, {id: 'containerId'}], }), clean({ _id: 'childItemId', type: 'item', weight: 17, value: 19, - ancestors: [{id: 'charId'}, {id: 'containerId'}], + parentId: 'containerId', }), clean({ _id: 'grandchildItemId', @@ -63,6 +53,16 @@ var testProperties = [ weight: 23, // 69 total value: 29, // 87 total quantity: 3, - ancestors: [{id: 'charId'}, {id: 'containerId'}, {id: 'childContainerId'}], + parentId: 'childContainerId', + }), + clean({ + _id: 'childContainerId', + type: 'container', + carried: true, + weight: 11, + value: 13, + parentId: 'containerId', }), ]; +applyNestedSetProperties(testProperties); +testProperties.sort(compareOrder); diff --git a/app/imports/api/engine/computation/computeComputation/tests/computePointBuys.testFn.js b/app/imports/api/engine/computation/computeComputation/tests/computePointBuys.testFn.js new file mode 100644 index 00000000..21b4897e --- /dev/null +++ b/app/imports/api/engine/computation/computeComputation/tests/computePointBuys.testFn.js @@ -0,0 +1,38 @@ +import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation'; +import { assert } from 'chai'; +import computeCreatureComputation from '../../computeCreatureComputation'; +import { propsFromForest } from '/imports/api/properties/tests/propTestBuilder.testFn'; + +export default async function () { + const computation = buildComputationFromProps(testProperties); + await computeCreatureComputation(computation); + const prop = id => computation.propsById[id]; + assert.equal(prop('strengthId').value, 11, 'Point buys should apply a base value when active'); +} + +var testProperties = propsFromForest([ + { + _id: 'strengthId', + variableName: 'strength', + type: 'attribute', + attributeType: 'ability', + baseValue: { + calculation: '8' + }, + }, { + // calculated inactive toggle with point buy under it + // It should not impact the ability score + type: 'toggle', + condition: { calculation: 'false' }, + children: [ + { + _id: 'inactivePointBuy', + type: 'pointBuy', + values: [{ variableName: 'strength', value: 13 }], + } + ] + }, { + type: 'pointBuy', + values: [{ variableName: 'strength', value: 11 }], + } +]); diff --git a/app/imports/api/engine/computation/computeComputation/tests/computeProficiencies.testFn.js b/app/imports/api/engine/computation/computeComputation/tests/computeProficiencies.testFn.js new file mode 100644 index 00000000..805e9eaf --- /dev/null +++ b/app/imports/api/engine/computation/computeComputation/tests/computeProficiencies.testFn.js @@ -0,0 +1,66 @@ +import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation'; +import { assert } from 'chai'; +import computeCreatureComputation from '../../computeCreatureComputation'; +import clean from '../../utility/cleanProp.testFn'; +import { applyNestedSetProperties, compareOrder } from '/imports/api/parenting/parentingFunctions'; + +export default async function () { + const computation = buildComputationFromProps(testProperties); + const hasLink = computation.dependencyGraph.hasLink; + await 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.proficiencyIds, 'The proficiency aggregator should be here') + assert.exists(prop('actionId').attackRoll.proficiencyIds[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', + attackRoll: { + calculation: 'strength.modifier', + }, + tags: ['rapier', 'martial weapon', 'weapon', 'attack'] + }), + clean({ + _id: 'profBonusId', + type: 'attribute', + variableName: 'proficiencyBonus', + 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'] + }), +]; +applyNestedSetProperties(testProperties); +testProperties.sort(compareOrder); diff --git a/app/imports/api/engine/computation/computeComputation/tests/computeSkills.testFn.js b/app/imports/api/engine/computation/computeComputation/tests/computeSkills.testFn.js index 2557296a..e1b1a049 100644 --- a/app/imports/api/engine/computation/computeComputation/tests/computeSkills.testFn.js +++ b/app/imports/api/engine/computation/computeComputation/tests/computeSkills.testFn.js @@ -1,11 +1,11 @@ -import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation.js'; +import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation'; import { assert } from 'chai'; -import computeCreatureComputation from '../../computeCreatureComputation.js'; -import clean from '../../utility/cleanProp.testFn.js'; +import computeCreatureComputation from '../../computeCreatureComputation'; +import clean from '../../utility/cleanProp.testFn'; -export default function(){ +export default async function () { const computation = buildComputationFromProps(testProperties); - computeCreatureComputation(computation); + await computeCreatureComputation(computation); const prop = id => computation.propsById[id]; assert.equal(prop('atheleticsId').proficiency, 2, 'Inherits proficiency from ability'); diff --git a/app/imports/api/engine/computation/computeComputation/tests/index.js b/app/imports/api/engine/computation/computeComputation/tests/index.js index b9c8baf7..0faa0989 100644 --- a/app/imports/api/engine/computation/computeComputation/tests/index.js +++ b/app/imports/api/engine/computation/computeComputation/tests/index.js @@ -1,34 +1,46 @@ -import computeAction from './computeAction.testFn.js'; -import computeAttribute from './computeAttribute.testFn.js'; -import computeClasses from './computeClasses.testFn.js'; -import computeConstants from './computeConstants.testFn.js'; -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 computeAction from './computeAction.testFn'; +import computeAttribute from './computeAttribute.testFn'; +import computeCalculations from './computeCalculations.testFn'; +import computeClasses from './computeClasses.testFn'; +import computeConstants from './computeConstants.testFn'; +import computeInventory from './computeInventory.testFn'; +import computeDamageMultipliers from './computeDamageMultipliers.testFn'; +import computeEffects from './computeEffects.testFn'; +import computeSkills from './computeSkills.testFn'; +import computeProficiencies from './computeProficiencies.testFn'; +import computePointBuys from './computePointBuys.testFn'; export default [{ text: 'Computes actions', fn: computeAction, -},{ +}, { text: 'Computes attributes', fn: computeAttribute, -},{ +}, { + text: 'Computes calculations', + fn: computeCalculations, +}, { 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 point buys', + fn: computePointBuys, +}, { + text: 'Computes proficiencies', + fn: computeProficiencies, }]; diff --git a/app/imports/api/engine/computation/computeCreatureComputation.js b/app/imports/api/engine/computation/computeCreatureComputation.js index 7cd4b41c..f74b4368 100644 --- a/app/imports/api/engine/computation/computeCreatureComputation.js +++ b/app/imports/api/engine/computation/computeCreatureComputation.js @@ -1,9 +1,10 @@ -import computeToggles from '/imports/api/engine/computation/computeComputation/computeToggles.js'; -import computeByType from '/imports/api/engine/computation/computeComputation/computeByType.js'; -import embedInlineCalculations from './utility/embedInlineCalculations.js'; +import computeToggles from '/imports/api/engine/computation/computeComputation/computeToggles'; +import computeByType from '/imports/api/engine/computation/computeComputation/computeByType'; +import embedInlineCalculations from './utility/embedInlineCalculations'; +import { removeEmptyCalculations } from './buildComputation/parseCalculationFields'; import path from 'ngraph.path'; -export default function computeCreatureComputation(computation){ +export default async function computeCreatureComputation(computation) { const stack = []; // Computation scope of {variableName: prop} const graph = computation.dependencyGraph; @@ -20,17 +21,17 @@ export default function computeCreatureComputation(computation){ stack.reverse(); // Depth first traversal of nodes - while (stack.length){ + while (stack.length) { let top = stack[stack.length - 1]; - if (top._visited){ + if (top._visited) { // The object has already been computed, skip stack.pop(); - } else if (top._visitedChildren){ + } else if (top._visitedChildren) { // Mark the object as visited and remove from stack top._visited = true; stack.pop(); - // Compute the top object of the stack - compute(computation, top); + // Compute the top object of the stack + await compute(computation, top); } else { top._visitedChildren = true; // Push dependencies to graph to be computed first @@ -39,17 +40,19 @@ export default function computeCreatureComputation(computation){ } // Finish the props after the dependency graph has been traversed - computation.props.forEach(finalizeProp); + for (const prop of computation.props) { + finalizeProp(prop); + } } -function compute(computation, node){ +async function compute(computation, node) { // Determine the prop's active status by its toggles computeToggles(computation, node); // Compute the property by type - computeByType[node.data?.type || '_variable']?.(computation, node); + await computeByType[node.data?.type || '_variable']?.(computation, node); } -function pushDependenciesToStack(nodeId, graph, stack, computation){ +function pushDependenciesToStack(nodeId, graph, stack, computation) { graph.forEachLinkedNode(nodeId, linkedNode => { if (linkedNode._visitedChildren && !linkedNode._visited) { // This is a dependency loop, find a path from the node to itself @@ -66,7 +69,7 @@ function pushDependenciesToStack(nodeId, graph, stack, computation){ loop = [linkedNode, ...newLoop]; } }, true); - + if (loop.length) { computation.errors.push({ type: 'dependencyLoop', @@ -80,11 +83,13 @@ function pushDependenciesToStack(nodeId, graph, stack, computation){ }, true); } -function finalizeProp(prop){ +function finalizeProp(prop) { // Embed the inline calculations prop._computationDetails?.inlineCalculations?.forEach(inlineCalcObj => { embedInlineCalculations(inlineCalcObj); }); + // Clean up the calculations that were never used + removeEmptyCalculations(prop); // Clean up the computation details delete prop._computationDetails; } diff --git a/app/imports/api/engine/computation/computeCreatureComputation.test.js b/app/imports/api/engine/computation/computeCreatureComputation.test.js index f64e6e7f..45018d3f 100644 --- a/app/imports/api/engine/computation/computeCreatureComputation.test.js +++ b/app/imports/api/engine/computation/computeCreatureComputation.test.js @@ -1,11 +1,11 @@ -import computeCreatureComputation from './computeCreatureComputation.js'; -import { buildComputationFromProps } from './buildCreatureComputation.js'; +import computeCreatureComputation from './computeCreatureComputation'; +import { buildComputationFromProps } from './buildCreatureComputation'; import { assert } from 'chai'; -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import computeTests from './computeComputation/tests/index.js'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; +import computeTests from './computeComputation/tests/index'; -describe('Compute compuation', function(){ - it('Computes something at all', function(){ +describe('Compute compuation', function () { + it('Computes something at all', function () { let computation = buildComputationFromProps(testProperties); computeCreatureComputation(computation); assert.exists(computation); @@ -28,7 +28,7 @@ var testProperties = [ }), ]; -function clean(prop){ +function clean(prop) { let schema = CreatureProperties.simpleSchema(prop); return schema.clean(prop); } diff --git a/app/imports/api/engine/computation/utility/applyFnToKey.js b/app/imports/api/engine/computation/utility/applyFnToKey.js index 0d4005af..d0dd90bd 100644 --- a/app/imports/api/engine/computation/utility/applyFnToKey.js +++ b/app/imports/api/engine/computation/utility/applyFnToKey.js @@ -1,16 +1,29 @@ import { get } from 'lodash'; -export default function applyFnToKey(doc, key, fn){ - if (key.includes('.$')){ +export default function applyFnToKey(doc, key, fn) { + if (key.includes('.$')) { applyToArrayKey(doc, key, fn); } else { applyToSingleKey(doc, key, fn); } } -function applyToSingleKey(doc, key, fn){ +export async function applyFnToKeyAsync(doc, key, fn) { + if (key.includes('.$')) { + await applyToArrayKeyAsync(doc, key, fn); + } else { + await applyToSingleKeyAsync(doc, key, fn); + } +} + +function applyToSingleKey(doc, key, fn) { // call the function with the current value and document for context - fn(doc, key); + return fn(doc, key); +} + +async function applyToSingleKeyAsync(doc, key, fn) { + // call the function with the current value and document for context + return await fn(doc, key); } /** @@ -19,7 +32,7 @@ function applyToSingleKey(doc, key, fn){ * Warning: Order might be confusing, it will traverse the deepest array in order * but the shallower arrays in reverse order */ -function applyToArrayKey(doc, key, fn){ +function applyToArrayKey(doc, key, fn) { const keySplit = key.split('.$'); // Stack based depth first traversal of arrays const array = get(doc, keySplit[0]); @@ -30,11 +43,12 @@ function applyToArrayKey(doc, key, fn){ currentPath: keySplit[0], indices: [], }]; - while(stack.length){ + while (stack.length) { const state = stack.pop(); - for (let index in state.array){ + if (!state) break; + for (let index in state.array) { const currentPath = `${state.currentPath}[${index}]${state.paths[0]}` - if (state.paths.length == 1){ + if (state.paths.length == 1) { applyToSingleKey(doc, currentPath, fn); } else { const array = get(doc, currentPath); @@ -49,3 +63,35 @@ function applyToArrayKey(doc, key, fn){ } } } + +async function applyToArrayKeyAsync(doc, key, fn) { + const keySplit = key.split('.$'); + // Stack based depth first traversal of arrays + const array = get(doc, keySplit[0]); + if (!array) return; + const stack = [{ + array, + paths: keySplit.slice(1), + currentPath: keySplit[0], + indices: [], + }]; + while (stack.length) { + const state = stack.pop(); + if (!state) break; + for (let index in state.array) { + const currentPath = `${state.currentPath}[${index}]${state.paths[0]}` + if (state.paths.length == 1) { + await applyToSingleKey(doc, currentPath, fn); + } else { + const array = get(doc, currentPath); + if (!array) return; + stack.push({ + array, + paths: state.paths.slice(1), + currentPath, + indices: [...state.indices, index], + }); + } + } + } +} diff --git a/app/imports/api/engine/computation/utility/applyFnToKey.test.js b/app/imports/api/engine/computation/utility/applyFnToKey.test.js index 5e1b3a3c..4c6680cb 100644 --- a/app/imports/api/engine/computation/utility/applyFnToKey.test.js +++ b/app/imports/api/engine/computation/utility/applyFnToKey.test.js @@ -1,9 +1,9 @@ -import applyFnToKey from './applyFnToKey.js'; +import applyFnToKey from './applyFnToKey'; import { assert } from 'chai'; import { get } from 'lodash'; -describe('apply function to key', function(){ - it('uses a basic key correctly', function(){ +describe('apply function to key', function () { + it('uses a basic key correctly', function () { let obj = getStartingObject(); applyFnToKey(obj, 'fox.name', (doc, key) => { assert.equal(obj, doc); @@ -11,7 +11,7 @@ describe('apply function to key', function(){ assert.equal(get(doc, key), 'foxy'); }); }); - it('uses a single nested key correctly', function(){ + it('uses a single nested key correctly', function () { let obj = getStartingObject(); let foxSounds = []; applyFnToKey(obj, 'fox.sound.$', (doc, key) => { @@ -21,7 +21,7 @@ describe('apply function to key', function(){ assert.include(foxSounds, 'tjoef'); assert.include(foxSounds, 'kek'); }); - it('uses a double nested key correctly', function(){ + it('uses a double nested key correctly', function () { let obj = getStartingObject(); let birdSounds = []; applyFnToKey(obj, 'birds.$.sound.$', (doc, key) => { @@ -33,7 +33,7 @@ describe('apply function to key', function(){ }); }); -function getStartingObject(){ +function getStartingObject() { return { fox: { name: 'foxy', @@ -48,7 +48,7 @@ function getStartingObject(){ sound: [ 'koer', ] - },{ + }, { name: 'parrot', sound: [ 'hello', diff --git a/app/imports/api/engine/computation/utility/cleanProp.testFn.js b/app/imports/api/engine/computation/utility/cleanProp.testFn.js index 1c845150..32bbbfe3 100644 --- a/app/imports/api/engine/computation/utility/cleanProp.testFn.js +++ b/app/imports/api/engine/computation/utility/cleanProp.testFn.js @@ -1,6 +1,9 @@ -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; -export default function cleanProp(prop){ +export default function cleanProp(prop) { + if (!prop.root) { + prop.root = { collection: 'creatures', id: 'testCreature' } + } let schema = CreatureProperties.simpleSchema(prop); return schema.clean(prop); } diff --git a/app/imports/api/engine/computation/utility/embedInlineCalculations.js b/app/imports/api/engine/computation/utility/embedInlineCalculations.js index 096cf921..073d3d4f 100644 --- a/app/imports/api/engine/computation/utility/embedInlineCalculations.js +++ b/app/imports/api/engine/computation/utility/embedInlineCalculations.js @@ -1,6 +1,6 @@ -import INLINE_CALCULATION_REGEX from '/imports/constants/INLINE_CALCULTION_REGEX.js'; +import INLINE_CALCULATION_REGEX from '/imports/constants/INLINE_CALCULATION_REGEX'; -export default function embedInlineCalculations(inlineCalcObj){ +export default function embedInlineCalculations(inlineCalcObj) { const string = inlineCalcObj.text; const calculations = inlineCalcObj.inlineCalculations; if (!string || !calculations) return; diff --git a/app/imports/api/engine/computation/utility/evaluateCalculation.js b/app/imports/api/engine/computation/utility/evaluateCalculation.js deleted file mode 100644 index 580cafde..00000000 --- a/app/imports/api/engine/computation/utility/evaluateCalculation.js +++ /dev/null @@ -1,19 +0,0 @@ -import resolve, { toString } from '/imports/parser/resolve.js'; - -export default function evaluateCalculation(calculation, scope, givenContext){ - const parseNode = calculation.parseNode; - const fn = calculation._parseLevel; - const calculationScope = {...calculation._localScope, ...scope}; - const {result: resultNode, context} = resolve(fn, parseNode, calculationScope, givenContext); - calculation.errors = context.errors; - if (resultNode?.parseType === 'constant'){ - calculation.value = resultNode.value; - } else if (resultNode?.parseType === 'error'){ - calculation.value = null; - } else { - calculation.value = toString(resultNode); - } - // remove the working fields - delete calculation._parseLevel; - delete calculation._localScope; -} diff --git a/app/imports/api/engine/computation/utility/findAncestorByType.js b/app/imports/api/engine/computation/utility/findAncestorByType.js deleted file mode 100644 index 492cc6d0..00000000 --- a/app/imports/api/engine/computation/utility/findAncestorByType.js +++ /dev/null @@ -1,10 +0,0 @@ -export default function findAncestorByType(prop, type, propsById){ - if (!prop || !prop.ancestors) return; - let ancestor; - for (let i = prop.ancestors.length - 1; i >= 0; i--){ - ancestor = propsById[prop.ancestors[i].id]; - if (ancestor && ancestor.type === type){ - return ancestor; - } - } -} diff --git a/app/imports/api/engine/computation/utility/stripFloatingPointOddities.js b/app/imports/api/engine/computation/utility/stripFloatingPointOddities.js index 8e55685c..3415ff1a 100644 --- a/app/imports/api/engine/computation/utility/stripFloatingPointOddities.js +++ b/app/imports/api/engine/computation/utility/stripFloatingPointOddities.js @@ -1,3 +1,8 @@ -export default function stripFloatingPointOddities(num, precision = 12){ +export default function stripFloatingPointOddities(num, precision = 12) { return +parseFloat(num.toPrecision(precision)) } + +export function safeStrip(num, precision = 12) { + if (!Number.isFinite(num)) return num; + return stripFloatingPointOddities(num, precision); +} diff --git a/app/imports/api/engine/computation/utility/walkdown.js b/app/imports/api/engine/computation/utility/walkdown.js deleted file mode 100644 index 0efad425..00000000 --- a/app/imports/api/engine/computation/utility/walkdown.js +++ /dev/null @@ -1,8 +0,0 @@ -export default function walkDown(tree, callback){ - let stack = [...tree]; - while(stack.length){ - let node = stack.pop(); - callback(node, stack); - stack.push(...node.children); - } -} diff --git a/app/imports/api/engine/computation/utility/walkdown.ts b/app/imports/api/engine/computation/utility/walkdown.ts new file mode 100644 index 00000000..f0dd5e54 --- /dev/null +++ b/app/imports/api/engine/computation/utility/walkdown.ts @@ -0,0 +1,14 @@ +import { TreeDoc } from '/imports/api/parenting/ChildSchema'; +import { TreeNode } from '/imports/api/parenting/parentingFunctions'; + +export default function walkDown( + trees: TreeNode[], callback: (node: TreeNode, stack: TreeNode[]) => any +) { + const stack = [...trees]; + while (stack.length) { + const node = stack.pop(); + if (!node) return; + callback(node, stack); + stack.push(...node.children); + } +} diff --git a/app/imports/api/engine/computation/writeComputation/writeAlteredProperties.js b/app/imports/api/engine/computation/writeComputation/writeAlteredProperties.js index fcad024c..0ef8487e 100644 --- a/app/imports/api/engine/computation/writeComputation/writeAlteredProperties.js +++ b/app/imports/api/engine/computation/writeComputation/writeAlteredProperties.js @@ -1,14 +1,14 @@ -import { Meteor } from 'meteor/meteor' -import { EJSON } from 'meteor/ejson'; -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import propertySchemasIndex from '/imports/api/properties/computedOnlyPropertySchemasIndex.js'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; +import propertySchemasIndex from '/imports/api/properties/computedOnlyPropertySchemasIndex'; +import bulkWrite, { addSetOp, addUnsetOp, newOperation } from '/imports/api/engine/shared/bulkWrite'; +import updateTabletopPropCount from '/imports/api/tabletop/functions/denormalizeTabletopPropCount' -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,27 +20,37 @@ export default function writeAlteredProperties(computation){ 'deactivatedBySelf', 'deactivatedByAncestor', 'deactivatedByToggle', + 'deactivatingToggleId', 'damage', 'dirty', + 'left', + 'right', + 'parentId', + 'triggerIds', ...schema.objectKeys(), ]; op = addChangedKeysToOp(op, keys, original, changed); - if (op){ + if (op) { bulkWriteOperations.push(op); } }); - bulkWriteProperties(bulkWriteOperations); + const writePromise = bulkWrite(bulkWriteOperations, CreatureProperties); //if (bulkWriteOperations.length) console.log(`Wrote ${bulkWriteOperations.length} props`); + + // Update the relevant tabletop's property count + if (computation.creature?.tabletopId) updateTabletopPropCount(computation.creature?.tabletopId); + + return writePromise; } 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 (!op) op = newOperation(original._id, changed.type); + if (!EJSON.equals(original[key], changed[key])) { + if (!op) op = newOperation(original._id); let value = changed[key]; - if (value === undefined){ + if (value === undefined) { // Unset values that become undefined addUnsetOp(op, key); } else { @@ -51,70 +61,3 @@ function addChangedKeysToOp(op, keys, original, changed) { } return op; } - -function newOperation(_id, type){ - let newOp = { - updateOne: { - filter: {_id}, - update: {}, - } - }; - if (Meteor.isClient){ - newOp.type = type; - } - return newOp; -} - -function addSetOp(op, key, value){ - if (op.updateOne.update.$set){ - op.updateOne.update.$set[key] = value; - } else { - op.updateOne.update.$set = {[key]: value}; - } -} - -function addUnsetOp(op, key){ - if (op.updateOne.update.$unset){ - op.updateOne.update.$unset[key] = 1; - } else { - op.updateOne.update.$unset = {[key]: 1}; - } -} - -// If we re-enable client-side sheet recalculation, this needs to be run on -// both client and server to preserve latency compensation. Bulkwrite breaks -// latency compensation and causes flickering -function writePropertiesSequentially(bulkWriteOps) { - bulkWriteOps.forEach(op => { - let updateOneOrMany = op.updateOne || op.updateMany; - CreatureProperties.update(updateOneOrMany.filter, updateOneOrMany.update, { - // The bulk code is bypassing validation, so do the same here - // selector: {type: op.type} // include this if bypass is off - bypassCollection2: true, - }); - }); - //if (bulkWriteOps.length) console.log(`Wrote ${bulkWriteOps.length} props`); -} - -// This is more efficient on the database, but significantly less efficient -// 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){ - if (!bulkWriteOps.length) return; - // bulkWrite is only available on the server - if (Meteor.isServer) { - CreatureProperties.rawCollection().bulkWrite( - bulkWriteOps, - {ordered : false}, - function(e){ - if (e) { - console.error('Bulk write failed: '); - console.error(e); - } - } - ); - } else { - writePropertiesSequentially(bulkWriteOps); - } -} diff --git a/app/imports/api/engine/computation/writeComputation/writeErrors.js b/app/imports/api/engine/computation/writeComputation/writeErrors.js deleted file mode 100644 index 66002515..00000000 --- a/app/imports/api/engine/computation/writeComputation/writeErrors.js +++ /dev/null @@ -1,9 +0,0 @@ -import Creatures from '/imports/api/creature/creatures/Creatures.js'; - -export default function(creatureId, errors = []){ - if (errors.length){ - Creatures.update(creatureId, {$set: {computeErrors: errors}}); - } else { - Creatures.update(creatureId, {$unset: {computeErrors: 1}}); - } -} diff --git a/app/imports/api/engine/computation/writeComputation/writeErrorsAndPropCount.js b/app/imports/api/engine/computation/writeComputation/writeErrorsAndPropCount.js new file mode 100644 index 00000000..383ad2d3 --- /dev/null +++ b/app/imports/api/engine/computation/writeComputation/writeErrorsAndPropCount.js @@ -0,0 +1,9 @@ +import Creatures from '/imports/api/creature/creatures/Creatures'; + +export default function writeErrorsAndPropCount(creatureId, errors = [], propCount) { + if (errors.length) { + Creatures.update(creatureId, { $set: { computeErrors: errors, propCount } }); + } else { + Creatures.update(creatureId, { $set: { propCount }, $unset: { computeErrors: 1 } }); + } +} diff --git a/app/imports/api/engine/computation/writeComputation/writeScope.js b/app/imports/api/engine/computation/writeComputation/writeScope.js index d96c2bfe..cf9af316 100644 --- a/app/imports/api/engine/computation/writeComputation/writeScope.js +++ b/app/imports/api/engine/computation/writeComputation/writeScope.js @@ -1,13 +1,22 @@ -import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables.js'; -import Creatures from '/imports/api/creature/creatures/Creatures.js'; +import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables'; +import Creatures from '/imports/api/creature/creatures/Creatures'; import { EJSON } from 'meteor/ejson'; export default function writeScope(creatureId, computation) { if (!creatureId) throw 'creatureId is required'; const scope = computation.scope; let variables = computation.variables; + // If the variables are not set, check if they can be fetched if (!variables) { - CreatureVariables.insert({ _creatureId: creatureId }); + variables = CreatureVariables.findOne({ + _creatureId: creatureId + }); + } + // Otherwise create a new variables document + if (!variables) { + CreatureVariables.insert({ + _creatureId: creatureId + }); variables = {}; } delete variables._id; @@ -15,18 +24,31 @@ 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 empty objects + if (Object.keys(scope[key]).length === 0) { + delete scope[key]; + 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] } } - + + // If this is a creature property, replace the property with a link + if (scope[key]._id && scope[key].type) { + scope[key] = { _propId: scope[key]._id }; + } + // Only update changed fields if (!EJSON.equals(variables[key], scope[key])) { if (!$set) $set = {}; @@ -53,9 +75,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 }) +} +*/ diff --git a/app/imports/api/engine/computeCreature.js b/app/imports/api/engine/computeCreature.js index d0a3d469..96362d7c 100644 --- a/app/imports/api/engine/computeCreature.js +++ b/app/imports/api/engine/computeCreature.js @@ -1,22 +1,23 @@ -import buildCreatureComputation from './computation/buildCreatureComputation.js'; -import computeCreatureComputation from './computation/computeCreatureComputation.js'; -import writeAlteredProperties from './computation/writeComputation/writeAlteredProperties.js'; -import writeScope from './computation/writeComputation/writeScope.js'; -import writeErrors from './computation/writeComputation/writeErrors.js'; +import buildCreatureComputation from './computation/buildCreatureComputation'; +import computeCreatureComputation from './computation/computeCreatureComputation'; +import writeAlteredProperties from './computation/writeComputation/writeAlteredProperties'; +import writeScope from './computation/writeComputation/writeScope'; +import writeErrorsAndPropCount from './computation/writeComputation/writeErrorsAndPropCount'; -export default function computeCreature(creatureId){ +export default async function computeCreature(creatureId) { if (Meteor.isClient) return; // console.log('compute ' + creatureId); const computation = buildCreatureComputation(creatureId); - computeComputation(computation, creatureId); + await computeComputation(computation, creatureId); } -function computeComputation(computation, creatureId) { +async function computeComputation(computation, creatureId) { try { - computeCreatureComputation(computation); - writeAlteredProperties(computation); - writeScope(creatureId, computation); - } catch (e){ + await computeCreatureComputation(computation); + const writePromise = writeAlteredProperties(computation); + const scopeWritePromise = writeScope(creatureId, computation); + await Promise.all([writePromise, scopeWritePromise]); + } catch (e) { const errorText = e.reason || e.message || e.toString(); computation.errors.push({ type: 'crash', @@ -30,8 +31,20 @@ function computeComputation(computation, creatureId) { logError.location = e.stack.split('\n')[1]; } console.error(logError); - throw e; } finally { - writeErrors(creatureId, computation.errors); + checkPropertyCount(computation) + writeErrorsAndPropCount(creatureId, computation.errors, computation.props.length); } } + +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} )` + }, + }); +} diff --git a/app/imports/api/engine/loadCreatures.js b/app/imports/api/engine/loadCreatures.js deleted file mode 100644 index aa036a37..00000000 --- a/app/imports/api/engine/loadCreatures.js +++ /dev/null @@ -1,303 +0,0 @@ -import { debounce } from 'lodash'; -import Creatures from '/imports/api/creature/creatures/Creatures.js'; -import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables'; -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import computeCreature from './computeCreature'; - -const COMPUTE_DEBOUNCE_TIME = 100; // ms -export const loadedCreatures = new Map(); // creatureId => {creature, properties, etc.} - -export function loadCreature(creatureId, subscription) { - if (!creatureId) throw 'creatureId is required'; - let creature = loadedCreatures.get(creatureId); - if (loadedCreatures.has(creatureId)) { - creature.subs.add(subscription); - } else { - creature = new LoadedCreature(subscription, creatureId); - loadedCreatures.set(creatureId, creature); - } - subscription.onStop(() => { - unloadCreature(creatureId, subscription); - }); -} - -function unloadCreature(creatureId, subscription) { - if (!creatureId) throw 'creatureId is required'; - const creature = loadedCreatures.get(creatureId); - if (!creature) return; - creature.subs.delete(subscription); - if (creature.subs.size === 0) { - creature.stop(); - loadedCreatures.delete(creatureId); - } -} - -export function getSingleProperty(creatureId, propertyId) { - if (loadedCreatures.has(creatureId)) { - const creature = loadedCreatures.get(creatureId); - const property = creature.properties.get(propertyId); - const cloneProp = EJSON.clone(property); - return cloneProp; - } - // console.time(`Cache miss on creature properties: ${creatureId}`) - const prop = CreatureProperties.findOne({ - _id: propertyId, - 'ancestors.id': creatureId, - 'removed': {$ne: true}, - }, { - sort: { order: 1 }, - }); - // console.timeEnd(`Cache miss on creature properties: ${creatureId}`); - return prop; -} - -export function getProperties(creatureId) { - if (loadedCreatures.has(creatureId)) { - const creature = loadedCreatures.get(creatureId); - const props = Array.from(creature.properties.values()); - const cloneProps = EJSON.clone(props); - return cloneProps - } - // console.time(`Cache miss on creature properties: ${creatureId}`) - const props = CreatureProperties.find({ - 'ancestors.id': creatureId, - 'removed': {$ne: true}, - }, { - sort: { order: 1 }, - }).fetch(); - // console.timeEnd(`Cache miss on creature properties: ${creatureId}`); - return props; -} - -export function getPropertiesOfType(creatureId, propType) { - if (loadedCreatures.has(creatureId)) { - const creature = loadedCreatures.get(creatureId); - const props = [] - for (const prop of creature.properties.values()){ - if (prop.type === propType) { - props.push(prop); - } - } - const cloneProps = EJSON.clone(props); - return cloneProps - } - // console.time(`Cache miss on creature properties: ${creatureId}`) - const props = CreatureProperties.find({ - 'ancestors.id': creatureId, - 'removed': { $ne: true }, - 'type': propType, - }, { - sort: { order: 1 }, - }).fetch(); - // console.timeEnd(`Cache miss on creature properties: ${creatureId}`); - return props; -} - -export function getCreature(creatureId) { - if (loadedCreatures.has(creatureId)) { - const loadedCreature = loadedCreatures.get(creatureId); - const creature = loadedCreature.creature; - if (creature) { - const cloneCreature = EJSON.clone(creature); - return cloneCreature; - } - } - // console.time(`Cache miss on Creature: ${creatureId}`); - const creature = Creatures.findOne(creatureId); - // console.timeEnd(`Cache miss on Creature: ${creatureId}`); - return creature; -} - -export function getVariables(creatureId) { - if (loadedCreatures.has(creatureId)) { - const loadedCreature = loadedCreatures.get(creatureId); - const variables = loadedCreature.variables; - if (variables) { - const cloneVarables = EJSON.clone(variables); - return cloneVarables; - } - } - // console.time(`Cache miss on variables: ${creatureId}`); - const variables = CreatureVariables.findOne({_creatureId: creatureId}); - // console.timeEnd(`Cache miss on variables: ${creatureId}`); - return variables; -} - -export function getProperyAncestors(creatureId, propertyId) { - const prop = getSingleProperty(creatureId, propertyId); - if (!prop) return []; - const ancestorIds = []; - prop.ancestors.forEach(ref => { - if (ref.collection === 'creatureProperties') { - ancestorIds.push(ref.id); - } - }); - if (loadedCreatures.has(creatureId)) { - // Get the ancestor properties from the cache - const creature = loadedCreatures.get(creatureId); - const props = []; - ancestorIds.forEach(id => { - const prop = creature.properties.get(id); - if (prop) { - props.push(prop); - } - }); - const cloneProps = EJSON.clone(props); - return cloneProps - } else { - // Fetch from database - return CreatureProperties.find({ - _id: { $in: ancestorIds }, - removed: {$ne: true}, - }, { - sort: { order: 1 }, - }).fetch(); - } -} - -export function getPropertyDecendants(creatureId, propertyId) { - const property = getSingleProperty(creatureId, propertyId); - if (!property) return []; - // This prop will always appear at the same position in the ancestor array - // of its decendants, so only check there - const expectedAncestorPostition = property.ancestors.length; - if (loadedCreatures.has(creatureId)) { - const creature = loadedCreatures.get(creatureId); - const props = []; - for(const prop of creature.properties.values()){ - if (prop.ancestors[expectedAncestorPostition]?.id === propertyId) { - props.push(prop); - } - } - const cloneProps = EJSON.clone(props); - return cloneProps - } else { - return CreatureProperties.find({ - 'ancestors.id': propertyId, - removed: { $ne: true }, - }, { - sort: { order: 1 }, - }).fetch(); - } -} - -class LoadedCreature { - constructor(sub, creatureId) { - // This may be called from a subscription, but we don't want the observers - // to be destroyed with it, so use a non-reactive context to observe - // the required documents - const self = this; - Tracker.nonreactive(() => { - self.subs = new Set([sub]); - - const compute = debounce(Meteor.bindEnvironment(() => { - computeCreature(creatureId); - }), COMPUTE_DEBOUNCE_TIME); - - self.properties = new Map(); - // Observe all creature properties which are needed for computation - self.propertyObserver = CreatureProperties.find({ - 'ancestors.id': creatureId, - removed: { $ne: true }, - }, { - sort: { order: 1 }, - }).observeChanges({ - added(id, fields) { - fields._id = id; - self.addProperty(fields); - if (fields.dirty) compute(); - }, - changed(id, fields) { - self.changeProperty(id, fields); - if (fields.dirty) compute(); - }, - removed(id) { - self.removeProperty(id); - compute(); - }, - }); - - // Observe the creature itself - self.creatureObserver = Creatures.find({ - _id: creatureId, - }).observeChanges({ - added(id, fields) { - fields._id = id; - self.addCreature(fields) - if (fields.dirty) compute(); - }, - changed(id, fields) { - self.changeCreature(id, fields); - if (fields.dirty) compute(); - }, - removed(id) { - self.removeCreature(id); - }, - }); - - // Observe the creature's variables - self.variablesObserver = CreatureVariables.find({ - _creatureId: creatureId, - }, { - fields: { _creatureId: 0}, - }).observeChanges({ - added(id, fields) { - fields._id = id; - self.addVariables(fields) - }, - changed(id, fields) { - self.changeVariables(id, fields); - }, - removed(id) { - self.removeVariables(id); - }, - }); - }); - } - stop() { - this.propertyObserver.stop(); - this.creatureObserver.stop(); - this.variablesObserver.stop(); - } - addProperty(prop) { - this.properties.set(prop._id, prop); - } - changeProperty(id, fields) { - LoadedCreature.changeMap(id, fields, this.properties); - } - removeProperty(id) { - this.properties.delete(id) - } - addCreature(creature) { - this.creature = creature; - } - changeCreature(id, fields) { - LoadedCreature.changeDoc(this.creature, fields); - } - removeCreature() { - delete this.creature; - } - addVariables(variables) { - this.variables = variables; - } - changeVariables(id, fields) { - LoadedCreature.changeDoc(this.variables, fields); - } - removeVariables() { - delete this.variables; - } - static changeMap(id, fields, map) { - const doc = map.get(id); - LoadedCreature.changeDoc(doc, fields); - } - static changeDoc(doc, fields) { - if (!doc) return; - for (let key in fields) { - if (key === undefined) { - delete doc[key]; - } else { - doc[key] = fields[key]; - } - } - } -} diff --git a/app/imports/api/engine/loadCreatures.ts b/app/imports/api/engine/loadCreatures.ts new file mode 100644 index 00000000..5e05df26 --- /dev/null +++ b/app/imports/api/engine/loadCreatures.ts @@ -0,0 +1,371 @@ +import { debounce } from 'lodash'; +import Creatures from '/imports/api/creature/creatures/Creatures'; +import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables'; +import CreatureProperties, { CreatureProperty } from '/imports/api/creature/creatureProperties/CreatureProperties'; +import computeCreature from './computeCreature'; +import { getFilter } from '/imports/api/parenting/parentingFunctions'; + +const COMPUTE_DEBOUNCE_TIME = 100; // ms +export const loadedCreatures: Map = new Map(); // creatureId => {creature, properties, etc.} + +// function logLoadedCreatures() { +// let creatureLoadString = ''; +// for (const [key, value] of loadedCreatures.entries()) { +// creatureLoadString += `${key}: ${value.subs.size}\n`; +// } +// console.log(creatureLoadString); +// } + +export function loadCreature(creatureId: string, subscription: Tracker.Computation) { + if (!creatureId) throw 'creatureId is required'; + let creature = loadedCreatures.get(creatureId); + if (!creature?.subs.has(subscription)) { + subscription.onStop(() => { + unloadCreature(creatureId, subscription); + }); + } + if (creature) { + creature.subs.add(subscription); + } else { + creature = new LoadedCreature(subscription, creatureId); + loadedCreatures.set(creatureId, creature); + } + // logLoadedCreatures() +} + +function unloadCreature(creatureId, subscription) { + if (!creatureId) throw 'creatureId is required'; + const creature = loadedCreatures.get(creatureId); + if (!creature) return; + creature.subs.delete(subscription); + if (creature.subs.size === 0) { + creature.stop(); + loadedCreatures.delete(creatureId); + } + // logLoadedCreatures() +} + +export function getSingleProperty(creatureId: string, propertyId: string) { + const creature = loadedCreatures.get(creatureId) + const property = creature?.properties.get(propertyId); + if (property?.removed) return; + if (property) { + return EJSON.clone(property); + } + // console.time(`Cache miss on creature properties: ${creatureId}`) + const prop = CreatureProperties.findOne({ + _id: propertyId, + 'root.id': creatureId, + 'removed': { $ne: true }, + }); + // console.timeEnd(`Cache miss on creature properties: ${creatureId}`); + return prop; +} + +export function getProperties(creatureId: string): CreatureProperty[] { + const creature = loadedCreatures.get(creatureId); + if (creature) { + const props = Array.from(creature.properties.values()) + .sort((a, b) => a.left - b.left) + .filter(prop => !prop.removed); + return EJSON.clone(props); + } + // console.time(`Cache miss on creature properties: ${creatureId}`) + const props = CreatureProperties.find({ + 'root.id': creatureId, + 'removed': { $ne: true }, + }, { + sort: { left: 1 }, + }).fetch(); + // console.timeEnd(`Cache miss on creature properties: ${creatureId}`); + return props; +} + +export function getPropertiesOfType(creatureId, propType) { + const creature = loadedCreatures.get(creatureId); + if (creature) { + const props = Array.from(creature.properties.values()) + .filter(prop => !prop.removed && prop.type === propType) + .sort((a, b) => a.left - b.left); + return EJSON.clone(props); + } + // console.time(`Cache miss on creature properties: ${creatureId}`) + const props = CreatureProperties.find({ + 'root.id': creatureId, + 'removed': { $ne: true }, + 'type': propType, + }, { + sort: { left: 1 }, + }).fetch(); + // console.timeEnd(`Cache miss on creature properties: ${creatureId}`); + return props; +} + +/** + * Get the properties of a creature that matches the filters given + * @param creatureId The id of the creature + * @param filterFn A function that returns true if the given prop matches the filter + * @param mongoFilter A mongo selector that is exactly equal to the above function + */ +export function getPropertiesByFilter(creatureId, filterFn: (any) => boolean, mongoFilter: Mongo.Selector) { + const creature = loadedCreatures.get(creatureId); + if (creature) { + const props: CreatureProperty[] = Array.from(creature.properties.values()) + .filter(filterFn) + .sort((a, b) => a.left - b.left); + return EJSON.clone(props); + } + // console.time(`Cache miss on creature properties: ${creatureId}`) + const props = CreatureProperties.find({ + 'root.id': creatureId, + 'removed': { $ne: true }, + ...mongoFilter + }, { + sort: { left: 1 }, + }).fetch(); + // console.timeEnd(`Cache miss on creature properties: ${creatureId}`); + return props; +} + +export function getCreature(creatureId: string) { + const loadedCreature = loadedCreatures.get(creatureId); + const loadedCreatureDoc = loadedCreature?.creature; + if (loadedCreatureDoc) { + return EJSON.clone(loadedCreatureDoc); + } + // console.time(`Cache miss on Creature: ${creatureId}`); + const creature = Creatures.findOne(creatureId); + // console.timeEnd(`Cache miss on Creature: ${creatureId}`); + return creature; +} + +export function getVariables(creatureId: string) { + const loadedCreature = loadedCreatures.get(creatureId); + const loadedVariables = loadedCreature?.variables; + if (loadedVariables) { + return EJSON.clone(loadedVariables); + } + // console.time(`Cache miss on variables: ${creatureId}`); + const variables = CreatureVariables.findOne({ _creatureId: creatureId }); + // console.timeEnd(`Cache miss on variables: ${creatureId}`); + return variables; +} + +export function replaceLinkedVariablesWithProps(variables) { + for (const key in variables) { + const propId = variables[key]?._propId; + if (!propId) continue; + variables[key] = getSingleProperty(variables._creatureId, propId); + } +} + +export function getPropertyAncestors(creatureId: string, propertyId: string) { + const prop = getSingleProperty(creatureId, propertyId); + if (!prop) return []; + const loadedCreature = loadedCreatures.get(creatureId); + if (loadedCreature) { + // Get the ancestor properties from the cache + const props: CreatureProperty[] = []; + let currentProp: CreatureProperty | undefined = prop; + // Iterate through parent chain to get all linked ancestors + while (currentProp?.parentId) { + currentProp = getSingleProperty(creatureId, currentProp.parentId); + if (currentProp) props.push(currentProp); + } + return EJSON.clone(props); + } else { + // Fetch from database + return CreatureProperties.find({ + ...getFilter.ancestors(prop), + removed: { $ne: true }, + }, { + sort: { left: 1 } + }).fetch(); + } +} + +export function getPropertyDescendants(creatureId, propertyId) { + const property = getSingleProperty(creatureId, propertyId); + if (!property) return []; + if (loadedCreatures.has(creatureId)) { + const creature = loadedCreatures.get(creatureId); + if (!creature) return []; + const props: CreatureProperty[] = []; + // Loop through all properties and find ones that match the nested set condition + for (const prop of creature.properties.values()) { + if ( + prop.left > property.left + && prop.right < property.right + && prop.removed !== true + ) { + props.push(prop); + } + } + const cloneProps = EJSON.clone(props).sort((a, b) => a.left - b.left); + return cloneProps; + } else { + return CreatureProperties.find({ + ...getFilter.descendants(property), + removed: { $ne: true }, + }, { + sort: { left: 1 }, + }).fetch(); + } +} + +/** + * @param {string} creatureId Creature ID + * @param {string | any} property prop or prop ID to get children of + * @returns {any[]} An array of child properties in tree order + */ +export function getPropertyChildren(creatureId, property) { + if (typeof property === 'string') { + property = getSingleProperty(creatureId, property); + } + if (!property) return []; + // This propertyId will always appear in the parent of the children + if (loadedCreatures.has(creatureId)) { + const creature = loadedCreatures.get(creatureId); + if (!creature) return []; + const props: CreatureProperty[] = []; + for (const prop of creature.properties.values()) { + if (prop.parentId === property._id && prop.removed !== true) { + props.push(prop); + } + } + const cloneProps = EJSON.clone(props); + return cloneProps.sort((a, b) => a.left - b.left); + } else { + return CreatureProperties.find({ + 'parentId': property._id, + removed: { $ne: true }, + }, { + sort: { left: 1 }, + }).fetch(); + } +} + +class LoadedCreature { + subs: Set; + propertyObserver: Meteor.LiveQueryHandle; + creatureObserver: Meteor.LiveQueryHandle; + variablesObserver: Meteor.LiveQueryHandle; + properties: Map; + creature: any; + variables: any; + + constructor(sub, creatureId) { + const self = this; + // This may be called from a subscription, but we don't want the observers + // to be destroyed with it, so use a non-reactive context to observe + // the required documents + Tracker.nonreactive(() => { + self.subs = new Set([sub]); + const compute = debounce(Meteor.bindEnvironment(() => { + computeCreature(creatureId); + }), COMPUTE_DEBOUNCE_TIME); + + self.properties = new Map(); + // Observe all creature properties which are needed for computation + self.propertyObserver = CreatureProperties.find({ + 'root.id': creatureId, + }).observeChanges({ + added(id, fields) { + fields._id = id; + self.addProperty(fields); + if (fields.dirty) compute(); + }, + changed(id, fields) { + self.changeProperty(id, fields); + if (fields.dirty) compute(); + }, + removed(id) { + self.removeProperty(id); + compute(); + }, + }); + + // Observe the creature itself + self.creatureObserver = Creatures.find({ + _id: creatureId, + }).observeChanges({ + added(id, fields) { + fields._id = id; + self.addCreature(fields) + if (fields.dirty) compute(); + }, + changed(id, fields) { + self.changeCreature(id, fields); + if (fields.dirty) compute(); + }, + removed() { + self.removeCreature(); + }, + }); + + // Observe the creature's variables + self.variablesObserver = CreatureVariables.find({ + _creatureId: creatureId, + }, { + fields: { _creatureId: 0 }, + }).observeChanges({ + added(id, fields) { + fields._id = id; + self.addVariables(fields) + }, + changed(id, fields) { + self.changeVariables(id, fields); + }, + removed() { + self.removeVariables(); + }, + }); + }); + } + stop() { + this.propertyObserver.stop(); + this.creatureObserver.stop(); + this.variablesObserver.stop(); + } + addProperty(prop) { + this.properties.set(prop._id, prop); + } + changeProperty(id, fields) { + LoadedCreature.changeMap(id, fields, this.properties); + } + removeProperty(id) { + this.properties.delete(id) + } + addCreature(creature) { + this.creature = creature; + } + changeCreature(id, fields) { + LoadedCreature.changeDoc(this.creature, fields); + } + removeCreature() { + delete this.creature; + } + addVariables(variables) { + this.variables = variables; + } + changeVariables(id, fields) { + LoadedCreature.changeDoc(this.variables, fields); + } + removeVariables() { + delete this.variables; + } + static changeMap(id, fields, map) { + const doc = map.get(id); + LoadedCreature.changeDoc(doc, fields); + } + static changeDoc(doc, fields) { + if (!doc) return; + for (const key in fields) { + if (key === undefined) { + delete doc[key]; + } else { + doc[key] = fields[key]; + } + } + } +} diff --git a/app/imports/api/engine/shared/bulkWrite.ts b/app/imports/api/engine/shared/bulkWrite.ts new file mode 100644 index 00000000..4a9bacd9 --- /dev/null +++ b/app/imports/api/engine/shared/bulkWrite.ts @@ -0,0 +1,61 @@ +// This is more efficient on the database, but significantly less efficient +// 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 +export default function bulkWrite(bulkWriteOps, collection): void | Promise { + if (!bulkWriteOps.length) return; + // bulkWrite is only available on the server + if (!Meteor.isServer) { + return writePropertiesSequentially(bulkWriteOps, collection); + } + return collection.rawCollection().bulkWrite( + bulkWriteOps, + { ordered: false } + ); +} + +// If we re-enable client-side sheet recalculation, this needs to be run on +// both client and server to preserve latency compensation. Bulkwrite breaks +// latency compensation and causes flickering +function writePropertiesSequentially(bulkWriteOps: any[], collection: Mongo.Collection) { + bulkWriteOps.forEach(op => { + const insertOne = op.insertOne; + if (insertOne) { + collection.insert(insertOne); + } + const updateOneOrMany = op.updateOne || op.updateMany; + if (updateOneOrMany) { + collection.update(updateOneOrMany.filter, updateOneOrMany.update, { + // The bulk code is bypassing validation, so do the same here + // @ts-expect-error Collection 2 has no typescript support + bypassCollection2: true, + }); + } + }); +} + +export function newOperation(_id) { + const newOp = { + updateOne: { + filter: { _id }, + update: {}, + } + }; + return newOp; +} + +export function addSetOp(op, key, value) { + if (op.updateOne.update.$set) { + op.updateOne.update.$set[key] = value; + } else { + op.updateOne.update.$set = { [key]: value }; + } +} + +export function addUnsetOp(op, key) { + if (op.updateOne.update.$unset) { + op.updateOne.update.$unset[key] = 1; + } else { + op.updateOne.update.$unset = { [key]: 1 }; + } +} diff --git a/app/imports/api/files/UserImages.js b/app/imports/api/files/UserImages.js deleted file mode 100644 index fad4576b..00000000 --- a/app/imports/api/files/UserImages.js +++ /dev/null @@ -1,19 +0,0 @@ -import { createS3FilesCollection } from '/imports/api/files/s3FileStorage.js'; - -const UserImages = createS3FilesCollection({ - collectionName: 'userImages', - storagePath: Meteor.isDevelopment ? '/DiceCloud/userImages/' : 'assets/app/userImages', - onBeforeUpload(file) { - // Allow upload files under 10MB - if (file.size > 10485760) { - return 'Please upload with size equal or less than 10MB'; - } - // Allow common image extensions - if (!/gif|png|jpe?g|webp/i.test(file.extension || '')) { - return 'Please upload an image file only'; - } - return true - } -}); - -export default UserImages; diff --git a/app/imports/api/files/assertUserHasFileSpace.ts b/app/imports/api/files/assertUserHasFileSpace.ts new file mode 100644 index 00000000..59d79713 --- /dev/null +++ b/app/imports/api/files/assertUserHasFileSpace.ts @@ -0,0 +1,23 @@ +import { getUserTier } from '/imports/api/users/patreon/tiers'; +import prettyBytes from 'pretty-bytes'; + +export default function assertUserHasFileSpace(userId: string | null, spaceRequiredInBytes: number) { + // Get the user + if (!userId) throw new Meteor.Error('permission-denied', 'No user was provided'); + const user = Meteor.users.findOne(userId, { fields: { fileStorageUsed: 1 } }); + if (!user) throw new Meteor.Error('permission-denied', 'User not found'); + + // Work out how much space they have and need + const fileStorageUsed = user.fileStorageUsed || 0; + const fileStorageAllowed = getUserTier(Meteor.userId()).fileStorage * 1000000; + let fileStorageLeft = fileStorageAllowed - fileStorageUsed; + if (fileStorageLeft < 0) fileStorageLeft = 0; + + // Throw an error if they don't have space + if (fileStorageLeft < spaceRequiredInBytes) { + throw new Meteor.Error('insufficient-space', + `Not enough storage space left, you need ${prettyBytes(spaceRequiredInBytes)}, ` + + `but only have ${prettyBytes(fileStorageLeft)} available` + ); + } +} \ No newline at end of file diff --git a/app/imports/api/files/client/s3FileStorage.js b/app/imports/api/files/client/s3FileStorage.js new file mode 100644 index 00000000..ce2777cc --- /dev/null +++ b/app/imports/api/files/client/s3FileStorage.js @@ -0,0 +1,24 @@ +// https://github.com/VeliovGroup/Meteor-Files/blob/master/docs/aws-s3-integration.md +import { FilesCollection } from 'meteor/ostrio:files'; + +const createS3FilesCollection = function ({ + collectionName, + storagePath, + onBeforeUpload, + onAfterUpload, + debug,// = !Meteor.isProduction, + allowClientCode = false, +}) { + const collection = new FilesCollection({ + collectionName, + storagePath, + onBeforeUpload, + onAfterUpload, + debug, + allowClientCode, + }); + + return collection; +} + +export { createS3FilesCollection }; diff --git a/app/imports/api/files/s3FileStorage.js b/app/imports/api/files/server/s3FileStorage.ts similarity index 62% rename from app/imports/api/files/s3FileStorage.js rename to app/imports/api/files/server/s3FileStorage.ts index 3c3c540e..02d67fa1 100644 --- a/app/imports/api/files/s3FileStorage.js +++ b/app/imports/api/files/server/s3FileStorage.ts @@ -2,9 +2,9 @@ import { Meteor } from 'meteor/meteor'; import { each, clone } from 'lodash'; import { Random } from 'meteor/random'; -import { FilesCollection } from 'meteor/ostrio:files'; +import { FileObj, FileRef, FilesCollection, FilesCollectionConfig } from 'meteor/ostrio:files'; import stream from 'stream'; -import S3 from 'aws-sdk/clients/s3'; +import { S3 } from '@aws-sdk/client-s3'; /* See fs-extra and graceful-fs NPM packages */ /* For better i/o performance */ @@ -21,44 +21,58 @@ Meteor.settings.useS3 = !!( s3Conf && s3Conf.key && s3Conf.secret && s3Conf.bucket && s3Conf.endpoint ); -const bound = Meteor.bindEnvironment((callback) => { +const bound = Meteor.bindEnvironment((callback: () => any) => { return callback(); }); let createS3FilesCollection; +type S3Metadata = { + pipePath: string, +} + +type S3FilesCollection = FilesCollection & { + readJSONFile?: (file: FileObj) => Promise +}; + /* Check settings existence in `Meteor.settings` */ /* This is the best practice for app security */ -if (Meteor.isServer && Meteor.settings.useS3) { +if (Meteor.settings.useS3) { // Create a new S3 object const s3 = new S3({ - accessKeyId: s3Conf.key, - secretAccessKey: s3Conf.secret, + credentials: { + accessKeyId: s3Conf.key, + secretAccessKey: s3Conf.secret, + }, + region: 'ENAM', endpoint: s3Conf.endpoint, - sslEnabled: true, // optional - maxRetries: 10, - httpOptions: { - timeout: 12000, - agent: false - } + tls: true, + maxAttempts: 10, }); - createS3FilesCollection = function({ + createS3FilesCollection = function ({ collectionName, storagePath, onBeforeUpload, onAfterUpload, - debug = !Meteor.isProduction, + debug,// = !Meteor.isProduction, allowClientCode = false, - }){ - const collection = new FilesCollection({ + }: { + collectionName: string, + storagePath: string, + onBeforeUpload: (...args: any[]) => any, + onAfterUpload: (...args: any[]) => any, + debug: boolean, + allowClientCode?: boolean, + }) { + const filesCollection: S3FilesCollection = new FilesCollection({ collectionName, storagePath, onBeforeUpload, onAfterUpload(fileRef) { // Call the provided afterUpload hook first onAfterUpload?.(fileRef); - + // Start moving files to AWS:S3 // after fully received by the Meteor server @@ -80,36 +94,43 @@ if (Meteor.isServer && Meteor.settings.useS3) { Key: filePath, Body: fs.createReadStream(vRef.path), ContentType: vRef.type, - }, (error) => { + }, (error: Error) => { bound(() => { if (error) { - console.error(error); - } else { - // Update FilesCollection with link to the file at AWS - const upd = { $set: {} }; - upd['$set']['versions.' + version + '.meta.pipePath'] = filePath; - - this.collection.update({ - _id: fileRef._id - }, upd, (updError) => { - if (updError) { - console.error(updError); - } else { - // Unlink original files from FS after successful upload to AWS:S3 - this.unlink(this.collection.findOne(fileRef._id), version); - } - }); + this.emit('s3Result', error, fileRef); + return console.error(error); } + // Update FilesCollection with link to the file at AWS + // any should actually be Mongo.Modifier>, but the types aren't quite set up + // Right for mongo modifiers on version.meta + const upd: any = { + $set: { + [`versions.${version}.meta.pipePath`]: filePath + } + }; + + filesCollection.collection.update({ + _id: fileRef._id + }, upd, undefined, (updError: any) => { + if (updError) { + this.emit('s3Result', updError, fileRef); + console.error(updError); + } else { + // Unlink original files from FS after successful upload to AWS:S3 + filesCollection.unlink(filesCollection.findOne(fileRef._id), version); + this.emit('s3Result', undefined, fileRef) + } + }); }); }); }); }, - interceptDownload(http, fileRef, version) { + interceptDownload(http: any, fileRef: FileRef, version: string) { // Intercept access to the file // And redirect request to AWS:S3 let path; - if (fileRef && fileRef.versions && fileRef.versions[version] && fileRef.versions[version].meta && fileRef.versions[version].meta.pipePath) { + if (fileRef?.versions?.[version]?.meta?.pipePath) { path = fileRef.versions[version].meta.pipePath; } @@ -122,32 +143,32 @@ if (Meteor.isServer && Meteor.settings.useS3) { // and to keep original file name, content-type, // content-disposition, chunked "streaming" and cache-control // we're using low-level .serve() method - const opts = { + const opts: Parameters[0] = { Bucket: s3Conf.bucket, Key: path }; if (http.request.headers.range) { - const vRef = fileRef.versions[version]; - let range = clone(http.request.headers.range); + const vRef = fileRef.versions[version]; + const range = clone(http.request.headers.range); const array = range.split(/bytes=([0-9]*)-([0-9]*)/); const start = parseInt(array[1]); - let end = parseInt(array[2]); + let end = parseInt(array[2]); if (isNaN(end)) { // Request data from AWS:S3 by small chunks - end = (start + this.chunkSize) - 1; + end = (start + (this.chunkSize || 0)) - 1; if (end >= vRef.size) { - end = vRef.size - 1; + end = vRef.size - 1; } } - opts.Range = `bytes=${start}-${end}`; + opts.Range = `bytes=${start}-${end}`; http.request.headers.range = `bytes=${start}-${end}`; } const fileColl = this; s3.getObject(opts, function (error) { if (error) { - console.error(error); + console.error('Error getting s3 object', opts, error); if (!http.response.finished) { http.response.end(); } @@ -173,17 +194,17 @@ if (Meteor.isServer && Meteor.settings.useS3) { allowClientCode, }); // Intercept FilesCollection's remove method to remove file from AWS:S3 - const _origRemove = collection.remove; - collection.remove = function (search) { + const _origRemove = filesCollection.remove; + filesCollection.remove = function (search) { const cursor = this.collection.find(search); cursor.forEach((fileRef) => { each(fileRef.versions, (vRef) => { - if (vRef && vRef.meta && vRef.meta.pipePath) { + if (vRef?.meta?.pipePath) { // Remove the object from AWS:S3 first, then we will call the original FilesCollection remove s3.deleteObject({ Bucket: s3Conf.bucket, Key: vRef.meta.pipePath, - }, (error) => { + }, (error: any) => { bound(() => { if (error) { console.error(error); @@ -195,18 +216,19 @@ if (Meteor.isServer && Meteor.settings.useS3) { }); //remove original file from database - _origRemove.call(this, search); + return _origRemove.call(this, search); }; - collection.readJSONFile = async function(file){ + filesCollection.readJSONFile = async function (file: FileObj) { // If there is the pipepath, use s3 to get the file - if (file?.versions?.original?.meta?.pipePath){ + if (file?.versions?.original?.meta?.pipePath) { const path = file.versions.original.meta.pipePath; const data = await s3.getObject({ Bucket: s3Conf.bucket, Key: path - }).promise(); - return JSON.parse(data.Body.toString('utf-8')); + }); + if (!data.Body) return; + return JSON.parse(await data.Body.transformToString()); } else { // Otherwise use the normal filesystem const fileString = await fsp.readFile(file.path, 'utf8'); @@ -214,18 +236,18 @@ if (Meteor.isServer && Meteor.settings.useS3) { } }; - return collection; + return filesCollection; } } else { - createS3FilesCollection = function({ + createS3FilesCollection = function ({ collectionName, storagePath, onBeforeUpload, onAfterUpload, - debug = !Meteor.isProduction, + debug,// = !Meteor.isProduction, allowClientCode = false, - }){ - const collection = new FilesCollection({ + }: FilesCollectionConfig) { + const collection: S3FilesCollection = new FilesCollection({ collectionName, storagePath, onBeforeUpload, @@ -234,13 +256,11 @@ if (Meteor.isServer && Meteor.settings.useS3) { allowClientCode, }); - if (Meteor.isServer) { - // Use the normal file system to read files - collection.readJSONFile = async function(file){ - const fileString = await fsp.readFile(file.path, 'utf8'); - return JSON.parse(fileString); - }; - } + // Use the normal file system to read files + collection.readJSONFile = async function (file) { + const fileString = await fsp.readFile(file.path, 'utf8'); + return JSON.parse(fileString); + }; return collection; } diff --git a/app/imports/api/files/userImages/UserImages.ts b/app/imports/api/files/userImages/UserImages.ts new file mode 100644 index 00000000..e6dff275 --- /dev/null +++ b/app/imports/api/files/userImages/UserImages.ts @@ -0,0 +1,34 @@ + +import { incrementFileStorageUsed } from '/imports/api/users/methods/updateFileStorageUsed'; +import assertUserHasFileSpace from '/imports/api/files/assertUserHasFileSpace'; +let createS3FilesCollection; +if (Meteor.isServer) { + createS3FilesCollection = require('/imports/api/files/server/s3FileStorage').createS3FilesCollection +} else { + createS3FilesCollection = require('/imports/api/files/client/s3FileStorage').createS3FilesCollection +} + +const UserImages = createS3FilesCollection({ + collectionName: 'userImages', + storagePath: Meteor.isDevelopment ? '../../../../../fileStorage/userImages' : 'assets/app/userImages', + onBeforeUpload(file) { + // Allow upload files under 30MB + if (file.size > 30_000_000) { + return 'Images must be less than 30MB'; + } + // Make sure the user has enough space + assertUserHasFileSpace(Meteor.userId(), file.size); + // Allow common image extensions + if (!/gif|png|jpe?g|webp/i.test(file.extension || '')) { + return 'Please upload an image file only'; + } + return true + }, + onAfterUpload(file) { + if (Meteor.isServer) incrementFileStorageUsed(file.userId, file.size); + } +}); + +import './methods'; + +export default UserImages; diff --git a/app/imports/api/files/userImages/methods/index.js b/app/imports/api/files/userImages/methods/index.js new file mode 100644 index 00000000..3458d456 --- /dev/null +++ b/app/imports/api/files/userImages/methods/index.js @@ -0,0 +1 @@ +import './removeUserImage'; diff --git a/app/imports/api/files/userImages/methods/removeUserImage.ts b/app/imports/api/files/userImages/methods/removeUserImage.ts new file mode 100644 index 00000000..e81b4b11 --- /dev/null +++ b/app/imports/api/files/userImages/methods/removeUserImage.ts @@ -0,0 +1,44 @@ +import SimpleSchema from 'simpl-schema'; +import { ValidatedMethod } from 'meteor/mdg:validated-method'; +import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; +import { incrementFileStorageUsed } from '/imports/api/users/methods/updateFileStorageUsed'; +import UserImages from '/imports/api/files/userImages/UserImages'; + +const removeUserImage = new ValidatedMethod({ + name: 'userImages.methods.remove', + validate: new SimpleSchema({ + 'fileId': { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, + }).validator(), + mixins: [RateLimiterMixin], + rateLimit: { + numRequests: 5, + timeInterval: 5000, + }, + async run({ fileId }) { + if (!this.userId) { + throw new Meteor.Error('logged-out', + 'The user must be logged in to remove a file'); + } + // fetch the file + const file = UserImages.findOne({ _id: fileId }).get(); + if (!file) { + throw new Meteor.Error('File not found', + 'The requested creature archive does not exist'); + } + // Assert ownership + const userId = file?.userId; + if (!userId || userId !== this.userId) { + throw new Meteor.Error('Permission denied', + 'You can only restore creatures you own'); + } + //Remove the archive once the restore succeeded + UserImages.remove({ _id: fileId }); + // Update the user's file storage limits + incrementFileStorageUsed(userId, -file.size); + }, +}); + +export default removeUserImage; diff --git a/app/imports/api/getModifierFields.js b/app/imports/api/getModifierFields.js index f26e42af..57548ede 100644 --- a/app/imports/api/getModifierFields.js +++ b/app/imports/api/getModifierFields.js @@ -1,8 +1,8 @@ -import MONGO_OPERATORS from '/imports/constants/MONGO_OPERATORS.js'; +import MONGO_OPERATORS from '/imports/constants/MONGO_OPERATORS'; -const hasAny = function(values){ - for (let value of values){ - if (this.has(value)){ +const hasAny = function (values) { + for (let value of values) { + if (this.has(value)) { return true; } } @@ -11,11 +11,11 @@ const hasAny = function(values){ // Returns a Set of fields the modifier changes // The set has been extended with the "hasAny" function -export default function getModifierFields (modifier) { +export default function getModifierFields(modifier) { let fields = new Set(); - for (let operator of MONGO_OPERATORS){ - if (modifier[operator]) for (let field in modifier[operator]){ + for (let operator of MONGO_OPERATORS) { + if (modifier[operator]) for (let field in modifier[operator]) { const indexOfDot = field.indexOf('.'); if (indexOfDot !== -1) { field = field.substring(0, indexOfDot); diff --git a/app/imports/api/icons/Icons.js b/app/imports/api/icons/Icons.js index 072d2467..eb2f8fe1 100644 --- a/app/imports/api/icons/Icons.js +++ b/app/imports/api/icons/Icons.js @@ -1,8 +1,8 @@ import SimpleSchema from 'simpl-schema'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import { assertAdmin } from '/imports/api/sharing/sharingPermissions.js'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; +import { assertAdmin } from '/imports/api/sharing/sharingPermissions'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; let Icons = new Mongo.Collection('icons'); diff --git a/app/imports/api/library/Libraries.js b/app/imports/api/library/Libraries.js index 696f0001..bdf2475b 100644 --- a/app/imports/api/library/Libraries.js +++ b/app/imports/api/library/Libraries.js @@ -1,12 +1,13 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import SimpleSchema from 'simpl-schema'; -import SharingSchema from '/imports/api/sharing/SharingSchema.js'; -import simpleSchemaMixin from '/imports/api/creature/mixins/simpleSchemaMixin.js'; -import { assertEditPermission, assertOwnership } from '/imports/api/sharing/sharingPermissions.js'; -import LibraryNodes from '/imports/api/library/LibraryNodes.js'; -import { getUserTier } from '/imports/api/users/patreon/tiers.js' -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; +import SharingSchema from '/imports/api/sharing/SharingSchema'; +import simpleSchemaMixin from '/imports/api/creature/mixins/simpleSchemaMixin'; +import { assertEditPermission, assertOwnership } from '/imports/api/sharing/sharingPermissions'; +import LibraryNodes from '/imports/api/library/LibraryNodes'; +import { getUserTier } from '/imports/api/users/patreon/tiers' +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; +import { getFilter } from '/imports/api/parenting/parentingFunctions'; /** * Libraries are trees of library nodes where each node represents a character @@ -29,6 +30,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 +115,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({ @@ -127,7 +161,7 @@ const removeLibrary = new ValidatedMethod({ export function removeLibaryWork(libraryId) { Libraries.remove(libraryId); - LibraryNodes.remove({ 'ancestors.id': libraryId }); + LibraryNodes.remove(getFilter.descendantsOfRoot(libraryId)); } -export { LibrarySchema, insertLibrary, updateLibraryName, updateLibraryDescription, removeLibrary }; +export { LibrarySchema, insertLibrary, updateLibraryName, updateLibraryDescription, updateLibraryShowInMarket, removeLibrary }; diff --git a/app/imports/api/library/LibraryCollections.js b/app/imports/api/library/LibraryCollections.js index aec4d351..b09912db 100644 --- a/app/imports/api/library/LibraryCollections.js +++ b/app/imports/api/library/LibraryCollections.js @@ -1,11 +1,11 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import SimpleSchema from 'simpl-schema'; -import SharingSchema from '/imports/api/sharing/SharingSchema.js'; -import simpleSchemaMixin from '/imports/api/creature/mixins/simpleSchemaMixin.js'; -import { assertEditPermission, assertOwnership } from '/imports/api/sharing/sharingPermissions.js'; -import { getUserTier } from '/imports/api/users/patreon/tiers.js' -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; +import SharingSchema from '/imports/api/sharing/SharingSchema'; +import simpleSchemaMixin from '/imports/api/creature/mixins/simpleSchemaMixin'; +import { assertEditPermission, assertOwnership } from '/imports/api/sharing/sharingPermissions'; +import { getUserTier } from '/imports/api/users/patreon/tiers' +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; /** * LibraryCollections are groups of libraries that are subscribed together at once @@ -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, diff --git a/app/imports/api/library/LibraryNodes.js b/app/imports/api/library/LibraryNodes.js index 50d97cd3..574ea464 100644 --- a/app/imports/api/library/LibraryNodes.js +++ b/app/imports/api/library/LibraryNodes.js @@ -3,18 +3,20 @@ import { Mongo } from 'meteor/mongo'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import SimpleSchema from 'simpl-schema'; -import ColorSchema from '/imports/api/properties/subSchemas/ColorSchema.js'; -import ChildSchema 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'; -import { softRemove } from '/imports/api/parenting/softRemove.js'; -import SoftRemovableSchema from '/imports/api/parenting/SoftRemovableSchema.js'; -import { storedIconsSchema } from '/imports/api/icons/Icons.js'; -import '/imports/api/library/methods/index.js'; -import { 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 ColorSchema from '/imports/api/properties/subSchemas/ColorSchema'; +import ChildSchema, { RefSchema } from '/imports/api/parenting/ChildSchema'; +import propertySchemasIndex from '/imports/api/properties/propertySchemasIndex'; +import Libraries from '/imports/api/library/Libraries'; +import { assertEditPermission } from '/imports/api/sharing/sharingPermissions'; +import { softRemove } from '/imports/api/parenting/softRemove'; +import SoftRemovableSchema from '/imports/api/parenting/SoftRemovableSchema'; +import { storedIconsSchema } from '/imports/api/icons/Icons'; +import '/imports/api/library/methods/index'; +import { updateReferenceNodeWork } from '/imports/api/library/methods/updateReferenceNode'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; +import { restore } from '/imports/api/parenting/softRemove'; +import { fetchDocByRef, getAncestry } from '/imports/api/parenting/parentingFunctions'; +import { rebuildNestedSets } from '/imports/api/parenting/parentingFunctions'; 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 @@ -67,6 +115,13 @@ for (let key in propertySchemasIndex) { schema.extend(propertySchemasIndex[key]); schema.extend(ChildSchema); schema.extend(SoftRemovableSchema); + // Use the any schema as a default schema for the collection + if (key === 'any') { + // @ts-expect-error don't have types for .attachSchema + LibraryNodes.attachSchema(schema); + } + // TODO make this an else branch and remove all {selector: {type: any}} options + // @ts-expect-error don't have types for .attachSchema LibraryNodes.attachSchema(schema, { selector: { type: key } }); @@ -74,7 +129,7 @@ for (let key in propertySchemasIndex) { function getLibrary(node) { if (!node) throw new Meteor.Error('No node provided'); - let library = Libraries.findOne(node.ancestors[0].id); + let library = Libraries.findOne(node.root.id); if (!library) throw new Meteor.Error('Library does not exist'); return library; } @@ -86,20 +141,54 @@ 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 + const parentDoc = fetchDocByRef(parentRef); + + // Check permission to edit + let rootLibrary; + if (parentRef.collection === 'libraries') { + rootLibrary = parentDoc; + } else if (parentRef.collection === 'libraryNodes') { + rootLibrary = Libraries.findOne(parentDoc.root.id); + libraryNode.parentId = parentRef.id; + } else { + throw `${parentRef.collection} is not a valid parent collection` + } + assertEditPermission(rootLibrary, this.userId); + + // Set the root of the node we are inserting + libraryNode.root = { collection: 'libraries', id: rootLibrary._id }; + + // 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 + rebuildNestedSets(LibraryNodes, rootLibrary._id); + + // Return the id of the inserted node return nodeId; }, }); @@ -114,12 +203,14 @@ const updateLibraryNode = new ValidatedMethod({ case 'order': case 'parent': case 'ancestors': + case 'parentId': + case 'root': return false; } }, mixins: [RateLimiterMixin], rateLimit: { - numRequests: 5, + numRequests: 15, timeInterval: 5000, }, run({ _id, path, value }) { @@ -196,7 +287,7 @@ const softRemoveLibraryNode = new ValidatedMethod({ run({ _id }) { let node = LibraryNodes.findOne(_id); assertNodeEditPermission(node, this.userId); - softRemove({ _id, collection: LibraryNodes }); + softRemove(LibraryNodes, node); } }); @@ -215,7 +306,7 @@ const restoreLibraryNode = new ValidatedMethod({ let node = LibraryNodes.findOne(_id); assertNodeEditPermission(node, this.userId); // Do work - restore({ _id, collection: LibraryNodes }); + restore(LibraryNodes, node); } }); diff --git a/app/imports/api/library/getCreatureLibraryIds.js b/app/imports/api/library/getCreatureLibraryIds.js index beb9c7d3..c5c4938e 100644 --- a/app/imports/api/library/getCreatureLibraryIds.js +++ b/app/imports/api/library/getCreatureLibraryIds.js @@ -1,5 +1,5 @@ -import LibraryCollections from '/imports/api/library/LibraryCollections.js'; -import Creatures from '/imports/api/creature/creatures/Creatures.js'; +import LibraryCollections from '/imports/api/library/LibraryCollections'; +import Creatures from '/imports/api/creature/creatures/Creatures'; import getUserLibraryIds from './getUserLibraryIds'; import { intersection, union } from 'lodash'; @@ -17,14 +17,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 || []; diff --git a/app/imports/api/library/getUserLibraryIds.js b/app/imports/api/library/getUserLibraryIds.js index 55f7498d..ed347b2e 100644 --- a/app/imports/api/library/getUserLibraryIds.js +++ b/app/imports/api/library/getUserLibraryIds.js @@ -1,5 +1,5 @@ -import LibraryCollections from '/imports/api/library/LibraryCollections.js'; -import Libraries from '/imports/api/library/Libraries.js'; +import LibraryCollections from '/imports/api/library/LibraryCollections'; +import Libraries from '/imports/api/library/Libraries'; import { union } from 'lodash'; export default function getUserLibraryIds(userId) { diff --git a/app/imports/api/library/methods/copyLibraryNodeTo.js b/app/imports/api/library/methods/copyLibraryNodeTo.js index f99b184f..a77f8d39 100644 --- a/app/imports/api/library/methods/copyLibraryNodeTo.js +++ b/app/imports/api/library/methods/copyLibraryNodeTo.js @@ -1,23 +1,24 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method'; import SimpleSchema from 'simpl-schema'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import { RefSchema } from '/imports/api/parenting/ChildSchema.js'; -import LibraryNodes from '/imports/api/library/LibraryNodes.js'; +import { RefSchema } from '/imports/api/parenting/ChildSchema'; +import LibraryNodes from '/imports/api/library/LibraryNodes'; import { assertDocCopyPermission, assertDocEditPermission -} from '/imports/api/sharing/sharingPermissions.js'; +} from '/imports/api/sharing/sharingPermissions'; import { setLineageOfDocs, - renewDocIds -} from '/imports/api/parenting/parenting.js'; -import { reorderDocs } from '/imports/api/parenting/order.js'; -import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js'; + renewDocIds, + getFilter +} from '/imports/api/parenting/parentingFunctions'; +import { rebuildNestedSets } from '/imports/api/parenting/parentingFunctions'; +import { fetchDocByRef } from '/imports/api/parenting/parentingFunctions'; var snackbar; if (Meteor.isClient) { snackbar = require( - '/imports/ui/components/snackbars/SnackbarQueue.js' + '/imports/client/ui/components/snackbars/SnackbarQueue' ).snackbar } @@ -46,16 +47,18 @@ const copyLibraryNodeTo = new ValidatedMethod({ ); } const libraryNode = LibraryNodes.findOne(_id); + if (!libraryNode) throw new Meteor.Error('not-found', 'Library node was not found'); + const parentDoc = fetchDocByRef(parent); assertDocCopyPermission(libraryNode, this.userId); assertDocEditPermission(parentDoc, this.userId); let decendants = LibraryNodes.find({ - 'ancestors.id': _id, + ...getFilter.descendants(libraryNode), removed: { $ne: true }, }, { limit: DUPLICATE_CHILDREN_LIMIT + 1, - sort: { order: 1 }, + sort: { left: 1 }, }).fetch(); if (decendants.length > DUPLICATE_CHILDREN_LIMIT) { @@ -69,28 +72,17 @@ const copyLibraryNodeTo = new ValidatedMethod({ const nodes = [libraryNode, ...decendants]; - const newAncestry = parentDoc.ancestors || []; - newAncestry.push(parent); - // re-map all the ancestors - setLineageOfDocs({ - docArray: nodes, - newAncestry, - oldParent: libraryNode.parent, - }); - // Give the docs new IDs without breaking internal references renewDocIds({ docArray: nodes }); // Order the root node - libraryNode.order = (parentDoc.order || 0) + 0.5; + libraryNode.left = Number.MAX_SAFE_INTEGER - 1; + libraryNode.right = Number.MAX_SAFE_INTEGER; LibraryNodes.batchInsert(nodes); // Tree structure changed by inserts, reorder the tree - reorderDocs({ - collection: LibraryNodes, - ancestorId: parent.collection === 'libraries' ? parent.id : parentDoc.ancestors[0].id, - }); + rebuildNestedSets(LibraryNodes, parentDoc.root.id); }, }); diff --git a/app/imports/api/library/methods/duplicateLibraryNode.js b/app/imports/api/library/methods/duplicateLibraryNode.js index ff35b8fb..ee8116b9 100644 --- a/app/imports/api/library/methods/duplicateLibraryNode.js +++ b/app/imports/api/library/methods/duplicateLibraryNode.js @@ -1,18 +1,19 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method'; import SimpleSchema from 'simpl-schema'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import LibraryNodes from '/imports/api/library/LibraryNodes.js'; -import { assertDocEditPermission } from '/imports/api/sharing/sharingPermissions.js'; +import LibraryNodes from '/imports/api/library/LibraryNodes'; +import { assertDocEditPermission } from '/imports/api/sharing/sharingPermissions'; import { setLineageOfDocs, - renewDocIds -} from '/imports/api/parenting/parenting.js'; -import { reorderDocs } from '/imports/api/parenting/order.js'; + renewDocIds, + getFilter +} from '/imports/api/parenting/parentingFunctions'; +import { rebuildNestedSets } from '/imports/api/parenting/parentingFunctions'; var snackbar; if (Meteor.isClient) { snackbar = require( - '/imports/ui/components/snackbars/SnackbarQueue.js' + '/imports/client/ui/components/snackbars/SnackbarQueue' ).snackbar } @@ -28,11 +29,13 @@ const duplicateLibraryNode = new ValidatedMethod({ }).validator(), mixins: [RateLimiterMixin], rateLimit: { - numRequests: 1, - timeInterval: 5000, + numRequests: 4, + timeInterval: 6000, }, run({ _id }) { let libraryNode = LibraryNodes.findOne(_id); + if (!libraryNode) throw new Meteor.Error('not-found', 'Library node was not found'); + assertDocEditPermission(libraryNode, this.userId); let randomSrc = DDP.randomStream('duplicateLibraryNode'); @@ -40,11 +43,11 @@ const duplicateLibraryNode = new ValidatedMethod({ libraryNode._id = libraryNodeId; let nodes = LibraryNodes.find({ - 'ancestors.id': _id, + ...getFilter.descendants(libraryNode), removed: { $ne: true }, }, { limit: DUPLICATE_CHILDREN_LIMIT + 1, - sort: { order: 1 }, + sort: { left: 1 }, }).fetch(); if (nodes.length > DUPLICATE_CHILDREN_LIMIT) { @@ -56,29 +59,17 @@ const duplicateLibraryNode = new ValidatedMethod({ } } - // re-map all the ancestors - setLineageOfDocs({ - docArray: nodes, - newAncestry: [ - ...libraryNode.ancestors, - { id: libraryNodeId, collection: 'libraryNodes' } - ], - oldParent: { id: _id, collection: 'libraryNodes' }, - }); - // Give the docs new IDs without breaking internal references - renewDocIds({ docArray: nodes }); + const allNodes = [libraryNode, ...nodes]; + renewDocIds({ docArray: allNodes }); // Order the root node libraryNode.order += 0.5; - LibraryNodes.batchInsert([libraryNode, ...nodes]); + LibraryNodes.batchInsert(allNodes); // Tree structure changed by inserts, reorder the tree - reorderDocs({ - collection: LibraryNodes, - ancestorId: libraryNode.ancestors[0].id, - }); + rebuildNestedSets(LibraryNodes, libraryNode.root.id); return libraryNodeId; }, diff --git a/app/imports/api/library/methods/getDefaultSlotFiller.js b/app/imports/api/library/methods/getDefaultSlotFiller.js new file mode 100644 index 00000000..fe8deb0e --- /dev/null +++ b/app/imports/api/library/methods/getDefaultSlotFiller.js @@ -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', + parentId: slot._id, + root: { ...slot.root }, + }; + return filler; +} diff --git a/app/imports/api/library/methods/index.js b/app/imports/api/library/methods/index.js index e67eec8a..ebc207ae 100644 --- a/app/imports/api/library/methods/index.js +++ b/app/imports/api/library/methods/index.js @@ -1,3 +1,3 @@ -import '/imports/api/library/methods/copyLibraryNodeTo.js'; -import '/imports/api/library/methods/duplicateLibraryNode.js'; -import '/imports/api/library/methods/updateReferenceNode.js'; +import '/imports/api/library/methods/copyLibraryNodeTo'; +import '/imports/api/library/methods/duplicateLibraryNode'; +import '/imports/api/library/methods/updateReferenceNode'; diff --git a/app/imports/api/library/methods/updateReferenceNode.js b/app/imports/api/library/methods/updateReferenceNode.js index f933d065..f0303ec0 100644 --- a/app/imports/api/library/methods/updateReferenceNode.js +++ b/app/imports/api/library/methods/updateReferenceNode.js @@ -1,12 +1,12 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method'; import SimpleSchema from 'simpl-schema'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import LibraryNodes from '/imports/api/library/LibraryNodes.js'; +import LibraryNodes from '/imports/api/library/LibraryNodes'; import { assertDocEditPermission, assertViewPermission, -} from '/imports/api/sharing/sharingPermissions.js'; -import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js'; +} from '/imports/api/sharing/sharingPermissions'; +import { fetchDocByRef } from '/imports/api/parenting/parentingFunctions'; const updateReferenceNode = new ValidatedMethod({ name: 'libraryNodes.updateReferenceNode', @@ -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){ - library = fetchDocByRef(doc.ancestors[0]); + if (doc.root.id !== node.root.id) { + library = fetchDocByRef(doc.root); 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); } diff --git a/app/imports/api/parenting/ChildSchema.js b/app/imports/api/parenting/ChildSchema.js deleted file mode 100644 index f4787bcc..00000000 --- a/app/imports/api/parenting/ChildSchema.js +++ /dev/null @@ -1,40 +0,0 @@ -import SimpleSchema from 'simpl-schema'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; - -const RefSchema = new SimpleSchema({ - id: { - type: String, - regEx: SimpleSchema.RegEx.Id, - // TODO: Rather than indexing this field, index `ancestors.0.id` to only - // index the root of the ancestor heirarchy to significantly reduce - // index size and improve performance - // All queries on an ancestor document need to target `ancestors.0.id` first - // before targeting a younger ancestor - index: 1 - }, - collection: { - type: String, - max: STORAGE_LIMITS.collectionName, - }, -}); - -let ChildSchema = new SimpleSchema({ - order: { - type: Number, - }, - parent: { - type: RefSchema, - optional: true, - }, - ancestors: { - type: Array, - defaultValue: [], - maxCount: STORAGE_LIMITS.ancestorCount, - }, - 'ancestors.$': { - type: RefSchema, - }, -}); - -export default ChildSchema; -export { RefSchema }; diff --git a/app/imports/api/parenting/ChildSchema.ts b/app/imports/api/parenting/ChildSchema.ts new file mode 100644 index 00000000..6c1d4e56 --- /dev/null +++ b/app/imports/api/parenting/ChildSchema.ts @@ -0,0 +1,87 @@ +import SimpleSchema from 'simpl-schema'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; + +export interface Reference { + collection: string, + id: string, +} + +export interface TreeDoc { + _id: string, + root: Reference, + parentId?: string, + left: number, + right: number, + removed?: true, +} + +const RefSchema = new SimpleSchema({ + id: { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, + collection: { + type: String, + max: STORAGE_LIMITS.collectionName, + }, +}); + +const ChildSchema = new SimpleSchema({ + root: { + type: Object, + }, + 'root.id': { + type: String, + regEx: SimpleSchema.RegEx.Id, + index: 1, + }, + 'root.collection': { + type: String, + max: STORAGE_LIMITS.collectionName, + }, + // Parent id of a document in the same collection + // Undefined parent id implies the root is the parent + parentId: { + type: String, + regEx: SimpleSchema.RegEx.Id, + optional: true, + }, + /** + * The tree structure goes as follows where the numbering follows a counterclockwise depth first + * path around the tree. The canonical structure comes from the root and parentId references, + * while the left and right numbering is used to optimize ancestor queries. + * + * Left can be used as the canonical ordering of properties in an expanded tree folder view. + * + * 1 Books 12 + * ┃ + * 2 Programming 11 + * ┏━━━━━━━━┻━━━━━━━━━┓ + * 3 Languages 4 5 Databases 10 + * ┏━━━━━━━┻━━━━━━━┓ + * 6 MongoDB 7 8 dbm 9 + */ + left: { + type: Number, + index: 1, + // Default to absolutely last with space for right + defaultValue: Number.MAX_SAFE_INTEGER - 1, + }, + right: { + type: Number, + index: 1, + // Default to zero children, so right = left + 1 + defaultValue: Number.MAX_SAFE_INTEGER, + } +}); + +export const treeDocFields = { + _id: 1, + root: 1, + parentId: 1, + left: 1, + right: 1, +} + +export default ChildSchema; +export { RefSchema }; diff --git a/app/imports/api/parenting/fetchDocByRef.js b/app/imports/api/parenting/fetchDocByRef.js deleted file mode 100644 index dfa485dc..00000000 --- a/app/imports/api/parenting/fetchDocByRef.js +++ /dev/null @@ -1,12 +0,0 @@ -import getCollectionByName from '/imports/api/parenting/getCollectionByName.js'; - -const docNotFoundError = function({id, collection}){ - throw new Meteor.Error('document-not-found', - `No document could be found with id: ${id} in ${collection}` - ); -}; - -export default function fetchDocByRef({id, collection}, options){ - return getCollectionByName(collection).findOne(id, options) || - docNotFoundError({id, collection}); -} diff --git a/app/imports/api/parenting/getCollectionByName.js b/app/imports/api/parenting/getCollectionByName.js deleted file mode 100644 index bc9fe4e0..00000000 --- a/app/imports/api/parenting/getCollectionByName.js +++ /dev/null @@ -1,11 +0,0 @@ -const collectionDoesntExistError = function(collectionName){ - throw new Meteor.Error('bad-collection-reference', - `Parent references collection ${collectionName}, which does not exist` - ); -}; - -const getCollectionByName = function(name){ - return Mongo.Collection.get(name) || collectionDoesntExistError(name); -}; - -export default getCollectionByName; diff --git a/app/imports/api/parenting/getDescendantsInDepthFirstOrder.js b/app/imports/api/parenting/getDescendantsInDepthFirstOrder.js deleted file mode 100644 index 9010d51b..00000000 --- a/app/imports/api/parenting/getDescendantsInDepthFirstOrder.js +++ /dev/null @@ -1,27 +0,0 @@ -import nodesToTree from '/imports/api/parenting/nodesToTree.js'; - -export default function getDescendantsInDepthFirstOrder({ - collection, - ancestorId, - filter, - options = {fields: {order: 1, ancestors: 1}}, -}){ - let forest = nodesToTree({collection, ancestorId, filter, options}); - let orderMemo = getDocsInDepthFirstOrder(forest); - return orderMemo; -} - -export function getDocsInDepthFirstOrder(forest){ - let docs = []; - forest.forEach(node => { - addNodeAndTraverse(node, docs) - }); - return docs; -} - -function addNodeAndTraverse(node, docs){ - docs.push(node.node); - node.children.forEach(child => { - addNodeAndTraverse(child, docs) - }); -} diff --git a/app/imports/api/parenting/nodesToTree.js b/app/imports/api/parenting/nodesToTree.js deleted file mode 100644 index b8589967..00000000 --- a/app/imports/api/parenting/nodesToTree.js +++ /dev/null @@ -1,115 +0,0 @@ -import { union, difference, sortBy, findLast } from 'lodash'; - -export function nodeArrayToTree(nodes){ - // Store a dict and list of all the nodes - let nodeIndex = {}; - let nodeList = []; - nodes.forEach( node => { - let treeNode = { - node: node, - children: [], - }; - nodeIndex[node._id] = treeNode; - nodeList.push(treeNode); - }); - // Create a forest of trees - let forest = []; - // Either the node is a child of its nearest found ancestor, or in the forest as a root - nodeList.forEach(treeNode => { - let ancestorInForest = findLast( - treeNode.node.ancestors, - ancestor => !!nodeIndex[ancestor.id] - ); - if (ancestorInForest){ - nodeIndex[ancestorInForest.id].children.push(treeNode); - } else { - forest.push(treeNode); - } - }); - return forest; -} - -// Fetch the documents from a collection, and return the tree of those documents -export default function nodesToTree({ - collection, ancestorId, filter, options = {}, - includeFilteredDocAncestors = false, includeFilteredDocDescendants = false -}){ - // Setup the filter - let collectionFilter = { - 'ancestors.id': ancestorId, - 'removed': {$ne: true}, - }; - if (filter){ - collectionFilter = { - ...collectionFilter, - ...filter, - } - } - // Set up the options - let collectionSort = { - order: 1 - }; - if (options && options.sort){ - collectionSort = { - ...collectionSort, - ...options.sort, - } - } - let collectionOptions = { - sort: collectionSort, - } - if (options){ - collectionOptions = { - ...collectionOptions, - ...options, - } - } - // Find all the nodes that match the filter - let docs = collection.find(collectionFilter, collectionOptions).map(doc => { - if (!filter) return doc; - // Mark the nodes that were found by the custom filter - doc._matchedDocumentFilter = true; - return doc; - }); - let ancestors = []; - let ancestorIds = []; - let docIds = []; - if (filter && (includeFilteredDocAncestors || includeFilteredDocDescendants)){ - docIds = docs.map(doc => doc._id) - } - if (filter && includeFilteredDocAncestors){ - // Add all ancestor ids to an array - docs.forEach(doc => { - ancestorIds = union(ancestorIds, doc.ancestors.map(ref => ref.id)); - }); - // Remove the IDs of docs we have already found - ancestorIds = difference(ancestorIds, docIds); - // Get the docs from the collection, don't worry about `removed` docs, - // if their descendant was not removed, neither are they - ancestors = collection.find({_id: {$in: ancestorIds}}).map(doc => { - // Mark that the nodes are ancestors of the found nodes - doc._ancestorOfMatchedDocument = true; - return doc; - }); - } - let descendants = []; - if (filter && includeFilteredDocDescendants){ - let exludeIds = union(ancestorIds, docIds); - descendants = collection.find({ - '_id': {$nin: exludeIds}, - 'ancestors.id': {$in: docIds}, - 'removed': {$ne: true}, - }).map(doc => { - // Mark that the nodes are descendants of the found nodes - doc._descendantOfMatchedDocument = true; - return doc; - }); - } - let nodes = sortBy([ - ...ancestors, - ...docs, - ...descendants - ], 'order'); - // Find all the nodes - return nodeArrayToTree(nodes); -} diff --git a/app/imports/api/parenting/order.js b/app/imports/api/parenting/order.js deleted file mode 100644 index c94e814b..00000000 --- a/app/imports/api/parenting/order.js +++ /dev/null @@ -1,156 +0,0 @@ -import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js'; -import getCollectionByName from '/imports/api/parenting/getCollectionByName.js'; -import getDescendantsInDepthFirstOrder from '/imports/api/parenting/getDescendantsInDepthFirstOrder.js' - -// Docs keep track of their depth-first order amongst their entire ancestor tree -export function compareOrder(docA, docB){ - // < 0 if A comes before B - // = 0 if A and B are the same order - // > 0 if B comes before A - - // They must share a root ancestor to be meaningfully sorted - if (docA.ancestors[0].id !== docB.ancestors[0].id){ - return 0; - } else { - return docA.order - docB.order; - } -} - -export function getHighestOrder({collection, ancestorId}){ - const highestOrderedDoc = collection.findOne({ - 'ancestors.id': ancestorId, - }, { - fields: {order: 1}, - sort: {order: -1}, - }); - return highestOrderedDoc ? highestOrderedDoc.order : -1; -} - -export function setDocToLastOrder({collection, doc}){ - doc.order = getHighestOrder({ - collection, - ancestorId: doc.ancestors[0].id, - }) + 1; -} - -// update the order of a doc, and shift the related docs around to suit the new -// order -function cheapUpdateDocOrder({docRef, order}){ - let doc = fetchDocByRef(docRef, {fields: { - order: 1, - parent: 1, - }}); - let collection = getCollectionByName(docRef.collection); - const currentOrder = doc.order; - if (currentOrder === order){ - return; - } else { - // First move the documents that are in the way - let inBetweenSelector, increment; - if (order > currentOrder){ - // Move in-between docs backward - inBetweenSelector = { - $gt: currentOrder, - $lte: order - }; - increment = -1; - } else if (order < currentOrder){ - // Move in-between docs forward - inBetweenSelector = { - $lt: currentOrder, - $gte: order - }; - increment = 1; - } - collection.update({ - 'ancestors.id': doc.ancestors[0].id, - order: inBetweenSelector, - }, { - $inc: {order: increment}, - }, { - multi: true, - selector: {type: 'any'}, - }); - // Then move the document itself - collection.update(doc._id, {$set: {order}}, {selector: {type: 'any'}}); - } -} - -export function cheapRemovedDocAtOrder({collection, doc}){ - // Decrement the order of all docs after the removed doc - collection.update({ - 'ancestors.id': doc.ancestors[0].id, - order: {$gt: doc.order}, - }, { - $inc: {order: -1}, - }, { - multi: true, - selector: {type: 'any'}, - }); -} - -export function cheapInsertedDocAtOrder({collection, ancestorId, order}){ - // Increment the order of all docs after the inserted doc - collection.update({ - 'ancestors.id': ancestorId, - order: {$gte: order}, - }, { - $inc: {order: 1}, - }, { - multi: true, - selector: {type: 'any'}, - }); -} - -// Update the order a single doc and re-order the entire related doc list -// with the change -export function safeUpdateDocOrder({docRef, order}){ - let collection = getCollectionByName(docRef.collection); - // Put the new doc half a step in front of its new order - // to ensure it's in front of whichever doc was there before - collection.update(docRef.id, { - $set: {order} - }, { - selector: {type: 'any'} - }); - // reorder all related docs so that order is back to being a continous - // set of whole numbers - let movedDoc = fetchDocByRef(docRef, {fields: {ancestors: 1}}); - let ancestorId = movedDoc.ancestors[0].id; - reorderDocs({collection, ancestorId}); -} - -export function reorderDocs({collection, ancestorId}){ - let orderedDocs = getDescendantsInDepthFirstOrder({collection, ancestorId}); - let bulkWrite = []; - orderedDocs.forEach((doc, index) => { - if (doc.order !== index){ - bulkWrite.push({ - updateOne : { - filter: {_id: doc._id}, - update: {$set: {order: index}}, - }, - }); - } - }); - if (Meteor.isServer && bulkWrite.length){ - collection.rawCollection().bulkWrite( - bulkWrite, - {ordered : false}, - function(e){ - if (e) { - console.error('Bulk write failed: '); - console.error(e); - } - } - ); - } else { - bulkWrite.forEach(op => { - collection.update( - op.updateOne.filter, - op.updateOne.update, - {selector: {type: 'any'}} - ); - }); - } -} diff --git a/app/imports/api/parenting/organizeMethods.js b/app/imports/api/parenting/organizeMethods.js index 88500085..31d93fce 100644 --- a/app/imports/api/parenting/organizeMethods.js +++ b/app/imports/api/parenting/organizeMethods.js @@ -1,65 +1,60 @@ import SimpleSchema from 'simpl-schema'; -import { union } from 'lodash'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import { updateParent } from '/imports/api/parenting/parenting.js'; -import { reorderDocs, safeUpdateDocOrder } from '/imports/api/parenting/order.js'; -import { RefSchema } from '/imports/api/parenting/ChildSchema.js'; -import { assertDocEditPermission } from '/imports/api/sharing/sharingPermissions.js'; -import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js'; -import getCollectionByName from '/imports/api/parenting/getCollectionByName.js'; -import Creatures from '/imports/api/creature/creatures/Creatures.js'; +import { RefSchema } from '/imports/api/parenting/ChildSchema'; +import { assertDocEditPermission, assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; +import { compact } from 'lodash'; +import Creatures from '/imports/api/creature/creatures/Creatures'; +import { fetchDocByRefAsync, getCollectionByName, moveDocBetweenRoots, moveDocWithinRoot } from '/imports/api/parenting/parentingFunctions'; -const organizeDoc = new ValidatedMethod({ - name: 'organize.organizeDoc', +const moveBetweenRoots = new ValidatedMethod({ + name: 'organize.moveDocBetweenRoots', validate: new SimpleSchema({ docRef: RefSchema, - parentRef: RefSchema, - order: { - type: Number, - // Should end in 0.5 to place it reliably between two existing documents + newPosition: { + type: Number, // Must end in .5 }, + newRootRef: RefSchema, skipRecompute: { type: Boolean, optional: true, }, + skipClient: { + type: Boolean, + optional: true, + }, }).validator(), mixins: [RateLimiterMixin], rateLimit: { numRequests: 5, timeInterval: 5000, }, - run({docRef, parentRef, order, skipRecompute}) { - let doc = fetchDocByRef(docRef); + async run({ docRef, newPosition, newRootRef, skipRecompute, skipClient }) { + if (skipClient && this.isSimulation) { + return; + } + let doc = await fetchDocByRefAsync(docRef); let collection = getCollectionByName(docRef.collection); // The user must be able to edit both the doc and its parent to move it // successfully assertDocEditPermission(doc, this.userId); - let parent = fetchDocByRef(parentRef); - assertDocEditPermission(parent, this.userId); + const newRoot = await fetchDocByRefAsync(newRootRef); + assertEditPermission(newRoot, this.userId); - // Change the doc's parent - 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'}}); - // Reorder both ancestors' documents - let oldAncestorId = doc.ancestors[0].id; - reorderDocs({collection, ancestorId: oldAncestorId}); - - let newAncestorId = getRootId(parent); - if (newAncestorId !== oldAncestorId){ - reorderDocs({collection, ancestorId: newAncestorId}); - } + // Move the doc + await moveDocBetweenRoots(doc, collection, newRootRef, newPosition); // Figure out which creatures need to be recalculated after this move - let docCreatures = getCreatureAncestors(doc); - let parentCreatures = getCreatureAncestors(parent); - if (!skipRecompute){ - let creaturesToRecompute = union(docCreatures, parentCreatures); - // Mark the creatures for recompute - Creatures.update({ - _id: { $in: creaturesToRecompute } + const creatureIdsToRecalculate = compact([ + getCreatureAncestorId(doc), + getCreatureAncestorId(newRoot), + ]); + + // Mark the creatures for recompute + if (!skipRecompute && creatureIdsToRecalculate.length) { + Creatures.updateAsync({ + _id: { $in: creatureIdsToRecalculate }, }, { $set: { dirty: true }, }); @@ -67,13 +62,20 @@ const organizeDoc = new ValidatedMethod({ }, }); -const reorderDoc = new ValidatedMethod({ - name: 'organize.reorderDoc', +const moveWithinRoot = new ValidatedMethod({ + name: 'organize.moveDocWithinRoot', validate: new SimpleSchema({ docRef: RefSchema, - order: { - type: Number, - // Should end in 0.5 to place it reliably between two existing documents + newPosition: { + type: Number, // Must end in .5 + }, + skipRecompute: { + type: Boolean, + optional: true, + }, + skipClient: { + type: Boolean, + optional: true, }, }).validator(), mixins: [RateLimiterMixin], @@ -81,15 +83,26 @@ const reorderDoc = new ValidatedMethod({ numRequests: 5, timeInterval: 5000, }, - run({docRef, order}) { - let doc = fetchDocByRef(docRef); + async run({ docRef, newPosition, skipRecompute, skipClient }) { + if (skipClient && this.isSimulation) { + return; + } + let doc = await fetchDocByRefAsync(docRef); + let collection = getCollectionByName(docRef.collection); + + // The user must be able to edit the doc assertDocEditPermission(doc, this.userId); - safeUpdateDocOrder({docRef, order}); - // Recompute the affected creatures - const ancestors = getCreatureAncestors(doc); - if (ancestors.length) { - Creatures.update({ - _id: { $in: ancestors } + + // Move the doc + await moveDocWithinRoot(doc, collection, newPosition); + + // Figure out which creature needs to be recalculated after this move + const creatureIdToRecalculate = getCreatureAncestorId(doc); + + // Mark the creatures for recompute + if (!skipRecompute && creatureIdToRecalculate) { + Creatures.updateAsync({ + _id: creatureIdToRecalculate, }, { $set: { dirty: true }, }); @@ -97,27 +110,13 @@ const reorderDoc = new ValidatedMethod({ }, }); -function getRootId(doc){ - if (doc.ancestors && doc.ancestors.length && doc.ancestors[0]){ - return doc.ancestors[0].id; - } else { +function getCreatureAncestorId(doc) { + if (doc.type === 'pc' || doc.type === 'npc' || doc.type === 'monster') { return doc._id; } + if (doc?.root?.collection === 'creatures') { + return doc.root.id; + } } -function getCreatureAncestors(doc){ - let ids = []; - if(doc.type === 'pc' || doc.type === 'npc' || doc.type === 'monster'){ - ids.push(doc._id); - } - if (doc.ancestors){ - doc.ancestors.forEach(ancestorRef => { - if (ancestorRef.collection === 'creatures'){ - ids.push(ancestorRef.id); - } - }); - } - return ids; -} - -export { organizeDoc, reorderDoc }; +export { moveBetweenRoots, moveWithinRoot }; diff --git a/app/imports/api/parenting/parenting.js b/app/imports/api/parenting/parenting.js deleted file mode 100644 index e5a8cc62..00000000 --- a/app/imports/api/parenting/parenting.js +++ /dev/null @@ -1,217 +0,0 @@ -import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js'; -import getCollectionByName from '/imports/api/parenting/getCollectionByName.js'; -import { flatten } from 'lodash'; - -const generalParents = [ - 'attribute', - 'buff', - 'classLevel', - 'feature', - 'folder', - 'root', - 'item', - 'spell', -]; - -// Which types are allowed as parents for other types -const allowedParenting = { - folder: [...generalParents, 'container'], - rollResult: ['roll', 'rollResult'], - container: ['root', 'folder'], - item: ['root', 'container', 'folder'], -}; - -const allParentTypes = new Set(flatten(Object.values(allowedParenting))); - -export function canBeParent(type){ - return true; - //TODO until there is a good reason to disallow certain parenting options, - // this should just let the user do whatever - return type && allParentTypes.has(type); -} - -export function getAllowedParents({childType}){ - return allowedParenting[childType] || generalParents; -} - -export function isParentAllowed({parentType = 'root', childType}){ - return true; - //TODO until there is a good reason to disallow certain parenting options, - // this should just let the user do whatever - if (!childType) throw 'childType is required'; - let allowedParents = getAllowedParents({childType}); - return allowedParents.includes(parentType); -} - -export function fetchParent({id, collection}){ - return fetchDocByRef({id, collection}); -} - -export function fetchChildren({ collection, parentId, filter = {}, options = {sort: {order: 1}} }){ - filter['parent.id'] = parentId; - let children = []; - children.push( - ...collection.find({ - 'parent.id': parentId - }, options).fetch() - ); - return children; -} - -export function updateChildren({collection, parentId, filter = {}, modifier, options={}}){ - filter['parent.id'] = parentId; - options.multi = true; - collection.update(filter, modifier, options); -} - -export function fetchDescendants({ collection, ancestorId, filter = {}, options}){ - filter['ancestors.id'] = ancestorId; - let descendants = []; - descendants.push(...collection.find(filter, options).fetch()); - return descendants; -} - -export function updateDescendants({collection, ancestorId, filter = {}, modifier, options={}}){ - filter['ancestors.id'] = ancestorId; - options.multi = true; - options.selector = {type: 'any'}; - collection.update(filter, modifier, options); -} - -export function forEachDescendant({collection, ancestorId, filter = {}, options}, callback){ - filter['ancestors.id'] = ancestorId; - collection.find(filter, options).forEach(callback); -} - -// 1 database read -export function getAncestry({parentRef, inheritedFields = {}}){ - let parentDoc = fetchDocByRef(parentRef, {fields: inheritedFields}); - let parent = { ...parentRef}; - for (let field in inheritedFields){ - if (inheritedFields[field]){ - parent[field] = parentDoc[field]; - } - } - - // Ancestors is [...parent's ancestors, parent ref] - let ancestors = parentDoc.ancestors || []; - ancestors.push(parent); - - return {parentDoc, parent, ancestors}; -} - -export function setLineageOfDocs({docArray, oldParent, newAncestry}){ - const newParent = newAncestry[newAncestry.length - 1]; - docArray.forEach(doc => { - if(doc.parent.id === oldParent.id){ - doc.parent = newParent; - } - let oldAncestors = doc.ancestors; - let oldParentIndex = oldAncestors.findIndex(a => a.id === oldParent.id); - if (oldParentIndex === -1) return; - doc.ancestors = [...newAncestry, ...oldAncestors.slice(oldParentIndex + 1)]; - }); -} - -/** - * Give documents new random ids and transform their references. - * Transform collections of re-IDed docs according to the collection map - */ -export function renewDocIds({docArray, collectionMap, idMap = {}}){ - // idMap is a map of {oldId: newId} - // Get a random generator that's consistent on client and server - let randomSrc = DDP.randomStream('renewDocIds'); - - // Give new ids and map the changes as {oldId: newId} - docArray.forEach(doc => { - let oldId = doc._id; - let newId = idMap[oldId] || randomSrc.id(); - doc._id = newId; - idMap[oldId] = newId; - }); - - // Remap all references using the new IDs - const remapReference = ref => { - if (idMap[ref.id]){ - ref.id = idMap[ref.id]; - ref.collection = collectionMap && collectionMap[ref.collection] || ref.collection; - } - } - docArray.forEach(doc => { - remapReference(doc.parent); - doc.ancestors.forEach(remapReference); - }); -} - -export function updateParent({docRef, parentRef}){ - let collection = getCollectionByName(docRef.collection); - let oldDoc = fetchDocByRef(docRef, {fields: { - parent: 1, - ancestors: 1, - type: 1, - }}); - let updateOptions = { selector: {type: 'any'} }; - - // Skip if we aren't changing the parent id - if (oldDoc.parent.id === parentRef.id) return; - - // Get the parent and its ancestry - let {parentDoc, parent, ancestors} = getAncestry({parentRef}); - - // Check that the doc isn't its own ancestor - ancestors.forEach(ancestor => { - if (docRef.id === ancestor.id){ - throw new Meteor.Error('invalid parenting', - 'A doc can\'t be its own ancestor') - } - }); - - // If the doc and its parent are in the same collection, apply the allowed - // parent rules based on type - if (docRef.collection === parentRef.collection){ - let parentAllowed = isParentAllowed({ - parentType: parentDoc.type, - childType: oldDoc.type - }); - if (!parentAllowed){ - throw new Meteor.Error('invalid parenting', - `Can't make ${oldDoc.type} a child of ${parentDoc.type}`) - } - } - - // update the document's parenting - collection.update(docRef.id, { - $set: {parent, ancestors} - }, updateOptions); - - // Remove the old ancestors from the descendants - updateDescendants({ - collection, - ancestorId: docRef.id, - modifier: {$pullAll: { - ancestors: oldDoc.ancestors, - }}, - options: updateOptions, - }); - - // Add the new ancestors to the descendants - updateDescendants({ - collection, - ancestorId: docRef.id, - modifier: {$push: { - ancestors: { - $each: ancestors, - $position: 0, - }, - }}, - options: updateOptions, - }); -} - -export function getName(doc){ - if (doc.name) return name; - var i = doc.ancestors.length; - while(i--) { - if (doc.ancestors[i].name) return doc.ancestors[i].name; - } -} diff --git a/app/imports/api/parenting/parentingFunctions.test.ts b/app/imports/api/parenting/parentingFunctions.test.ts new file mode 100644 index 00000000..eb5e533b --- /dev/null +++ b/app/imports/api/parenting/parentingFunctions.test.ts @@ -0,0 +1,395 @@ +import '/imports/api/simpleSchemaConfig'; +import { docsToForest, calculateNestedSetOperations, getFilter, moveDocWithinRoot, moveDocBetweenRoots } from '/imports/api/parenting/parentingFunctions' +import { TreeDoc } from '/imports/api/parenting/ChildSchema'; +import { assert } from 'chai'; + +function doc(_id, left, right, parentId?): TreeDoc { + const doc = { _id, root: { id: 'root', collection: 'col' }, left, right, parentId }; + if (!parentId) delete doc.parentId; + return doc; +} + +function op(_id, left, right) { + return { + updateOne: { + filter: { _id }, + update: { $set: { left, right } }, + }, + }; +} + +describe('Parenting with nested sets', function () { + /** + * Test the following structure + * + * 1 Books 12 + * ┃ + * 2 Programming 11 + * ┏━━━━━━━━┻━━━━━━━━━┓ + * 3 Languages 4 5 Databases 10 + * ┏━━━━━━━┻━━━━━━━┓ + * 6 MongoDB 7 8 dbm 9 + */ + it('Takes a set of documents and builds the forest', function () { + const docArray: Array = [ + doc('Books', 1, 12), + doc('Programming', 2, 11), + doc('Languages', 3, 4), + doc('Databases', 5, 10), + doc('MongoDB', 6, 7), + doc('dbm', 8, 9), + ]; + const forest = docsToForest(docArray); + assert.deepEqual(forest, [ + { + doc: doc('Books', 1, 12), children: [ + { + doc: doc('Programming', 2, 11), children: [ + { doc: doc('Languages', 3, 4), children: [] }, + { + doc: doc('Databases', 5, 10), children: [ + { doc: doc('MongoDB', 6, 7), children: [] }, + { doc: doc('dbm', 8, 9), children: [] }, + ] + } + ] + } + ] + } + ]); + }); + it('Can recalculate left and right for docs with set parents', function () { + const docArray = [ + doc('Books', 71, 33, undefined), + doc('Programming', 72, 33, 'Books'), + doc('Languages', 73, 33, 'Programming'), + doc('Databases', 74, 33, 'Programming'), + doc('MongoDB', 75, 33, 'Databases'), + doc('dbm', 76, 33, 'Databases'), + ]; + const ops = calculateNestedSetOperations(docArray); + assert.deepEqual(ops, [ + op('Books', 1, 12), + op('Programming', 2, 11), + op('Languages', 3, 4), + op('Databases', 5, 10), + op('MongoDB', 6, 7), + op('dbm', 8, 9), + ]); + }); + it('Can recalculate left and right for docs with set parents in random order', function () { + const docArray = [ + doc('MongoDB', 71, 33, 'Databases'), + doc('Programming', 72, 33, 'Books'), + doc('Languages', 73, 33, 'Programming'), + doc('Books', 74, 33, undefined), + doc('Databases', 75, 33, 'Programming'), + doc('dbm', 76, 33, 'Databases'), + ]; + const ops = calculateNestedSetOperations(docArray); + assert.deepEqual(ops, [ + op('Books', 1, 12), + op('Programming', 2, 11), + op('Languages', 3, 4), + op('Databases', 5, 10), + op('MongoDB', 6, 7), + op('dbm', 8, 9), + ]); + }); +}); + +describe('Document tree filters can fetch other documents based on their position in the tree', function () { + // Add the docs to a new collection + /** + * Test the following structure + * + * 1 Books 12 + * ┃ + * 2 Programming 11 + * ┏━━━━━━━━┻━━━━━━━━━┓ + * 3 Languages 4 5 Databases 10 + * ┏━━━━━━━┻━━━━━━━┓ + * 6 MongoDB 7 8 dbm 9 + */ + const treeCollection: Mongo.Collection = new Mongo.Collection('treeDocs'); + treeCollection.remove({}); + [ + doc('Books', 1, 12, undefined), + doc('Programming', 2, 11, 'Books'), + doc('Languages', 3, 4, 'Programming'), + doc('Databases', 5, 10, 'Programming'), + doc('MongoDB', 6, 7, 'Databases'), + doc('dbm', 8, 9, 'Databases'), + ].map(doc => { + return treeCollection.insert(doc); + }); + const docs: TreeDoc[] = treeCollection.find({}).fetch(); + + it('Can filter ancestors', async function () { + const ancestorIds: { [id: string]: string[] } = {}; + docs.forEach(doc => { + ancestorIds[doc._id] = treeCollection.find( + getFilter.ancestors(doc) + ).map(doc => doc._id); + }); + assert.isEmpty(ancestorIds['Books'], 'Books has no ancestors'); + assert.sameMembers(ancestorIds['Programming'], ['Books']); + assert.sameMembers(ancestorIds['Languages'], ['Books', 'Programming']); + assert.sameMembers(ancestorIds['Databases'], ['Books', 'Programming']); + assert.sameMembers(ancestorIds['MongoDB'], ['Books', 'Programming', 'Databases']); + assert.sameMembers(ancestorIds['dbm'], ['Books', 'Programming', 'Databases']); + }); + + it('Can filter descendants', async function () { + const descendantIds: { [id: string]: string[] } = {}; + docs.forEach(doc => { + descendantIds[doc._id] = treeCollection.find( + getFilter.descendants(doc) + ).map(doc => doc._id); + }); + assert.isEmpty(descendantIds['MongoDB'], 'MongoDB has no descendants'); + assert.isEmpty(descendantIds['dbm'], 'dbm has no descendants'); + assert.isEmpty(descendantIds['Languages'], 'Languages has no descendants'); + assert.sameMembers(descendantIds['Databases'], ['dbm', 'MongoDB']); + assert.sameMembers(descendantIds['Programming'], ['dbm', 'MongoDB', 'Languages', 'Databases']); + assert.sameMembers(descendantIds['Books'], [ + 'dbm', 'MongoDB', 'Languages', 'Databases', 'Programming' + ]); + }); + + it('Can filter children', async function () { + const childrenIds: { [id: string]: string[] } = {}; + docs.forEach(doc => { + childrenIds[doc._id] = treeCollection.find( + getFilter.children(doc) + ).map(doc => doc._id); + }); + assert.sameMembers(childrenIds['Books'], ['Programming']); + assert.sameMembers(childrenIds['Programming'], ['Languages', 'Databases']); + assert.isEmpty(childrenIds['Languages'], 'Languages has no children'); + assert.sameMembers(childrenIds['Databases'], ['dbm', 'MongoDB']); + assert.isEmpty(childrenIds['MongoDB'], 'MongoDB has no children'); + assert.isEmpty(childrenIds['dbm'], 'dbm has no children'); + }); + + it('Can filter parents', async function () { + const parentIds: { [id: string]: string[] } = {}; + docs.forEach(doc => { + parentIds[doc._id] = treeCollection.find( + getFilter.parent(doc) + ).map(doc => doc._id); + }); + assert.isEmpty(parentIds['Books'], 'Books has no parent'); + assert.sameMembers(parentIds['Programming'], ['Books']); + assert.sameMembers(parentIds['Languages'], ['Programming']); + assert.sameMembers(parentIds['Databases'], ['Programming']); + assert.sameMembers(parentIds['MongoDB'], ['Databases']); + assert.sameMembers(parentIds['dbm'], ['Databases']); + }); +}); + +describe('Document can be moved withing root without breaking the tree', function () { + /** + * Test the following structure + * + * 1 Books 12 13 Videos 24 + * ┃ ┃ + * 2 Programming 11 14 Cooking 23 + * ┏━━━━━━━━┻━━━━━━━━━┓ ┏━━━━━━━━┻━━━━━━━━━┓ + * 3 Languages 4 5 Databases 10 15 Meat 16 17 Vegetarian 22 + * ┏━━━━━━━┻━━━━━━━┓ ┏━━━━━━━┻━━━━━━━┓ + * 6 MongoDB 7 8 dbm 9 18 Pasta 19 20 Mains 21 + */ + const treeCollection: Mongo.Collection = new Mongo.Collection('treeDocsMove'); + beforeEach(function () { + treeCollection.remove({}); + [ + doc('Books', 1, 12, undefined), + doc('Programming', 2, 11, 'Books'), + doc('Languages', 3, 4, 'Programming'), + doc('Databases', 5, 10, 'Programming'), + doc('MongoDB', 6, 7, 'Databases'), + doc('dbm', 8, 9, 'Databases'), + doc('Videos', 13, 24, undefined), + doc('Cooking', 14, 23, 'Videos'), + doc('Meat', 15, 16, 'Cooking'), + doc('Vegetarian', 17, 22, 'Cooking'), + doc('Pasta', 18, 19, 'Vegetarian'), + doc('Mains', 20, 21, 'Vegetarian'), + ].map(doc => { + return treeCollection.insert(doc); + }); + }); + it('can move a document within its parent', async function () { + const languagesDoc = await treeCollection.findOneAsync({ _id: 'Languages' }); + if (!languagesDoc) throw new Error('Languages doc not found'); + await moveDocWithinRoot(languagesDoc, treeCollection, 10.5); + /** + * Expected resulting structure + * 1 Books 12 13 Videos 24 + * ┃ ┃ + * 2 Programming 11 14 Cooking 23 + * ┏━━━━━━━━┻━━━━━━━━━┓ ┏━━━━━━━━┻━━━━━━━━━┓ + * 3 Databases 8 9 Languages 10 15 Meat 16 17 Vegetarian 22 + * ┏━━━━━━━┻━━━━━━━┓ ┏━━━━━━━┻━━━━━━━┓ + * 4 MongoDB 5 6 dbm 7 18 Pasta 19 20 Mains 21 + */ + const docs = await treeCollection.find({}, { sort: { left: 1 } }).fetchAsync(); + assert.deepEqual(docs, [ + doc('Books', 1, 12, undefined), + doc('Programming', 2, 11, 'Books'), + doc('Databases', 3, 8, 'Programming'), + doc('MongoDB', 4, 5, 'Databases'), + doc('dbm', 6, 7, 'Databases'), + doc('Languages', 9, 10, 'Programming'), + doc('Videos', 13, 24, undefined), + doc('Cooking', 14, 23, 'Videos'), + doc('Meat', 15, 16, 'Cooking'), + doc('Vegetarian', 17, 22, 'Cooking'), + doc('Pasta', 18, 19, 'Vegetarian'), + doc('Mains', 20, 21, 'Vegetarian'), + ]); + }); + it('can move a document within its parent to the start of the tree', async function () { + const videosDoc = await treeCollection.findOneAsync({ _id: 'Videos' }); + if (!videosDoc) throw new Error('Languages doc not found'); + await moveDocWithinRoot(videosDoc, treeCollection, 0.5); + /** + * Expected resulting structure + * + * 1 Videos 12 13 Books 24 + * ┃ ┃ + * 2 Cooking 11 14 Programming 23 + * ┏━━━━━━━━┻━━━━━━━━━┓ ┏━━━━━━━━┻━━━━━━━━━┓ + * 3 Meat 4 5 Vegetarian 10 15 Languages 16 17 Databases 22 + * ┏━━━━━━━┻━━━━━━━┓ ┏━━━━━━━┻━━━━━━━┓ + * 6 Pasta 7 8 Mains 9 18 MongoDB 19 20 dbm 21 + **/ + const docs = await treeCollection.find({}, { sort: { left: 1 } }).fetchAsync(); + assert.deepEqual(docs, [ + doc('Videos', 1, 12, undefined), + doc('Cooking', 2, 11, 'Videos'), + doc('Meat', 3, 4, 'Cooking'), + doc('Vegetarian', 5, 10, 'Cooking'), + doc('Pasta', 6, 7, 'Vegetarian'), + doc('Mains', 8, 9, 'Vegetarian'), + doc('Books', 13, 24, undefined), + doc('Programming', 14, 23, 'Books'), + doc('Languages', 15, 16, 'Programming'), + doc('Databases', 17, 22, 'Programming'), + doc('MongoDB', 18, 19, 'Databases'), + doc('dbm', 20, 21, 'Databases'), + ]); + }); + it('can move a document to a whole new parent', async function () { + const videos = await treeCollection.findOneAsync({ _id: 'Videos' }); + if (!videos) throw new Error('Videos doc not found'); + await moveDocWithinRoot(videos, treeCollection, 6.5); + /** + * Expected resulting structure + * 1 Books 24 + * ┃ + * 2 Programming 23 + * ┏━━━━━━━━┻━━━━━━━━━┓ + * 3 Languages 4 5 Databases 22 + * ┏━━━━━━━┻━━━━━━━┓ + * 6 MongoDB 19 20 dbm 21 + * ┃ + * 7 Videos 18 + * ┃ + * 8 Cooking 17 + * ┏━━━━━━━━┻━━━━━━━━━┓ + * 9 Meat 10 11 Vegetarian 16 + * ┏━━━━━━━┻━━━━━━━┓ + * 12 Pasta 13 14 Mains 15 + */ + const docs = await treeCollection.find({}, { sort: { left: 1 } }).fetchAsync(); + assert.deepEqual(docs, [ + doc('Books', 1, 24, undefined), + doc('Programming', 2, 23, 'Books'), + doc('Languages', 3, 4, 'Programming'), + doc('Databases', 5, 22, 'Programming'), + doc('MongoDB', 6, 19, 'Databases'), + doc('Videos', 7, 18, 'MongoDB'), + doc('Cooking', 8, 17, 'Videos'), + doc('Meat', 9, 10, 'Cooking'), + doc('Vegetarian', 11, 16, 'Cooking'), + doc('Pasta', 12, 13, 'Vegetarian'), + doc('Mains', 14, 15, 'Vegetarian'), + doc('dbm', 20, 21, 'Databases'), + ]); + }); +}); + + +describe('Documents can be moved between roots without breaking the trees', function () { + /** + * Test the following structure + * Root 1 Root 2 + * 1 Books 12 1 Videos 12 + * ┃ ┃ + * 2 Programming 11 2 Cooking 11 + * ┏━━━━━━━━┻━━━━━━━━━┓ ┏━━━━━━━━┻━━━━━━━━━┓ + * 3 Languages 4 5 Databases 10 3 Meat 4 5 Vegetarian 10 + * ┏━━━━━━━┻━━━━━━━┓ ┏━━━━━━━┻━━━━━━━┓ + * 6 MongoDB 7 8 dbm 9 6 Pasta 7 8 Mains 9 + */ + const treeCollection: Mongo.Collection = new Mongo.Collection('treeDocsMoveBetween'); + const doc = function (_id, left, right, parentId, rootId): TreeDoc { + const doc = { _id, root: { id: rootId, collection: 'someCol' }, left, right, parentId }; + if (!parentId) delete doc.parentId; + return doc; + } + beforeEach(function () { + treeCollection.remove({}); + [ + doc('Books', 1, 12, undefined, 'root1'), + doc('Programming', 2, 11, 'Books', 'root1'), + doc('Languages', 3, 4, 'Programming', 'root1'), + doc('Databases', 5, 10, 'Programming', 'root1'), + doc('MongoDB', 6, 7, 'Databases', 'root1'), + doc('dbm', 8, 9, 'Databases', 'root1'), + doc('Videos', 1, 12, undefined, 'root2'), + doc('Cooking', 2, 11, 'Videos', 'root2'), + doc('Meat', 3, 4, 'Cooking', 'root2'), + doc('Vegetarian', 5, 10, 'Cooking', 'root2'), + doc('Pasta', 6, 7, 'Vegetarian', 'root2'), + doc('Mains', 8, 9, 'Vegetarian', 'root2'), + ].map(doc => { + return treeCollection.insert(doc); + }); + }); + it('can move a document from one root to another', async function () { + /** + * Move veg to languages + * Root 1 Root 2 + * 1 Books 18 1 Videos 6 + * ┃ ┃ + * 2 Programming 17 2 Cooking 5 + * ┏━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━┓ ┃ + * 3 Languages 10 11 Databases 16 3 Meat 4 + * ┃ ┏━━━━━━━┻━━━━━━┓ + * 4 Vegetarian 9 12 MongoDB 13 14 dbm 15 + * ┏━━━━━━━┻━━━━━━━┓ + * 5 Pasta 6 7 Mains 8 + */ + const vegDoc = await treeCollection.findOneAsync({ _id: 'Vegetarian' }); + if (!vegDoc) throw new Error('Vegetarian doc not found'); + await moveDocBetweenRoots(vegDoc, treeCollection, { id: 'root1', collection: 'someCol' }, 3.5); + const docs = await treeCollection.find({}, { sort: { 'root.id': 1, left: 1 } }).fetchAsync(); + assert.deepEqual(docs, [ + doc('Books', 1, 18, undefined, 'root1'), + doc('Programming', 2, 17, 'Books', 'root1'), + doc('Languages', 3, 10, 'Programming', 'root1'), + doc('Vegetarian', 4, 9, 'Languages', 'root1'), + doc('Pasta', 5, 6, 'Vegetarian', 'root1'), + doc('Mains', 7, 8, 'Vegetarian', 'root1'), + doc('Databases', 11, 16, 'Programming', 'root1'), + doc('MongoDB', 12, 13, 'Databases', 'root1'), + doc('dbm', 14, 15, 'Databases', 'root1'), + doc('Videos', 1, 6, undefined, 'root2'), + doc('Cooking', 2, 5, 'Videos', 'root2'), + doc('Meat', 3, 4, 'Cooking', 'root2'), + ]); + }); +}); diff --git a/app/imports/api/parenting/parentingFunctions.ts b/app/imports/api/parenting/parentingFunctions.ts new file mode 100644 index 00000000..21856923 --- /dev/null +++ b/app/imports/api/parenting/parentingFunctions.ts @@ -0,0 +1,864 @@ +import { chain, reverse } from 'lodash'; +import { TreeDoc, treeDocFields, Reference } from '/imports/api/parenting/ChildSchema'; +import { getProperties } from '/imports/api/engine/loadCreatures'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; + +export function getCollectionByName(name: string): Mongo.Collection { + const collection = Mongo.Collection.get(name) + if (!collection) { + throw new Meteor.Error('bad-collection-reference', + `Parent references collection ${name}, which does not exist` + ); + } + return collection; +} + +function assertDocFound(doc, ref) { + if (!doc) { + throw new Meteor.Error('document-not-found', + `No document could be found with id: ${ref.id} in ${ref.collection}` + ); + } +} + +export function fetchDocByRefAsync(ref: Reference, options?: Mongo.Options): Promise { + const doc = getCollectionByName(ref.collection).findOneAsync(ref.id, options); + assertDocFound(doc, ref); + return doc; +} + +export function fetchDocByRef(ref: Reference, options?: Mongo.Options): TreeDoc { + const doc: TreeDoc = getCollectionByName(ref.collection).findOne(ref.id, options); + assertDocFound(doc, ref); + return doc; +} + +export interface TreeNode { + doc: T, + children: TreeNode[] +} + +/** + * + * @param nodes An array of documents that share a common root. Must already be sorted by `.left` in ascending order + * @returns An array of tree nodes that each contain a document and its children. Children are + * assigned based on the nearest ancestor included in the input, which may or may not be their + * actual direct parents + */ +export function docsToForest(docs: Array): TreeNode[] { + if (!docs.length) return []; + const forest: TreeNode[] = []; + const ancestorStack: TreeNode[] = []; + let currentAncestor: TreeNode | undefined; + docs.forEach(doc => { + const node: TreeNode = { + doc, + children: [], + }; + // Remove ancestors from the stack until we find one that contains the current document + // Ancestor contains document if ancestor.left < doc.left and ancestor.right > doc.right + // ancestor.left < doc.left is ensured already, because we sorted by doc.left + while (currentAncestor && currentAncestor.doc.right < doc.left) { + currentAncestor = ancestorStack.pop(); + } + // Add this child to its place in the forest, either as a child of the ancestor or as the root + // of a new tree + if (currentAncestor) { + currentAncestor.children.push(node); + } else { + forest.push(node); + } + // Move the last ancestor onto the stack and make this node the new one + if (currentAncestor) ancestorStack.push(currentAncestor); + currentAncestor = node; + }); + return forest; +} + +/** + * Fetch the documents from a collection, and return the tree of those documents, potentially + * including their ancestors or descendants as required + * @param param options + * @returns An array of tree nodes that each contain a document and its children. Children are + * assigned based on the nearest ancestor included in the input, which may or may not be their + * actual direct parents + */ +type FilteredDoc = { + _descendantOfMatchedDocument?: boolean, + _matchedDocumentFilter?: boolean, + _ancestorOfMatchedDocument?: boolean, +} & TreeDoc; + +export function filterToForest( + collection: Mongo.Collection, + rootId: string, + filter?: Mongo.Selector, + { + options = >{}, + includeFilteredDocAncestors = false, + includeFilteredDocDescendants = false + } = {} +): TreeNode[] { + if (!Meteor.isClient) throw 'Only available on the client'; + // Setup the filter + let collectionFilter: Mongo.Selector = { + 'root.id': rootId, + 'removed': { $ne: true }, + }; + if (filter) { + collectionFilter = { + ...collectionFilter, + ...filter, + } + } + // Set up the options + let collectionSort: Mongo.Options['sort'] = { + left: 1 + }; + if (options.sort) { + collectionSort = { + ...collectionSort, + ...options.sort, + } + } + let collectionOptions: Mongo.Options = { + sort: collectionSort, + } + if (options) { + collectionOptions = { + ...collectionOptions, + ...options, + } + } + // Find all the docs that match the filter + const docs: TreeDoc[] = collection.find(collectionFilter, collectionOptions) + .map(doc => { + if (!filter) return doc; + // Mark the docs that were found by the custom filter + doc._matchedDocumentFilter = true; + return doc; + }); + + // Get the doc ancestors + let ancestors: FilteredDoc[] = []; + if (filter && includeFilteredDocAncestors) { + ancestors = collection.find(getFilter.ancestorsOfAll(docs), collectionOptions).map((doc: FilteredDoc) => { + // Mark that the nodes are ancestors of the found nodes + doc._ancestorOfMatchedDocument = true; + return doc; + }); + } + + // Get the doc descendants + let descendants: FilteredDoc[] = []; + if (filter && includeFilteredDocDescendants) { + descendants = collection.find({ + 'removed': { $ne: true }, + ...getFilter.descendantsOfAll(docs), + }).map((doc: FilteredDoc) => { + // Mark that the nodes are descendants of the found nodes + doc._descendantOfMatchedDocument = true; + return doc; + }); + } + const nodes = chain([ + ...ancestors, + ...docs, + ...descendants + ]).uniqBy('_id') + .sortBy('left') + .value(); + // Find all the nodes + return docsToForest(nodes); +} + +export type Forest = { + trees: TreeNode[], + nodeIndex: { [_id: string]: TreeNode }, + orphanIds: string[], +} + +/** + * Takes a complete set of documents and builds a forest using just their `.parentIds` + * Uses `.left` for sibling order within a parent only. + * Orphans whose direct parents can't be found are collected separately + * @param docs An array of all document that share a common root already sorted by `.left` in + * ascending order + * @returns forest: An array of tree nodes that each contain a document and its children. + * orphans: an array of the same, but their parents weren't in the input array + */ +export function docsToForestByParentId(docs: T[]): Forest { + // Collect all the docs in a dict by id + const nodeIndex = <{ [_id: string]: TreeNode }>{}; + docs.forEach(doc => { + nodeIndex[doc._id] = { doc, children: [] }; + }); + // Assign the docs to their parent or the forest or orphanage + const trees: TreeNode[] = []; + const orphanIds: string[] = []; + docs.forEach(doc => { + const node = nodeIndex[doc._id]; + if (!doc.parentId) { + // Root is parent + trees.push(node); + } else if (nodeIndex[doc.parentId]) { + // Parent is found + nodeIndex[doc.parentId].children.push(node); + } else { + // Parent is missing, unset it, and store orphan + node.doc.parentId = undefined; + orphanIds.push(node.doc._id); + trees.push(node); + } + }); + return { + trees, + orphanIds, + nodeIndex + }; +} + +export const getFilter = { + /** + * + * @param doc A document or array of documents that share a root + * @returns A query filter that finds all the ancestors of the doc(s) + */ + ancestors(doc: TreeDoc) { + return { + 'root.id': doc.root.id, + left: { $lt: doc.left }, + right: { $gt: doc.right }, + }; + }, + ancestorsOfAll(docs: Array) { + // The ancestors of no documents is a query that returns nothing + if (docs.length === 0) { + return { _id: '' }; + } + // Fallback to the simpler filter for a single document + if (docs.length === 1) { + return getFilter.ancestors(docs[0]); + } + // Build a filter that selects all ancestors + const filter = { + 'root.id': docs[0].root.id, + $or: [], + }; + docs.forEach(doc => { + filter.$or.push({ + left: { $lt: doc.left }, + right: { $gt: doc.right }, + }); + }); + return filter; + }, + descendantsOfRoot(rootId: string) { + return { + 'root.id': rootId, + } + }, + /** + * @param rootIds a non-empty array of ids + */ + descendantsOfAllRoots(rootIds: string[]) { + if (!rootIds.length) throw 'rootIds can\'t be empty'; + return { + 'root.id': { $in: rootIds }, + }; + }, + descendants(doc: TreeDoc) { + return { + 'root.id': doc.root.id, + left: { $gt: doc.left }, + right: { $lt: doc.right }, + }; + }, + descendantsOfAll(docs: Array) { + // The descendants of no documents is a query that returns nothing + if (docs.length === 0) { + return { _id: '' }; + } + // Fallback to the simpler filter for a single document + if (docs.length === 1) { + return getFilter.descendants(docs[0]); + } + // Build a filter that selects all descendants + const filter = { + 'root.id': docs[0].root.id, + $or: <{ + left: { $gt: number }, + right: { $lt: number }, + }[]>[], + }; + docs.forEach(doc => { + filter.$or.push({ + left: { $gt: doc.left }, + right: { $lt: doc.right }, + }); + }); + return filter; + }, + children(doc: TreeDoc) { + return { + 'root.id': doc.root.id, + parentId: doc._id, + }; + }, + parent(doc: TreeDoc) { + return { + _id: doc.parentId, + }; + }, +} + +/** + * Give documents new random ids and transform their references. + * Transform collections of re-IDed docs according to the collection map + */ +export function renewDocIds({ docArray, collectionMap = {}, idMap = {} }) { + // idMap is a map of {oldId: newId} + // Get a random generator that's consistent on client and server + const randomSrc = DDP.randomStream('renewDocIds'); + + // Give new ids and map the changes as {oldId: newId} + docArray.forEach(doc => { + const oldId = doc._id; + const newId = oldId in idMap ? idMap[oldId] : randomSrc.id(); + doc._id = newId; + idMap[oldId] = newId; + }); + + // Get the id from the map if it exists, leave unchanged otherwise + const remap = id => id in idMap ? idMap[id] : id + + // If there are references by id that need to be maintained when copying from + // a library, here is where we would update them + docArray.forEach(doc => { + // Remap the root and parent ids + doc.root.id = remap(doc.root.id); + doc.root.collection = collectionMap[doc.root.collection] || doc.root.collection; + doc.parentId = remap(doc.parentId); + + // Remap itemIds of items selected as ammo + doc.resource?.itemsConsumed?.forEach(itemConsumed => { + itemConsumed.itemId = remap(itemConsumed.itemId); + }); + }); +} + +/** + * Moves a document within its root. The destination must be halfway between two positions (n.5) + * @param doc + * @param collection + */ +export async function moveDocWithinRoot(doc: TreeDoc, collection: Mongo.Collection, newPosition: number) { + let move: number; + let includedRange; + // Ensure the destination is at a midway point + if (newPosition % 1 !== 0.5) { + throw new Meteor.Error('invalid-move', 'Destination must be halfway between two positions (n.5)'); + } + + // Get the distance to move and the range between the current document and the destination + const docSize = doc.right - doc.left + 1; + let shiftDistance; + if (newPosition < doc.left) { + move = newPosition - doc.left + 0.5; + includedRange = { left: newPosition, right: doc.left - 0.5 }; + shiftDistance = docSize; + } else if (newPosition > doc.right) { + move = newPosition - doc.right - 0.5; + includedRange = { left: doc.right + 0.5, right: newPosition }; + shiftDistance = -docSize; + } else { + throw new Meteor.Error('invalid-move', 'Destination must be outside the doc\'s current location'); + } + + // If the move isn't meaningfully changing the doc's location, skip + if (Math.abs(move) < 1) { + return; + } + + // Get the new parent of the doc after the move + const newParent = await collection.findOneAsync({ + 'root.id': doc.root.id, + left: { $lt: newPosition }, + right: { $gt: newPosition }, + }, { + sort: { left: -1 }, // Many ancestors match, taking the right most one gets the immediate parent + fields: { _id: 1 }, + }); + const newParentId = newParent?._id; + + // Use bulk operations with $set only, because using $inc caused a lot of trouble with both + // latency compensation and oplog tailing + const bulkOps: any[] = []; + + // Move the doc and its children the move distance + await collection.find({ + 'root.id': doc.root.id, + left: { $gte: doc.left }, + right: { $lte: doc.right }, + }, { + fields: { _id: 1, left: 1, right: 1 }, + }).forEachAsync(moveDoc => { + const update = { + $set: { + left: (moveDoc.left + move) || 0, + right: (moveDoc.right + move) || 0, + } + }; + bulkOps.push({ + updateOne: { + filter: { _id: moveDoc._id }, + update, + } + }); + }); + + // Change the doc's parent if necessary + if (newParentId !== doc.parentId) { + let update; + if (newParentId) { + update = { $set: { parentId: newParentId } }; + } else { + update = { $unset: { parentId: 1 } }; + } + bulkOps.push({ + updateOne: { + filter: { _id: doc._id }, + update, + } + }); + } + + // Move all the lefts and rights of documents between the current doc edge and the destination + await collection.find({ + 'root.id': doc.root.id, + $or: [ + { left: { $gt: includedRange.left, $lt: includedRange.right } }, + { right: { $gt: includedRange.left, $lt: includedRange.right } }, + ], + }, { + fields: { _id: 1, left: 1, right: 1 }, + }).forEachAsync(doc => { + const $set: { [P in keyof TreeDoc]?: TreeDoc[P] } = {}; + if (doc.left > includedRange.left && doc.left < includedRange.right) { + $set.left = (doc.left + shiftDistance) || 0; + } + if (doc.right > includedRange.left && doc.right < includedRange.right) { + $set.right = (doc.right + shiftDistance || 0); + } + bulkOps.push({ + updateOne: { + filter: { _id: doc._id }, + update: { $set }, + } + }); + }); + + await writeBulkOperations(collection, bulkOps); + return rebuildNestedSets(collection, doc.root.id); +} + +export async function moveDocBetweenRoots(doc: TreeDoc, collection: Mongo.Collection, newRoot: Reference, newPosition: number) { + if (newRoot.id === doc.root.id) { + throw new Meteor.Error('invalid-move', 'Document is already in the given root') + } + + // Use bulk operations with $set only, because using $inc caused a lot of trouble with both + // latency compensation and oplog tailing + const bulkOps: { + updateOne: { + filter: Mongo.Query, + update: Mongo.Modifier + } + }[] = []; + + // Get the new parent of the doc after the move + const newParent = await collection.findOneAsync({ + 'root.id': newRoot.id, + left: { $lt: newPosition }, + right: { $gt: newPosition }, + }, { + sort: { left: -1 }, // Many ancestors match, taking the right most one gets the immediate parent + fields: { _id: 1 }, + }); + const newParentId = newParent?._id; + + // Change the doc's parent if necessary + if (newParentId !== doc.parentId) { + let update; + if (newParentId) { + update = { $set: { parentId: newParentId } }; + } else { + update = { $unset: { parentId: 1 } }; + } + bulkOps.push({ + updateOne: { + filter: { _id: doc._id }, + update, + } + }); + } + + // Open a gap in the root we are moving to at the new location + const docSize = doc.right - doc.left + 1; + await collection.find({ + 'root.id': newRoot.id, + $or: [ + { left: { $gt: newPosition } }, + { right: { $gt: newPosition } }, + ], + }, { + fields: { _id: 1, left: 1, right: 1 }, + }).forEachAsync(openGapDoc => { + const $set: { [P in keyof TreeDoc]?: TreeDoc[P] } = {}; + if (openGapDoc.left > newPosition) { + $set.left = (openGapDoc.left + docSize) || 0; + } + if (openGapDoc.right > newPosition) { + $set.right = (openGapDoc.right + docSize) || 0; + } + bulkOps.push({ + updateOne: { + filter: { _id: openGapDoc._id }, + update: { $set }, + } + }); + }); + + // Move the doc and its children the move distance, and set their new root + const move = newPosition + 0.5 - doc.left; + await collection.find({ + 'root.id': doc.root.id, + left: { $gte: doc.left }, + right: { $lte: doc.right }, + }, { + fields: { _id: 1, left: 1, right: 1 }, + }).forEachAsync(moveDoc => { + bulkOps.push({ + updateOne: { + filter: { _id: moveDoc._id }, + update: { + $set: { + left: (moveDoc.left + move) || 0, + right: (moveDoc.right + move) || 0, + root: newRoot + } + }, + } + }); + }); + + // Close the gap in the root we are leaving + await collection.find({ + 'root.id': doc.root.id, + $or: [ + { left: { $gt: doc.right } }, + { right: { $gt: doc.right } }, + ], + }, { + fields: { _id: 1, left: 1, right: 1 }, + }).forEachAsync(closeGapDoc => { + const $set: { [P in keyof TreeDoc]?: TreeDoc[P] } = {}; + if (closeGapDoc.left > doc.right) { + $set.left = (closeGapDoc.left - docSize) || 0; + } + if (closeGapDoc.right > doc.right) { + $set.right = (closeGapDoc.right - docSize || 0); + } + bulkOps.push({ + updateOne: { + filter: { _id: closeGapDoc._id }, + update: { $set }, + } + }); + }); + + return writeBulkOperations(collection, bulkOps); +} + +/** + * Changes the doc to be a child of the parent, and then rebuilds the nested sets of the roots + * of both doc and parent + * @deprecated Use moveDocWithinRoot or moveDocBetweenRoots instead + * @param doc The doc to move + * @param parent The new parent of the doc, null to move the doc to the root of the tree + * @param collection + * @returns + */ +export async function changeParent(doc: TreeDoc, parent: TreeDoc | null, collection: Mongo.Collection, order?: number) { + // Skip if we aren't changing the parent id + if (doc.parentId === parent?._id) return; + + // Store the original roots + const rootChange = parent && doc.root.id !== parent.root?.id; + + // Check that the doc isn't becoming its own ancestor + if (parent && parent.left > doc.left && parent.right < doc.right) { + throw new Meteor.Error('invalid parenting', 'A doc can\'t be its own ancestor'); + } + + // update the document's parenting and root if necessary + let update: Mongo.Modifier; + if (!parent) { + update = { + $unset: { parentId: 1 } + }; + } else { + update = { + $set: { parentId: parent?._id } + }; + } + if (rootChange && update.$set) { + update.$set.root = parent.root; + } + if (order) { + if (!update.$set) update.$set = {}; + update.$set.left = order; + } + + await collection.updateAsync(doc._id, update); + + // Rebuild the nested sets of everything on the root document(s) + rebuildNestedSets(collection, doc.root.id); + if (rootChange) { + rebuildNestedSets(collection, parent.root.id); + } +} + +export function compareOrder(docA, docB) { + // < 0 if A comes before B + // = 0 if A and B are the same order + // > 0 if B comes before A + + // They must share a root ancestor to be meaningfully sorted + if (docA.root.id !== docB.root.id) { + return 0; + } else { + return docA.left - docB.left; + } +} + +/** + * Determine if two properties have an ancestor relationship, returns true if A is an ancestor of B + * or B is an ancestor of A + */ +export function hasAncestorRelationship(propA: TreeDoc, propB: TreeDoc): boolean { + // If they don't share a root, they can't share an ancestor relationship + if (propA.root.id !== propB.root.id) { + return false; + } + // Return if there is an parent relationship in either direction + return propA.parentId === propB._id + || propB.parentId === propA._id + // or an ancestor relationship in either direction + || isAncestor(propA, propB) + || isAncestor(propB, propA); +} + +/** + * Returns true if A is a direct ancestor of B, assuming their roots are equal + */ +export function isAncestor(propA?: TreeDoc, propB?: TreeDoc): boolean { + if (!propA || !propB) return false; + return propA.left < propB.left && propA.right > propB.right; +} + +/** + * @deprecated Just set left to Number.MAX_SAFE_INTEGER instead + */ +export function setDocToLastOrder(collection: Mongo.Collection, doc: TreeDoc) { + doc.left = Number.MAX_SAFE_INTEGER; +} + +export function rebuildNestedSets(collection: Mongo.Collection, rootId: string) { + const docs = collection.find({ + 'root.id': rootId, + removed: { $ne: true } + }, { + fields: treeDocFields, + sort: { + //Reverse sorting so that arrays can be used as stacks with the first item on top + left: 1, + }, + }).fetch(); + + const operations = calculateNestedSetOperations(docs); + return writeBulkOperations(collection, operations); +} + +export function rebuildCreatureNestedSets(creatureId) { + const docs = getProperties(creatureId); + const operations = calculateNestedSetOperations(docs); + return writeBulkOperations(CreatureProperties as Mongo.Collection, operations); +} + +/** Calculates the operations needed to make a tree of nested sets + * Warning: Will reverse the order of docs! + * Walk around the tree numbering left on the way down and right on the way up like so: + * + * 1 Books 12 + * ┃ + * 2 Programming 11 + * ┏━━━━━━━━┻━━━━━━━━━┓ + * 3 Languages 4 5 Databases 10 + * ┏━━━━━━━┻━━━━━━━┓ + * 6 MongoDB 7 8 dbm 9 + * + * + * @param docs + * @returns + */ +export function calculateNestedSetOperations(docs: TreeDoc[]) { + const { trees: stack, orphanIds } = docsToForestByParentId(reverse(docs)); + const removeMissingParentsOp = orphanIds.length ? { + updateMany: { + filter: { _id: { $in: orphanIds } }, + update: { $unset: { parentId: 1 } }, + } + } : undefined; + const visitedNodes = new Set(); + const visitedChildren = new Set(); + const opsById: { [_id: string]: any } = {} + let count = 1; + + while (stack.length) { + const top = stack[stack.length - 1]; + if (visitedNodes.has(top)) { + // We've arrived at this node again for some reason, this shouldn't happen + console.log('visited already, parent loop maybe?') + stack.pop(); + } else if (visitedChildren.has(top)) { + // We've arrived at this node after visiting the children, + // we must be on the way up, mark the right number + visitedNodes.add(top); + stack.pop(); + if (top.doc.right !== count) { + if (!opsById[top.doc._id]) { + opsById[top.doc._id] = { + updateOne: { + filter: { _id: top.doc._id }, + update: { $set: { right: count } } + } + } + } else { + opsById[top.doc._id].updateOne.update.$set.right = count; + } + } + count += 1; + } else { + // We're arriving at this node for the first time + // We must be on the way down, mark the left number and go visit the children + visitedChildren.add(top); + stack.push(...top.children); + if (top.doc.left !== count) { + opsById[top.doc._id] = { + updateOne: { + filter: { _id: top.doc._id }, + update: { $set: { left: count } }, + } + }; + } + count += 1; + } + } + + const operations = [...Object.values(opsById)]; + if (removeMissingParentsOp) operations.push(removeMissingParentsOp); + return operations; +} + +/** + * Same as calculateNestedSetOperations, but applies the ops to the properties + * @param docs An array of documents that share a common root. Must already be sorted by `.left` in ascending order + * @returns The documents as a forest of tree nodes + */ +export function applyNestedSetProperties(docs: T[]): Forest { + // Walk around the tree numbering left on the way down and right on the way up like so: + const forest = docsToForestByParentId(reverse([...docs])); + const { trees, orphanIds } = forest; + + const stack = [...trees]; + const visitedNodes = new Set(); + const visitedChildren = new Set(); + let count = 1; + + while (stack.length) { + const top = stack[stack.length - 1]; + if (orphanIds.includes(top.doc._id)) { + delete top.doc.parentId; + } + if (visitedNodes.has(top)) { + // We've arrived at this node again for some reason, this shouldn't happen + console.log('visited already, parent loop maybe?') + stack.pop(); + } else if (visitedChildren.has(top)) { + // We've arrived at this node after visiting the children, + // we must be on the way up, mark the right number + visitedNodes.add(top); + stack.pop(); + if (top.doc.right !== count) { + top.doc.right = count; + } + count += 1; + } else { + // We're arriving at this node for the first time + // We must be on the way down, mark the left number and go visit the children + visitedChildren.add(top); + stack.push(...top.children); + if (top.doc.left !== count) { + top.doc.left = count; + } + count += 1; + } + } + return forest; +} + +/** + * Write some number of bulk operations to the collection, uses a bulk write on the server + * and iterates through regular updates on the client + * Resolves once all writes have completed + * @param collection The collection to write to + * @param operations An array of bulk operations to write + * @returns Promise + */ +function writeBulkOperations(collection: Mongo.Collection, operations) { + if (Meteor.isServer) { + if (!operations.length) return Promise.resolve(); + return new Promise((resolve, reject) => { + collection.rawCollection().bulkWrite( + operations, + { ordered: false }, + function (e) { + if (e) { + reject(e); + } else { + resolve(undefined); + } + } + ); + }); + } else { + // Don't do latency compensation if there are too many operations, it just causes client + // lag without much benefit + operations.forEach(op => { + if (op.updateOne) { + collection.update( + op.updateOne.filter, + op.updateOne.update, + ); + } else if (op.updateMany) { + collection.update( + op.updateMany.filter, + op.updateMany.update, + { multi: true }, + ) + } + }); + } + return Promise.resolve(); +} diff --git a/app/imports/api/parenting/softRemove.js b/app/imports/api/parenting/softRemove.js deleted file mode 100644 index 099abfc2..00000000 --- a/app/imports/api/parenting/softRemove.js +++ /dev/null @@ -1,74 +0,0 @@ -import getCollectionByName from '/imports/api/parenting/getCollectionByName.js'; -import { updateDescendants } from '/imports/api/parenting/parenting.js'; - -export function softRemove({_id, collection}){ - let removalDate = new Date(); - if (typeof collection === 'string') { - collection = getCollectionByName(collection); - } - // Remove this document - collection.update( - _id, { - $set: { - removed: true, - removedAt: removalDate, - }, - $unset: { - removedWith: 1, - } - }, { - selector: {type: 'any'}, - }, - ); - // Remove all the descendants that have not yet been removed, and set them to be - // removed with this document - updateDescendants({ - collection, - ancestorId: _id, - filter: {removed: {$ne: true}}, - modifier: {$set: { - removed: true, - removedAt: removalDate, - removedWith: _id, - }}, - }); -} - -const restoreError = function(){ - throw new Meteor.Error('restore-failed', - 'Could not restore this document, maybe it was removed by a parent?' - ); -}; - -export function restore({ _id, collection, extraUpdates}){ - if (typeof collection === 'string') { - collection = getCollectionByName(collection); - } - const update = { - $unset: { - removed: 1, - removedAt: 1, - }, - ...extraUpdates - } - - let numUpdated = collection.update({ - _id, - removedWith: {$exists: false} - }, update , { - selector: {type: 'any'}, - },); - if (numUpdated === 0) restoreError(); - updateDescendants({ - collection, - ancestorId: _id, - filter: { - removedWith: _id, - }, - modifier: { $unset: { - removed: 1, - removedAt: 1, - removedWith: 1, - }}, - }); -} diff --git a/app/imports/api/parenting/softRemove.ts b/app/imports/api/parenting/softRemove.ts new file mode 100644 index 00000000..3783a3b5 --- /dev/null +++ b/app/imports/api/parenting/softRemove.ts @@ -0,0 +1,102 @@ +import { getCollectionByName, getFilter } from '/imports/api/parenting/parentingFunctions'; +import { TreeDoc } from '/imports/api/parenting/ChildSchema'; + +export function softRemove(collectionOrName: Mongo.Collection | string, docOrId?: TreeDoc | string) { + const removalDate = new Date(); + + let collection: Mongo.Collection; + if (typeof collectionOrName === 'string') { + collection = getCollectionByName(collectionOrName); + } else { + collection = collectionOrName; + } + + let doc: TreeDoc | undefined; + if (typeof docOrId === 'string') { + doc = collection.findOne(docOrId); + } else { + doc = docOrId + } + if (!doc) { + throw new Meteor.Error('not found', 'The document to remove was not found'); + } + + // Remove this document + collection.update( + doc._id, + { + $set: { + removed: true, + removedAt: removalDate, + }, + $unset: { + removedWith: 1, + } + } + ); + // Remove all the descendants that have not yet been removed, and set them to be + // removed with this document + collection.update({ + ...getFilter.descendants(doc), + removed: { $ne: true }, + }, { + $set: { + removed: true, + removedAt: removalDate, + removedWith: doc._id, + } + }, { + multi: true, + }); +} + +const restoreError = function () { + throw new Meteor.Error('restore-failed', + 'Could not restore this document, maybe it was removed by a parent?' + ); +}; + +export function restore(collectionOrName: Mongo.Collection | string, docOrId: TreeDoc | string, extraUpdates?) { + + let collection: Mongo.Collection; + if (typeof collectionOrName === 'string') { + collection = getCollectionByName(collectionOrName); + } else { + collection = collectionOrName; + } + + let doc: TreeDoc | undefined; + if (typeof docOrId === 'string') { + doc = collection.findOne(docOrId); + } else { + doc = docOrId + } + if (!doc) { + throw new Meteor.Error('not found', 'The document to remove was not found'); + } + + const numUpdated: number = collection.update({ + _id: doc._id, + removedWith: { $exists: false } + }, { + $unset: { + removed: 1, + removedAt: 1, + }, + ...extraUpdates + }); + + if (numUpdated === 0) restoreError(); + + return collection.update({ + removedWith: doc._id, + }, { + $unset: { + removed: 1, + removedAt: 1, + removedWith: 1, + } + }, { + multi: true, + }) + 1; +} diff --git a/app/imports/api/properties/Actions.js b/app/imports/api/properties/Actions.ts similarity index 68% rename from app/imports/api/properties/Actions.js rename to app/imports/api/properties/Actions.ts index 99910081..f4d87bdc 100644 --- a/app/imports/api/properties/Actions.js +++ b/app/imports/api/properties/Actions.ts @@ -1,13 +1,70 @@ import SimpleSchema from 'simpl-schema'; -import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; -import { storedIconsSchema } from '/imports/api/icons/Icons.js'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; -import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema'; +import { storedIconsSchema } from '/imports/api/icons/Icons'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; +import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX'; +import { CreatureProperty } from '/imports/api/creature/creatureProperties/CreatureProperties'; +import { InlineCalculation } from '/imports/api/properties/subSchemas/inlineCalculationField'; +import { CalculatedField } from '/imports/api/properties/subSchemas/computedField'; +import Property from '/imports/api/properties/Properties.type'; + +export type CreatureAction = Action & CreatureProperty & { + overridden?: boolean + insufficientResources?: boolean +} /* * Actions are things a character can do */ -let ActionSchema = createPropertySchema({ +export interface Action extends ActionBase { + type: 'action' +} + +/** + * Base property type for both spells and actions + */ +export interface ActionBase extends Property { + name?: string + summary?: InlineCalculation + description?: InlineCalculation + actionType: 'action' | 'bonus' | 'attack' | 'reaction' | 'free' | 'long' | 'event' + variableName?: string + target: 'self' | 'singleTarget' | 'multipleTargets' + attackRoll?: CalculatedField + uses?: CalculatedField + usesUsed?: number + reset?: string + silent?: boolean + usesLeft?: number + // Resources + resources: { + itemsConsumed: { + _id: string + tag?: string + itemName?: string + quantity?: CalculatedField + itemId?: string + available?: number + }[] + attributesConsumed: { + _id: string + variableName?: string + quantity?: CalculatedField + available?: number + statName?: string + }[] + conditions?: { + _id: string, + condition?: CalculatedField + conditionNote?: string, + }[] + } +} + +/* + * Actions are things a character can do + */ +const ActionSchema = createPropertySchema({ name: { type: String, optional: true, @@ -50,7 +107,6 @@ let ActionSchema = createPropertySchema({ attackRoll: { type: 'fieldToCompute', optional: true, - defaultValue: 'strength.modifier + proficiencyBonus', }, // Calculation of how many times this action can be used uses: { @@ -78,6 +134,7 @@ let ActionSchema = createPropertySchema({ 'resources.itemsConsumed': { type: Array, defaultValue: [], + max: 32, }, 'resources.itemsConsumed.$': { type: Object, @@ -105,6 +162,7 @@ let ActionSchema = createPropertySchema({ 'resources.attributesConsumed': { type: Array, defaultValue: [], + max: 32, }, 'resources.attributesConsumed.$': { type: Object, @@ -125,6 +183,30 @@ let ActionSchema = createPropertySchema({ type: 'fieldToCompute', optional: true, }, + 'resources.conditions': { + type: Array, + defaultValue: [], + max: 32, + }, + 'resources.conditions.$': { + type: Object, + }, + 'resources.conditions.$._id': { + type: String, + regEx: SimpleSchema.RegEx.Id, + autoValue() { + if (!this.isSet) return Random.id(); + } + }, + 'resources.conditions.$.condition': { + type: 'fieldToCompute', + optional: true, + }, + 'resources.conditions.$.conditionNote': { + type: String, + optional: true, + max: STORAGE_LIMITS.calculation, + }, // Prevent the property from showing up in the log silent: { type: Boolean, @@ -153,6 +235,7 @@ const ComputedOnlyActionSchema = createPropertySchema({ optional: true, }, uses: { + parseLevel: 'reduce', type: 'computedOnlyField', optional: true, }, @@ -162,6 +245,12 @@ const ComputedOnlyActionSchema = createPropertySchema({ optional: true, removeBeforeCompute: true, }, + // Denormalised tag if event is overridden by one with the same variable name + overridden: { + type: Boolean, + optional: true, + removeBeforeCompute: true, + }, // Resources resources: { type: Object, diff --git a/app/imports/api/properties/Adjustments.js b/app/imports/api/properties/Adjustments.js index 6b2607f5..8521063c 100644 --- a/app/imports/api/properties/Adjustments.js +++ b/app/imports/api/properties/Adjustments.js @@ -1,6 +1,6 @@ import SimpleSchema from 'simpl-schema'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; -import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema'; const AdjustmentSchema = createPropertySchema({ // The roll that determines how much to change the attribute diff --git a/app/imports/api/properties/Attributes.js b/app/imports/api/properties/Attributes.ts similarity index 56% rename from app/imports/api/properties/Attributes.js rename to app/imports/api/properties/Attributes.ts index 94e5d657..88d0fcae 100644 --- a/app/imports/api/properties/Attributes.js +++ b/app/imports/api/properties/Attributes.ts @@ -1,12 +1,58 @@ 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 VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema'; +import { CreatureProperty } from '/imports/api/creature/creatureProperties/CreatureProperties'; +import { CalculatedField } from '/imports/api/properties/subSchemas/computedField'; +import { InlineCalculation } from '/imports/api/properties/subSchemas/inlineCalculationField'; +import { ConstantValueType } from '/imports/parser/parseTree/constant'; +import Property from '/imports/api/properties/Properties.type'; + +export type CreatureAttribute = Attribute & CreatureProperty & { + total?: ConstantValueType; + value?: ConstantValueType; + modifier?: number; + proficiency?: 0 | 0.49 | 0.5 | 1 | 2; + advantage?: -1 | 0 | 1; + constitutionMod?: number; + hide?: true; + overridden?: true; + effectIds?: string[]; + proficiencyIds?: string[]; + definitions?: { _id: string, type: string, row?: number }[]; +} + +export interface Attribute extends Property { + type: 'attribute'; + name?: string; + variableName?: string; + attributeType: 'ability' | 'stat' | 'modifier' | 'hitDice' | 'healthBar' | 'resource' | + 'spellSlot' | 'utility'; + hitDiceSize?: 'd1' | 'd2' | 'd4' | 'd6' | 'd8' | 'd10' | 'd12' | 'd20' | 'd100'; + spellSlotLevel?: CalculatedField; + healthBarColorMid?: string; + healthBarColorLow?: string; + healthBarNoDamage?: true; + healthBarNoHealing?: true; + healthBarNoDamageOverflow?: true; + healthBarNoHealingOverflow?: true; + healthBarDamageOrder?: number; + healthBarHealingOrder?: number; + baseValue?: CalculatedField; + description?: InlineCalculation; + damage?: number; + decimal?: true; + ignoreLowerLimit?: true; + ignoreUpperLimit?: true; + hideWhenTotalZero?: true; + hideWhenValueZero?: true; + reset?: string; +} /* * Attributes are numbered stats of a character */ -let AttributeSchema = createPropertySchema({ +const AttributeSchema = createPropertySchema({ name: { type: String, optional: true, @@ -134,7 +180,7 @@ let AttributeSchema = createPropertySchema({ }, }); -let ComputedOnlyAttributeSchema = createPropertySchema({ +const ComputedOnlyAttributeSchema = createPropertySchema({ description: { type: 'computedOnlyInlineCalculationField', optional: true, @@ -173,6 +219,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, @@ -192,18 +245,104 @@ let ComputedOnlyAttributeSchema = createPropertySchema({ removeBeforeCompute: true, }, // A list of effect ids targeting this attribute - effects: { + 'effectIds': { type: Array, optional: true, removeBeforeCompute: true, }, - 'effects.$': { + 'effectIds.$': { + type: String, + }, + 'proficiencyIds': { + type: Array, + optional: true, + removeBeforeCompute: true, + }, + 'proficiencyIds.$': { + type: String, + }, + 'definitions': { + type: Array, + optional: true, + removeBeforeCompute: true, + }, + 'definitions.$': { type: Object, - blackbox: true, + }, + 'definitions.$._id': { + type: String, + }, + 'definitions.$.type': { + optional: true, + type: String, + }, + 'definitions.$.row': { + type: Number, + optional: true, + }, + // Triggers that fire when this property is damaged + 'damageTriggerIds': { + type: Object, + optional: true, + removeBeforeCompute: true, + }, + 'damageTriggerIds.before': { + type: Array, + optional: true, + }, + 'damageTriggerIds.before.$': { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, + 'damageTriggerIds.after': { + type: Array, + optional: true, + }, + 'damageTriggerIds.after.$': { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, + 'damageTriggerIds.afterChildren': { + type: Array, + optional: true, + }, + 'damageTriggerIds.afterChildren.$': { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, + // Triggers that fire when this property is used to make a check + 'checkTriggerIds': { + type: Object, + optional: true, + removeBeforeCompute: true, + }, + 'checkTriggerIds.before': { + type: Array, + optional: true, + }, + 'checkTriggerIds.before.$': { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, + 'checkTriggerIds.after': { + type: Array, + optional: true, + }, + 'checkTriggerIds.after.$': { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, + 'checkTriggerIds.afterChildren': { + type: Array, + optional: true, + }, + 'checkTriggerIds.afterChildren.$': { + type: String, + regEx: SimpleSchema.RegEx.Id, }, }); -const ComputedAttributeSchema = new SimpleSchema() +const ComputedAttributeSchema = new SimpleSchema({}) .extend(ComputedOnlyAttributeSchema) .extend(AttributeSchema); diff --git a/app/imports/api/properties/Branches.js b/app/imports/api/properties/Branches.js index 73716eb6..4f65c8a3 100644 --- a/app/imports/api/properties/Branches.js +++ b/app/imports/api/properties/Branches.js @@ -1,6 +1,6 @@ import SimpleSchema from 'simpl-schema'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; -import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema'; let BranchSchema = createPropertySchema({ branchType: { @@ -22,7 +22,7 @@ let BranchSchema = createPropertySchema({ 'index', // if it has option children, asks to select one // Otherwise presents its own text with yes/no - //'choice', + 'choice', //'option', ], defaultValue: 'if', @@ -52,7 +52,7 @@ let ComputedOnlyBranchSchema = createPropertySchema({ }, }); -const ComputedBranchSchema = new SimpleSchema() +const ComputedBranchSchema = new SimpleSchema({}) .extend(BranchSchema) .extend(ComputedOnlyBranchSchema); diff --git a/app/imports/api/properties/BuffRemovers.js b/app/imports/api/properties/BuffRemovers.js index b96466cb..8adce152 100644 --- a/app/imports/api/properties/BuffRemovers.js +++ b/app/imports/api/properties/BuffRemovers.js @@ -1,6 +1,6 @@ import SimpleSchema from 'simpl-schema'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; -import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema'; let BuffRemoverSchema = createPropertySchema({ name: { diff --git a/app/imports/api/properties/Buffs.js b/app/imports/api/properties/Buffs.js index 386c402b..dab5594b 100644 --- a/app/imports/api/properties/Buffs.js +++ b/app/imports/api/properties/Buffs.js @@ -1,6 +1,6 @@ import SimpleSchema from 'simpl-schema'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; -import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema'; let BuffSchema = createPropertySchema({ name: { diff --git a/app/imports/api/properties/ClassLevels.js b/app/imports/api/properties/ClassLevels.js index bbd76d58..30c75cb5 100644 --- a/app/imports/api/properties/ClassLevels.js +++ b/app/imports/api/properties/ClassLevels.js @@ -1,7 +1,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 VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema'; const ClassLevelSchema = createPropertySchema({ name: { @@ -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({ diff --git a/app/imports/api/properties/Classes.js b/app/imports/api/properties/Classes.js index e6e6471f..8242b1bc 100644 --- a/app/imports/api/properties/Classes.js +++ b/app/imports/api/properties/Classes.js @@ -1,6 +1,6 @@ import SimpleSchema from 'simpl-schema'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; -import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema'; // Classes are like slots, except they only take class levels and enforce that // lower levels are taken before higher levels @@ -40,7 +40,7 @@ let ClassSchema = createPropertySchema({ 'extraTags.$._id': { type: String, regEx: SimpleSchema.RegEx.Id, - autoValue(){ + autoValue() { if (!this.isSet) return Random.id(); } }, @@ -74,7 +74,7 @@ const ComputedOnlyClassSchema = createPropertySchema({ type: 'computedOnlyField', optional: true, }, - + // Denormalised fields level: { type: SimpleSchema.Integer, diff --git a/app/imports/api/properties/Constants.js b/app/imports/api/properties/Constants.js index ded0f090..2ef18326 100644 --- a/app/imports/api/properties/Constants.js +++ b/app/imports/api/properties/Constants.js @@ -1,12 +1,14 @@ import SimpleSchema from 'simpl-schema'; -import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js'; -import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js'; +import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX'; +import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema'; import { parse, prettifyParseError, -} from '/imports/parser/parser.js'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; -import resolve, { Context, traverse } from '/imports/parser/resolve.js'; +} from '/imports/parser/parser'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; +import resolve from '/imports/parser/resolve'; +import Context from '../../parser/types/Context'; +import traverse from '/imports/parser/traverse'; /* * Constants are primitive values that can be used elsewhere in computations diff --git a/app/imports/api/properties/Containers.js b/app/imports/api/properties/Containers.js index 6936bd15..f840a0bf 100644 --- a/app/imports/api/properties/Containers.js +++ b/app/imports/api/properties/Containers.js @@ -1,6 +1,6 @@ import SimpleSchema from 'simpl-schema'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; -import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema'; let ContainerSchema = createPropertySchema({ name: { diff --git a/app/imports/api/properties/CreatureTemplates.ts b/app/imports/api/properties/CreatureTemplates.ts new file mode 100644 index 00000000..7f48edf0 --- /dev/null +++ b/app/imports/api/properties/CreatureTemplates.ts @@ -0,0 +1,40 @@ +import SimpleSchema from 'simpl-schema'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema'; + +// Creature templates represent creatures that don't yet exist +// Used to store creatures in the library, or as templates for another creature to summon +const CreatureTemplateSchema = createPropertySchema({ + name: { + type: String, + max: STORAGE_LIMITS.name, + optional: true, + }, + description: { + type: 'inlineCalculationFieldToCompute', + optional: true, + }, + picture: { + type: String, + optional: true, + max: STORAGE_LIMITS.url, + }, + avatarPicture: { + type: String, + optional: true, + max: STORAGE_LIMITS.url, + }, +}); + +const ComputedOnlyCreatureTemplateSchema = createPropertySchema({ + description: { + type: 'computedOnlyInlineCalculationField', + optional: true, + }, +}); + +const ComputedCreatureTemplateSchema = new SimpleSchema({}) + .extend(CreatureTemplateSchema) + .extend(ComputedOnlyCreatureTemplateSchema); + +export { CreatureTemplateSchema, ComputedCreatureTemplateSchema, ComputedOnlyCreatureTemplateSchema }; diff --git a/app/imports/api/properties/DamageMultipliers.js b/app/imports/api/properties/DamageMultipliers.js index 3c75fbbf..c76f104f 100644 --- a/app/imports/api/properties/DamageMultipliers.js +++ b/app/imports/api/properties/DamageMultipliers.js @@ -1,6 +1,6 @@ import SimpleSchema from 'simpl-schema'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; -import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; +import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX'; /* * DamageMultipliers are multipliers that affect how much damage is taken from diff --git a/app/imports/api/properties/Damages.js b/app/imports/api/properties/Damages.js index 190dd4b5..7964839f 100644 --- a/app/imports/api/properties/Damages.js +++ b/app/imports/api/properties/Damages.js @@ -1,7 +1,7 @@ import SimpleSchema from 'simpl-schema'; -import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; -import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; +import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX'; const DamageSchema = createPropertySchema({ // The roll that determines how much to damage the attribute @@ -32,6 +32,28 @@ const DamageSchema = createPropertySchema({ type: Boolean, optional: true, }, + // remove the entire object if there is no saving throw + save: { + type: Object, + optional: true, + }, + // The computed DC + 'save.dc': { + type: 'fieldToCompute', + optional: true, + }, + // The variable name of save to roll + 'save.stat': { + type: String, + optional: true, + max: STORAGE_LIMITS.variableName, + }, + // The damage to deal on a successful save + 'save.damageFunction': { + type: 'fieldToCompute', + optional: true, + parseLevel: 'compile', + }, }); const ComputedOnlyDamageSchema = createPropertySchema({ @@ -40,9 +62,23 @@ const ComputedOnlyDamageSchema = createPropertySchema({ optional: true, parseLevel: 'compile', }, + save: { + type: Object, + optional: true, + }, + 'save.dc': { + type: 'computedOnlyField', + parseLevel: 'compile', + optional: true, + }, + 'save.damageFunction': { + type: 'computedOnlyField', + parseLevel: 'compile', + optional: true, + }, }); -const ComputedDamageSchema = new SimpleSchema() +const ComputedDamageSchema = new SimpleSchema({}) .extend(DamageSchema) .extend(ComputedOnlyDamageSchema); diff --git a/app/imports/api/properties/Effects.js b/app/imports/api/properties/Effects.js index e1ba0c37..347a4000 100644 --- a/app/imports/api/properties/Effects.js +++ b/app/imports/api/properties/Effects.js @@ -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 STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema'; +import TagTargetingSchema from '/imports/api/properties/subSchemas/TagTargetingSchema'; /* * 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: { diff --git a/app/imports/api/properties/Features.js b/app/imports/api/properties/Features.js index 91fc95b6..e4b467c1 100644 --- a/app/imports/api/properties/Features.js +++ b/app/imports/api/properties/Features.js @@ -1,6 +1,6 @@ import SimpleSchema from 'simpl-schema'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; -import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema'; let FeatureSchema = createPropertySchema({ name: { diff --git a/app/imports/api/properties/Folders.js b/app/imports/api/properties/Folders.js index aa68e56f..96bc2840 100644 --- a/app/imports/api/properties/Folders.js +++ b/app/imports/api/properties/Folders.js @@ -1,20 +1,52 @@ -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; -import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; +import SimpleSchema from 'simpl-schema'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema'; // 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, }, + hideStatsGroup: { + type: Boolean, + optional: true, + }, + tab: { + type: String, + optional: true, + allowedValues: [ + 'stats', 'features', 'actions', 'spells', 'inventory', 'journal', 'build' + ], + }, + location: { + type: String, + optional: true, + allowedValues: [ + 'start', 'events', 'stats', 'skills', 'proficiencies', 'end' + ], + }, }); -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 }; diff --git a/app/imports/api/properties/Items.js b/app/imports/api/properties/Items.ts similarity index 54% rename from app/imports/api/properties/Items.js rename to app/imports/api/properties/Items.ts index a328cfcf..d513b690 100644 --- a/app/imports/api/properties/Items.js +++ b/app/imports/api/properties/Items.ts @@ -1,6 +1,14 @@ import SimpleSchema from 'simpl-schema'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; -import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema'; +import { CreatureProperty } from '/imports/api/creature/creatureProperties/CreatureProperties'; + +export interface Item extends CreatureProperty { + type: 'item' + name?: string + plural?: string + quantity: number +} const ItemSchema = createPropertySchema({ name: { @@ -55,16 +63,51 @@ const ItemSchema = createPropertySchema({ type: Boolean, defaultValue: false, }, + // Prevent the property from showing up in the log + silent: { + type: Boolean, + optional: true, + }, + // Triggers that fire when this property is used as ammo + 'ammoTriggerIds': { + type: Object, + optional: true, + removeBeforeCompute: true, + }, + 'ammoTriggerIds.before': { + type: Array, + optional: true, + }, + 'ammoTriggerIds.before.$': { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, + 'ammoTriggerIds.after': { + type: Array, + optional: true, + }, + 'ammoTriggerIds.after.$': { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, + 'ammoTriggerIds.afterChildren': { + type: Array, + optional: true, + }, + 'ammoTriggerIds.afterChildren.$': { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, }); -let ComputedOnlyItemSchema = createPropertySchema({ +const ComputedOnlyItemSchema = createPropertySchema({ description: { type: 'computedOnlyInlineCalculationField', optional: true, }, }); -const ComputedItemSchema = new SimpleSchema() +const ComputedItemSchema = new SimpleSchema({}) .extend(ItemSchema) .extend(ComputedOnlyItemSchema); diff --git a/app/imports/api/properties/Notes.js b/app/imports/api/properties/Notes.js index 1e738b08..699f981d 100644 --- a/app/imports/api/properties/Notes.js +++ b/app/imports/api/properties/Notes.js @@ -1,6 +1,6 @@ import SimpleSchema from 'simpl-schema'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; -import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema'; let NoteSchema = createPropertySchema({ name: { @@ -16,6 +16,11 @@ let NoteSchema = createPropertySchema({ type: 'inlineCalculationFieldToCompute', optional: true, }, + // Prevent the property from showing up in the log + silent: { + type: Boolean, + optional: true, + }, }); let ComputedOnlyNoteSchema = createPropertySchema({ diff --git a/app/imports/api/properties/PointBuys.js b/app/imports/api/properties/PointBuys.js index db20911a..5c45aab4 100644 --- a/app/imports/api/properties/PointBuys.js +++ b/app/imports/api/properties/PointBuys.js @@ -1,8 +1,8 @@ import SimpleSchema from 'simpl-schema'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; -import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js'; -import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; -import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; +import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema'; +import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema'; /* * PointBuys are reason-value attached to skills and abilities @@ -29,7 +29,7 @@ let PointBuySchema = createPropertySchema({ 'values.$._id': { type: String, regEx: SimpleSchema.RegEx.Id, - autoValue(){ + autoValue() { if (!this.isSet) return Random.id(); } }, @@ -49,18 +49,6 @@ let PointBuySchema = createPropertySchema({ type: Number, optional: true, }, - 'values.$.min': { - type: 'fieldToCompute', - optional: true, - }, - 'values.$.max': { - type: 'fieldToCompute', - optional: true, - }, - 'values.$.cost': { - type: 'fieldToCompute', - optional: true, - }, min: { type: 'fieldToCompute', optional: true, @@ -102,19 +90,6 @@ const ComputedOnlyPointBuySchema = createPropertySchema({ 'values.$': { type: Object, }, - 'values.$.min': { - type: 'computedOnlyField', - optional: true, - }, - 'values.$.max': { - type: 'computedOnlyField', - optional: true, - }, - 'values.$.cost': { - type: 'computedOnlyField', - optional: true, - parseLevel: 'compile', - }, 'values.$.spent': { type: Number, optional: true, diff --git a/app/imports/api/properties/Proficiencies.js b/app/imports/api/properties/Proficiencies.js index e79276ff..fe48b01b 100644 --- a/app/imports/api/properties/Proficiencies.js +++ b/app/imports/api/properties/Proficiencies.js @@ -1,5 +1,6 @@ import SimpleSchema from 'simpl-schema'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; +import TagTargetingSchema from '/imports/api/properties/subSchemas/TagTargetingSchema'; 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({}); diff --git a/app/imports/api/properties/Properties.type.ts b/app/imports/api/properties/Properties.type.ts new file mode 100644 index 00000000..e7aaa73f --- /dev/null +++ b/app/imports/api/properties/Properties.type.ts @@ -0,0 +1,12 @@ +import { TreeDoc } from '/imports/api/parenting/ChildSchema'; + +export default interface Property extends TreeDoc { + _id: string + _migrationError?: string + tags: string[] + icon?: { + name: string + shape: string + }, + slotQuantityFilled?: number +} \ No newline at end of file diff --git a/app/imports/api/properties/References.js b/app/imports/api/properties/References.js index 31ae8288..31459cdf 100644 --- a/app/imports/api/properties/References.js +++ b/app/imports/api/properties/References.js @@ -1,5 +1,5 @@ import SimpleSchema from 'simpl-schema'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; let ReferenceSchema = new SimpleSchema({ ref: { @@ -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, diff --git a/app/imports/api/properties/Rolls.js b/app/imports/api/properties/Rolls.js index 7a2c5eeb..4dcb5367 100644 --- a/app/imports/api/properties/Rolls.js +++ b/app/imports/api/properties/Rolls.js @@ -1,7 +1,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 VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema'; /** * Rolls are children to actions or other rolls, they are triggered with 0 or @@ -56,7 +56,7 @@ let ComputedOnlyRollSchema = createPropertySchema({ }, }); -const ComputedRollSchema = new SimpleSchema() +const ComputedRollSchema = new SimpleSchema({}) .extend(RollSchema) .extend(ComputedOnlyRollSchema); diff --git a/app/imports/api/properties/SavingThrows.js b/app/imports/api/properties/SavingThrows.js index d0045187..19442806 100644 --- a/app/imports/api/properties/SavingThrows.js +++ b/app/imports/api/properties/SavingThrows.js @@ -1,6 +1,6 @@ import SimpleSchema from 'simpl-schema'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; -import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema'; // These are the rolls made when saves are called for // For the saving throw bonus or proficiency, see ./Skills.js @@ -45,7 +45,7 @@ const ComputedOnlySavingThrowSchema = createPropertySchema({ }, }); -const ComputedSavingThrowSchema = new SimpleSchema() +const ComputedSavingThrowSchema = new SimpleSchema({}) .extend(SavingThrowSchema) .extend(ComputedOnlySavingThrowSchema); diff --git a/app/imports/api/properties/Skills.js b/app/imports/api/properties/Skills.js index a2083cdf..a9204f9a 100644 --- a/app/imports/api/properties/Skills.js +++ b/app/imports/api/properties/Skills.js @@ -1,7 +1,8 @@ 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 VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema'; +import TagTargetingSchema from '/imports/api/properties/subSchemas/TagTargetingSchema'; /* * 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 @@ -132,18 +134,43 @@ let ComputedOnlySkillSchema = createPropertySchema({ removeBeforeCompute: true, }, // A list of effect ids targeting this skill - effects: { + 'effectIds': { type: Array, optional: true, removeBeforeCompute: true, }, - 'effects.$': { + 'effectIds.$': { + type: String, + }, + 'proficiencyIds': { + type: Array, + optional: true, + removeBeforeCompute: true, + }, + 'proficiencyIds.$': { + type: String, + }, + 'definitions': { + type: Array, + optional: true, + removeBeforeCompute: true, + }, + 'definitions.$': { type: Object, - blackbox: true, + }, + 'definitions.$._id': { + type: String, + }, + 'definitions.$.type': { + type: String, + }, + 'definitions.$.row': { + type: Number, + optional: true, }, }) -const ComputedSkillSchema = new SimpleSchema() +const ComputedSkillSchema = new SimpleSchema({}) .extend(ComputedOnlySkillSchema) .extend(SkillSchema); diff --git a/app/imports/api/properties/SlotFillers.js b/app/imports/api/properties/SlotFillers.js deleted file mode 100644 index 185e7ba2..00000000 --- a/app/imports/api/properties/SlotFillers.js +++ /dev/null @@ -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 }; diff --git a/app/imports/api/properties/Slots.js b/app/imports/api/properties/Slots.js index 310ba198..fd41c36c 100644 --- a/app/imports/api/properties/Slots.js +++ b/app/imports/api/properties/Slots.js @@ -1,6 +1,6 @@ import SimpleSchema from 'simpl-schema'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; -import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema'; let SlotSchema = createPropertySchema({ name: { @@ -37,7 +37,7 @@ let SlotSchema = createPropertySchema({ 'extraTags.$._id': { type: String, regEx: SimpleSchema.RegEx.Id, - autoValue(){ + autoValue() { if (!this.isSet) return Random.id(); } }, diff --git a/app/imports/api/properties/SpellLists.js b/app/imports/api/properties/SpellLists.js index 285832eb..e99c6ff5 100644 --- a/app/imports/api/properties/SpellLists.js +++ b/app/imports/api/properties/SpellLists.js @@ -1,6 +1,6 @@ import SimpleSchema from 'simpl-schema'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; -import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema'; let SpellListSchema = createPropertySchema({ name: { diff --git a/app/imports/api/properties/Spells.js b/app/imports/api/properties/Spells.ts similarity index 68% rename from app/imports/api/properties/Spells.js rename to app/imports/api/properties/Spells.ts index 575c9b5e..1822aab6 100644 --- a/app/imports/api/properties/Spells.js +++ b/app/imports/api/properties/Spells.ts @@ -1,6 +1,30 @@ -import { ActionSchema, ComputedOnlyActionSchema } from '/imports/api/properties/Actions.js'; +import { ActionBase, ActionSchema, ComputedOnlyActionSchema } from '/imports/api/properties/Actions'; import SimpleSchema from 'simpl-schema'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; +import { CreatureProperty } from '/imports/api/creature/creatureProperties/CreatureProperties'; + +export interface Spell extends ActionBase { + name?: string + type: 'spell' + alwaysPrepared?: boolean + prepared?: boolean + castWithoutSpellSlots?: boolean + hasAttackRoll?: boolean + castingTime?: string + range?: string + duration?: string + verbal?: boolean + somatic?: boolean + concentration?: boolean + material?: string + ritual?: boolean + level?: number + school?: string +} + +export function isSpell(prop: CreatureProperty): prop is Spell { + return prop.type === 'spell'; +} const magicSchools = [ 'abjuration', @@ -13,7 +37,7 @@ const magicSchools = [ 'transmutation', ]; -let SpellSchema = new SimpleSchema({}) +const SpellSchema = new SimpleSchema({}) .extend(ActionSchema) .extend({ name: { @@ -91,10 +115,10 @@ let SpellSchema = new SimpleSchema({}) }, }); -const ComputedOnlySpellSchema = new SimpleSchema() +const ComputedOnlySpellSchema = new SimpleSchema({}) .extend(ComputedOnlyActionSchema); -const ComputedSpellSchema = new SimpleSchema() +const ComputedSpellSchema = new SimpleSchema({}) .extend(SpellSchema) .extend(ComputedOnlySpellSchema); diff --git a/app/imports/api/properties/Toggles.js b/app/imports/api/properties/Toggles.js index 755ed89b..398cf2f6 100644 --- a/app/imports/api/properties/Toggles.js +++ b/app/imports/api/properties/Toggles.js @@ -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 STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema'; +import TagTargetingSchema from '/imports/api/properties/subSchemas/TagTargetingSchema'; const ToggleSchema = createPropertySchema({ name: { @@ -31,7 +32,12 @@ const ToggleSchema = createPropertySchema({ type: 'fieldToCompute', optional: true, }, -}); + // Prevent the property from showing up in the log + silent: { + type: Boolean, + optional: true, + }, +}).extend(TagTargetingSchema); const ComputedOnlyToggleSchema = createPropertySchema({ condition: { @@ -40,7 +46,7 @@ const ComputedOnlyToggleSchema = createPropertySchema({ }, }); -const ComputedToggleSchema = new SimpleSchema() +const ComputedToggleSchema = new SimpleSchema({}) .extend(ComputedOnlyToggleSchema) .extend(ToggleSchema); diff --git a/app/imports/api/properties/Triggers.js b/app/imports/api/properties/Triggers.js index 0bfd7d9d..f2bfa797 100644 --- a/app/imports/api/properties/Triggers.js +++ b/app/imports/api/properties/Triggers.js @@ -1,6 +1,6 @@ import SimpleSchema from 'simpl-schema'; -import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; const eventOptions = { doActionProperty: 'Do action', @@ -18,10 +18,12 @@ const eventOptions = { const timingOptions = { before: 'Before', after: 'After', + afterChildren: 'After Children', } const actionPropertyTypeOptions = { action: 'Action', + ammo: 'Ammo used', adjustment: 'Attribute damage', branch: 'Branch', buff: 'Buff', @@ -91,7 +93,7 @@ let TriggerSchema = createPropertySchema({ 'extraTags.$._id': { type: String, regEx: SimpleSchema.RegEx.Id, - autoValue(){ + autoValue() { if (!this.isSet) return Random.id(); } }, @@ -132,7 +134,7 @@ const ComputedOnlyTriggerSchema = createPropertySchema({ }, }); -const ComputedTriggerSchema = new SimpleSchema() +const ComputedTriggerSchema = new SimpleSchema({}) .extend(TriggerSchema) .extend(ComputedOnlyTriggerSchema); diff --git a/app/imports/api/properties/computedOnlyPropertySchemasIndex.js b/app/imports/api/properties/computedOnlyPropertySchemasIndex.js index 93f3793a..3017d84a 100644 --- a/app/imports/api/properties/computedOnlyPropertySchemasIndex.js +++ b/app/imports/api/properties/computedOnlyPropertySchemasIndex.js @@ -1,45 +1,46 @@ import SimpleSchema from 'simpl-schema'; -import { ComputedOnlyActionSchema } from '/imports/api/properties/Actions.js'; -import { ComputedOnlyAdjustmentSchema } from '/imports/api/properties/Adjustments.js'; -import { ComputedOnlyAttributeSchema } from '/imports/api/properties/Attributes.js'; -import { ComputedOnlyBuffSchema } from '/imports/api/properties/Buffs.js'; -import { ComputedOnlyBuffRemoverSchema } from '/imports/api/properties/BuffRemovers.js'; -import { ComputedOnlyBranchSchema } from '/imports/api/properties/Branches.js'; -import { ComputedOnlyClassSchema } from '/imports/api/properties/Classes.js'; -import { ComputedOnlyClassLevelSchema } from '/imports/api/properties/ClassLevels.js'; -import { ComputedOnlyConstantSchema } from '/imports/api/properties/Constants.js'; -import { ComputedOnlyContainerSchema } from '/imports/api/properties/Containers.js'; -import { ComputedOnlyDamageSchema } from '/imports/api/properties/Damages.js'; -import { ComputedOnlyDamageMultiplierSchema } from '/imports/api/properties/DamageMultipliers.js'; -import { ComputedOnlyEffectSchema } from '/imports/api/properties/Effects.js'; -import { ComputedOnlyFeatureSchema } from '/imports/api/properties/Features.js'; -import { ComputedOnlyFolderSchema } from '/imports/api/properties/Folders.js'; -import { ComputedOnlyItemSchema } from '/imports/api/properties/Items.js'; -import { ComputedOnlyNoteSchema } from '/imports/api/properties/Notes.js'; -import { ComputedOnlyPointBuySchema } from '/imports/api/properties/PointBuys.js'; -import { ComputedOnlyProficiencySchema } from '/imports/api/properties/Proficiencies.js'; -import { ComputedOnlyReferenceSchema } from '/imports/api/properties/References.js'; -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'; -import { ComputedOnlyTriggerSchema } from '/imports/api/properties/Triggers.js'; +import { ComputedOnlyActionSchema } from '/imports/api/properties/Actions'; +import { ComputedOnlyAdjustmentSchema } from '/imports/api/properties/Adjustments'; +import { ComputedOnlyAttributeSchema } from '/imports/api/properties/Attributes'; +import { ComputedOnlyBranchSchema } from '/imports/api/properties/Branches'; +import { ComputedOnlyBuffRemoverSchema } from '/imports/api/properties/BuffRemovers'; +import { ComputedOnlyBuffSchema } from '/imports/api/properties/Buffs'; +import { ComputedOnlyClassLevelSchema } from '/imports/api/properties/ClassLevels'; +import { ComputedOnlyClassSchema } from '/imports/api/properties/Classes'; +import { ComputedOnlyConstantSchema } from '/imports/api/properties/Constants'; +import { ComputedOnlyContainerSchema } from '/imports/api/properties/Containers'; +import { ComputedOnlyCreatureTemplateSchema } from '/imports/api/properties/CreatureTemplates'; +import { ComputedOnlyDamageMultiplierSchema } from '/imports/api/properties/DamageMultipliers'; +import { ComputedOnlyDamageSchema } from '/imports/api/properties/Damages'; +import { ComputedOnlyEffectSchema } from '/imports/api/properties/Effects'; +import { ComputedOnlyFeatureSchema } from '/imports/api/properties/Features'; +import { ComputedOnlyFolderSchema } from '/imports/api/properties/Folders'; +import { ComputedOnlyItemSchema } from '/imports/api/properties/Items'; +import { ComputedOnlyNoteSchema } from '/imports/api/properties/Notes'; +import { ComputedOnlyPointBuySchema } from '/imports/api/properties/PointBuys'; +import { ComputedOnlyProficiencySchema } from '/imports/api/properties/Proficiencies'; +import { ComputedOnlyReferenceSchema } from '/imports/api/properties/References'; +import { ComputedOnlyRollSchema } from '/imports/api/properties/Rolls'; +import { ComputedOnlySavingThrowSchema } from '/imports/api/properties/SavingThrows'; +import { ComputedOnlySkillSchema } from '/imports/api/properties/Skills'; +import { ComputedOnlySlotSchema } from '/imports/api/properties/Slots'; +import { ComputedOnlySpellListSchema } from '/imports/api/properties/SpellLists'; +import { ComputedOnlySpellSchema } from '/imports/api/properties/Spells'; +import { ComputedOnlyToggleSchema } from '/imports/api/properties/Toggles'; +import { ComputedOnlyTriggerSchema } from '/imports/api/properties/Triggers'; const propertySchemasIndex = { action: ComputedOnlyActionSchema, adjustment: ComputedOnlyAdjustmentSchema, attribute: ComputedOnlyAttributeSchema, + branch: ComputedOnlyBranchSchema, buff: ComputedOnlyBuffSchema, buffRemover: ComputedOnlyBuffRemoverSchema, - branch: ComputedOnlyBranchSchema, class: ComputedOnlyClassSchema, classLevel: ComputedOnlyClassLevelSchema, constant: ComputedOnlyConstantSchema, container: ComputedOnlyContainerSchema, + creature: ComputedOnlyCreatureTemplateSchema, damage: ComputedOnlyDamageSchema, damageMultiplier: ComputedOnlyDamageMultiplierSchema, effect: ComputedOnlyEffectSchema, @@ -54,9 +55,8 @@ const propertySchemasIndex = { roll: ComputedOnlyRollSchema, savingThrow: ComputedOnlySavingThrowSchema, skill: ComputedOnlySkillSchema, - slotFiller: ComputedOnlySlotFillerSchema, - spellList: ComputedOnlySpellListSchema, spell: ComputedOnlySpellSchema, + spellList: ComputedOnlySpellListSchema, toggle: ComputedOnlyToggleSchema, trigger: ComputedOnlyTriggerSchema, any: new SimpleSchema({}), diff --git a/app/imports/api/properties/computedPropertySchemasIndex.js b/app/imports/api/properties/computedPropertySchemasIndex.js index 7bdfb302..86fba824 100644 --- a/app/imports/api/properties/computedPropertySchemasIndex.js +++ b/app/imports/api/properties/computedPropertySchemasIndex.js @@ -1,49 +1,53 @@ +import '/imports/api/simpleSchemaConfig'; import SimpleSchema from 'simpl-schema'; -import { ComputedActionSchema } from '/imports/api/properties/Actions.js'; -import { ComputedAdjustmentSchema } from '/imports/api/properties/Adjustments.js'; -import { ComputedAttributeSchema } from '/imports/api/properties/Attributes.js'; -import { ComputedBuffSchema } from '/imports/api/properties/Buffs.js'; -import { ComputedBuffRemoverSchema } from '/imports/api/properties/BuffRemovers.js'; -import { ComputedBranchSchema } from '/imports/api/properties/Branches.js'; -import { ComputedClassSchema } from '/imports/api/properties/Classes.js'; -import { ComputedClassLevelSchema } from '/imports/api/properties/ClassLevels.js'; -import { ConstantSchema } from '/imports/api/properties/Constants.js'; -import { ComputedContainerSchema } from '/imports/api/properties/Containers.js'; -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 { ComputedItemSchema } from '/imports/api/properties/Items.js'; -import { ComputedNoteSchema } from '/imports/api/properties/Notes.js'; -import { ComputedPointBuySchema } from '/imports/api/properties/PointBuys.js'; -import { ProficiencySchema } from '/imports/api/properties/Proficiencies.js'; -import { ReferenceSchema } from '/imports/api/properties/References.js'; -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'; -import { ComputedTriggerSchema } from '/imports/api/properties/Triggers.js'; +import { ComputedActionSchema } from '/imports/api/properties/Actions'; +import { ComputedAdjustmentSchema } from '/imports/api/properties/Adjustments'; +import { ComputedAttributeSchema } from '/imports/api/properties/Attributes'; +import { ComputedBuffSchema } from '/imports/api/properties/Buffs'; +import { ComputedBuffRemoverSchema } from '/imports/api/properties/BuffRemovers'; +import { ComputedBranchSchema } from '/imports/api/properties/Branches'; +import { ComputedClassSchema } from '/imports/api/properties/Classes'; +import { ComputedClassLevelSchema } from '/imports/api/properties/ClassLevels'; +import { ConstantSchema } from '/imports/api/properties/Constants'; +import { ComputedContainerSchema } from '/imports/api/properties/Containers'; +import { ComputedCreatureTemplateSchema } from '/imports/api/properties/CreatureTemplates'; +import { ComputedDamageSchema } from '/imports/api/properties/Damages'; +import { DamageMultiplierSchema } from '/imports/api/properties/DamageMultipliers'; +import { ComputedEffectSchema } from '/imports/api/properties/Effects'; +import { ComputedFeatureSchema } from '/imports/api/properties/Features'; +import { ComputedFolderSchema } from '/imports/api/properties/Folders'; +import { ComputedItemSchema } from '/imports/api/properties/Items'; +import { ComputedNoteSchema } from '/imports/api/properties/Notes'; +import { ComputedPointBuySchema } from '/imports/api/properties/PointBuys'; +import { ProficiencySchema } from '/imports/api/properties/Proficiencies'; +import { ReferenceSchema } from '/imports/api/properties/References'; +import { ComputedRollSchema } from '/imports/api/properties/Rolls'; +import { ComputedSavingThrowSchema } from '/imports/api/properties/SavingThrows'; +import { ComputedSkillSchema } from '/imports/api/properties/Skills'; +import { ComputedSlotSchema } from '/imports/api/properties/Slots'; +import { ComputedSpellSchema } from '/imports/api/properties/Spells'; +import { ComputedSpellListSchema } from '/imports/api/properties/SpellLists'; +import { ComputedToggleSchema } from '/imports/api/properties/Toggles'; +import { ComputedTriggerSchema } from '/imports/api/properties/Triggers'; const propertySchemasIndex = { action: ComputedActionSchema, adjustment: ComputedAdjustmentSchema, attribute: ComputedAttributeSchema, + branch: ComputedBranchSchema, buff: ComputedBuffSchema, buffRemover: ComputedBuffRemoverSchema, - branch: ComputedBranchSchema, class: ComputedClassSchema, classLevel: ComputedClassLevelSchema, constant: ConstantSchema, + container: ComputedContainerSchema, + creature: ComputedCreatureTemplateSchema, damage: ComputedDamageSchema, damageMultiplier: DamageMultiplierSchema, effect: ComputedEffectSchema, feature: ComputedFeatureSchema, - folder: FolderSchema, + folder: ComputedFolderSchema, + item: ComputedItemSchema, note: ComputedNoteSchema, pointBuy: ComputedPointBuySchema, proficiency: ProficiencySchema, @@ -52,13 +56,10 @@ const propertySchemasIndex = { roll: ComputedRollSchema, savingThrow: ComputedSavingThrowSchema, skill: ComputedSkillSchema, - slotFiller: SlotFillerSchema, - spellList: ComputedSpellListSchema, spell: ComputedSpellSchema, + spellList: ComputedSpellListSchema, toggle: ComputedToggleSchema, trigger: ComputedTriggerSchema, - container: ComputedContainerSchema, - item: ComputedItemSchema, any: new SimpleSchema({}), }; diff --git a/app/imports/api/properties/propertySchemasIndex.js b/app/imports/api/properties/propertySchemasIndex.js index fdccedc9..4e022b63 100644 --- a/app/imports/api/properties/propertySchemasIndex.js +++ b/app/imports/api/properties/propertySchemasIndex.js @@ -1,49 +1,52 @@ import SimpleSchema from 'simpl-schema'; -import { ActionSchema } from '/imports/api/properties/Actions.js'; -import { AdjustmentSchema } from '/imports/api/properties/Adjustments.js'; -import { AttributeSchema } from '/imports/api/properties/Attributes.js'; -import { BuffSchema } from '/imports/api/properties/Buffs.js'; -import { BuffRemoverSchema } from '/imports/api/properties/BuffRemovers.js'; -import { BranchSchema } from '/imports/api/properties/Branches.js'; -import { ClassSchema } from '/imports/api/properties/Classes.js'; -import { ClassLevelSchema } from '/imports/api/properties/ClassLevels.js'; -import { ConstantSchema } from '/imports/api/properties/Constants.js'; -import { DamageSchema } from '/imports/api/properties/Damages.js'; -import { DamageMultiplierSchema } from '/imports/api/properties/DamageMultipliers.js'; -import { EffectSchema } from '/imports/api/properties/Effects.js'; -import { FeatureSchema } from '/imports/api/properties/Features.js'; -import { FolderSchema } from '/imports/api/properties/Folders.js'; -import { NoteSchema } from '/imports/api/properties/Notes.js'; -import { PointBuySchema } from '/imports/api/properties/PointBuys.js'; -import { ProficiencySchema } from '/imports/api/properties/Proficiencies.js'; -import { ReferenceSchema } from '/imports/api/properties/References.js'; -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'; -import { TriggerSchema } from '/imports/api/properties/Triggers.js'; -import { ContainerSchema } from '/imports/api/properties/Containers.js'; -import { ItemSchema } from '/imports/api/properties/Items.js'; +import { ActionSchema } from '/imports/api/properties/Actions'; +import { AdjustmentSchema } from '/imports/api/properties/Adjustments'; +import { AttributeSchema } from '/imports/api/properties/Attributes'; +import { BranchSchema } from '/imports/api/properties/Branches'; +import { BuffRemoverSchema } from '/imports/api/properties/BuffRemovers'; +import { BuffSchema } from '/imports/api/properties/Buffs'; +import { ClassLevelSchema } from '/imports/api/properties/ClassLevels'; +import { ClassSchema } from '/imports/api/properties/Classes'; +import { ConstantSchema } from '/imports/api/properties/Constants'; +import { ContainerSchema } from '/imports/api/properties/Containers'; +import { CreatureTemplateSchema } from '/imports/api/properties/CreatureTemplates'; +import { DamageMultiplierSchema } from '/imports/api/properties/DamageMultipliers'; +import { DamageSchema } from '/imports/api/properties/Damages'; +import { EffectSchema } from '/imports/api/properties/Effects'; +import { FeatureSchema } from '/imports/api/properties/Features'; +import { FolderSchema } from '/imports/api/properties/Folders'; +import { ItemSchema } from '/imports/api/properties/Items'; +import { NoteSchema } from '/imports/api/properties/Notes'; +import { PointBuySchema } from '/imports/api/properties/PointBuys'; +import { ProficiencySchema } from '/imports/api/properties/Proficiencies'; +import { ReferenceSchema } from '/imports/api/properties/References'; +import { RollSchema } from '/imports/api/properties/Rolls'; +import { SavingThrowSchema } from '/imports/api/properties/SavingThrows'; +import { SkillSchema } from '/imports/api/properties/Skills'; +import { SlotSchema } from '/imports/api/properties/Slots'; +import { SpellListSchema } from '/imports/api/properties/SpellLists'; +import { SpellSchema } from '/imports/api/properties/Spells'; +import { ToggleSchema } from '/imports/api/properties/Toggles'; +import { TriggerSchema } from '/imports/api/properties/Triggers'; const propertySchemasIndex = { action: ActionSchema, adjustment: AdjustmentSchema, attribute: AttributeSchema, + branch: BranchSchema, buff: BuffSchema, buffRemover: BuffRemoverSchema, - branch: BranchSchema, class: ClassSchema, classLevel: ClassLevelSchema, constant: ConstantSchema, + container: ContainerSchema, + creature: CreatureTemplateSchema, damage: DamageSchema, damageMultiplier: DamageMultiplierSchema, effect: EffectSchema, feature: FeatureSchema, folder: FolderSchema, + item: ItemSchema, note: NoteSchema, pointBuy: PointBuySchema, proficiency: ProficiencySchema, @@ -52,13 +55,10 @@ const propertySchemasIndex = { roll: RollSchema, savingThrow: SavingThrowSchema, skill: SkillSchema, - slotFiller: SlotFillerSchema, - spellList: SpellListSchema, spell: SpellSchema, + spellList: SpellListSchema, toggle: ToggleSchema, trigger: TriggerSchema, - container: ContainerSchema, - item: ItemSchema, any: new SimpleSchema({}), }; diff --git a/app/imports/api/properties/subSchemas/ColorSchema.js b/app/imports/api/properties/subSchemas/ColorSchema.ts similarity index 84% rename from app/imports/api/properties/subSchemas/ColorSchema.js rename to app/imports/api/properties/subSchemas/ColorSchema.ts index 0597b336..34fd807f 100644 --- a/app/imports/api/properties/subSchemas/ColorSchema.js +++ b/app/imports/api/properties/subSchemas/ColorSchema.ts @@ -1,5 +1,9 @@ import SimpleSchema from 'simpl-schema'; +export interface Colored { + color?: string, +} + const ColorSchema = new SimpleSchema({ color: { type: String, diff --git a/app/imports/api/properties/subSchemas/DeathSavesSchema.js b/app/imports/api/properties/subSchemas/DeathSavesSchema.js deleted file mode 100644 index b6238544..00000000 --- a/app/imports/api/properties/subSchemas/DeathSavesSchema.js +++ /dev/null @@ -1,26 +0,0 @@ -import SimpleSchema from 'simpl-schema'; - -const DeathSavesSchema = new SimpleSchema({ - pass: { - type: SimpleSchema.Integer, - min: 0, - max: 3, - defaultValue: 0, - }, - fail: { - type: SimpleSchema.Integer, - min: 0, - max: 3, - defaultValue: 0, - }, - canDeathSave: { - type: Boolean, - defaultValue: true, - }, - stable: { - type: Boolean, - defaultValue: false, - }, -}); - -export default DeathSavesSchema; diff --git a/app/imports/api/properties/subSchemas/ErrorSchema.js b/app/imports/api/properties/subSchemas/ErrorSchema.js index 41635874..636281e6 100644 --- a/app/imports/api/properties/subSchemas/ErrorSchema.js +++ b/app/imports/api/properties/subSchemas/ErrorSchema.js @@ -1,5 +1,5 @@ import SimpleSchema from 'simpl-schema'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; const ErrorSchema = new SimpleSchema({ message: { diff --git a/app/imports/api/properties/subSchemas/InlineComputationSchema.js b/app/imports/api/properties/subSchemas/InlineComputationSchema.js index 14a33e75..a52120c2 100644 --- a/app/imports/api/properties/subSchemas/InlineComputationSchema.js +++ b/app/imports/api/properties/subSchemas/InlineComputationSchema.js @@ -1,6 +1,6 @@ import SimpleSchema from 'simpl-schema'; -import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; +import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; const InlineComputationSchema = new SimpleSchema({ // The part between bracers {} diff --git a/app/imports/api/properties/subSchemas/RollDetailsSchema.js b/app/imports/api/properties/subSchemas/RollDetailsSchema.js index 44f39e5e..9ea3f279 100644 --- a/app/imports/api/properties/subSchemas/RollDetailsSchema.js +++ b/app/imports/api/properties/subSchemas/RollDetailsSchema.js @@ -1,5 +1,5 @@ import SimpleSchema from 'simpl-schema'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; const RollDetailsSchema = new SimpleSchema({ number: { diff --git a/app/imports/api/properties/subSchemas/RollResultsSchema.js b/app/imports/api/properties/subSchemas/RollResultsSchema.js index 8aea32e9..b15746f3 100644 --- a/app/imports/api/properties/subSchemas/RollResultsSchema.js +++ b/app/imports/api/properties/subSchemas/RollResultsSchema.js @@ -1,5 +1,5 @@ import SimpleSchema from 'simpl-schema'; -import ResultsSchema from '/imports/api/properties/subSchemas/ResultsSchema.js'; +import ResultsSchema from '/imports/api/properties/subSchemas/ResultsSchema'; let RollResultsSchema = new SimpleSchema({ _id: { diff --git a/app/imports/api/properties/subSchemas/TagTargetingSchema.js b/app/imports/api/properties/subSchemas/TagTargetingSchema.js new file mode 100644 index 00000000..ea33d758 --- /dev/null +++ b/app/imports/api/properties/subSchemas/TagTargetingSchema.js @@ -0,0 +1,57 @@ +import SimpleSchema from 'simpl-schema'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; + +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; diff --git a/app/imports/api/properties/subSchemas/computedField.js b/app/imports/api/properties/subSchemas/computedField.ts similarity index 53% rename from app/imports/api/properties/subSchemas/computedField.js rename to app/imports/api/properties/subSchemas/computedField.ts index 0d7ad114..515613e1 100644 --- a/app/imports/api/properties/subSchemas/computedField.js +++ b/app/imports/api/properties/subSchemas/computedField.ts @@ -1,10 +1,25 @@ import SimpleSchema from 'simpl-schema'; -import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; +import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; +import ParseNode from '/imports/parser/parseTree/ParseNode'; +import { ConstantValueType } from '/imports/parser/parseTree/constant'; + +export interface CalculatedField { + calculation?: string; + value?: ConstantValueType; + valueNode: ParseNode; + effectIds?: string[]; + proficiencyIds?: string[]; + unaffected?: ConstantValueType; + parseNode?: ParseNode; + parseError?: any; + hash?: number; + errors?: any[]; +} // Get schemas that apply fields directly so they can be gracefully extended // because {type: Schema} fields can't be extended -function fieldToCompute(field){ +function fieldToCompute(field) { const schemaObj = { [`${field}.calculation`]: { type: String, @@ -17,22 +32,42 @@ function fieldToCompute(field){ return new SimpleSchema(schemaObj); } -function computedOnlyField(field){ +function computedOnlyField(field) { const schemaObj = { + // The value (or calculation string) before any effects/proficiencies are applied or rolls made + [`${field}.unaffected`]: { + type: SimpleSchema.oneOf(String, Number), + optional: true, + blackbox: true, + }, + // The value (or calculation string) after applying all effects [`${field}.value`]: { type: SimpleSchema.oneOf(String, Number), optional: true, - removeBeforeCompute: true, + blackbox: true, }, - // A list of effects targeting this calculation - [`${field}.effects`]: { + // The value as a parse node, after applying all effects + [`${field}.valueNode`]: { + type: SimpleSchema.oneOf(String, Number), + optional: true, + blackbox: true, + }, + // A list of effect Ids targeting this calculation + [`${field}.effectIds`]: { type: Array, optional: true, removeBeforeCompute: true, }, - [`${field}.effects.$`]: { - type: Object, - blackbox: true, + [`${field}.effectIds.$`]: { + type: String, + }, + [`${field}.proficiencyIds`]: { + type: Array, + optional: true, + removeBeforeCompute: true, + }, + [`${field}.proficiencyIds.$`]: { + type: String, }, // A cache of the parse result of the calculation [`${field}.parseNode`]: { @@ -56,18 +91,42 @@ function computedOnlyField(field){ maxCount: STORAGE_LIMITS.errorCount, removeBeforeCompute: true, }, - [`${field}.errors.$`]:{ + [`${field}.errors.$`]: { type: ErrorSchema, }, + // Effect aggregations + [`${field}.advantage`]: { + type: Number, + optional: true, + removeBeforeCompute: true, + }, + [`${field}.disadvantage`]: { + type: Number, + optional: true, + removeBeforeCompute: true, + }, + [`${field}.fail`]: { + type: Number, + optional: true, + removeBeforeCompute: true, + }, + [`${field}.conditional`]: { + type: Array, + optional: true, + removeBeforeCompute: true, + }, + [`${field}.conditional.$`]: { + type: String, + }, } includeParentFields(field, schemaObj); return new SimpleSchema(schemaObj); } // We must include parent array and object fields for the schema to be valid -function includeParentFields(field, schemaObj){ +function includeParentFields(field, schemaObj) { const splitField = field.split('.'); - if (splitField.length === 1){ + if (splitField.length === 1) { schemaObj[field] = { type: Object, optional: true, @@ -78,8 +137,8 @@ function includeParentFields(field, schemaObj){ let key = ''; splitField.push(''); splitField.forEach((value, index) => { - if (key){ - if (value === '$'){ + if (key) { + if (value === '$') { schemaObj[key] = { type: Array, optional: true @@ -90,7 +149,7 @@ function includeParentFields(field, schemaObj){ optional: true, }; // the last object is the computed field - if (index === splitField.length - 1){ + if (index === splitField.length - 1) { schemaObj[key].computedField = true; } } @@ -102,7 +161,7 @@ function includeParentFields(field, schemaObj){ // This should rarely be used, since the other two will merge correctly when // uncomputed and computedOnly schemas are merged -function computedField(field){ +function computedField(field) { return computedField(field).extend(computedOnlyField(field)); } diff --git a/app/imports/api/properties/subSchemas/createPropertySchema.js b/app/imports/api/properties/subSchemas/createPropertySchema.js index a866295c..2d2d6b11 100644 --- a/app/imports/api/properties/subSchemas/createPropertySchema.js +++ b/app/imports/api/properties/subSchemas/createPropertySchema.js @@ -1,17 +1,17 @@ import { inlineCalculationFieldToCompute, computedOnlyInlineCalculationField, -} from '/imports/api/properties/subSchemas/inlineCalculationField.js'; +} from '/imports/api/properties/subSchemas/inlineCalculationField'; import { fieldToCompute, computedOnlyField, -} from '/imports/api/properties/subSchemas/computedField.js'; +} from '/imports/api/properties/subSchemas/computedField'; import SimpleSchema from 'simpl-schema'; // Search through the schema for keys whose type is 'fieldToCompute' etc. // replace the type with Object and attach extend the schema with // the required fields to make the computation work -export default function createPropertySchema(definition){ +export default function createPropertySchema(definition) { const computationFields = { inlineCalculationFieldToCompute: [], computedOnlyInlineCalculationField: [], @@ -20,18 +20,18 @@ export default function createPropertySchema(definition){ }; const computedKeys = Object.keys(computationFields); - for (let key in definition){ + for (let key in definition) { const def = definition[key]; - if (computedKeys.includes(def.type)){ + if (computedKeys.includes(def.type)) { computationFields[def.type].push(key); applyDefaultCalculationValue(definition, key); def.type = Object; - if (!def.optional){ + if (!def.optional) { console.warn( `computed field: '${key}' of '${def.type}' is expected to be optional` ); } - if (def.removeBeforeCompute){ + if (def.removeBeforeCompute) { console.warn( `computed field: '${key}' of '${def.type}' should not be removed before computation` ) @@ -58,12 +58,12 @@ export default function createPropertySchema(definition){ return schema } -function applyDefaultCalculationValue(definition, key){ +function applyDefaultCalculationValue(definition, key) { const def = definition[key]; if ( def.type === 'computedOnlyField' || def.type === 'computedOnlyInlineCalculationField' - ){ + ) { // don't apply defaults to computed only fields // because it would add the calculation field which should only appear // on the fields to compute @@ -72,12 +72,12 @@ function applyDefaultCalculationValue(definition, key){ let defaultValue = def.defaultValue; if (!defaultValue) return; let calcField; - if (def.type === 'fieldToCompute'){ + if (def.type === 'fieldToCompute') { calcField = key + '.calculation' } else { calcField = key + '.text' } - if (definition[calcField]){ + if (definition[calcField]) { definition[calcField].defaultValue = defaultValue; } else { definition[calcField] = { diff --git a/app/imports/api/properties/subSchemas/inlineCalculationField.js b/app/imports/api/properties/subSchemas/inlineCalculationField.ts similarity index 86% rename from app/imports/api/properties/subSchemas/inlineCalculationField.js rename to app/imports/api/properties/subSchemas/inlineCalculationField.ts index 22e37654..c50ea24d 100644 --- a/app/imports/api/properties/subSchemas/inlineCalculationField.js +++ b/app/imports/api/properties/subSchemas/inlineCalculationField.ts @@ -1,10 +1,18 @@ import SimpleSchema from 'simpl-schema'; -import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; +import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; +import { CalculatedField } from './computedField'; + +export interface InlineCalculation { + text?: string, + hash?: number, + value?: string, + inlineCalculations: CalculatedField[], +} // Get schemas that apply fields directly so they can be gracefully extended // because {type: Schema} fields can't be extended -function inlineCalculationFieldToCompute(field){ +function inlineCalculationFieldToCompute(field) { return new SimpleSchema({ // The object should already be set, but set again just in case [field]: { @@ -20,7 +28,7 @@ function inlineCalculationFieldToCompute(field){ }); } -function computedOnlyInlineCalculationField(field){ +function computedOnlyInlineCalculationField(field) { return new SimpleSchema({ // The object should already be set, but set again just in case [field]: { @@ -30,9 +38,8 @@ function computedOnlyInlineCalculationField(field){ }, // a hash of the text to see if the current cached values need to be updated [`${field}.hash`]: { - type: String, + type: Number, optional: true, - max: STORAGE_LIMITS.inlineCalculationField, }, [`${field}.value`]: { type: String, @@ -90,7 +97,7 @@ function computedOnlyInlineCalculationField(field){ }); } -function computedInlineCalculationField(field){ +function computedInlineCalculationField(field) { return inlineCalculationFieldToCompute(field).extend( computedOnlyInlineCalculationField(field) ) diff --git a/app/imports/api/properties/tests/propTestBuilder.testFn.js b/app/imports/api/properties/tests/propTestBuilder.testFn.js new file mode 100644 index 00000000..4d17e677 --- /dev/null +++ b/app/imports/api/properties/tests/propTestBuilder.testFn.js @@ -0,0 +1,48 @@ +import { applyNestedSetProperties } from '/imports/api/parenting/parentingFunctions'; + +/** + * Take a forest of props, which can have sub-props nested in children: [], and return a list of + * clean props with correct tree and ancestry data + * @param props + * @returns + */ +export function propsFromForest( + props, + creatureId = Random.id(), + parentId = undefined, + recursionDepth = 0 +) { + const result = []; + props.forEach(prop => { + const children = prop.children; + // Check the property has a type + if (!prop.type) { + throw new Error('Type is required on every property, not found on doc: ' + JSON.stringify(prop, null, 2)); + } + // Create the clean doc + const doc = { + ...prop, + left: result.length, + root: { id: creatureId, collection: 'creatures' }, + }; + if (parentId) { + doc.parentId = parentId; + } + if (!doc._id) { + doc._id = Random.id(); + } + delete doc.children; + + // Add the doc to the result and ancestry + result.push(doc); + if (children) { + result.push(...propsFromForest(children, creatureId, doc._id, recursionDepth + 1)); + } + }); + + // Apply the nested set properties on the top level + if (recursionDepth === 0) { + applyNestedSetProperties(result); + } + return result; +} diff --git a/app/imports/api/sharing/SharingSchema.js b/app/imports/api/sharing/SharingSchema.ts similarity index 62% rename from app/imports/api/sharing/SharingSchema.js rename to app/imports/api/sharing/SharingSchema.ts index 629f90bb..e33413ed 100644 --- a/app/imports/api/sharing/SharingSchema.js +++ b/app/imports/api/sharing/SharingSchema.ts @@ -1,16 +1,25 @@ import SimpleSchema from 'simpl-schema'; -import '/imports/api/sharing/sharing.js'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; -let SharingSchema = new SimpleSchema({ +export interface Shared { + owner: string, + readers: string[], + writers: string[], + public: boolean, + readersCanCopy?: true, +} + +const SharingSchema = new SimpleSchema({ owner: { type: String, regEx: SimpleSchema.RegEx.Id, + //@ts-expect-error index not defined index: 1 }, readers: { type: Array, defaultValue: [], + //@ts-expect-error index not defined index: 1, maxCount: STORAGE_LIMITS.readersCount, }, @@ -21,6 +30,7 @@ let SharingSchema = new SimpleSchema({ writers: { type: Array, defaultValue: [], + //@ts-expect-error index not defined index: 1, maxCount: STORAGE_LIMITS.writersCount, }, @@ -31,6 +41,7 @@ let SharingSchema = new SimpleSchema({ public: { type: Boolean, defaultValue: false, + //@ts-expect-error index not defined index: 1, }, readersCanCopy: { diff --git a/app/imports/api/sharing/sharing.js b/app/imports/api/sharing/sharing.js index 7062ff40..89222dbf 100644 --- a/app/imports/api/sharing/sharing.js +++ b/app/imports/api/sharing/sharing.js @@ -1,11 +1,10 @@ import SimpleSchema from 'simpl-schema'; -import { assertOwnership } from '/imports/api/sharing/sharingPermissions.js'; -import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js'; -import getCollectionByName from '/imports/api/parenting/getCollectionByName.js'; -import { RefSchema } from '/imports/api/parenting/ChildSchema.js'; +import { assertOwnership } from '/imports/api/sharing/sharingPermissions'; +import { getCollectionByName, fetchDocByRef } from '/imports/api/parenting/parentingFunctions'; +import { RefSchema } from '/imports/api/parenting/ChildSchema'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import { getUserTier } from '/imports/api/users/patreon/tiers.js'; +import { getUserTier } from '/imports/api/users/patreon/tiers'; const setPublic = new ValidatedMethod({ name: 'sharing.setPublic', diff --git a/app/imports/api/sharing/sharingPermissions.js b/app/imports/api/sharing/sharingPermissions.js index 5591c59c..8c600f92 100644 --- a/app/imports/api/sharing/sharingPermissions.js +++ b/app/imports/api/sharing/sharingPermissions.js @@ -1,5 +1,5 @@ -import { _ } from 'meteor/underscore'; -import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js'; +import { includes } from 'lodash'; +import { fetchDocByRef } from '/imports/api/parenting/parentingFunctions'; function assertIdValid(userId) { if (!userId || typeof userId !== 'string') { @@ -50,7 +50,7 @@ export function assertEditPermission(doc, userId) { // Ensure the user is authorized for this specific document if ( doc.owner === userId || - _.contains(doc.writers, userId) + includes(doc.writers, userId) ) { return true; } else { @@ -82,11 +82,11 @@ export function assertCopyPermission(doc, userId) { // Ensure the user is authorized for this specific document if ( doc.owner === userId || - _.contains(doc.writers, userId) + includes(doc.writers, userId) ) { return true; } else if ( - (_.contains(doc.readers, userId) || doc.public) && + (includes(doc.readers, userId) || doc.public) && doc.readersCanCopy ) { return true; @@ -98,8 +98,8 @@ export function assertCopyPermission(doc, userId) { function getRoot(doc) { assertdocExists(doc); - if (doc.ancestors && doc.ancestors.length && doc.ancestors[0]) { - return fetchDocByRef(doc.ancestors[0]); + if (doc.root) { + return fetchDocByRef(doc.root); } else { return doc; } @@ -134,8 +134,8 @@ export function assertViewPermission(doc, userId) { if ( doc.owner === userId || - _.contains(doc.readers, userId) || - _.contains(doc.writers, userId) + includes(doc.readers, userId) || + includes(doc.writers, userId) ) { return true; } else { diff --git a/app/imports/api/tabletop/Messages.js b/app/imports/api/tabletop/Messages.js index 634f32d9..07cd270a 100644 --- a/app/imports/api/tabletop/Messages.js +++ b/app/imports/api/tabletop/Messages.js @@ -1,8 +1,8 @@ import SimpleSchema from 'simpl-schema'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import Creatures from '/imports/api/creature/creatures/Creatures.js'; -import Tabletops, { assertUserInTabletop } from '/imports/api/tabletop/Tabletops.js'; +import Creatures from '/imports/api/creature/creatures/Creatures'; +import Tabletops, { assertUserInTabletop } from '/imports/api/tabletop/Tabletops'; let Messages = new Mongo.Collection('messages'); diff --git a/app/imports/api/tabletop/TabletopMaps.js b/app/imports/api/tabletop/TabletopMaps.js new file mode 100644 index 00000000..e315daf8 --- /dev/null +++ b/app/imports/api/tabletop/TabletopMaps.js @@ -0,0 +1,49 @@ +import SimpleSchema from 'simpl-schema'; +import ChildSchema from '/imports/api/parenting/ChildSchema'; + +let TabletopMaps = new Mongo.Collection('tabletopmaps'); + +let TabletopMapschema = new SimpleSchema({ + name: { + type: String, + optional: true, + }, + texture: { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, + position: { + type: Object, + optional: true, + }, + 'position.x': { + type: Number, + }, + 'position.y': { + type: Number, + }, + width: { + type: Number, + }, + height: { + type: Number, + }, + rotation: { + type: Number, + max: 360, + min: 0, + }, + // If this map was copied from a library map, this ID will be set + libraryMapId: { + type: String, + regEx: SimpleSchema.RegEx.Id, + optional: true, + }, +}); + +const schema = new SimpleSchema({}); +schema.extend(ChildSchema); +schema.extend(TabletopMapschema); +TabletopMaps.attachSchema(schema); + +export default TabletopMaps; diff --git a/app/imports/api/tabletop/TabletopObjects.js b/app/imports/api/tabletop/TabletopObjects.js new file mode 100644 index 00000000..907ef01c --- /dev/null +++ b/app/imports/api/tabletop/TabletopObjects.js @@ -0,0 +1,43 @@ +import SimpleSchema from 'simpl-schema'; +import ChildSchema from '/imports/api/parenting/ChildSchema'; + +let TabletopObjects = new Mongo.Collection('tabletopObjects'); + +let TabletopObjectSchema = new SimpleSchema({ + name: { + type: String, + optional: true, + }, + texture: { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, + position: { + type: Object, + optional: true, + }, + 'position.x': { + type: Number, + }, + 'position.y': { + type: Number, + }, + width: { + type: Number, + }, + height: { + type: Number, + }, + rotation: { + type: Number, + max: 360, + min: 0, + }, +}); + +const schema = new SimpleSchema({}); +schema.extend(ChildSchema); +schema.extend(TabletopObjectSchema); +TabletopObjects.attachSchema(schema); + +export default TabletopObjects; diff --git a/app/imports/api/tabletop/Tabletops.js b/app/imports/api/tabletop/Tabletops.js deleted file mode 100644 index 5c81986c..00000000 --- a/app/imports/api/tabletop/Tabletops.js +++ /dev/null @@ -1,55 +0,0 @@ -import SimpleSchema from 'simpl-schema'; - -let Tabletops = new Mongo.Collection('tabletops'); - -const InitiativeSchema = new SimpleSchema({ - active: { - type: Boolean, - defaultValue: false, - }, - roundNumber: { - type: SimpleSchema.Integer, - defaultValue: 0, - }, - initiativeNumber: { - type: SimpleSchema.Integer, - optional: true, - }, - activeCreature: { - type: String, - regEx: SimpleSchema.RegEx.id, - optional: true, - }, -}); - -// All creatures in a tabletop have a shared time and space. -let TabletopSchema = new SimpleSchema({ - name: { - type: String, - optional: true, - }, - initiative: { - type: InitiativeSchema, - defaultValue: {}, - }, - gameMaster: { - type: String, - regEx: SimpleSchema.RegEx.id, - }, - players: { - type: Array, - defaultValue: [], - }, - 'players.$': { - type: String, - regEx: SimpleSchema.RegEx.id, - }, -}); - -Tabletops.attachSchema(TabletopSchema); - -import '/imports/api/tabletop/methods/removeTabletop.js'; -import '/imports/api/tabletop/methods/insertTabletop.js'; -import '/imports/api/tabletop/methods/addCreaturesToTabletop.js'; - -export default Tabletops; diff --git a/app/imports/api/tabletop/Tabletops.ts b/app/imports/api/tabletop/Tabletops.ts new file mode 100644 index 00000000..6b701960 --- /dev/null +++ b/app/imports/api/tabletop/Tabletops.ts @@ -0,0 +1,129 @@ +import SimpleSchema from 'simpl-schema'; + +export type Tabletop = { + name?: string, + description?: string, + imageUrl?: string, + owner: string, + gameMasters: string[], + players: string[], + spectators: string[], + public?: true, + initiative: { + active: boolean, + roundNumber: number, + initiativeNumber?: number, + activeCreature?: string, + }, + propCount: number, +} + +const Tabletops = new Mongo.Collection('tabletops'); + +const InitiativeSchema = new SimpleSchema({ + active: { + type: Boolean, + defaultValue: false, + }, + roundNumber: { + type: SimpleSchema.Integer, + defaultValue: 0, + }, + initiativeNumber: { + type: SimpleSchema.Integer, + optional: true, + }, + activeCreature: { + type: String, + regEx: SimpleSchema.RegEx.Id, + optional: true, + }, +}); + +// All creatures in a tabletop have a shared time and space. +const TabletopSchema = new SimpleSchema({ + // Details + name: { + type: String, + optional: true, + }, + description: { + type: String, + optional: true, + }, + imageUrl: { + type: String, + optional: true, + }, + + // Permissions by userId + // Who owns this tabletop and can delete it + owner: String, + // The owner will need to included in one of these arrays for specific permissions + // A user should not appear in more than one of the following arrays + gameMasters: { + type: Array, + defaultValue: [], + }, + 'gameMasters.$': { + type: String, + //@ts-expect-error Index not defined in simpl-schema package + index: 1, + }, + players: { + type: Array, + defaultValue: [], + }, + 'players.$': { + type: String, + //@ts-expect-error Index not defined in simpl-schema package + index: 1, + }, + spectators: { + type: Array, + defaultValue: [], + }, + 'spectators.$': { + type: String, + //@ts-expect-error Index not defined in simpl-schema package + index: 1, + }, + // Does everyone else have the spectator permission? + public: { + type: Boolean, + optional: true, + //@ts-expect-error Index not defined in simpl-schema package + index: 1, + }, + + // Initiative + initiative: { + type: InitiativeSchema, + defaultValue: {}, + }, + + // Denormalized fields + // Number of properties on all creatures in this tabletop + propCount: { + type: SimpleSchema.Integer, + defaultValue: 0, + }, + // Number of creatures in this tabletop + creatureCount: { + type: SimpleSchema.Integer, + defaultValue: 0, + }, +}); + +//@ts-expect-error attachSchema not defined in simpl-schema package +Tabletops.attachSchema(TabletopSchema); + +import '/imports/api/tabletop/methods/removeTabletop'; +import '/imports/api/tabletop/methods/insertTabletop'; +import '/imports/api/tabletop/methods/updateTabletop'; +import '/imports/api/tabletop/methods/addCreaturesToTabletop'; +import '/imports/api/tabletop/methods/updateTabletopSharing'; +import '/imports/api/tabletop/methods/addCreaturesFromLibraryToTabletop'; +import '/imports/api/tabletop/methods/removeCreatureFromTabletop'; + +export default Tabletops; diff --git a/app/imports/api/tabletop/functions/denormalizeTabletopPropCount.ts b/app/imports/api/tabletop/functions/denormalizeTabletopPropCount.ts new file mode 100644 index 00000000..4c166702 --- /dev/null +++ b/app/imports/api/tabletop/functions/denormalizeTabletopPropCount.ts @@ -0,0 +1,52 @@ +import { debounce } from 'lodash'; +import Creatures from '/imports/api/creature/creatures/Creatures'; +import Tabletops from '/imports/api/tabletop/Tabletops'; + +// Store a function per tabletop to debounce the update +const queues: Record void> = {}; +/** + * Update the propCount field on a tabletop to reflect the sum of all propCounts of creatures in + * that tabletop. + * Debounced by 1s, per tabletop + */ +export default function updateTabletopPropCount(tabletopId: string) { + if (!tabletopId) return; + + // Server only + if (Meteor.isClient) return; + + // If there isn't a debounced function for this tabletop, create one + if (!queues[tabletopId]) { + queues[tabletopId] = debounce(() => { + doUpdateTabletopPropCount(tabletopId); + // When this function is actually run, delete the debounced function + delete queues[tabletopId]; + }, 1_000); + } + + // Call the debounced function for this tabletop + queues[tabletopId](); +} + +/** + * Update the propCount field on a tabletop to reflect the sum of all propCounts of creatures in + * that tabletop + */ +async function doUpdateTabletopPropCount(tabletopId: string) { + let propCount = 0; + let creatureCount = 0; + await Creatures.find({ + tabletopId + }, { + fields: { propCount: 1 } + }).forEachAsync(creature => { + creatureCount += 1; + propCount += creature.propCount || 0; + }); + return Tabletops.update(tabletopId, { + $set: { + propCount, + creatureCount, + } + }); +} diff --git a/app/imports/api/tabletop/methods/addCreaturesFromLibraryToTabletop.ts b/app/imports/api/tabletop/methods/addCreaturesFromLibraryToTabletop.ts new file mode 100644 index 00000000..5ec398f3 --- /dev/null +++ b/app/imports/api/tabletop/methods/addCreaturesFromLibraryToTabletop.ts @@ -0,0 +1,121 @@ +import SimpleSchema from 'simpl-schema'; +import { ValidatedMethod } from 'meteor/mdg:validated-method'; +import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; +import { assertUserInTabletop } from './shared/tabletopPermissions'; +import { assertUserHasPaidBenefits } from '/imports/api/users/patreon/tiers'; +import Creatures from '/imports/api/creature/creatures/Creatures'; +import LibraryNodes from '/imports/api/library/LibraryNodes'; +import { getFilter, renewDocIds } from '/imports/api/parenting/parentingFunctions'; +import { reifyNodeReferences, storeLibraryNodeReferences } from '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; +import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables'; +import Tabletops from '/imports/api/tabletop/Tabletops'; +import { assertTabletopHasPropSpace } from '/imports/api/tabletop/methods/shared/tabletopLimits' + +const addCreaturesFromLibraryToTabletop = new ValidatedMethod({ + + name: 'tabletops.addCreaturesFromLibraryToTabletop', + + validate: new SimpleSchema({ + 'libraryNodeIds': { + type: Array, + }, + 'libraryNodeIds.$': { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, + tabletopId: { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, + }).validator(), + + mixins: [RateLimiterMixin], + rateLimit: { + numRequests: 1, + timeInterval: 5_000, + }, + + run({ libraryNodeIds, tabletopId }) { + if (!this.userId) { + throw new Meteor.Error('tabletops.addCreatures.denied', + 'You need to be logged in to remove a tabletop'); + } + assertUserHasPaidBenefits(this.userId); + const tabletop = Tabletops.findOne(tabletopId); + assertUserInTabletop(tabletop, this.userId); + assertTabletopHasPropSpace(tabletop); + + for (const nodeId of libraryNodeIds) { + const creatureNode = LibraryNodes.findOne({ + _id: nodeId, + type: 'creature', + removed: { $ne: true }, + }); + + if (!creatureNode) { + if (Meteor.isClient) return {}; + else { + throw new Meteor.Error( + 'Insert property from library failed', + `No library creature with id '${nodeId}' was found` + ); + } + } + + // Insert the creature + const creatureId = Creatures.insert({ + ...creatureNode, + _id: Random.id(), + type: 'monster', + tabletopId, + owner: this.userId, + readers: [], + writers: [this.userId], + public: false, + dirty: true, + settings: {}, + }); + + // Insert the creature variables + CreatureVariables.insert({ + _creatureId: creatureId, + }); + + insertSubProperties(creatureNode, creatureId); + } + }, +}); + +function insertSubProperties(node, creatureId: string) { + let nodes = LibraryNodes.find({ + ...getFilter.descendants(node), + removed: { $ne: true }, + }).fetch(); + + for (const node of nodes) { + node.root = { + '_id': creatureId, + collection: 'creatures', + }; + } + + // Convert all references into actual nodes + nodes = reifyNodeReferences(nodes); + + // set libraryNodeIds + storeLibraryNodeReferences(nodes); + + // Give the docs new IDs without breaking internal references + renewDocIds({ + docArray: nodes, + collectionMap: { 'libraryNodes': 'creatureProperties' } + }); + + // Insert the creature properties + // @ts-expect-error Batch insert not defined + if (nodes.length) CreatureProperties.batchInsert(nodes); + return node; +} + +export default addCreaturesFromLibraryToTabletop; diff --git a/app/imports/api/tabletop/methods/addCreaturesToTabletop.js b/app/imports/api/tabletop/methods/addCreaturesToTabletop.js index b17b953b..4f18fdf1 100644 --- a/app/imports/api/tabletop/methods/addCreaturesToTabletop.js +++ b/app/imports/api/tabletop/methods/addCreaturesToTabletop.js @@ -1,10 +1,11 @@ import SimpleSchema from 'simpl-schema'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import { assertUserInTabletop } from './shared/tabletopPermissions.js'; -import { assertAdmin } from '/imports/api/sharing/sharingPermissions.js'; -import { assertUserHasPaidBenefits } from '/imports/api/users/patreon/tiers.js'; -import Creatures from '/imports/api/creature/creatures/Creatures.js'; +import { assertUserInTabletop } from './shared/tabletopPermissions'; +import { assertUserHasPaidBenefits } from '/imports/api/users/patreon/tiers'; +import Creatures from '/imports/api/creature/creatures/Creatures'; +import Tabletops from '/imports/api/tabletop/Tabletops'; +import { assertTabletopHasPropSpace } from '/imports/api/tabletop/methods/shared/tabletopLimits'; const addCreaturesToTabletop = new ValidatedMethod({ @@ -13,14 +14,15 @@ const addCreaturesToTabletop = new ValidatedMethod({ validate: new SimpleSchema({ 'creatureIds': { type: Array, + max: 20, }, 'creatureIds.$': { type: String, - regEx: SimpleSchema.RegEx.id, + regEx: SimpleSchema.RegEx.Id, }, tabletopId: { type: String, - regEx: SimpleSchema.RegEx.id, + regEx: SimpleSchema.RegEx.Id, }, }).validator(), @@ -30,23 +32,25 @@ const addCreaturesToTabletop = new ValidatedMethod({ timeInterval: 5000, }, - run({tabletopId, creatureIds}) { + run({ tabletopId, creatureIds }) { if (!this.userId) { throw new Meteor.Error('tabletops.addCreatures.denied', - 'You need to be logged in to remove a tabletop'); + 'You need to be logged in to remove a tabletop'); } assertUserHasPaidBenefits(this.userId); - assertUserInTabletop(tabletopId, this.userId); - assertAdmin(this.userId); + const tabletop = Tabletops.findOne(tabletopId); + assertUserInTabletop(tabletop, this.userId); + assertTabletopHasPropSpace(tabletop); Creatures.update({ - _id: {$in: creatureIds}, + _id: { $in: creatureIds }, + // You must have write permission for the creatures you $or: [ - {writers: this.userId}, - {owner: this.userId}, + { writers: this.userId }, + { owner: this.userId }, ], }, { - $set: {tabletop: tabletopId}, + $set: { tabletopId }, }, { multi: true, }); diff --git a/app/imports/api/tabletop/methods/insertTabletop.js b/app/imports/api/tabletop/methods/insertTabletop.js index 62ad2156..c0827257 100644 --- a/app/imports/api/tabletop/methods/insertTabletop.js +++ b/app/imports/api/tabletop/methods/insertTabletop.js @@ -1,8 +1,7 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import Tabletops from '../Tabletops.js'; -import { assertAdmin } from '/imports/api/sharing/sharingPermissions.js'; -import { assertUserHasPaidBenefits } from '/imports/api/users/patreon/tiers.js'; +import Tabletops from '../Tabletops'; +import { assertUserHasPaidBenefits, getUserTier } from '/imports/api/users/patreon/tiers'; const insertTabletop = new ValidatedMethod({ @@ -11,8 +10,9 @@ const insertTabletop = new ValidatedMethod({ validate: null, mixins: [RateLimiterMixin], + // @ts-expect-error Rate limit not defined rateLimit: { - numRequests: 5, + numRequests: 2, timeInterval: 5000, }, @@ -22,13 +22,24 @@ const insertTabletop = new ValidatedMethod({ 'You need to be logged in to insert a tabletop'); } assertUserHasPaidBenefits(this.userId); - assertAdmin(this.userId); + let tier = getUserTier(this.userId); + const currentTabletopCount = Tabletops.find({ owner: this.userId }).count(); + + if (tier.tabletopSlots !== -1 && tier.tabletopSlots <= currentTabletopCount) { + throw new Meteor.Error('limit-reached', 'You have reached your maximum number of tabletops'); + } return Tabletops.insert({ - gameMaster: this.userId, + owner: this.userId, + gameMasters: [this.userId], + players: [], + spectators: [], + initiative: { + active: false, + roundNumber: 0, + }, }); }, - }); export default insertTabletop; diff --git a/app/imports/api/tabletop/methods/removeCreatureFromTabletop.ts b/app/imports/api/tabletop/methods/removeCreatureFromTabletop.ts new file mode 100644 index 00000000..cd8a50bc --- /dev/null +++ b/app/imports/api/tabletop/methods/removeCreatureFromTabletop.ts @@ -0,0 +1,89 @@ +import SimpleSchema from 'simpl-schema'; +import { ValidatedMethod } from 'meteor/mdg:validated-method'; +import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; +import { assertUserInTabletop } from './shared/tabletopPermissions'; +import { assertUserHasPaidBenefits } from '/imports/api/users/patreon/tiers'; +import Creatures from '/imports/api/creature/creatures/Creatures'; +import updateTabletopPropCount from '/imports/api/tabletop/functions/denormalizeTabletopPropCount'; +import { getCreature } from '/imports/api/engine/loadCreatures'; +import { removeCreatureWork } from '/imports/api/creature/creatures/methods/removeCreature'; +import { assertOwnership } from '/imports/api/creature/creatures/creaturePermissions'; + +const removeCreatureFromTabletop = new ValidatedMethod({ + + name: 'tabletops.removeCreature', + + validate: new SimpleSchema({ + tabletopId: { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, + 'creatureIds': { + type: Array, + }, + 'creatureIds.$': { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, + }).validator(), + + mixins: [RateLimiterMixin], + rateLimit: { + numRequests: 10, + timeInterval: 5000, + }, + + run({ tabletopId, creatureIds }) { + if (!this.userId) { + throw new Meteor.Error('tabletops.removeCreature.denied', + 'You need to be logged in to remove creatures from tabletop'); + } + assertUserHasPaidBenefits(this.userId); + assertUserInTabletop(tabletopId, this.userId); + + const creaturesToRemove: any[] = []; + const creatureIdsToClearTabletopId: string[] = []; + + for (const creatureId of creatureIds) { + const creature = getCreature(creatureId); + // Make sure the creature exists and is in this tabletop + if (!creature || creature.tabletopId !== tabletopId) continue; + switch (creature.type) { + // Remove character creatures from the tabletop + case 'pc': + creatureIdsToClearTabletopId.push(creatureId); + break; + // Delete non player characters and monsters + case 'npc': + case 'monster': + creaturesToRemove.push(creature); + break; + } + } + + // Clear tabletopId from all player characters + if (creatureIdsToClearTabletopId.length) Creatures.update({ + _id: { $in: creatureIdsToClearTabletopId }, + $or: [ + { writers: this.userId }, + { owner: this.userId }, + ], + }, { + $unset: { tabletopId: 1 }, + }, { + multi: true, + }); + + // Remove all non player characters and monsters + for (const creature of creaturesToRemove) { + assertOwnership(creature, this.userId) + removeCreatureWork(creature._id); + } + + if (Meteor.isServer) { + updateTabletopPropCount(tabletopId); + } + }, +}); + +export default removeCreatureFromTabletop; diff --git a/app/imports/api/tabletop/methods/removeTabletop.js b/app/imports/api/tabletop/methods/removeTabletop.js index 4636eb4b..c828651f 100644 --- a/app/imports/api/tabletop/methods/removeTabletop.js +++ b/app/imports/api/tabletop/methods/removeTabletop.js @@ -1,11 +1,10 @@ import SimpleSchema from 'simpl-schema'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import Tabletops from '../Tabletops.js'; -import { assertAdmin } from '/imports/api/sharing/sharingPermissions.js'; -import { assertUserHasPaidBenefits } from '/imports/api/users/patreon/tiers.js'; -import { assertUserIsTabletopOwner } from './shared/tabletopPermissions.js'; -import Creatures from '/imports/api/creature/creatures/Creatures.js'; +import Tabletops from '../Tabletops'; +import { assertUserHasPaidBenefits } from '/imports/api/users/patreon/tiers'; +import { assertUserIsTabletopOwner } from './shared/tabletopPermissions'; +import Creatures from '/imports/api/creature/creatures/Creatures'; const removeTabletop = new ValidatedMethod({ @@ -14,11 +13,12 @@ const removeTabletop = new ValidatedMethod({ validate: new SimpleSchema({ tabletopId: { type: String, - regEx: SimpleSchema.RegEx.id, + regEx: SimpleSchema.RegEx.Id, }, }).validator(), mixins: [RateLimiterMixin], + // @ts-expect-error Rate limit not defined rateLimit: { numRequests: 5, timeInterval: 5000, @@ -31,7 +31,6 @@ const removeTabletop = new ValidatedMethod({ } assertUserHasPaidBenefits(this.userId); assertUserIsTabletopOwner(tabletopId, this.userId); - assertAdmin(this.userId); let removed = Tabletops.remove({ _id: tabletopId, diff --git a/app/imports/api/tabletop/methods/shared/tabletopLimits.ts b/app/imports/api/tabletop/methods/shared/tabletopLimits.ts new file mode 100644 index 00000000..01be33a4 --- /dev/null +++ b/app/imports/api/tabletop/methods/shared/tabletopLimits.ts @@ -0,0 +1,13 @@ +const MAX_PROP_COUNT = 10_000; +const MAX_CREATURE_COUNT = 110; + +export function assertTabletopHasPropSpace(tabletop) { + if (tabletop.propCount >= MAX_PROP_COUNT) { + throw new Meteor.Error('tabletops.denied', + 'This tabletop is full, either remove some creatures or reduce how many properties each creature has'); + } + if (tabletop.creatureCount >= MAX_CREATURE_COUNT) { + throw new Meteor.Error('tabletops.denied', + 'This tabletop is full, you can\'t add any more creatures to it'); + } +} diff --git a/app/imports/api/tabletop/methods/shared/tabletopPermissions.js b/app/imports/api/tabletop/methods/shared/tabletopPermissions.js deleted file mode 100644 index 2f14543d..00000000 --- a/app/imports/api/tabletop/methods/shared/tabletopPermissions.js +++ /dev/null @@ -1,25 +0,0 @@ -import Tabletops from '../../Tabletops.js'; - -export function assertUserInTabletop(tabletopId, userId){ - let tabletop = Tabletops.findOne(tabletopId); - if (!tabletop){ - throw new Meteor.Error('Tabletop does not exist', - 'No tabletop could be found for the given tabletop id'); - } - if (tabletop.gameMaster !== userId && !tabletop.players.includes(userId)){ - throw new Meteor.Error('Not in tabletop', - 'The user is not the gamemaster or a player in the given tabletop'); - } -} - -export function assertUserIsTabletopOwner(tabletopId, userId){ - let tabletop = Tabletops.findOne(tabletopId); - if (!tabletop){ - throw new Meteor.Error('Tabletop does not exist', - 'No tabletop could be found for the given tabletop id'); - } - if (tabletop.gameMaster !== userId){ - throw new Meteor.Error('Not the owner', - 'The user is not the owner of the given tabletop'); - } -} diff --git a/app/imports/api/tabletop/methods/shared/tabletopPermissions.ts b/app/imports/api/tabletop/methods/shared/tabletopPermissions.ts new file mode 100644 index 00000000..7305c527 --- /dev/null +++ b/app/imports/api/tabletop/methods/shared/tabletopPermissions.ts @@ -0,0 +1,56 @@ +import Tabletops, { Tabletop } from '/imports/api/tabletop/Tabletops'; + +type TabletopOrId = Tabletop | string | undefined; + +function assertTabletopExists(tabletop: Tabletop | undefined): asserts tabletop is Tabletop { + if (!tabletop) { + throw new Meteor.Error('Tabletop does not exist', + 'Tabletop not found'); + } +} + +function getTabletop(tabletop: TabletopOrId): Tabletop | undefined { + if (typeof tabletop === 'string') { + return Tabletops.findOne(tabletop, { + fields: { gameMasters: 1, players: 1, owner: 1, spectators: 1 } + }); + } else { + return tabletop + } +} + +export function assertUserInTabletop(tabletopOrId: TabletopOrId, userId: string) { + const tabletop = getTabletop(tabletopOrId); + assertTabletopExists(tabletop); + if (!tabletop.gameMasters.includes(userId) && !tabletop.players.includes(userId)) { + throw new Meteor.Error('Not in tabletop', + 'You are not a game master or a player in the tabletop'); + } +} + +export function assertUserGameMasterOfTabletop(tabletopOrId: TabletopOrId, userId: string) { + const tabletop = getTabletop(tabletopOrId); + assertTabletopExists(tabletop); + if (tabletop.gameMasters.includes(userId)) { + throw new Meteor.Error('not-game-master', + 'You are not a game master in the tabletop'); + } +} + +export function assertCanEditTabletop(tabletopOrId: TabletopOrId, userId: string) { + const tabletop = getTabletop(tabletopOrId); + assertTabletopExists(tabletop); + if (tabletop.owner !== userId && tabletop.gameMasters.includes(userId)) { + throw new Meteor.Error('not-editor', + 'You are not an owner or game master of the tabletop'); + } +} + +export function assertUserIsTabletopOwner(tabletopOrId: TabletopOrId, userId: string) { + const tabletop = getTabletop(tabletopOrId); + assertTabletopExists(tabletop); + if (tabletop.owner !== userId) { + throw new Meteor.Error('not-owner', + 'You are not the owner of the tabletop'); + } +} diff --git a/app/imports/api/tabletop/methods/updateTabletop.js b/app/imports/api/tabletop/methods/updateTabletop.js new file mode 100644 index 00000000..d0a7620b --- /dev/null +++ b/app/imports/api/tabletop/methods/updateTabletop.js @@ -0,0 +1,54 @@ +import { ValidatedMethod } from 'meteor/mdg:validated-method'; +import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; +import Tabletops from '../Tabletops'; +import { assertUserHasPaidBenefits } from '/imports/api/users/patreon/tiers'; +import { assertCanEditTabletop } from './shared/tabletopPermissions'; + +const updateTabletop = new ValidatedMethod({ + + name: 'tabletops.update', + + validate({ _id, path }) { + if (!_id) return false; + // Allowed fields + let allowedFields = [ + 'name', + 'description', + 'imageUrl', + 'public', + ]; + if (!allowedFields.includes(path[0])) { + throw new Meteor.Error('tabletops.update.denied', + 'This field can\'t be updated using this method'); + } + }, + + mixins: [RateLimiterMixin], + // @ts-expect-error Rate limit not defined + rateLimit: { + numRequests: 5, + timeInterval: 5000, + }, + + run({ _id, path, value }) { + if (!this.userId) { + throw new Meteor.Error('tabletops.update.denied', + 'You need to be logged in to update a tabletop'); + } + assertUserHasPaidBenefits(this.userId); + assertCanEditTabletop(_id, this.userId); + + if (value === undefined || value === null) { + Tabletops.update(_id, { + $unset: { [path.join('.')]: 1 }, + }); + } else { + Tabletops.update(_id, { + $set: { [path.join('.')]: value }, + }); + } + }, + +}); + +export default updateTabletop; diff --git a/app/imports/api/tabletop/methods/updateTabletopSharing.js b/app/imports/api/tabletop/methods/updateTabletopSharing.js new file mode 100644 index 00000000..795571ee --- /dev/null +++ b/app/imports/api/tabletop/methods/updateTabletopSharing.js @@ -0,0 +1,121 @@ +import { ValidatedMethod } from 'meteor/mdg:validated-method'; +import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; +import Tabletops from '../Tabletops'; +import { assertUserHasPaidBenefits } from '/imports/api/users/patreon/tiers'; +import { assertCanEditTabletop, assertUserIsTabletopOwner } from './shared/tabletopPermissions'; + +const updateTabletopSharing = new ValidatedMethod({ + + name: 'tabletops.updateSharing', + + validate({ tabletopId, userId, role }) { + if (!userId) return false; + if (!tabletopId) return false; + // Allowed fields + const roles = [ + 'owner', + 'gameMaster', + 'player', + 'spectator', + 'none', + ]; + if (!roles.includes(role)) { + throw new Meteor.Error('tabletops.updateSharing.denied', + 'Invalid role selected'); + } + }, + + mixins: [RateLimiterMixin], + // @ts-expect-error Rate limit not defined + rateLimit: { + numRequests: 5, + timeInterval: 5000, + }, + + run({ tabletopId, userId, role }) { + if (!this.userId) { + throw new Meteor.Error('tabletops.update.denied', + 'You need to be logged in to update a tabletop'); + } + const tabletop = Tabletops.findOne(tabletopId); + assertUserHasPaidBenefits(this.userId); + assertCanEditTabletop(tabletop, this.userId); + + if (role === 'owner') { + assertUserIsTabletopOwner(tabletop, this.userId); + } + + // Check that the new user exists + if (Meteor.isServer) { + const userToAdd = Meteor.users.findOne({ _id: userId }, { fields: { _id: 1 } }); + if (!userToAdd) { + throw new Meteor.Error('User not found', + 'The user could not be found' + ); + } + } + + let update; + switch (role) { + case 'owner': + update = { + $set: { owner: userId }, + $addToSet: { + gameMasters: this.userId, + }, + $pull: { + players: this.userId, + spectators: this.userId, + }, + }; + break; + case 'gameMaster': + update = { + $addToSet: { + gameMasters: userId, + }, + $pull: { + players: userId, + spectators: userId, + }, + }; + break; + case 'player': + update = { + $addToSet: { + players: userId, + }, + $pull: { + gameMasters: userId, + spectators: userId, + }, + }; + break; + case 'spectator': + update = { + $addToSet: { + spectators: userId, + }, + $pull: { + gameMasters: userId, + players: userId, + }, + }; + break; + case 'none': + update = { + $pull: { + gameMasters: userId, + players: userId, + spectators: userId, + }, + }; + break; + } + if (!update) return; + return Tabletops.update(tabletopId, update) + }, + +}); + +export default updateTabletopSharing; diff --git a/app/imports/api/users/Invites.js b/app/imports/api/users/Invites.js index af98adf2..19799b92 100644 --- a/app/imports/api/users/Invites.js +++ b/app/imports/api/users/Invites.js @@ -1,9 +1,9 @@ import SimpleSchema from 'simpl-schema'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import { getUserTier } from '/imports/api/users/patreon/tiers.js'; +import { getUserTier } from '/imports/api/users/patreon/tiers'; -let Invites= new Mongo.Collection('invites'); +let Invites = new Mongo.Collection('invites'); let InviteSchema = new SimpleSchema({ inviter: { @@ -34,13 +34,13 @@ let InviteSchema = new SimpleSchema({ }, }); -if (Meteor.isServer){ - Accounts.onLogin(function({user}){ +if (Meteor.isServer) { + Accounts.onLogin(function ({ user }) { alignInvitesWithPatreonTier(user); }); } -function alignInvitesWithPatreonTier(user){ +function alignInvitesWithPatreonTier(user) { const tier = getUserTier(user); let availableInvites = tier.invites; let currentlyFundedInvites = []; @@ -48,7 +48,7 @@ function alignInvitesWithPatreonTier(user){ Invites.find({ inviter: user._id }).forEach(invite => { - if (invite.isFunded){ + if (invite.isFunded) { currentlyFundedInvites.push(invite); } else { currenltyUnfundedInvites.push(invite); @@ -63,23 +63,23 @@ function alignInvitesWithPatreonTier(user){ currenltyUnfundedInvites.sort((a, b) => b.dateConfirmed - a.dateConfirmed); // Defund or delete excess invites - while (currentlyFundedInvites.length > availableInvites){ + while (currentlyFundedInvites.length > availableInvites) { let inviteToDefund = currentlyFundedInvites.pop(); - if (inviteToDefund.invitee){ - Invites.update(inviteToDefund._id, {$set: {isFunded: false}}); + if (inviteToDefund.invitee) { + Invites.update(inviteToDefund._id, { $set: { isFunded: false } }); } else { Invites.remove(inviteToDefund._id); } } // Fund unfunded invites or insert new ones - while (currentlyFundedInvites.length < availableInvites){ - if (currenltyUnfundedInvites.length){ + while (currentlyFundedInvites.length < availableInvites) { + if (currenltyUnfundedInvites.length) { let inviteToFund = currenltyUnfundedInvites.pop(); currentlyFundedInvites.push(inviteToFund); - Invites.update(inviteToFund._id, {$set: {isFunded: true}}); + Invites.update(inviteToFund._id, { $set: { isFunded: true } }); } else { - let inviteId = Invites.insert({inviter: user._id, isFunded: true}); - currentlyFundedInvites.push({_id: inviteId}); + let inviteId = Invites.insert({ inviter: user._id, isFunded: true }); + currentlyFundedInvites.push({ _id: inviteId }); } } } @@ -97,17 +97,17 @@ const getInviteToken = new ValidatedMethod({ numRequests: 5, timeInterval: 5000, }, - run({inviteId}) { + run({ inviteId }) { let invite = Invites.findOne(inviteId); if (this.userId !== invite.inviter) { throw new Meteor.Error('Invites.methods.getToken.denied', - 'You need to be the inviter of the invite to create a token'); + 'You need to be the inviter of the invite to create a token'); } - if (invite.inviteToken){ + if (invite.inviteToken) { return invite.inviteToken; } else { let inviteToken = Random.id(5); - Invites.update(inviteId, {$set: {inviteToken}}) + Invites.update(inviteId, { $set: { inviteToken } }) return inviteToken; } }, @@ -125,32 +125,32 @@ const acceptInviteToken = new ValidatedMethod({ numRequests: 5, timeInterval: 5000, }, - run({inviteToken}) { + run({ inviteToken }) { if (!this.userId) { throw new Meteor.Error('Invites.methods.acceptToken.denied', - 'You need to be the logged in to accept a token'); + 'You need to be the logged in to accept a token'); } if (Meteor.isClient) return; - let invite = Invites.findOne({inviteToken}); - if (!invite){ + let invite = Invites.findOne({ inviteToken }); + if (!invite) { throw new Meteor.Error('Invites.methods.acceptToken.notFound', - 'No invite could be found for this link, maybe it has already been claimed'); + 'No invite could be found for this link, maybe it has already been claimed'); } // If the invitee is already filled, fix unexpected case by deleting the token - if (invite.invitee){ + if (invite.invitee) { Invites.update(invite._id, { - $unset: {inviteToken: 1} + $unset: { inviteToken: 1 } }); throw new Meteor.Error('Invites.methods.acceptToken.alreadyAccepted', - 'This invite has already been claimed'); + 'This invite has already been claimed'); } - if (this.userId === invite.inviter){ + if (this.userId === invite.inviter) { throw new Meteor.Error('Invites.methods.acceptToken.ownToken', - 'You can\'t accept your own invite'); + 'You can\'t accept your own invite'); } Invites.update(invite._id, { - $set: {invitee: this.userId}, - $unset: {inviteToken: 1}, + $set: { invitee: this.userId }, + $unset: { inviteToken: 1 }, }); }, }); @@ -168,28 +168,28 @@ const revokeInvite = new ValidatedMethod({ numRequests: 5, timeInterval: 5000, }, - run({inviteId}) { + run({ inviteId }) { if (!this.userId) { throw new Meteor.Error('Invites.methods.revokeInvite.denied', - 'You need to be the logged in to revoke a token'); + 'You need to be the logged in to revoke a token'); } if (Meteor.isClient) return; let invite = Invites.findOne(inviteId); - if (!invite){ + if (!invite) { throw new Meteor.Error('Invites.methods.revokeInvite.notFound', - 'No invite could be found for this id'); + 'No invite could be found for this id'); } if (this.userId !== invite.inviter) { throw new Meteor.Error('Invites.methods.revokeInvite.denied', - 'You are not the owner of this invite'); + 'You are not the owner of this invite'); } // If the invitee is empty, the token has already been revoked - if (!invite.invitee){ + if (!invite.invitee) { return; } Invites.update(invite._id, { - $unset: {invitee: 1, dateConfirmed: 1}, + $unset: { invitee: 1, dateConfirmed: 1 }, }); }, }); diff --git a/app/imports/api/users/Users.js b/app/imports/api/users/Users.js index 54328914..b219eeba 100644 --- a/app/imports/api/users/Users.js +++ b/app/imports/api/users/Users.js @@ -1,11 +1,12 @@ import SimpleSchema from 'simpl-schema'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -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 Libraries from '/imports/api/library/Libraries'; +import LibraryCollections from '/imports/api/library/LibraryCollections'; +import '/imports/api/users/methods/deleteMyAccount'; +import '/imports/api/users/methods/addEmail'; +import '/imports/api/users/methods/removeEmail'; +import '/imports/api/users/methods/updateFileStorageUsed'; 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,30 @@ Meteor.users.setPreference = new ValidatedMethod({ }, }); +if (Meteor.isServer) { + Accounts.onCreateUser((options, user) => { + 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, + }, () => {/**/ }); + } + return user; + }); +} + Meteor.users.subscribeToLibrary = new ValidatedMethod({ name: 'users.subscribeToLibrary', validate: new SimpleSchema({ @@ -264,15 +289,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 +326,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 }, }); diff --git a/app/imports/api/users/methods/deleteMyAccount.js b/app/imports/api/users/methods/deleteMyAccount.js index 5c555855..c452583c 100644 --- a/app/imports/api/users/methods/deleteMyAccount.js +++ b/app/imports/api/users/methods/deleteMyAccount.js @@ -1,8 +1,8 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import Libraries, { removeLibaryWork } from '/imports/api/library/Libraries.js'; -import Creatures from '/imports/api/creature/creatures/Creatures.js'; -import { removeCreatureWork } from '/imports/api/creature/creatures/methods/removeCreature.js'; +import Libraries, { removeLibaryWork } from '/imports/api/library/Libraries'; +import Creatures from '/imports/api/creature/creatures/Creatures'; +import { removeCreatureWork } from '/imports/api/creature/creatures/methods/removeCreature'; Meteor.users.deleteMyAccount = new ValidatedMethod({ name: 'users.deleteMyAccount', diff --git a/app/imports/api/users/methods/linkWithPatreon.js b/app/imports/api/users/methods/linkWithPatreon.js index 05ec62fb..c204e1f2 100644 --- a/app/imports/api/users/methods/linkWithPatreon.js +++ b/app/imports/api/users/methods/linkWithPatreon.js @@ -1,6 +1,5 @@ // Adds accounts-patreon support to bozhao:link-accounts import { Meteor } from 'meteor/meteor'; -import { Accounts } from 'meteor/accounts-base'; export default function linkWithPatreon(options, callback) { if (!Meteor.userId()) { diff --git a/app/imports/api/users/methods/updateFileStorageUsed.js b/app/imports/api/users/methods/updateFileStorageUsed.js index 4fad2feb..656915b2 100644 --- a/app/imports/api/users/methods/updateFileStorageUsed.js +++ b/app/imports/api/users/methods/updateFileStorageUsed.js @@ -1,7 +1,7 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import ArchiveCreatureFiles from '/imports/api/creature/archive/ArchiveCreatureFiles.js'; -import UserImages from '/imports/api/files/UserImages.js'; +import ArchiveCreatureFiles from '/imports/api/creature/archive/ArchiveCreatureFiles'; +import UserImages from '/imports/api/files/userImages/UserImages'; const fileCollections = [ArchiveCreatureFiles, UserImages]; const updateFileStorageUsed = new ValidatedMethod({ @@ -29,7 +29,7 @@ export default updateFileStorageUsed; export function updateFileStorageUsedWork(userId) { if (!userId) { throw new Meteor.Error('idRequired', - 'No user ID was provided to update file storage used') + 'No user ID was provided to update file storage used') } let sum = 0; @@ -51,7 +51,7 @@ export function incrementFileStorageUsed(userId, amount) { throw new Meteor.Error('idRequired', 'No user ID was provided to update file storage used') } - + const user = Meteor.users.findOne(userId); if (!user) { throw new Meteor.Error('noUser', 'User not found'); diff --git a/app/imports/api/users/patreon/tiers.js b/app/imports/api/users/patreon/tiers.js index edd1d44a..88a5cb2e 100644 --- a/app/imports/api/users/patreon/tiers.js +++ b/app/imports/api/users/patreon/tiers.js @@ -1,6 +1,6 @@ import { findLast } from 'lodash'; -import getEntitledCents from '/imports/api/users/patreon/getEntitledCents.js'; -import Invites from '/imports/api/users/Invites.js'; +import getEntitledCents from '/imports/api/users/patreon/getEntitledCents'; +import Invites from '/imports/api/users/Invites'; const patreonDisabled = !!Meteor.settings?.public?.disablePatreon; const TIERS = Object.freeze([ @@ -9,6 +9,7 @@ const TIERS = Object.freeze([ minimumEntitledCents: 0, invites: 0, characterSlots: 5, + tabletopSlots: 0, fileStorage: 50, paidBenefits: false, }, { @@ -16,6 +17,7 @@ const TIERS = Object.freeze([ minimumEntitledCents: 100, invites: 0, characterSlots: 5, + tabletopSlots: 0, fileStorage: 50, paidBenefits: false, }, { @@ -23,6 +25,7 @@ const TIERS = Object.freeze([ minimumEntitledCents: 300, invites: 0, characterSlots: 5, + tabletopSlots: 0, fileStorage: 50, paidBenefits: false, }, { @@ -31,6 +34,7 @@ const TIERS = Object.freeze([ minimumEntitledCents: 500, invites: 0, characterSlots: 20, + tabletopSlots: 4, fileStorage: 200, paidBenefits: true, }, { @@ -39,6 +43,7 @@ const TIERS = Object.freeze([ minimumEntitledCents: 1000, invites: 2, characterSlots: 50, + tabletopSlots: 10, fileStorage: 500, paidBenefits: true, }, { @@ -47,6 +52,7 @@ const TIERS = Object.freeze([ minimumEntitledCents: 2000, invites: 5, characterSlots: 120, + tabletopSlots: 24, fileStorage: 1000, paidBenefits: true, }, { @@ -55,6 +61,7 @@ const TIERS = Object.freeze([ minimumEntitledCents: 5000, invites: 15, characterSlots: -1, // Unlimited characters + tabletopSlots: -1, // Unlimited tabletops fileStorage: 2000, paidBenefits: true, }, @@ -66,6 +73,7 @@ const GUEST_TIER = Object.freeze({ guest: true, invites: 0, characterSlots: 20, + tabletopSlots: 4, fileStorage: 200, paidBenefits: true, }); @@ -76,18 +84,19 @@ const PATREON_DISABLED_TIER = Object.freeze({ name: 'Outlander', invites: 0, characterSlots: -1, // Can have infinitely many characters + tabletopSlots: -1, // Infinite tabletops fileStorage: 1000000, // 1TB file storage paidBenefits: true, }); -export function getTierByEntitledCents(entitledCents = 0){ +export function getTierByEntitledCents(entitledCents = 0) { if (patreonDisabled) return PATREON_DISABLED_TIER; - return findLast(TIERS, tier => entitledCents >= tier.minimumEntitledCents); + return findLast(TIERS, tier => entitledCents >= tier.minimumEntitledCents) || TIERS[0]; } -export function getUserTier(user){ +export function getUserTier(user) { if (!user) throw 'user must be provided'; - if (typeof user === 'string'){ + if (typeof user === 'string') { user = Meteor.users.findOne(user, { fields: { 'services.patreon': 1, @@ -99,19 +108,19 @@ export function getUserTier(user){ const entitledCents = getEntitledCents(user); const tier = getTierByEntitledCents(entitledCents); if (tier.paidBenefits) return tier; - let invite = Invites.findOne({invitee: user._id, isFunded: true}); - if (invite){ + let invite = Invites.findOne({ invitee: user._id, isFunded: true }); + if (invite) { return GUEST_TIER; } else { return tier; } } -export function assertUserHasPaidBenefits(user){ +export function assertUserHasPaidBenefits(user) { let tier = getUserTier(user); - if (!tier.paidBenefits){ + if (!tier.paidBenefits) { throw new Meteor.Error('no paid benefits', - `The ${tier.name} tier does not have the required benefits`); + `The ${tier.name} tier does not have the required benefits`); } } diff --git a/app/imports/api/users/patreon/updatePatreonOnLogin.js b/app/imports/api/users/patreon/updatePatreonOnLogin.js index cdff000e..f00d739e 100644 --- a/app/imports/api/users/patreon/updatePatreonOnLogin.js +++ b/app/imports/api/users/patreon/updatePatreonOnLogin.js @@ -1,11 +1,11 @@ -import updatePatreonDetails from '/imports/api/users/patreon/updatePatreonDetails.js'; +import updatePatreonDetails from '/imports/api/users/patreon/updatePatreonDetails'; const ONE_DAY = 24 * 60 * 60 * 1000; -Accounts.onLogin(({user}) => { +Accounts.onLogin(({ user }) => { let patreon = user.services && user.services.patreon; - if (patreon){ + if (patreon) { const timeSinceIdentityUpdate = new Date() - patreon.lastUpdatedIdentity; - if (timeSinceIdentityUpdate > ONE_DAY){ + if (timeSinceIdentityUpdate > ONE_DAY) { updatePatreonDetails(user); } } diff --git a/app/imports/api/utility/asyncMap.ts b/app/imports/api/utility/asyncMap.ts new file mode 100644 index 00000000..b216a336 --- /dev/null +++ b/app/imports/api/utility/asyncMap.ts @@ -0,0 +1,19 @@ +/** + * Async compatible map that processes all items in parallel + */ +export async function parallelMap(array: any[], fn: (doc: any) => Promise): Promise { + return await Promise.all(array.map(fn)); +} + +/** + * Async compatible map that processes all items in series + */ +export async function serialMap(array: any[], fn: (doc: any) => Promise): Promise { + const results: any[] = []; + for (const doc of array) { + const result = await fn(doc); + results.push(result); + } + return results; +} + diff --git a/app/imports/api/utility/escapeRegex.js b/app/imports/api/utility/escapeRegex.js new file mode 100644 index 00000000..4f0be7de --- /dev/null +++ b/app/imports/api/utility/escapeRegex.js @@ -0,0 +1,3 @@ +export default function escapeRegex(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} \ No newline at end of file diff --git a/app/imports/api/utility/getPropertyTitle.ts b/app/imports/api/utility/getPropertyTitle.ts new file mode 100644 index 00000000..e76f9098 --- /dev/null +++ b/app/imports/api/utility/getPropertyTitle.ts @@ -0,0 +1,6 @@ +import { getPropertyName } from '/imports/constants/PROPERTIES'; + +export default function getPropertyTitle(prop) { + if (prop.name) return prop.name; + return getPropertyName(prop.type); +} diff --git a/app/imports/api/utility/numberToSignedString.js b/app/imports/api/utility/numberToSignedString.js new file mode 100644 index 00000000..f60b3412 --- /dev/null +++ b/app/imports/api/utility/numberToSignedString.js @@ -0,0 +1,11 @@ +export default function numberToSignedString(number, spaced) { + if (typeof number !== 'number') return number; + if (number === 0) { + return spaced ? '+ 0' : '+0'; + } else if (number > 0) { + return spaced ? `+ ${number}` : `+${number}`; + } else { + // 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}`; + } +} diff --git a/app/imports/api/utility/timeout.ts b/app/imports/api/utility/timeout.ts new file mode 100644 index 00000000..99f8d1f0 --- /dev/null +++ b/app/imports/api/utility/timeout.ts @@ -0,0 +1,5 @@ +export default async function timeout(ms: number): Promise { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +} diff --git a/app/imports/client/serviceWorker.js b/app/imports/client/serviceWorker.js index 5caa9669..e7b25b45 100644 --- a/app/imports/client/serviceWorker.js +++ b/app/imports/client/serviceWorker.js @@ -1,5 +1,5 @@ Meteor.startup(() => { navigator.serviceWorker.register('/sw.js') - .then() - .catch(error => console.log('ServiceWorker registration failed: ', error)); + .then() + .catch(error => console.log('ServiceWorker registration failed: ', error)); }); diff --git a/app/imports/ui/components/CardHighlight.vue b/app/imports/client/ui/components/CardHighlight.vue similarity index 100% rename from app/imports/ui/components/CardHighlight.vue rename to app/imports/client/ui/components/CardHighlight.vue diff --git a/app/imports/ui/components/CoinValue.vue b/app/imports/client/ui/components/CoinValue.vue similarity index 89% rename from app/imports/ui/components/CoinValue.vue rename to app/imports/client/ui/components/CoinValue.vue index b13aa9ef..21ea3de3 100644 --- a/app/imports/ui/components/CoinValue.vue +++ b/app/imports/client/ui/components/CoinValue.vue @@ -19,7 +19,7 @@ + + \ No newline at end of file diff --git a/app/imports/client/ui/components/HexagonProgressStack.vue b/app/imports/client/ui/components/HexagonProgressStack.vue new file mode 100644 index 00000000..23cf796d --- /dev/null +++ b/app/imports/client/ui/components/HexagonProgressStack.vue @@ -0,0 +1,65 @@ + + + + + \ No newline at end of file diff --git a/app/imports/ui/components/HorizontalHex.vue b/app/imports/client/ui/components/HorizontalHex.vue similarity index 100% rename from app/imports/ui/components/HorizontalHex.vue rename to app/imports/client/ui/components/HorizontalHex.vue diff --git a/app/imports/client/ui/components/ImageUploadInput.vue b/app/imports/client/ui/components/ImageUploadInput.vue new file mode 100644 index 00000000..970076c5 --- /dev/null +++ b/app/imports/client/ui/components/ImageUploadInput.vue @@ -0,0 +1,149 @@ + + + + + \ No newline at end of file diff --git a/app/imports/ui/components/IncrementButton.vue b/app/imports/client/ui/components/IncrementButton.vue similarity index 93% rename from app/imports/ui/components/IncrementButton.vue rename to app/imports/client/ui/components/IncrementButton.vue index 865ec7bb..62c3768d 100644 --- a/app/imports/ui/components/IncrementButton.vue +++ b/app/imports/client/ui/components/IncrementButton.vue @@ -32,7 +32,7 @@ + + + + diff --git a/app/imports/ui/components/global/IconPicker.vue b/app/imports/client/ui/components/global/IconPicker.vue similarity index 78% rename from app/imports/ui/components/global/IconPicker.vue rename to app/imports/client/ui/components/global/IconPicker.vue index 13ef7954..722685e5 100644 --- a/app/imports/ui/components/global/IconPicker.vue +++ b/app/imports/client/ui/components/global/IconPicker.vue @@ -5,26 +5,32 @@ transition="slide-y-transition" min-width="290px" style="overflow-y: auto;" + left > + + + + \ No newline at end of file diff --git a/app/imports/ui/components/global/SmartInputMixin.js b/app/imports/client/ui/components/global/SmartInputMixin.js similarity index 100% rename from app/imports/ui/components/global/SmartInputMixin.js rename to app/imports/client/ui/components/global/SmartInputMixin.js diff --git a/app/imports/ui/components/global/SmartSelect.vue b/app/imports/client/ui/components/global/SmartSelect.vue similarity index 86% rename from app/imports/ui/components/global/SmartSelect.vue rename to app/imports/client/ui/components/global/SmartSelect.vue index 005ea2ae..0fd2da51 100644 --- a/app/imports/ui/components/global/SmartSelect.vue +++ b/app/imports/client/ui/components/global/SmartSelect.vue @@ -23,7 +23,7 @@ diff --git a/app/imports/ui/components/global/SmartSwitch.vue b/app/imports/client/ui/components/global/SmartSwitch.vue similarity index 77% rename from app/imports/ui/components/global/SmartSwitch.vue rename to app/imports/client/ui/components/global/SmartSwitch.vue index 716e0998..49dd73da 100644 --- a/app/imports/ui/components/global/SmartSwitch.vue +++ b/app/imports/client/ui/components/global/SmartSwitch.vue @@ -10,7 +10,7 @@ diff --git a/app/imports/ui/components/global/SvgIcon.vue b/app/imports/client/ui/components/global/SvgIcon.vue similarity index 100% rename from app/imports/ui/components/global/SvgIcon.vue rename to app/imports/client/ui/components/global/SvgIcon.vue diff --git a/app/imports/ui/components/global/TextArea.vue b/app/imports/client/ui/components/global/TextArea.vue similarity index 84% rename from app/imports/ui/components/global/TextArea.vue rename to app/imports/client/ui/components/global/TextArea.vue index 2fe4f8c8..08405878 100644 --- a/app/imports/ui/components/global/TextArea.vue +++ b/app/imports/client/ui/components/global/TextArea.vue @@ -14,7 +14,7 @@ diff --git a/app/imports/client/ui/components/tree/TreeSearchInput.vue b/app/imports/client/ui/components/tree/TreeSearchInput.vue new file mode 100644 index 00000000..7e5b6a93 --- /dev/null +++ b/app/imports/client/ui/components/tree/TreeSearchInput.vue @@ -0,0 +1,186 @@ + + + + + diff --git a/app/imports/ui/creature/CreatureForm.vue b/app/imports/client/ui/creature/CreatureForm.vue similarity index 73% rename from app/imports/ui/creature/CreatureForm.vue rename to app/imports/client/ui/creature/CreatureForm.vue index 1fdc4073..7c1b2533 100644 --- a/app/imports/ui/creature/CreatureForm.vue +++ b/app/imports/client/ui/creature/CreatureForm.vue @@ -2,55 +2,76 @@
- - + + + + + + + + @@ -62,6 +83,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 +91,7 @@ label="Discord Webhook URL" hint="This creature's logs will be posted to the discord channel" placeholder="https://discordapp.com/api/webhooks//" + :disabled="!editPermission" :value="model.settings.discordWebhook" @change="(value, ack) => $emit('change', {path: ['settings','discordWebhook'], value, ack})" /> @@ -96,12 +119,13 @@ + + + + mdi-graph + + Dependency Graph + +
diff --git a/app/imports/ui/creature/CreatureFormDialog.vue b/app/imports/client/ui/creature/CreatureFormDialog.vue similarity index 85% rename from app/imports/ui/creature/CreatureFormDialog.vue rename to app/imports/client/ui/creature/CreatureFormDialog.vue index 04fa4d19..6dbdb7bd 100644 --- a/app/imports/ui/creature/CreatureFormDialog.vue +++ b/app/imports/client/ui/creature/CreatureFormDialog.vue @@ -10,6 +10,7 @@ @@ -32,12 +33,12 @@ - - diff --git a/app/imports/client/ui/creature/actions/ActionDialog.vue b/app/imports/client/ui/creature/actions/ActionDialog.vue new file mode 100644 index 00000000..7929101b --- /dev/null +++ b/app/imports/client/ui/creature/actions/ActionDialog.vue @@ -0,0 +1,295 @@ + + + + + diff --git a/app/imports/client/ui/creature/actions/doAction.ts b/app/imports/client/ui/creature/actions/doAction.ts new file mode 100644 index 00000000..3973cdcc --- /dev/null +++ b/app/imports/client/ui/creature/actions/doAction.ts @@ -0,0 +1,110 @@ +import { Store } from 'vuex'; +import { insertAction } from '/imports/api/engine/action/methods/insertAction'; +import Task from '/imports/api/engine/action/tasks/Task'; +import EngineActions, { EngineAction } from '/imports/api/engine/action/EngineActions'; +import InputProvider from '/imports/api/engine/action/functions/userInput/InputProvider'; +import applyAction from '/imports/api/engine/action/functions/applyAction'; +import { runAction } from '/imports/api/engine/action/methods/runAction'; +import getDeterministicDiceRoller from '/imports/api/engine/action/functions/userInput/getDeterministicDiceRoller'; +import { getSingleProperty } from '../../../../api/engine/loadCreatures'; + +type BaseDoActionParams = { + creatureId: string; + $store: Store; + elementId: string; +} + +type DoTaskParams = BaseDoActionParams & { + task: Task; + propId?: undefined; +} + +type DoActionParams = BaseDoActionParams & { + propId: string; + task?: undefined; +} + +/** + * Apply an action on the client that first creates the action on both the client and server, then + * simulates the action, opening the action dialog if necessary to get input from the user, saving + * the decisions the user makes, then applying the action as a method call to the server with the + * saved decisions, which will persist the action results. + */ +export default async function doAction({ propId, creatureId, $store, elementId, task }: DoActionParams | DoTaskParams): Promise { + if (!task) { + if (!propId) throw new Meteor.Error('no-prop-id', 'Either propId or task must be provided'); + task = { + prop: getSingleProperty(creatureId, propId), + targetIds: [], + }; + } + // Create the action + const actionId = insertAction.call({ + action: { + creatureId, + task, + results: [], + taskCount: 0, + _decisions: [], + } + }); + + // Get the inserted and cleaned action instance + const action = EngineActions.findOne(actionId); + + if (!action) throw new Meteor.Error('not-found', 'The action could not be found'); + + // Applying the action is deterministic, so we apply it, if it asks for user input, we escape and + // create a dialog that will re-apply the action, but with the ability to actually get input + // Either way, call the action method afterwards + try { + const finishedAction = await applyAction( + action, getErrorOnInputRequestProvider(action._id), { simulate: true } + ); + return callActionMethod(finishedAction); + } catch (e) { + if (e !== 'input-requested') throw e; + return new Promise((resolve, reject) => { + $store.commit('pushDialogStack', { + component: 'action-dialog', + elementId, + data: { + actionId, + task, + }, + async callback(action: EngineAction) { + try { + if (action) await callActionMethod(action); + resolve(); + } + catch (e) { + reject(e); + } + return elementId; + }, + }); + }) + } +} + +const callActionMethod = (action: EngineAction) => { + if (!action._id) throw new Meteor.Error('type-error', 'Action must have and _id'); + return runAction.callAsync({ actionId: action._id, decisions: action._decisions }); +} + +const throwInputRequestedError = () => { + throw 'input-requested'; +} + +function getErrorOnInputRequestProvider(actionId) { + const errorOnInputRequest: InputProvider = { + targetIds: throwInputRequestedError, + nextStep: throwInputRequestedError, + rollDice: getDeterministicDiceRoller(actionId), + choose: throwInputRequestedError, + advantage: throwInputRequestedError, + check: throwInputRequestedError, + castSpell: throwInputRequestedError, + } + return errorOnInputRequest; +} diff --git a/app/imports/client/ui/creature/actions/input/AdvantageInput.vue b/app/imports/client/ui/creature/actions/input/AdvantageInput.vue new file mode 100644 index 00000000..b1bd1dd4 --- /dev/null +++ b/app/imports/client/ui/creature/actions/input/AdvantageInput.vue @@ -0,0 +1,59 @@ + + + diff --git a/app/imports/ui/properties/components/spells/CastSpellWithSlotDialog.vue b/app/imports/client/ui/creature/actions/input/CastSpellInput.vue similarity index 69% rename from app/imports/ui/properties/components/spells/CastSpellWithSlotDialog.vue rename to app/imports/client/ui/creature/actions/input/CastSpellInput.vue index dfdc0160..81d096fa 100644 --- a/app/imports/ui/properties/components/spells/CastSpellWithSlotDialog.vue +++ b/app/imports/client/ui/creature/actions/input/CastSpellInput.vue @@ -1,70 +1,64 @@ diff --git a/app/imports/client/ui/creature/actions/input/ChoiceInput.vue b/app/imports/client/ui/creature/actions/input/ChoiceInput.vue new file mode 100644 index 00000000..c594cfd1 --- /dev/null +++ b/app/imports/client/ui/creature/actions/input/ChoiceInput.vue @@ -0,0 +1,87 @@ + + + \ No newline at end of file diff --git a/app/imports/client/ui/creature/actions/input/RollInput.vue b/app/imports/client/ui/creature/actions/input/RollInput.vue new file mode 100644 index 00000000..3d515250 --- /dev/null +++ b/app/imports/client/ui/creature/actions/input/RollInput.vue @@ -0,0 +1,45 @@ + + + diff --git a/app/imports/client/ui/creature/actions/input/TargetsInput.vue b/app/imports/client/ui/creature/actions/input/TargetsInput.vue new file mode 100644 index 00000000..eb30e195 --- /dev/null +++ b/app/imports/client/ui/creature/actions/input/TargetsInput.vue @@ -0,0 +1,90 @@ + + + \ No newline at end of file diff --git a/app/imports/ui/creature/archive/ArchiveDialog.vue b/app/imports/client/ui/creature/archive/ArchiveDialog.vue similarity index 92% rename from app/imports/ui/creature/archive/ArchiveDialog.vue rename to app/imports/client/ui/creature/archive/ArchiveDialog.vue index 5d5b041d..8113ac42 100644 --- a/app/imports/ui/creature/archive/ArchiveDialog.vue +++ b/app/imports/client/ui/creature/archive/ArchiveDialog.vue @@ -57,16 +57,16 @@ + + diff --git a/app/imports/ui/creature/character/CharacterSheet.vue b/app/imports/client/ui/creature/character/CharacterSheet.vue similarity index 56% rename from app/imports/ui/creature/character/CharacterSheet.vue rename to app/imports/client/ui/creature/character/CharacterSheet.vue index e2e225cc..63a842b6 100644 --- a/app/imports/ui/creature/character/CharacterSheet.vue +++ b/app/imports/client/ui/creature/character/CharacterSheet.vue @@ -30,31 +30,33 @@
- + + + + - - + + @@ -68,40 +70,102 @@
+ + + + Stats + mdi-chart-box + + + Actions + mdi-lightning-bolt + + + Spells + mdi-fire + + + Inventory + mdi-cube + + + Features + mdi-text + + + Journal + mdi-book-open-variant + + + Build + mdi-wrench + + + Tree + mdi-file-tree + + + + diff --git a/app/imports/ui/creature/character/CharacterSheetFab.vue b/app/imports/client/ui/creature/character/CharacterSheetFab.vue similarity index 72% rename from app/imports/ui/creature/character/CharacterSheetFab.vue rename to app/imports/client/ui/creature/character/CharacterSheetFab.vue index 5af7abd0..26564325 100644 --- a/app/imports/ui/creature/character/CharacterSheetFab.vue +++ b/app/imports/client/ui/creature/character/CharacterSheetFab.vue @@ -1,7 +1,7 @@ @@ -250,7 +311,7 @@ export default { background: none !important; } -.character-sheet-fab { +.character-sheet-extension-fab { bottom: -24px; right: 8px; margin-left: 16px; diff --git a/app/imports/ui/creature/character/CreatureRootDialog.vue b/app/imports/client/ui/creature/character/CreatureRootDialog.vue similarity index 79% rename from app/imports/ui/creature/character/CreatureRootDialog.vue rename to app/imports/client/ui/creature/character/CreatureRootDialog.vue index 9d9ecffe..8d6c9730 100644 --- a/app/imports/ui/creature/character/CreatureRootDialog.vue +++ b/app/imports/client/ui/creature/character/CreatureRootDialog.vue @@ -63,18 +63,17 @@ diff --git a/app/imports/ui/creature/character/characterSheetTabs/BuildTab.vue b/app/imports/client/ui/creature/character/characterSheetTabs/BuildTab.vue similarity index 83% rename from app/imports/ui/creature/character/characterSheetTabs/BuildTab.vue rename to app/imports/client/ui/creature/character/characterSheetTabs/BuildTab.vue index b4e4d9a4..68cdb813 100644 --- a/app/imports/ui/creature/character/characterSheetTabs/BuildTab.vue +++ b/app/imports/client/ui/creature/character/characterSheetTabs/BuildTab.vue @@ -13,9 +13,19 @@ + + + @@ -76,14 +86,12 @@ + + + + + diff --git a/app/imports/ui/creature/character/characterSheetTabs/InventoryTab.vue b/app/imports/client/ui/creature/character/characterSheetTabs/InventoryTab.vue similarity index 68% rename from app/imports/ui/creature/character/characterSheetTabs/InventoryTab.vue rename to app/imports/client/ui/creature/character/characterSheetTabs/InventoryTab.vue index ea77fddd..13655b90 100644 --- a/app/imports/ui/creature/character/characterSheetTabs/InventoryTab.vue +++ b/app/imports/client/ui/creature/character/characterSheetTabs/InventoryTab.vue @@ -1,6 +1,14 @@ diff --git a/app/imports/client/ui/creature/character/characterSheetTabs/JournalTab.vue b/app/imports/client/ui/creature/character/characterSheetTabs/JournalTab.vue new file mode 100644 index 00000000..7ea6007c --- /dev/null +++ b/app/imports/client/ui/creature/character/characterSheetTabs/JournalTab.vue @@ -0,0 +1,105 @@ + + + + + \ No newline at end of file diff --git a/app/imports/client/ui/creature/character/characterSheetTabs/SpellsTab.vue b/app/imports/client/ui/creature/character/characterSheetTabs/SpellsTab.vue new file mode 100644 index 00000000..a84c9872 --- /dev/null +++ b/app/imports/client/ui/creature/character/characterSheetTabs/SpellsTab.vue @@ -0,0 +1,185 @@ + + + + + diff --git a/app/imports/client/ui/creature/character/characterSheetTabs/StatsTab.vue b/app/imports/client/ui/creature/character/characterSheetTabs/StatsTab.vue new file mode 100644 index 00000000..63a1c5f6 --- /dev/null +++ b/app/imports/client/ui/creature/character/characterSheetTabs/StatsTab.vue @@ -0,0 +1,653 @@ + + + diff --git a/app/imports/ui/creature/character/characterSheetTabs/TreeTab.vue b/app/imports/client/ui/creature/character/characterSheetTabs/TreeTab.vue similarity index 87% rename from app/imports/ui/creature/character/characterSheetTabs/TreeTab.vue rename to app/imports/client/ui/creature/character/characterSheetTabs/TreeTab.vue index ae2988fc..bef24336 100644 --- a/app/imports/ui/creature/character/characterSheetTabs/TreeTab.vue +++ b/app/imports/client/ui/creature/character/characterSheetTabs/TreeTab.vue @@ -11,9 +11,14 @@ @@ -54,12 +54,12 @@ + + \ No newline at end of file diff --git a/app/imports/ui/creature/character/printedCharacterSheet/PrintedInventory.vue b/app/imports/client/ui/creature/character/printedCharacterSheet/PrintedInventory.vue similarity index 50% rename from app/imports/ui/creature/character/printedCharacterSheet/PrintedInventory.vue rename to app/imports/client/ui/creature/character/printedCharacterSheet/PrintedInventory.vue index 53694a81..0540dec4 100644 --- a/app/imports/ui/creature/character/printedCharacterSheet/PrintedInventory.vue +++ b/app/imports/client/ui/creature/character/printedCharacterSheet/PrintedInventory.vue @@ -1,105 +1,94 @@ diff --git a/app/imports/ui/creature/character/printedCharacterSheet/PrintedSpells.vue b/app/imports/client/ui/creature/character/printedCharacterSheet/PrintedSpells.vue similarity index 59% rename from app/imports/ui/creature/character/printedCharacterSheet/PrintedSpells.vue rename to app/imports/client/ui/creature/character/printedCharacterSheet/PrintedSpells.vue index 1d60374b..3ac63b7f 100644 --- a/app/imports/ui/creature/character/printedCharacterSheet/PrintedSpells.vue +++ b/app/imports/client/ui/creature/character/printedCharacterSheet/PrintedSpells.vue @@ -1,47 +1,48 @@ + + \ No newline at end of file diff --git a/app/imports/ui/creature/character/printedCharacterSheet/components/PrintedSkill.vue b/app/imports/client/ui/creature/character/printedCharacterSheet/components/PrintedSkill.vue similarity index 88% rename from app/imports/ui/creature/character/printedCharacterSheet/components/PrintedSkill.vue rename to app/imports/client/ui/creature/character/printedCharacterSheet/components/PrintedSkill.vue index 2920634c..d61526fe 100644 --- a/app/imports/ui/creature/character/printedCharacterSheet/components/PrintedSkill.vue +++ b/app/imports/client/ui/creature/character/printedCharacterSheet/components/PrintedSkill.vue @@ -11,7 +11,7 @@ :value="model.proficiency" class="prof-icon" /> -
+
{{ displayedModifier }}
+ + diff --git a/app/imports/ui/creature/character/printedCharacterSheet/components/PrintedSpellList.vue b/app/imports/client/ui/creature/character/printedCharacterSheet/components/PrintedSpellList.vue similarity index 82% rename from app/imports/ui/creature/character/printedCharacterSheet/components/PrintedSpellList.vue rename to app/imports/client/ui/creature/character/printedCharacterSheet/components/PrintedSpellList.vue index e158000f..13ec0b26 100644 --- a/app/imports/ui/creature/character/printedCharacterSheet/components/PrintedSpellList.vue +++ b/app/imports/client/ui/creature/character/printedCharacterSheet/components/PrintedSpellList.vue @@ -1,5 +1,8 @@ + + diff --git a/app/imports/ui/creature/creatureList/CreatureStorageStats.vue b/app/imports/client/ui/creature/creatureList/CreatureStorageStats.vue similarity index 67% rename from app/imports/ui/creature/creatureList/CreatureStorageStats.vue rename to app/imports/client/ui/creature/creatureList/CreatureStorageStats.vue index 6bbcfb66..1f1027d4 100644 --- a/app/imports/ui/creature/creatureList/CreatureStorageStats.vue +++ b/app/imports/client/ui/creature/creatureList/CreatureStorageStats.vue @@ -1,21 +1,23 @@ + + diff --git a/app/imports/ui/creature/creatureProperties/CreaturePropertyDialog.vue b/app/imports/client/ui/creature/creatureProperties/CreaturePropertyDialog.vue similarity index 52% rename from app/imports/ui/creature/creatureProperties/CreaturePropertyDialog.vue rename to app/imports/client/ui/creature/creatureProperties/CreaturePropertyDialog.vue index da516221..a31ac8f6 100644 --- a/app/imports/ui/creature/creatureProperties/CreaturePropertyDialog.vue +++ b/app/imports/client/ui/creature/creatureProperties/CreaturePropertyDialog.vue @@ -9,20 +9,20 @@ style="flex-grow: 0;" @duplicate="duplicate" @remove="remove" + @copy-to-library="copyToLibrary" @toggle-editing="editing = !editing" - @color-changed="value => change({path: ['color'], value})" /> -
- - +
- Close - -
+ + + Close + +
+ diff --git a/app/imports/client/ui/creature/dependencyGraph/DependencyGraphDialog.vue b/app/imports/client/ui/creature/dependencyGraph/DependencyGraphDialog.vue new file mode 100644 index 00000000..df8d83cf --- /dev/null +++ b/app/imports/client/ui/creature/dependencyGraph/DependencyGraphDialog.vue @@ -0,0 +1,215 @@ + + + + + \ No newline at end of file diff --git a/app/imports/ui/creature/experiences/ExperienceForm.vue b/app/imports/client/ui/creature/experiences/ExperienceForm.vue similarity index 94% rename from app/imports/ui/creature/experiences/ExperienceForm.vue rename to app/imports/client/ui/creature/experiences/ExperienceForm.vue index e80040aa..61cb0e12 100644 --- a/app/imports/ui/creature/experiences/ExperienceForm.vue +++ b/app/imports/client/ui/creature/experiences/ExperienceForm.vue @@ -38,7 +38,7 @@ diff --git a/app/imports/client/ui/creature/slots/SlotCardsToFill.vue b/app/imports/client/ui/creature/slots/SlotCardsToFill.vue new file mode 100644 index 00000000..71da9799 --- /dev/null +++ b/app/imports/client/ui/creature/slots/SlotCardsToFill.vue @@ -0,0 +1,122 @@ + + + + + diff --git a/app/imports/client/ui/creature/slots/SlotFillDialog.vue b/app/imports/client/ui/creature/slots/SlotFillDialog.vue new file mode 100644 index 00000000..2767ad0c --- /dev/null +++ b/app/imports/client/ui/creature/slots/SlotFillDialog.vue @@ -0,0 +1,561 @@ + + + + + +resolveimport { toString } from '/imports/parser/toString'; diff --git a/app/imports/ui/dialogStack/DeleteConfirmationDialog.vue b/app/imports/client/ui/dialogStack/DeleteConfirmationDialog.vue similarity index 95% rename from app/imports/ui/dialogStack/DeleteConfirmationDialog.vue rename to app/imports/client/ui/dialogStack/DeleteConfirmationDialog.vue index c8b76735..71fbd4ba 100644 --- a/app/imports/ui/dialogStack/DeleteConfirmationDialog.vue +++ b/app/imports/client/ui/dialogStack/DeleteConfirmationDialog.vue @@ -41,7 +41,7 @@ + + diff --git a/app/imports/ui/dialogStack/HelpDialog.vue b/app/imports/client/ui/dialogStack/HelpDialog.vue similarity index 84% rename from app/imports/ui/dialogStack/HelpDialog.vue rename to app/imports/client/ui/dialogStack/HelpDialog.vue index 9fe700b2..e9f3407d 100644 --- a/app/imports/ui/dialogStack/HelpDialog.vue +++ b/app/imports/client/ui/dialogStack/HelpDialog.vue @@ -37,10 +37,10 @@ \ No newline at end of file diff --git a/app/imports/client/ui/docs/DocCard.vue b/app/imports/client/ui/docs/DocCard.vue new file mode 100644 index 00000000..7ad6c8bd --- /dev/null +++ b/app/imports/client/ui/docs/DocCard.vue @@ -0,0 +1,57 @@ + + + + + \ No newline at end of file diff --git a/app/imports/client/ui/docs/DocEditForm.vue b/app/imports/client/ui/docs/DocEditForm.vue new file mode 100644 index 00000000..c71a4c7d --- /dev/null +++ b/app/imports/client/ui/docs/DocEditForm.vue @@ -0,0 +1,210 @@ + + + \ No newline at end of file diff --git a/app/imports/client/ui/docs/DocListItem.vue b/app/imports/client/ui/docs/DocListItem.vue new file mode 100644 index 00000000..3b7f1675 --- /dev/null +++ b/app/imports/client/ui/docs/DocListItem.vue @@ -0,0 +1,30 @@ + + + diff --git a/app/imports/client/ui/docs/DocToolbar.vue b/app/imports/client/ui/docs/DocToolbar.vue new file mode 100644 index 00000000..eb65aabf --- /dev/null +++ b/app/imports/client/ui/docs/DocToolbar.vue @@ -0,0 +1,62 @@ + + + diff --git a/app/imports/client/ui/docs/DocViewer.vue b/app/imports/client/ui/docs/DocViewer.vue new file mode 100644 index 00000000..90738a4e --- /dev/null +++ b/app/imports/client/ui/docs/DocViewer.vue @@ -0,0 +1,161 @@ + + + + + \ No newline at end of file diff --git a/app/imports/client/ui/docs/DocsRightDrawer.vue b/app/imports/client/ui/docs/DocsRightDrawer.vue new file mode 100644 index 00000000..a4feb76b --- /dev/null +++ b/app/imports/client/ui/docs/DocsRightDrawer.vue @@ -0,0 +1,69 @@ + + + + + diff --git a/app/imports/client/ui/docs/getDocLink.js b/app/imports/client/ui/docs/getDocLink.js new file mode 100644 index 00000000..0d3be2cb --- /dev/null +++ b/app/imports/client/ui/docs/getDocLink.js @@ -0,0 +1,9 @@ +export default function getDocLink(doc, urlName) { + if (!urlName) urlName = doc.urlName; + const address = ['/docs']; + doc.ancestors?.forEach(a => { + address.push(a.urlName); + }); + address.push(urlName); + return address.join('/'); +} \ No newline at end of file diff --git a/app/imports/ui/files/ArchiveFileCard.vue b/app/imports/client/ui/files/ArchiveFileCard.vue similarity index 89% rename from app/imports/ui/files/ArchiveFileCard.vue rename to app/imports/client/ui/files/ArchiveFileCard.vue index d8ec1f25..9eb926ac 100644 --- a/app/imports/ui/files/ArchiveFileCard.vue +++ b/app/imports/client/ui/files/ArchiveFileCard.vue @@ -10,6 +10,7 @@ Restore @@ -32,10 +33,10 @@ diff --git a/app/imports/client/ui/files/userImages/ImageInputDialog.vue b/app/imports/client/ui/files/userImages/ImageInputDialog.vue new file mode 100644 index 00000000..d9f04b5d --- /dev/null +++ b/app/imports/client/ui/files/userImages/ImageInputDialog.vue @@ -0,0 +1,194 @@ + + + + + diff --git a/app/imports/client/ui/files/userImages/ImagePreviewDialog.vue b/app/imports/client/ui/files/userImages/ImagePreviewDialog.vue new file mode 100644 index 00000000..bc75b65d --- /dev/null +++ b/app/imports/client/ui/files/userImages/ImagePreviewDialog.vue @@ -0,0 +1,47 @@ + + + + + diff --git a/app/imports/client/ui/files/userImages/UserImageCard.vue b/app/imports/client/ui/files/userImages/UserImageCard.vue new file mode 100644 index 00000000..0ecf75c3 --- /dev/null +++ b/app/imports/client/ui/files/userImages/UserImageCard.vue @@ -0,0 +1,109 @@ + + + + + diff --git a/app/imports/ui/icons/IconAdmin.vue b/app/imports/client/ui/icons/IconAdmin.vue similarity index 88% rename from app/imports/ui/icons/IconAdmin.vue rename to app/imports/client/ui/icons/IconAdmin.vue index 3219be83..783a785e 100644 --- a/app/imports/ui/icons/IconAdmin.vue +++ b/app/imports/client/ui/icons/IconAdmin.vue @@ -27,8 +27,8 @@ \ No newline at end of file diff --git a/app/imports/client/ui/layouts/Sidebar.vue b/app/imports/client/ui/layouts/Sidebar.vue new file mode 100644 index 00000000..a77de3ee --- /dev/null +++ b/app/imports/client/ui/layouts/Sidebar.vue @@ -0,0 +1,151 @@ + + + + + diff --git a/app/imports/ui/layouts/SingleCardLayout.vue b/app/imports/client/ui/layouts/SingleCardLayout.vue similarity index 100% rename from app/imports/ui/layouts/SingleCardLayout.vue rename to app/imports/client/ui/layouts/SingleCardLayout.vue diff --git a/app/imports/client/ui/library/InsertLibraryNodeButton.vue b/app/imports/client/ui/library/InsertLibraryNodeButton.vue new file mode 100644 index 00000000..34a16cdb --- /dev/null +++ b/app/imports/client/ui/library/InsertLibraryNodeButton.vue @@ -0,0 +1,80 @@ + + + + + diff --git a/app/imports/ui/library/LibraryAndNode.vue b/app/imports/client/ui/library/LibraryAndNode.vue similarity index 67% rename from app/imports/ui/library/LibraryAndNode.vue rename to app/imports/client/ui/library/LibraryAndNode.vue index aa6601db..f6df57c6 100644 --- a/app/imports/ui/library/LibraryAndNode.vue +++ b/app/imports/client/ui/library/LibraryAndNode.vue @@ -1,5 +1,12 @@ + + diff --git a/app/imports/ui/library/LibraryCollectionCreationDialog.vue b/app/imports/client/ui/library/LibraryCollectionCreationDialog.vue similarity index 94% rename from app/imports/ui/library/LibraryCollectionCreationDialog.vue rename to app/imports/client/ui/library/LibraryCollectionCreationDialog.vue index ed764930..f65895b7 100644 --- a/app/imports/ui/library/LibraryCollectionCreationDialog.vue +++ b/app/imports/client/ui/library/LibraryCollectionCreationDialog.vue @@ -44,8 +44,8 @@ diff --git a/app/imports/ui/library/LibraryCollectionHeader.vue b/app/imports/client/ui/library/LibraryCollectionHeader.vue similarity index 93% rename from app/imports/ui/library/LibraryCollectionHeader.vue rename to app/imports/client/ui/library/LibraryCollectionHeader.vue index 39713450..247387d9 100644 --- a/app/imports/ui/library/LibraryCollectionHeader.vue +++ b/app/imports/client/ui/library/LibraryCollectionHeader.vue @@ -5,7 +5,7 @@ :class="isSelected && !disabled && 'primary--text v-list-item--active'" > diff --git a/app/imports/ui/library/LibraryContentsContainer.vue b/app/imports/client/ui/library/LibraryContentsContainer.vue similarity index 63% rename from app/imports/ui/library/LibraryContentsContainer.vue rename to app/imports/client/ui/library/LibraryContentsContainer.vue index 1e70fa07..6bda5be4 100644 --- a/app/imports/ui/library/LibraryContentsContainer.vue +++ b/app/imports/client/ui/library/LibraryContentsContainer.vue @@ -6,9 +6,10 @@ :children="libraryChildren" :organize="organizeMode" :selected-node="selectedNode" + :root="{collection: 'libraries', id: libraryId}" @selected="e => $emit('selected', e)" - @reordered="reordered" - @reorganized="reorganized" + @move-within-root="moveWithinRoot" + @move-between-roots="moveBetweenRoots" /> diff --git a/app/imports/ui/library/LibraryList.vue b/app/imports/client/ui/library/LibraryList.vue similarity index 88% rename from app/imports/ui/library/LibraryList.vue rename to app/imports/client/ui/library/LibraryList.vue index c850ddd7..e9b74c77 100644 --- a/app/imports/ui/library/LibraryList.vue +++ b/app/imports/client/ui/library/LibraryList.vue @@ -9,6 +9,7 @@ :model="library" :to="{ name: 'singleLibrary', params: { id: library._id }}" :selection="selection" + :single-select="singleSelect" :is-selected="librariesSelected && librariesSelected.includes(library._id)" :selected-by-collection="librariesSelectedByCollections && librariesSelectedByCollections.includes(library._id)" :disabled="disabled" @@ -26,6 +27,7 @@ :open="openCollections[libraryCollection._id]" :model="libraryCollection" :selection="selection" + :single-select="singleSelect" :is-selected="libraryCollectionsSelected && libraryCollectionsSelected.includes(libraryCollection._id)" :disabled="disabled" @select="val => $emit('select-library-collection', libraryCollection._id, val)" @@ -37,6 +39,7 @@ :model="library" :to="{ name: 'singleLibrary', params: { id: library._id }}" :selection="selection" + :single-select="singleSelect" :is-selected="librariesSelected && librariesSelected.includes(library._id)" :selected-by-collection="librariesSelectedByCollections && librariesSelectedByCollections.includes(library._id)" :disabled="disabled" @@ -44,30 +47,24 @@ @select="val => $emit('select-library', library._id, val)" /> - - - - - + + + + + + + diff --git a/app/imports/client/ui/library/LibraryNodeExpansionContent.vue b/app/imports/client/ui/library/LibraryNodeExpansionContent.vue new file mode 100644 index 00000000..a19745fe --- /dev/null +++ b/app/imports/client/ui/library/LibraryNodeExpansionContent.vue @@ -0,0 +1,81 @@ + + + + + diff --git a/app/imports/client/ui/library/LibrarySecondTree.vue b/app/imports/client/ui/library/LibrarySecondTree.vue new file mode 100644 index 00000000..4d322a21 --- /dev/null +++ b/app/imports/client/ui/library/LibrarySecondTree.vue @@ -0,0 +1,125 @@ + + + + + diff --git a/app/imports/ui/library/MoveLibraryNodeDialog.vue b/app/imports/client/ui/library/MoveLibraryNodeDialog.vue similarity index 83% rename from app/imports/ui/library/MoveLibraryNodeDialog.vue rename to app/imports/client/ui/library/MoveLibraryNodeDialog.vue index f0d22101..7151391d 100644 --- a/app/imports/ui/library/MoveLibraryNodeDialog.vue +++ b/app/imports/client/ui/library/MoveLibraryNodeDialog.vue @@ -23,8 +23,8 @@ diff --git a/app/imports/ui/log/CharacterLog.vue b/app/imports/client/ui/log/CharacterLog.vue similarity index 51% rename from app/imports/ui/log/CharacterLog.vue rename to app/imports/client/ui/log/CharacterLog.vue index 4cfdcdab..bde5c6aa 100644 --- a/app/imports/ui/log/CharacterLog.vue +++ b/app/imports/client/ui/log/CharacterLog.vue @@ -25,21 +25,26 @@ :hint="inputHint" :error-messages="inputError" :disabled="!editPermission" + :loading="submitLoading" @click:append-outer="submit" @keyup.enter="submit" + @keyup.up="decrementHistory" + @keyup.down="incrementHistory" />
diff --git a/app/imports/ui/log/LogEntry.vue b/app/imports/client/ui/log/LogEntry.vue similarity index 68% rename from app/imports/ui/log/LogEntry.vue rename to app/imports/client/ui/log/LogEntry.vue index 90f7087f..b8739bde 100644 --- a/app/imports/ui/log/LogEntry.vue +++ b/app/imports/client/ui/log/LogEntry.vue @@ -2,6 +2,9 @@ + + {{ model.creatureName }} + diff --git a/app/imports/client/ui/log/TabletopLog.vue b/app/imports/client/ui/log/TabletopLog.vue new file mode 100644 index 00000000..0b5feb5c --- /dev/null +++ b/app/imports/client/ui/log/TabletopLog.vue @@ -0,0 +1,188 @@ + + + + + +resolveimport { toString } from '/imports/parser/toString'; diff --git a/app/imports/client/ui/log/TabletopLogContent.vue b/app/imports/client/ui/log/TabletopLogContent.vue new file mode 100644 index 00000000..19103458 --- /dev/null +++ b/app/imports/client/ui/log/TabletopLogContent.vue @@ -0,0 +1,126 @@ + + + + + + + diff --git a/app/imports/client/ui/log/TabletopLogEntry.vue b/app/imports/client/ui/log/TabletopLogEntry.vue new file mode 100644 index 00000000..73a04e0c --- /dev/null +++ b/app/imports/client/ui/log/TabletopLogEntry.vue @@ -0,0 +1,70 @@ + + + diff --git a/app/imports/ui/markdownCofig.js b/app/imports/client/ui/markdownCofig.js similarity index 86% rename from app/imports/ui/markdownCofig.js rename to app/imports/client/ui/markdownCofig.js index c0ff6a03..651ff39e 100644 --- a/app/imports/ui/markdownCofig.js +++ b/app/imports/client/ui/markdownCofig.js @@ -4,7 +4,6 @@ import DOMPurify from 'dompurify'; marked.setOptions({ breaks: true, gfm: true, - sanitizer: DOMPurify.sanitize, silent: true, smartLists: true, smartypants: true, diff --git a/app/imports/ui/pages/About.vue b/app/imports/client/ui/pages/About.vue similarity index 88% rename from app/imports/ui/pages/About.vue rename to app/imports/client/ui/pages/About.vue index e643dcd1..67fa476b 100644 --- a/app/imports/ui/pages/About.vue +++ b/app/imports/client/ui/pages/About.vue @@ -2,8 +2,8 @@

DiceCloud is a single-developer project started in 2014 with the aim of @@ -87,7 +87,16 @@ export default { name: 'Dai', title: 'A Kobold\'s Best Friend', avatar: 'dai' - },], + }, { + name: 'Vibes', + title: 'Kell of Nothing', + avatar: 'vibes' + }, { + name: 'ßlue', + title: 'Embodiment of Greed', + avatar: 'blue' + }, + ], }}, } diff --git a/app/imports/ui/pages/Account.vue b/app/imports/client/ui/pages/Account.vue similarity index 89% rename from app/imports/ui/pages/Account.vue rename to app/imports/client/ui/pages/Account.vue index 1deea14a..195db51d 100644 --- a/app/imports/ui/pages/Account.vue +++ b/app/imports/client/ui/pages/Account.vue @@ -20,15 +20,18 @@ - + Preferences - @@ -226,15 +229,15 @@ + + \ No newline at end of file diff --git a/app/imports/ui/pages/Documentation.vue b/app/imports/client/ui/pages/Documentation.vue similarity index 90% rename from app/imports/ui/pages/Documentation.vue rename to app/imports/client/ui/pages/Documentation.vue index d8c83c53..e43d2a29 100644 --- a/app/imports/ui/pages/Documentation.vue +++ b/app/imports/client/ui/pages/Documentation.vue @@ -35,9 +35,9 @@ + + diff --git a/app/imports/ui/pages/InviteError.vue b/app/imports/client/ui/pages/InviteError.vue similarity index 100% rename from app/imports/ui/pages/InviteError.vue rename to app/imports/client/ui/pages/InviteError.vue diff --git a/app/imports/ui/pages/InviteSuccess.vue b/app/imports/client/ui/pages/InviteSuccess.vue similarity index 100% rename from app/imports/ui/pages/InviteSuccess.vue rename to app/imports/client/ui/pages/InviteSuccess.vue diff --git a/app/imports/ui/pages/LaunchCountdown.vue b/app/imports/client/ui/pages/LaunchCountdown.vue similarity index 94% rename from app/imports/ui/pages/LaunchCountdown.vue rename to app/imports/client/ui/pages/LaunchCountdown.vue index 100c1db6..025a64df 100644 --- a/app/imports/ui/pages/LaunchCountdown.vue +++ b/app/imports/client/ui/pages/LaunchCountdown.vue @@ -27,7 +27,7 @@ diff --git a/app/imports/ui/pages/LibraryCollection.vue b/app/imports/client/ui/pages/LibraryCollection.vue similarity index 91% rename from app/imports/ui/pages/LibraryCollection.vue rename to app/imports/client/ui/pages/LibraryCollection.vue index 414e2320..3fd82153 100644 --- a/app/imports/ui/pages/LibraryCollection.vue +++ b/app/imports/client/ui/pages/LibraryCollection.vue @@ -44,9 +44,9 @@ diff --git a/app/imports/ui/pages/PatreonLevelTooLow.vue b/app/imports/client/ui/pages/PatreonLevelTooLow.vue similarity index 98% rename from app/imports/ui/pages/PatreonLevelTooLow.vue rename to app/imports/client/ui/pages/PatreonLevelTooLow.vue index 0fd34646..6957b722 100644 --- a/app/imports/ui/pages/PatreonLevelTooLow.vue +++ b/app/imports/client/ui/pages/PatreonLevelTooLow.vue @@ -23,7 +23,7 @@ + + diff --git a/app/imports/ui/creature/creatureProperties/AddCreaturePropertyDialog.vue b/app/imports/client/ui/properties/InsertPropertyDialog.vue similarity index 78% rename from app/imports/ui/creature/creatureProperties/AddCreaturePropertyDialog.vue rename to app/imports/client/ui/properties/InsertPropertyDialog.vue index 60eea258..78904e89 100644 --- a/app/imports/ui/creature/creatureProperties/AddCreaturePropertyDialog.vue +++ b/app/imports/client/ui/properties/InsertPropertyDialog.vue @@ -16,7 +16,7 @@ flat @change="propertyHelpChanged" /> - Create - + Library @@ -56,31 +59,43 @@ > - + - - + - Cancel + {{ tab === 1 ? "Discard" : "Cancel" }} diff --git a/app/imports/client/ui/properties/PropertyForm.vue b/app/imports/client/ui/properties/PropertyForm.vue new file mode 100644 index 00000000..c0643a44 --- /dev/null +++ b/app/imports/client/ui/properties/PropertyForm.vue @@ -0,0 +1,311 @@ + + + diff --git a/app/imports/client/ui/properties/components/PropertyCard.vue b/app/imports/client/ui/properties/components/PropertyCard.vue new file mode 100644 index 00000000..b22ec03e --- /dev/null +++ b/app/imports/client/ui/properties/components/PropertyCard.vue @@ -0,0 +1,30 @@ + + + diff --git a/app/imports/client/ui/properties/components/actions/ActionCard.vue b/app/imports/client/ui/properties/components/actions/ActionCard.vue new file mode 100644 index 00000000..ab3dfce4 --- /dev/null +++ b/app/imports/client/ui/properties/components/actions/ActionCard.vue @@ -0,0 +1,307 @@ + + + + + + + diff --git a/app/imports/client/ui/properties/components/actions/ActionConditionView.vue b/app/imports/client/ui/properties/components/actions/ActionConditionView.vue new file mode 100644 index 00000000..309e9389 --- /dev/null +++ b/app/imports/client/ui/properties/components/actions/ActionConditionView.vue @@ -0,0 +1,29 @@ + + + diff --git a/app/imports/ui/properties/components/actions/AttributeConsumedView.vue b/app/imports/client/ui/properties/components/actions/AttributeConsumedView.vue similarity index 54% rename from app/imports/ui/properties/components/actions/AttributeConsumedView.vue rename to app/imports/client/ui/properties/components/actions/AttributeConsumedView.vue index 26571332..2d152209 100644 --- a/app/imports/ui/properties/components/actions/AttributeConsumedView.vue +++ b/app/imports/client/ui/properties/components/actions/AttributeConsumedView.vue @@ -4,16 +4,24 @@ :class="insufficient && 'error--text'" >

- {{ model.quantity && model.quantity.value }} + {{ model.quantity.value }}
{{ model.statName || model.variableName }}
+
+ ({{ model.available }}) +
diff --git a/app/imports/client/ui/properties/components/actions/EventButton.vue b/app/imports/client/ui/properties/components/actions/EventButton.vue new file mode 100644 index 00000000..518c1598 --- /dev/null +++ b/app/imports/client/ui/properties/components/actions/EventButton.vue @@ -0,0 +1,68 @@ + + + + + diff --git a/app/imports/ui/properties/components/actions/ItemConsumedView.vue b/app/imports/client/ui/properties/components/actions/ItemConsumedView.vue similarity index 78% rename from app/imports/ui/properties/components/actions/ItemConsumedView.vue rename to app/imports/client/ui/properties/components/actions/ItemConsumedView.vue index 3363c5a4..11be31fb 100644 --- a/app/imports/ui/properties/components/actions/ItemConsumedView.vue +++ b/app/imports/client/ui/properties/components/actions/ItemConsumedView.vue @@ -27,28 +27,30 @@ :color="model.itemColor" />
- - + {{ quantity }}
-
- - +
- Select item - + {{ model.itemName }} +
+
+ ({{ model.available }}) +
+ +
+ Select item
\ No newline at end of file diff --git a/app/imports/ui/properties/components/attributes/HitDiceListTile.vue b/app/imports/client/ui/properties/components/attributes/HitDiceListTile.vue similarity index 96% rename from app/imports/ui/properties/components/attributes/HitDiceListTile.vue rename to app/imports/client/ui/properties/components/attributes/HitDiceListTile.vue index 7cfff667..19359d05 100644 --- a/app/imports/ui/properties/components/attributes/HitDiceListTile.vue +++ b/app/imports/client/ui/properties/components/attributes/HitDiceListTile.vue @@ -56,7 +56,7 @@ diff --git a/app/imports/ui/properties/components/attributes/ResourceCardContent.vue b/app/imports/client/ui/properties/components/attributes/ResourceCardContent.vue similarity index 60% rename from app/imports/ui/properties/components/attributes/ResourceCardContent.vue rename to app/imports/client/ui/properties/components/attributes/ResourceCardContent.vue index 1f9de91f..325651be 100644 --- a/app/imports/ui/properties/components/attributes/ResourceCardContent.vue +++ b/app/imports/client/ui/properties/components/attributes/ResourceCardContent.vue @@ -1,26 +1,28 @@ + + diff --git a/app/imports/client/ui/properties/components/attributes/SpellSlotListTile.vue b/app/imports/client/ui/properties/components/attributes/SpellSlotListTile.vue new file mode 100644 index 00000000..af69866c --- /dev/null +++ b/app/imports/client/ui/properties/components/attributes/SpellSlotListTile.vue @@ -0,0 +1,157 @@ + + + + + diff --git a/app/imports/ui/properties/components/attributes/SplitListLayout.vue b/app/imports/client/ui/properties/components/attributes/SplitListLayout.vue similarity index 100% rename from app/imports/ui/properties/components/attributes/SplitListLayout.vue rename to app/imports/client/ui/properties/components/attributes/SplitListLayout.vue diff --git a/app/imports/ui/properties/components/buffs/BuffListItem.vue b/app/imports/client/ui/properties/components/buffs/BuffListItem.vue similarity index 91% rename from app/imports/ui/properties/components/buffs/BuffListItem.vue rename to app/imports/client/ui/properties/components/buffs/BuffListItem.vue index b122f33c..ef2cb391 100644 --- a/app/imports/ui/properties/components/buffs/BuffListItem.vue +++ b/app/imports/client/ui/properties/components/buffs/BuffListItem.vue @@ -10,7 +10,7 @@ mdi-delete diff --git a/app/imports/ui/properties/components/damageMultipliers/DamageMultiplierCard.vue b/app/imports/client/ui/properties/components/damageMultipliers/DamageMultiplierCard.vue similarity index 100% rename from app/imports/ui/properties/components/damageMultipliers/DamageMultiplierCard.vue rename to app/imports/client/ui/properties/components/damageMultipliers/DamageMultiplierCard.vue diff --git a/app/imports/ui/properties/components/effects/InlineEffect.vue b/app/imports/client/ui/properties/components/effects/InlineEffect.vue similarity index 91% rename from app/imports/ui/properties/components/effects/InlineEffect.vue rename to app/imports/client/ui/properties/components/effects/InlineEffect.vue index 9ac97e38..fdfed3c9 100644 --- a/app/imports/ui/properties/components/effects/InlineEffect.vue +++ b/app/imports/client/ui/properties/components/effects/InlineEffect.vue @@ -32,17 +32,23 @@ + + diff --git a/app/imports/client/ui/properties/components/folders/folderGroupComponents/FolderGroupChildren.vue b/app/imports/client/ui/properties/components/folders/folderGroupComponents/FolderGroupChildren.vue new file mode 100644 index 00000000..30cfd16d --- /dev/null +++ b/app/imports/client/ui/properties/components/folders/folderGroupComponents/FolderGroupChildren.vue @@ -0,0 +1,67 @@ + + + diff --git a/app/imports/client/ui/properties/components/folders/folderGroupComponents/SlotBuildTree.vue b/app/imports/client/ui/properties/components/folders/folderGroupComponents/SlotBuildTree.vue new file mode 100644 index 00000000..acc27dbf --- /dev/null +++ b/app/imports/client/ui/properties/components/folders/folderGroupComponents/SlotBuildTree.vue @@ -0,0 +1,82 @@ + + + \ No newline at end of file diff --git a/app/imports/client/ui/properties/components/folders/propertyComponentIndex.js b/app/imports/client/ui/properties/components/folders/propertyComponentIndex.js new file mode 100644 index 00000000..8676d317 --- /dev/null +++ b/app/imports/client/ui/properties/components/folders/propertyComponentIndex.js @@ -0,0 +1,59 @@ +import action from '/imports/client/ui/properties/components/folders/folderGroupComponents/ActionGroupComponent.vue'; +//import adjustment from ''; +import attribute from './folderGroupComponents/AttributeGroupComponent.vue'; +import buff from '/imports/client/ui/properties/components/buffs/BuffListItem.vue'; +//import buffRemover from ''; +//import branch from ''; +//import constant from ''; +import container from '/imports/client/ui/properties/components/inventory/ContainerCard.vue'; +//import classComponent from ''; +//import classLevel from ''; +//import damage from ''; +//import damageMultiplier from ''; +//import effect from ''; +import feature from '/imports/client/ui/properties/components/features/FeatureCard.vue'; +// import folder from ''; +import item from '/imports/client/ui/properties/components/inventory/ItemListTile.vue'; +import note from '/imports/client/ui/properties/components/persona/NoteCard.vue'; +//import pointBuy from ''; +//import proficiency from ''; +import propertySlot from '/imports/client/ui/properties/components/folders/folderGroupComponents/SlotBuildTree.vue'; +//import reference from ''; +//import roll from ''; +//import savingThrow from ''; +import skill from '/imports/client/ui/properties/components/skills/SkillListTile.vue'; +import spellList from '/imports/client/ui/properties/components/spells/SpellListCard.vue'; +import spell from '/imports/client/ui/properties/components/spells/SpellListTile.vue'; +import toggle from '/imports/client/ui/properties/components/toggles/ToggleCard.vue'; +//import trigger from ''; + +export default { + action, + //adjustment, + attribute, + buff, + //buffRemover, + //branch, + //constant, + container, + //class: classComponent, + //classLevel, + //damage, + //damageMultiplier, + //effect, + feature, + // folder // Like actions, we don't show sub-folders + item, + note, + //pointBuy, + //proficiency, + propertySlot, + //reference, + //roll, + //savingThrow, + skill, + spellList, + spell, + toggle, + //trigger, +}; diff --git a/app/imports/client/ui/properties/components/folders/tabFoldersMixin.js b/app/imports/client/ui/properties/components/folders/tabFoldersMixin.js new file mode 100644 index 00000000..2d4a0eff --- /dev/null +++ b/app/imports/client/ui/properties/components/folders/tabFoldersMixin.js @@ -0,0 +1,58 @@ +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties' +import FolderGroupCard from '/imports/client/ui/properties/components/folders/FolderGroupCard.vue'; +import softRemoveProperty from '/imports/api/creature/creatureProperties/methods/softRemoveProperty'; +import { snackbar } from '/imports/client/ui/components/snackbars/SnackbarQueue'; +import { getFilter } from '/imports/api/parenting/parentingFunctions'; + +function getFolders(creatureId, tab, location) { + return CreatureProperties.find({ + ...getFilter.descendantsOfRoot(creatureId), + groupStats: true, + inactive: { $ne: true }, + removed: { $ne: true }, + tab, + location, + }, { + sort: { + left: 1, + } + }); +} + +export default { + components: { + FolderGroupCard, + }, + meteor: { + startFolders() { + return getFolders(this.creatureId, this.tabName, 'start'); + }, + endFolders() { + return getFolders(this.creatureId, this.tabName, 'end'); + }, + }, + methods: { + clickProperty({ _id }) { + this.$store.commit('pushDialogStack', { + component: 'creature-property-dialog', + elementId: `${_id}`, + data: { _id }, + }); + }, + clickTreeProperty({ _id }) { + this.$store.commit('pushDialogStack', { + component: 'creature-property-dialog', + elementId: `tree-node-${_id}`, + data: { _id }, + }); + }, + softRemove(_id) { + softRemoveProperty.call({ _id }, error => { + if (error) { + snackbar({ text: error.reason || error.message || error.toString() }); + console.error(error); + } + }); + }, + } +}; \ No newline at end of file diff --git a/app/imports/ui/properties/components/inventory/ContainerCard.vue b/app/imports/client/ui/properties/components/inventory/ContainerCard.vue similarity index 85% rename from app/imports/ui/properties/components/inventory/ContainerCard.vue rename to app/imports/client/ui/properties/components/inventory/ContainerCard.vue index 0843dcfd..734cde9b 100644 --- a/app/imports/ui/properties/components/inventory/ContainerCard.vue +++ b/app/imports/client/ui/properties/components/inventory/ContainerCard.vue @@ -43,11 +43,11 @@ + + diff --git a/app/imports/ui/properties/components/skills/SkillListTile.vue b/app/imports/client/ui/properties/components/skills/SkillListTile.vue similarity index 68% rename from app/imports/ui/properties/components/skills/SkillListTile.vue rename to app/imports/client/ui/properties/components/skills/SkillListTile.vue index bbcb773e..99487af2 100644 --- a/app/imports/ui/properties/components/skills/SkillListTile.vue +++ b/app/imports/client/ui/properties/components/skills/SkillListTile.vue @@ -6,17 +6,15 @@ > - mdi-chevron-double-down - + diff --git a/app/imports/client/ui/properties/forms/ActionConditionsListForm.vue b/app/imports/client/ui/properties/forms/ActionConditionsListForm.vue new file mode 100644 index 00000000..67ab7ffb --- /dev/null +++ b/app/imports/client/ui/properties/forms/ActionConditionsListForm.vue @@ -0,0 +1,41 @@ + + + diff --git a/app/imports/ui/properties/forms/ActionForm.vue b/app/imports/client/ui/properties/forms/ActionForm.vue similarity index 66% rename from app/imports/ui/properties/forms/ActionForm.vue rename to app/imports/client/ui/properties/forms/ActionForm.vue index 00aef28a..b6e26449 100644 --- a/app/imports/ui/properties/forms/ActionForm.vue +++ b/app/imports/client/ui/properties/forms/ActionForm.vue @@ -1,33 +1,43 @@ + + diff --git a/app/imports/ui/properties/forms/AttributeConsumedForm.vue b/app/imports/client/ui/properties/forms/AttributeConsumedForm.vue similarity index 83% rename from app/imports/ui/properties/forms/AttributeConsumedForm.vue rename to app/imports/client/ui/properties/forms/AttributeConsumedForm.vue index b7f0421c..b85428ca 100644 --- a/app/imports/ui/properties/forms/AttributeConsumedForm.vue +++ b/app/imports/client/ui/properties/forms/AttributeConsumedForm.vue @@ -31,8 +31,8 @@ + + diff --git a/app/imports/ui/properties/forms/AttributesConsumedListForm.vue b/app/imports/client/ui/properties/forms/AttributesConsumedListForm.vue similarity index 82% rename from app/imports/ui/properties/forms/AttributesConsumedListForm.vue rename to app/imports/client/ui/properties/forms/AttributesConsumedListForm.vue index 97ff2d81..099d84ff 100644 --- a/app/imports/ui/properties/forms/AttributesConsumedListForm.vue +++ b/app/imports/client/ui/properties/forms/AttributesConsumedListForm.vue @@ -29,8 +29,8 @@ diff --git a/app/imports/client/ui/properties/forms/BuffRemoverForm.vue b/app/imports/client/ui/properties/forms/BuffRemoverForm.vue new file mode 100644 index 00000000..a53a849d --- /dev/null +++ b/app/imports/client/ui/properties/forms/BuffRemoverForm.vue @@ -0,0 +1,123 @@ + + + + + diff --git a/app/imports/client/ui/properties/forms/ClassForm.vue b/app/imports/client/ui/properties/forms/ClassForm.vue new file mode 100644 index 00000000..d1699390 --- /dev/null +++ b/app/imports/client/ui/properties/forms/ClassForm.vue @@ -0,0 +1,109 @@ + + + diff --git a/app/imports/client/ui/properties/forms/ClassLevelForm.vue b/app/imports/client/ui/properties/forms/ClassLevelForm.vue new file mode 100644 index 00000000..264c947f --- /dev/null +++ b/app/imports/client/ui/properties/forms/ClassLevelForm.vue @@ -0,0 +1,59 @@ + + + + + diff --git a/app/imports/client/ui/properties/forms/ConstantForm.vue b/app/imports/client/ui/properties/forms/ConstantForm.vue new file mode 100644 index 00000000..3d4fc14f --- /dev/null +++ b/app/imports/client/ui/properties/forms/ConstantForm.vue @@ -0,0 +1,55 @@ + + + + + diff --git a/app/imports/client/ui/properties/forms/ContainerForm.vue b/app/imports/client/ui/properties/forms/ContainerForm.vue new file mode 100644 index 00000000..b5d88007 --- /dev/null +++ b/app/imports/client/ui/properties/forms/ContainerForm.vue @@ -0,0 +1,89 @@ + + + diff --git a/app/imports/client/ui/properties/forms/CreatureTemplateForm.vue b/app/imports/client/ui/properties/forms/CreatureTemplateForm.vue new file mode 100644 index 00000000..30365f8c --- /dev/null +++ b/app/imports/client/ui/properties/forms/CreatureTemplateForm.vue @@ -0,0 +1,53 @@ + + + diff --git a/app/imports/client/ui/properties/forms/DamageForm.vue b/app/imports/client/ui/properties/forms/DamageForm.vue new file mode 100644 index 00000000..3b6ff212 --- /dev/null +++ b/app/imports/client/ui/properties/forms/DamageForm.vue @@ -0,0 +1,184 @@ + + + + + diff --git a/app/imports/client/ui/properties/forms/DamageMultiplierForm.vue b/app/imports/client/ui/properties/forms/DamageMultiplierForm.vue new file mode 100644 index 00000000..3319dccc --- /dev/null +++ b/app/imports/client/ui/properties/forms/DamageMultiplierForm.vue @@ -0,0 +1,132 @@ + + + + + diff --git a/app/imports/client/ui/properties/forms/EffectForm.vue b/app/imports/client/ui/properties/forms/EffectForm.vue new file mode 100644 index 00000000..d9e69fab --- /dev/null +++ b/app/imports/client/ui/properties/forms/EffectForm.vue @@ -0,0 +1,253 @@ + + + + + diff --git a/app/imports/ui/properties/forms/FeatureForm.vue b/app/imports/client/ui/properties/forms/FeatureForm.vue similarity index 70% rename from app/imports/ui/properties/forms/FeatureForm.vue rename to app/imports/client/ui/properties/forms/FeatureForm.vue index c3f85735..fd196d65 100644 --- a/app/imports/ui/properties/forms/FeatureForm.vue +++ b/app/imports/client/ui/properties/forms/FeatureForm.vue @@ -1,17 +1,10 @@ + + diff --git a/app/imports/ui/properties/forms/ItemConsumedForm.vue b/app/imports/client/ui/properties/forms/ItemConsumedForm.vue similarity index 89% rename from app/imports/ui/properties/forms/ItemConsumedForm.vue rename to app/imports/client/ui/properties/forms/ItemConsumedForm.vue index d65fd6bc..3ebbe2b8 100644 --- a/app/imports/ui/properties/forms/ItemConsumedForm.vue +++ b/app/imports/client/ui/properties/forms/ItemConsumedForm.vue @@ -31,7 +31,7 @@ diff --git a/app/imports/ui/properties/forms/PointBuyForm.vue b/app/imports/client/ui/properties/forms/PointBuyForm.vue similarity index 81% rename from app/imports/ui/properties/forms/PointBuyForm.vue rename to app/imports/client/ui/properties/forms/PointBuyForm.vue index 41426acb..b25c853c 100644 --- a/app/imports/ui/properties/forms/PointBuyForm.vue +++ b/app/imports/client/ui/properties/forms/PointBuyForm.vue @@ -1,26 +1,13 @@ diff --git a/app/imports/client/ui/properties/forms/ProficiencyForm.vue b/app/imports/client/ui/properties/forms/ProficiencyForm.vue new file mode 100644 index 00000000..0782040a --- /dev/null +++ b/app/imports/client/ui/properties/forms/ProficiencyForm.vue @@ -0,0 +1,104 @@ + + + + + diff --git a/app/imports/client/ui/properties/forms/ReferenceForm.vue b/app/imports/client/ui/properties/forms/ReferenceForm.vue new file mode 100644 index 00000000..b7985334 --- /dev/null +++ b/app/imports/client/ui/properties/forms/ReferenceForm.vue @@ -0,0 +1,116 @@ + + + + + diff --git a/app/imports/ui/properties/forms/ResourcesForm.vue b/app/imports/client/ui/properties/forms/ResourcesForm.vue similarity index 69% rename from app/imports/ui/properties/forms/ResourcesForm.vue rename to app/imports/client/ui/properties/forms/ResourcesForm.vue index e3fc0dcb..76510809 100644 --- a/app/imports/ui/properties/forms/ResourcesForm.vue +++ b/app/imports/client/ui/properties/forms/ResourcesForm.vue @@ -1,5 +1,17 @@ + + Add Condition + Add Resource @@ -55,12 +70,14 @@ diff --git a/app/imports/client/ui/properties/forms/RollForm.vue b/app/imports/client/ui/properties/forms/RollForm.vue new file mode 100644 index 00000000..a04c517f --- /dev/null +++ b/app/imports/client/ui/properties/forms/RollForm.vue @@ -0,0 +1,80 @@ + + + + + diff --git a/app/imports/client/ui/properties/forms/SavingThrowForm.vue b/app/imports/client/ui/properties/forms/SavingThrowForm.vue new file mode 100644 index 00000000..30ce8558 --- /dev/null +++ b/app/imports/client/ui/properties/forms/SavingThrowForm.vue @@ -0,0 +1,66 @@ + + + diff --git a/app/imports/client/ui/properties/forms/SkillForm.vue b/app/imports/client/ui/properties/forms/SkillForm.vue new file mode 100644 index 00000000..c3e16383 --- /dev/null +++ b/app/imports/client/ui/properties/forms/SkillForm.vue @@ -0,0 +1,175 @@ + + + + + diff --git a/app/imports/client/ui/properties/forms/SlotForm.vue b/app/imports/client/ui/properties/forms/SlotForm.vue new file mode 100644 index 00000000..f932b4b4 --- /dev/null +++ b/app/imports/client/ui/properties/forms/SlotForm.vue @@ -0,0 +1,192 @@ + + + diff --git a/app/imports/ui/properties/forms/SpellForm.vue b/app/imports/client/ui/properties/forms/SpellForm.vue similarity index 81% rename from app/imports/ui/properties/forms/SpellForm.vue rename to app/imports/client/ui/properties/forms/SpellForm.vue index ee03d9ba..608cd08e 100644 --- a/app/imports/ui/properties/forms/SpellForm.vue +++ b/app/imports/client/ui/properties/forms/SpellForm.vue @@ -1,17 +1,5 @@ diff --git a/app/imports/client/ui/properties/forms/ToggleForm.vue b/app/imports/client/ui/properties/forms/ToggleForm.vue new file mode 100644 index 00000000..fbe23910 --- /dev/null +++ b/app/imports/client/ui/properties/forms/ToggleForm.vue @@ -0,0 +1,132 @@ + + + + + diff --git a/app/imports/client/ui/properties/forms/TriggerForm.vue b/app/imports/client/ui/properties/forms/TriggerForm.vue new file mode 100644 index 00000000..ed4e75db --- /dev/null +++ b/app/imports/client/ui/properties/forms/TriggerForm.vue @@ -0,0 +1,163 @@ + + + diff --git a/app/imports/ui/properties/forms/shared/CalculationErrorList.vue b/app/imports/client/ui/properties/forms/shared/CalculationErrorList.vue similarity index 100% rename from app/imports/ui/properties/forms/shared/CalculationErrorList.vue rename to app/imports/client/ui/properties/forms/shared/CalculationErrorList.vue diff --git a/app/imports/ui/properties/forms/shared/ComputedField.vue b/app/imports/client/ui/properties/forms/shared/ComputedField.vue similarity index 67% rename from app/imports/ui/properties/forms/shared/ComputedField.vue rename to app/imports/client/ui/properties/forms/shared/ComputedField.vue index 9d3ac194..ff22b77e 100644 --- a/app/imports/ui/properties/forms/shared/ComputedField.vue +++ b/app/imports/client/ui/properties/forms/shared/ComputedField.vue @@ -9,7 +9,10 @@ v-if="showValue" #value > - {{ model.value }} + {{ displayedValue }} + + @@ -17,7 +20,7 @@ diff --git a/app/imports/ui/properties/forms/shared/IconColorMenu.vue b/app/imports/client/ui/properties/forms/shared/IconColorMenu.vue similarity index 52% rename from app/imports/ui/properties/forms/shared/IconColorMenu.vue rename to app/imports/client/ui/properties/forms/shared/IconColorMenu.vue index 820cd3a6..824f645c 100644 --- a/app/imports/ui/properties/forms/shared/IconColorMenu.vue +++ b/app/imports/client/ui/properties/forms/shared/IconColorMenu.vue @@ -1,32 +1,39 @@ diff --git a/app/imports/ui/properties/forms/shared/InlineComputationField.vue b/app/imports/client/ui/properties/forms/shared/InlineComputationField.vue similarity index 91% rename from app/imports/ui/properties/forms/shared/InlineComputationField.vue rename to app/imports/client/ui/properties/forms/shared/InlineComputationField.vue index bbd88d8f..def4fb0b 100644 --- a/app/imports/ui/properties/forms/shared/InlineComputationField.vue +++ b/app/imports/client/ui/properties/forms/shared/InlineComputationField.vue @@ -33,7 +33,7 @@ \ No newline at end of file diff --git a/app/imports/client/ui/properties/forms/shared/lists/attributeListMixin.js b/app/imports/client/ui/properties/forms/shared/lists/attributeListMixin.js new file mode 100644 index 00000000..02f2977b --- /dev/null +++ b/app/imports/client/ui/properties/forms/shared/lists/attributeListMixin.js @@ -0,0 +1,11 @@ +import createListOfProperties from '/imports/client/ui/properties/forms/shared/lists/createListOfProperties'; + +const attributeListMixin = { + meteor: { + attributeList() { + return createListOfProperties({ type: { $in: ['attribute', 'skill'] } }); + }, + }, +}; + +export default attributeListMixin; diff --git a/app/imports/ui/properties/forms/shared/lists/createListOfProperties.js b/app/imports/client/ui/properties/forms/shared/lists/createListOfProperties.js similarity index 84% rename from app/imports/ui/properties/forms/shared/lists/createListOfProperties.js rename to app/imports/client/ui/properties/forms/shared/lists/createListOfProperties.js index ed5f9d3d..9282281b 100644 --- a/app/imports/ui/properties/forms/shared/lists/createListOfProperties.js +++ b/app/imports/client/ui/properties/forms/shared/lists/createListOfProperties.js @@ -1,5 +1,5 @@ -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import LibraryNodes from '/imports/api/library/LibraryNodes.js'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; +import LibraryNodes from '/imports/api/library/LibraryNodes'; export default function createListOfProperties(filter = {}, getNamesWithValues) { filter.removed = { $ne: true }; @@ -15,7 +15,7 @@ export default function createListOfProperties(filter = {}, getNamesWithValues) }); } } - let options = { sort: { order: 1, variableName: 1 } } + let options = { sort: { left: 1, variableName: 1 } } CreatureProperties.find(filter, options).forEach(addUniquePropertys); LibraryNodes.find(filter, options).forEach(addUniquePropertys); if (getNamesWithValues) return propertyList; diff --git a/app/imports/client/ui/properties/forms/shared/lists/saveListMixin.js b/app/imports/client/ui/properties/forms/shared/lists/saveListMixin.js new file mode 100644 index 00000000..6c5b4524 --- /dev/null +++ b/app/imports/client/ui/properties/forms/shared/lists/saveListMixin.js @@ -0,0 +1,11 @@ +import createListOfProperties from '/imports/client/ui/properties/forms/shared/lists/createListOfProperties' + +const saveListMixin = { + meteor: { + saveList() { + return createListOfProperties({ type: 'skill', skillType: 'save' }); + }, + }, +}; + +export default saveListMixin; diff --git a/app/imports/client/ui/properties/forms/shared/lists/skillListMixin.js b/app/imports/client/ui/properties/forms/shared/lists/skillListMixin.js new file mode 100644 index 00000000..932a3d70 --- /dev/null +++ b/app/imports/client/ui/properties/forms/shared/lists/skillListMixin.js @@ -0,0 +1,11 @@ +import createListOfProperties from '/imports/client/ui/properties/forms/shared/lists/createListOfProperties' + +const skillListMixin = { + meteor: { + skillList() { + return createListOfProperties({ type: 'skill' }); + }, + }, +}; + +export default skillListMixin; diff --git a/app/imports/client/ui/properties/forms/shared/propertyFormIndex.js b/app/imports/client/ui/properties/forms/shared/propertyFormIndex.js new file mode 100644 index 00000000..6e138da0 --- /dev/null +++ b/app/imports/client/ui/properties/forms/shared/propertyFormIndex.js @@ -0,0 +1,61 @@ +import ActionForm from '/imports/client/ui/properties/forms/ActionForm.vue'; +import AdjustmentForm from '/imports/client/ui/properties/forms/AdjustmentForm.vue'; +import AttributeForm from '/imports/client/ui/properties/forms/AttributeForm.vue'; +import BuffForm from '/imports/client/ui/properties/forms/BuffForm.vue'; +import BuffRemoverForm from '/imports/client/ui/properties/forms/BuffRemoverForm.vue'; +import BranchForm from '/imports/client/ui/properties/forms/BranchForm.vue'; +import ClassForm from '/imports/client/ui/properties/forms/ClassForm.vue'; +import ClassLevelForm from '/imports/client/ui/properties/forms/ClassLevelForm.vue'; +import ConstantForm from '/imports/client/ui/properties/forms/ConstantForm.vue'; +import ContainerForm from '/imports/client/ui/properties/forms/ContainerForm.vue'; +import CreatureTemplateForm from '/imports/client/ui/properties/forms/CreatureTemplateForm.vue'; +import DamageForm from '/imports/client/ui/properties/forms/DamageForm.vue'; +import DamageMultiplierForm from '/imports/client/ui/properties/forms/DamageMultiplierForm.vue'; +import EffectForm from '/imports/client/ui/properties/forms/EffectForm.vue'; +import FeatureForm from '/imports/client/ui/properties/forms/FeatureForm.vue'; +import FolderForm from '/imports/client/ui/properties/forms/FolderForm.vue'; +import ItemForm from '/imports/client/ui/properties/forms/ItemForm.vue'; +import NoteForm from '/imports/client/ui/properties/forms/NoteForm.vue'; +import PointBuyForm from '/imports/client/ui/properties/forms/PointBuyForm.vue'; +import ProficiencyForm from '/imports/client/ui/properties/forms/ProficiencyForm.vue'; +import ReferenceForm from '/imports/client/ui/properties/forms/ReferenceForm.vue'; +import RollForm from '/imports/client/ui/properties/forms/RollForm.vue'; +import SavingThrowForm from '/imports/client/ui/properties/forms/SavingThrowForm.vue'; +import SkillForm from '/imports/client/ui/properties/forms/SkillForm.vue'; +import SlotForm from '/imports/client/ui/properties/forms/SlotForm.vue'; +import SpellListForm from '/imports/client/ui/properties/forms/SpellListForm.vue'; +import SpellForm from '/imports/client/ui/properties/forms/SpellForm.vue'; +import ToggleForm from '/imports/client/ui/properties/forms/ToggleForm.vue'; +import TriggerForm from '/imports/client/ui/properties/forms/TriggerForm.vue'; + +export default { + action: ActionForm, + adjustment: AdjustmentForm, + attribute: AttributeForm, + buff: BuffForm, + buffRemover: BuffRemoverForm, + branch: BranchForm, + constant: ConstantForm, + container: ContainerForm, + class: ClassForm, + classLevel: ClassLevelForm, + creature: CreatureTemplateForm, + damage: DamageForm, + damageMultiplier: DamageMultiplierForm, + effect: EffectForm, + feature: FeatureForm, + folder: FolderForm, + item: ItemForm, + note: NoteForm, + pointBuy: PointBuyForm, + proficiency: ProficiencyForm, + propertySlot: SlotForm, + reference: ReferenceForm, + roll: RollForm, + savingThrow: SavingThrowForm, + skill: SkillForm, + spellList: SpellListForm, + spell: SpellForm, + toggle: ToggleForm, + trigger: TriggerForm, +}; diff --git a/app/imports/client/ui/properties/forms/shared/propertyFormMixin.js b/app/imports/client/ui/properties/forms/shared/propertyFormMixin.js new file mode 100644 index 00000000..84f6760e --- /dev/null +++ b/app/imports/client/ui/properties/forms/shared/propertyFormMixin.js @@ -0,0 +1,30 @@ +import ComputedField from '/imports/client/ui/properties/forms/shared/ComputedField.vue'; +import InlineComputationField from '/imports/client/ui/properties/forms/shared/InlineComputationField.vue'; +import FormSection, { FormSections } from '/imports/client/ui/properties/forms/shared/FormSection.vue'; + +export default { + components: { + ComputedField, + InlineComputationField, + FormSection, + FormSections, + }, + props: { + model: { + type: [Object, Array], + default: () => ({}), + }, + errors: { + type: Object, + default: () => ({}), + }, + }, + methods: { + change(path, value, ack) { + if (!Array.isArray(path)) { + path = [path]; + } + this.$emit('change', { path, value, ack }); + } + }, +} diff --git a/app/imports/ui/properties/forms/shared/schemaFormMixin.js b/app/imports/client/ui/properties/forms/shared/schemaFormMixin.js similarity index 100% rename from app/imports/ui/properties/forms/shared/schemaFormMixin.js rename to app/imports/client/ui/properties/forms/shared/schemaFormMixin.js diff --git a/app/imports/ui/properties/shared/ProficiencyIcon.vue b/app/imports/client/ui/properties/shared/ProficiencyIcon.vue similarity index 80% rename from app/imports/ui/properties/shared/ProficiencyIcon.vue rename to app/imports/client/ui/properties/shared/ProficiencyIcon.vue index 223ef725..c4227d92 100644 --- a/app/imports/ui/properties/shared/ProficiencyIcon.vue +++ b/app/imports/client/ui/properties/shared/ProficiencyIcon.vue @@ -5,7 +5,7 @@ + + diff --git a/app/imports/ui/properties/shared/SelectablePropertyDialog.vue b/app/imports/client/ui/properties/shared/SelectablePropertyDialog.vue similarity index 88% rename from app/imports/ui/properties/shared/SelectablePropertyDialog.vue rename to app/imports/client/ui/properties/shared/SelectablePropertyDialog.vue index e5b7d28d..6537c21b 100644 --- a/app/imports/ui/properties/shared/SelectablePropertyDialog.vue +++ b/app/imports/client/ui/properties/shared/SelectablePropertyDialog.vue @@ -37,9 +37,9 @@ diff --git a/app/imports/ui/properties/treeNodeViews/DefaultTreeNode.vue b/app/imports/client/ui/properties/treeNodeViews/DefaultTreeNode.vue similarity index 81% rename from app/imports/ui/properties/treeNodeViews/DefaultTreeNode.vue rename to app/imports/client/ui/properties/treeNodeViews/DefaultTreeNode.vue index 1f35c7b9..7cb7f455 100644 --- a/app/imports/ui/properties/treeNodeViews/DefaultTreeNode.vue +++ b/app/imports/client/ui/properties/treeNodeViews/DefaultTreeNode.vue @@ -14,7 +14,7 @@ diff --git a/app/imports/ui/properties/treeNodeViews/SavingThrowTreeNode.vue b/app/imports/client/ui/properties/treeNodeViews/SavingThrowTreeNode.vue similarity index 83% rename from app/imports/ui/properties/treeNodeViews/SavingThrowTreeNode.vue rename to app/imports/client/ui/properties/treeNodeViews/SavingThrowTreeNode.vue index 10767f99..cf12e1bc 100644 --- a/app/imports/ui/properties/treeNodeViews/SavingThrowTreeNode.vue +++ b/app/imports/client/ui/properties/treeNodeViews/SavingThrowTreeNode.vue @@ -17,8 +17,8 @@ diff --git a/app/imports/ui/properties/viewers/DamageMultiplierViewer.vue b/app/imports/client/ui/properties/viewers/DamageMultiplierViewer.vue similarity index 94% rename from app/imports/ui/properties/viewers/DamageMultiplierViewer.vue rename to app/imports/client/ui/properties/viewers/DamageMultiplierViewer.vue index cde9cba9..a2782c9c 100644 --- a/app/imports/ui/properties/viewers/DamageMultiplierViewer.vue +++ b/app/imports/client/ui/properties/viewers/DamageMultiplierViewer.vue @@ -58,7 +58,7 @@ + + diff --git a/app/imports/ui/properties/viewers/EffectViewer.vue b/app/imports/client/ui/properties/viewers/EffectViewer.vue similarity index 74% rename from app/imports/ui/properties/viewers/EffectViewer.vue rename to app/imports/client/ui/properties/viewers/EffectViewer.vue index 5880fc10..48b605f8 100644 --- a/app/imports/ui/properties/viewers/EffectViewer.vue +++ b/app/imports/client/ui/properties/viewers/EffectViewer.vue @@ -17,39 +17,10 @@ name="Amount" :value="displayedValue || ' '" /> - -
-
- - {{ tag }} - -
-
- - {{ ex.operation }} - -
- - {{ extraTag }} - -
-
-
-
+ :model="model" + /> + + diff --git a/app/imports/ui/properties/viewers/ItemViewer.vue b/app/imports/client/ui/properties/viewers/ItemViewer.vue similarity index 78% rename from app/imports/ui/properties/viewers/ItemViewer.vue rename to app/imports/client/ui/properties/viewers/ItemViewer.vue index 0db65ce7..48a12493 100644 --- a/app/imports/ui/properties/viewers/ItemViewer.vue +++ b/app/imports/client/ui/properties/viewers/ItemViewer.vue @@ -20,6 +20,23 @@ @change="changeQuantity" /> + + + mdi-delete + + Equipped - + + + + diff --git a/app/imports/ui/properties/viewers/SlotViewer.vue b/app/imports/client/ui/properties/viewers/SlotViewer.vue similarity index 89% rename from app/imports/ui/properties/viewers/SlotViewer.vue rename to app/imports/client/ui/properties/viewers/SlotViewer.vue index 55ceeb8d..d2953471 100644 --- a/app/imports/ui/properties/viewers/SlotViewer.vue +++ b/app/imports/client/ui/properties/viewers/SlotViewer.vue @@ -60,9 +60,9 @@ diff --git a/app/imports/ui/properties/viewers/TriggerViewer.vue b/app/imports/client/ui/properties/viewers/TriggerViewer.vue similarity index 89% rename from app/imports/ui/properties/viewers/TriggerViewer.vue rename to app/imports/client/ui/properties/viewers/TriggerViewer.vue index ce7e1f44..f610b7fe 100644 --- a/app/imports/ui/properties/viewers/TriggerViewer.vue +++ b/app/imports/client/ui/properties/viewers/TriggerViewer.vue @@ -40,9 +40,9 @@ + + + diff --git a/app/imports/client/ui/properties/viewers/shared/OutlinedInput.vue b/app/imports/client/ui/properties/viewers/shared/OutlinedInput.vue new file mode 100644 index 00000000..6b63f803 --- /dev/null +++ b/app/imports/client/ui/properties/viewers/shared/OutlinedInput.vue @@ -0,0 +1,61 @@ + + + + + diff --git a/app/imports/ui/properties/viewers/shared/PropertyDescription.vue b/app/imports/client/ui/properties/viewers/shared/PropertyDescription.vue similarity index 85% rename from app/imports/ui/properties/viewers/shared/PropertyDescription.vue rename to app/imports/client/ui/properties/viewers/shared/PropertyDescription.vue index 884bbdd8..67701bf1 100644 --- a/app/imports/ui/properties/viewers/shared/PropertyDescription.vue +++ b/app/imports/client/ui/properties/viewers/shared/PropertyDescription.vue @@ -13,8 +13,8 @@ \ No newline at end of file diff --git a/app/imports/ui/properties/viewers/shared/PropertyVariableName.vue b/app/imports/client/ui/properties/viewers/shared/PropertyVariableName.vue similarity index 100% rename from app/imports/ui/properties/viewers/shared/PropertyVariableName.vue rename to app/imports/client/ui/properties/viewers/shared/PropertyVariableName.vue diff --git a/app/imports/client/ui/properties/viewers/shared/propertyViewerIndex.js b/app/imports/client/ui/properties/viewers/shared/propertyViewerIndex.js new file mode 100644 index 00000000..a0cc904f --- /dev/null +++ b/app/imports/client/ui/properties/viewers/shared/propertyViewerIndex.js @@ -0,0 +1,61 @@ +import ActionViewer from '/imports/client/ui/properties/viewers/ActionViewer.vue'; +import AdjustmentViewer from '/imports/client/ui/properties/viewers/AdjustmentViewer.vue'; +import AttributeViewer from '/imports/client/ui/properties/viewers/AttributeViewer.vue'; +import BuffViewer from '/imports/client/ui/properties/viewers/BuffViewer.vue'; +import BuffRemoverViewer from '/imports/client/ui/properties/viewers/BuffRemoverViewer.vue'; +import BranchViewer from '/imports/client/ui/properties/viewers/BranchViewer.vue'; +import ContainerViewer from '/imports/client/ui/properties/viewers/ContainerViewer.vue'; +import ClassViewer from '/imports/client/ui/properties/viewers/ClassViewer.vue'; +import ClassLevelViewer from '/imports/client/ui/properties/viewers/ClassLevelViewer.vue'; +import ConstantViewer from '/imports/client/ui/properties/viewers/ConstantViewer.vue'; +import CreatureTemplateViewer from '/imports/client/ui/properties/viewers/CreatureTemplateViewer.vue'; +import DamageViewer from '/imports/client/ui/properties/viewers/DamageViewer.vue'; +import DamageMultiplierViewer from '/imports/client/ui/properties/viewers/DamageMultiplierViewer.vue'; +import EffectViewer from '/imports/client/ui/properties/viewers/EffectViewer.vue'; +import FeatureViewer from '/imports/client/ui/properties/viewers/FeatureViewer.vue'; +import FolderViewer from '/imports/client/ui/properties/viewers/FolderViewer.vue'; +import ItemViewer from '/imports/client/ui/properties/viewers/ItemViewer.vue'; +import NoteViewer from '/imports/client/ui/properties/viewers/NoteViewer.vue'; +import PointBuyViewer from '/imports/client/ui/properties/viewers/PointBuyViewer.vue'; +import ProficiencyViewer from '/imports/client/ui/properties/viewers/ProficiencyViewer.vue'; +import ReferenceViewer from '/imports/client/ui/properties/viewers/ReferenceViewer.vue'; +import RollViewer from '/imports/client/ui/properties/viewers/RollViewer.vue'; +import SkillViewer from '/imports/client/ui/properties/viewers/SkillViewer.vue'; +import SavingThrowViewer from '/imports/client/ui/properties/viewers/SavingThrowViewer.vue'; +import SlotViewer from '/imports/client/ui/properties/viewers/SlotViewer.vue'; +import SpellListViewer from '/imports/client/ui/properties/viewers/SpellListViewer.vue'; +import SpellViewer from '/imports/client/ui/properties/viewers/SpellViewer.vue'; +import ToggleViewer from '/imports/client/ui/properties/viewers/ToggleViewer.vue'; +import TriggerViewer from '/imports/client/ui/properties/viewers/TriggerViewer.vue'; + +export default { + action: ActionViewer, + adjustment: AdjustmentViewer, + attribute: AttributeViewer, + buff: BuffViewer, + buffRemover: BuffRemoverViewer, + branch: BranchViewer, + container: ContainerViewer, + class: ClassViewer, + classLevel: ClassLevelViewer, + constant: ConstantViewer, + creature: CreatureTemplateViewer, + damage: DamageViewer, + damageMultiplier: DamageMultiplierViewer, + effect: EffectViewer, + feature: FeatureViewer, + folder: FolderViewer, + item: ItemViewer, + note: NoteViewer, + pointBuy: PointBuyViewer, + proficiency: ProficiencyViewer, + propertySlot: SlotViewer, + roll: RollViewer, + reference: ReferenceViewer, + savingThrow: SavingThrowViewer, + skill: SkillViewer, + spellList: SpellListViewer, + spell: SpellViewer, + toggle: ToggleViewer, + trigger: TriggerViewer, +}; diff --git a/app/imports/client/ui/properties/viewers/shared/propertyViewerMixin.js b/app/imports/client/ui/properties/viewers/shared/propertyViewerMixin.js new file mode 100644 index 00000000..4bec5f4e --- /dev/null +++ b/app/imports/client/ui/properties/viewers/shared/propertyViewerMixin.js @@ -0,0 +1,23 @@ +import PropertyName from '/imports/client/ui/properties/viewers/shared/PropertyName.vue'; +import PropertyVariableName from '/imports/client/ui/properties/viewers/shared/PropertyVariableName.vue'; +import PropertyField from '/imports/client/ui/properties/viewers/shared/PropertyField.vue'; +import PropertyDescription from '/imports/client/ui/properties/viewers/shared/PropertyDescription.vue'; +import PropertyTags from '/imports/client/ui/properties/viewers/shared/PropertyTags.vue'; + +const propertyViewerMixin = { + components: { + PropertyName, + PropertyVariableName, + PropertyField, + PropertyDescription, + PropertyTags, + }, + props: { + model: { + type: Object, + required: true, + }, + }, +}; + +export default propertyViewerMixin; diff --git a/app/imports/ui/router.js b/app/imports/client/ui/router.js similarity index 64% rename from app/imports/ui/router.js rename to app/imports/client/ui/router.js index bb8dc307..a9c1d0f4 100644 --- a/app/imports/ui/router.js +++ b/app/imports/client/ui/router.js @@ -1,45 +1,49 @@ import { RouterFactory, nativeScrollBehavior } from 'meteor/akryum:vue-router2'; -import { acceptInviteToken } from '/imports/api/users/Invites.js'; -import MAINTENANCE_MODE from '/imports/constants/MAINTENANCE_MODE.js'; +import { acceptInviteToken } from '/imports/api/users/Invites'; +import MAINTENANCE_MODE from '/imports/constants/MAINTENANCE_MODE'; // Components -const Home = () => import('/imports/ui/pages/Home.vue'); -const About = () => import('/imports/ui/pages/About.vue'); -const CharacterList = () => import('/imports/ui/pages/CharacterList.vue'); -const CharacterListToolbarItems = () => import('/imports/ui/creature/creatureList/CharacterListToolbarItems.vue'); -const Library = () => import('/imports/ui/pages/Library.vue'); -const LibraryCollection = () => import('/imports/ui/pages/LibraryCollection.vue'); -const LibraryCollectionToolbar = () => import('/imports/ui/library/LibraryCollectionToolbar.vue'); -const CharacterSheetPage = () => import('/imports/ui/pages/CharacterSheetPage.vue'); -const CharacterSheetToolbar = () => import('/imports/ui/creature/character/CharacterSheetToolbar.vue'); -const CharacterSheetRightDrawer = () => import('/imports/ui/creature/character/CharacterSheetRightDrawer.vue'); -const CharacterSheetPrinted = () => import('/imports/ui/creature/character/printedCharacterSheet/CharacterSheetPrinted.vue'); -const SignIn = () => import('/imports/ui/pages/SignIn.vue'); -const Register = () => import('/imports/ui/pages/Register.vue'); -const IconAdmin = () => import('/imports/ui/icons/IconAdmin.vue'); -//const Friends = () => import('/imports/ui/pages/Friends.vue' ); -const Feedback = () => import('/imports/ui/pages/Feedback.vue'); -const FunctionReference = () => import('/imports/ui/pages/FunctionReference.vue'); -const Account = () => import('/imports/ui/pages/Account.vue'); -const InviteSuccess = () => import('/imports/ui/pages/InviteSuccess.vue'); -const InviteError = () => import('/imports/ui/pages/InviteError.vue'); -const EmailVerificationSuccess = () => import('/imports/ui/pages/EmailVerificationSuccess.vue'); -const EmailVerificationError = () => import('/imports/ui/pages/EmailVerificationError.vue'); -const ResetPassword = () => import('/imports/ui/pages/ResetPassword.vue'); -const NotImplemented = () => import('/imports/ui/pages/NotImplemented.vue'); -const PatreonLevelTooLow = () => import('/imports/ui/pages/PatreonLevelTooLow.vue'); -const SingleLibrary = () => import('/imports/ui/pages/SingleLibrary.vue'); -const SingleLibraryToolbar = () => import('/imports/ui/library/SingleLibraryToolbar.vue'); -const Tabletops = () => import('/imports/ui/pages/Tabletops.vue'); -const Tabletop = () => import('/imports/ui/pages/Tabletop.vue'); -const TabletopToolbar = () => import('/imports/ui/tabletop/TabletopToolbar.vue'); -const TabletopRightDrawer = () => import('/imports/ui/tabletop/TabletopRightDrawer.vue'); -const Admin = () => import('/imports/ui/pages/Admin.vue'); -const Maintenance = () => import('/imports/ui/pages/Maintenance.vue'); -const Files = () => import('/imports/ui/pages/Files.vue'); -const Documentation = () => import('/imports/ui/pages/Documentation.vue'); +const Home = () => import('/imports/client/ui/pages/Home.vue'); +const About = () => import('/imports/client/ui/pages/About.vue'); +const CharacterList = () => import('/imports/client/ui/pages/CharacterList.vue'); +const CharacterListToolbarItems = () => import('/imports/client/ui/creature/creatureList/CharacterListToolbarItems.vue'); +const Library = () => import('/imports/client/ui/pages/Library.vue'); +const LibraryCollection = () => import('/imports/client/ui/pages/LibraryCollection.vue'); +const LibraryCollectionToolbar = () => import('/imports/client/ui/library/LibraryCollectionToolbar.vue'); +const LibraryBrowser = () => import('/imports/client/ui/pages/LibraryBrowser.vue'); +const CharacterSheetPage = () => import('/imports/client/ui/pages/CharacterSheetPage.vue'); +const CharacterSheetToolbar = () => import('/imports/client/ui/creature/character/CharacterSheetToolbar.vue'); +const CharacterSheetRightDrawer = () => import('/imports/client/ui/creature/character/CharacterSheetRightDrawer.vue'); +const CharacterSheetPrinted = () => import('/imports/client/ui/creature/character/printedCharacterSheet/CharacterSheetPrinted.vue'); +const CharacterSheetPrintedToolbar = () => import('/imports/client/ui/creature/character/printedCharacterSheet/CharacterSheetPrintedToolbar.vue'); +const SignIn = () => import('/imports/client/ui/pages/SignIn.vue'); +const Register = () => import('/imports/client/ui/pages/Register.vue'); +const IconAdmin = () => import('/imports/client/ui/icons/IconAdmin.vue'); +//const Friends = () => import('/imports/client/ui/pages/Friends.vue' ); +const Feedback = () => import('/imports/client/ui/pages/Feedback.vue'); +const FunctionReference = () => import('/imports/client/ui/pages/FunctionReference.vue'); +const Account = () => import('/imports/client/ui/pages/Account.vue'); +const InviteSuccess = () => import('/imports/client/ui/pages/InviteSuccess.vue'); +const InviteError = () => import('/imports/client/ui/pages/InviteError.vue'); +const EmailVerificationSuccess = () => import('/imports/client/ui/pages/EmailVerificationSuccess.vue'); +const EmailVerificationError = () => import('/imports/client/ui/pages/EmailVerificationError.vue'); +const ResetPassword = () => import('/imports/client/ui/pages/ResetPassword.vue'); +const NotImplemented = () => import('/imports/client/ui/pages/NotImplemented.vue'); +const PatreonLevelTooLow = () => import('/imports/client/ui/pages/PatreonLevelTooLow.vue'); +const SingleLibrary = () => import('/imports/client/ui/pages/SingleLibrary.vue'); +const SingleLibraryToolbar = () => import('/imports/client/ui/library/SingleLibraryToolbar.vue'); +const Tabletops = () => import('/imports/client/ui/pages/Tabletops.vue'); +const Tabletop = () => import('/imports/client/ui/pages/Tabletop.vue'); +const TabletopToolbar = () => import('/imports/client/ui/tabletop/TabletopToolbar.vue'); +const TabletopRightDrawer = () => import('/imports/client/ui/tabletop/TabletopRightDrawer.vue'); +const Admin = () => import('/imports/client/ui/pages/Admin.vue'); +const Maintenance = () => import('/imports/client/ui/pages/Maintenance.vue'); +const Files = () => import('/imports/client/ui/pages/Files.vue'); +const DocsPage = () => import('/imports/client/ui/pages/DocsPage.vue'); +const DocToolbar = () => import('/imports/client/ui/docs/DocToolbar.vue'); +const DocsRightDrawer = () => import('/imports/client/ui/docs/DocsRightDrawer.vue'); // Not found -const NotFound = () => import('/imports/ui/pages/NotFound.vue'); +const NotFound = () => import('/imports/client/ui/pages/NotFound.vue'); let userSubscription = Meteor.subscribe('user'); @@ -166,6 +170,15 @@ RouterFactory.configure(router => { meta: { title: 'Library Collection', }, + }, { + name: 'libraryBrowser', + path: '/community-libraries', + components: { + default: LibraryBrowser, + }, + meta: { + title: 'Community Libraries', + }, }, { name: 'characterSheet', path: '/character/:id', @@ -184,6 +197,7 @@ RouterFactory.configure(router => { alias: '/print-character/:id/:urlName', components: { default: CharacterSheetPrinted, + toolbar: CharacterSheetPrintedToolbar, }, meta: { title: 'Print Character Sheet', @@ -193,6 +207,9 @@ RouterFactory.configure(router => { name: 'tabletops', component: Tabletops, beforeEnter: ensureLoggedIn, + meta: { + title: 'Tabletops', + }, }, { path: '/tabletop/:id', name: 'tabletop', @@ -264,17 +281,11 @@ RouterFactory.configure(router => { title: 'Functions', }, }, { - path: '/docs/:docPath([^/]+.*)', + path: '/docs/:docPath([^/]+.*)?', components: { - default: Documentation, - }, - meta: { - title: 'Documentation', - }, - }, { - path: '/docs', - components: { - default: Documentation, + default: DocsPage, + toolbar: DocToolbar, + rightDrawer: DocsRightDrawer, }, meta: { title: 'Documentation', @@ -377,7 +388,11 @@ RouterFactory.configure(router => { function redirectIfMaintenance(to, from, next) { if (!MAINTENANCE_MODE) return next(); - if (to?.path === '/admin' || to?.path === '/maintenance' || to?.path === '/sign-in') return next(); + if ( + to?.path === '/admin' || + to?.path === '/maintenance' || + to?.path === '/sign-in' + ) return next(); Tracker.autorun((computation) => { if (userSubscription.ready()) { computation.stop(); diff --git a/app/imports/ui/sharing/ShareDialog.vue b/app/imports/client/ui/sharing/ShareDialog.vue similarity index 96% rename from app/imports/ui/sharing/ShareDialog.vue rename to app/imports/client/ui/sharing/ShareDialog.vue index 27d8318f..0ab55b6c 100644 --- a/app/imports/ui/sharing/ShareDialog.vue +++ b/app/imports/client/ui/sharing/ShareDialog.vue @@ -27,7 +27,7 @@ v-if="model.public && docRef.collection === 'libraries'" readonly label="Link" - :value="'https://beta.dicecloud.com' + $router.resolve({ + :value="window.location.origin + $router.resolve({ name: 'singleLibrary', params: { id: model._id }, }).href" @@ -139,9 +139,9 @@ import { setPublic, setReadersCanCopy, updateUserSharePermissions -} from '/imports/api/sharing/sharing.js'; -import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js'; -import DialogBase from '/imports/ui/dialogStack/DialogBase.vue'; +} from '/imports/api/sharing/sharing'; +import { fetchDocByRef } from '/imports/api/parenting/parentingFunctions'; +import DialogBase from '/imports/client/ui/dialogStack/DialogBase.vue'; export default { components: { diff --git a/app/imports/ui/sharing/TransferOwnershipDialog.vue b/app/imports/client/ui/sharing/TransferOwnershipDialog.vue similarity index 91% rename from app/imports/ui/sharing/TransferOwnershipDialog.vue rename to app/imports/client/ui/sharing/TransferOwnershipDialog.vue index aa8f35f0..c9f3326d 100644 --- a/app/imports/ui/sharing/TransferOwnershipDialog.vue +++ b/app/imports/client/ui/sharing/TransferOwnershipDialog.vue @@ -37,8 +37,8 @@ + + diff --git a/app/imports/client/ui/tabletop/CreatureFromLibraryDialog.vue b/app/imports/client/ui/tabletop/CreatureFromLibraryDialog.vue new file mode 100644 index 00000000..1fa60d1c --- /dev/null +++ b/app/imports/client/ui/tabletop/CreatureFromLibraryDialog.vue @@ -0,0 +1,377 @@ + + + + + +resolveimport { toString } from '/imports/parser/toString'; diff --git a/app/imports/ui/tabletop/SelectCreaturesDialog.vue b/app/imports/client/ui/tabletop/SelectCreaturesDialog.vue similarity index 89% rename from app/imports/ui/tabletop/SelectCreaturesDialog.vue rename to app/imports/client/ui/tabletop/SelectCreaturesDialog.vue index 939b0d27..4801f4e3 100644 --- a/app/imports/ui/tabletop/SelectCreaturesDialog.vue +++ b/app/imports/client/ui/tabletop/SelectCreaturesDialog.vue @@ -30,9 +30,9 @@ @@ -234,12 +243,25 @@ export default { diff --git a/app/imports/client/ui/tabletop/TabletopBuffCard.vue b/app/imports/client/ui/tabletop/TabletopBuffCard.vue new file mode 100644 index 00000000..e0e94529 --- /dev/null +++ b/app/imports/client/ui/tabletop/TabletopBuffCard.vue @@ -0,0 +1,276 @@ + + + + + + + diff --git a/app/imports/client/ui/tabletop/TabletopComponent.vue b/app/imports/client/ui/tabletop/TabletopComponent.vue new file mode 100644 index 00000000..b821d88e --- /dev/null +++ b/app/imports/client/ui/tabletop/TabletopComponent.vue @@ -0,0 +1,288 @@ + + + + + diff --git a/app/imports/client/ui/tabletop/TabletopCreatureCard.vue b/app/imports/client/ui/tabletop/TabletopCreatureCard.vue new file mode 100644 index 00000000..da20895f --- /dev/null +++ b/app/imports/client/ui/tabletop/TabletopCreatureCard.vue @@ -0,0 +1,146 @@ + + + + + + + diff --git a/app/imports/client/ui/tabletop/TabletopCreatureListItem.vue b/app/imports/client/ui/tabletop/TabletopCreatureListItem.vue new file mode 100644 index 00000000..f0782788 --- /dev/null +++ b/app/imports/client/ui/tabletop/TabletopCreatureListItem.vue @@ -0,0 +1,76 @@ + + + + + \ No newline at end of file diff --git a/app/imports/client/ui/tabletop/TabletopDialog.vue b/app/imports/client/ui/tabletop/TabletopDialog.vue new file mode 100644 index 00000000..c131c4c6 --- /dev/null +++ b/app/imports/client/ui/tabletop/TabletopDialog.vue @@ -0,0 +1,217 @@ + + + + diff --git a/app/imports/client/ui/tabletop/TabletopForm.vue b/app/imports/client/ui/tabletop/TabletopForm.vue new file mode 100644 index 00000000..2f2cdabb --- /dev/null +++ b/app/imports/client/ui/tabletop/TabletopForm.vue @@ -0,0 +1,267 @@ + + + + diff --git a/app/imports/client/ui/tabletop/TabletopMap.vue b/app/imports/client/ui/tabletop/TabletopMap.vue new file mode 100644 index 00000000..7987abbe --- /dev/null +++ b/app/imports/client/ui/tabletop/TabletopMap.vue @@ -0,0 +1,8 @@ + + + diff --git a/app/imports/ui/tabletop/TabletopRightDrawer.vue b/app/imports/client/ui/tabletop/TabletopRightDrawer.vue similarity index 88% rename from app/imports/ui/tabletop/TabletopRightDrawer.vue rename to app/imports/client/ui/tabletop/TabletopRightDrawer.vue index 9d5fc55f..59058bc5 100644 --- a/app/imports/ui/tabletop/TabletopRightDrawer.vue +++ b/app/imports/client/ui/tabletop/TabletopRightDrawer.vue @@ -10,7 +10,7 @@ \ No newline at end of file diff --git a/app/imports/client/ui/tabletop/TabletopViewer.vue b/app/imports/client/ui/tabletop/TabletopViewer.vue new file mode 100644 index 00000000..22d46d7f --- /dev/null +++ b/app/imports/client/ui/tabletop/TabletopViewer.vue @@ -0,0 +1,103 @@ + + + + + diff --git a/app/imports/client/ui/tabletop/selectedCreatureBar/CreatureBarIcon.vue b/app/imports/client/ui/tabletop/selectedCreatureBar/CreatureBarIcon.vue new file mode 100644 index 00000000..cd9dd1cf --- /dev/null +++ b/app/imports/client/ui/tabletop/selectedCreatureBar/CreatureBarIcon.vue @@ -0,0 +1,53 @@ + + + diff --git a/app/imports/server/action.js b/app/imports/client/ui/tabletop/selectedCreatureBar/CreaturePortrait.vue similarity index 100% rename from app/imports/server/action.js rename to app/imports/client/ui/tabletop/selectedCreatureBar/CreaturePortrait.vue diff --git a/app/imports/client/ui/tabletop/selectedCreatureBar/SelectedCreatureBar.vue b/app/imports/client/ui/tabletop/selectedCreatureBar/SelectedCreatureBar.vue new file mode 100644 index 00000000..fde79ec9 --- /dev/null +++ b/app/imports/client/ui/tabletop/selectedCreatureBar/SelectedCreatureBar.vue @@ -0,0 +1,474 @@ + + + + + diff --git a/app/imports/ui/themes.js b/app/imports/client/ui/themes.js similarity index 100% rename from app/imports/ui/themes.js rename to app/imports/client/ui/themes.js diff --git a/app/imports/ui/user/DeleteUserAccountDialog.vue b/app/imports/client/ui/user/DeleteUserAccountDialog.vue similarity index 94% rename from app/imports/ui/user/DeleteUserAccountDialog.vue rename to app/imports/client/ui/user/DeleteUserAccountDialog.vue index a5a2d5ac..3575b796 100644 --- a/app/imports/ui/user/DeleteUserAccountDialog.vue +++ b/app/imports/client/ui/user/DeleteUserAccountDialog.vue @@ -89,10 +89,10 @@ - - \ No newline at end of file diff --git a/app/imports/ui/components/RollPopup.vue b/app/imports/ui/components/RollPopup.vue deleted file mode 100644 index 0167bc1d..00000000 --- a/app/imports/ui/components/RollPopup.vue +++ /dev/null @@ -1,118 +0,0 @@ - - - - - diff --git a/app/imports/ui/components/global/globalIndex.js b/app/imports/ui/components/global/globalIndex.js deleted file mode 100644 index 84c28bbe..00000000 --- a/app/imports/ui/components/global/globalIndex.js +++ /dev/null @@ -1,25 +0,0 @@ -import Vue from 'vue'; -// Global components -import DatePicker from '/imports/ui/components/global/DatePicker.vue'; -import IconPicker from '/imports/ui/components/global/IconPicker.vue'; -import TextField from '/imports/ui/components/global/TextField.vue'; -import TextArea from '/imports/ui/components/global/TextArea.vue'; -import SmartSelect from '/imports/ui/components/global/SmartSelect.vue'; -import SmartBtn from '/imports/ui/components/global/SmartBtn.vue'; -import SmartCombobox from '/imports/ui/components/global/SmartCombobox.vue'; -import SmartCheckbox from '/imports/ui/components/global/SmartCheckbox.vue'; -import SmartSwitch from '/imports/ui/components/global/SmartSwitch.vue'; -import SvgIcon from '/imports/ui/components/global/SvgIcon.vue'; -import SmartSlider from '/imports/ui/components/global/SmartSlider.vue'; - -Vue.component('DatePicker', DatePicker); -Vue.component('IconPicker', IconPicker); -Vue.component('TextField', TextField); -Vue.component('TextArea', TextArea); -Vue.component('SmartSelect', SmartSelect); -Vue.component('SmartBtn', SmartBtn); -Vue.component('SmartCombobox', SmartCombobox); -Vue.component('SmartCheckbox', SmartCheckbox); -Vue.component('SmartSlider', SmartSlider); -Vue.component('SmartSwitch', SmartSwitch); -Vue.component('SvgIcon', SvgIcon); diff --git a/app/imports/ui/components/rolls/Check.vue b/app/imports/ui/components/rolls/Check.vue deleted file mode 100644 index 19103522..00000000 --- a/app/imports/ui/components/rolls/Check.vue +++ /dev/null @@ -1,99 +0,0 @@ - - - - - diff --git a/app/imports/ui/components/tree/TreeSearchInput.vue b/app/imports/ui/components/tree/TreeSearchInput.vue deleted file mode 100644 index 5abac7e5..00000000 --- a/app/imports/ui/components/tree/TreeSearchInput.vue +++ /dev/null @@ -1,71 +0,0 @@ - - - - - diff --git a/app/imports/ui/creature/character/characterSheetTabs/FeaturesTab.vue b/app/imports/ui/creature/character/characterSheetTabs/FeaturesTab.vue deleted file mode 100644 index c1bfc215..00000000 --- a/app/imports/ui/creature/character/characterSheetTabs/FeaturesTab.vue +++ /dev/null @@ -1,60 +0,0 @@ - - - - - diff --git a/app/imports/ui/creature/character/characterSheetTabs/JournalTab.vue b/app/imports/ui/creature/character/characterSheetTabs/JournalTab.vue deleted file mode 100644 index f37e7b5b..00000000 --- a/app/imports/ui/creature/character/characterSheetTabs/JournalTab.vue +++ /dev/null @@ -1,58 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/imports/ui/creature/character/characterSheetTabs/SpellsTab.vue b/app/imports/ui/creature/character/characterSheetTabs/SpellsTab.vue deleted file mode 100644 index a5e307cc..00000000 --- a/app/imports/ui/creature/character/characterSheetTabs/SpellsTab.vue +++ /dev/null @@ -1,109 +0,0 @@ - - - - - diff --git a/app/imports/ui/creature/character/characterSheetTabs/StatsTab.vue b/app/imports/ui/creature/character/characterSheetTabs/StatsTab.vue deleted file mode 100644 index 78847e45..00000000 --- a/app/imports/ui/creature/character/characterSheetTabs/StatsTab.vue +++ /dev/null @@ -1,597 +0,0 @@ - - - - - diff --git a/app/imports/ui/creature/character/printedCharacterSheet/components/PrintedSpell.vue b/app/imports/ui/creature/character/printedCharacterSheet/components/PrintedSpell.vue deleted file mode 100644 index 8367c2a0..00000000 --- a/app/imports/ui/creature/character/printedCharacterSheet/components/PrintedSpell.vue +++ /dev/null @@ -1,82 +0,0 @@ - - - - - diff --git a/app/imports/ui/creature/creatureList/CreatureListTile.vue b/app/imports/ui/creature/creatureList/CreatureListTile.vue deleted file mode 100644 index 2bb28101..00000000 --- a/app/imports/ui/creature/creatureList/CreatureListTile.vue +++ /dev/null @@ -1,70 +0,0 @@ - - - diff --git a/app/imports/ui/creature/creatureProperties/CreaturePropertiesTree.vue b/app/imports/ui/creature/creatureProperties/CreaturePropertiesTree.vue deleted file mode 100644 index 8cc185f2..00000000 --- a/app/imports/ui/creature/creatureProperties/CreaturePropertiesTree.vue +++ /dev/null @@ -1,93 +0,0 @@ - - - - - diff --git a/app/imports/ui/creature/creatureProperties/CreaturePropertyCreationDialog.vue b/app/imports/ui/creature/creatureProperties/CreaturePropertyCreationDialog.vue deleted file mode 100644 index db6ceb72..00000000 --- a/app/imports/ui/creature/creatureProperties/CreaturePropertyCreationDialog.vue +++ /dev/null @@ -1,51 +0,0 @@ - - - - - diff --git a/app/imports/ui/creature/creatureProperties/CreaturePropertyInsertForm.vue b/app/imports/ui/creature/creatureProperties/CreaturePropertyInsertForm.vue deleted file mode 100644 index 951bbc13..00000000 --- a/app/imports/ui/creature/creatureProperties/CreaturePropertyInsertForm.vue +++ /dev/null @@ -1,93 +0,0 @@ - - - - - diff --git a/app/imports/ui/creature/slots/SlotCard.vue b/app/imports/ui/creature/slots/SlotCard.vue deleted file mode 100644 index 44f6ab76..00000000 --- a/app/imports/ui/creature/slots/SlotCard.vue +++ /dev/null @@ -1,76 +0,0 @@ - - - diff --git a/app/imports/ui/creature/slots/SlotCardsToFill.vue b/app/imports/ui/creature/slots/SlotCardsToFill.vue deleted file mode 100644 index 043489e7..00000000 --- a/app/imports/ui/creature/slots/SlotCardsToFill.vue +++ /dev/null @@ -1,141 +0,0 @@ - - - - - diff --git a/app/imports/ui/creature/slots/SlotFillDialog.vue b/app/imports/ui/creature/slots/SlotFillDialog.vue deleted file mode 100644 index cbad8fb9..00000000 --- a/app/imports/ui/creature/slots/SlotFillDialog.vue +++ /dev/null @@ -1,408 +0,0 @@ - - - - - diff --git a/app/imports/ui/dialogStack/DialogComponentIndex.js b/app/imports/ui/dialogStack/DialogComponentIndex.js deleted file mode 100644 index ef89fc6a..00000000 --- a/app/imports/ui/dialogStack/DialogComponentIndex.js +++ /dev/null @@ -1,66 +0,0 @@ -// Load commonly used dialogs immediately -import AddCreaturePropertyDialog from '/imports/ui/creature/creatureProperties/AddCreaturePropertyDialog.vue'; -import CharacterCreationDialog from '/imports/ui/creature/character/CharacterCreationDialog.vue'; -import CastSpellWithSlotDialog from '/imports/ui/properties/components/spells/CastSpellWithSlotDialog.vue'; -import CreatureFormDialog from '/imports/ui/creature/CreatureFormDialog.vue'; -import CreaturePropertyCreationDialog from '/imports/ui/creature/creatureProperties/CreaturePropertyCreationDialog.vue'; -import CreaturePropertyDialog from '/imports/ui/creature/creatureProperties/CreaturePropertyDialog.vue'; -import CreaturePropertyFromLibraryDialog from '/imports/ui/creature/creatureProperties/CreaturePropertyFromLibraryDialog.vue'; -import CreatureRootDialog from '/imports/ui/creature/character/CreatureRootDialog.vue'; -import DeleteConfirmationDialog from '/imports/ui/dialogStack/DeleteConfirmationDialog.vue'; -import ExperienceInsertDialog from '/imports/ui/creature/experiences/ExperienceInsertDialog.vue'; -import ExperienceListDialog from '/imports/ui/creature/experiences/ExperienceListDialog.vue'; -import HelpDialog from '/imports/ui/dialogStack/HelpDialog.vue'; -import LevelUpDialog from '/imports/ui/creature/slots/LevelUpDialog.vue'; -import SelectLibraryNodeDialog from '/imports/ui/library/SelectLibraryNodeDialog.vue'; -import SlotFillDialog from '/imports/ui/creature/slots/SlotFillDialog.vue'; -import TierTooLowDialog from '/imports/ui/user/TierTooLowDialog.vue'; -import TransferOwnershipDialog from '/imports/ui/sharing/TransferOwnershipDialog.vue'; - -// Lazily load less common dialogs -const ArchiveDialog = () => import('/imports/ui/creature/archive/ArchiveDialog.vue'); -const DeleteUserAccountDialog = () => import('/imports/ui/user/DeleteUserAccountDialog.vue'); -const InviteDialog = () => import('/imports/ui/user/InviteDialog.vue'); -const LibraryCollectionCreationDialog = () => import('/imports/ui/library/LibraryCollectionCreationDialog.vue'); -const LibraryCollectionEditDialog = () => import('/imports/ui/library/LibraryCollectionEditDialog.vue'); -const LibraryCreationDialog = () => import('/imports/ui/library/LibraryCreationDialog.vue'); -const LibraryEditDialog = () => import('/imports/ui/library/LibraryEditDialog.vue'); -const LibraryNodeCreationDialog = () => import('/imports/ui/library/LibraryNodeCreationDialog.vue'); -const LibraryNodeDialog = () => import('/imports/ui/library/LibraryNodeDialog.vue'); -const MoveLibraryNodeDialog = () => import('/imports/ui/library/MoveLibraryNodeDialog.vue'); -const SelectCreaturesDialog = () => import('/imports/ui/tabletop/SelectCreaturesDialog.vue'); -const ShareDialog = () => import('/imports/ui/sharing/ShareDialog.vue'); -const UsernameDialog = () => import('/imports/ui/user/UsernameDialog.vue'); - -export default { - AddCreaturePropertyDialog, - ArchiveDialog, - CastSpellWithSlotDialog, - CharacterCreationDialog, - CreatureFormDialog, - CreaturePropertyCreationDialog, - CreaturePropertyDialog, - CreaturePropertyFromLibraryDialog, - CreatureRootDialog, - DeleteConfirmationDialog, - DeleteUserAccountDialog, - ExperienceInsertDialog, - ExperienceListDialog, - HelpDialog, - InviteDialog, - LevelUpDialog, - LibraryCollectionCreationDialog, - LibraryCollectionEditDialog, - LibraryCreationDialog, - LibraryEditDialog, - LibraryNodeCreationDialog, - LibraryNodeDialog, - MoveLibraryNodeDialog, - SelectCreaturesDialog, - SelectLibraryNodeDialog, - ShareDialog, - SlotFillDialog, - TierTooLowDialog, - TransferOwnershipDialog, - UsernameDialog, -}; diff --git a/app/imports/ui/dialogStack/DialogStack.vue b/app/imports/ui/dialogStack/DialogStack.vue deleted file mode 100644 index 9a28588f..00000000 --- a/app/imports/ui/dialogStack/DialogStack.vue +++ /dev/null @@ -1,292 +0,0 @@ - - - - - diff --git a/app/imports/ui/files/UserImageCard.vue b/app/imports/ui/files/UserImageCard.vue deleted file mode 100644 index 85c5d155..00000000 --- a/app/imports/ui/files/UserImageCard.vue +++ /dev/null @@ -1,57 +0,0 @@ - - - diff --git a/app/imports/ui/layouts/Sidebar.vue b/app/imports/ui/layouts/Sidebar.vue deleted file mode 100644 index 31b5fa60..00000000 --- a/app/imports/ui/layouts/Sidebar.vue +++ /dev/null @@ -1,141 +0,0 @@ - - - diff --git a/app/imports/ui/library/InsertLibraryNodeButton.vue b/app/imports/ui/library/InsertLibraryNodeButton.vue deleted file mode 100644 index c9e4ba0f..00000000 --- a/app/imports/ui/library/InsertLibraryNodeButton.vue +++ /dev/null @@ -1,100 +0,0 @@ - - - - - diff --git a/app/imports/ui/library/LibraryNodeCreationDialog.vue b/app/imports/ui/library/LibraryNodeCreationDialog.vue deleted file mode 100644 index 2f4ff67d..00000000 --- a/app/imports/ui/library/LibraryNodeCreationDialog.vue +++ /dev/null @@ -1,34 +0,0 @@ - - - - - diff --git a/app/imports/ui/library/LibraryNodeDialog.vue b/app/imports/ui/library/LibraryNodeDialog.vue deleted file mode 100644 index 977f3906..00000000 --- a/app/imports/ui/library/LibraryNodeDialog.vue +++ /dev/null @@ -1,296 +0,0 @@ - - - - - diff --git a/app/imports/ui/library/LibraryNodeExpansionContent.vue b/app/imports/ui/library/LibraryNodeExpansionContent.vue deleted file mode 100644 index 30e614db..00000000 --- a/app/imports/ui/library/LibraryNodeExpansionContent.vue +++ /dev/null @@ -1,65 +0,0 @@ - - - - - diff --git a/app/imports/ui/library/LibraryNodeInsertForm.vue b/app/imports/ui/library/LibraryNodeInsertForm.vue deleted file mode 100644 index c006dde6..00000000 --- a/app/imports/ui/library/LibraryNodeInsertForm.vue +++ /dev/null @@ -1,120 +0,0 @@ - - - - - diff --git a/app/imports/ui/pages/Home.vue b/app/imports/ui/pages/Home.vue deleted file mode 100644 index 72642172..00000000 --- a/app/imports/ui/pages/Home.vue +++ /dev/null @@ -1,148 +0,0 @@ - - - - - diff --git a/app/imports/ui/pages/Tabletops.vue b/app/imports/ui/pages/Tabletops.vue deleted file mode 100644 index 5417feb4..00000000 --- a/app/imports/ui/pages/Tabletops.vue +++ /dev/null @@ -1,70 +0,0 @@ - - - - - diff --git a/app/imports/ui/properties/components/actions/EventButton.vue b/app/imports/ui/properties/components/actions/EventButton.vue deleted file mode 100644 index 29bdb0c6..00000000 --- a/app/imports/ui/properties/components/actions/EventButton.vue +++ /dev/null @@ -1,80 +0,0 @@ - - - - - diff --git a/app/imports/ui/properties/components/attributes/HealthBarCard.vue b/app/imports/ui/properties/components/attributes/HealthBarCard.vue deleted file mode 100644 index 48b4b89a..00000000 --- a/app/imports/ui/properties/components/attributes/HealthBarCard.vue +++ /dev/null @@ -1,27 +0,0 @@ - - - diff --git a/app/imports/ui/properties/components/attributes/HealthBarCardContainer.vue b/app/imports/ui/properties/components/attributes/HealthBarCardContainer.vue deleted file mode 100644 index db3b9c66..00000000 --- a/app/imports/ui/properties/components/attributes/HealthBarCardContainer.vue +++ /dev/null @@ -1,75 +0,0 @@ - - - diff --git a/app/imports/ui/properties/components/attributes/SpellSlotListTile.vue b/app/imports/ui/properties/components/attributes/SpellSlotListTile.vue deleted file mode 100644 index f4a00384..00000000 --- a/app/imports/ui/properties/components/attributes/SpellSlotListTile.vue +++ /dev/null @@ -1,109 +0,0 @@ - - - - - diff --git a/app/imports/ui/properties/components/folders/folderGroupComponents/AttributeGroupComponent.vue b/app/imports/ui/properties/components/folders/folderGroupComponents/AttributeGroupComponent.vue deleted file mode 100644 index 30677f39..00000000 --- a/app/imports/ui/properties/components/folders/folderGroupComponents/AttributeGroupComponent.vue +++ /dev/null @@ -1,95 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/imports/ui/properties/components/folders/propertyComponentIndex.js b/app/imports/ui/properties/components/folders/propertyComponentIndex.js deleted file mode 100644 index b61d0181..00000000 --- a/app/imports/ui/properties/components/folders/propertyComponentIndex.js +++ /dev/null @@ -1,61 +0,0 @@ -import action from '/imports/ui/properties/components/folders/folderGroupComponents/ActionGroupComponent.vue'; -//import adjustment from ''; -import attribute from './folderGroupComponents/AttributeGroupComponent.vue'; -import buff from '/imports/ui/properties/components/buffs/BuffListItem.vue'; -//import buffRemover from ''; -//import branch from ''; -//import constant from ''; -import container from '/imports/ui/properties/components/inventory/ContainerCard.vue'; -//import classComponent from ''; -//import classLevel from ''; -//import damage from ''; -//import damageMultiplier from ''; -//import effect from ''; -import feature from '/imports/ui/properties/components/features/FeatureCard.vue'; -// import folder from ''; -import item from '/imports/ui/properties/components/inventory/ItemListTile.vue'; -import note from '/imports/ui/properties/components/persona/NoteCard.vue'; -//import pointBuy from ''; -//import proficiency from ''; -//import propertySlot from ''; -//import reference from ''; -//import roll from ''; -//import savingThrow from ''; -import skill from '/imports/ui/properties/components/skills/SkillListTile.vue'; -//import slotFiller from ''; -//import spellList from ''; -//import spell from ''; -import toggle from '/imports/ui/properties/components/toggles/ToggleCard.vue'; -//import trigger from ''; - -export default { - action, - //adjustment, - attribute, - buff, - //buffRemover, - //branch, - //constant, - container, - //class: classComponent, - //classLevel, - //damage, - //damageMultiplier, - //effect, - feature, - //folder, - item, - note, - //pointBuy, - //proficiency, - //propertySlot, - //reference, - //roll, - //savingThrow, - skill, - //slotFiller, - //spellList, - //spell, - toggle, - //trigger, -}; diff --git a/app/imports/ui/properties/forms/AdjustmentForm.vue b/app/imports/ui/properties/forms/AdjustmentForm.vue deleted file mode 100644 index 31712722..00000000 --- a/app/imports/ui/properties/forms/AdjustmentForm.vue +++ /dev/null @@ -1,143 +0,0 @@ - - - - - diff --git a/app/imports/ui/properties/forms/AttributeForm.vue b/app/imports/ui/properties/forms/AttributeForm.vue deleted file mode 100644 index 8569e69b..00000000 --- a/app/imports/ui/properties/forms/AttributeForm.vue +++ /dev/null @@ -1,358 +0,0 @@ - - - - - diff --git a/app/imports/ui/properties/forms/BuffRemoverForm.vue b/app/imports/ui/properties/forms/BuffRemoverForm.vue deleted file mode 100644 index ed5845af..00000000 --- a/app/imports/ui/properties/forms/BuffRemoverForm.vue +++ /dev/null @@ -1,186 +0,0 @@ - - - - - diff --git a/app/imports/ui/properties/forms/ClassForm.vue b/app/imports/ui/properties/forms/ClassForm.vue deleted file mode 100644 index f4bd101e..00000000 --- a/app/imports/ui/properties/forms/ClassForm.vue +++ /dev/null @@ -1,181 +0,0 @@ - - - diff --git a/app/imports/ui/properties/forms/ClassLevelForm.vue b/app/imports/ui/properties/forms/ClassLevelForm.vue deleted file mode 100644 index 854380e2..00000000 --- a/app/imports/ui/properties/forms/ClassLevelForm.vue +++ /dev/null @@ -1,81 +0,0 @@ - - - - - diff --git a/app/imports/ui/properties/forms/ConstantForm.vue b/app/imports/ui/properties/forms/ConstantForm.vue deleted file mode 100644 index 27817bf3..00000000 --- a/app/imports/ui/properties/forms/ConstantForm.vue +++ /dev/null @@ -1,58 +0,0 @@ - - - - - diff --git a/app/imports/ui/properties/forms/ContainerForm.vue b/app/imports/ui/properties/forms/ContainerForm.vue deleted file mode 100644 index 58f127e0..00000000 --- a/app/imports/ui/properties/forms/ContainerForm.vue +++ /dev/null @@ -1,101 +0,0 @@ - - - diff --git a/app/imports/ui/properties/forms/DamageForm.vue b/app/imports/ui/properties/forms/DamageForm.vue deleted file mode 100644 index 1faa66f3..00000000 --- a/app/imports/ui/properties/forms/DamageForm.vue +++ /dev/null @@ -1,123 +0,0 @@ - - - - - diff --git a/app/imports/ui/properties/forms/DamageMultiplierForm.vue b/app/imports/ui/properties/forms/DamageMultiplierForm.vue deleted file mode 100644 index 551f1514..00000000 --- a/app/imports/ui/properties/forms/DamageMultiplierForm.vue +++ /dev/null @@ -1,150 +0,0 @@ - - - - - diff --git a/app/imports/ui/properties/forms/EffectForm.vue b/app/imports/ui/properties/forms/EffectForm.vue deleted file mode 100644 index 694db21d..00000000 --- a/app/imports/ui/properties/forms/EffectForm.vue +++ /dev/null @@ -1,342 +0,0 @@ - - - - - diff --git a/app/imports/ui/properties/forms/FolderForm.vue b/app/imports/ui/properties/forms/FolderForm.vue deleted file mode 100644 index b616505e..00000000 --- a/app/imports/ui/properties/forms/FolderForm.vue +++ /dev/null @@ -1,55 +0,0 @@ - - - - - diff --git a/app/imports/ui/properties/forms/NoteForm.vue b/app/imports/ui/properties/forms/NoteForm.vue deleted file mode 100644 index ba9eeafd..00000000 --- a/app/imports/ui/properties/forms/NoteForm.vue +++ /dev/null @@ -1,54 +0,0 @@ - - - diff --git a/app/imports/ui/properties/forms/ProficiencyForm.vue b/app/imports/ui/properties/forms/ProficiencyForm.vue deleted file mode 100644 index 4fcf2825..00000000 --- a/app/imports/ui/properties/forms/ProficiencyForm.vue +++ /dev/null @@ -1,65 +0,0 @@ - - - - - diff --git a/app/imports/ui/properties/forms/ReferenceForm.vue b/app/imports/ui/properties/forms/ReferenceForm.vue deleted file mode 100644 index 8c580322..00000000 --- a/app/imports/ui/properties/forms/ReferenceForm.vue +++ /dev/null @@ -1,84 +0,0 @@ - - - - - diff --git a/app/imports/ui/properties/forms/RollForm.vue b/app/imports/ui/properties/forms/RollForm.vue deleted file mode 100644 index c06ceaa6..00000000 --- a/app/imports/ui/properties/forms/RollForm.vue +++ /dev/null @@ -1,109 +0,0 @@ - - - - - diff --git a/app/imports/ui/properties/forms/SavingThrowForm.vue b/app/imports/ui/properties/forms/SavingThrowForm.vue deleted file mode 100644 index 8e0c5540..00000000 --- a/app/imports/ui/properties/forms/SavingThrowForm.vue +++ /dev/null @@ -1,111 +0,0 @@ - - - diff --git a/app/imports/ui/properties/forms/SkillForm.vue b/app/imports/ui/properties/forms/SkillForm.vue deleted file mode 100644 index 91d30970..00000000 --- a/app/imports/ui/properties/forms/SkillForm.vue +++ /dev/null @@ -1,153 +0,0 @@ - - - - - diff --git a/app/imports/ui/properties/forms/SlotFillerForm.vue b/app/imports/ui/properties/forms/SlotFillerForm.vue deleted file mode 100644 index 28890a30..00000000 --- a/app/imports/ui/properties/forms/SlotFillerForm.vue +++ /dev/null @@ -1,114 +0,0 @@ - - - diff --git a/app/imports/ui/properties/forms/SlotForm.vue b/app/imports/ui/properties/forms/SlotForm.vue deleted file mode 100644 index f2dc7c9b..00000000 --- a/app/imports/ui/properties/forms/SlotForm.vue +++ /dev/null @@ -1,272 +0,0 @@ - - - diff --git a/app/imports/ui/properties/forms/SpellListForm.vue b/app/imports/ui/properties/forms/SpellListForm.vue deleted file mode 100644 index 3855b78a..00000000 --- a/app/imports/ui/properties/forms/SpellListForm.vue +++ /dev/null @@ -1,123 +0,0 @@ - - - - - diff --git a/app/imports/ui/properties/forms/ToggleForm.vue b/app/imports/ui/properties/forms/ToggleForm.vue deleted file mode 100644 index 5f98729c..00000000 --- a/app/imports/ui/properties/forms/ToggleForm.vue +++ /dev/null @@ -1,131 +0,0 @@ - - - - - diff --git a/app/imports/ui/properties/forms/TriggerForm.vue b/app/imports/ui/properties/forms/TriggerForm.vue deleted file mode 100644 index d48932a7..00000000 --- a/app/imports/ui/properties/forms/TriggerForm.vue +++ /dev/null @@ -1,214 +0,0 @@ - - - diff --git a/app/imports/ui/properties/forms/shared/FormSections.vue b/app/imports/ui/properties/forms/shared/FormSections.vue deleted file mode 100644 index 70701ed4..00000000 --- a/app/imports/ui/properties/forms/shared/FormSections.vue +++ /dev/null @@ -1,12 +0,0 @@ - - - diff --git a/app/imports/ui/properties/forms/shared/lists/attributeListMixin.js b/app/imports/ui/properties/forms/shared/lists/attributeListMixin.js deleted file mode 100644 index 3ae537ba..00000000 --- a/app/imports/ui/properties/forms/shared/lists/attributeListMixin.js +++ /dev/null @@ -1,11 +0,0 @@ -import createListOfProperties from '/imports/ui/properties/forms/shared/lists/createListOfProperties.js'; - -const attributeListMixin = { - meteor: { - attributeList(){ - return createListOfProperties({type: {$in: ['attribute', 'skill']}}); - }, - }, -}; - -export default attributeListMixin; diff --git a/app/imports/ui/properties/forms/shared/lists/saveListMixin.js b/app/imports/ui/properties/forms/shared/lists/saveListMixin.js deleted file mode 100644 index 035817f6..00000000 --- a/app/imports/ui/properties/forms/shared/lists/saveListMixin.js +++ /dev/null @@ -1,11 +0,0 @@ -import createListOfProperties from '/imports/ui/properties/forms/shared/lists/createListOfProperties.js' - -const saveListMixin = { - meteor: { - saveList(){ - return createListOfProperties({type: 'skill', skillType: 'save'}); - }, - }, -}; - -export default saveListMixin; diff --git a/app/imports/ui/properties/forms/shared/lists/skillListMixin.js b/app/imports/ui/properties/forms/shared/lists/skillListMixin.js deleted file mode 100644 index 69d12256..00000000 --- a/app/imports/ui/properties/forms/shared/lists/skillListMixin.js +++ /dev/null @@ -1,11 +0,0 @@ -import createListOfProperties from '/imports/ui/properties/forms/shared/lists/createListOfProperties.js' - -const skillListMixin = { - meteor: { - skillList(){ - return createListOfProperties({type: 'skill'}); - }, - }, -}; - -export default skillListMixin; diff --git a/app/imports/ui/properties/forms/shared/propertyFormIndex.js b/app/imports/ui/properties/forms/shared/propertyFormIndex.js deleted file mode 100644 index ef29ea2d..00000000 --- a/app/imports/ui/properties/forms/shared/propertyFormIndex.js +++ /dev/null @@ -1,61 +0,0 @@ -import ActionForm from '/imports/ui/properties/forms/ActionForm.vue'; -import AdjustmentForm from '/imports/ui/properties/forms/AdjustmentForm.vue'; -import AttributeForm from '/imports/ui/properties/forms/AttributeForm.vue'; -import BuffForm from '/imports/ui/properties/forms/BuffForm.vue'; -import BuffRemoverForm from '/imports/ui/properties/forms/BuffRemoverForm.vue'; -import BranchForm from '/imports/ui/properties/forms/BranchForm.vue'; -import ClassForm from '/imports/ui/properties/forms/ClassForm.vue'; -import ClassLevelForm from '/imports/ui/properties/forms/ClassLevelForm.vue'; -import ConstantForm from '/imports/ui/properties/forms/ConstantForm.vue'; -import ContainerForm from '/imports/ui/properties/forms/ContainerForm.vue'; -import DamageForm from '/imports/ui/properties/forms/DamageForm.vue'; -import DamageMultiplierForm from '/imports/ui/properties/forms/DamageMultiplierForm.vue'; -import EffectForm from '/imports/ui/properties/forms/EffectForm.vue'; -import FeatureForm from '/imports/ui/properties/forms/FeatureForm.vue'; -import FolderForm from '/imports/ui/properties/forms/FolderForm.vue'; -import ItemForm from '/imports/ui/properties/forms/ItemForm.vue'; -import NoteForm from '/imports/ui/properties/forms/NoteForm.vue'; -import PointBuyForm from '/imports/ui/properties/forms/PointBuyForm.vue'; -import ProficiencyForm from '/imports/ui/properties/forms/ProficiencyForm.vue'; -import ReferenceForm from '/imports/ui/properties/forms/ReferenceForm.vue'; -import RollForm from '/imports/ui/properties/forms/RollForm.vue'; -import SavingThrowForm from '/imports/ui/properties/forms/SavingThrowForm.vue'; -import SkillForm from '/imports/ui/properties/forms/SkillForm.vue'; -import SlotForm from '/imports/ui/properties/forms/SlotForm.vue'; -import SlotFillerForm from '/imports/ui/properties/forms/SlotFillerForm.vue'; -import SpellListForm from '/imports/ui/properties/forms/SpellListForm.vue'; -import SpellForm from '/imports/ui/properties/forms/SpellForm.vue'; -import ToggleForm from '/imports/ui/properties/forms/ToggleForm.vue'; -import TriggerForm from '/imports/ui/properties/forms/TriggerForm.vue'; - -export default { - action: ActionForm, - adjustment: AdjustmentForm, - attribute: AttributeForm, - buff: BuffForm, - buffRemover: BuffRemoverForm, - branch: BranchForm, - constant: ConstantForm, - container: ContainerForm, - class: ClassForm, - classLevel: ClassLevelForm, - damage: DamageForm, - damageMultiplier: DamageMultiplierForm, - effect: EffectForm, - feature: FeatureForm, - folder: FolderForm, - item: ItemForm, - note: NoteForm, - pointBuy: PointBuyForm, - proficiency: ProficiencyForm, - propertySlot: SlotForm, - reference: ReferenceForm, - roll: RollForm, - savingThrow: SavingThrowForm, - skill: SkillForm, - slotFiller: SlotFillerForm, - spellList: SpellListForm, - spell: SpellForm, - toggle: ToggleForm, - trigger: TriggerForm, -}; diff --git a/app/imports/ui/properties/forms/shared/propertyFormMixin.js b/app/imports/ui/properties/forms/shared/propertyFormMixin.js deleted file mode 100644 index 172db58f..00000000 --- a/app/imports/ui/properties/forms/shared/propertyFormMixin.js +++ /dev/null @@ -1,40 +0,0 @@ -import ComputedField from '/imports/ui/properties/forms/shared/ComputedField.vue'; -import InlineComputationField from '/imports/ui/properties/forms/shared/InlineComputationField.vue'; -import FormSection, { FormSections } from '/imports/ui/properties/forms/shared/FormSection.vue'; - -export default { - components: { - ComputedField, - InlineComputationField, - FormSection, - FormSections, - }, - props: { - model: { - type: [Object, Array], - default: () => ({}), - }, - errors: { - type: Object, - default: () => ({}), - }, - }, - mounted(){ - // Don't autofocus on mobile, it brings up the on-screen keyboard - if (this.$vuetify.breakpoint.smAndDown) return; - - setTimeout(() => { - if (this.$refs.focusFirst && this.$refs.focusFirst.focus){ - this.$refs.focusFirst.focus() - } - }, 300); - }, - methods: { - change(path, value, ack){ - if (!Array.isArray(path)){ - path = [path]; - } - this.$emit('change', {path, value, ack}); - } - }, -} diff --git a/app/imports/ui/properties/shared/PropertyViewer.vue b/app/imports/ui/properties/shared/PropertyViewer.vue deleted file mode 100644 index 78b3c4d7..00000000 --- a/app/imports/ui/properties/shared/PropertyViewer.vue +++ /dev/null @@ -1,31 +0,0 @@ - - - - - diff --git a/app/imports/ui/properties/shared/getPropertyTitle.js b/app/imports/ui/properties/shared/getPropertyTitle.js deleted file mode 100644 index 7caecd3d..00000000 --- a/app/imports/ui/properties/shared/getPropertyTitle.js +++ /dev/null @@ -1,6 +0,0 @@ -import { getPropertyName } from '/imports/constants/PROPERTIES.js'; - -export default function getPropertyTitle(prop){ - if (prop.name) return prop.name; - return getPropertyName(prop.type); -} diff --git a/app/imports/ui/properties/treeNodeViews/DamageTreeNode.vue b/app/imports/ui/properties/treeNodeViews/DamageTreeNode.vue deleted file mode 100644 index 9c3ce1d1..00000000 --- a/app/imports/ui/properties/treeNodeViews/DamageTreeNode.vue +++ /dev/null @@ -1,35 +0,0 @@ - - - diff --git a/app/imports/ui/properties/treeNodeViews/treeNodeViewIndex.js b/app/imports/ui/properties/treeNodeViews/treeNodeViewIndex.js deleted file mode 100644 index 1f9772e8..00000000 --- a/app/imports/ui/properties/treeNodeViews/treeNodeViewIndex.js +++ /dev/null @@ -1,23 +0,0 @@ -import DefaultTreeNode from '/imports/ui/properties/treeNodeViews/DefaultTreeNode.vue'; -import AdjustmentTreeNode from '/imports/ui/properties/treeNodeViews/AdjustmentTreeNode.vue'; -import BranchTreeNode from '/imports/ui/properties/treeNodeViews/BranchTreeNode.vue'; -import ItemTreeNode from '/imports/ui/properties/treeNodeViews/ItemTreeNode.vue'; -import DamageTreeNode from '/imports/ui/properties/treeNodeViews/DamageTreeNode.vue'; -import EffectTreeNode from '/imports/ui/properties/treeNodeViews/EffectTreeNode.vue'; -import ClassLevelTreeNode from '/imports/ui/properties/treeNodeViews/ClassLevelTreeNode.vue'; -import ProficiencyTreeNode from '/imports/ui/properties/treeNodeViews/ProficiencyTreeNode.vue'; -import ReferenceTreeNode from '/imports/ui/properties/treeNodeViews/ReferenceTreeNode.vue'; -import SavingThrowTreeNode from '/imports/ui/properties/treeNodeViews/SavingThrowTreeNode.vue'; - -export default { - default: DefaultTreeNode, - adjustment: AdjustmentTreeNode, - branch: BranchTreeNode, - classLevel: ClassLevelTreeNode, - damage: DamageTreeNode, - effect: EffectTreeNode, - item: ItemTreeNode, - proficiency: ProficiencyTreeNode, - reference: ReferenceTreeNode, - savingThrow: SavingThrowTreeNode, -} diff --git a/app/imports/ui/properties/viewers/DamageViewer.vue b/app/imports/ui/properties/viewers/DamageViewer.vue deleted file mode 100644 index 8862e10c..00000000 --- a/app/imports/ui/properties/viewers/DamageViewer.vue +++ /dev/null @@ -1,39 +0,0 @@ - - - - - diff --git a/app/imports/ui/properties/viewers/FolderViewer.vue b/app/imports/ui/properties/viewers/FolderViewer.vue deleted file mode 100644 index 51a51a76..00000000 --- a/app/imports/ui/properties/viewers/FolderViewer.vue +++ /dev/null @@ -1,14 +0,0 @@ - - - - - diff --git a/app/imports/ui/properties/viewers/ReferenceViewer.vue b/app/imports/ui/properties/viewers/ReferenceViewer.vue deleted file mode 100644 index 3806984a..00000000 --- a/app/imports/ui/properties/viewers/ReferenceViewer.vue +++ /dev/null @@ -1,40 +0,0 @@ - - - - - diff --git a/app/imports/ui/properties/viewers/SlotFillerViewer.vue b/app/imports/ui/properties/viewers/SlotFillerViewer.vue deleted file mode 100644 index d3cb60c5..00000000 --- a/app/imports/ui/properties/viewers/SlotFillerViewer.vue +++ /dev/null @@ -1,61 +0,0 @@ - - - diff --git a/app/imports/ui/properties/viewers/shared/propertyViewerIndex.js b/app/imports/ui/properties/viewers/shared/propertyViewerIndex.js deleted file mode 100644 index 70151b48..00000000 --- a/app/imports/ui/properties/viewers/shared/propertyViewerIndex.js +++ /dev/null @@ -1,61 +0,0 @@ -import ActionViewer from '/imports/ui/properties/viewers/ActionViewer.vue'; -import AdjustmentViewer from '/imports/ui/properties/viewers/AdjustmentViewer.vue'; -import AttributeViewer from '/imports/ui/properties/viewers/AttributeViewer.vue'; -import BuffViewer from '/imports/ui/properties/viewers/BuffViewer.vue'; -import BuffRemoverViewer from '/imports/ui/properties/viewers/BuffRemoverViewer.vue'; -import BranchViewer from '/imports/ui/properties/viewers/BranchViewer.vue'; -import ContainerViewer from '/imports/ui/properties/viewers/ContainerViewer.vue'; -import ClassViewer from '/imports/ui/properties/viewers/ClassViewer.vue'; -import ClassLevelViewer from '/imports/ui/properties/viewers/ClassLevelViewer.vue'; -import ConstantViewer from '/imports/ui/properties/viewers/ConstantViewer.vue'; -import DamageViewer from '/imports/ui/properties/viewers/DamageViewer.vue'; -import DamageMultiplierViewer from '/imports/ui/properties/viewers/DamageMultiplierViewer.vue'; -import EffectViewer from '/imports/ui/properties/viewers/EffectViewer.vue'; -import FeatureViewer from '/imports/ui/properties/viewers/FeatureViewer.vue'; -import FolderViewer from '/imports/ui/properties/viewers/FolderViewer.vue'; -import ItemViewer from '/imports/ui/properties/viewers/ItemViewer.vue'; -import NoteViewer from '/imports/ui/properties/viewers/NoteViewer.vue'; -import PointBuyViewer from '/imports/ui/properties/viewers/PointBuyViewer.vue'; -import ProficiencyViewer from '/imports/ui/properties/viewers/ProficiencyViewer.vue'; -import ReferenceViewer from '/imports/ui/properties/viewers/ReferenceViewer.vue'; -import RollViewer from '/imports/ui/properties/viewers/RollViewer.vue'; -import SkillViewer from '/imports/ui/properties/viewers/SkillViewer.vue'; -import SavingThrowViewer from '/imports/ui/properties/viewers/SavingThrowViewer.vue'; -import SlotViewer from '/imports/ui/properties/viewers/SlotViewer.vue'; -import SlotFillerViewer from '/imports/ui/properties/viewers/SlotFillerViewer.vue'; -import SpellListViewer from '/imports/ui/properties/viewers/SpellListViewer.vue'; -import SpellViewer from '/imports/ui/properties/viewers/SpellViewer.vue'; -import ToggleViewer from '/imports/ui/properties/viewers/ToggleViewer.vue'; -import TriggerViewer from '/imports/ui/properties/viewers/TriggerViewer.vue'; - -export default { - action: ActionViewer, - adjustment: AdjustmentViewer, - attribute: AttributeViewer, - buff: BuffViewer, - buffRemover: BuffRemoverViewer, - branch: BranchViewer, - container: ContainerViewer, - class: ClassViewer, - classLevel: ClassLevelViewer, - constant: ConstantViewer, - damage: DamageViewer, - damageMultiplier: DamageMultiplierViewer, - effect: EffectViewer, - feature: FeatureViewer, - folder: FolderViewer, - item: ItemViewer, - note: NoteViewer, - pointBuy: PointBuyViewer, - proficiency: ProficiencyViewer, - propertySlot: SlotViewer, - roll: RollViewer, - reference: ReferenceViewer, - savingThrow: SavingThrowViewer, - slotFiller: SlotFillerViewer, - skill: SkillViewer, - spellList: SpellListViewer, - spell: SpellViewer, - toggle: ToggleViewer, - trigger: TriggerViewer, -}; diff --git a/app/imports/ui/properties/viewers/shared/propertyViewerMixin.js b/app/imports/ui/properties/viewers/shared/propertyViewerMixin.js deleted file mode 100644 index de4278f2..00000000 --- a/app/imports/ui/properties/viewers/shared/propertyViewerMixin.js +++ /dev/null @@ -1,23 +0,0 @@ -import PropertyName from '/imports/ui/properties/viewers/shared/PropertyName.vue'; -import PropertyVariableName from '/imports/ui/properties/viewers/shared/PropertyVariableName.vue'; -import PropertyField from '/imports/ui/properties/viewers/shared/PropertyField.vue'; -import PropertyDescription from '/imports/ui/properties/viewers/shared/PropertyDescription.vue'; -import PropertyTags from '/imports/ui/properties/viewers/shared/PropertyTags.vue'; - -const propertyViewerMixin = { - components: { - PropertyName, - PropertyVariableName, - PropertyField, - PropertyDescription, - PropertyTags, - }, - props: { - model: { - type: Object, - required: true, - }, - }, -}; - -export default propertyViewerMixin; diff --git a/app/imports/ui/tabletop/TabletopActionCards.vue b/app/imports/ui/tabletop/TabletopActionCards.vue deleted file mode 100644 index 2e732f15..00000000 --- a/app/imports/ui/tabletop/TabletopActionCards.vue +++ /dev/null @@ -1,55 +0,0 @@ - - - - - diff --git a/app/imports/ui/tabletop/TabletopComponent.vue b/app/imports/ui/tabletop/TabletopComponent.vue deleted file mode 100644 index ee3b8a58..00000000 --- a/app/imports/ui/tabletop/TabletopComponent.vue +++ /dev/null @@ -1,106 +0,0 @@ - - - - - diff --git a/app/imports/ui/tabletop/TabletopCreatureCard.vue b/app/imports/ui/tabletop/TabletopCreatureCard.vue deleted file mode 100644 index 80c1561b..00000000 --- a/app/imports/ui/tabletop/TabletopCreatureCard.vue +++ /dev/null @@ -1,37 +0,0 @@ - - - - - diff --git a/app/imports/ui/tabletop/TabletopLog.vue b/app/imports/ui/tabletop/TabletopLog.vue deleted file mode 100644 index 35ac1ad1..00000000 --- a/app/imports/ui/tabletop/TabletopLog.vue +++ /dev/null @@ -1,55 +0,0 @@ - - - - - diff --git a/app/imports/ui/tabletop/TabletopMap.vue b/app/imports/ui/tabletop/TabletopMap.vue deleted file mode 100644 index 605da292..00000000 --- a/app/imports/ui/tabletop/TabletopMap.vue +++ /dev/null @@ -1,11 +0,0 @@ - - - - - diff --git a/app/imports/ui/utility/escapeRegex.js b/app/imports/ui/utility/escapeRegex.js deleted file mode 100644 index 8ed9c0b9..00000000 --- a/app/imports/ui/utility/escapeRegex.js +++ /dev/null @@ -1,3 +0,0 @@ -RegExp.escape = function(s) { - return s.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'); -}; diff --git a/app/imports/ui/utility/numberToSignedString.js b/app/imports/ui/utility/numberToSignedString.js deleted file mode 100644 index da6b124b..00000000 --- a/app/imports/ui/utility/numberToSignedString.js +++ /dev/null @@ -1,10 +0,0 @@ -export default function numberToSignedString(number, spaced){ - if (typeof number !== 'number') return number; - if (number === 0){ - return spaced ? '+ 0' : '+0'; - } else if (number > 0){ - return spaced ? `+ ${number}` : `+${number}`; - } else { - return spaced ? `- ${Math.abs(number) || number}` : `${number}`; - } -} diff --git a/app/imports/ui/utility/sortEffects.js b/app/imports/ui/utility/sortEffects.js deleted file mode 100644 index d7779cdd..00000000 --- a/app/imports/ui/utility/sortEffects.js +++ /dev/null @@ -1,21 +0,0 @@ -const INDEX = { - 'base': 1, - 'add': 2, - 'mul': 3, - 'min': 4, - 'max': 5, - 'advantage': 6, - 'disadvantage': 7, - 'passiveAdd': 8, - 'fail': 9, - 'conditional': 10, -}; - -function sortEffects(effects){ - if (!effects || !effects.length) return []; - return [...effects].sort( - (a, b) => (INDEX[a.operation] || 99) - (INDEX[b.operation] || 99) - ); -} - -export default sortEffects; diff --git a/app/imports/ui/vuexStore.js b/app/imports/ui/vuexStore.js deleted file mode 100644 index 173e276b..00000000 --- a/app/imports/ui/vuexStore.js +++ /dev/null @@ -1,65 +0,0 @@ -import Vue from 'vue'; -import Vuex from 'vuex'; -import dialogStackStore from '/imports/ui/dialogStack/dialogStackStore.js'; -import Creatures from '/imports/api/creature/creatures/Creatures.js'; -const tabs = ['stats', 'features', 'inventory', 'spells', 'journal', 'build', 'tree']; -const tabsWithoutSpells = ['stats', 'features', 'inventory', 'journal', 'build', 'tree']; - -Vue.use(Vuex); -const store = new Vuex.Store({ - strict: process.env.NODE_ENV !== 'production', - modules: { - dialogStack: dialogStackStore, - }, - state: { - drawer: undefined, - rightDrawer: undefined, - pageTitle: undefined, - characterSheetTabs: {}, - showDetailsDialog: false, - }, - getters: { - tabById: (state) => (id) => { - if (id in state.characterSheetTabs){ - return state.characterSheetTabs[id]; - } else { - return 0; - } - }, - tabNameById: (state) => (id) => { - const tabNumber = state.characterSheetTabs[id]; - const creature = Creatures.findOne(id); - if (creature?.settings?.hideSpellsTab) { - return tabsWithoutSpells[tabNumber]; - } else { - return tabs[tabNumber] - } - } - }, - mutations: { - toggleDrawer (state) { - state.drawer = !state.drawer; - }, - toggleRightDrawer (state) { - state.rightDrawer = !state.rightDrawer; - }, - setDrawer (state, value) { - state.drawer = value; - }, - setRightDrawer (state, value) { - state.rightDrawer = value; - }, - setPageTitle (state, value) { - state.pageTitle = value; - document.title = value; - }, - setTabForCharacterSheet(state, {tab, id}){ - Vue.set(state.characterSheetTabs, id, tab); - }, - setShowDetailsDialog(state, value){ - state.showDetailsDialog = value; - }, - }, -}); - -export default store; diff --git a/app/jsconfig.json b/app/jsconfig.json deleted file mode 100644 index b97e629d..00000000 --- a/app/jsconfig.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "compilerOptions": { - "module": "ESNext", - "moduleResolution": "Node", - "target": "ES2020", - "jsx": "react", - "strictNullChecks": true, - "strictFunctionTypes": true, - "baseUrl": ".", - "paths": { - "/*": [ - "./*" - ] - } - }, - "vueCompilerOptions": { - "target": 2, - }, - "exclude": [ - "node_modules", - "**/node_modules/*" - ] -} \ No newline at end of file diff --git a/app/package-lock.json b/app/package-lock.json index 6e4b3ba5..7c536713 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -1,9 +1,2850 @@ { "name": "dicecloud", - "version": "2.0.43", + "version": "2.1.0", "lockfileVersion": 1, "requires": true, "dependencies": { + "@aws-crypto/crc32": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-3.0.0.tgz", + "integrity": "sha512-IzSgsrxUcsrejQbPVilIKy16kAT52EwB6zSaI+M3xxIhKh5+aldEyvI+z6erM7TCLB2BJsFrtHjp6/4/sr+3dA==", + "requires": { + "@aws-crypto/util": "^3.0.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^1.11.1" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + } + } + }, + "@aws-crypto/crc32c": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-3.0.0.tgz", + "integrity": "sha512-ENNPPManmnVJ4BTXlOjAgD7URidbAznURqD0KvfREyc4o20DPYdEldU1f5cQ7Jbj0CJJSPaMIk/9ZshdB3210w==", + "requires": { + "@aws-crypto/util": "^3.0.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^1.11.1" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + } + } + }, + "@aws-crypto/ie11-detection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/ie11-detection/-/ie11-detection-3.0.0.tgz", + "integrity": "sha512-341lBBkiY1DfDNKai/wXM3aujNBkXR7tq1URPQDL9wi3AUbI80NR74uF1TXHMm7po1AcnFk8iu2S2IeU/+/A+Q==", + "requires": { + "tslib": "^1.11.1" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + } + } + }, + "@aws-crypto/sha1-browser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha1-browser/-/sha1-browser-3.0.0.tgz", + "integrity": "sha512-NJth5c997GLHs6nOYTzFKTbYdMNA6/1XlKVgnZoaZcQ7z7UJlOgj2JdbHE8tiYLS3fzXNCguct77SPGat2raSw==", + "requires": { + "@aws-crypto/ie11-detection": "^3.0.0", + "@aws-crypto/supports-web-crypto": "^3.0.0", + "@aws-crypto/util": "^3.0.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@aws-sdk/util-utf8-browser": "^3.0.0", + "tslib": "^1.11.1" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + } + } + }, + "@aws-crypto/sha256-browser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-3.0.0.tgz", + "integrity": "sha512-8VLmW2B+gjFbU5uMeqtQM6Nj0/F1bro80xQXCW6CQBWgosFWXTx77aeOF5CAIAmbOK64SdMBJdNr6J41yP5mvQ==", + "requires": { + "@aws-crypto/ie11-detection": "^3.0.0", + "@aws-crypto/sha256-js": "^3.0.0", + "@aws-crypto/supports-web-crypto": "^3.0.0", + "@aws-crypto/util": "^3.0.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@aws-sdk/util-utf8-browser": "^3.0.0", + "tslib": "^1.11.1" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + } + } + }, + "@aws-crypto/sha256-js": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-3.0.0.tgz", + "integrity": "sha512-PnNN7os0+yd1XvXAy23CFOmTbMaDxgxXtTKHybrJ39Y8kGzBATgBFibWJKH6BhytLI/Zyszs87xCOBNyBig6vQ==", + "requires": { + "@aws-crypto/util": "^3.0.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^1.11.1" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + } + } + }, + "@aws-crypto/supports-web-crypto": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-3.0.0.tgz", + "integrity": "sha512-06hBdMwUAb2WFTuGG73LSC0wfPu93xWwo5vL2et9eymgmu3Id5vFAHBbajVWiGhPO37qcsdCap/FqXvJGJWPIg==", + "requires": { + "tslib": "^1.11.1" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + } + } + }, + "@aws-crypto/util": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-3.0.0.tgz", + "integrity": "sha512-2OJlpeJpCR48CC8r+uKVChzs9Iungj9wkZrl8Z041DWEWvyIHILYKCPNzJghKsivj+S3mLo6BVc7mBNzdxA46w==", + "requires": { + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-utf8-browser": "^3.0.0", + "tslib": "^1.11.1" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + } + } + }, + "@aws-sdk/client-cognito-identity": { + "version": "3.540.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-cognito-identity/-/client-cognito-identity-3.540.0.tgz", + "integrity": "sha512-03vUaIKjvdcOmjDi8Fv9JgY+VQrt9QBpRkI8A1lrdPNgWqTEZXZi/zBsFRsxTe6hgsrZtxVnxLu6krSRILuqtw==", + "dev": true, + "optional": true, + "requires": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/client-sts": "3.540.0", + "@aws-sdk/core": "3.535.0", + "@aws-sdk/credential-provider-node": "3.540.0", + "@aws-sdk/middleware-host-header": "3.535.0", + "@aws-sdk/middleware-logger": "3.535.0", + "@aws-sdk/middleware-recursion-detection": "3.535.0", + "@aws-sdk/middleware-user-agent": "3.540.0", + "@aws-sdk/region-config-resolver": "3.535.0", + "@aws-sdk/types": "3.535.0", + "@aws-sdk/util-endpoints": "3.540.0", + "@aws-sdk/util-user-agent-browser": "3.535.0", + "@aws-sdk/util-user-agent-node": "3.535.0", + "@smithy/config-resolver": "^2.2.0", + "@smithy/core": "^1.4.0", + "@smithy/fetch-http-handler": "^2.5.0", + "@smithy/hash-node": "^2.2.0", + "@smithy/invalid-dependency": "^2.2.0", + "@smithy/middleware-content-length": "^2.2.0", + "@smithy/middleware-endpoint": "^2.5.0", + "@smithy/middleware-retry": "^2.2.0", + "@smithy/middleware-serde": "^2.3.0", + "@smithy/middleware-stack": "^2.2.0", + "@smithy/node-config-provider": "^2.3.0", + "@smithy/node-http-handler": "^2.5.0", + "@smithy/protocol-http": "^3.3.0", + "@smithy/smithy-client": "^2.5.0", + "@smithy/types": "^2.12.0", + "@smithy/url-parser": "^2.2.0", + "@smithy/util-base64": "^2.3.0", + "@smithy/util-body-length-browser": "^2.2.0", + "@smithy/util-body-length-node": "^2.3.0", + "@smithy/util-defaults-mode-browser": "^2.2.0", + "@smithy/util-defaults-mode-node": "^2.3.0", + "@smithy/util-endpoints": "^1.2.0", + "@smithy/util-middleware": "^2.2.0", + "@smithy/util-retry": "^2.2.0", + "@smithy/util-utf8": "^2.3.0", + "tslib": "^2.6.2" + }, + "dependencies": { + "@aws-sdk/types": { + "version": "3.535.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.535.0.tgz", + "integrity": "sha512-aY4MYfduNj+sRR37U7XxYR8wemfbKP6lx00ze2M2uubn7mZotuVrWYAafbMSXrdEMSToE5JDhr28vArSOoLcSg==", + "dev": true, + "optional": true, + "requires": { + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + } + }, + "@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + }, + "@smithy/types": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.12.0.tgz", + "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + }, + "@smithy/util-base64": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-2.3.0.tgz", + "integrity": "sha512-s3+eVwNeJuXUwuMbusncZNViuhv2LjVJ1nMwTqSA0XAC7gjKhqqxRdJPhR8+YrkoZ9IiIbFk/yK6ACe/xlF+hw==", + "dev": true, + "optional": true, + "requires": { + "@smithy/util-buffer-from": "^2.2.0", + "@smithy/util-utf8": "^2.3.0", + "tslib": "^2.6.2" + } + }, + "@smithy/util-body-length-browser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-2.2.0.tgz", + "integrity": "sha512-dtpw9uQP7W+n3vOtx0CfBD5EWd7EPdIdsQnWTDoFf77e3VUf05uA7R7TGipIo8e4WL2kuPdnsr3hMQn9ziYj5w==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + }, + "@smithy/util-body-length-node": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-2.3.0.tgz", + "integrity": "sha512-ITWT1Wqjubf2CJthb0BuT9+bpzBfXeMokH/AAa5EJQgbv9aPMVfnM76iFIZVFf50hYXGbtiV71BHAthNWd6+dw==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + }, + "@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "dev": true, + "optional": true, + "requires": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + } + }, + "@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "dev": true, + "optional": true, + "requires": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + } + } + } + }, + "@aws-sdk/client-s3": { + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.523.0.tgz", + "integrity": "sha512-d8kFgZpdHOCLtv38nNkItTs3Ew+Ui/YadkCprvbY0boCrFZFTynficFM4orVk+fV3beJ2qVeJm6t8t14V5TaVA==", + "requires": { + "@aws-crypto/sha1-browser": "3.0.0", + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/client-sts": "3.523.0", + "@aws-sdk/core": "3.523.0", + "@aws-sdk/credential-provider-node": "3.523.0", + "@aws-sdk/middleware-bucket-endpoint": "3.523.0", + "@aws-sdk/middleware-expect-continue": "3.523.0", + "@aws-sdk/middleware-flexible-checksums": "3.523.0", + "@aws-sdk/middleware-host-header": "3.523.0", + "@aws-sdk/middleware-location-constraint": "3.523.0", + "@aws-sdk/middleware-logger": "3.523.0", + "@aws-sdk/middleware-recursion-detection": "3.523.0", + "@aws-sdk/middleware-sdk-s3": "3.523.0", + "@aws-sdk/middleware-signing": "3.523.0", + "@aws-sdk/middleware-ssec": "3.523.0", + "@aws-sdk/middleware-user-agent": "3.523.0", + "@aws-sdk/region-config-resolver": "3.523.0", + "@aws-sdk/signature-v4-multi-region": "3.523.0", + "@aws-sdk/types": "3.523.0", + "@aws-sdk/util-endpoints": "3.523.0", + "@aws-sdk/util-user-agent-browser": "3.523.0", + "@aws-sdk/util-user-agent-node": "3.523.0", + "@aws-sdk/xml-builder": "3.523.0", + "@smithy/config-resolver": "^2.1.3", + "@smithy/core": "^1.3.4", + "@smithy/eventstream-serde-browser": "^2.1.3", + "@smithy/eventstream-serde-config-resolver": "^2.1.3", + "@smithy/eventstream-serde-node": "^2.1.3", + "@smithy/fetch-http-handler": "^2.4.3", + "@smithy/hash-blob-browser": "^2.1.3", + "@smithy/hash-node": "^2.1.3", + "@smithy/hash-stream-node": "^2.1.3", + "@smithy/invalid-dependency": "^2.1.3", + "@smithy/md5-js": "^2.1.3", + "@smithy/middleware-content-length": "^2.1.3", + "@smithy/middleware-endpoint": "^2.4.3", + "@smithy/middleware-retry": "^2.1.3", + "@smithy/middleware-serde": "^2.1.3", + "@smithy/middleware-stack": "^2.1.3", + "@smithy/node-config-provider": "^2.2.3", + "@smithy/node-http-handler": "^2.4.1", + "@smithy/protocol-http": "^3.2.1", + "@smithy/smithy-client": "^2.4.1", + "@smithy/types": "^2.10.1", + "@smithy/url-parser": "^2.1.3", + "@smithy/util-base64": "^2.1.1", + "@smithy/util-body-length-browser": "^2.1.1", + "@smithy/util-body-length-node": "^2.2.1", + "@smithy/util-defaults-mode-browser": "^2.1.3", + "@smithy/util-defaults-mode-node": "^2.2.2", + "@smithy/util-endpoints": "^1.1.3", + "@smithy/util-retry": "^2.1.3", + "@smithy/util-stream": "^2.1.3", + "@smithy/util-utf8": "^2.1.1", + "@smithy/util-waiter": "^2.1.3", + "fast-xml-parser": "4.2.5", + "tslib": "^2.5.0" + }, + "dependencies": { + "@aws-sdk/client-sso": { + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.523.0.tgz", + "integrity": "sha512-vob/Tk9bIr6VIyzScBWsKpP92ACI6/aOXBL2BITgvRWl5Umqi1jXFtfssj/N2UJHM4CBMRwxIJ33InfN0gPxZw==", + "requires": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/core": "3.523.0", + "@aws-sdk/middleware-host-header": "3.523.0", + "@aws-sdk/middleware-logger": "3.523.0", + "@aws-sdk/middleware-recursion-detection": "3.523.0", + "@aws-sdk/middleware-user-agent": "3.523.0", + "@aws-sdk/region-config-resolver": "3.523.0", + "@aws-sdk/types": "3.523.0", + "@aws-sdk/util-endpoints": "3.523.0", + "@aws-sdk/util-user-agent-browser": "3.523.0", + "@aws-sdk/util-user-agent-node": "3.523.0", + "@smithy/config-resolver": "^2.1.3", + "@smithy/core": "^1.3.4", + "@smithy/fetch-http-handler": "^2.4.3", + "@smithy/hash-node": "^2.1.3", + "@smithy/invalid-dependency": "^2.1.3", + "@smithy/middleware-content-length": "^2.1.3", + "@smithy/middleware-endpoint": "^2.4.3", + "@smithy/middleware-retry": "^2.1.3", + "@smithy/middleware-serde": "^2.1.3", + "@smithy/middleware-stack": "^2.1.3", + "@smithy/node-config-provider": "^2.2.3", + "@smithy/node-http-handler": "^2.4.1", + "@smithy/protocol-http": "^3.2.1", + "@smithy/smithy-client": "^2.4.1", + "@smithy/types": "^2.10.1", + "@smithy/url-parser": "^2.1.3", + "@smithy/util-base64": "^2.1.1", + "@smithy/util-body-length-browser": "^2.1.1", + "@smithy/util-body-length-node": "^2.2.1", + "@smithy/util-defaults-mode-browser": "^2.1.3", + "@smithy/util-defaults-mode-node": "^2.2.2", + "@smithy/util-endpoints": "^1.1.3", + "@smithy/util-middleware": "^2.1.3", + "@smithy/util-retry": "^2.1.3", + "@smithy/util-utf8": "^2.1.1", + "tslib": "^2.5.0" + } + }, + "@aws-sdk/client-sso-oidc": { + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.523.0.tgz", + "integrity": "sha512-OktkdiuJ5DtYgNrJlo53Tf7pJ+UWfOt7V7or0ije6MysLP18GwlTkbg2UE4EUtfOxt/baXxHMlExB1vmRtlATw==", + "requires": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/client-sts": "3.523.0", + "@aws-sdk/core": "3.523.0", + "@aws-sdk/middleware-host-header": "3.523.0", + "@aws-sdk/middleware-logger": "3.523.0", + "@aws-sdk/middleware-recursion-detection": "3.523.0", + "@aws-sdk/middleware-user-agent": "3.523.0", + "@aws-sdk/region-config-resolver": "3.523.0", + "@aws-sdk/types": "3.523.0", + "@aws-sdk/util-endpoints": "3.523.0", + "@aws-sdk/util-user-agent-browser": "3.523.0", + "@aws-sdk/util-user-agent-node": "3.523.0", + "@smithy/config-resolver": "^2.1.3", + "@smithy/core": "^1.3.4", + "@smithy/fetch-http-handler": "^2.4.3", + "@smithy/hash-node": "^2.1.3", + "@smithy/invalid-dependency": "^2.1.3", + "@smithy/middleware-content-length": "^2.1.3", + "@smithy/middleware-endpoint": "^2.4.3", + "@smithy/middleware-retry": "^2.1.3", + "@smithy/middleware-serde": "^2.1.3", + "@smithy/middleware-stack": "^2.1.3", + "@smithy/node-config-provider": "^2.2.3", + "@smithy/node-http-handler": "^2.4.1", + "@smithy/protocol-http": "^3.2.1", + "@smithy/smithy-client": "^2.4.1", + "@smithy/types": "^2.10.1", + "@smithy/url-parser": "^2.1.3", + "@smithy/util-base64": "^2.1.1", + "@smithy/util-body-length-browser": "^2.1.1", + "@smithy/util-body-length-node": "^2.2.1", + "@smithy/util-defaults-mode-browser": "^2.1.3", + "@smithy/util-defaults-mode-node": "^2.2.2", + "@smithy/util-endpoints": "^1.1.3", + "@smithy/util-middleware": "^2.1.3", + "@smithy/util-retry": "^2.1.3", + "@smithy/util-utf8": "^2.1.1", + "tslib": "^2.5.0" + } + }, + "@aws-sdk/client-sts": { + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.523.0.tgz", + "integrity": "sha512-ggAkL8szaJkqD8oOsS68URJ9XMDbLA/INO/NPZJqv9BhmftecJvfy43uUVWGNs6n4YXNzfF0Y+zQ3DT0fZkv9g==", + "requires": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/core": "3.523.0", + "@aws-sdk/middleware-host-header": "3.523.0", + "@aws-sdk/middleware-logger": "3.523.0", + "@aws-sdk/middleware-recursion-detection": "3.523.0", + "@aws-sdk/middleware-user-agent": "3.523.0", + "@aws-sdk/region-config-resolver": "3.523.0", + "@aws-sdk/types": "3.523.0", + "@aws-sdk/util-endpoints": "3.523.0", + "@aws-sdk/util-user-agent-browser": "3.523.0", + "@aws-sdk/util-user-agent-node": "3.523.0", + "@smithy/config-resolver": "^2.1.3", + "@smithy/core": "^1.3.4", + "@smithy/fetch-http-handler": "^2.4.3", + "@smithy/hash-node": "^2.1.3", + "@smithy/invalid-dependency": "^2.1.3", + "@smithy/middleware-content-length": "^2.1.3", + "@smithy/middleware-endpoint": "^2.4.3", + "@smithy/middleware-retry": "^2.1.3", + "@smithy/middleware-serde": "^2.1.3", + "@smithy/middleware-stack": "^2.1.3", + "@smithy/node-config-provider": "^2.2.3", + "@smithy/node-http-handler": "^2.4.1", + "@smithy/protocol-http": "^3.2.1", + "@smithy/smithy-client": "^2.4.1", + "@smithy/types": "^2.10.1", + "@smithy/url-parser": "^2.1.3", + "@smithy/util-base64": "^2.1.1", + "@smithy/util-body-length-browser": "^2.1.1", + "@smithy/util-body-length-node": "^2.2.1", + "@smithy/util-defaults-mode-browser": "^2.1.3", + "@smithy/util-defaults-mode-node": "^2.2.2", + "@smithy/util-endpoints": "^1.1.3", + "@smithy/util-middleware": "^2.1.3", + "@smithy/util-retry": "^2.1.3", + "@smithy/util-utf8": "^2.1.1", + "fast-xml-parser": "4.2.5", + "tslib": "^2.5.0" + } + }, + "@aws-sdk/core": { + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.523.0.tgz", + "integrity": "sha512-JHa3ngEWkTzZ2YTn6EavcADC8gv6zZU4U9WBAleClh6ioXH0kGMBawZje3y0F0mKyLTfLhFqFUlCV5sngI/Qcw==", + "requires": { + "@smithy/core": "^1.3.4", + "@smithy/protocol-http": "^3.2.1", + "@smithy/signature-v4": "^2.1.3", + "@smithy/smithy-client": "^2.4.1", + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + } + }, + "@aws-sdk/credential-provider-env": { + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.523.0.tgz", + "integrity": "sha512-Y6DWdH6/OuMDoNKVzZlNeBc6f1Yjk1lYMjANKpIhMbkRCvLJw/PYZKOZa8WpXbTYdgg9XLjKybnLIb3ww3uuzA==", + "requires": { + "@aws-sdk/types": "3.523.0", + "@smithy/property-provider": "^2.1.3", + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + } + }, + "@aws-sdk/credential-provider-http": { + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.523.0.tgz", + "integrity": "sha512-6YUtePbn3UFpY9qfVwHFWIVnFvVS5vsbGxxkTO02swvZBvVG4sdG0Xj0AbotUNQNY9QTCN7WkhwIrd50rfDQ9Q==", + "requires": { + "@aws-sdk/types": "3.523.0", + "@smithy/fetch-http-handler": "^2.4.3", + "@smithy/node-http-handler": "^2.4.1", + "@smithy/property-provider": "^2.1.3", + "@smithy/protocol-http": "^3.2.1", + "@smithy/smithy-client": "^2.4.1", + "@smithy/types": "^2.10.1", + "@smithy/util-stream": "^2.1.3", + "tslib": "^2.5.0" + } + }, + "@aws-sdk/credential-provider-ini": { + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.523.0.tgz", + "integrity": "sha512-dRch5Ts67FFRZY5r9DpiC3PM6BVHv1tRcy1b26hoqfFkxP9xYH3dsTSPBog1azIqaJa2GcXqEvKCqhghFTt4Xg==", + "requires": { + "@aws-sdk/client-sts": "3.523.0", + "@aws-sdk/credential-provider-env": "3.523.0", + "@aws-sdk/credential-provider-process": "3.523.0", + "@aws-sdk/credential-provider-sso": "3.523.0", + "@aws-sdk/credential-provider-web-identity": "3.523.0", + "@aws-sdk/types": "3.523.0", + "@smithy/credential-provider-imds": "^2.2.3", + "@smithy/property-provider": "^2.1.3", + "@smithy/shared-ini-file-loader": "^2.3.3", + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + } + }, + "@aws-sdk/credential-provider-node": { + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.523.0.tgz", + "integrity": "sha512-0aW5ylA8pZmvv/8qA/+iel4acEyzSlHRiaHYL3L0qu9SSoe2a92+RHjrxKl6+Sb55eA2mRfQjaN8oOa5xiYyKA==", + "requires": { + "@aws-sdk/credential-provider-env": "3.523.0", + "@aws-sdk/credential-provider-http": "3.523.0", + "@aws-sdk/credential-provider-ini": "3.523.0", + "@aws-sdk/credential-provider-process": "3.523.0", + "@aws-sdk/credential-provider-sso": "3.523.0", + "@aws-sdk/credential-provider-web-identity": "3.523.0", + "@aws-sdk/types": "3.523.0", + "@smithy/credential-provider-imds": "^2.2.3", + "@smithy/property-provider": "^2.1.3", + "@smithy/shared-ini-file-loader": "^2.3.3", + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + } + }, + "@aws-sdk/credential-provider-process": { + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.523.0.tgz", + "integrity": "sha512-f0LP9KlFmMvPWdKeUKYlZ6FkQAECUeZMmISsv6NKtvPCI9e4O4cLTeR09telwDK8P0HrgcRuZfXM7E30m8re0Q==", + "requires": { + "@aws-sdk/types": "3.523.0", + "@smithy/property-provider": "^2.1.3", + "@smithy/shared-ini-file-loader": "^2.3.3", + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + } + }, + "@aws-sdk/credential-provider-sso": { + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.523.0.tgz", + "integrity": "sha512-/VfOJuI8ImV//W4gr+yieF/4shzWAzWYeaaNu7hv161C5YW7/OoCygwRVHSnF4KKeUGQZomZWwml5zHZ57f8xQ==", + "requires": { + "@aws-sdk/client-sso": "3.523.0", + "@aws-sdk/token-providers": "3.523.0", + "@aws-sdk/types": "3.523.0", + "@smithy/property-provider": "^2.1.3", + "@smithy/shared-ini-file-loader": "^2.3.3", + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + } + }, + "@aws-sdk/credential-provider-web-identity": { + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.523.0.tgz", + "integrity": "sha512-EyBwVoTNZrhLRIHly3JnLzy86deT2hHGoxSCrT3+cVcF1Pq3FPp6n9fUkHd6Yel+wFrjpXCRggLddPvajUoXtQ==", + "requires": { + "@aws-sdk/client-sts": "3.523.0", + "@aws-sdk/types": "3.523.0", + "@smithy/property-provider": "^2.1.3", + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + } + }, + "@aws-sdk/middleware-host-header": { + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.523.0.tgz", + "integrity": "sha512-4g3q7Ta9sdD9TMUuohBAkbx/e3I/juTqfKi7TPgP+8jxcYX72MOsgemAMHuP6CX27eyj4dpvjH+w4SIVDiDSmg==", + "requires": { + "@aws-sdk/types": "3.523.0", + "@smithy/protocol-http": "^3.2.1", + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + } + }, + "@aws-sdk/middleware-logger": { + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.523.0.tgz", + "integrity": "sha512-PeDNJNhfiaZx54LBaLTXzUaJ9LXFwDFFIksipjqjvxMafnoVcQwKbkoPUWLe5ytT4nnL1LogD3s55mERFUsnwg==", + "requires": { + "@aws-sdk/types": "3.523.0", + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + } + }, + "@aws-sdk/middleware-recursion-detection": { + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.523.0.tgz", + "integrity": "sha512-nZ3Vt7ehfSDYnrcg/aAfjjvpdE+61B3Zk68i6/hSUIegT3IH9H1vSW67NDKVp+50hcEfzWwM2HMPXxlzuyFyrw==", + "requires": { + "@aws-sdk/types": "3.523.0", + "@smithy/protocol-http": "^3.2.1", + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + } + }, + "@aws-sdk/middleware-user-agent": { + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.523.0.tgz", + "integrity": "sha512-5OoKkmAPNaxLgJuS65gByW1QknGvvXdqzrIMXLsm9LjbsphTOscyvT439qk3Jf08TL4Zlw2x+pZMG7dZYuMAhQ==", + "requires": { + "@aws-sdk/types": "3.523.0", + "@aws-sdk/util-endpoints": "3.523.0", + "@smithy/protocol-http": "^3.2.1", + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + } + }, + "@aws-sdk/region-config-resolver": { + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.523.0.tgz", + "integrity": "sha512-IypIAecBc8b4jM0uVBEj90NYaIsc0vuLdSFyH4LPO7is4rQUet4CkkD+S036NvDdcdxBsQ4hJZBmWrqiizMHhQ==", + "requires": { + "@aws-sdk/types": "3.523.0", + "@smithy/node-config-provider": "^2.2.3", + "@smithy/types": "^2.10.1", + "@smithy/util-config-provider": "^2.2.1", + "@smithy/util-middleware": "^2.1.3", + "tslib": "^2.5.0" + } + }, + "@aws-sdk/token-providers": { + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.523.0.tgz", + "integrity": "sha512-m3sPEnLuGV3JY9A8ytcz90SogVtjxEyIxUDFeswxY4C5wP/36yOq3ivenRu07dH+QIJnBhsQdjnHwJfrIetG6g==", + "requires": { + "@aws-sdk/client-sso-oidc": "3.523.0", + "@aws-sdk/types": "3.523.0", + "@smithy/property-provider": "^2.1.3", + "@smithy/shared-ini-file-loader": "^2.3.3", + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + } + }, + "@aws-sdk/types": { + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.523.0.tgz", + "integrity": "sha512-AqGIu4u+SxPiUuNBp2acCVcq80KDUFjxe6e3cMTvKWTzCbrVk1AXv0dAaJnCmdkWIha6zJDWxpIk/aL4EGhZ9A==", + "requires": { + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + } + }, + "@aws-sdk/util-endpoints": { + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.523.0.tgz", + "integrity": "sha512-f4qe4AdafjAZoVGoVt69Jb2rXCgo306OOobSJ/f4bhQ0zgAjGELKJATNRRe0J7P28+ffmSxeuYwM3r4gDkD/QA==", + "requires": { + "@aws-sdk/types": "3.523.0", + "@smithy/types": "^2.10.1", + "@smithy/util-endpoints": "^1.1.3", + "tslib": "^2.5.0" + } + }, + "@aws-sdk/util-user-agent-browser": { + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.523.0.tgz", + "integrity": "sha512-6ZRNdGHX6+HQFqTbIA5+i8RWzxFyxsZv8D3soRfpdyWIKkzhSz8IyRKXRciwKBJDaC7OX2jzGE90wxRQft27nA==", + "requires": { + "@aws-sdk/types": "3.523.0", + "@smithy/types": "^2.10.1", + "bowser": "^2.11.0", + "tslib": "^2.5.0" + } + }, + "@aws-sdk/util-user-agent-node": { + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.523.0.tgz", + "integrity": "sha512-tW7vliJ77EsE8J1bzFpDYCiUyrw2NTcem+J5ddiWD4HA/xNQUyX0CMOXMBZCBA31xLTIchyz0LkZHlDsmB9LUw==", + "requires": { + "@aws-sdk/types": "3.523.0", + "@smithy/node-config-provider": "^2.2.3", + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + } + }, + "@smithy/abort-controller": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-2.1.3.tgz", + "integrity": "sha512-c2aYH2Wu1RVE3rLlVgg2kQOBJGM0WbjReQi5DnPTm2Zb7F0gk7J2aeQeaX2u/lQZoHl6gv8Oac7mt9alU3+f4A==", + "requires": { + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + } + }, + "@smithy/config-resolver": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-2.1.4.tgz", + "integrity": "sha512-AW2WUZmBAzgO3V3ovKtsUbI3aBNMeQKFDumoqkNxaVDWF/xfnxAWqBKDr/NuG7c06N2Rm4xeZLPiJH/d+na0HA==", + "requires": { + "@smithy/node-config-provider": "^2.2.4", + "@smithy/types": "^2.10.1", + "@smithy/util-config-provider": "^2.2.1", + "@smithy/util-middleware": "^2.1.3", + "tslib": "^2.5.0" + } + }, + "@smithy/core": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-1.3.5.tgz", + "integrity": "sha512-Rrc+e2Jj6Gu7Xbn0jvrzZlSiP2CZocIOfZ9aNUA82+1sa6GBnxqL9+iZ9EKHeD9aqD1nU8EK4+oN2EiFpSv7Yw==", + "requires": { + "@smithy/middleware-endpoint": "^2.4.4", + "@smithy/middleware-retry": "^2.1.4", + "@smithy/middleware-serde": "^2.1.3", + "@smithy/protocol-http": "^3.2.1", + "@smithy/smithy-client": "^2.4.2", + "@smithy/types": "^2.10.1", + "@smithy/util-middleware": "^2.1.3", + "tslib": "^2.5.0" + } + }, + "@smithy/credential-provider-imds": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-2.2.4.tgz", + "integrity": "sha512-DdatjmBZQnhGe1FhI8gO98f7NmvQFSDiZTwC3WMvLTCKQUY+Y1SVkhJqIuLu50Eb7pTheoXQmK+hKYUgpUWsNA==", + "requires": { + "@smithy/node-config-provider": "^2.2.4", + "@smithy/property-provider": "^2.1.3", + "@smithy/types": "^2.10.1", + "@smithy/url-parser": "^2.1.3", + "tslib": "^2.5.0" + } + }, + "@smithy/eventstream-codec": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-2.1.3.tgz", + "integrity": "sha512-rGlCVuwSDv6qfKH4/lRxFjcZQnIE0LZ3D4lkMHg7ZSltK9rA74r0VuGSvWVQ4N/d70VZPaniFhp4Z14QYZsa+A==", + "requires": { + "@aws-crypto/crc32": "3.0.0", + "@smithy/types": "^2.10.1", + "@smithy/util-hex-encoding": "^2.1.1", + "tslib": "^2.5.0" + } + }, + "@smithy/fetch-http-handler": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-2.4.3.tgz", + "integrity": "sha512-Fn/KYJFo6L5I4YPG8WQb2hOmExgRmNpVH5IK2zU3JKrY5FKW7y9ar5e0BexiIC9DhSKqKX+HeWq/Y18fq7Dkpw==", + "requires": { + "@smithy/protocol-http": "^3.2.1", + "@smithy/querystring-builder": "^2.1.3", + "@smithy/types": "^2.10.1", + "@smithy/util-base64": "^2.1.1", + "tslib": "^2.5.0" + } + }, + "@smithy/hash-node": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-2.1.3.tgz", + "integrity": "sha512-FsAPCUj7VNJIdHbSxMd5uiZiF20G2zdSDgrgrDrHqIs/VMxK85Vqk5kMVNNDMCZmMezp6UKnac0B4nAyx7HJ9g==", + "requires": { + "@smithy/types": "^2.10.1", + "@smithy/util-buffer-from": "^2.1.1", + "@smithy/util-utf8": "^2.1.1", + "tslib": "^2.5.0" + } + }, + "@smithy/invalid-dependency": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-2.1.3.tgz", + "integrity": "sha512-wkra7d/G4CbngV4xsjYyAYOvdAhahQje/WymuQdVEnXFExJopEu7fbL5AEAlBPgWHXwu94VnCSG00gVzRfExyg==", + "requires": { + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + } + }, + "@smithy/middleware-content-length": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-2.1.3.tgz", + "integrity": "sha512-aJduhkC+dcXxdnv5ZpM3uMmtGmVFKx412R1gbeykS5HXDmRU6oSsyy2SoHENCkfOGKAQOjVE2WVqDJibC0d21g==", + "requires": { + "@smithy/protocol-http": "^3.2.1", + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + } + }, + "@smithy/middleware-endpoint": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-2.4.4.tgz", + "integrity": "sha512-4yjHyHK2Jul4JUDBo2sTsWY9UshYUnXeb/TAK/MTaPEb8XQvDmpwSFnfIRDU45RY1a6iC9LCnmJNg/yHyfxqkw==", + "requires": { + "@smithy/middleware-serde": "^2.1.3", + "@smithy/node-config-provider": "^2.2.4", + "@smithy/shared-ini-file-loader": "^2.3.4", + "@smithy/types": "^2.10.1", + "@smithy/url-parser": "^2.1.3", + "@smithy/util-middleware": "^2.1.3", + "tslib": "^2.5.0" + } + }, + "@smithy/middleware-retry": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-2.1.4.tgz", + "integrity": "sha512-Cyolv9YckZTPli1EkkaS39UklonxMd08VskiuMhURDjC0HHa/AD6aK/YoD21CHv9s0QLg0WMLvk9YeLTKkXaFQ==", + "requires": { + "@smithy/node-config-provider": "^2.2.4", + "@smithy/protocol-http": "^3.2.1", + "@smithy/service-error-classification": "^2.1.3", + "@smithy/smithy-client": "^2.4.2", + "@smithy/types": "^2.10.1", + "@smithy/util-middleware": "^2.1.3", + "@smithy/util-retry": "^2.1.3", + "tslib": "^2.5.0", + "uuid": "^8.3.2" + } + }, + "@smithy/middleware-serde": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-2.1.3.tgz", + "integrity": "sha512-s76LId+TwASrHhUa9QS4k/zeXDUAuNuddKklQzRgumbzge5BftVXHXIqL4wQxKGLocPwfgAOXWx+HdWhQk9hTg==", + "requires": { + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + } + }, + "@smithy/middleware-stack": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-2.1.3.tgz", + "integrity": "sha512-opMFufVQgvBSld/b7mD7OOEBxF6STyraVr1xel1j0abVILM8ALJvRoFbqSWHGmaDlRGIiV9Q5cGbWi0sdiEaLQ==", + "requires": { + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + } + }, + "@smithy/node-config-provider": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-2.2.4.tgz", + "integrity": "sha512-nqazHCp8r4KHSFhRQ+T0VEkeqvA0U+RhehBSr1gunUuNW3X7j0uDrWBxB2gE9eutzy6kE3Y7L+Dov/UXT871vg==", + "requires": { + "@smithy/property-provider": "^2.1.3", + "@smithy/shared-ini-file-loader": "^2.3.4", + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + } + }, + "@smithy/node-http-handler": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-2.4.1.tgz", + "integrity": "sha512-HCkb94soYhJMxPCa61wGKgmeKpJ3Gftx1XD6bcWEB2wMV1L9/SkQu/6/ysKBnbOzWRE01FGzwrTxucHypZ8rdg==", + "requires": { + "@smithy/abort-controller": "^2.1.3", + "@smithy/protocol-http": "^3.2.1", + "@smithy/querystring-builder": "^2.1.3", + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + } + }, + "@smithy/property-provider": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-2.1.3.tgz", + "integrity": "sha512-bMz3se+ySKWNrgm7eIiQMa2HO/0fl2D0HvLAdg9pTMcpgp4SqOAh6bz7Ik6y7uQqSrk4rLjIKgbQ6yzYgGehCQ==", + "requires": { + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + } + }, + "@smithy/protocol-http": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-3.2.1.tgz", + "integrity": "sha512-KLrQkEw4yJCeAmAH7hctE8g9KwA7+H2nSJwxgwIxchbp/L0B5exTdOQi9D5HinPLlothoervGmhpYKelZ6AxIA==", + "requires": { + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + } + }, + "@smithy/querystring-builder": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-2.1.3.tgz", + "integrity": "sha512-kFD3PnNqKELe6m9GRHQw/ftFFSZpnSeQD4qvgDB6BQN6hREHELSosVFUMPN4M3MDKN2jAwk35vXHLoDrNfKu0A==", + "requires": { + "@smithy/types": "^2.10.1", + "@smithy/util-uri-escape": "^2.1.1", + "tslib": "^2.5.0" + } + }, + "@smithy/querystring-parser": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-2.1.3.tgz", + "integrity": "sha512-3+CWJoAqcBMR+yvz6D+Fc5VdoGFtfenW6wqSWATWajrRMGVwJGPT3Vy2eb2bnMktJc4HU4bpjeovFa566P3knQ==", + "requires": { + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + } + }, + "@smithy/service-error-classification": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-2.1.3.tgz", + "integrity": "sha512-iUrpSsem97bbXHHT/v3s7vaq8IIeMo6P6cXdeYHrx0wOJpMeBGQF7CB0mbJSiTm3//iq3L55JiEm8rA7CTVI8A==", + "requires": { + "@smithy/types": "^2.10.1" + } + }, + "@smithy/shared-ini-file-loader": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-2.3.4.tgz", + "integrity": "sha512-CiZmPg9GeDKbKmJGEFvJBsJcFnh0AQRzOtQAzj1XEa8N/0/uSN/v1LYzgO7ry8hhO8+9KB7+DhSW0weqBra4Aw==", + "requires": { + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + } + }, + "@smithy/signature-v4": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-2.1.3.tgz", + "integrity": "sha512-Jq4iPPdCmJojZTsPePn4r1ULShh6ONkokLuxp1Lnk4Sq7r7rJp4HlA1LbPBq4bD64TIzQezIpr1X+eh5NYkNxw==", + "requires": { + "@smithy/eventstream-codec": "^2.1.3", + "@smithy/is-array-buffer": "^2.1.1", + "@smithy/types": "^2.10.1", + "@smithy/util-hex-encoding": "^2.1.1", + "@smithy/util-middleware": "^2.1.3", + "@smithy/util-uri-escape": "^2.1.1", + "@smithy/util-utf8": "^2.1.1", + "tslib": "^2.5.0" + } + }, + "@smithy/smithy-client": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-2.4.2.tgz", + "integrity": "sha512-ntAFYN51zu3N3mCd95YFcFi/8rmvm//uX+HnK24CRbI6k5Rjackn0JhgKz5zOx/tbNvOpgQIwhSX+1EvEsBLbA==", + "requires": { + "@smithy/middleware-endpoint": "^2.4.4", + "@smithy/middleware-stack": "^2.1.3", + "@smithy/protocol-http": "^3.2.1", + "@smithy/types": "^2.10.1", + "@smithy/util-stream": "^2.1.3", + "tslib": "^2.5.0" + } + }, + "@smithy/types": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.10.1.tgz", + "integrity": "sha512-hjQO+4ru4cQ58FluQvKKiyMsFg0A6iRpGm2kqdH8fniyNd2WyanoOsYJfMX/IFLuLxEoW6gnRkNZy1y6fUUhtA==", + "requires": { + "tslib": "^2.5.0" + } + }, + "@smithy/url-parser": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-2.1.3.tgz", + "integrity": "sha512-X1NRA4WzK/ihgyzTpeGvI9Wn45y8HmqF4AZ/FazwAv8V203Ex+4lXqcYI70naX9ETqbqKVzFk88W6WJJzCggTQ==", + "requires": { + "@smithy/querystring-parser": "^2.1.3", + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + } + }, + "@smithy/util-defaults-mode-browser": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-2.1.4.tgz", + "integrity": "sha512-J6XAVY+/g7jf03QMnvqPyU+8jqGrrtXoKWFVOS+n1sz0Lg8HjHJ1ANqaDN+KTTKZRZlvG8nU5ZrJOUL6VdwgcQ==", + "requires": { + "@smithy/property-provider": "^2.1.3", + "@smithy/smithy-client": "^2.4.2", + "@smithy/types": "^2.10.1", + "bowser": "^2.11.0", + "tslib": "^2.5.0" + } + }, + "@smithy/util-defaults-mode-node": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-2.2.3.tgz", + "integrity": "sha512-ttUISrv1uVOjTlDa3nznX33f0pthoUlP+4grhTvOzcLhzArx8qHB94/untGACOG3nlf8vU20nI2iWImfzoLkYA==", + "requires": { + "@smithy/config-resolver": "^2.1.4", + "@smithy/credential-provider-imds": "^2.2.4", + "@smithy/node-config-provider": "^2.2.4", + "@smithy/property-provider": "^2.1.3", + "@smithy/smithy-client": "^2.4.2", + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + } + }, + "@smithy/util-endpoints": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-1.1.4.tgz", + "integrity": "sha512-/qAeHmK5l4yQ4/bCIJ9p49wDe9rwWtOzhPHblu386fwPNT3pxmodgcs9jDCV52yK9b4rB8o9Sj31P/7Vzka1cg==", + "requires": { + "@smithy/node-config-provider": "^2.2.4", + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + } + }, + "@smithy/util-middleware": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-2.1.3.tgz", + "integrity": "sha512-/+2fm7AZ2ozl5h8wM++ZP0ovE9/tiUUAHIbCfGfb3Zd3+Dyk17WODPKXBeJ/TnK5U+x743QmA0xHzlSm8I/qhw==", + "requires": { + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + } + }, + "@smithy/util-retry": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-2.1.3.tgz", + "integrity": "sha512-Kbvd+GEMuozbNUU3B89mb99tbufwREcyx2BOX0X2+qHjq6Gvsah8xSDDgxISDwcOHoDqUWO425F0Uc/QIRhYkg==", + "requires": { + "@smithy/service-error-classification": "^2.1.3", + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + } + }, + "@smithy/util-stream": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-2.1.3.tgz", + "integrity": "sha512-HvpEQbP8raTy9n86ZfXiAkf3ezp1c3qeeO//zGqwZdrfaoOpGKQgF2Sv1IqZp7wjhna7pvczWaGUHjcOPuQwKw==", + "requires": { + "@smithy/fetch-http-handler": "^2.4.3", + "@smithy/node-http-handler": "^2.4.1", + "@smithy/types": "^2.10.1", + "@smithy/util-base64": "^2.1.1", + "@smithy/util-buffer-from": "^2.1.1", + "@smithy/util-hex-encoding": "^2.1.1", + "@smithy/util-utf8": "^2.1.1", + "tslib": "^2.5.0" + } + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + } + } + }, + "@aws-sdk/client-sso": { + "version": "3.540.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.540.0.tgz", + "integrity": "sha512-rrQZMuw4sxIo3eyAUUzPQRA336mPRnrAeSlSdVHBKZD8Fjvoy0lYry2vNhkPLpFZLso1J66KRyuIv4LzRR3v1Q==", + "dev": true, + "optional": true, + "requires": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/core": "3.535.0", + "@aws-sdk/middleware-host-header": "3.535.0", + "@aws-sdk/middleware-logger": "3.535.0", + "@aws-sdk/middleware-recursion-detection": "3.535.0", + "@aws-sdk/middleware-user-agent": "3.540.0", + "@aws-sdk/region-config-resolver": "3.535.0", + "@aws-sdk/types": "3.535.0", + "@aws-sdk/util-endpoints": "3.540.0", + "@aws-sdk/util-user-agent-browser": "3.535.0", + "@aws-sdk/util-user-agent-node": "3.535.0", + "@smithy/config-resolver": "^2.2.0", + "@smithy/core": "^1.4.0", + "@smithy/fetch-http-handler": "^2.5.0", + "@smithy/hash-node": "^2.2.0", + "@smithy/invalid-dependency": "^2.2.0", + "@smithy/middleware-content-length": "^2.2.0", + "@smithy/middleware-endpoint": "^2.5.0", + "@smithy/middleware-retry": "^2.2.0", + "@smithy/middleware-serde": "^2.3.0", + "@smithy/middleware-stack": "^2.2.0", + "@smithy/node-config-provider": "^2.3.0", + "@smithy/node-http-handler": "^2.5.0", + "@smithy/protocol-http": "^3.3.0", + "@smithy/smithy-client": "^2.5.0", + "@smithy/types": "^2.12.0", + "@smithy/url-parser": "^2.2.0", + "@smithy/util-base64": "^2.3.0", + "@smithy/util-body-length-browser": "^2.2.0", + "@smithy/util-body-length-node": "^2.3.0", + "@smithy/util-defaults-mode-browser": "^2.2.0", + "@smithy/util-defaults-mode-node": "^2.3.0", + "@smithy/util-endpoints": "^1.2.0", + "@smithy/util-middleware": "^2.2.0", + "@smithy/util-retry": "^2.2.0", + "@smithy/util-utf8": "^2.3.0", + "tslib": "^2.6.2" + }, + "dependencies": { + "@aws-sdk/types": { + "version": "3.535.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.535.0.tgz", + "integrity": "sha512-aY4MYfduNj+sRR37U7XxYR8wemfbKP6lx00ze2M2uubn7mZotuVrWYAafbMSXrdEMSToE5JDhr28vArSOoLcSg==", + "dev": true, + "optional": true, + "requires": { + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + } + }, + "@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + }, + "@smithy/types": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.12.0.tgz", + "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + }, + "@smithy/util-base64": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-2.3.0.tgz", + "integrity": "sha512-s3+eVwNeJuXUwuMbusncZNViuhv2LjVJ1nMwTqSA0XAC7gjKhqqxRdJPhR8+YrkoZ9IiIbFk/yK6ACe/xlF+hw==", + "dev": true, + "optional": true, + "requires": { + "@smithy/util-buffer-from": "^2.2.0", + "@smithy/util-utf8": "^2.3.0", + "tslib": "^2.6.2" + } + }, + "@smithy/util-body-length-browser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-2.2.0.tgz", + "integrity": "sha512-dtpw9uQP7W+n3vOtx0CfBD5EWd7EPdIdsQnWTDoFf77e3VUf05uA7R7TGipIo8e4WL2kuPdnsr3hMQn9ziYj5w==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + }, + "@smithy/util-body-length-node": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-2.3.0.tgz", + "integrity": "sha512-ITWT1Wqjubf2CJthb0BuT9+bpzBfXeMokH/AAa5EJQgbv9aPMVfnM76iFIZVFf50hYXGbtiV71BHAthNWd6+dw==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + }, + "@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "dev": true, + "optional": true, + "requires": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + } + }, + "@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "dev": true, + "optional": true, + "requires": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + } + } + } + }, + "@aws-sdk/client-sso-oidc": { + "version": "3.540.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.540.0.tgz", + "integrity": "sha512-LZYK0lBRQK8D8M3Sqc96XiXkAV2v70zhTtF6weyzEpgwxZMfSuFJjs0jFyhaeZBZbZv7BBghIdhJ5TPavNxGMQ==", + "dev": true, + "optional": true, + "requires": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/client-sts": "3.540.0", + "@aws-sdk/core": "3.535.0", + "@aws-sdk/middleware-host-header": "3.535.0", + "@aws-sdk/middleware-logger": "3.535.0", + "@aws-sdk/middleware-recursion-detection": "3.535.0", + "@aws-sdk/middleware-user-agent": "3.540.0", + "@aws-sdk/region-config-resolver": "3.535.0", + "@aws-sdk/types": "3.535.0", + "@aws-sdk/util-endpoints": "3.540.0", + "@aws-sdk/util-user-agent-browser": "3.535.0", + "@aws-sdk/util-user-agent-node": "3.535.0", + "@smithy/config-resolver": "^2.2.0", + "@smithy/core": "^1.4.0", + "@smithy/fetch-http-handler": "^2.5.0", + "@smithy/hash-node": "^2.2.0", + "@smithy/invalid-dependency": "^2.2.0", + "@smithy/middleware-content-length": "^2.2.0", + "@smithy/middleware-endpoint": "^2.5.0", + "@smithy/middleware-retry": "^2.2.0", + "@smithy/middleware-serde": "^2.3.0", + "@smithy/middleware-stack": "^2.2.0", + "@smithy/node-config-provider": "^2.3.0", + "@smithy/node-http-handler": "^2.5.0", + "@smithy/protocol-http": "^3.3.0", + "@smithy/smithy-client": "^2.5.0", + "@smithy/types": "^2.12.0", + "@smithy/url-parser": "^2.2.0", + "@smithy/util-base64": "^2.3.0", + "@smithy/util-body-length-browser": "^2.2.0", + "@smithy/util-body-length-node": "^2.3.0", + "@smithy/util-defaults-mode-browser": "^2.2.0", + "@smithy/util-defaults-mode-node": "^2.3.0", + "@smithy/util-endpoints": "^1.2.0", + "@smithy/util-middleware": "^2.2.0", + "@smithy/util-retry": "^2.2.0", + "@smithy/util-utf8": "^2.3.0", + "tslib": "^2.6.2" + }, + "dependencies": { + "@aws-sdk/types": { + "version": "3.535.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.535.0.tgz", + "integrity": "sha512-aY4MYfduNj+sRR37U7XxYR8wemfbKP6lx00ze2M2uubn7mZotuVrWYAafbMSXrdEMSToE5JDhr28vArSOoLcSg==", + "dev": true, + "optional": true, + "requires": { + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + } + }, + "@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + }, + "@smithy/types": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.12.0.tgz", + "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + }, + "@smithy/util-base64": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-2.3.0.tgz", + "integrity": "sha512-s3+eVwNeJuXUwuMbusncZNViuhv2LjVJ1nMwTqSA0XAC7gjKhqqxRdJPhR8+YrkoZ9IiIbFk/yK6ACe/xlF+hw==", + "dev": true, + "optional": true, + "requires": { + "@smithy/util-buffer-from": "^2.2.0", + "@smithy/util-utf8": "^2.3.0", + "tslib": "^2.6.2" + } + }, + "@smithy/util-body-length-browser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-2.2.0.tgz", + "integrity": "sha512-dtpw9uQP7W+n3vOtx0CfBD5EWd7EPdIdsQnWTDoFf77e3VUf05uA7R7TGipIo8e4WL2kuPdnsr3hMQn9ziYj5w==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + }, + "@smithy/util-body-length-node": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-2.3.0.tgz", + "integrity": "sha512-ITWT1Wqjubf2CJthb0BuT9+bpzBfXeMokH/AAa5EJQgbv9aPMVfnM76iFIZVFf50hYXGbtiV71BHAthNWd6+dw==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + }, + "@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "dev": true, + "optional": true, + "requires": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + } + }, + "@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "dev": true, + "optional": true, + "requires": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + } + } + } + }, + "@aws-sdk/client-sts": { + "version": "3.540.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.540.0.tgz", + "integrity": "sha512-ITHUQxvpqfQX6obfpIi3KYGzZYfe/I5Ixjfxoi5lB7ISCtmxqObKB1fzD93wonkMJytJ7LUO8panZl/ojiJ1uw==", + "dev": true, + "optional": true, + "requires": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/core": "3.535.0", + "@aws-sdk/middleware-host-header": "3.535.0", + "@aws-sdk/middleware-logger": "3.535.0", + "@aws-sdk/middleware-recursion-detection": "3.535.0", + "@aws-sdk/middleware-user-agent": "3.540.0", + "@aws-sdk/region-config-resolver": "3.535.0", + "@aws-sdk/types": "3.535.0", + "@aws-sdk/util-endpoints": "3.540.0", + "@aws-sdk/util-user-agent-browser": "3.535.0", + "@aws-sdk/util-user-agent-node": "3.535.0", + "@smithy/config-resolver": "^2.2.0", + "@smithy/core": "^1.4.0", + "@smithy/fetch-http-handler": "^2.5.0", + "@smithy/hash-node": "^2.2.0", + "@smithy/invalid-dependency": "^2.2.0", + "@smithy/middleware-content-length": "^2.2.0", + "@smithy/middleware-endpoint": "^2.5.0", + "@smithy/middleware-retry": "^2.2.0", + "@smithy/middleware-serde": "^2.3.0", + "@smithy/middleware-stack": "^2.2.0", + "@smithy/node-config-provider": "^2.3.0", + "@smithy/node-http-handler": "^2.5.0", + "@smithy/protocol-http": "^3.3.0", + "@smithy/smithy-client": "^2.5.0", + "@smithy/types": "^2.12.0", + "@smithy/url-parser": "^2.2.0", + "@smithy/util-base64": "^2.3.0", + "@smithy/util-body-length-browser": "^2.2.0", + "@smithy/util-body-length-node": "^2.3.0", + "@smithy/util-defaults-mode-browser": "^2.2.0", + "@smithy/util-defaults-mode-node": "^2.3.0", + "@smithy/util-endpoints": "^1.2.0", + "@smithy/util-middleware": "^2.2.0", + "@smithy/util-retry": "^2.2.0", + "@smithy/util-utf8": "^2.3.0", + "tslib": "^2.6.2" + }, + "dependencies": { + "@aws-sdk/types": { + "version": "3.535.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.535.0.tgz", + "integrity": "sha512-aY4MYfduNj+sRR37U7XxYR8wemfbKP6lx00ze2M2uubn7mZotuVrWYAafbMSXrdEMSToE5JDhr28vArSOoLcSg==", + "dev": true, + "optional": true, + "requires": { + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + } + }, + "@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + }, + "@smithy/types": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.12.0.tgz", + "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + }, + "@smithy/util-base64": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-2.3.0.tgz", + "integrity": "sha512-s3+eVwNeJuXUwuMbusncZNViuhv2LjVJ1nMwTqSA0XAC7gjKhqqxRdJPhR8+YrkoZ9IiIbFk/yK6ACe/xlF+hw==", + "dev": true, + "optional": true, + "requires": { + "@smithy/util-buffer-from": "^2.2.0", + "@smithy/util-utf8": "^2.3.0", + "tslib": "^2.6.2" + } + }, + "@smithy/util-body-length-browser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-2.2.0.tgz", + "integrity": "sha512-dtpw9uQP7W+n3vOtx0CfBD5EWd7EPdIdsQnWTDoFf77e3VUf05uA7R7TGipIo8e4WL2kuPdnsr3hMQn9ziYj5w==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + }, + "@smithy/util-body-length-node": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-2.3.0.tgz", + "integrity": "sha512-ITWT1Wqjubf2CJthb0BuT9+bpzBfXeMokH/AAa5EJQgbv9aPMVfnM76iFIZVFf50hYXGbtiV71BHAthNWd6+dw==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + }, + "@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "dev": true, + "optional": true, + "requires": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + } + }, + "@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "dev": true, + "optional": true, + "requires": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + } + } + } + }, + "@aws-sdk/core": { + "version": "3.535.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.535.0.tgz", + "integrity": "sha512-+Yusa9HziuaEDta1UaLEtMAtmgvxdxhPn7jgfRY6PplqAqgsfa5FR83sxy5qr2q7xjQTwHtV4MjQVuOjG9JsLw==", + "dev": true, + "optional": true, + "requires": { + "@smithy/core": "^1.4.0", + "@smithy/protocol-http": "^3.3.0", + "@smithy/signature-v4": "^2.2.0", + "@smithy/smithy-client": "^2.5.0", + "@smithy/types": "^2.12.0", + "fast-xml-parser": "4.2.5", + "tslib": "^2.6.2" + }, + "dependencies": { + "@smithy/types": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.12.0.tgz", + "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + } + } + }, + "@aws-sdk/credential-provider-cognito-identity": { + "version": "3.540.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-cognito-identity/-/credential-provider-cognito-identity-3.540.0.tgz", + "integrity": "sha512-XOTAIuVgticX+43GMpRbi5OHmJAhHfoHYsVGu0eRLhri1yFqUHXJgHUd51QQtlA8cFQN7JnFFM6sF5EDCPF49g==", + "dev": true, + "optional": true, + "requires": { + "@aws-sdk/client-cognito-identity": "3.540.0", + "@aws-sdk/types": "3.535.0", + "@smithy/property-provider": "^2.2.0", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + }, + "dependencies": { + "@aws-sdk/types": { + "version": "3.535.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.535.0.tgz", + "integrity": "sha512-aY4MYfduNj+sRR37U7XxYR8wemfbKP6lx00ze2M2uubn7mZotuVrWYAafbMSXrdEMSToE5JDhr28vArSOoLcSg==", + "dev": true, + "optional": true, + "requires": { + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + } + }, + "@smithy/types": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.12.0.tgz", + "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + } + } + }, + "@aws-sdk/credential-provider-env": { + "version": "3.535.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.535.0.tgz", + "integrity": "sha512-XppwO8c0GCGSAvdzyJOhbtktSEaShg14VJKg8mpMa1XcgqzmcqqHQjtDWbx5rZheY1VdpXZhpEzJkB6LpQejpA==", + "dev": true, + "optional": true, + "requires": { + "@aws-sdk/types": "3.535.0", + "@smithy/property-provider": "^2.2.0", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + }, + "dependencies": { + "@aws-sdk/types": { + "version": "3.535.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.535.0.tgz", + "integrity": "sha512-aY4MYfduNj+sRR37U7XxYR8wemfbKP6lx00ze2M2uubn7mZotuVrWYAafbMSXrdEMSToE5JDhr28vArSOoLcSg==", + "dev": true, + "optional": true, + "requires": { + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + } + }, + "@smithy/types": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.12.0.tgz", + "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + } + } + }, + "@aws-sdk/credential-provider-http": { + "version": "3.535.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.535.0.tgz", + "integrity": "sha512-kdj1wCmOMZ29jSlUskRqN04S6fJ4dvt0Nq9Z32SA6wO7UG8ht6Ot9h/au/eTWJM3E1somZ7D771oK7dQt9b8yw==", + "dev": true, + "optional": true, + "requires": { + "@aws-sdk/types": "3.535.0", + "@smithy/fetch-http-handler": "^2.5.0", + "@smithy/node-http-handler": "^2.5.0", + "@smithy/property-provider": "^2.2.0", + "@smithy/protocol-http": "^3.3.0", + "@smithy/smithy-client": "^2.5.0", + "@smithy/types": "^2.12.0", + "@smithy/util-stream": "^2.2.0", + "tslib": "^2.6.2" + }, + "dependencies": { + "@aws-sdk/types": { + "version": "3.535.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.535.0.tgz", + "integrity": "sha512-aY4MYfduNj+sRR37U7XxYR8wemfbKP6lx00ze2M2uubn7mZotuVrWYAafbMSXrdEMSToE5JDhr28vArSOoLcSg==", + "dev": true, + "optional": true, + "requires": { + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + } + }, + "@smithy/types": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.12.0.tgz", + "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + } + } + }, + "@aws-sdk/credential-provider-ini": { + "version": "3.540.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.540.0.tgz", + "integrity": "sha512-igN/RbsnulIBwqXbwsWmR3srqmtbPF1dm+JteGvUY31FW65fTVvWvSr945Y/cf1UbhPmIQXntlsqESqpkhTHwg==", + "dev": true, + "optional": true, + "requires": { + "@aws-sdk/client-sts": "3.540.0", + "@aws-sdk/credential-provider-env": "3.535.0", + "@aws-sdk/credential-provider-process": "3.535.0", + "@aws-sdk/credential-provider-sso": "3.540.0", + "@aws-sdk/credential-provider-web-identity": "3.540.0", + "@aws-sdk/types": "3.535.0", + "@smithy/credential-provider-imds": "^2.3.0", + "@smithy/property-provider": "^2.2.0", + "@smithy/shared-ini-file-loader": "^2.4.0", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + }, + "dependencies": { + "@aws-sdk/types": { + "version": "3.535.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.535.0.tgz", + "integrity": "sha512-aY4MYfduNj+sRR37U7XxYR8wemfbKP6lx00ze2M2uubn7mZotuVrWYAafbMSXrdEMSToE5JDhr28vArSOoLcSg==", + "dev": true, + "optional": true, + "requires": { + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + } + }, + "@smithy/types": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.12.0.tgz", + "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + } + } + }, + "@aws-sdk/credential-provider-node": { + "version": "3.540.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.540.0.tgz", + "integrity": "sha512-HKQZJbLHlrHX9A0B1poiYNXIIQfy8whTjuosTCYKPDBhhUyVAQfxy/KG726j0v43IhaNPLgTGZCJve4hAsazSw==", + "dev": true, + "optional": true, + "requires": { + "@aws-sdk/credential-provider-env": "3.535.0", + "@aws-sdk/credential-provider-http": "3.535.0", + "@aws-sdk/credential-provider-ini": "3.540.0", + "@aws-sdk/credential-provider-process": "3.535.0", + "@aws-sdk/credential-provider-sso": "3.540.0", + "@aws-sdk/credential-provider-web-identity": "3.540.0", + "@aws-sdk/types": "3.535.0", + "@smithy/credential-provider-imds": "^2.3.0", + "@smithy/property-provider": "^2.2.0", + "@smithy/shared-ini-file-loader": "^2.4.0", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + }, + "dependencies": { + "@aws-sdk/types": { + "version": "3.535.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.535.0.tgz", + "integrity": "sha512-aY4MYfduNj+sRR37U7XxYR8wemfbKP6lx00ze2M2uubn7mZotuVrWYAafbMSXrdEMSToE5JDhr28vArSOoLcSg==", + "dev": true, + "optional": true, + "requires": { + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + } + }, + "@smithy/types": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.12.0.tgz", + "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + } + } + }, + "@aws-sdk/credential-provider-process": { + "version": "3.535.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.535.0.tgz", + "integrity": "sha512-9O1OaprGCnlb/kYl8RwmH7Mlg8JREZctB8r9sa1KhSsWFq/SWO0AuJTyowxD7zL5PkeS4eTvzFFHWCa3OO5epA==", + "dev": true, + "optional": true, + "requires": { + "@aws-sdk/types": "3.535.0", + "@smithy/property-provider": "^2.2.0", + "@smithy/shared-ini-file-loader": "^2.4.0", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + }, + "dependencies": { + "@aws-sdk/types": { + "version": "3.535.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.535.0.tgz", + "integrity": "sha512-aY4MYfduNj+sRR37U7XxYR8wemfbKP6lx00ze2M2uubn7mZotuVrWYAafbMSXrdEMSToE5JDhr28vArSOoLcSg==", + "dev": true, + "optional": true, + "requires": { + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + } + }, + "@smithy/types": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.12.0.tgz", + "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + } + } + }, + "@aws-sdk/credential-provider-sso": { + "version": "3.540.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.540.0.tgz", + "integrity": "sha512-tKkFqK227LF5ajc5EL6asXS32p3nkofpP8G7NRpU7zOEOQCg01KUc4JRX+ItI0T007CiN1J19yNoFqHLT/SqHg==", + "dev": true, + "optional": true, + "requires": { + "@aws-sdk/client-sso": "3.540.0", + "@aws-sdk/token-providers": "3.540.0", + "@aws-sdk/types": "3.535.0", + "@smithy/property-provider": "^2.2.0", + "@smithy/shared-ini-file-loader": "^2.4.0", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + }, + "dependencies": { + "@aws-sdk/types": { + "version": "3.535.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.535.0.tgz", + "integrity": "sha512-aY4MYfduNj+sRR37U7XxYR8wemfbKP6lx00ze2M2uubn7mZotuVrWYAafbMSXrdEMSToE5JDhr28vArSOoLcSg==", + "dev": true, + "optional": true, + "requires": { + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + } + }, + "@smithy/types": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.12.0.tgz", + "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + } + } + }, + "@aws-sdk/credential-provider-web-identity": { + "version": "3.540.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.540.0.tgz", + "integrity": "sha512-OpDm9w3A168B44hSjpnvECP4rvnFzD86rN4VYdGADuCvEa5uEcdA/JuT5WclFPDqdWEmFBqS1pxBIJBf0g2Q9Q==", + "dev": true, + "optional": true, + "requires": { + "@aws-sdk/client-sts": "3.540.0", + "@aws-sdk/types": "3.535.0", + "@smithy/property-provider": "^2.2.0", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + }, + "dependencies": { + "@aws-sdk/types": { + "version": "3.535.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.535.0.tgz", + "integrity": "sha512-aY4MYfduNj+sRR37U7XxYR8wemfbKP6lx00ze2M2uubn7mZotuVrWYAafbMSXrdEMSToE5JDhr28vArSOoLcSg==", + "dev": true, + "optional": true, + "requires": { + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + } + }, + "@smithy/types": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.12.0.tgz", + "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + } + } + }, + "@aws-sdk/credential-providers": { + "version": "3.540.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-providers/-/credential-providers-3.540.0.tgz", + "integrity": "sha512-tAmvqdZngCrER5/AAwTmDSjO05LGIshKL+lwcJr2OUV5jtQVzfbFrorf+b5dnI+3i8+zGcEAV9omra4XGrO9Kg==", + "dev": true, + "optional": true, + "requires": { + "@aws-sdk/client-cognito-identity": "3.540.0", + "@aws-sdk/client-sso": "3.540.0", + "@aws-sdk/client-sts": "3.540.0", + "@aws-sdk/credential-provider-cognito-identity": "3.540.0", + "@aws-sdk/credential-provider-env": "3.535.0", + "@aws-sdk/credential-provider-http": "3.535.0", + "@aws-sdk/credential-provider-ini": "3.540.0", + "@aws-sdk/credential-provider-node": "3.540.0", + "@aws-sdk/credential-provider-process": "3.535.0", + "@aws-sdk/credential-provider-sso": "3.540.0", + "@aws-sdk/credential-provider-web-identity": "3.540.0", + "@aws-sdk/types": "3.535.0", + "@smithy/credential-provider-imds": "^2.3.0", + "@smithy/property-provider": "^2.2.0", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + }, + "dependencies": { + "@aws-sdk/types": { + "version": "3.535.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.535.0.tgz", + "integrity": "sha512-aY4MYfduNj+sRR37U7XxYR8wemfbKP6lx00ze2M2uubn7mZotuVrWYAafbMSXrdEMSToE5JDhr28vArSOoLcSg==", + "dev": true, + "optional": true, + "requires": { + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + } + }, + "@smithy/types": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.12.0.tgz", + "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + } + } + }, + "@aws-sdk/middleware-bucket-endpoint": { + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.523.0.tgz", + "integrity": "sha512-mrZbixWjk0d9NqxC4xBnKtfwErum0we4Uk2O4fgvDVI+XxAimUlZ9c4o/QJ2+TzeQ/8QclT2k4WidsQdWtPNvg==", + "requires": { + "@aws-sdk/types": "3.523.0", + "@aws-sdk/util-arn-parser": "3.495.0", + "@smithy/node-config-provider": "^2.2.3", + "@smithy/protocol-http": "^3.2.1", + "@smithy/types": "^2.10.1", + "@smithy/util-config-provider": "^2.2.1", + "tslib": "^2.5.0" + }, + "dependencies": { + "@aws-sdk/types": { + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.523.0.tgz", + "integrity": "sha512-AqGIu4u+SxPiUuNBp2acCVcq80KDUFjxe6e3cMTvKWTzCbrVk1AXv0dAaJnCmdkWIha6zJDWxpIk/aL4EGhZ9A==", + "requires": { + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + } + }, + "@smithy/node-config-provider": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-2.2.4.tgz", + "integrity": "sha512-nqazHCp8r4KHSFhRQ+T0VEkeqvA0U+RhehBSr1gunUuNW3X7j0uDrWBxB2gE9eutzy6kE3Y7L+Dov/UXT871vg==", + "requires": { + "@smithy/property-provider": "^2.1.3", + "@smithy/shared-ini-file-loader": "^2.3.4", + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + } + }, + "@smithy/property-provider": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-2.1.3.tgz", + "integrity": "sha512-bMz3se+ySKWNrgm7eIiQMa2HO/0fl2D0HvLAdg9pTMcpgp4SqOAh6bz7Ik6y7uQqSrk4rLjIKgbQ6yzYgGehCQ==", + "requires": { + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + } + }, + "@smithy/protocol-http": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-3.2.1.tgz", + "integrity": "sha512-KLrQkEw4yJCeAmAH7hctE8g9KwA7+H2nSJwxgwIxchbp/L0B5exTdOQi9D5HinPLlothoervGmhpYKelZ6AxIA==", + "requires": { + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + } + }, + "@smithy/shared-ini-file-loader": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-2.3.4.tgz", + "integrity": "sha512-CiZmPg9GeDKbKmJGEFvJBsJcFnh0AQRzOtQAzj1XEa8N/0/uSN/v1LYzgO7ry8hhO8+9KB7+DhSW0weqBra4Aw==", + "requires": { + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + } + }, + "@smithy/types": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.10.1.tgz", + "integrity": "sha512-hjQO+4ru4cQ58FluQvKKiyMsFg0A6iRpGm2kqdH8fniyNd2WyanoOsYJfMX/IFLuLxEoW6gnRkNZy1y6fUUhtA==", + "requires": { + "tslib": "^2.5.0" + } + } + } + }, + "@aws-sdk/middleware-expect-continue": { + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.523.0.tgz", + "integrity": "sha512-E5DyRAHU39VHaAlQLqXYS/IKpgk3vsryuU6kkOcIIK8Dgw0a2tjoh5AOCaNa8pD+KgAGrFp35JIMSX1zui5diA==", + "requires": { + "@aws-sdk/types": "3.523.0", + "@smithy/protocol-http": "^3.2.1", + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + }, + "dependencies": { + "@aws-sdk/types": { + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.523.0.tgz", + "integrity": "sha512-AqGIu4u+SxPiUuNBp2acCVcq80KDUFjxe6e3cMTvKWTzCbrVk1AXv0dAaJnCmdkWIha6zJDWxpIk/aL4EGhZ9A==", + "requires": { + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + } + }, + "@smithy/protocol-http": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-3.2.1.tgz", + "integrity": "sha512-KLrQkEw4yJCeAmAH7hctE8g9KwA7+H2nSJwxgwIxchbp/L0B5exTdOQi9D5HinPLlothoervGmhpYKelZ6AxIA==", + "requires": { + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + } + }, + "@smithy/types": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.10.1.tgz", + "integrity": "sha512-hjQO+4ru4cQ58FluQvKKiyMsFg0A6iRpGm2kqdH8fniyNd2WyanoOsYJfMX/IFLuLxEoW6gnRkNZy1y6fUUhtA==", + "requires": { + "tslib": "^2.5.0" + } + } + } + }, + "@aws-sdk/middleware-flexible-checksums": { + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.523.0.tgz", + "integrity": "sha512-lIa1TdWY9q4zsDFarfSnYcdrwPR+nypaU4n6hb95i620/1F5M5s6H8P0hYtwTNNvx+slrR8F3VBML9pjBtzAHw==", + "requires": { + "@aws-crypto/crc32": "3.0.0", + "@aws-crypto/crc32c": "3.0.0", + "@aws-sdk/types": "3.523.0", + "@smithy/is-array-buffer": "^2.1.1", + "@smithy/protocol-http": "^3.2.1", + "@smithy/types": "^2.10.1", + "@smithy/util-utf8": "^2.1.1", + "tslib": "^2.5.0" + }, + "dependencies": { + "@aws-sdk/types": { + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.523.0.tgz", + "integrity": "sha512-AqGIu4u+SxPiUuNBp2acCVcq80KDUFjxe6e3cMTvKWTzCbrVk1AXv0dAaJnCmdkWIha6zJDWxpIk/aL4EGhZ9A==", + "requires": { + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + } + }, + "@smithy/protocol-http": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-3.2.1.tgz", + "integrity": "sha512-KLrQkEw4yJCeAmAH7hctE8g9KwA7+H2nSJwxgwIxchbp/L0B5exTdOQi9D5HinPLlothoervGmhpYKelZ6AxIA==", + "requires": { + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + } + }, + "@smithy/types": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.10.1.tgz", + "integrity": "sha512-hjQO+4ru4cQ58FluQvKKiyMsFg0A6iRpGm2kqdH8fniyNd2WyanoOsYJfMX/IFLuLxEoW6gnRkNZy1y6fUUhtA==", + "requires": { + "tslib": "^2.5.0" + } + } + } + }, + "@aws-sdk/middleware-host-header": { + "version": "3.535.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.535.0.tgz", + "integrity": "sha512-0h6TWjBWtDaYwHMQJI9ulafeS4lLaw1vIxRjbpH0svFRt6Eve+Sy8NlVhECfTU2hNz/fLubvrUxsXoThaLBIew==", + "dev": true, + "optional": true, + "requires": { + "@aws-sdk/types": "3.535.0", + "@smithy/protocol-http": "^3.3.0", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + }, + "dependencies": { + "@aws-sdk/types": { + "version": "3.535.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.535.0.tgz", + "integrity": "sha512-aY4MYfduNj+sRR37U7XxYR8wemfbKP6lx00ze2M2uubn7mZotuVrWYAafbMSXrdEMSToE5JDhr28vArSOoLcSg==", + "dev": true, + "optional": true, + "requires": { + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + } + }, + "@smithy/types": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.12.0.tgz", + "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + } + } + }, + "@aws-sdk/middleware-location-constraint": { + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.523.0.tgz", + "integrity": "sha512-1QAUXX3U0jkARnU0yyjk81EO4Uw5dCeQOtvUY5s3bUOHatR3ThosQeIr6y9BCsbXHzNnDe1ytCjqAPyo8r/bYw==", + "requires": { + "@aws-sdk/types": "3.523.0", + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + }, + "dependencies": { + "@aws-sdk/types": { + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.523.0.tgz", + "integrity": "sha512-AqGIu4u+SxPiUuNBp2acCVcq80KDUFjxe6e3cMTvKWTzCbrVk1AXv0dAaJnCmdkWIha6zJDWxpIk/aL4EGhZ9A==", + "requires": { + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + } + }, + "@smithy/types": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.10.1.tgz", + "integrity": "sha512-hjQO+4ru4cQ58FluQvKKiyMsFg0A6iRpGm2kqdH8fniyNd2WyanoOsYJfMX/IFLuLxEoW6gnRkNZy1y6fUUhtA==", + "requires": { + "tslib": "^2.5.0" + } + } + } + }, + "@aws-sdk/middleware-logger": { + "version": "3.535.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.535.0.tgz", + "integrity": "sha512-huNHpONOrEDrdRTvSQr1cJiRMNf0S52NDXtaPzdxiubTkP+vni2MohmZANMOai/qT0olmEVX01LhZ0ZAOgmg6A==", + "dev": true, + "optional": true, + "requires": { + "@aws-sdk/types": "3.535.0", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + }, + "dependencies": { + "@aws-sdk/types": { + "version": "3.535.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.535.0.tgz", + "integrity": "sha512-aY4MYfduNj+sRR37U7XxYR8wemfbKP6lx00ze2M2uubn7mZotuVrWYAafbMSXrdEMSToE5JDhr28vArSOoLcSg==", + "dev": true, + "optional": true, + "requires": { + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + } + }, + "@smithy/types": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.12.0.tgz", + "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + } + } + }, + "@aws-sdk/middleware-recursion-detection": { + "version": "3.535.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.535.0.tgz", + "integrity": "sha512-am2qgGs+gwqmR4wHLWpzlZ8PWhm4ktj5bYSgDrsOfjhdBlWNxvPoID9/pDAz5RWL48+oH7I6SQzMqxXsFDikrw==", + "dev": true, + "optional": true, + "requires": { + "@aws-sdk/types": "3.535.0", + "@smithy/protocol-http": "^3.3.0", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + }, + "dependencies": { + "@aws-sdk/types": { + "version": "3.535.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.535.0.tgz", + "integrity": "sha512-aY4MYfduNj+sRR37U7XxYR8wemfbKP6lx00ze2M2uubn7mZotuVrWYAafbMSXrdEMSToE5JDhr28vArSOoLcSg==", + "dev": true, + "optional": true, + "requires": { + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + } + }, + "@smithy/types": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.12.0.tgz", + "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + } + } + }, + "@aws-sdk/middleware-sdk-s3": { + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.523.0.tgz", + "integrity": "sha512-cCZ3+XcAJMSC2rsw5F2h+ILVgjijRTxgzD6l7vExhc7UUOOPxXa6R9oGV3+6ANQ/P0w8rvE78j8UAMzlpq+cZA==", + "requires": { + "@aws-sdk/types": "3.523.0", + "@aws-sdk/util-arn-parser": "3.495.0", + "@smithy/node-config-provider": "^2.2.3", + "@smithy/protocol-http": "^3.2.1", + "@smithy/signature-v4": "^2.1.3", + "@smithy/smithy-client": "^2.4.1", + "@smithy/types": "^2.10.1", + "@smithy/util-config-provider": "^2.2.1", + "tslib": "^2.5.0" + }, + "dependencies": { + "@aws-sdk/types": { + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.523.0.tgz", + "integrity": "sha512-AqGIu4u+SxPiUuNBp2acCVcq80KDUFjxe6e3cMTvKWTzCbrVk1AXv0dAaJnCmdkWIha6zJDWxpIk/aL4EGhZ9A==", + "requires": { + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + } + }, + "@smithy/abort-controller": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-2.1.3.tgz", + "integrity": "sha512-c2aYH2Wu1RVE3rLlVgg2kQOBJGM0WbjReQi5DnPTm2Zb7F0gk7J2aeQeaX2u/lQZoHl6gv8Oac7mt9alU3+f4A==", + "requires": { + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + } + }, + "@smithy/eventstream-codec": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-2.1.3.tgz", + "integrity": "sha512-rGlCVuwSDv6qfKH4/lRxFjcZQnIE0LZ3D4lkMHg7ZSltK9rA74r0VuGSvWVQ4N/d70VZPaniFhp4Z14QYZsa+A==", + "requires": { + "@aws-crypto/crc32": "3.0.0", + "@smithy/types": "^2.10.1", + "@smithy/util-hex-encoding": "^2.1.1", + "tslib": "^2.5.0" + } + }, + "@smithy/fetch-http-handler": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-2.4.3.tgz", + "integrity": "sha512-Fn/KYJFo6L5I4YPG8WQb2hOmExgRmNpVH5IK2zU3JKrY5FKW7y9ar5e0BexiIC9DhSKqKX+HeWq/Y18fq7Dkpw==", + "requires": { + "@smithy/protocol-http": "^3.2.1", + "@smithy/querystring-builder": "^2.1.3", + "@smithy/types": "^2.10.1", + "@smithy/util-base64": "^2.1.1", + "tslib": "^2.5.0" + } + }, + "@smithy/middleware-endpoint": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-2.4.4.tgz", + "integrity": "sha512-4yjHyHK2Jul4JUDBo2sTsWY9UshYUnXeb/TAK/MTaPEb8XQvDmpwSFnfIRDU45RY1a6iC9LCnmJNg/yHyfxqkw==", + "requires": { + "@smithy/middleware-serde": "^2.1.3", + "@smithy/node-config-provider": "^2.2.4", + "@smithy/shared-ini-file-loader": "^2.3.4", + "@smithy/types": "^2.10.1", + "@smithy/url-parser": "^2.1.3", + "@smithy/util-middleware": "^2.1.3", + "tslib": "^2.5.0" + } + }, + "@smithy/middleware-serde": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-2.1.3.tgz", + "integrity": "sha512-s76LId+TwASrHhUa9QS4k/zeXDUAuNuddKklQzRgumbzge5BftVXHXIqL4wQxKGLocPwfgAOXWx+HdWhQk9hTg==", + "requires": { + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + } + }, + "@smithy/middleware-stack": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-2.1.3.tgz", + "integrity": "sha512-opMFufVQgvBSld/b7mD7OOEBxF6STyraVr1xel1j0abVILM8ALJvRoFbqSWHGmaDlRGIiV9Q5cGbWi0sdiEaLQ==", + "requires": { + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + } + }, + "@smithy/node-config-provider": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-2.2.4.tgz", + "integrity": "sha512-nqazHCp8r4KHSFhRQ+T0VEkeqvA0U+RhehBSr1gunUuNW3X7j0uDrWBxB2gE9eutzy6kE3Y7L+Dov/UXT871vg==", + "requires": { + "@smithy/property-provider": "^2.1.3", + "@smithy/shared-ini-file-loader": "^2.3.4", + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + } + }, + "@smithy/node-http-handler": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-2.4.1.tgz", + "integrity": "sha512-HCkb94soYhJMxPCa61wGKgmeKpJ3Gftx1XD6bcWEB2wMV1L9/SkQu/6/ysKBnbOzWRE01FGzwrTxucHypZ8rdg==", + "requires": { + "@smithy/abort-controller": "^2.1.3", + "@smithy/protocol-http": "^3.2.1", + "@smithy/querystring-builder": "^2.1.3", + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + } + }, + "@smithy/property-provider": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-2.1.3.tgz", + "integrity": "sha512-bMz3se+ySKWNrgm7eIiQMa2HO/0fl2D0HvLAdg9pTMcpgp4SqOAh6bz7Ik6y7uQqSrk4rLjIKgbQ6yzYgGehCQ==", + "requires": { + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + } + }, + "@smithy/protocol-http": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-3.2.1.tgz", + "integrity": "sha512-KLrQkEw4yJCeAmAH7hctE8g9KwA7+H2nSJwxgwIxchbp/L0B5exTdOQi9D5HinPLlothoervGmhpYKelZ6AxIA==", + "requires": { + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + } + }, + "@smithy/querystring-builder": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-2.1.3.tgz", + "integrity": "sha512-kFD3PnNqKELe6m9GRHQw/ftFFSZpnSeQD4qvgDB6BQN6hREHELSosVFUMPN4M3MDKN2jAwk35vXHLoDrNfKu0A==", + "requires": { + "@smithy/types": "^2.10.1", + "@smithy/util-uri-escape": "^2.1.1", + "tslib": "^2.5.0" + } + }, + "@smithy/querystring-parser": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-2.1.3.tgz", + "integrity": "sha512-3+CWJoAqcBMR+yvz6D+Fc5VdoGFtfenW6wqSWATWajrRMGVwJGPT3Vy2eb2bnMktJc4HU4bpjeovFa566P3knQ==", + "requires": { + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + } + }, + "@smithy/shared-ini-file-loader": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-2.3.4.tgz", + "integrity": "sha512-CiZmPg9GeDKbKmJGEFvJBsJcFnh0AQRzOtQAzj1XEa8N/0/uSN/v1LYzgO7ry8hhO8+9KB7+DhSW0weqBra4Aw==", + "requires": { + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + } + }, + "@smithy/signature-v4": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-2.1.3.tgz", + "integrity": "sha512-Jq4iPPdCmJojZTsPePn4r1ULShh6ONkokLuxp1Lnk4Sq7r7rJp4HlA1LbPBq4bD64TIzQezIpr1X+eh5NYkNxw==", + "requires": { + "@smithy/eventstream-codec": "^2.1.3", + "@smithy/is-array-buffer": "^2.1.1", + "@smithy/types": "^2.10.1", + "@smithy/util-hex-encoding": "^2.1.1", + "@smithy/util-middleware": "^2.1.3", + "@smithy/util-uri-escape": "^2.1.1", + "@smithy/util-utf8": "^2.1.1", + "tslib": "^2.5.0" + } + }, + "@smithy/smithy-client": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-2.4.2.tgz", + "integrity": "sha512-ntAFYN51zu3N3mCd95YFcFi/8rmvm//uX+HnK24CRbI6k5Rjackn0JhgKz5zOx/tbNvOpgQIwhSX+1EvEsBLbA==", + "requires": { + "@smithy/middleware-endpoint": "^2.4.4", + "@smithy/middleware-stack": "^2.1.3", + "@smithy/protocol-http": "^3.2.1", + "@smithy/types": "^2.10.1", + "@smithy/util-stream": "^2.1.3", + "tslib": "^2.5.0" + } + }, + "@smithy/types": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.10.1.tgz", + "integrity": "sha512-hjQO+4ru4cQ58FluQvKKiyMsFg0A6iRpGm2kqdH8fniyNd2WyanoOsYJfMX/IFLuLxEoW6gnRkNZy1y6fUUhtA==", + "requires": { + "tslib": "^2.5.0" + } + }, + "@smithy/url-parser": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-2.1.3.tgz", + "integrity": "sha512-X1NRA4WzK/ihgyzTpeGvI9Wn45y8HmqF4AZ/FazwAv8V203Ex+4lXqcYI70naX9ETqbqKVzFk88W6WJJzCggTQ==", + "requires": { + "@smithy/querystring-parser": "^2.1.3", + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + } + }, + "@smithy/util-middleware": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-2.1.3.tgz", + "integrity": "sha512-/+2fm7AZ2ozl5h8wM++ZP0ovE9/tiUUAHIbCfGfb3Zd3+Dyk17WODPKXBeJ/TnK5U+x743QmA0xHzlSm8I/qhw==", + "requires": { + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + } + }, + "@smithy/util-stream": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-2.1.3.tgz", + "integrity": "sha512-HvpEQbP8raTy9n86ZfXiAkf3ezp1c3qeeO//zGqwZdrfaoOpGKQgF2Sv1IqZp7wjhna7pvczWaGUHjcOPuQwKw==", + "requires": { + "@smithy/fetch-http-handler": "^2.4.3", + "@smithy/node-http-handler": "^2.4.1", + "@smithy/types": "^2.10.1", + "@smithy/util-base64": "^2.1.1", + "@smithy/util-buffer-from": "^2.1.1", + "@smithy/util-hex-encoding": "^2.1.1", + "@smithy/util-utf8": "^2.1.1", + "tslib": "^2.5.0" + } + } + } + }, + "@aws-sdk/middleware-signing": { + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-signing/-/middleware-signing-3.523.0.tgz", + "integrity": "sha512-pFXV4don6qcmew/OvEjLUr2foVjzoJ8o5k57Oz9yAHz8INx3RHK8MP/K4mVhHo6n0SquRcWrm4kY/Tw+89gkEA==", + "requires": { + "@aws-sdk/types": "3.523.0", + "@smithy/property-provider": "^2.1.3", + "@smithy/protocol-http": "^3.2.1", + "@smithy/signature-v4": "^2.1.3", + "@smithy/types": "^2.10.1", + "@smithy/util-middleware": "^2.1.3", + "tslib": "^2.5.0" + }, + "dependencies": { + "@aws-sdk/types": { + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.523.0.tgz", + "integrity": "sha512-AqGIu4u+SxPiUuNBp2acCVcq80KDUFjxe6e3cMTvKWTzCbrVk1AXv0dAaJnCmdkWIha6zJDWxpIk/aL4EGhZ9A==", + "requires": { + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + } + }, + "@smithy/eventstream-codec": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-2.1.3.tgz", + "integrity": "sha512-rGlCVuwSDv6qfKH4/lRxFjcZQnIE0LZ3D4lkMHg7ZSltK9rA74r0VuGSvWVQ4N/d70VZPaniFhp4Z14QYZsa+A==", + "requires": { + "@aws-crypto/crc32": "3.0.0", + "@smithy/types": "^2.10.1", + "@smithy/util-hex-encoding": "^2.1.1", + "tslib": "^2.5.0" + } + }, + "@smithy/property-provider": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-2.1.3.tgz", + "integrity": "sha512-bMz3se+ySKWNrgm7eIiQMa2HO/0fl2D0HvLAdg9pTMcpgp4SqOAh6bz7Ik6y7uQqSrk4rLjIKgbQ6yzYgGehCQ==", + "requires": { + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + } + }, + "@smithy/protocol-http": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-3.2.1.tgz", + "integrity": "sha512-KLrQkEw4yJCeAmAH7hctE8g9KwA7+H2nSJwxgwIxchbp/L0B5exTdOQi9D5HinPLlothoervGmhpYKelZ6AxIA==", + "requires": { + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + } + }, + "@smithy/signature-v4": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-2.1.3.tgz", + "integrity": "sha512-Jq4iPPdCmJojZTsPePn4r1ULShh6ONkokLuxp1Lnk4Sq7r7rJp4HlA1LbPBq4bD64TIzQezIpr1X+eh5NYkNxw==", + "requires": { + "@smithy/eventstream-codec": "^2.1.3", + "@smithy/is-array-buffer": "^2.1.1", + "@smithy/types": "^2.10.1", + "@smithy/util-hex-encoding": "^2.1.1", + "@smithy/util-middleware": "^2.1.3", + "@smithy/util-uri-escape": "^2.1.1", + "@smithy/util-utf8": "^2.1.1", + "tslib": "^2.5.0" + } + }, + "@smithy/types": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.10.1.tgz", + "integrity": "sha512-hjQO+4ru4cQ58FluQvKKiyMsFg0A6iRpGm2kqdH8fniyNd2WyanoOsYJfMX/IFLuLxEoW6gnRkNZy1y6fUUhtA==", + "requires": { + "tslib": "^2.5.0" + } + }, + "@smithy/util-middleware": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-2.1.3.tgz", + "integrity": "sha512-/+2fm7AZ2ozl5h8wM++ZP0ovE9/tiUUAHIbCfGfb3Zd3+Dyk17WODPKXBeJ/TnK5U+x743QmA0xHzlSm8I/qhw==", + "requires": { + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + } + } + } + }, + "@aws-sdk/middleware-ssec": { + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.523.0.tgz", + "integrity": "sha512-FaqAZQeF5cQzZLOIboIJRaWVOQ2F2pJZAXGF5D7nJsxYNFChotA0O0iWimBRxU35RNn7yirVxz35zQzs20ddIw==", + "requires": { + "@aws-sdk/types": "3.523.0", + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + }, + "dependencies": { + "@aws-sdk/types": { + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.523.0.tgz", + "integrity": "sha512-AqGIu4u+SxPiUuNBp2acCVcq80KDUFjxe6e3cMTvKWTzCbrVk1AXv0dAaJnCmdkWIha6zJDWxpIk/aL4EGhZ9A==", + "requires": { + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + } + }, + "@smithy/types": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.10.1.tgz", + "integrity": "sha512-hjQO+4ru4cQ58FluQvKKiyMsFg0A6iRpGm2kqdH8fniyNd2WyanoOsYJfMX/IFLuLxEoW6gnRkNZy1y6fUUhtA==", + "requires": { + "tslib": "^2.5.0" + } + } + } + }, + "@aws-sdk/middleware-user-agent": { + "version": "3.540.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.540.0.tgz", + "integrity": "sha512-8Rd6wPeXDnOYzWj1XCmOKcx/Q87L0K1/EHqOBocGjLVbN3gmRxBvpmR1pRTjf7IsWfnnzN5btqtcAkfDPYQUMQ==", + "dev": true, + "optional": true, + "requires": { + "@aws-sdk/types": "3.535.0", + "@aws-sdk/util-endpoints": "3.540.0", + "@smithy/protocol-http": "^3.3.0", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + }, + "dependencies": { + "@aws-sdk/types": { + "version": "3.535.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.535.0.tgz", + "integrity": "sha512-aY4MYfduNj+sRR37U7XxYR8wemfbKP6lx00ze2M2uubn7mZotuVrWYAafbMSXrdEMSToE5JDhr28vArSOoLcSg==", + "dev": true, + "optional": true, + "requires": { + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + } + }, + "@smithy/types": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.12.0.tgz", + "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + } + } + }, + "@aws-sdk/region-config-resolver": { + "version": "3.535.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.535.0.tgz", + "integrity": "sha512-IXOznDiaItBjsQy4Fil0kzX/J3HxIOknEphqHbOfUf+LpA5ugcsxuQQONrbEQusCBnfJyymrldBvBhFmtlU9Wg==", + "dev": true, + "optional": true, + "requires": { + "@aws-sdk/types": "3.535.0", + "@smithy/node-config-provider": "^2.3.0", + "@smithy/types": "^2.12.0", + "@smithy/util-config-provider": "^2.3.0", + "@smithy/util-middleware": "^2.2.0", + "tslib": "^2.6.2" + }, + "dependencies": { + "@aws-sdk/types": { + "version": "3.535.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.535.0.tgz", + "integrity": "sha512-aY4MYfduNj+sRR37U7XxYR8wemfbKP6lx00ze2M2uubn7mZotuVrWYAafbMSXrdEMSToE5JDhr28vArSOoLcSg==", + "dev": true, + "optional": true, + "requires": { + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + } + }, + "@smithy/types": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.12.0.tgz", + "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + }, + "@smithy/util-config-provider": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-2.3.0.tgz", + "integrity": "sha512-HZkzrRcuFN1k70RLqlNK4FnPXKOpkik1+4JaBoHNJn+RnJGYqaa3c5/+XtLOXhlKzlRgNvyaLieHTW2VwGN0VQ==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + } + } + }, + "@aws-sdk/signature-v4-multi-region": { + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.523.0.tgz", + "integrity": "sha512-TU1AfF6YlihdMy4H5YtkmFYmA/Zrh7sqk2V6tPiR2Vu6idc+9xm1R0UE/2V/DKgMIkxfr4+cAojtp2kqYuuF/A==", + "requires": { + "@aws-sdk/middleware-sdk-s3": "3.523.0", + "@aws-sdk/types": "3.523.0", + "@smithy/protocol-http": "^3.2.1", + "@smithy/signature-v4": "^2.1.3", + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + }, + "dependencies": { + "@aws-sdk/types": { + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.523.0.tgz", + "integrity": "sha512-AqGIu4u+SxPiUuNBp2acCVcq80KDUFjxe6e3cMTvKWTzCbrVk1AXv0dAaJnCmdkWIha6zJDWxpIk/aL4EGhZ9A==", + "requires": { + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + } + }, + "@smithy/eventstream-codec": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-2.1.3.tgz", + "integrity": "sha512-rGlCVuwSDv6qfKH4/lRxFjcZQnIE0LZ3D4lkMHg7ZSltK9rA74r0VuGSvWVQ4N/d70VZPaniFhp4Z14QYZsa+A==", + "requires": { + "@aws-crypto/crc32": "3.0.0", + "@smithy/types": "^2.10.1", + "@smithy/util-hex-encoding": "^2.1.1", + "tslib": "^2.5.0" + } + }, + "@smithy/protocol-http": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-3.2.1.tgz", + "integrity": "sha512-KLrQkEw4yJCeAmAH7hctE8g9KwA7+H2nSJwxgwIxchbp/L0B5exTdOQi9D5HinPLlothoervGmhpYKelZ6AxIA==", + "requires": { + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + } + }, + "@smithy/signature-v4": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-2.1.3.tgz", + "integrity": "sha512-Jq4iPPdCmJojZTsPePn4r1ULShh6ONkokLuxp1Lnk4Sq7r7rJp4HlA1LbPBq4bD64TIzQezIpr1X+eh5NYkNxw==", + "requires": { + "@smithy/eventstream-codec": "^2.1.3", + "@smithy/is-array-buffer": "^2.1.1", + "@smithy/types": "^2.10.1", + "@smithy/util-hex-encoding": "^2.1.1", + "@smithy/util-middleware": "^2.1.3", + "@smithy/util-uri-escape": "^2.1.1", + "@smithy/util-utf8": "^2.1.1", + "tslib": "^2.5.0" + } + }, + "@smithy/types": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.10.1.tgz", + "integrity": "sha512-hjQO+4ru4cQ58FluQvKKiyMsFg0A6iRpGm2kqdH8fniyNd2WyanoOsYJfMX/IFLuLxEoW6gnRkNZy1y6fUUhtA==", + "requires": { + "tslib": "^2.5.0" + } + }, + "@smithy/util-middleware": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-2.1.3.tgz", + "integrity": "sha512-/+2fm7AZ2ozl5h8wM++ZP0ovE9/tiUUAHIbCfGfb3Zd3+Dyk17WODPKXBeJ/TnK5U+x743QmA0xHzlSm8I/qhw==", + "requires": { + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + } + } + } + }, + "@aws-sdk/token-providers": { + "version": "3.540.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.540.0.tgz", + "integrity": "sha512-9BvtiVEZe5Ev88Wa4ZIUbtT6BVcPwhxmVInQ6c12MYNb0WNL54BN6wLy/eknAfF05gpX2/NDU2pUDOyMPdm/+g==", + "dev": true, + "optional": true, + "requires": { + "@aws-sdk/client-sso-oidc": "3.540.0", + "@aws-sdk/types": "3.535.0", + "@smithy/property-provider": "^2.2.0", + "@smithy/shared-ini-file-loader": "^2.4.0", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + }, + "dependencies": { + "@aws-sdk/types": { + "version": "3.535.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.535.0.tgz", + "integrity": "sha512-aY4MYfduNj+sRR37U7XxYR8wemfbKP6lx00ze2M2uubn7mZotuVrWYAafbMSXrdEMSToE5JDhr28vArSOoLcSg==", + "dev": true, + "optional": true, + "requires": { + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + } + }, + "@smithy/types": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.12.0.tgz", + "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + } + } + }, + "@aws-sdk/types": { + "version": "3.515.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.515.0.tgz", + "integrity": "sha512-B3gUpiMlpT6ERaLvZZ61D0RyrQPsFYDkCncLPVkZOKkCOoFU46zi1o6T5JcYiz8vkx1q9RGloQ5exh79s5pU/w==", + "requires": { + "@smithy/types": "^2.9.1", + "tslib": "^2.5.0" + } + }, + "@aws-sdk/util-arn-parser": { + "version": "3.495.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.495.0.tgz", + "integrity": "sha512-hwdA3XAippSEUxs7jpznwD63YYFR+LtQvlEcebPTgWR9oQgG9TfS+39PUfbnEeje1ICuOrN3lrFqFbmP9uzbMg==", + "requires": { + "tslib": "^2.5.0" + } + }, + "@aws-sdk/util-endpoints": { + "version": "3.540.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.540.0.tgz", + "integrity": "sha512-1kMyQFAWx6f8alaI6UT65/5YW/7pDWAKAdNwL6vuJLea03KrZRX3PMoONOSJpAS5m3Ot7HlWZvf3wZDNTLELZw==", + "dev": true, + "optional": true, + "requires": { + "@aws-sdk/types": "3.535.0", + "@smithy/types": "^2.12.0", + "@smithy/util-endpoints": "^1.2.0", + "tslib": "^2.6.2" + }, + "dependencies": { + "@aws-sdk/types": { + "version": "3.535.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.535.0.tgz", + "integrity": "sha512-aY4MYfduNj+sRR37U7XxYR8wemfbKP6lx00ze2M2uubn7mZotuVrWYAafbMSXrdEMSToE5JDhr28vArSOoLcSg==", + "dev": true, + "optional": true, + "requires": { + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + } + }, + "@smithy/types": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.12.0.tgz", + "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + } + } + }, + "@aws-sdk/util-locate-window": { + "version": "3.495.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.495.0.tgz", + "integrity": "sha512-MfaPXT0kLX2tQaR90saBT9fWQq2DHqSSJRzW+MZWsmF+y5LGCOhO22ac/2o6TKSQm7h0HRc2GaADqYYYor62yg==", + "requires": { + "tslib": "^2.5.0" + } + }, + "@aws-sdk/util-user-agent-browser": { + "version": "3.535.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.535.0.tgz", + "integrity": "sha512-RWMcF/xV5n+nhaA/Ff5P3yNP3Kur/I+VNZngog4TEs92oB/nwOdAg/2JL8bVAhUbMrjTjpwm7PItziYFQoqyig==", + "dev": true, + "optional": true, + "requires": { + "@aws-sdk/types": "3.535.0", + "@smithy/types": "^2.12.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "dependencies": { + "@aws-sdk/types": { + "version": "3.535.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.535.0.tgz", + "integrity": "sha512-aY4MYfduNj+sRR37U7XxYR8wemfbKP6lx00ze2M2uubn7mZotuVrWYAafbMSXrdEMSToE5JDhr28vArSOoLcSg==", + "dev": true, + "optional": true, + "requires": { + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + } + }, + "@smithy/types": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.12.0.tgz", + "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + } + } + }, + "@aws-sdk/util-user-agent-node": { + "version": "3.535.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.535.0.tgz", + "integrity": "sha512-dRek0zUuIT25wOWJlsRm97nTkUlh1NDcLsQZIN2Y8KxhwoXXWtJs5vaDPT+qAg+OpcNj80i1zLR/CirqlFg/TQ==", + "dev": true, + "optional": true, + "requires": { + "@aws-sdk/types": "3.535.0", + "@smithy/node-config-provider": "^2.3.0", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + }, + "dependencies": { + "@aws-sdk/types": { + "version": "3.535.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.535.0.tgz", + "integrity": "sha512-aY4MYfduNj+sRR37U7XxYR8wemfbKP6lx00ze2M2uubn7mZotuVrWYAafbMSXrdEMSToE5JDhr28vArSOoLcSg==", + "dev": true, + "optional": true, + "requires": { + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + } + }, + "@smithy/types": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.12.0.tgz", + "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + } + } + }, + "@aws-sdk/util-utf8-browser": { + "version": "3.259.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-utf8-browser/-/util-utf8-browser-3.259.0.tgz", + "integrity": "sha512-UvFa/vR+e19XookZF8RzFZBrw2EUkQWxiBW0yYQAhvk3C+QVGl0H3ouca8LDBlBfQKXwmW3huo/59H8rwb1wJw==", + "requires": { + "tslib": "^2.3.1" + } + }, + "@aws-sdk/xml-builder": { + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.523.0.tgz", + "integrity": "sha512-wfvyVymj2TUw7SuDor9IuFcAzJZvWRBZotvY/wQJOlYa3UP3Oezzecy64N4FWfBJEsZdrTN+HOZFl+IzTWWnUA==", + "requires": { + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + }, + "dependencies": { + "@smithy/types": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.10.1.tgz", + "integrity": "sha512-hjQO+4ru4cQ58FluQvKKiyMsFg0A6iRpGm2kqdH8fniyNd2WyanoOsYJfMX/IFLuLxEoW6gnRkNZy1y6fUUhtA==", + "requires": { + "tslib": "^2.5.0" + } + } + } + }, "@babel/code-frame": { "version": "7.12.11", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", @@ -50,17 +2891,17 @@ } }, "@babel/parser": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.19.4.tgz", - "integrity": "sha512-qpVT7gtuOLjWeDTKLkJ6sryqLliBaFpAtGeqw5cs5giLldvh+Ch0plqnUMKoVAUS6ZEueQQiZV+p5pxtPitEsA==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.9.tgz", + "integrity": "sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==", "dev": true }, "@babel/runtime": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.1.tgz", - "integrity": "sha512-mrzLkl6U9YLF8qpqI7TB82PESyEGjm/0Ly91jG575eVxMMlb8fYfOXFZIJ8XfLrJZQbm7dlKry2bJmXBUEkdFg==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz", + "integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==", "requires": { - "regenerator-runtime": "^0.13.10" + "regenerator-runtime": "^0.14.0" } }, "@chenfengyuan/vue-countdown": { @@ -83,6 +2924,38 @@ "mime-types": "^2.1.12" } }, + "@emnapi/runtime": { + "version": "0.45.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-0.45.0.tgz", + "integrity": "sha512-Txumi3td7J4A/xTTwlssKieHKTGl3j4A1tglBx72auZ49YK7ePY6XZricgIg9mnZT4xPfA+UPCUdnhRuEFDL+w==", + "optional": true, + "requires": { + "tslib": "^2.4.0" + } + }, + "@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^3.3.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true + } + } + }, + "@eslint-community/regexpp": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "dev": true + }, "@eslint/eslintrc": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", @@ -125,10 +2998,151 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "@img/sharp-darwin-arm64": { + "version": "0.33.2", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.2.tgz", + "integrity": "sha512-itHBs1rPmsmGF9p4qRe++CzCgd+kFYktnsoR1sbIAfsRMrJZau0Tt1AH9KVnufc2/tU02Gf6Ibujx+15qRE03w==", + "optional": true, + "requires": { + "@img/sharp-libvips-darwin-arm64": "1.0.1" + } + }, + "@img/sharp-darwin-x64": { + "version": "0.33.2", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.2.tgz", + "integrity": "sha512-/rK/69Rrp9x5kaWBjVN07KixZanRr+W1OiyKdXcbjQD6KbW+obaTeBBtLUAtbBsnlTTmWthw99xqoOS7SsySDg==", + "optional": true, + "requires": { + "@img/sharp-libvips-darwin-x64": "1.0.1" + } + }, + "@img/sharp-libvips-darwin-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.1.tgz", + "integrity": "sha512-kQyrSNd6lmBV7O0BUiyu/OEw9yeNGFbQhbxswS1i6rMDwBBSX+e+rPzu3S+MwAiGU3HdLze3PanQ4Xkfemgzcw==", + "optional": true + }, + "@img/sharp-libvips-darwin-x64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.1.tgz", + "integrity": "sha512-eVU/JYLPVjhhrd8Tk6gosl5pVlvsqiFlt50wotCvdkFGf+mDNBJxMh+bvav+Wt3EBnNZWq8Sp2I7XfSjm8siog==", + "optional": true + }, + "@img/sharp-libvips-linux-arm": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.1.tgz", + "integrity": "sha512-FtdMvR4R99FTsD53IA3LxYGghQ82t3yt0ZQ93WMZ2xV3dqrb0E8zq4VHaTOuLEAuA83oDawHV3fd+BsAPadHIQ==", + "optional": true + }, + "@img/sharp-libvips-linux-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.1.tgz", + "integrity": "sha512-bnGG+MJjdX70mAQcSLxgeJco11G+MxTz+ebxlz8Y3dxyeb3Nkl7LgLI0mXupoO+u1wRNx/iRj5yHtzA4sde1yA==", + "optional": true + }, + "@img/sharp-libvips-linux-s390x": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.1.tgz", + "integrity": "sha512-3+rzfAR1YpMOeA2zZNp+aYEzGNWK4zF3+sdMxuCS3ey9HhDbJ66w6hDSHDMoap32DueFwhhs3vwooAB2MaK4XQ==", + "optional": true + }, + "@img/sharp-libvips-linux-x64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.1.tgz", + "integrity": "sha512-3NR1mxFsaSgMMzz1bAnnKbSAI+lHXVTqAHgc1bgzjHuXjo4hlscpUxc0vFSAPKI3yuzdzcZOkq7nDPrP2F8Jgw==", + "optional": true + }, + "@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.1.tgz", + "integrity": "sha512-5aBRcjHDG/T6jwC3Edl3lP8nl9U2Yo8+oTl5drd1dh9Z1EBfzUKAJFUDTDisDjUwc7N4AjnPGfCA3jl3hY8uDg==", + "optional": true + }, + "@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.1.tgz", + "integrity": "sha512-dcT7inI9DBFK6ovfeWRe3hG30h51cBAP5JXlZfx6pzc/Mnf9HFCQDLtYf4MCBjxaaTfjCCjkBxcy3XzOAo5txw==", + "optional": true + }, + "@img/sharp-linux-arm": { + "version": "0.33.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.2.tgz", + "integrity": "sha512-Fndk/4Zq3vAc4G/qyfXASbS3HBZbKrlnKZLEJzPLrXoJuipFNNwTes71+Ki1hwYW5lch26niRYoZFAtZVf3EGA==", + "optional": true, + "requires": { + "@img/sharp-libvips-linux-arm": "1.0.1" + } + }, + "@img/sharp-linux-arm64": { + "version": "0.33.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.2.tgz", + "integrity": "sha512-pz0NNo882vVfqJ0yNInuG9YH71smP4gRSdeL09ukC2YLE6ZyZePAlWKEHgAzJGTiOh8Qkaov6mMIMlEhmLdKew==", + "optional": true, + "requires": { + "@img/sharp-libvips-linux-arm64": "1.0.1" + } + }, + "@img/sharp-linux-s390x": { + "version": "0.33.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.2.tgz", + "integrity": "sha512-MBoInDXDppMfhSzbMmOQtGfloVAflS2rP1qPcUIiITMi36Mm5YR7r0ASND99razjQUpHTzjrU1flO76hKvP5RA==", + "optional": true, + "requires": { + "@img/sharp-libvips-linux-s390x": "1.0.1" + } + }, + "@img/sharp-linux-x64": { + "version": "0.33.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.2.tgz", + "integrity": "sha512-xUT82H5IbXewKkeF5aiooajoO1tQV4PnKfS/OZtb5DDdxS/FCI/uXTVZ35GQ97RZXsycojz/AJ0asoz6p2/H/A==", + "optional": true, + "requires": { + "@img/sharp-libvips-linux-x64": "1.0.1" + } + }, + "@img/sharp-linuxmusl-arm64": { + "version": "0.33.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.2.tgz", + "integrity": "sha512-F+0z8JCu/UnMzg8IYW1TMeiViIWBVg7IWP6nE0p5S5EPQxlLd76c8jYemG21X99UzFwgkRo5yz2DS+zbrnxZeA==", + "optional": true, + "requires": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.1" + } + }, + "@img/sharp-linuxmusl-x64": { + "version": "0.33.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.2.tgz", + "integrity": "sha512-+ZLE3SQmSL+Fn1gmSaM8uFusW5Y3J9VOf+wMGNnTtJUMUxFhv+P4UPaYEYT8tqnyYVaOVGgMN/zsOxn9pSsO2A==", + "optional": true, + "requires": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.1" + } + }, + "@img/sharp-wasm32": { + "version": "0.33.2", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.2.tgz", + "integrity": "sha512-fLbTaESVKuQcpm8ffgBD7jLb/CQLcATju/jxtTXR1XCLwbOQt+OL5zPHSDMmp2JZIeq82e18yE0Vv7zh6+6BfQ==", + "optional": true, + "requires": { + "@emnapi/runtime": "^0.45.0" + } + }, + "@img/sharp-win32-ia32": { + "version": "0.33.2", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.2.tgz", + "integrity": "sha512-okBpql96hIGuZ4lN3+nsAjGeggxKm7hIRu9zyec0lnfB8E7Z6p95BuRZzDDXZOl2e8UmR4RhYt631i7mfmKU8g==", + "optional": true + }, + "@img/sharp-win32-x64": { + "version": "0.33.2", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.2.tgz", + "integrity": "sha512-E4magOks77DK47FwHUIGH0RYWSgRBfGdK56kIHSVeB9uIS4pPFr4N2kIVsXdQQo4LzOsENKV5KAhRlRL7eMAdg==", + "optional": true + }, "@mapbox/node-pre-gyp": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.10.tgz", - "integrity": "sha512-4ySo4CjzStuprMwk35H5pPbkymjv1SF3jGLj6rAHp/xT/RF7TL7bd9CTm1xDY49K2qF7jmR/g7k+SkLETP6opA==", + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", "requires": { "detect-libc": "^2.0.0", "https-proxy-agent": "^5.0.0", @@ -141,6 +3155,16 @@ "tar": "^6.1.11" } }, + "@mongodb-js/saslprep": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.5.tgz", + "integrity": "sha512-XLNOMH66KhJzUJNwT/qlMnS4WsNDWD5ASdyaSH3EtK+F4r/CFGa3jT4GNi4mfOitGvWXtdLgQJkQjxSVrio+jA==", + "dev": true, + "optional": true, + "requires": { + "sparse-bitfield": "^3.0.3" + } + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -167,36 +3191,1429 @@ "fastq": "^1.6.0" } }, + "@smithy/abort-controller": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-2.2.0.tgz", + "integrity": "sha512-wRlta7GuLWpTqtFfGo+nZyOO1vEvewdNR1R4rTxpC8XU6vG/NDyrFBhwLZsqg1NUoR1noVaXJPC/7ZK47QCySw==", + "dev": true, + "optional": true, + "requires": { + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + }, + "dependencies": { + "@smithy/types": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.12.0.tgz", + "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + } + } + }, + "@smithy/chunked-blob-reader": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader/-/chunked-blob-reader-2.1.1.tgz", + "integrity": "sha512-NjNFCKxC4jVvn+lUr3Yo4/PmUJj3tbyqH6GNHueyTGS5Q27vlEJ1MkNhUDV8QGxJI7Bodnc2pD18lU2zRfhHlQ==", + "requires": { + "tslib": "^2.5.0" + } + }, + "@smithy/chunked-blob-reader-native": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-2.1.1.tgz", + "integrity": "sha512-zNW+43dltfNMUrBEYLMWgI8lQr0uhtTcUyxkgC9EP4j17WREzgSFMPUFVrVV6Rc2+QtWERYjb4tzZnQGa7R9fQ==", + "requires": { + "@smithy/util-base64": "^2.1.1", + "tslib": "^2.5.0" + } + }, + "@smithy/config-resolver": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-2.2.0.tgz", + "integrity": "sha512-fsiMgd8toyUba6n1WRmr+qACzXltpdDkPTAaDqc8QqPBUzO+/JKwL6bUBseHVi8tu9l+3JOK+tSf7cay+4B3LA==", + "dev": true, + "optional": true, + "requires": { + "@smithy/node-config-provider": "^2.3.0", + "@smithy/types": "^2.12.0", + "@smithy/util-config-provider": "^2.3.0", + "@smithy/util-middleware": "^2.2.0", + "tslib": "^2.6.2" + }, + "dependencies": { + "@smithy/types": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.12.0.tgz", + "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + }, + "@smithy/util-config-provider": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-2.3.0.tgz", + "integrity": "sha512-HZkzrRcuFN1k70RLqlNK4FnPXKOpkik1+4JaBoHNJn+RnJGYqaa3c5/+XtLOXhlKzlRgNvyaLieHTW2VwGN0VQ==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + } + } + }, + "@smithy/core": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-1.4.0.tgz", + "integrity": "sha512-uu9ZDI95Uij4qk+L6kyFjdk11zqBkcJ3Lv0sc6jZrqHvLyr0+oeekD3CnqMafBn/5PRI6uv6ulW3kNLRBUHeVw==", + "dev": true, + "optional": true, + "requires": { + "@smithy/middleware-endpoint": "^2.5.0", + "@smithy/middleware-retry": "^2.2.0", + "@smithy/middleware-serde": "^2.3.0", + "@smithy/protocol-http": "^3.3.0", + "@smithy/smithy-client": "^2.5.0", + "@smithy/types": "^2.12.0", + "@smithy/util-middleware": "^2.2.0", + "tslib": "^2.6.2" + }, + "dependencies": { + "@smithy/types": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.12.0.tgz", + "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + } + } + }, + "@smithy/credential-provider-imds": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-2.3.0.tgz", + "integrity": "sha512-BWB9mIukO1wjEOo1Ojgl6LrG4avcaC7T/ZP6ptmAaW4xluhSIPZhY+/PI5YKzlk+jsm+4sQZB45Bt1OfMeQa3w==", + "dev": true, + "optional": true, + "requires": { + "@smithy/node-config-provider": "^2.3.0", + "@smithy/property-provider": "^2.2.0", + "@smithy/types": "^2.12.0", + "@smithy/url-parser": "^2.2.0", + "tslib": "^2.6.2" + }, + "dependencies": { + "@smithy/types": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.12.0.tgz", + "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + } + } + }, + "@smithy/eventstream-codec": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-2.2.0.tgz", + "integrity": "sha512-8janZoJw85nJmQZc4L8TuePp2pk1nxLgkxIR0TUjKJ5Dkj5oelB9WtiSSGXCQvNsJl0VSTvK/2ueMXxvpa9GVw==", + "dev": true, + "optional": true, + "requires": { + "@aws-crypto/crc32": "3.0.0", + "@smithy/types": "^2.12.0", + "@smithy/util-hex-encoding": "^2.2.0", + "tslib": "^2.6.2" + }, + "dependencies": { + "@smithy/types": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.12.0.tgz", + "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + }, + "@smithy/util-hex-encoding": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-2.2.0.tgz", + "integrity": "sha512-7iKXR+/4TpLK194pVjKiasIyqMtTYJsgKgM242Y9uzt5dhHnUDvMNb+3xIhRJ9QhvqGii/5cRUt4fJn3dtXNHQ==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + } + } + }, + "@smithy/eventstream-serde-browser": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-2.1.3.tgz", + "integrity": "sha512-qAgKbZ9m2oBfSyJWWurX/MvQFRPrYypj79cDSleEgDwBoez6Tfd+FTpu2L/j3ZeC3mDlDHIKWksoeaXZpLLAHw==", + "requires": { + "@smithy/eventstream-serde-universal": "^2.1.3", + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + }, + "dependencies": { + "@smithy/types": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.10.1.tgz", + "integrity": "sha512-hjQO+4ru4cQ58FluQvKKiyMsFg0A6iRpGm2kqdH8fniyNd2WyanoOsYJfMX/IFLuLxEoW6gnRkNZy1y6fUUhtA==", + "requires": { + "tslib": "^2.5.0" + } + } + } + }, + "@smithy/eventstream-serde-config-resolver": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-2.1.3.tgz", + "integrity": "sha512-48rvsNv/MgAFCxOE0qwR7ZwKhaEdDoTxqH5HM+T6SDxICmPGb7gEuQzjTxQhcieCPgqyXeZFW8cU0QJxdowuIg==", + "requires": { + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + }, + "dependencies": { + "@smithy/types": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.10.1.tgz", + "integrity": "sha512-hjQO+4ru4cQ58FluQvKKiyMsFg0A6iRpGm2kqdH8fniyNd2WyanoOsYJfMX/IFLuLxEoW6gnRkNZy1y6fUUhtA==", + "requires": { + "tslib": "^2.5.0" + } + } + } + }, + "@smithy/eventstream-serde-node": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-2.1.3.tgz", + "integrity": "sha512-RPJWWDhj8isk3NtGfm3Xt1WdHyX9ZE42V+m1nLU1I0zZ1hEol/oawHsTnhva/VR5bn+bJ2zscx+BYr0cEPRtmg==", + "requires": { + "@smithy/eventstream-serde-universal": "^2.1.3", + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + }, + "dependencies": { + "@smithy/types": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.10.1.tgz", + "integrity": "sha512-hjQO+4ru4cQ58FluQvKKiyMsFg0A6iRpGm2kqdH8fniyNd2WyanoOsYJfMX/IFLuLxEoW6gnRkNZy1y6fUUhtA==", + "requires": { + "tslib": "^2.5.0" + } + } + } + }, + "@smithy/eventstream-serde-universal": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-2.1.3.tgz", + "integrity": "sha512-ssvSMk1LX2jRhiOVgVLGfNJXdB8SvyjieKcJDHq698Gi3LOog6g/+l7ggrN+hZxyjUiDF4cUxgKaZTBUghzhLw==", + "requires": { + "@smithy/eventstream-codec": "^2.1.3", + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + }, + "dependencies": { + "@smithy/eventstream-codec": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-2.1.3.tgz", + "integrity": "sha512-rGlCVuwSDv6qfKH4/lRxFjcZQnIE0LZ3D4lkMHg7ZSltK9rA74r0VuGSvWVQ4N/d70VZPaniFhp4Z14QYZsa+A==", + "requires": { + "@aws-crypto/crc32": "3.0.0", + "@smithy/types": "^2.10.1", + "@smithy/util-hex-encoding": "^2.1.1", + "tslib": "^2.5.0" + } + }, + "@smithy/types": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.10.1.tgz", + "integrity": "sha512-hjQO+4ru4cQ58FluQvKKiyMsFg0A6iRpGm2kqdH8fniyNd2WyanoOsYJfMX/IFLuLxEoW6gnRkNZy1y6fUUhtA==", + "requires": { + "tslib": "^2.5.0" + } + } + } + }, + "@smithy/fetch-http-handler": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-2.5.0.tgz", + "integrity": "sha512-BOWEBeppWhLn/no/JxUL/ghTfANTjT7kg3Ww2rPqTUY9R4yHPXxJ9JhMe3Z03LN3aPwiwlpDIUcVw1xDyHqEhw==", + "dev": true, + "optional": true, + "requires": { + "@smithy/protocol-http": "^3.3.0", + "@smithy/querystring-builder": "^2.2.0", + "@smithy/types": "^2.12.0", + "@smithy/util-base64": "^2.3.0", + "tslib": "^2.6.2" + }, + "dependencies": { + "@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + }, + "@smithy/types": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.12.0.tgz", + "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + }, + "@smithy/util-base64": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-2.3.0.tgz", + "integrity": "sha512-s3+eVwNeJuXUwuMbusncZNViuhv2LjVJ1nMwTqSA0XAC7gjKhqqxRdJPhR8+YrkoZ9IiIbFk/yK6ACe/xlF+hw==", + "dev": true, + "optional": true, + "requires": { + "@smithy/util-buffer-from": "^2.2.0", + "@smithy/util-utf8": "^2.3.0", + "tslib": "^2.6.2" + } + }, + "@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "dev": true, + "optional": true, + "requires": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + } + }, + "@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "dev": true, + "optional": true, + "requires": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + } + } + } + }, + "@smithy/hash-blob-browser": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-2.1.3.tgz", + "integrity": "sha512-sHLTM5xQYw5Wxz07DFo+eh1PVC6P5+kazQRF1k5nsvOhZG5VnkIy4LZ7N0ZNWqJx16g9otGd5MvqUOpb3WWtgA==", + "requires": { + "@smithy/chunked-blob-reader": "^2.1.1", + "@smithy/chunked-blob-reader-native": "^2.1.1", + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + }, + "dependencies": { + "@smithy/types": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.10.1.tgz", + "integrity": "sha512-hjQO+4ru4cQ58FluQvKKiyMsFg0A6iRpGm2kqdH8fniyNd2WyanoOsYJfMX/IFLuLxEoW6gnRkNZy1y6fUUhtA==", + "requires": { + "tslib": "^2.5.0" + } + } + } + }, + "@smithy/hash-node": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-2.2.0.tgz", + "integrity": "sha512-zLWaC/5aWpMrHKpoDF6nqpNtBhlAYKF/7+9yMN7GpdR8CzohnWfGtMznPybnwSS8saaXBMxIGwJqR4HmRp6b3g==", + "dev": true, + "optional": true, + "requires": { + "@smithy/types": "^2.12.0", + "@smithy/util-buffer-from": "^2.2.0", + "@smithy/util-utf8": "^2.3.0", + "tslib": "^2.6.2" + }, + "dependencies": { + "@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + }, + "@smithy/types": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.12.0.tgz", + "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + }, + "@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "dev": true, + "optional": true, + "requires": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + } + }, + "@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "dev": true, + "optional": true, + "requires": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + } + } + } + }, + "@smithy/hash-stream-node": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-2.1.3.tgz", + "integrity": "sha512-fWpUx2ca/u5lcD5RhNJogEG5FD7H0RDDpYmfQgxFqIUv3Ow7bZsapMukh8uzQPVO8R+NDAvSdxmgXoy4Hz8sFw==", + "requires": { + "@smithy/types": "^2.10.1", + "@smithy/util-utf8": "^2.1.1", + "tslib": "^2.5.0" + }, + "dependencies": { + "@smithy/types": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.10.1.tgz", + "integrity": "sha512-hjQO+4ru4cQ58FluQvKKiyMsFg0A6iRpGm2kqdH8fniyNd2WyanoOsYJfMX/IFLuLxEoW6gnRkNZy1y6fUUhtA==", + "requires": { + "tslib": "^2.5.0" + } + } + } + }, + "@smithy/invalid-dependency": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-2.2.0.tgz", + "integrity": "sha512-nEDASdbKFKPXN2O6lOlTgrEEOO9NHIeO+HVvZnkqc8h5U9g3BIhWsvzFo+UcUbliMHvKNPD/zVxDrkP1Sbgp8Q==", + "dev": true, + "optional": true, + "requires": { + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + }, + "dependencies": { + "@smithy/types": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.12.0.tgz", + "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + } + } + }, + "@smithy/is-array-buffer": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.1.1.tgz", + "integrity": "sha512-xozSQrcUinPpNPNPds4S7z/FakDTh1MZWtRP/2vQtYB/u3HYrX2UXuZs+VhaKBd6Vc7g2XPr2ZtwGBNDN6fNKQ==", + "requires": { + "tslib": "^2.5.0" + } + }, + "@smithy/md5-js": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-2.1.3.tgz", + "integrity": "sha512-zmn3M6+mP4IJlSmXBN9964AztgkIO8b5lRzAgdJn9AdCFwA6xLkcW2B6uEnpBjvotxtQMmXTUP19tIO7NmFPpw==", + "requires": { + "@smithy/types": "^2.10.1", + "@smithy/util-utf8": "^2.1.1", + "tslib": "^2.5.0" + }, + "dependencies": { + "@smithy/types": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.10.1.tgz", + "integrity": "sha512-hjQO+4ru4cQ58FluQvKKiyMsFg0A6iRpGm2kqdH8fniyNd2WyanoOsYJfMX/IFLuLxEoW6gnRkNZy1y6fUUhtA==", + "requires": { + "tslib": "^2.5.0" + } + } + } + }, + "@smithy/middleware-content-length": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-2.2.0.tgz", + "integrity": "sha512-5bl2LG1Ah/7E5cMSC+q+h3IpVHMeOkG0yLRyQT1p2aMJkSrZG7RlXHPuAgb7EyaFeidKEnnd/fNaLLaKlHGzDQ==", + "dev": true, + "optional": true, + "requires": { + "@smithy/protocol-http": "^3.3.0", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + }, + "dependencies": { + "@smithy/types": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.12.0.tgz", + "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + } + } + }, + "@smithy/middleware-endpoint": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-2.5.0.tgz", + "integrity": "sha512-OBhI9ZEAG8Xen0xsFJwwNOt44WE2CWkfYIxTognC8x42Lfsdf0VN/wCMqpdkySMDio/vts10BiovAxQp0T0faA==", + "dev": true, + "optional": true, + "requires": { + "@smithy/middleware-serde": "^2.3.0", + "@smithy/node-config-provider": "^2.3.0", + "@smithy/shared-ini-file-loader": "^2.4.0", + "@smithy/types": "^2.12.0", + "@smithy/url-parser": "^2.2.0", + "@smithy/util-middleware": "^2.2.0", + "tslib": "^2.6.2" + }, + "dependencies": { + "@smithy/types": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.12.0.tgz", + "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + } + } + }, + "@smithy/middleware-retry": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-2.2.0.tgz", + "integrity": "sha512-PsjDOLpbevgn37yJbawmfVoanru40qVA8UEf2+YA1lvOefmhuhL6ZbKtGsLAWDRnE1OlAmedsbA/htH6iSZjNA==", + "dev": true, + "optional": true, + "requires": { + "@smithy/node-config-provider": "^2.3.0", + "@smithy/protocol-http": "^3.3.0", + "@smithy/service-error-classification": "^2.1.5", + "@smithy/smithy-client": "^2.5.0", + "@smithy/types": "^2.12.0", + "@smithy/util-middleware": "^2.2.0", + "@smithy/util-retry": "^2.2.0", + "tslib": "^2.6.2", + "uuid": "^8.3.2" + }, + "dependencies": { + "@smithy/types": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.12.0.tgz", + "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "optional": true + } + } + }, + "@smithy/middleware-serde": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-2.3.0.tgz", + "integrity": "sha512-sIADe7ojwqTyvEQBe1nc/GXB9wdHhi9UwyX0lTyttmUWDJLP655ZYE1WngnNyXREme8I27KCaUhyhZWRXL0q7Q==", + "dev": true, + "optional": true, + "requires": { + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + }, + "dependencies": { + "@smithy/types": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.12.0.tgz", + "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + } + } + }, + "@smithy/middleware-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-2.2.0.tgz", + "integrity": "sha512-Qntc3jrtwwrsAC+X8wms8zhrTr0sFXnyEGhZd9sLtsJ/6gGQKFzNB+wWbOcpJd7BR8ThNCoKt76BuQahfMvpeA==", + "dev": true, + "optional": true, + "requires": { + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + }, + "dependencies": { + "@smithy/types": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.12.0.tgz", + "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + } + } + }, + "@smithy/node-config-provider": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-2.3.0.tgz", + "integrity": "sha512-0elK5/03a1JPWMDPaS726Iw6LpQg80gFut1tNpPfxFuChEEklo2yL823V94SpTZTxmKlXFtFgsP55uh3dErnIg==", + "dev": true, + "optional": true, + "requires": { + "@smithy/property-provider": "^2.2.0", + "@smithy/shared-ini-file-loader": "^2.4.0", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + }, + "dependencies": { + "@smithy/types": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.12.0.tgz", + "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + } + } + }, + "@smithy/node-http-handler": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-2.5.0.tgz", + "integrity": "sha512-mVGyPBzkkGQsPoxQUbxlEfRjrj6FPyA3u3u2VXGr9hT8wilsoQdZdvKpMBFMB8Crfhv5dNkKHIW0Yyuc7eABqA==", + "dev": true, + "optional": true, + "requires": { + "@smithy/abort-controller": "^2.2.0", + "@smithy/protocol-http": "^3.3.0", + "@smithy/querystring-builder": "^2.2.0", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + }, + "dependencies": { + "@smithy/types": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.12.0.tgz", + "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + } + } + }, + "@smithy/property-provider": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-2.2.0.tgz", + "integrity": "sha512-+xiil2lFhtTRzXkx8F053AV46QnIw6e7MV8od5Mi68E1ICOjCeCHw2XfLnDEUHnT9WGUIkwcqavXjfwuJbGlpg==", + "dev": true, + "optional": true, + "requires": { + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + }, + "dependencies": { + "@smithy/types": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.12.0.tgz", + "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + } + } + }, + "@smithy/protocol-http": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-3.3.0.tgz", + "integrity": "sha512-Xy5XK1AFWW2nlY/biWZXu6/krgbaf2dg0q492D8M5qthsnU2H+UgFeZLbM76FnH7s6RO/xhQRkj+T6KBO3JzgQ==", + "dev": true, + "optional": true, + "requires": { + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + }, + "dependencies": { + "@smithy/types": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.12.0.tgz", + "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + } + } + }, + "@smithy/querystring-builder": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-2.2.0.tgz", + "integrity": "sha512-L1kSeviUWL+emq3CUVSgdogoM/D9QMFaqxL/dd0X7PCNWmPXqt+ExtrBjqT0V7HLN03Vs9SuiLrG3zy3JGnE5A==", + "dev": true, + "optional": true, + "requires": { + "@smithy/types": "^2.12.0", + "@smithy/util-uri-escape": "^2.2.0", + "tslib": "^2.6.2" + }, + "dependencies": { + "@smithy/types": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.12.0.tgz", + "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + }, + "@smithy/util-uri-escape": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-2.2.0.tgz", + "integrity": "sha512-jtmJMyt1xMD/d8OtbVJ2gFZOSKc+ueYJZPW20ULW1GOp/q/YIM0wNh+u8ZFao9UaIGz4WoPW8hC64qlWLIfoDA==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + } + } + }, + "@smithy/querystring-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-2.2.0.tgz", + "integrity": "sha512-BvHCDrKfbG5Yhbpj4vsbuPV2GgcpHiAkLeIlcA1LtfpMz3jrqizP1+OguSNSj1MwBHEiN+jwNisXLGdajGDQJA==", + "dev": true, + "optional": true, + "requires": { + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + }, + "dependencies": { + "@smithy/types": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.12.0.tgz", + "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + } + } + }, + "@smithy/service-error-classification": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-2.1.5.tgz", + "integrity": "sha512-uBDTIBBEdAQryvHdc5W8sS5YX7RQzF683XrHePVdFmAgKiMofU15FLSM0/HU03hKTnazdNRFa0YHS7+ArwoUSQ==", + "dev": true, + "optional": true, + "requires": { + "@smithy/types": "^2.12.0" + }, + "dependencies": { + "@smithy/types": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.12.0.tgz", + "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + } + } + }, + "@smithy/shared-ini-file-loader": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-2.4.0.tgz", + "integrity": "sha512-WyujUJL8e1B6Z4PBfAqC/aGY1+C7T0w20Gih3yrvJSk97gpiVfB+y7c46T4Nunk+ZngLq0rOIdeVeIklk0R3OA==", + "dev": true, + "optional": true, + "requires": { + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + }, + "dependencies": { + "@smithy/types": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.12.0.tgz", + "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + } + } + }, + "@smithy/signature-v4": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-2.2.0.tgz", + "integrity": "sha512-+B5TNzj/fRZzVW3z8UUJOkNx15+4E0CLuvJmJUA1JUIZFp3rdJ/M2H5r2SqltaVPXL0oIxv/6YK92T9TsFGbFg==", + "dev": true, + "optional": true, + "requires": { + "@smithy/eventstream-codec": "^2.2.0", + "@smithy/is-array-buffer": "^2.2.0", + "@smithy/types": "^2.12.0", + "@smithy/util-hex-encoding": "^2.2.0", + "@smithy/util-middleware": "^2.2.0", + "@smithy/util-uri-escape": "^2.2.0", + "@smithy/util-utf8": "^2.3.0", + "tslib": "^2.6.2" + }, + "dependencies": { + "@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + }, + "@smithy/types": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.12.0.tgz", + "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + }, + "@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "dev": true, + "optional": true, + "requires": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + } + }, + "@smithy/util-hex-encoding": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-2.2.0.tgz", + "integrity": "sha512-7iKXR+/4TpLK194pVjKiasIyqMtTYJsgKgM242Y9uzt5dhHnUDvMNb+3xIhRJ9QhvqGii/5cRUt4fJn3dtXNHQ==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + }, + "@smithy/util-uri-escape": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-2.2.0.tgz", + "integrity": "sha512-jtmJMyt1xMD/d8OtbVJ2gFZOSKc+ueYJZPW20ULW1GOp/q/YIM0wNh+u8ZFao9UaIGz4WoPW8hC64qlWLIfoDA==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + }, + "@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "dev": true, + "optional": true, + "requires": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + } + } + } + }, + "@smithy/smithy-client": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-2.5.0.tgz", + "integrity": "sha512-DDXWHWdimtS3y/Kw1Jo46KQ0ZYsDKcldFynQERUGBPDpkW1lXOTHy491ALHjwfiBQvzsVKVxl5+ocXNIgJuX4g==", + "dev": true, + "optional": true, + "requires": { + "@smithy/middleware-endpoint": "^2.5.0", + "@smithy/middleware-stack": "^2.2.0", + "@smithy/protocol-http": "^3.3.0", + "@smithy/types": "^2.12.0", + "@smithy/util-stream": "^2.2.0", + "tslib": "^2.6.2" + }, + "dependencies": { + "@smithy/types": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.12.0.tgz", + "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + } + } + }, + "@smithy/types": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.9.1.tgz", + "integrity": "sha512-vjXlKNXyprDYDuJ7UW5iobdmyDm6g8dDG+BFUncAg/3XJaN45Gy5RWWWUVgrzIK7S4R1KWgIX5LeJcfvSI24bw==", + "requires": { + "tslib": "^2.5.0" + } + }, + "@smithy/url-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-2.2.0.tgz", + "integrity": "sha512-hoA4zm61q1mNTpksiSWp2nEl1dt3j726HdRhiNgVJQMj7mLp7dprtF57mOB6JvEk/x9d2bsuL5hlqZbBuHQylQ==", + "dev": true, + "optional": true, + "requires": { + "@smithy/querystring-parser": "^2.2.0", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + }, + "dependencies": { + "@smithy/types": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.12.0.tgz", + "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + } + } + }, + "@smithy/util-base64": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-2.1.1.tgz", + "integrity": "sha512-UfHVpY7qfF/MrgndI5PexSKVTxSZIdz9InghTFa49QOvuu9I52zLPLUHXvHpNuMb1iD2vmc6R+zbv/bdMipR/g==", + "requires": { + "@smithy/util-buffer-from": "^2.1.1", + "tslib": "^2.5.0" + } + }, + "@smithy/util-body-length-browser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-2.1.1.tgz", + "integrity": "sha512-ekOGBLvs1VS2d1zM2ER4JEeBWAvIOUKeaFch29UjjJsxmZ/f0L3K3x0dEETgh3Q9bkZNHgT+rkdl/J/VUqSRag==", + "requires": { + "tslib": "^2.5.0" + } + }, + "@smithy/util-body-length-node": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-2.2.1.tgz", + "integrity": "sha512-/ggJG+ta3IDtpNVq4ktmEUtOkH1LW64RHB5B0hcr5ZaWBmo96UX2cIOVbjCqqDickTXqBWZ4ZO0APuaPrD7Abg==", + "requires": { + "tslib": "^2.5.0" + } + }, + "@smithy/util-buffer-from": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.1.1.tgz", + "integrity": "sha512-clhNjbyfqIv9Md2Mg6FffGVrJxw7bgK7s3Iax36xnfVj6cg0fUG7I4RH0XgXJF8bxi+saY5HR21g2UPKSxVCXg==", + "requires": { + "@smithy/is-array-buffer": "^2.1.1", + "tslib": "^2.5.0" + } + }, + "@smithy/util-config-provider": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-2.2.1.tgz", + "integrity": "sha512-50VL/tx9oYYcjJn/qKqNy7sCtpD0+s8XEBamIFo4mFFTclKMNp+rsnymD796uybjiIquB7VCB/DeafduL0y2kw==", + "requires": { + "tslib": "^2.5.0" + } + }, + "@smithy/util-defaults-mode-browser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-2.2.0.tgz", + "integrity": "sha512-2okTdZaCBvOJszAPU/KSvlimMe35zLOKbQpHhamFJmR7t95HSe0K3C92jQPjKY3PmDBD+7iMkOnuW05F5OlF4g==", + "dev": true, + "optional": true, + "requires": { + "@smithy/property-provider": "^2.2.0", + "@smithy/smithy-client": "^2.5.0", + "@smithy/types": "^2.12.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "dependencies": { + "@smithy/types": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.12.0.tgz", + "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + } + } + }, + "@smithy/util-defaults-mode-node": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-2.3.0.tgz", + "integrity": "sha512-hfKXnNLmsW9cmLb/JXKIvtuO6Cf4SuqN5PN1C2Ru/TBIws+m1wSgb+A53vo0r66xzB6E82inKG2J7qtwdi+Kkw==", + "dev": true, + "optional": true, + "requires": { + "@smithy/config-resolver": "^2.2.0", + "@smithy/credential-provider-imds": "^2.3.0", + "@smithy/node-config-provider": "^2.3.0", + "@smithy/property-provider": "^2.2.0", + "@smithy/smithy-client": "^2.5.0", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + }, + "dependencies": { + "@smithy/types": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.12.0.tgz", + "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + } + } + }, + "@smithy/util-endpoints": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-1.2.0.tgz", + "integrity": "sha512-BuDHv8zRjsE5zXd3PxFXFknzBG3owCpjq8G3FcsXW3CykYXuEqM3nTSsmLzw5q+T12ZYuDlVUZKBdpNbhVtlrQ==", + "dev": true, + "optional": true, + "requires": { + "@smithy/node-config-provider": "^2.3.0", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + }, + "dependencies": { + "@smithy/types": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.12.0.tgz", + "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + } + } + }, + "@smithy/util-hex-encoding": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-2.1.1.tgz", + "integrity": "sha512-3UNdP2pkYUUBGEXzQI9ODTDK+Tcu1BlCyDBaRHwyxhA+8xLP8agEKQq4MGmpjqb4VQAjq9TwlCQX0kP6XDKYLg==", + "requires": { + "tslib": "^2.5.0" + } + }, + "@smithy/util-middleware": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-2.2.0.tgz", + "integrity": "sha512-L1qpleXf9QD6LwLCJ5jddGkgWyuSvWBkJwWAZ6kFkdifdso+sk3L3O1HdmPvCdnCK3IS4qWyPxev01QMnfHSBw==", + "dev": true, + "optional": true, + "requires": { + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + }, + "dependencies": { + "@smithy/types": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.12.0.tgz", + "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + } + } + }, + "@smithy/util-retry": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-2.2.0.tgz", + "integrity": "sha512-q9+pAFPTfftHXRytmZ7GzLFFrEGavqapFc06XxzZFcSIGERXMerXxCitjOG1prVDR9QdjqotF40SWvbqcCpf8g==", + "dev": true, + "optional": true, + "requires": { + "@smithy/service-error-classification": "^2.1.5", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + }, + "dependencies": { + "@smithy/types": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.12.0.tgz", + "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + } + } + }, + "@smithy/util-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-2.2.0.tgz", + "integrity": "sha512-17faEXbYWIRst1aU9SvPZyMdWmqIrduZjVOqCPMIsWFNxs5yQQgFrJL6b2SdiCzyW9mJoDjFtgi53xx7EH+BXA==", + "dev": true, + "optional": true, + "requires": { + "@smithy/fetch-http-handler": "^2.5.0", + "@smithy/node-http-handler": "^2.5.0", + "@smithy/types": "^2.12.0", + "@smithy/util-base64": "^2.3.0", + "@smithy/util-buffer-from": "^2.2.0", + "@smithy/util-hex-encoding": "^2.2.0", + "@smithy/util-utf8": "^2.3.0", + "tslib": "^2.6.2" + }, + "dependencies": { + "@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + }, + "@smithy/types": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.12.0.tgz", + "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + }, + "@smithy/util-base64": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-2.3.0.tgz", + "integrity": "sha512-s3+eVwNeJuXUwuMbusncZNViuhv2LjVJ1nMwTqSA0XAC7gjKhqqxRdJPhR8+YrkoZ9IiIbFk/yK6ACe/xlF+hw==", + "dev": true, + "optional": true, + "requires": { + "@smithy/util-buffer-from": "^2.2.0", + "@smithy/util-utf8": "^2.3.0", + "tslib": "^2.6.2" + } + }, + "@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "dev": true, + "optional": true, + "requires": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + } + }, + "@smithy/util-hex-encoding": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-2.2.0.tgz", + "integrity": "sha512-7iKXR+/4TpLK194pVjKiasIyqMtTYJsgKgM242Y9uzt5dhHnUDvMNb+3xIhRJ9QhvqGii/5cRUt4fJn3dtXNHQ==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.6.2" + } + }, + "@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "dev": true, + "optional": true, + "requires": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + } + } + } + }, + "@smithy/util-uri-escape": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-2.1.1.tgz", + "integrity": "sha512-saVzI1h6iRBUVSqtnlOnc9ssU09ypo7n+shdQ8hBTZno/9rZ3AuRYvoHInV57VF7Qn7B+pFJG7qTzFiHxWlWBw==", + "requires": { + "tslib": "^2.5.0" + } + }, + "@smithy/util-utf8": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.1.1.tgz", + "integrity": "sha512-BqTpzYEcUMDwAKr7/mVRUtHDhs6ZoXDi9NypMvMfOr/+u1NW7JgqodPDECiiLboEm6bobcPcECxzjtQh865e9A==", + "requires": { + "@smithy/util-buffer-from": "^2.1.1", + "tslib": "^2.5.0" + } + }, + "@smithy/util-waiter": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-2.1.3.tgz", + "integrity": "sha512-3R0wNFAQQoH9e4m+bVLDYNOst2qNxtxFgq03WoNHWTBOqQT3jFnOBRj1W51Rf563xDA5kwqjziksxn6RKkHB+Q==", + "requires": { + "@smithy/abort-controller": "^2.1.3", + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + }, + "dependencies": { + "@smithy/abort-controller": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-2.1.3.tgz", + "integrity": "sha512-c2aYH2Wu1RVE3rLlVgg2kQOBJGM0WbjReQi5DnPTm2Zb7F0gk7J2aeQeaX2u/lQZoHl6gv8Oac7mt9alU3+f4A==", + "requires": { + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + } + }, + "@smithy/types": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.10.1.tgz", + "integrity": "sha512-hjQO+4ru4cQ58FluQvKKiyMsFg0A6iRpGm2kqdH8fniyNd2WyanoOsYJfMX/IFLuLxEoW6gnRkNZy1y6fUUhtA==", + "requires": { + "tslib": "^2.5.0" + } + } + } + }, "@tozd/vue-observer-utils": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/@tozd/vue-observer-utils/-/vue-observer-utils-0.5.0.tgz", "integrity": "sha512-HeRxWFJB7FXcQigH2LvauiR0l7hA4qqBC6hK9rBeKf076Ew08C4lx3eo7/YmvADt3b8ZP1j+TN0pGCEhKYOhEA==" }, + "@types/chai": { + "version": "4.3.11", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.11.tgz", + "integrity": "sha512-qQR1dr2rGIHYlJulmr8Ioq3De0Le9E4MJ5AiaeAETJJpndT1uUNHsGFK3L/UIu+rbkQSdj8J/w2bCsBZc/Y5fQ==", + "dev": true + }, + "@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/jquery": { + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.29.tgz", + "integrity": "sha512-oXQQC9X9MOPRrMhPHHOsXqeQDnWeCDT3PelUIg/Oy8FAbzSZtFHRjc7IpbfFVmpLtJ+UOoywpRsuO5Jxjybyeg==", + "dev": true, + "requires": { + "@types/sizzle": "*" + } + }, "@types/json-schema": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", - "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "@types/lodash": { + "version": "4.14.202", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz", + "integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==", + "dev": true + }, + "@types/meteor": { + "version": "2.9.8", + "resolved": "https://registry.npmjs.org/@types/meteor/-/meteor-2.9.8.tgz", + "integrity": "sha512-p96s1lMqtwt0hz50yokQJA+V9BQzNMLJfahwlbbfXKwbI/OMNCKhRfnxiiNROXAF080zctJlEcpjmv0YG7ztkA==", + "dev": true, + "requires": { + "@types/connect": "*", + "@types/jquery": "*", + "@types/node": "*", + "@types/nodemailer": "*", + "@types/react": "*", + "@types/underscore": "*", + "mongodb": "^4.3.1" + } + }, + "@types/meteor-mdg-validated-method": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@types/meteor-mdg-validated-method/-/meteor-mdg-validated-method-1.2.10.tgz", + "integrity": "sha512-3xWIlRZuTaxe1Ogo/kA+eaJ9Dnlptk4wOb1e92ZjQ4mB3lRXW5JcFU7o/y6yd0L62xTtukjZOatXy/xpoDEdGQ==", + "dev": true, + "requires": { + "@types/meteor": "*", + "@types/node": "*" + } + }, + "@types/mocha": { + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.6.tgz", + "integrity": "sha512-dJvrYWxP/UcXm36Qn36fxhUKu8A/xMRXVT2cliFF1Z7UA9liG5Psj3ezNSZw+5puH2czDXRLcXQxf8JbJt0ejg==", + "dev": true + }, + "@types/node": { + "version": "20.11.30", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.30.tgz", + "integrity": "sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==", + "dev": true, + "requires": { + "undici-types": "~5.26.4" + } + }, + "@types/nodemailer": { + "version": "6.4.14", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.14.tgz", + "integrity": "sha512-fUWthHO9k9DSdPCSPRqcu6TWhYyxTBg382vlNIttSe9M7XfsT06y0f24KHXtbnijPGGRIcVvdKHTNikOI6qiHA==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/prop-types": { + "version": "15.7.12", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", + "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==", + "dev": true + }, + "@types/react": { + "version": "18.2.71", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.71.tgz", + "integrity": "sha512-PxEsB9OjmQeYGffoWnYAd/r5FiJuUw2niFQHPc2v2idwh8wGPkkYzOHuinNJJY6NZqfoTCiOIizDOz38gYNsyw==", + "dev": true, + "requires": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "@types/scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-YIoDCTH3Af6XM5VuwGG/QL/CJqga1Zm3NkU3HZ4ZHK2fRMPYP1VczsTUqtsf43PH/iJNVlPHAo2oWX7BSdB2Hw==", "dev": true }, "@types/semver": { - "version": "7.3.13", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz", - "integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==", + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz", + "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==", "dev": true }, - "@typescript-eslint/eslint-plugin": { - "version": "5.42.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.42.0.tgz", - "integrity": "sha512-5TJh2AgL6+wpL8H/GTSjNb4WrjKoR2rqvFxR/DDTqYNk6uXn8BJMEcncLSpMbf/XV1aS0jAjYwn98uvVCiAywQ==", + "@types/simpl-schema": { + "version": "1.12.7", + "resolved": "https://registry.npmjs.org/@types/simpl-schema/-/simpl-schema-1.12.7.tgz", + "integrity": "sha512-GhXOCJqKcDeawYoIe4Jly7C5ePR3Uh3jaswb1U+Ruh1x7EtZqOJMMyxnoVvJUIl5b+v7yoFs4RVny/ZowyXBDw==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "5.42.0", - "@typescript-eslint/type-utils": "5.42.0", - "@typescript-eslint/utils": "5.42.0", + "@types/meteor": "*" + } + }, + "@types/sizzle": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.8.tgz", + "integrity": "sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg==", + "dev": true + }, + "@types/underscore": { + "version": "1.11.15", + "resolved": "https://registry.npmjs.org/@types/underscore/-/underscore-1.11.15.tgz", + "integrity": "sha512-HP38xE+GuWGlbSRq9WrZkousaQ7dragtZCruBVMi0oX1migFZavZ3OROKHSkNp/9ouq82zrWtZpg18jFnVN96g==", + "dev": true + }, + "@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", + "dev": true + }, + "@types/whatwg-url": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-8.2.2.tgz", + "integrity": "sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/webidl-conversions": "*" + } + }, + "@typescript-eslint/eslint-plugin": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", + "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", + "dev": true, + "requires": { + "@eslint-community/regexpp": "^4.4.0", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/type-utils": "5.62.0", + "@typescript-eslint/utils": "5.62.0", "debug": "^4.3.4", + "graphemer": "^1.4.0", "ignore": "^5.2.0", "natural-compare-lite": "^1.4.0", - "regexpp": "^3.2.0", "semver": "^7.3.7", "tsutils": "^3.21.0" }, @@ -209,27 +4626,18 @@ "requires": { "ms": "2.1.2" } - }, - "semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } } } }, "@typescript-eslint/parser": { - "version": "5.42.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.42.0.tgz", - "integrity": "sha512-Ixh9qrOTDRctFg3yIwrLkgf33AHyEIn6lhyf5cCfwwiGtkWhNpVKlEZApi3inGQR/barWnY7qY8FbGKBO7p3JA==", + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", + "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "5.42.0", - "@typescript-eslint/types": "5.42.0", - "@typescript-eslint/typescript-estree": "5.42.0", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", "debug": "^4.3.4" }, "dependencies": { @@ -245,23 +4653,23 @@ } }, "@typescript-eslint/scope-manager": { - "version": "5.42.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.42.0.tgz", - "integrity": "sha512-l5/3IBHLH0Bv04y+H+zlcLiEMEMjWGaCX6WyHE5Uk2YkSGAMlgdUPsT/ywTSKgu9D1dmmKMYgYZijObfA39Wow==", + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", + "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", "dev": true, "requires": { - "@typescript-eslint/types": "5.42.0", - "@typescript-eslint/visitor-keys": "5.42.0" + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0" } }, "@typescript-eslint/type-utils": { - "version": "5.42.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.42.0.tgz", - "integrity": "sha512-HW14TXC45dFVZxnVW8rnUGnvYyRC0E/vxXShFCthcC9VhVTmjqOmtqj6H5rm9Zxv+ORxKA/1aLGD7vmlLsdlOg==", + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", + "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", "dev": true, "requires": { - "@typescript-eslint/typescript-estree": "5.42.0", - "@typescript-eslint/utils": "5.42.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "@typescript-eslint/utils": "5.62.0", "debug": "^4.3.4", "tsutils": "^3.21.0" }, @@ -278,19 +4686,19 @@ } }, "@typescript-eslint/types": { - "version": "5.42.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.42.0.tgz", - "integrity": "sha512-t4lzO9ZOAUcHY6bXQYRuu+3SSYdD9TS8ooApZft4WARt4/f2Cj/YpvbTe8A4GuhT4bNW72goDMOy7SW71mZwGw==", + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", + "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "5.42.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.42.0.tgz", - "integrity": "sha512-2O3vSq794x3kZGtV7i4SCWZWCwjEtkWfVqX4m5fbUBomOsEOyd6OAD1qU2lbvV5S8tgy/luJnOYluNyYVeOTTg==", + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", + "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", "dev": true, "requires": { - "@typescript-eslint/types": "5.42.0", - "@typescript-eslint/visitor-keys": "5.42.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -306,98 +4714,100 @@ "requires": { "ms": "2.1.2" } - }, - "semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } } } }, "@typescript-eslint/utils": { - "version": "5.42.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.42.0.tgz", - "integrity": "sha512-JZ++3+h1vbeG1NUECXQZE3hg0kias9kOtcQr3+JVQ3whnjvKuMyktJAAIj6743OeNPnGBmjj7KEmiDL7qsdnCQ==", + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", + "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", "dev": true, "requires": { + "@eslint-community/eslint-utils": "^4.2.0", "@types/json-schema": "^7.0.9", "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.42.0", - "@typescript-eslint/types": "5.42.0", - "@typescript-eslint/typescript-estree": "5.42.0", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", "eslint-scope": "^5.1.1", - "eslint-utils": "^3.0.0", "semver": "^7.3.7" - }, - "dependencies": { - "eslint-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", - "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", - "dev": true, - "requires": { - "eslint-visitor-keys": "^2.0.0" - } - }, - "semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - } } }, "@typescript-eslint/visitor-keys": { - "version": "5.42.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.42.0.tgz", - "integrity": "sha512-QHbu5Hf/2lOEOwy+IUw0GoSCuAzByTAWWrOTKzTzsotiUnWFpuKnXcAhC9YztAf2EElQ0VvIK+pHJUPkM0q7jg==", + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", + "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", "dev": true, "requires": { - "@typescript-eslint/types": "5.42.0", + "@typescript-eslint/types": "5.62.0", "eslint-visitor-keys": "^3.3.0" }, "dependencies": { "eslint-visitor-keys": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", - "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true } } }, "@vue/compiler-core": { - "version": "3.2.41", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.41.tgz", - "integrity": "sha512-oA4mH6SA78DT+96/nsi4p9DX97PHcNROxs51lYk7gb9Z4BPKQ3Mh+BLn6CQZBw857Iuhu28BfMSRHAlPvD4vlw==", + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.19.tgz", + "integrity": "sha512-gj81785z0JNzRcU0Mq98E56e4ltO1yf8k5PQ+tV/7YHnbZkrM0fyFyuttnN8ngJZjbpofWE/m4qjKBiLl8Ju4w==", "dev": true, "requires": { - "@babel/parser": "^7.16.4", - "@vue/shared": "3.2.41", + "@babel/parser": "^7.23.9", + "@vue/shared": "3.4.19", + "entities": "^4.5.0", "estree-walker": "^2.0.2", - "source-map": "^0.6.1" + "source-map-js": "^1.0.2" } }, "@vue/compiler-dom": { - "version": "3.2.41", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.2.41.tgz", - "integrity": "sha512-xe5TbbIsonjENxJsYRbDJvthzqxLNk+tb3d/c47zgREDa/PCp6/Y4gC/skM4H6PIuX5DAxm7fFJdbjjUH2QTMw==", + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.19.tgz", + "integrity": "sha512-vm6+cogWrshjqEHTzIDCp72DKtea8Ry/QVpQRYoyTIg9k7QZDX6D8+HGURjtmatfgM8xgCFtJJaOlCaRYRK3QA==", "dev": true, "requires": { - "@vue/compiler-core": "3.2.41", - "@vue/shared": "3.2.41" + "@vue/compiler-core": "3.4.19", + "@vue/shared": "3.4.19" + } + }, + "@vue/reactivity": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.19.tgz", + "integrity": "sha512-+VcwrQvLZgEclGZRHx4O2XhyEEcKaBi50WbxdVItEezUf4fqRh838Ix6amWTdX0CNb/b6t3Gkz3eOebfcSt+UA==", + "dev": true, + "requires": { + "@vue/shared": "3.4.19" + } + }, + "@vue/runtime-core": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.19.tgz", + "integrity": "sha512-/Z3tFwOrerJB/oyutmJGoYbuoadphDcJAd5jOuJE86THNZji9pYjZroQ2NFsZkTxOq0GJbb+s2kxTYToDiyZzw==", + "dev": true, + "requires": { + "@vue/reactivity": "3.4.19", + "@vue/shared": "3.4.19" + } + }, + "@vue/runtime-dom": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.19.tgz", + "integrity": "sha512-IyZzIDqfNCF0OyZOauL+F4yzjMPN2rPd8nhqPP2N1lBn3kYqJpPHHru+83Rkvo2lHz5mW+rEeIMEF9qY3PB94g==", + "dev": true, + "requires": { + "@vue/runtime-core": "3.4.19", + "@vue/shared": "3.4.19", + "csstype": "^3.1.3" } }, "@vue/shared": { - "version": "3.2.41", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.41.tgz", - "integrity": "sha512-W9mfWLHmJhkfAmV+7gDjcHeAWALQtgGT3JErxULl0oz6R6+3ug91I7IErs93eCFhPCZPHBs4QJS7YWEV7A3sxw==", + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.19.tgz", + "integrity": "sha512-/KliRRHMF6LoiThEy+4c1Z4KB/gbPrGjWwJR+crg2otgrf/egKzRaCPvJ51S5oetgsgXLfc4Rm5ZgrKHZrtMSw==", "dev": true }, "abbrev": { @@ -425,6 +4835,11 @@ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true }, + "add-event-listener": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/add-event-listener/-/add-event-listener-0.0.1.tgz", + "integrity": "sha512-hjRmkeDqFUWEFcDHP/Lp0Pa4MhIJk/oQX8B7lFiNrjBKHjf0q+ivCJrucY8d8UI5d0QkZgV2jGdAGXxEZcm3nA==" + }, "agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -445,10 +4860,10 @@ "uri-js": "^4.2.2" } }, - "animejs": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/animejs/-/animejs-2.2.0.tgz", - "integrity": "sha1-Ne79/FNbgZScnLBvCz5gwC5v3IA=" + "alea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/alea/-/alea-1.0.1.tgz", + "integrity": "sha512-QU+wv+ziDXaMxRdsQg/aH7sVfWdhKps5YP97IIwFkHCsbDZA3k87JXoZ5/iuemf4ntytzIWeScrRpae8+lDrXA==" }, "ansi-colors": { "version": "4.1.1", @@ -470,16 +4885,6 @@ "color-convert": "^1.9.0" } }, - "anymatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", - "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", - "dev": true, - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, "aproba": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", @@ -539,35 +4944,6 @@ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, - "available-typed-arrays": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", - "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==" - }, - "aws-sdk": { - "version": "2.1247.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1247.0.tgz", - "integrity": "sha512-hBiVzkm5pxGchl+dn+uIApk76n3UOGuDmQBr1H2J25Ls8F7M9sNiumJby/cSjis+U+gAhl7u414SMY+ZTAJAkQ==", - "requires": { - "buffer": "4.9.2", - "events": "1.1.1", - "ieee754": "1.1.13", - "jmespath": "0.16.0", - "querystring": "0.2.0", - "sax": "1.2.1", - "url": "0.10.3", - "util": "^0.12.4", - "uuid": "8.0.0", - "xml2js": "0.4.19" - }, - "dependencies": { - "uuid": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz", - "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==" - } - } - }, "aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", @@ -602,14 +4978,15 @@ "base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true }, "bcrypt": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.0.tgz", - "integrity": "sha512-RHBS7HI5N5tEnGTmtR/pppX0mmDSBpQ4aCBsj7CEQfYXDcO74A8sIBYcJMuCsis2E81zDxeENYhv66oZwLiA+Q==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", + "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", "requires": { - "@mapbox/node-pre-gyp": "^1.0.10", + "@mapbox/node-pre-gyp": "^1.0.11", "node-addon-api": "^5.0.0" } }, @@ -628,32 +5005,10 @@ } } }, - "binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "dev": true - }, - "bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "requires": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - }, - "dependencies": { - "buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "requires": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - } - } + "bowser": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz", + "integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==" }, "brace-expansion": { "version": "1.1.11", @@ -673,14 +5028,23 @@ "fill-range": "^7.0.1" } }, - "buffer": { - "version": "4.9.2", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", - "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "bson": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/bson/-/bson-4.7.2.tgz", + "integrity": "sha512-Ry9wCtIZ5kGqkJoi6aD8KjxFZEx78guTQDnpXWiNthsxzrxAK/i8E6pCHAIZTbaEFWcOCvbecMukfK7XUvyLpQ==", + "dev": true, "requires": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4", - "isarray": "^1.0.0" + "buffer": "^5.6.0" + } + }, + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" } }, "buffer-from": { @@ -688,44 +5052,30 @@ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" }, - "call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - } - }, "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true }, - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" - }, "caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" }, "chai": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.6.tgz", - "integrity": "sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz", + "integrity": "sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==", "dev": true, "requires": { "assertion-error": "^1.1.0", - "check-error": "^1.0.2", - "deep-eql": "^3.0.1", - "get-func-name": "^2.0.0", - "loupe": "^2.3.1", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", "pathval": "^1.1.1", - "type-detect": "^4.0.5" + "type-detect": "^4.0.8" } }, "chalk": { @@ -780,25 +5130,12 @@ } }, "check-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", - "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==", - "dev": true - }, - "chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", "dev": true, "requires": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "fsevents": "~2.3.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" + "get-func-name": "^2.0.2" } }, "chownr": { @@ -811,16 +5148,6 @@ "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.4.2.tgz", "integrity": "sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A==" }, - "cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" - } - }, "clone": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", @@ -894,12 +5221,12 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, "console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" }, "core-js": { "version": "2.6.12", @@ -927,6 +5254,46 @@ "resolved": "https://registry.npmjs.org/css-box-shadow/-/css-box-shadow-1.0.0-3.tgz", "integrity": "sha512-9jaqR6e7Ohds+aWwmhe6wILJ99xYQbfmK9QQB9CcMjDbTxPZjwEmUQpU91OG05Xgm8BahT5fW+svbsQGjS/zPg==" }, + "csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true + }, + "cytoscape": { + "version": "3.28.1", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.28.1.tgz", + "integrity": "sha512-xyItz4O/4zp9/239wCcH8ZcFuuZooEeF8KHRmzjDfGdXsj3OG9MFSMA0pJE0uX3uCN/ygof6hHf4L7lst+JaDg==", + "requires": { + "heap": "^0.2.6", + "lodash": "^4.17.21" + } + }, + "cytoscape-dagre": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/cytoscape-dagre/-/cytoscape-dagre-2.5.0.tgz", + "integrity": "sha512-VG2Knemmshop4kh5fpLO27rYcyUaaDkRw+6PiX4bstpB+QFt0p2oauMrsjVbUamGWQ6YNavh7x2em2uZlzV44g==", + "requires": { + "dagre": "^0.8.5" + } + }, + "cytoscape-klay": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/cytoscape-klay/-/cytoscape-klay-3.1.4.tgz", + "integrity": "sha512-VwPj0VR25GPfy6qXVQRi/MYlZM/zkdvRhHlgqbM//lSvstgM6fhp3ik/uM8Wr8nlhskfqz/M1fIDmR6UckbS2A==", + "requires": { + "klayjs": "^0.4.1" + } + }, + "dagre": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/dagre/-/dagre-0.8.5.tgz", + "integrity": "sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==", + "requires": { + "graphlib": "^2.1.8", + "lodash": "^4.17.15" + } + }, "dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", @@ -956,33 +5323,15 @@ "ms": "2.1.2" } }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" - }, - "decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "requires": { - "mimic-response": "^3.1.0" - } - }, "deep-eql": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", - "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", + "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", "dev": true, "requires": { "type-detect": "^4.0.0" } }, - "deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" - }, "deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -997,17 +5346,12 @@ "delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" }, "detect-libc": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", - "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==" - }, - "dijkstrajs": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.2.tgz", - "integrity": "sha512-QV6PMaHTCNmKSeP6QoXhVTw9snc9VD8MulTT0Bd99Pacp4SS1cjcrYPgBPmibqKVtMJJfqC6XvOXgPMEEPH/fg==" + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", + "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==" }, "dir-glob": { "version": "3.0.1", @@ -1048,9 +5392,9 @@ } }, "dompurify": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.0.tgz", - "integrity": "sha512-Be9tbQMZds4a3C6xTmz68NlMfeONA//4dOavl/1rNw50E+/QO0KVpbcU0PcaW0nsQxurXls9ZocqFxk8R2mWEA==" + "version": "2.4.7", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.7.tgz", + "integrity": "sha512-kxxKlPEDa6Nc5WJi+qRgPbOAbgTpSULL+vI3NUXsZMlkJxTqYI9wg5ZTay2sFrdZRWHPWNi+EdAhcJf81WtoMQ==" }, "ecc-jsbn": { "version": "0.1.2", @@ -1066,19 +5410,6 @@ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, - "encode-utf8": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/encode-utf8/-/encode-utf8-1.0.3.tgz", - "integrity": "sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==" - }, - "end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "requires": { - "once": "^1.4.0" - } - }, "enquirer": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", @@ -1088,6 +5419,12 @@ "ansi-colors": "^4.1.1" } }, + "entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true + }, "escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -1184,9 +5521,9 @@ }, "dependencies": { "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true } } @@ -1316,16 +5653,6 @@ "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" }, - "events": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", - "integrity": "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==" - }, - "expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==" - }, "extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -1342,9 +5669,9 @@ "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==" }, "fast-glob": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", - "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "dev": true, "requires": { "@nodelib/fs.stat": "^2.0.2", @@ -1365,10 +5692,18 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "fast-xml-parser": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.2.5.tgz", + "integrity": "sha512-B9/wizE4WngqQftFPmdaMYlXoJlJOYxGQOanC77fq9k8+Z0v5dDSVh+3glErdIROP//s/jgb7ZuxKfB8nVyo0g==", + "requires": { + "strnum": "^1.0.5" + } + }, "fastq": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", - "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.16.0.tgz", + "integrity": "sha512-ifCoaXsDrsdkWTtiNJX5uzHDsrck5TzfKKDcuFFTIrrc/BS076qgEIfoIy1VeZqViznfKiysPYTh/QeHtnIsYA==", "dev": true, "requires": { "reusify": "^1.0.4" @@ -1392,15 +5727,6 @@ "to-regex-range": "^5.0.1" } }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, "flat-cache": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", @@ -1417,14 +5743,6 @@ "integrity": "sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw==", "dev": true }, - "for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", - "requires": { - "is-callable": "^1.1.3" - } - }, "forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", @@ -1440,17 +5758,22 @@ "mime-types": "^2.1.12" } }, - "fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" - }, "fs-minipass": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", "requires": { "minipass": "^3.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "requires": { + "yallist": "^4.0.0" + } + } } }, "fs.realpath": { @@ -1458,18 +5781,6 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, - "fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "optional": true - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, "functional-red-black-tree": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", @@ -1492,27 +5803,12 @@ "wide-align": "^1.1.2" } }, - "get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" - }, "get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", "dev": true }, - "get-intrinsic": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", - "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", - "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" - } - }, "getpass": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", @@ -1521,10 +5817,10 @@ "assert-plus": "^1.0.0" } }, - "github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" + "gintersect": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/gintersect/-/gintersect-0.1.0.tgz", + "integrity": "sha512-jps8Ckj6u8yLxOYzBVJbPqvRdeHOINQgRtufaLHkunwNQcSEdZU0ejPBapSimXJEQ9mdQW4hsEUN7DfJEcTvQQ==" }, "glob": { "version": "7.1.6", @@ -1571,12 +5867,18 @@ "slash": "^3.0.0" } }, - "gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "graphlib": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz", + "integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==", "requires": { - "get-intrinsic": "^1.1.3" + "lodash": "^4.17.15" } }, "har-schema": { @@ -1606,37 +5908,21 @@ } } }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "requires": { - "function-bind": "^1.1.1" - } - }, "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "dev": true }, - "has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" - }, - "has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", - "requires": { - "has-symbols": "^1.0.2" - } - }, "has-unicode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" + }, + "heap": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/heap/-/heap-0.2.7.tgz", + "integrity": "sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg==" }, "http-signature": { "version": "1.2.0", @@ -1658,26 +5944,21 @@ } }, "ieee754": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", - "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true }, "ignore": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", - "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==" + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==" }, "ignore-styles": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ignore-styles/-/ignore-styles-5.0.1.tgz", "integrity": "sha1-tJ7yJ0va/NikiAqWa/440aC/RnE=" }, - "immutable": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.1.0.tgz", - "integrity": "sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==", - "dev": true - }, "import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -1708,18 +5989,28 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, - "ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" - }, - "is-arguments": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", - "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "dev": true, "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "dependencies": { + "jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "dev": true + }, + "sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true + } } }, "is-arrayish": { @@ -1727,20 +6018,6 @@ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" }, - "is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "requires": { - "binary-extensions": "^2.0.0" - } - }, - "is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==" - }, "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -1752,14 +6029,6 @@ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" }, - "is-generator-function": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", - "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", - "requires": { - "has-tostringtag": "^1.0.0" - } - }, "is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -1775,28 +6044,11 @@ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true }, - "is-typed-array": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", - "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==", - "requires": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" - } - }, "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" - }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -1808,11 +6060,6 @@ "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" }, - "jmespath": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", - "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==" - }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -1866,24 +6113,10 @@ "verror": "1.10.0" } }, - "less": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/less/-/less-1.4.2.tgz", - "integrity": "sha1-t97v6Yo6h77jZEEbPfLR7+WkEtA=", - "requires": { - "mime": "1.2.x", - "mkdirp": "~0.3.4", - "request": ">=2.12.0", - "ycssmin": ">=1.0.1" - }, - "dependencies": { - "mkdirp": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz", - "integrity": "sha1-3j5fiWHIjHh+4TaN+EmsRBPsqNc=", - "optional": true - } - } + "klayjs": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/klayjs/-/klayjs-0.4.1.tgz", + "integrity": "sha512-WUNxuO7O79TEkxCj6OIaK5TJBkaWaR/IKNTakgV9PwDn+mrr63MLHed34AcE2yTaDntgO6l0zGFIzhcoTeroTA==" }, "levn": { "version": "0.4.1", @@ -1895,14 +6128,6 @@ "type-check": "~0.4.0" } }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "requires": { - "p-locate": "^4.1.0" - } - }, "lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -1922,7 +6147,7 @@ "lodash.omit": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.omit/-/lodash.omit-4.5.0.tgz", - "integrity": "sha1-brGa5aHuHdnfC5aeZs4Lf6MLXmA=" + "integrity": "sha512-XeqSp49hNGmlkj2EJlfrQFIzQ6lXdNro9sddtQzcJY8QaoC2GO0DT7xaIokHeyM+mIT0mPMlPvkYzg2xCuHdZg==" }, "lodash.template": { "version": "4.5.0", @@ -1948,12 +6173,12 @@ "dev": true }, "loupe": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz", - "integrity": "sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ==", + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", "dev": true, "requires": { - "get-func-name": "^2.0.0" + "get-func-name": "^2.0.1" } }, "lru-cache": { @@ -1973,35 +6198,23 @@ }, "dependencies": { "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" } } }, - "map-age-cleaner": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", - "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", - "dev": true, - "requires": { - "p-defer": "^1.0.0" - } - }, "marked": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/marked/-/marked-4.2.1.tgz", - "integrity": "sha512-VK1/jNtwqDLvPktNpL0Fdg3qoeUZhmRsuiIjPEy/lHwXW4ouLoZfO4XoWd4ClDt+hupV1VLpkZhEovjU0W/kqA==" + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==" }, - "mem": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/mem/-/mem-6.1.1.tgz", - "integrity": "sha512-Ci6bIfq/UgcxPTYa8dQQ5FY3BzKkT894bwXWXxC/zqs0XgMO2cT20CGkOqda7gZNkmK5VP4x89IGZ6K7hfbn3Q==", + "memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", "dev": true, - "requires": { - "map-age-cleaner": "^0.1.3", - "mimic-fn": "^3.0.0" - } + "optional": true }, "merge2": { "version": "1.4.1", @@ -2018,9 +6231,9 @@ } }, "meteor-node-stubs": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/meteor-node-stubs/-/meteor-node-stubs-1.2.5.tgz", - "integrity": "sha512-FLlOFZx3KnZ5s3yPCK+x58DyX9ewN+oQ12LcpwBXMLtzJ/YyprMQVivd6KIrahZbKJrNenPNUGuDS37WUFg+Mw==", + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/meteor-node-stubs/-/meteor-node-stubs-1.2.7.tgz", + "integrity": "sha512-20bAFUhEIOD/Cos2nmvhqf2NOKpTf63WVQ+nwuaX2OFj31sU6GL4KkNylkWum8McwsH0LsMr/F+UHhduTX7KRg==", "requires": { "assert": "^2.0.0", "browserify-zlib": "^0.2.0", @@ -2804,12 +7017,6 @@ "picomatch": "^2.3.1" } }, - "mime": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.2.11.tgz", - "integrity": "sha1-WCA+7Ybjpe8XrtK32evUfwpg3RA=", - "optional": true - }, "mime-db": { "version": "1.46.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.46.0.tgz", @@ -2823,17 +7030,6 @@ "mime-db": "1.46.0" } }, - "mimic-fn": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-3.1.0.tgz", - "integrity": "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==", - "dev": true - }, - "mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==" - }, "minify-css-string": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/minify-css-string/-/minify-css-string-1.0.0.tgz", @@ -2847,18 +7043,10 @@ "brace-expansion": "^1.1.7" } }, - "minimist": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", - "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==" - }, "minipass": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.4.tgz", - "integrity": "sha512-I9WPbWHCGu8W+6k1ZiGpPu0GkoKBeorkfKNuAFBNS1HNFJvke82sxvI5bzcCNpWPorkOO5QQ+zomzzwRxejXiw==", - "requires": { - "yallist": "^4.0.0" - } + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==" }, "minizlib": { "version": "2.1.2", @@ -2867,6 +7055,16 @@ "requires": { "minipass": "^3.0.0", "yallist": "^4.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "requires": { + "yallist": "^4.0.0" + } + } } }, "mkdirp": { @@ -2874,16 +7072,61 @@ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" }, - "mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" - }, "mongo-object": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/mongo-object/-/mongo-object-0.1.4.tgz", "integrity": "sha512-QtYk0gupWEn2+iB+DDRt1L+WbcNYvJRaHdih/dcqthOa1DbnREUGSs2WGcW478GNYpElflo/yybZXu0sTiRXHg==" }, + "mongodb": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.17.2.tgz", + "integrity": "sha512-mLV7SEiov2LHleRJPMPrK2PMyhXFZt2UQLC4VD4pnth3jMjYKHhtqfwwkkvS/NXuo/Fp3vbhaNcXrIDaLRb9Tg==", + "dev": true, + "requires": { + "@aws-sdk/credential-providers": "^3.186.0", + "@mongodb-js/saslprep": "^1.1.0", + "bson": "^4.7.2", + "mongodb-connection-string-url": "^2.6.0", + "socks": "^2.7.1" + } + }, + "mongodb-connection-string-url": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz", + "integrity": "sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==", + "dev": true, + "requires": { + "@types/whatwg-url": "^8.2.1", + "whatwg-url": "^11.0.0" + }, + "dependencies": { + "tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "dev": true, + "requires": { + "punycode": "^2.1.1" + } + }, + "webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true + }, + "whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "dev": true, + "requires": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + } + } + } + }, "moo": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz", @@ -2894,11 +7137,6 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, - "napi-build-utils": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", - "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==" - }, "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -2922,11 +7160,89 @@ "randexp": "0.4.6" } }, + "ngraph.centrality": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/ngraph.centrality/-/ngraph.centrality-0.3.0.tgz", + "integrity": "sha512-Qmu9dDHJAx+GAW2AMqmhaub1rINS+fHZGZJ3zPI36ENAXmVNQ/Jkq79br1sg6NUHz/pRBT9MXMuwDyYKmMt8Mw==" + }, "ngraph.events": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ngraph.events/-/ngraph.events-1.2.1.tgz", "integrity": "sha512-D4C+nXH/RFxioGXQdHu8ELDtC6EaCiNsZtih0IvyGN81OZSUby4jXoJ5+RNWasfsd0FnKxxpAROyUMzw64QNsw==" }, + "ngraph.expose": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/ngraph.expose/-/ngraph.expose-0.0.0.tgz", + "integrity": "sha512-Hr88MuhgoSLVGf2aaaXcKl22Rn95duWsjRcoeJMP9PtFmYHGFw/3ctDqBf5phnIyktm0P/Quxs5EGg6xgJcZAQ==" + }, + "ngraph.forcelayout": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/ngraph.forcelayout/-/ngraph.forcelayout-0.5.0.tgz", + "integrity": "sha512-qOd1S9unFLw313+l0M/Dk1MePLDUSl4h9RyOtAbo0CyeefnN4PICiRz0LOewR5WuFmQD0/RmZLpjTKu0H7LTKQ==", + "requires": { + "ngraph.events": "0.0.4", + "ngraph.physics.simulator": "^0.3.0" + }, + "dependencies": { + "ngraph.events": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/ngraph.events/-/ngraph.events-0.0.4.tgz", + "integrity": "sha512-SY7MdNQoy5KyaVxg03PYCnGF6J7l4p8lEdmYm/5oIqFAmLhg0BmzZzlRqobJ0nEPT6xZlonUQbvCcXtarPZNrg==" + } + } + }, + "ngraph.fromjson": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/ngraph.fromjson/-/ngraph.fromjson-0.1.9.tgz", + "integrity": "sha512-f3GLjbUq239wx4s5A0fDptj9dcNeaEIJU3gm74hWvYK7onD7sFtedP7jVHZA7UJ2FwkKgEhzbPeltv92ycuKZQ==", + "requires": { + "ngraph.graph": "0.0.14" + }, + "dependencies": { + "ngraph.events": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/ngraph.events/-/ngraph.events-0.0.3.tgz", + "integrity": "sha512-UrewofHOFk/05otBm9GD4DA3PTEY/yaElhCclmGC4IcmAYaSDRrC3lENQxJ00AzeBnz1GY2xH7Ct7AfIdhsdWA==" + }, + "ngraph.graph": { + "version": "0.0.14", + "resolved": "https://registry.npmjs.org/ngraph.graph/-/ngraph.graph-0.0.14.tgz", + "integrity": "sha512-ERTLng4KrsGbR7iLZFvg5H+zJ7V+SY8RDqZKYCnOZib5W8M5LCvcil9/8eiJcTRUIPPXW3j8hqPCdLnBvgsn/A==", + "requires": { + "ngraph.events": "0.0.3" + } + } + } + }, + "ngraph.generators": { + "version": "0.0.19", + "resolved": "https://registry.npmjs.org/ngraph.generators/-/ngraph.generators-0.0.19.tgz", + "integrity": "sha512-P3XqB1sH4zrzM6bMGTtuT/6K76Rnhf1qE8Zu7PkAvhQVCQzdLYiL2/8DwhcPLsetRJHNJv0uwpW9TpntBAqKrw==", + "requires": { + "ngraph.graph": "0.0.14", + "ngraph.random": "0.1.0" + }, + "dependencies": { + "ngraph.events": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/ngraph.events/-/ngraph.events-0.0.3.tgz", + "integrity": "sha512-UrewofHOFk/05otBm9GD4DA3PTEY/yaElhCclmGC4IcmAYaSDRrC3lENQxJ00AzeBnz1GY2xH7Ct7AfIdhsdWA==" + }, + "ngraph.graph": { + "version": "0.0.14", + "resolved": "https://registry.npmjs.org/ngraph.graph/-/ngraph.graph-0.0.14.tgz", + "integrity": "sha512-ERTLng4KrsGbR7iLZFvg5H+zJ7V+SY8RDqZKYCnOZib5W8M5LCvcil9/8eiJcTRUIPPXW3j8hqPCdLnBvgsn/A==", + "requires": { + "ngraph.events": "0.0.3" + } + }, + "ngraph.random": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/ngraph.random/-/ngraph.random-0.1.0.tgz", + "integrity": "sha512-KXCfzk/ZB79BxQSWMvYPGayx3Mb+7n5GPnc8SW0rwysqRV/3QxEKrLU/UVC8eGjc2SYGofqX+uhUE6IXfqR5VA==" + } + } + }, "ngraph.graph": { "version": "19.1.0", "resolved": "https://registry.npmjs.org/ngraph.graph/-/ngraph.graph-19.1.0.tgz", @@ -2935,23 +7251,63 @@ "ngraph.events": "^1.2.1" } }, - "ngraph.path": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/ngraph.path/-/ngraph.path-1.4.0.tgz", - "integrity": "sha512-yJZay4tP0wcjqkkf8zlMQ/T+JOgU+EWfdE4w4TG8OS94B12J/+Z44UOYxVJErE8E6/wFunX1hMZEB1/GHsBYHg==" + "ngraph.merge": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/ngraph.merge/-/ngraph.merge-0.0.1.tgz", + "integrity": "sha512-iXchI5xMjYzA96mee//O7I7gtd4cCakWaSTu11aMTxRDbvBK2qpDDytYg58jO3usAUkjFxBdy1gxYppKmBDuRQ==" }, - "node-abi": { - "version": "3.26.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.26.0.tgz", - "integrity": "sha512-jRVtMFTChbi2i/jqo/i2iP9634KMe+7K1v35mIdj3Mn59i5q27ZYhn+sW6npISM/PQg7HrP2kwtRBMmh5Uvzdg==", + "ngraph.path": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/ngraph.path/-/ngraph.path-1.5.0.tgz", + "integrity": "sha512-2IdmqfBYq2zbGHtpmskdWF6x/nIWZkhfs1taMgg2waBJRn4xNqe7gBiRtD1YS5ZcKhp0trK+Gw94Rli2emMs1Q==" + }, + "ngraph.physics.primitives": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/ngraph.physics.primitives/-/ngraph.physics.primitives-0.0.7.tgz", + "integrity": "sha512-7jPm14fYcuJ9kytOVNOKxFy6r/Uu9Dnj++uT3iR9XkBcsBahn2xcYJkV6vF1bIb1fQ5XrDCRjRIOcMwEum6jwQ==" + }, + "ngraph.physics.simulator": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/ngraph.physics.simulator/-/ngraph.physics.simulator-0.3.0.tgz", + "integrity": "sha512-ObW+HL+hQBIIdc6xG/+qrLe8qv+Sf0X3lq/l2hsjFrIwWtpRKLrSvUUoXiNIeFqRmY/C+PkGo3U+XY523lJ+Fw==", "requires": { - "semver": "^7.3.5" + "ngraph.events": "0.0.3", + "ngraph.expose": "0.0.0", + "ngraph.merge": "0.0.1", + "ngraph.physics.primitives": "0.0.7", + "ngraph.quadtreebh": "0.0.4", + "ngraph.random": "0.0.1" + }, + "dependencies": { + "ngraph.events": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/ngraph.events/-/ngraph.events-0.0.3.tgz", + "integrity": "sha512-UrewofHOFk/05otBm9GD4DA3PTEY/yaElhCclmGC4IcmAYaSDRrC3lENQxJ00AzeBnz1GY2xH7Ct7AfIdhsdWA==" + } } }, + "ngraph.quadtreebh": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/ngraph.quadtreebh/-/ngraph.quadtreebh-0.0.4.tgz", + "integrity": "sha512-xTIkWGXt5Ajnoq9VOr0xDOI9ZL+q4sPhD0Z7vxvn4MCa+l0wf43rg0C7qv0t+RIOgbQBAp0xDpn568hpXAckJA==", + "requires": { + "ngraph.random": "0.0.1" + } + }, + "ngraph.random": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/ngraph.random/-/ngraph.random-0.0.1.tgz", + "integrity": "sha512-QPKU7ChXF/VrvMQxVo9aWcvXCXp98VfL4nKUteTW/olDqeUqQ61t7m+jvFb8Dj7kKvlKlnsbDA1aWLJGmm17XA==" + }, + "ngraph.tojson": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/ngraph.tojson/-/ngraph.tojson-0.1.4.tgz", + "integrity": "sha512-Ii2BTqi8zBRMLH8vTc8pMUKQFJaqbgttG9DKUaazoPVpwC/ww4jyTOHe2ZKaGGZRepnGLqSZ27wZUm7n8MjIgA==" + }, "node-addon-api": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.0.0.tgz", - "integrity": "sha512-CvkDw2OEnme7ybCykJpVcKH+uAOLV2qLqiyla128dN9TkEWfrYmxG6C2boDe5KcNQqZF3orkqzGgOMvZ/JNekA==" + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==" }, "node-fetch": { "version": "2.6.7", @@ -2969,12 +7325,6 @@ "abbrev": "1" } }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true - }, "npmlog": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", @@ -2994,7 +7344,7 @@ "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" }, "once": { "version": "1.4.0", @@ -3018,33 +7368,6 @@ "word-wrap": "^1.2.3" } }, - "p-defer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", - "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", - "dev": true - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "requires": { - "p-limit": "^2.2.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" - }, "parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -3054,11 +7377,6 @@ "callsites": "^3.0.0" } }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" - }, "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -3093,30 +7411,6 @@ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true }, - "pngjs": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", - "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==" - }, - "prebuild-install": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", - "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==", - "requires": { - "detect-libc": "^2.0.0", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^1.0.1", - "node-abi": "^3.3.0", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - } - }, "prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -3124,9 +7418,9 @@ "dev": true }, "pretty-bytes": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.0.0.tgz", - "integrity": "sha512-6UqkYefdogmzqAZWzJ7laYeJnaXDy2/J+ZqiiMtS7t7OfpXWTlaeGMwX8U6EFvPV/YWWEKRkS8hKS4k60WHTOg==" + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz", + "integrity": "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==" }, "prism-media": { "version": "1.3.1", @@ -3144,45 +7438,20 @@ "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" }, - "qrcode": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.1.tgz", - "integrity": "sha512-nS8NJ1Z3md8uTjKtP+SGGhfqmTCs5flU/xR623oI0JX+Wepz9R8UrRVCTBTJm3qGw3rH6jJ6MUHjkDx15cxSSg==", - "requires": { - "dijkstrajs": "^1.0.1", - "encode-utf8": "^1.0.3", - "pngjs": "^5.0.0", - "yargs": "^15.3.1" - } - }, "qrcode.vue": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/qrcode.vue/-/qrcode.vue-1.7.0.tgz", "integrity": "sha512-R7t6Y3fDDtcU7L4rtqwGUDP9xD64gJhIwpfjhRCTKmBoYF6SS49PIJHRJ048cse6OI7iwTwgyy2C46N9Ygoc6g==" }, "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" - }, - "querystring": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==" + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==" }, "queue-microtask": { "version": "1.2.3", @@ -3204,47 +7473,20 @@ "ret": "~0.1.10" } }, - "rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "dependencies": { - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" - } - } - }, "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "requires": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, - "readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "requires": { - "picomatch": "^2.2.1" - } - }, "regenerator-runtime": { - "version": "0.13.10", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.10.tgz", - "integrity": "sha512-KepLsg4dU12hryUO7bp/axHAKvwGOCV0sGloQtpagJ12ai+ojVDqkeGSiRX1zlq+kjIMZ1t7gpze+26QqtdGqw==" + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, "regexpp": { "version": "3.2.0", @@ -3279,22 +7521,12 @@ "uuid": "^3.3.2" } }, - "require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" - }, "require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true }, - "require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" - }, "requireindex": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.2.0.tgz", @@ -3345,26 +7577,10 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, - "sass": { - "version": "1.56.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.56.0.tgz", - "integrity": "sha512-WFJ9XrpkcnqZcYuLRJh5qiV6ibQOR4AezleeEjTjMsCocYW59dEG19U3fwTTXxzi2Ed3yjPBp727hbbj53pHFw==", - "dev": true, - "requires": { - "chokidar": ">=3.0.0 <4.0.0", - "immutable": "^4.0.0", - "source-map-js": ">=0.6.2 <2.0.0" - } - }, - "sax": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", - "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==" - }, "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "requires": { "lru-cache": "^6.0.0" } @@ -3372,7 +7588,7 @@ "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" }, "setimmediate": { "version": "1.0.5", @@ -3380,28 +7596,32 @@ "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" }, "sharp": { - "version": "0.30.7", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.30.7.tgz", - "integrity": "sha512-G+MY2YW33jgflKPTXXptVO28HvNOo9G3j0MybYAHeEmby+QuD2U98dT6ueht9cv/XDqZspSpIhoSW+BAKJ7Hig==", + "version": "0.33.2", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.2.tgz", + "integrity": "sha512-WlYOPyyPDiiM07j/UO+E720ju6gtNtHjEGg5vovUk1Lgxyjm2LFO+37Nt/UI3MMh2l6hxTWQWi7qk3cXJTutcQ==", "requires": { + "@img/sharp-darwin-arm64": "0.33.2", + "@img/sharp-darwin-x64": "0.33.2", + "@img/sharp-libvips-darwin-arm64": "1.0.1", + "@img/sharp-libvips-darwin-x64": "1.0.1", + "@img/sharp-libvips-linux-arm": "1.0.1", + "@img/sharp-libvips-linux-arm64": "1.0.1", + "@img/sharp-libvips-linux-s390x": "1.0.1", + "@img/sharp-libvips-linux-x64": "1.0.1", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.1", + "@img/sharp-libvips-linuxmusl-x64": "1.0.1", + "@img/sharp-linux-arm": "0.33.2", + "@img/sharp-linux-arm64": "0.33.2", + "@img/sharp-linux-s390x": "0.33.2", + "@img/sharp-linux-x64": "0.33.2", + "@img/sharp-linuxmusl-arm64": "0.33.2", + "@img/sharp-linuxmusl-x64": "0.33.2", + "@img/sharp-wasm32": "0.33.2", + "@img/sharp-win32-ia32": "0.33.2", + "@img/sharp-win32-x64": "0.33.2", "color": "^4.2.3", - "detect-libc": "^2.0.1", - "node-addon-api": "^5.0.0", - "prebuild-install": "^7.1.1", - "semver": "^7.3.7", - "simple-get": "^4.0.1", - "tar-fs": "^2.1.1", - "tunnel-agent": "^0.6.0" - }, - "dependencies": { - "semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", - "requires": { - "lru-cache": "^6.0.0" - } - } + "detect-libc": "^2.0.2", + "semver": "^7.5.4" } }, "shebang-command": { @@ -3434,29 +7654,22 @@ "mongo-object": "^0.1.4" } }, - "simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==" - }, - "simple-get": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "requires": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, "simple-swizzle": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", "requires": { "is-arrayish": "^0.3.1" } }, + "simplesvg": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/simplesvg/-/simplesvg-0.0.10.tgz", + "integrity": "sha512-iCVx1A/kI4U3cGPRMRQaGLbIFNDXuB8rsaAsO2mM5wYFDs/MrfmHhrSCqNbOylgt9MhhZU3uMsSQnZM853kwXQ==", + "requires": { + "add-event-listener": "0.0.1" + } + }, "slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -3506,6 +7719,22 @@ } } }, + "smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true + }, + "socks": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.1.tgz", + "integrity": "sha512-B6w7tkwNid7ToxjZ08rQMT8M9BJAf8DKx8Ft4NivzH0zBUfd6jldGcisJn/RLgxcX3FPNDdNQCUEMMT79b+oCQ==", + "dev": true, + "requires": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + } + }, "sortablejs": { "version": "1.10.2", "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.10.2.tgz", @@ -3531,6 +7760,16 @@ "source-map": "^0.6.0" } }, + "sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "dev": true, + "optional": true, + "requires": { + "memory-pager": "^1.0.2" + } + }, "speakingurl": { "version": "14.0.1", "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", @@ -3604,13 +7843,10 @@ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true }, - "styles": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/styles/-/styles-0.2.1.tgz", - "integrity": "sha1-hJJ7pEf6pvJJ7NIK3wu4X606UUE=", - "requires": { - "less": "~1.4.0" - } + "strnum": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", + "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==" }, "supports-color": { "version": "5.5.0", @@ -3687,54 +7923,29 @@ } }, "tar": { - "version": "6.1.11", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", - "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", + "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==", "requires": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", - "minipass": "^3.0.0", + "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, - "tar-fs": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", - "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", - "requires": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - }, - "dependencies": { - "chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" - } - } - }, - "tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "requires": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - } - }, "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "thumbhash": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/thumbhash/-/thumbhash-0.1.1.tgz", + "integrity": "sha512-kH5pKeIIBPQXAOni2AiY/Cu/NKdkFREdpH+TLdM0g6WA7RriCv0kPLgP731ady67MhTAqrVG/4mnEeibVuCJcg==" + }, "to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -3759,10 +7970,9 @@ "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" }, "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "tsutils": { "version": "3.21.0", @@ -3771,6 +7981,14 @@ "dev": true, "requires": { "tslib": "^1.8.1" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } } }, "tunnel-agent": { @@ -3808,15 +8026,16 @@ "dev": true }, "typescript": { - "version": "4.8.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", - "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", + "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", "dev": true }, - "underscore": { - "version": "1.13.6", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", - "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==" + "undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true }, "uri-js": { "version": "4.2.2", @@ -3826,38 +8045,10 @@ "punycode": "^2.1.0" } }, - "url": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", - "integrity": "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==", - "requires": { - "punycode": "1.3.2", - "querystring": "0.2.0" - }, - "dependencies": { - "punycode": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", - "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==" - } - } - }, - "util": { - "version": "0.12.5", - "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", - "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", - "requires": { - "inherits": "^2.0.3", - "is-arguments": "^1.0.4", - "is-generator-function": "^1.0.7", - "is-typed-array": "^1.1.3", - "which-typed-array": "^1.1.2" - } - }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "uuid": { "version": "3.4.0", @@ -3880,10 +8071,43 @@ "extsprintf": "^1.2.0" } }, + "vivagraphjs": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/vivagraphjs/-/vivagraphjs-0.12.0.tgz", + "integrity": "sha512-Air+vUHXAWj8NTWUnbU800yKC7SiHpCVwpKIPfDtr5436YoMd7cpg8blt6Fn9xarx+sz1osRxGHBHTaHvcsR6Q==", + "requires": { + "gintersect": "0.1.0", + "ngraph.centrality": "0.3.0", + "ngraph.events": "0.0.3", + "ngraph.forcelayout": "0.5.0", + "ngraph.fromjson": "0.1.9", + "ngraph.generators": "0.0.19", + "ngraph.graph": "0.0.14", + "ngraph.merge": "0.0.1", + "ngraph.random": "0.0.1", + "ngraph.tojson": "0.1.4", + "simplesvg": "0.0.10" + }, + "dependencies": { + "ngraph.events": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/ngraph.events/-/ngraph.events-0.0.3.tgz", + "integrity": "sha512-UrewofHOFk/05otBm9GD4DA3PTEY/yaElhCclmGC4IcmAYaSDRrC3lENQxJ00AzeBnz1GY2xH7Ct7AfIdhsdWA==" + }, + "ngraph.graph": { + "version": "0.0.14", + "resolved": "https://registry.npmjs.org/ngraph.graph/-/ngraph.graph-0.0.14.tgz", + "integrity": "sha512-ERTLng4KrsGbR7iLZFvg5H+zJ7V+SY8RDqZKYCnOZib5W8M5LCvcil9/8eiJcTRUIPPXW3j8hqPCdLnBvgsn/A==", + "requires": { + "ngraph.events": "0.0.3" + } + } + } + }, "vue": { - "version": "2.6.10", - "resolved": "https://registry.npmjs.org/vue/-/vue-2.6.10.tgz", - "integrity": "sha512-ImThpeNU9HbdZL3utgMCq0oiMzAkt1mcgy3/E6zWC/G6AaQoeuFdsl9nDhTDU3X1R6FK7nsIUuRACVcjI+A2GQ==" + "version": "2.6.14", + "resolved": "https://registry.npmjs.org/vue/-/vue-2.6.14.tgz", + "integrity": "sha512-x2284lgYvjOMj3Za7kqzRcUSxBboHqtgRE2zlos1qWaOye5yUmHn42LB1250NJBLRwEcdrB0JRwyPTEPhfQjiQ==" }, "vue-eslint-parser": { "version": "7.11.0", @@ -3918,9 +8142,9 @@ } }, "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true } } @@ -3952,9 +8176,9 @@ } }, "vuetify": { - "version": "2.6.12", - "resolved": "https://registry.npmjs.org/vuetify/-/vuetify-2.6.12.tgz", - "integrity": "sha512-qe3hcMpWmT1O15tp+p65lOS7UKZ/hQYQktCsw9iXx2u3RwVbX6GR82gY2iROrKsiAzYDvMgrYxWQwY/pUfkekw==" + "version": "2.6.15", + "resolved": "https://registry.npmjs.org/vuetify/-/vuetify-2.6.15.tgz", + "integrity": "sha512-2a6sBSHzivXgi9pZMyHuzTgMyInCkj/BrVwTnoCa1Y/Dnfwj7lkWzgKQDScbGVK0q4vJ+YHoBBrLOmnhz1R0YA==" }, "vuetify-upload-button": { "version": "2.0.2", @@ -3989,24 +8213,6 @@ "isexe": "^2.0.0" } }, - "which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" - }, - "which-typed-array": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", - "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==", - "requires": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0", - "is-typed-array": "^1.1.10" - } - }, "wide-align": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", @@ -4016,44 +8222,11 @@ } }, "word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true }, - "wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "requires": { - "color-convert": "^2.0.1" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - } - } - }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -4064,62 +8237,10 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.3.tgz", "integrity": "sha512-kQ/dHIzuLrS6Je9+uv81ueZomEwH0qVYstcAQ4/Z93K8zeko9gtAbttJWzoC5ukqXY1PpoouV3+VSOqEAFt5wg==" }, - "xml2js": { - "version": "0.4.19", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", - "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", - "requires": { - "sax": ">=0.6.0", - "xmlbuilder": "~9.0.1" - } - }, - "xmlbuilder": { - "version": "9.0.7", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", - "integrity": "sha512-7YXTQc3P2l9+0rjaUbLwMKRhtmwg1M1eDf6nag7urC7pIPYLD9W/jmzQ4ptRSUbodw5S0jfoGTflLemQibSpeQ==" - }, - "y18n": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" - }, "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, - "yargs": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", - "requires": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" - } - }, - "yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - }, - "ycssmin": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ycssmin/-/ycssmin-1.0.1.tgz", - "integrity": "sha1-fN3o23jPqwDSkBw7IwHjBPr03xY=", - "optional": true } } } diff --git a/app/package.json b/app/package.json index 0f39f729..65a82bac 100644 --- a/app/package.json +++ b/app/package.json @@ -1,17 +1,21 @@ { "name": "dicecloud", - "version": "2.0.43", - "description": "Unofficial Online Realtime D&D 5e App", + "version": "2.1.0", + "description": "Online Realtime TTRPG Engine", "license": "GPL-3.0", "repository": { "type": "git", "url": "https://github.com/ThaumRystra/DiceCloud" }, - "author": "Stefan Zermatten", + "author": "Thaum Rystra", "scripts": { - "run": "meteor", + "serve": "meteor run --raw-logs --settings settings.json", "debug": "meteor --inspect", - "test": "meteor test --driver-package meteortesting:mocha --port 3001", + "bundle-viz": "meteor --extra-packages bundle-visualizer --production", + "lint": "eslint .", + "test": "meteor test --driver-package meteortesting:mocha --port 3001 --raw-logs", + "test:coverage": "COVERAGE=1 COVERAGE_OUT_LCOVONLY=1 COVERAGE_OUT_REMAP=1 COVERAGE_APP_FOLDER=$PWD/ meteor test --once --raw-logs --driver-package meteortesting:mocha", + "test:watch:coverage": "COVERAGE=1 COVERAGE_OUT_LCOVONLY=1 COVERAGE_OUT_REMAP=1 COVERAGE_APP_FOLDER=$PWD/ TEST_WATCH=1 meteor test --raw-logs --driver-package meteortesting:mocha", "build": "meteor build ../build --architecture os.linux.x86_64" }, "engines": { @@ -19,59 +23,66 @@ "npm": "6.13.x" }, "dependencies": { - "@babel/runtime": "^7.20.1", + "@aws-sdk/client-s3": "^3.523.0", + "@babel/runtime": "^7.23.9", "@chenfengyuan/vue-countdown": "^1.1.5", "@tozd/vue-observer-utils": "^0.5.0", - "animejs": "^2.2.0", - "aws-sdk": "^2.1247.0", - "bcrypt": "^5.1.0", + "alea": "^1.0.1", + "bcrypt": "^5.1.1", "chroma-js": "^2.4.2", - "core-js": "^2.6.11", "css-box-shadow": "^1.0.0-3", + "cytoscape": "^3.28.1", + "cytoscape-dagre": "^2.5.0", + "cytoscape-klay": "^3.1.4", + "dagre": "^0.8.5", "date-fns": "^1.30.1", "ddp-rate-limiter-mixin": "^1.1.10", "discord.js": "^12.5.3", - "dompurify": "^2.4.0", - "ignore": "^5.2.0", + "dompurify": "^2.4.7", + "ignore": "^5.3.1", "ignore-styles": "^5.0.1", "lodash": "^4.17.20", - "marked": "^4.2.1", - "meteor-node-stubs": "^1.2.5", + "marked": "^4.3.0", + "meteor-node-stubs": "^1.2.7", "minify-css-string": "^1.0.0", "moo": "^0.5.2", "nearley": "^2.19.1", "ngraph.graph": "^19.1.0", - "ngraph.path": "^1.4.0", - "pretty-bytes": "^6.0.0", - "qrcode": "^1.5.1", + "ngraph.path": "^1.5.0", + "pretty-bytes": "^6.1.1", "qrcode.vue": "^1.7.0", "request": "^2.88.2", - "sharp": "^0.30.7", + "sharp": "^0.33.2", "simpl-schema": "^1.13.1", "source-map-support": "^0.5.21", "speakingurl": "^14.0.1", - "styles": "^0.2.1", - "underscore": "^1.13.6", - "vue": "2.6.10", + "thumbhash": "^0.1.1", + "vivagraphjs": "^0.12.0", + "vue": "2.6.14", "vue-meteor-tracker": "^2.0.0", "vue-reactive-provide": "^0.3.0", "vue-router": "^3.6.5", "vuedraggable": "^2.23.2", - "vuetify": "^2.6.12", + "vuetify": "2.6.15", "vuetify-upload-button": "^2.0.2", "vuex": "^3.1.3" }, "devDependencies": { - "@typescript-eslint/eslint-plugin": "^5.42.0", - "@typescript-eslint/parser": "^5.42.0", - "@vue/compiler-dom": "^3.2.41", - "chai": "^4.3.6", + "@types/chai": "^4.3.11", + "@types/lodash": "^4.14.202", + "@types/meteor": "^2.9.8", + "@types/meteor-mdg-validated-method": "^1.2.10", + "@types/mocha": "^10.0.6", + "@types/simpl-schema": "^1.12.7", + "@typescript-eslint/eslint-plugin": "^5.62.0", + "@typescript-eslint/parser": "^5.62.0", + "@vue/compiler-dom": "^3.4.19", + "@vue/runtime-dom": "^3.4.19", + "chai": "^4.4.1", "eslint": "^7.32.0", "eslint-plugin-vue": "^7.20.0", "eslint-plugin-vuetify": "^1.1.0", - "mem": "^6.1.1", - "sass": "^1.56.0", - "typescript": "^4.8.4" + "typescript": "^5.3.3" }, "eslintConfig": { "extends": [ @@ -121,7 +132,9 @@ "quotes": [ "error", "single" - ] + ], + "@typescript-eslint/no-this-alias": "off", + "@typescript-eslint/no-explicit-any": "off" } } } diff --git a/app/private/docs/computed-fields.md b/app/private/docs/computed-fields.md deleted file mode 100644 index 6ca1bef5..00000000 --- a/app/private/docs/computed-fields.md +++ /dev/null @@ -1,81 +0,0 @@ -# Computed fields - -Some fields in DiceCloud creature properties expect calculations. These fields are then computed by the DiceCloud engine. - -Some fields, like the value of an attirbute, resolve down to a single number, while others, like the damage to deal in an attack, only simplify their calculation as far as they can, and then resolve down to a number when applied. Avoid adding dice rolls to calculations that expect to resolve down to a number, because they will re-roll every time the creature is recalculated, causing instability in the creature's stats. - -## Parser - -The DiceCloud parser can understand the following syntax: - -| | | -| :- | :- | -| **Numbers** | `13`, `3.14` | -| **Dice rolls** | `3d6`, `(1 + 2)d4`| -| **Strings of text** | `'Some text'`, `"some other text"` | -| **Boolean values** | `true` or `false`. When DiceCloud expects a boolean, `0`, an empty string `''` and `false` are all considered false by DiceCloud's engine, every other value is considered true. | -| **Variable names** | `variableName` | -| **Addition and subtraction** | `1 + 2 + 3`, `12 - 6` | -| **Multiplication** | `6 * 4`, `12 * 2` = `24` | -| **Exponents** | `3 ^ 2` Raise 3 to the power of 2 | -| **Modulo** | Returns the remainder of a division operation `15 % 6` = `3` | -| **AND** | `&` or `&&`: Returns the value of the right hand side if the left side is true `true & 'cat'` = `'cat'` | -| **OR** | `|` or `||`: Returns the left hand side if it is true, otherwise returns the right hand side `'dog' || 'cat'` = `'dog'` | -| **NOT** | `!` returns false if the value after it is true, otherwise returns false | -| **Comparisons** | greater than: `>`, less than: `<`, greater than or equal to: `>=`, less than or equal to: `<=`, equal: `=` or `==` or `===`, not equal: `!=` or `!==` | -| **If-else** | `condition ? resultIfTrue : resultIfFalse`, `level > 10 ? 'high tier' : 'low tier'` | -| **Arrays** | lists of values `[3, 6, 9, 12]`. | -| **Array Indexes** | A value can be chosen from an array using another set of square brackets: `[3, 6, 9, 12][2]` = `[6]` because `[2]` fetches the 2nd value in the array. Arrays start at 1 in DiceCloud so that level tables can have 20 entries and be accessed by `array[level]`. | -| **Function calls** | `functionName(argument1, argument1)` See [Functions](/docs/functions) for a full list of available functions. | - -## Special variables - -### Built-in variables - -These variables are added to the creature automatically when relevant. They can be overriden if needed by creating a property with the same variable name. They can also be targetted by effects. - -- `xp` A total of all the experiences with xp added to the character sheet -- `milestoneLevels` A total of all the experiences with milestone levels added to the character sheet -- `itemsAttuned` Number of items the creature is attuned to -- `weightEquipment` Total weight of all equipment on the creature -- `valueEquipment` Total value of all equipment on the creature -- `weightTotal` Total weight of the creature's entire inventory -- `valueTotal` Total value of the creature's entire inventory -- `weightCarried` Total weight of all carried items and containers -- `valueCarried` Total value of all carried items and containers -- `level` The current level of the creature, including all class levels -- `criticalHitTarget` Defaults to 20, the natural roll needed to consider an attack roll as a critical hit - -### Action variables - -These variables are available during an action after the relevant property has been applied. - -For Advanced users, a [Roll](/docs/property/roll) can set these variables, overriding the default behavior. - -#### [Actions](/docs/property/action) - -- `$attackDiceRoll` The value of the d20 roll before any modifiers were applied. -- `$attackRoll` The total attack roll after modifiers. -- `$criticalHit` Set to `true` if the attack roll's d20 is a natural 20. If `criticalHitTarget` is set, the attack roll's d20 must instead be equal to or greater than `criticalHitTarget` for this to be set to `true`. -- `$criticalMiss` Set to `true` if the attack roll was not a critical hit and rolled a natural 1 on the d20 roll. -- `$attackHit` If the attack roll is higher than or equal to the target's AC or a critical hit this is set to `true`. Remains unset if there is no target for the attack unless the attack is a critical hit. -- `$attackMiss` If the attack roll is lower than the target's AC or a critical miss, this is set to `true`. Remains unset if there is no target for the attack unless the attack is a critical miss. - -#### [Damage](/docs/property/damage) - -- `$lastDamageType` The type of damage dealt last, any damage that has the `extra` type will use this damage type instead - -#### [Saving throws](/docs/property/saving-throw) - -- `$saveFailed` Set to `true` if the target failed its saving throw or there are no targets for the saving throw -- `$saveSucceeded` Set to `true` if the target made its saving throw or there are no targets for the saving throw -- `$saveDiceRoll` The unmodified d20 roll the target made to save -- `$saveRoll` The final value of the saving throw roll after modifiers - -## Ancestor references - -The ancestors of a property can be accessed directly using the `#ancestorType` syntax. - -For example, a spell might need to know the save DC of the spell list that it is inside of, it can use `#spellList.dc`. - -Triggers and their children work differently: They don't have access to their own ancestors, but rather inherit the ancestors of the property that caused them to fire. For example, a trigger at the root of the creature's tree might be fired by a spell being cast, you can still use references to ancestors like `#spellList.attackRollBonus` inside that trigger as if it were under the spell itself. diff --git a/app/private/docs/defaultDocs.json b/app/private/docs/defaultDocs.json new file mode 100644 index 00000000..96328366 --- /dev/null +++ b/app/private/docs/defaultDocs.json @@ -0,0 +1,746 @@ +[ + { + "_id": "ioei4uvDdGTAFqZrB", + "name": "Properties", + "left": 1, + "right": 1, + "urlName": "property", + "root": { + "id": "DDDDDDDDDDDDDDDDD", + "collection": "docs" + }, + "href": "/docs/property", + "published": true, + "description": "Properties are all the things you can add to a character, like ability scores, actions, spells, and items.", + "icon": { + "name": "skills", + "shape": "M119.1 25v.1c-25 3.2-47.1 32-47.1 68.8 0 20.4 7.1 38.4 17.5 50.9L99.7 157 84 159.9c-13.7 2.6-23.8 9.9-32.2 21.5-8.5 11.5-14.9 27.5-19.4 45.8-8.2 33.6-9.9 74.7-10.1 110.5h44l11.9 158.4h96.3L185 337.7h41.9c0-36.2-.3-77.8-7.8-111.7-4-18.5-10.2-34.4-18.7-45.9-8.6-11.4-19.2-18.7-34.5-21l-16-2.5L160 144c10-12.5 16.7-30.2 16.7-50.1 0-39.2-24.8-68.8-52.4-68.8-2.9 0-4.7-.1-5.2-.1zM440 33c-17.2 0-31 13.77-31 31s13.8 31 31 31 31-13.77 31-31-13.8-31-31-31zM311 55v48H208v18h103v158h-55v18h55v110H208v18h103v32h80.8c-.5-2.9-.8-5.9-.8-9 0-3.1.3-6.1.8-9H329V297h62.8c-.5-2.9-.8-5.9-.8-9 0-3.1.3-6.1.8-9H329V73h62.8c-.5-2.92-.8-5.93-.8-9 0-3.07.3-6.08.8-9H311zm129 202c-17.2 0-31 13.8-31 31s13.8 31 31 31 31-13.8 31-31-13.8-31-31-31zm0 160c-17.2 0-31 13.8-31 31s13.8 31 31 31 31-13.8 31-31-13.8-31-31-31z" + } + }, + { + "_id": "Lt5ccP99yjDsh4oMJ", + "name": "Actions", + "parentId": "ioei4uvDdGTAFqZrB", + "root": { + "id": "DDDDDDDDDDDDDDDDD", + "collection": "docs" + }, + "left": 2, + "right": 2, + "urlName": "action", + "description": "Actions are things your character can do. When an action is taken, all the properties under it are applied.\n\nAdd actions to your character sheet, then add children under the action to determine what happenes when the action is applied.\n\nWhen an action is applied it will create an entry in the character's log detailing all the properties that were applied and what their results were.\n\nSee [Static and Activated Properties](https://dicecloud.com/docs/concepts/static-and-activated-properties) for more information on what properties can be applied by actions.\n\n---\n\n### Name\nname\n\nThe name of the action.\n\n### Action type\nactionType\n\nHow long the action takes to perform.\n\nAllows [inline calculations](/docs/concepts/inline-calculations).\n\n### Variable name\nvariableName\n\nFor Event-type actions, the variable name to use when referencing this event.\n\n### Attack roll\nattackRoll\n\nA [computed field](/docs/concepts/computed-fields) which calculates the attack roll modifier. If this field is empty, no attack roll will be made. Use 0 to make an attack roll without a modifier.\n\nThe following variables may be added to the action scope when attack rolls are made:\n\n - `~attackDiceRoll` The value of the d20 roll before any modifiers were applied.\n - `~attackRoll` The total attack roll after modifiers.\n - `~criticalHit` Set to `true` if the attack roll's d20 is a natural 20. If `~criticalHitTarget` is set, the attack roll's d20 must instead be equal to or greater than `~criticalHitTarget` for this to be set to `true`.\n - `~criticalMiss` Set to `true` if the attack roll was not a critical hit and rolled a natural 1 on the d20 roll.\n - `~attackHit` If the attack roll is higher than or equal to the target's AC or a critical hit this is set to `true`. Remains unset if there is no target for the attack unless the attack is a critical hit.\n - `~attackMiss` If the attack roll is lower than the target's AC or a critical miss, this is set to `true`. Remains unset if there is no target for the attack unless the attack is a critical miss.\n - `~attackAdvantage` If the attack roll is made with Advantage, this is set to `1`. If the attack roll is made with Disadvantage, this is set to `-1`. Remains unset if the attack was made normally.\n\n### Summary\nsummary\n\nA brief overview of what the action does. This will appear in the action card, and shows in the log when the action is applied.\n\nAllows [inline calculations](/docs/concepts/inline-calculations).\n\n### Description\ndescription\n\nA more detailed description of the action. The description does not show in the action card or the log when the action is applied.\n\nAllows [inline calculations](/docs/concepts/inline-calculations).\n\n### Resource\n\nA resource can be any attribute that has a variable name. If the resource attribute is less than the amount required, the action can't be applied.\n\nIf you want to reduce an attribute when taking the action, but want the action to be applied regardless of the value of that attribute, consider using an [Attribute Damage](/docs/property/attribute-damage) property as a child of the action instead. Also use Attribute Damage when the amount to reduce the attribute is determined by a dice roll rather than a stable computed number.\n\n#### Resource attribute\nresources.attributesConsumed.0.variableName\n\nThe variable name of the attribute that will be consumed when taking this action.\n\n#### Resource quantity\nresources.attributesConsumed.0.quantity\n\nA [computed field](/docs/concepts/computed-fields) which determines how much of the attribute is required to apply this action. This amount will be deducted from the attribute every time the action is taken.\n\n### Ammo\n\nAmmo represents items that are required to take the action. If an item is not selected, or there is insufficient quantity of the selected item, the action can't be applied.\n\n#### Ammo item\nresources.itemsConsumed.0.tag\n\nSpecify what tag an item must have to be considered valid ammo for this action. Any item with this tag can be selected as ammo for this action.\n\n#### Ammo quantity\nresources.itemsConsumed.0.quantity\n\nA [computed field](/docs/concepts/computed-fields) which determines how many of the selected items are required to take this action. The quantity is deducted from the total quantity of the item when this action is applied.\n\n### Target\ntarget\n\nWho this action should apply to. The properties under the action will be applied to the Targets.\n\n- **Self** The action will apply its properties to the creature taking the action\n- **Single Target** The action will apply its properties without a target (for now)\n- **Multiple Targets** The action will apply its properties without a target (for now)\n\n### Uses\nuses\n\nA [computed field](/docs/concepts/computed-fields) which determines how many times this action can be used before it needs to be reset.\n\n### Uses used\nusesUsed\n\nHow many of this action's uses have already been used. Should ideally be between 0 and the total uses available. This number is set to 0 when the action has uses and its uses are reset.\n\n### Reset\nreset\n\nIf set, the uses used field is set to 0 at the appropriate time.\n\n- **Long rest** Reset when the long rest button is pushed\n- **Short rest** Reset when either the long or short rest button is pushed", + "published": true, + "href": "/docs/property/action" + }, + { + "_id": "FwpkjToybWQKCDhSr", + "name": "Attribute Damage", + "parentId": "ioei4uvDdGTAFqZrB", + "root": { + "id": "DDDDDDDDDDDDDDDDD", + "collection": "docs" + }, + "left": 3, + "right": 3, + "urlName": "attribute-damage", + "description": "When applied, attribute damage reduces the value of the [Attribute](/docs/property/attribute) by some amount or set the value of an attribute to some amount. Attribute damage can by applied by actions or triggers.\n\nUsing a negative value to damage an attribute will heal the attribute instead.\n\n---\n\n### Attribute\nstat\n\nThe variable name of the attribute to target.\n\n### Amount\namount\n\nA [computed field](/docs/concepts/computed-fields) which determined the amount to damage the attribute or set the attribute's value to.\n\n### Operation\noperation\n\n- **Damage** Reduce the value of the attribute by the amount, negative values heal the attribute instead\n- **Set** Set the value of the attribute to the amount\n\n### Target\ntarget\n\n- **Target** Apply the attribute damage to the same target as the action applying this property\n- **Self** Apply the attribute damage to the creature taking the action\n\n---\n
\n\n*Note: To refer to an Attribute Damage property by [ancestor reference](/docs/concepts/computed-fields#ancestor-references), use `#adjustment` instead of `#attributeDamage`.*", + "href": "/docs/property/attribute-damage", + "published": true + }, + { + "_id": "qdqo83AqkzQhtC2Em", + "name": "Attributes", + "parentId": "ioei4uvDdGTAFqZrB", + "root": { + "id": "DDDDDDDDDDDDDDDDD", + "collection": "docs" + }, + "left": 4, + "right": 4, + "urlName": "attribute", + "description": "Attributes represent the basic numerical values of the creature, including everything from stats to health bars and more. The are the most common targets for [effects](/docs/property/effect), which affect them with a static bonus.\n\nAttributes have the following fields that can be accessed in calculations with `variableName.field`:\n\n- `total` - The default value of the attribute before being damaged.\n- `value` - The current value of the attribute including damage. `variableName` and `variableName.value` are equivalent.\n- `damage` - The amount of damage the attribute has taken.\n- `modifier` - If the attribute's type is Ability Score, its corresponding modifier in D&D 5th Edition rules.\n\n---\n\n### Name\nname\n\nThe human-readable name of the attribute.\n\n### Variable name\nvariableName\n\nThe name used to refer to the attribute in calculations and by effects. Must start with a letter and be made up of only letters and numbers without spaces, symbols, or punctuation.\n\nIf multiple attributes share a variable name, only the last attribute on the [character tree](/docs/concepts/tree) will count as the defining attribute and appear on the sheet, while other attributes with that variable name will be used as base value [effects](/docs/property/effect).\n\n### Base value\nbaseValue\n\nA [computed field](/docs/concepts/computed-fields) that determines the starting value of the attribute before it is modified by effects and other properties. Multiple properties can set the base value for a given variable name, when this happens the highest base value is chosen, and then all other effects are applied.\n\n### Attribute type\nattributeType\n\n- **Ability** - Ability scores like Strength, Dexterity, etc. Ability scores get a modifier which can be accessed in calculations as `variableName.modifier`.\n- **Stat** - Any simple numerical value that appears on the sheet, e.g. speed, armor class.\n- **Modifier** - Any numerical value that appears on the sheet with a `+` or `-` sign, e.g. proficiency bonus.\n- **Hit Dice** - Hit dice let you select the appropriate hit dice size. Creatures regain half their total hit dice on long rest by default.\n- **Health Bar** - Health bars can be made to take or ignore damage in a specified order.\n- **Resource** - Rages, sorcery points, things that are spent to use actions.\n- **Spell Slot** - Spell slots have a specific level and are used to cast spells.\n- **Utility** - Utility attributes don't show up anywhere on the sheet, but can still be used for calculations.\n\n### Description\ndescription\n\nA detailed description of the attribute. Allows [inline calculations](/docs/concepts/inline-calculations).\n\n### Health bar settings\n\nHealth bars can take or ignore damage and healing from applied damage properties targeting a creature. A lower ordered health bar will take damage before a higher ordered one.\n\nHealth bars can also change color depending on their value. At 50%+ full they are their property color, between 50% and 0% they fade from their half-full color to their empty color.\n\n##### Health Bar Fields\n- `healthBarColorMid` The color when below half health.\n- `healthBarColorLow` The color when empty.\n- `healthBarNoDamage` If true, this health bar will ignore damage.\n- `healthBarNoHealing` If true, this health bar will ignore healing.\n- `healthBarNoDamageOverflow` If true, this health bar will not allow excess damage to overflow past it.\n- `healthBarNoHealingOverflow` If true, this health bar will not allow excess healing to overflow past it.\n- `healthBarDamageOrder` The numeric priority with which damage will apply to this health bar.\n- `healthBarHealingOrder` The numeric priority with which healing will apply to this health bar.\n\n### Hit dice size\nhitDiceSize\n\nHit dice can come in various sizes, from d4 to d20.\n\n### Spell slot level\nspellSlotLevel\n\nSpell slots require a level, to determine what levels of spells they can be used to cast.\n\n### Damage\ndamage\n\nThe amount of damage this attribute has currently taken. Subtracted from `.total` to calculate `.value`.\n\n### Reset\nreset\n\nIf set, the damage on this attribute is reset to 0 at the appropriate time.\n\n- **Long rest** - Reset when the long rest button is pushed\n- **Short rest** - Reset when either the long or short rest button is pushed\n\n### Allow decimal values\ndecimal\n\nIf this is set, the attribute will not round-down when its value has a decimal.\n\n### Can be damaged into negative values\nignoreLowerBound\n\nIf this is set the attribute can be damaged past zero into negative values.\n\n### Can be incremented above total\nignoreUpperBound\n\nIf this is set the attribute can have negative damage such that the value exceeds the total. This can be useful if you are using the attribute to count, it can start at zero and be healed upwards to keep count.\n\n### Hide when total is zero\nhideWhenTotalZero\n\nIf this is set, the attribute will not be displayed on the sheet when its value before damage is 0.\n\n### Hide when value is zero\nhideWhenValueZero\n\nIf this is set, the attribute will not be displayed on the sheet when its value after damage is 0.", + "published": true, + "href": "/docs/property/attribute" + }, + { + "_id": "X5NKw8m6ruy9Srynd", + "name": "Branches", + "parentId": "ioei4uvDdGTAFqZrB", + "root": { + "id": "DDDDDDDDDDDDDDDDD", + "collection": "docs" + }, + "left": 5, + "right": 5, + "urlName": "branch", + "published": true, + "description": "Branches are applied by actions (or triggers), when they are applied they can control which of their immediate children are applied.\n\n---\n\n### Branch type\ntype\n\n- **If condition is true** Apply children if the condition (a [computed field](/docs/concepts/computed-fields)) resolves to `true` or a non-zero number\n- **Attack hit** Apply children if the attack roll hit the target.\n- **Attack miss** Apply children if the attack roll missed the target.\n- **Save failed** Apply children if target failed its saving throw.\n- **Save succeeded** Apply children if target made its saving throw.\n- **Apply to each target** Apply children separately to each target.\n- **Random** Apply one of the immediate children at random.\n- **Calculated Index** Use the index (a [computed field](/docs/concepts/computed-fields)) to choose which child to apply, starting at 1 for the first child.\n\n### Condition / Index\ncondition\n\nA calculation the branch should use to determine whether to activate its children, or which children to activate. Used as a boolean in \"If condition is true\" mode, and a numerical index in \"Calculated Index\" mode.", + "href": "/docs/property/branch" + }, + { + "_id": "hraKfb96M3LKEfYdp", + "name": "Buffs", + "parentId": "ioei4uvDdGTAFqZrB", + "root": { + "id": "DDDDDDDDDDDDDDDDD", + "collection": "docs" + }, + "left": 8, + "right": 8, + "urlName": "buff", + "href": "/docs/property/buff", + "description": "Buffs are temporary changes to a character sheet that can be applied by actions. When a buff is applied, it is copied to the target character along with all of its children properties. \n\nBuffs can either be manually removed from the stats page, or be removed by an action applying a [buff remover](/docs/property/remove-buff/) property.\n\n### Variable freezing\n\nWhen a buff is applied, all the calculations in the child properties have their variables frozen to their values at the time the buff is applied. You can prevent this behavior for the whole buff by using the \"Don't freeze variables\" option, or on an individual variable reference by prefixing the variable with the keyword `~target.`.\n\nFor example, if a character has 10 strength and 16 dexterity, and applies a buff with some child property containing the calculation `~target.strength + dexterity`, the property's calculation will become `strength + 16` when it is copied to the target character.\n\n---\n\n### Name\nname\n\nThe name of the buff.\n\n### Description\ndescription\n\nDescription of the applied buff.\n\nAllows [inline calculations](/docs/concepts/inline-calculations).\n\n### Target\ntarget\n\n- **Target** Apply the buff to the target of the action\n- **Self** Apply the buff to the creature taking the action\n\n### Hide remove button\nhideRemoveButton\n\nIf this is set, the remove button next to the buff on the stats page will be hidden. Use this when you expect the buff to be removed automatically by another action.\n\n### Don't freeze variables\nskipCrystalization\n\nPrevent the buff from freezing variables in child property calculations to their value at the time the buff was applied.", + "published": true + }, + { + "_id": "E2DFwsCoiKy2Rc9Mz", + "name": "Concepts", + "left": 10, + "right": 10, + "urlName": "concepts", + "href": "/docs/concepts", + "root": { + "id": "DDDDDDDDDDDDDDDDD", + "collection": "docs" + }, + "published": true, + "description": "The inner workings of DiceCloud's character engine", + "icon": { + "name": "light-bulb", + "shape": "M247 18.656c-80.965 0-146.875 65.02-146.875 145.625 0 45.63 15.758 67 33.313 94.845 11.706 18.57 23.767 39.91 30.53 70.563h165.095c6.728-31.387 18.91-53.12 30.718-71.875 17.58-27.92 33.314-48.912 33.314-93.532 0-80.66-65.127-145.624-146.094-145.624zm-99.78 127.906L170.437 167 210 201.813l31.188-34.125 6.78-7.438 6.907 7.344 30.75 32.72 39.97-33.47 22.686-19-7.655 28.594L304.75 310.28l-18.063-4.842 28.22-105.25-24.032 20.125-6.78 5.656-6.033-6.44-29.906-31.78-30.562 33.438-6.188 6.78-6.875-6.062-23.25-20.437 27.94 104.218-18.064 4.812-35.937-134.063-8-29.875zm22.593 201.813V389.5L315 348.375H169.812zm153.593 17.063l-153.594 43.53v29.438l153.594-43.5v-29.47zm0 48.875L203.97 448.156h119.436v-33.844zm-132.562 52.53v20.533h113.282v-20.53h-113.28z" + } + }, + { + "_id": "5Fwk543djf4hfqxCc", + "name": "Class levels", + "parentId": "ioei4uvDdGTAFqZrB", + "root": { + "id": "DDDDDDDDDDDDDDDDD", + "collection": "docs" + }, + "left": 11, + "right": 11, + "urlName": "class-level", + "href": "/docs/property/class-level", + "description": "A class level is a property that represents a single level in a class. It is generally used as a child of a [Class property](/docs/property/class).\n\nFeatures and bonuses that are given by a class level get added as children of the class level.\n\n---\n\n### Level\nlevel\n\nWhich level this property represents.\n\n### Name\nname\n\nThe name of the class or subclass this level is part of\n\n### Variable name\nvariableName\n\nThe same variable name of the class this level belongs to.\n\n### Description\ndescription\n\nA description of the benefits gained with this level. Allows [inline calculations](/docs/concepts/inline-calculations).", + "published": true + }, + { + "_id": "yX4ZuoHQwtGv3QJXX", + "name": "Classes", + "parentId": "ioei4uvDdGTAFqZrB", + "root": { + "id": "DDDDDDDDDDDDDDDDD", + "collection": "docs" + }, + "left": 12, + "right": 12, + "urlName": "class", + "href": "/docs/property/class", + "published": true, + "description": "A class is a property that expects [class levels](/docs/property/class-level) as its immediate children.\n\nLeveling up a class means choosing, or manually adding, class level properties to it. Class levels with the same variable name as the class, and that match all the required tags are found in libraries and added to the class.\n\nThe total level of the class can be accessed in calculations using `classVariableName.level`.\n\n---\n\n### Name\nname\n\nThe name of the class\n\n### Variable name\nvariableName\n\nThe name used to refer to the class in calculations. Must start with a letter and be made up of only letters and numbers without spaces, symbols, or punctuation.\n\n### Description\ndescription\n\nA description of the class.\n\n### Tags required\nslotTags.0\n\nOnly class levels with the same variable name as the class, and with tags that match the tags required will be returned from libraries when leveling up this class. \n\nAdditional tag lists can be created using either OR mode (a property with either all of the tags in the main list *or* all of the tags in an OR list will be selectable) or NOT mode (a property with any tag in a NOT list cannot be selected).\n\n##### Additional Tag List Fields\n- `extraTags.0.operation` Whether this additional tag list is an OR list or a NOT list.\n- `extraTags.0.tags.0` The tags in this additional tag list.\n\n### Condition\nslotCondition\n\nA [computed field](/docs/concepts/computed-fields) to determine if the class is allowed to level up. If this field results in `true` or a number that is not 0, the class can be leveled, otherwise leveling is disabled." + }, + { + "_id": "giHu6Ej7qvsZr4zrJ", + "name": "Constants", + "parentId": "ioei4uvDdGTAFqZrB", + "root": { + "id": "DDDDDDDDDDDDDDDDD", + "collection": "docs" + }, + "left": 13, + "right": 13, + "urlName": "constant", + "href": "/docs/property/constant", + "description": "Constants are properties that store some primitive value in a variable name for use in other calculations.\n\nUnlike attributes, constants can store more than just numbers:\n\n- Arrays: `[1,2,3,4]`\n- Text strings: `'I am a cat'`\n- Numbers: `3.14`\n- Boolean values: `true`, `false`\n- Dice rolls: `1d20 + 2`\n\nHowever, constants can't use other variables in their calculations.\n\n### Overriding constants\n\nIf multiple constants have the same variable name, only the last active constant in the [character tree](/docs/concepts/tree) will be used as the definition for that variable name.\n\nThis can be used to re-write the value of some constant by ensuring there is a new active constant later in the sheet.\n\n---\n\n### Name\nname\n\nThe name of the constants\n\n### Variable Name\nvariableName\n\nThe name used to refer to the constant in calculations. Must start with a letter and be made up of only letters and numbers without spaces, symbols, or punctuation.\n\n### Value\nvalue\n\nA [calculation](/docs/concepts/computed-fields) of the final value of the constant.", + "published": true + }, + { + "_id": "ghEJ3rh6YHCfuBirJ", + "name": "Containers", + "parentId": "ioei4uvDdGTAFqZrB", + "root": { + "id": "DDDDDDDDDDDDDDDDD", + "collection": "docs" + }, + "left": 14, + "right": 14, + "urlName": "container", + "href": "/docs/property/container", + "description": "Containers are things that [items](/docs/property/item) can be put inside of.\n\n---\n\n### Name\nname\n\nThe name of the container\n\n### Carried\ncarried\n\nIf this is set the weight of the container and its contents will be added to the character's weight carried.\n\n### Value\nvalue\n\nThe value of the container in gold pieces. Silver pieces are worth 0.1 gp and copper pieces are worth 0.01 gp. So a container that is worth 2 gp 4 sp 7 cp will have a value of 2.47 gp.\n\n### Weight\nweight\n\nThe weight of the container in lb.\n\n### Description\ndescription\n\nA description of the container. Allows [inline calculations](/docs/concepts/inline-calculations).\n\n### Contents' Weight\ncontentsWeight\n\nWeight of all the contents.\n\n### Carried Weight\ncarriedWeight\n\n Weight of all the carried contents (some sub-containers might not be carried). Zero if `contentsWeightless` is true.\n\n### Contents' Value\ncontentsValue\n\nValue of all the contents.\n\n### Carried Value\ncarriedValue\n\n Value of all the carried contents (some sub-containers might not be carried). Zero if `contentsWeightless` is true.\n\n### Contents are weightless\ncontentsWeightless\n\nIf this is set and the container is carried, only the container's own weight will be added to the weight carried by the creature.", + "published": true + }, + { + "_id": "B7xb4iC33np9JurzR", + "name": "Damage multipliers", + "parentId": "ioei4uvDdGTAFqZrB", + "root": { + "id": "DDDDDDDDDDDDDDDDD", + "collection": "docs" + }, + "left": 15, + "right": 15, + "urlName": "damage-multiplier", + "href": "/docs/property/damage-multiplier", + "description": "Damage multipliers are used to define vulnerability, resistance, and immunity to damage types.\n\nA single multiplier can apply to multiple damage types, and choose whether or not to apply to an incoming source of damage based on the tags present on that damage.\n\n---\n\n### Name\nname\n\nThe name of the feature that gives this damage multiplier\n\n### Value\nvalue\n\n- **Immunity** The creature takes no damage from matching damage sources\n- **Resistance** Damage from matching sources is halved.\n- **Vulnerability** Damage from matching sources is doubled.\n\n### Damage types\ndamageTypes.0\n\nA list of damage types that this property applies to. Custom types can be used.\n\n### Damage tags required\nincludeTags.0\n\nThis damage multiplier will only be applied if the incoming damage has all of these tags present.\n\n### Damage tags excluded\nexcludeTags.0\n\nThis damage multiplier will only apply if the incoming damage has none of these tags present.", + "published": true + }, + { + "_id": "oiQLbNiip7JHyGBG4", + "name": "Damage", + "parentId": "ioei4uvDdGTAFqZrB", + "root": { + "id": "DDDDDDDDDDDDDDDDD", + "collection": "docs" + }, + "left": 16, + "right": 16, + "urlName": "damage", + "href": "/docs/property/damage", + "published": true, + "description": "Damage can be applied by an action to damage a target creature's [health bars](/docs/property/attribute). The damage will be modified by [damage multipliers](/docs/property/damage-multiplier), which apply vulnerability, resistance, and immunity before the damage is applied.\n\n---\n\n### Damage\namount\n\nA [computed field](/docs/concepts/computed-fields) that determines how much damage to do to the target creature.\n\n### Damage type\ndamageType\n\nDamage type determines how the damage is treated by [damage multipliers](/docs/property/damage-multiplier). A custom type can be used, or one of the existing types can be selected.\n\nThere are two special damage types:\n\n- **Extra** - Damage with the `extra` type will take on the damage type of whatever damage was applied before it by an action, e.g. an action deals 12 piercing damage and 3 extra damage, it will instead deal 15 piercing damage.\n - The last damage dealt is stored as `~lastDamageType`, which can be accessed or set using a [Roll](/docs/property/roll) to change the damage type of the following \"extra\" damage properties.\n- **Healing** - Damage with the `healing` type will heal a creature instead of damaging them.\n\n### Target\ntarget\n\n- **Target** - Apply the damage to the target of the action\n- **Self** - Apply the damage to the creature taking the action" + }, + { + "_id": "FHdAjYY2er9xfYsJs", + "name": "Effects", + "parentId": "ioei4uvDdGTAFqZrB", + "root": { + "id": "DDDDDDDDDDDDDDDDD", + "collection": "docs" + }, + "left": 17, + "right": 17, + "urlName": "effect", + "href": "/docs/property/effect", + "description": "Effects are the core of the DiceCloud engine. Effect change the values of attributes, skills, and calculations in a way that is transparent and auditable, keeping character sheets organized and understandable, even when using intricate homebrew rules on high level characters.\n\n---\n\n### Name\nname\n\nThe name of the feature that causes this effect.\n\n### Operation\noperation\n\nThe operation determines what the effect will do to the affected property or calculation.\n\n- **Base Value** - Set the base value of the affected property. If a property has multiple base values, the highest is used\n- **Add** - Add the value to the affected property or calculation\n- **Multiply** - Multiply the affected property by the value\n- **Minimum** - Prevent the affected property from having a value less than the effect value\n- **Maximum** - Prevent the affected property from having a value greater than the effect value\n- **Set** - Set the value affected property to the effect value\n- **Advantage** - Give advantage to checks made using the affected property\n- **Disadvantage** - Give disadvantage to checks made using the affected property\n- **Passive bonus** - Add the effect value to the passive scores based on the affected property\n- **Fail** - Checks made using the affected property automatically fail\n- **Conditional benefit** - Add some text to the affected property describing the benefit received.\n\n### Value\namount\n\nA [computed field](/docs/concepts/computed-fields) that determines the value of the effect.\n\n### Text\ntext\n\nIf the operation is a conditional benefit, the note text that will show on affected properties.\n\n### Target stats by variable name\ntargetByTags = false\n\nIf selected the effect will apply to all properties that have the given variable names.\n\n### Variable names\nstats.0\n\nA list of variable names of properties to target with this effect.\n\n### Target properties by tags\ntargetByTags = true\n\nWhen targeting properties by tag, any property can be targeted with an effect. If the property is one that can usually be targeted by variable name, the effect will apply as usual, however if the effect targets another property, it will apply to a [computed field](/docs/concepts/computed-fields) on the property instead.\n\nThese effects can be used for adding a bonus to a specific attack or damage roll, or manipulating any computed field on the creature.\n\n### Tags required\ntargetTags.0\n\nOnly properties that match the required tags will be targeted by the effect.\n\nAdditional tag lists can be created using either OR mode (a property with either all of the tags in the main list *or* all of the tags in an OR list will be targeted) or NOT mode (a property with any tag in a NOT list will not be targeted).\n\n##### Additional Tag List Fields\n- `extraTags.0.operation` Whether this additional tag list is an OR list or a NOT list.\n- `extraTags.0.tags.0` The tags in this additional tag list.\n\n### Target field\ntargetField\n\nIf a property has multiple computed fields, which field should be targeted by this effect.", + "published": true + }, + { + "_id": "GAEPgagGYv3a2QWnE", + "name": "Features", + "parentId": "ioei4uvDdGTAFqZrB", + "root": { + "id": "DDDDDDDDDDDDDDDDD", + "collection": "docs" + }, + "left": 18, + "right": 18, + "urlName": "feature", + "href": "/docs/property/feature", + "description": "Features appear on the features tab. Classes, backgrounds, and race can all give a creature features.\n\n---\n\n### Name\nname\n\nThe name of the feature.\n\n### Summary\nsummary\n\nA summary of the feature. This will appear on the feature card. Allows [inline calculations](/docs/concepts/inline-calculations).\n\n### Description\ndescription\n\nA detailed description of the feature. Allows [inline calculations](/docs/concepts/inline-calculations).", + "published": true + }, + { + "_id": "7o6sWuQeoBtiQRLdL", + "name": "Folders", + "parentId": "ioei4uvDdGTAFqZrB", + "root": { + "id": "DDDDDDDDDDDDDDDDD", + "collection": "docs" + }, + "left": 19, + "right": 19, + "urlName": "folder", + "href": "/docs/property/folder", + "published": true, + "description": "Folders allow the [character tree](/docs/concepts/tree) to be organized.\n\n### Folders in actions\n\nWhen a folder is the child of an action, it and its children will not show on the action card, but will still appear in the detail view of the action and be applied when the action is taken.\n\n### Grouping children\n\nWhen the folder is set to group stats on a card, the immediate children of the folder will be shown in a single card in a location on the sheet you can specify. For now, the following properties are supported:\n\n- Actions, including event actions\n- Attributes\n- Buffs\n- Containers\n- Features\n- Items\n- Notes\n- Skills\n- Toggles (with _Show on character sheet_ enabled)\n\n---\n\n### Name\nname\n\nThe name of the folder.\n\n### Description\ndescription\n\nA description for the folder. Only shown when selecting it from a library, e.g. via slot.\n\n### Group children on a card\ngroupStats\n\nIf set, immediate children will be grouped on a card in the sheet.\n\n### Hide children from their default locations\nhideStatsGroup\n\nIf set, the children of this folder will only show on the grouped card, and be hidden from elsewhere on the sheet.\n\n### Tab\ntab\n\nDetermines which tab the card will show up on.\n\n### Location\nlocation\n\nWhere on the tab the card will be positioned." + }, + { + "_id": "5MsdJBbpALgMnYBwk", + "name": "Items", + "parentId": "ioei4uvDdGTAFqZrB", + "root": { + "id": "DDDDDDDDDDDDDDDDD", + "collection": "docs" + }, + "left": 20, + "right": 20, + "urlName": "item", + "href": "/docs/property/item", + "published": true, + "description": "Items are shown on the Inventory tab. Items can be carried, put in containers, or equipped on a creature. The children of an item are not active unless the item is equipped.\n\n---\n\n### Icon\n\nAn icon representing the item.\n\n### Equipped\nequipped\n\nIf set, the item appears in the equipment list on the inventory tab and its children become active on the creature.\n\n### Name\nname\n\nThe name of the item.\n\n### Plural name\nplural\n\nThe name to use if the quantity of the item is higher than 1.\n\n### Value\nvalue\n\nThe value of a single item in gold pieces. Silver pieces are worth 0.1 gp and copper pieces are worth 0.01 gp. So an item that is worth 2 gp 4 sp 7 cp will have a value of 2.47 gp.\n\n### Weight\nweight\n\nThe weight of a single item in lb.\n\n### Quantity\nquantity\n\nNumber of items. The value and quantity will be multiplied by the quantity to get the total value and weight of this stack of items.\n\n### Description\ndescription\n\nA description of the item. Allows [inline calculations](/docs/concepts/inline-calculations).\n\n### Show increment button\nshowIncrement\n\nIf this is set, the item will show an increment button in the detail view and on the inventory tab. This button can be used to quickly adjust the quantity of the item.\n\n### Requires attunement\nrequiresAttunement\n\nIf set, the item requires attunement to use.\n\n### Attuned\nattuned\n\nIf set, the item is attuned and counts towards the total number of attuned items for the creature.\n\nIf a child property needs to determine if its parent item is attuned it can use `#item.attuned` in calculations, see *Ancestor references* in [computed fields](/docs/concepts/computed-fields)." + }, + { + "_id": "74utQna6D4ayYyrLp", + "name": "Notes", + "parentId": "ioei4uvDdGTAFqZrB", + "root": { + "id": "DDDDDDDDDDDDDDDDD", + "collection": "docs" + }, + "left": 21, + "right": 21, + "urlName": "note", + "href": "/docs/property/note", + "published": true, + "description": "Notes are used to store text on the creature that does not have a direct mechanical impact. Notes appear on the journal tab when active on the character, or are shown in the log when applied by an [action](/docs/property/action).\n\n---\n\n### Name\nname\n\nName of the note.\n\n### Summary\nsummary\n\nA summary of the note. This will appear on the note card and in the log when applied by an [action](/docs/property/action). Allows [inline calculations](/docs/concepts/inline-calculations).\n\n### Description\ndescription\n\nA detailed description of the feature. Allows [inline calculations](/docs/concepts/inline-calculations)." + }, + { + "_id": "cuhusZb8xYW8dj743", + "name": "Point buy", + "parentId": "ioei4uvDdGTAFqZrB", + "root": { + "id": "DDDDDDDDDDDDDDDDD", + "collection": "docs" + }, + "left": 22, + "right": 22, + "urlName": "point-buy", + "href": "/docs/property/point-buy", + "published": true, + "description": "A point buy is a set of rows that lets the user choose a set of stats based on a cost per stat.\n\n---\n\n### Table name\nname\n\nThe name of the point buy table.\n\n### Min\nmin\n\nThe lowest value available for each row\n\n### Max\nmax\n\nThe highest value available for each row\n\n### Cost\ncost\n\nA function that uses `value` as the value of a row and determines the cost of that value. For standard D&D 5e point buy, this function is :\n```js\n[0, 1, 2, 3, 4, 5, 7, 9][value - 7]\n```\n\n### Total available points\ntotal\n\nA [computed field](/docs/concepts/computed-fields) that determines how many points are available to spend in total\n\n## Rows\nvalues.0\n\nUp to 32 rows can be added to a point buy table\n\n### Row name\nvalues.0.name\n\nThe name of the row that will appear in the table\n\n### Row variable name\nvalues.0.variableName\n\nThe variable name of the row that can be used in calculations. Must start with a letter and be made up of only letters and numbers without spaces, symbols, or punctuation.\n\nIf the variable name matches an attribute with the same variable name, the row's value will be used as a base value for that attribute.\n\n### Row default value\n\nThe default value for the row. Only visible in libraries." + }, + { + "_id": "Jh92aYezHsEbSkriy", + "name": "Proficiencies", + "parentId": "ioei4uvDdGTAFqZrB", + "root": { + "id": "DDDDDDDDDDDDDDDDD", + "collection": "docs" + }, + "left": 23, + "right": 23, + "urlName": "proficiency", + "href": "/docs/property/proficiency", + "description": "Proficiencies add proficiency to an existing skill on the creature. If you need to add a tool or language proficiency to a creature, use a [Skill](/docs/property/skill) instead.\n\n---\n\n### Name\nname\n\nName of the feature that is adding this proficiency\n\n### Skills\nstats.0\n\nA list of variable names of the skills to add proficiency to.\n\n### Proficiency\nvalue\n\nHow much proficiency to add to the skill. If a skill has multiple proficiencies added to it, the highest one will be used.", + "published": true + }, + { + "_id": "Nik9WERFxgjnp4cpE", + "name": "Remove Buff", + "parentId": "ioei4uvDdGTAFqZrB", + "root": { + "id": "DDDDDDDDDDDDDDDDD", + "collection": "docs" + }, + "left": 24, + "right": 24, + "urlName": "remove-buff", + "href": "/docs/property/remove-buff", + "published": true, + "description": "This property can remove a specific buff from a targeted creature.\n\n### Name\nname\n\nThe name of the property. This shows in the log when the property is applied.\n\n### Remove parent buff\ntargetParentBuff\n\nWhen this is set and the property is applied, the property will remove the nearest parent buff. If this property is not the child of any buffs, it will log an error.\n\n### Remove all\nremoveAll\n\nWhen this is set, all buffs that match the target tags will be removed from the targeted creature. If not set, only the first buff found with the matching tags will be removed.\n\n### Target\ntarget\n\n- **Target** - Matching buffs will be removed from the targeted creature\n- **Self** - Matching buffs will be removed from the creature that applied the action\n\n### Tags required\ntargetTags.0\n\nAny buff that has all of the required tags will be removed when the property is applied.\n\nAdditional tag lists can be created using either OR mode (a property with either all of the tags in the main list *or* all of the tags in an OR list will be removed) or NOT mode (a property with any tag in a NOT list will not be removed).\n\n##### Additional Tag List Fields\n- `extraTags.0.operation` Whether this additional tag list is an OR list or a NOT list.\n- `extraTags.0.tags.0` The tags in this additional tag list.\n\n---\n
\n\n*Note: To refer to a Remove Buff property by [ancestor reference](/docs/concepts/computed-fields#ancestor-references), use `#buffRemover` instead of `#removeBuff`.*" + }, + { + "_id": "8e67Pmq7RvggHp4pX", + "name": "Rolls", + "parentId": "ioei4uvDdGTAFqZrB", + "root": { + "id": "DDDDDDDDDDDDDDDDD", + "collection": "docs" + }, + "left": 25, + "right": 25, + "urlName": "roll", + "href": "/docs/property/roll", + "published": true, + "description": "Rolls are properties that store the result of a calculation to a variable name when applied by an [action](/docs/property/action). The variable name only exists for the duration of that particalar action.\n\nRolls can be useful if you need to deal the same damage to multiple targets, or if damage needs to be rolled then halved by succeeding on a saving throw.\n\n---\n\n### Name\nname\n\nName of the roll. This will be shown in the log when the roll is applied.\n\n### Variable name\nvariableName\n\nThe variable name to store the result of the roll for the duration of the action. Must start with a letter and be made up of only letters and numbers without spaces, symbols, or punctiation.\n\n### Roll\nroll\n\nA [computed field](/docs/concepts/computed-fields) that is computed when the roll is applied by an action." + }, + { + "_id": "Ecc7oWEtoJgXaYLtS", + "name": "Saving throws", + "parentId": "ioei4uvDdGTAFqZrB", + "root": { + "id": "DDDDDDDDDDDDDDDDD", + "collection": "docs" + }, + "left": 26, + "right": 26, + "urlName": "saving-throw", + "href": "/docs/property/saving-throw", + "description": "Saving throws are properties that cause the target to make a saving throw when applied. If you want to add a type of saving throw like Strength Save to a creature, use a [skill](/docs/property/skill) instead.\n\nWhen a saving throw is applied, the following variables are added to the scope of that action:\n\n- `~saveFailed` - Set to `true` if the target failed its saving throw or there are no targets for the saving throw\n- `~saveSucceeded` - Set to `true` if the target made its saving throw or there are no targets for the saving throw\n- `~saveDiceRoll` - The unmodified d20 roll the target made to save\n- `~saveRoll` - The final value of the saving throw roll after modifiers\n\n### Name\nname\n\nThe name of the saving throw. Usually the ability saving throw targeted: \"Strength Save\".\n\n### DC\ndc\n\nThe DC of the saving throw that the target needs to meet\n\n### Save\nstat\n\nThe variable name of the skill that will be used to make the saving throw.\n\n### Target\ntarget\n\n- **Target** - Apply the saving throw to the targets of the action. Each target will make the saving throw in turn. Child properties will be applied to each target separately with the results of their individual saving throw. If a value like damage needs to be shared between targets, it should be calculated in a [Roll](/docs/property/roll) before the saving throw.\n- **Self** - Apply the saving throw to the creature taking the action. The creature taking the action will become the target for all child properties.", + "published": true + }, + { + "_id": "BKW9roawHgYcP2act", + "name": "Skills", + "parentId": "ioei4uvDdGTAFqZrB", + "root": { + "id": "DDDDDDDDDDDDDDDDD", + "collection": "docs" + }, + "left": 27, + "right": 27, + "urlName": "skill", + "href": "/docs/property/skill", + "description": "Skills represent things the creature can be proficient in. Skills can have their values or behavior modifier by [effects](/docs/property/effect), and their proficiencies modified by [proficiencies](/docs/property/proficiency).\n\n---\n\n### Name\nname\n\nThe name of the skill.\n\n### Variable name\nvariableName\n\nThe name used to refer to the skill in calculations and by effects. Must start with a letter and be made up of only letters and numbers without spaces, symbols, or punctiation.\n\n### Ability\nability\n\nThe ability score that is the basis for checks made with this skill\n\n### Type\nskillType\n\n- **Skill** - Regular skills like *Athletics*, *Sleight* of Hand\n- **Save** - Saving throws like *Strength*, *Charisma*\n- **Check** - Checks that aren't skill like *Initiative*\n- **Tool** - Tool proficiencies\n- **Weapon** - Weapon proficiencies\n- **Armor** - Armor proficiencies\n- **Language** - Language proficiencies\n- **Utility** - Skills that don't show on the charcater sheet but can be used in calculations\n\n### Description\ndescription\n\nA detailed description of the skill. Allows [inline calculations](/docs/concepts/inline-calculations).\n\n### Base value\nbaseValue\n\nA [computed field](/docs/concepts/computed-fields) that determines the starting value of the skill before it is modified by effects and other properties. Multiple properties can set the base value for a given variable name, when this happens the highest base value is chosen, and then all other effects are applied.\n\n### Base proficiency\nbaseProficiency\n\nThe starting proficiency of the skill.\n\n### Proficiency\nproficiency\n\n- `0` if the skill has no proficiency\n- `1` if the skill has proficiency\n- `0.5` if the skill has half proficiency\n- `0.49` if the skill has half proficiency rounded down\n- `2` if the skill has double proficiency.", + "published": true + }, + { + "_id": "bj5Bh5gsmjkLpYqA4", + "name": "Slots", + "parentId": "ioei4uvDdGTAFqZrB", + "root": { + "id": "DDDDDDDDDDDDDDDDD", + "collection": "docs" + }, + "left": 29, + "right": 29, + "urlName": "slot", + "href": "/docs/property/slot", + "description": "Slots are the main way creatures interact with libraries. A slot can be filled by choosing a property from a library that fits that particular slot. Slots show up on the build tab, and are highlighted when they have space that can be filled.\n\nIn a complete library, a creature can be built entirely by choosing which properties to fill each slot with. If you are building a creature without a library, you should either ignore slots entirely, or fill them with your own custom properties.\n\n---\n\n### Name\nname\n\nThe name of the slot.\n\n### Type \nslotType\n\nWhat kind of property this slot expects to fill it.\n\n### Tags required\nslotTags.0\n\nProperties in a library must have the required tags to fill the slot.\n\nAdditional tag lists can be created using either OR mode (a property with either all of the tags in the main list *or* all of the tags in an OR list will be selectable) or NOT mode (a property with any tag in a NOT list cannot be selected).\n\n##### Additional Tag List Fields\n- `extraTags.0.operation` Whether this additional tag list is an OR list or a NOT list.\n- `extraTags.0.tags.0` The tags in this additional tag list.\n\n### Quantity\nquantityExpected\n\nHow many properties are expected to fill this slot. Use 0 for allowing an unlimited number of properties.\n\n### Condition\nslotCondition\n\nA [computed field](/docs/concepts/computed-fields) that determines whether this slot can accept new properties.\n\n### Unique\nunique\n\nThe slot can control how it deals with the uniqueness of properties that fill it.\n\n### Description\ndescription\n\nA detailed description of the slot. Allows [inline calculations](/docs/concepts/inline-calculations).\n\n### Hide when full\nhideWhenFull\n\nWhen set the slot will hide itself when it is filled.\n\n### Ignored\nignored\n\nWhen set the slot will not show a prompt card on the build tab.\n\n---\n
\n\n*Note: To refer to a Slot property by [ancestor reference](/docs/concepts/computed-fields#ancestor-references), use `#propertySlot` instead of `#slot`.*", + "published": true + }, + { + "_id": "h9Jw5bfSLq3D2jmeD", + "name": "Spell lists", + "parentId": "ioei4uvDdGTAFqZrB", + "root": { + "id": "DDDDDDDDDDDDDDDDD", + "collection": "docs" + }, + "left": 30, + "right": 30, + "urlName": "spell-list", + "href": "/docs/property/spell-list", + "published": true, + "description": "Spell lists are collections of [spells](/docs/property/spell).\n\n---\n\n### Name\nname\n\nThe name of the spell list.\n\n### Description\ndescription\n\nA detailed description of the spell list.\n\nAllows [inline calculations](/docs/concepts/inline-calculations).\n\n### Maximum prepared spells\nmaxPrepared\n\nA [computed field](/docs/concepts/computed-fields) that determines how many spells can be considered ready to cast in this spell list.\n\n### Spell casting ability\nability\n\nThe spellcasting ability for this spell list. The variable name of the ability can be accessed using `#spellList.ability` and the ability modifier with `#spellList.abilityMod`. Setting this field will automatically update Spell save DC and Attack roll bonus if they aren't set manually.\n\n### Spell save DC\ndc\n\nA [computed field](/docs/concepts/computed-fields) that determines the DC of saving throws in this spell list. Spells can access the DC of their spell list using `#spellList.dc`\n\n### Attack roll bonus\nattackRollBonus\n\nA [computed field](/docs/concepts/computed-fields) that determines the bonus to add to a d20 when making a spell attack with a spell in this spell list. Spells can access the attack roll bonus of their spell list using `#spellList.attackRollBonus`" + }, + { + "_id": "Mji9Cnp2TcFHmQebt", + "name": "Spells", + "parentId": "ioei4uvDdGTAFqZrB", + "root": { + "id": "DDDDDDDDDDDDDDDDD", + "collection": "docs" + }, + "left": 31, + "right": 31, + "urlName": "spell", + "href": "/docs/property/spell", + "description": "Spells work similarly to [actions](/docs/property/action). They appear on the spells tab and can be cast with or without using up spell slots.\n\n---\n\n### Always prepared\nalwaysPrepared\n\nA spell that is always prepared does not count towards the spell list's maximum prepared spells and is always active and ready to cast.\n\n### Prepared\nprepared\n\nA prepared spell is ready to cast and counts against a spell list's maximum prepared spells.\n\n### Cast without spell slots\ncastWithoutSpellSlots\n\nWhen set, this spell can be cast without consuming spell slots. It will however consume its own uses and resources.\n\n### Name\nname\n\nThe name of the spell.\n\n### Level\nlevel\n\nThe minimum level at which the spell can be cast. Will also be its level when cast as a ritual or without spell slots.\n\n### School\nschool\n\nWhat school the spell belongs to.\n\n### Casting time\ncastingTime\n\nHow long the spell takes to Cast\n\n### Range\nrange\n\nThe range of the spell\n\n### Duration\nduration\n\nHow long the spell lasts\n\n### Components\n\nWhether the spell requires verbal, somatic, or material components and whether the spell is a ritual or requires concentration.\n\n##### Component Fields\n- `verbal` - Whether the spell has a verbal component.\n- `somatic` - Whether the spell has a verbal component.\n- `material` - The spell's material component, if it has one.\n- `concentration` - Whether the spell requires concentration.\n- `ritual` - Whether the spell can be cast as a ritual.\n\n### Target\ntarget\n\nWho this spell should apply to. The properties under the spell will be applied to the targets.\n\n- **Self** - The spell will apply its properties to the creature casting the spell\n- **Single Target** - The spell will apply its properties without a target (for now)\n- **Multiple Targets** - The spell will apply its properties without a target (for now)\n\n### Attack roll\nattackRoll\n\nA [computed field](/docs/concepts/computed-fields) which calculates the spell attack roll modifier. If this field is empty, no attack roll will be made. Use 0 to make an attack roll without a modifier. To use the spell list's attack roll bonus use `#spellList.attackRollBonus`.\n\nThe following variables may be added to the action scope when attack rolls are made:\n\n - `~attackDiceRoll` The value of the d20 roll before any modifiers were applied.\n - `~attackRoll` The total attack roll after modifiers.\n - `~criticalHit` Set to `true` if the attack roll's d20 is a natural 20. If `~criticalHitTarget` is set, the attack roll's d20 must instead be equal to or greater than `~criticalHitTarget` for this to be set to `true`.\n - `~criticalMiss` Set to `true` if the attack roll was not a critical hit and rolled a natural 1 on the d20 roll.\n - `~attackHit` If the attack roll is higher than or equal to the target's AC or a critical hit this is set to `true`. Remains unset if there is no target for the attack unless the attack is a critical hit.\n - `~attackMiss` If the attack roll is lower than the target's AC or a critical miss, this is set to `true`. Remains unset if there is no target for the attack unless the attack is a critical miss.\n\n### Summary\nsummary\n\nA brief overview of what the spell does. This will show in the log when the spell is cast. Allows [inline calculations](/docs/concepts/inline-calculations).\n\n### Description\ndescription\n\nA more detailed description of the spell. The description does not show in the log when the spell is cast. Allows [inline calculations](/docs/concepts/inline-calculations).\n\n### Resource\n\nA resource can be any attribute that has a variable name. If the resource attribute is less than the amount required, the spell can't be cast.\n\nIf you want to reduce an attribute when casting the spell, but want the spell to be applied regardless of the value of that attribute, consider using an [Attribute Damage](/docs/property/attribute-damage) property as a child of the spell instead. Also use Attribute Damage when the amount to reduce the attribute is determined by a dice roll rather than a stable computed number.\n\n#### Resource attribute\nresources.attributesConsumed.0.variableName\n\nThe variable name of the attribute that will be consumed when casting this spell.\n\n#### Resource quantity\nresources.attributesConsumed.0.quantity\n\nA [computed field](/docs/concepts/computed-fields) which determines how much of the attribute is required to apply this spell. This amount will be deducted from the attribute every time the spell is cast\n\n### Ammo\n\nAmmo represents items that are requied to cast the spell. If an item is not selected, or there is insufficient quantity of the selected item, the spell can't be appled.\n\n#### Ammo item\nresources.itemsConsumed.0.tag\n\nSpecify what tag an item must have to be considered valid ammo for this spell. Any item with this tag can be selected as ammo for this spell.\n\n#### Ammo quantity\nresources.itemsConsumed.0.quantity\n\nA [computed field](/docs/concepts/computed-fields) which determines how many of the selected items are required to cast this spell. The quantity is deducted from the total quantity of the item when this spell is applied.\n\n### Uses\nuses\n\nA [computed field](/docs/concepts/computed-fields) which determines how many times this spell can be used before it needs to be reset.\n\n### Uses used\nusesUsed\n\nHow many of this spell's uses have already been used. Should ideally be between 0 and the total uses available. This number is set to 0 when the spell has uses and its uses are reset.\n\n### Reset\nreset\n\nIf set, the uses used field is set to 0 at the appropriate time.\n\n- **Long rest** - Reset when the long rest button is pushed\n- **Short rest** - Reset when either the long or short rest button is pushed", + "published": true + }, + { + "_id": "AXBHkYpg8ABbyk6qz", + "name": "Toggles", + "parentId": "ioei4uvDdGTAFqZrB", + "root": { + "id": "DDDDDDDDDDDDDDDDD", + "collection": "docs" + }, + "left": 32, + "right": 32, + "urlName": "toggle", + "href": "/docs/property/toggle", + "published": true, + "description": "Toggles are a way to turn on and off parts of a creature. When a toggle is off, none of its children will be active.\n\nCalculated toggles should be avoided if possible, because while they offer a lot of power and flexibility to the creature engine, they often create [dependency loops](/docs/concepts/dependency-loops) that can be difficult to troubleshoot, causing parts of a creature to calculate incorrectly.\n\nCalculated toggles can be applied by [actions](/docs/property/action) and will apply their children if the condition is true, but they should be avoided in favor of [conditional branches](/docs/property/branch) which can do the same, but are more efficient.\n\n---\n\n### Name\nname\n\nThe name of the toggle.\n\n### Variable name\nvariableName\n\nThe name used to refer to the value of the toggle in calculations. Must start with a letter and be made up of only letters and numbers without spaces, symbols, or punctiation.\n\n### Show on character sheet\nshowUI\n\nIf set, the toggle with show a checkbox on the character sheet. A calculated toggle will show a disabled checkbox, filled in if the toggle's calculation returned `true` or a value that isn't 0.\n\n### State\n\n- **Enabled** - The toggle and its children are active (`enabled = true`)\n- **Disabled** - The toggle and its children are inactive (`disabled = true`)\n- **Calculated** - The active status of the toggle depends on the result of the condition. Use with caution. (`enabled = false && disabled = false`)\n\n### Condition\ncondition\n\nA [computed field](/docs/concepts/computed-fields) that determines if the toggle is active. Use with caution." + }, + { + "_id": "v7eRZRdMoDPah7ZtE", + "name": "Triggers", + "parentId": "ioei4uvDdGTAFqZrB", + "root": { + "id": "DDDDDDDDDDDDDDDDD", + "collection": "docs" + }, + "left": 33, + "right": 33, + "urlName": "trigger", + "href": "/docs/property/trigger", + "description": "Triggers apply their children whenever their condition is met. They work like [actions](/docs/property/action) that are taken automatically.\n\nWhen activated, triggers are treated as children of the property that triggered them. This means you have access to things like `#spellList.dc` when triggering from a spell, no matter where the trigger is on your sheet; however, you will not be able to access a trigger's actual ancestors in this way as normal.\n\n---\n\n### Name\nname\n\nThe name of the trigger.\n\n### Timing\ntiming\n\n- **Before** - The trigger is applied before the triggering event takes place\n- **After** - The trigger is fired after the triggering event\n\n### Event\nevent\n\n- **Do action** - While the creature is doing an action, the action property specified in *Event type* is applied\n- **Roll check** - The creature makes a check\n- **Attribute damaged or healed** - One of the creature's attributes changed value through attribute damage or manual adjustment\n- **Short or long rest**\n- **Short rest**\n- **Long Rest**\n\n### Event type\nactionPropertyType\n\nThe trigger will apply when this property type is applied by the action\n\n### Tags required\ntargetTags.0\n\nIf this trigger is fired by a property, the property must match these tags for the trigger to fire.\n\nAdditional tag lists can be created using either OR mode (a property with either all of the tags in the main list *or* all of the tags in an OR list will fire the trigger) or NOT mode (a property with any tag in a NOT list will not fire the trigger).\n\n##### Additional Tag List Fields\n- `extraTags.0.operation` - Whether this additional tag list is an OR list or a NOT list.\n- `extraTags.0.tags.0` - The tags in this additional tag list.\n\n### Condition\ncondition\n\nA [computed field](/docs/concepts/computed-fields) to determine if the trigger should fire. The trigger will fire if the condition field is empty or if it returns `true` or a value that isn't 0.\n\n### Description\ndescription\n\nA detailed description of the trigger. Allows [inline calculations](/docs/concepts/inline-calculations).", + "published": true + }, + { + "_id": "bTLn3sMpzxr7SAfD7", + "name": "Computed fields", + "parentId": "E2DFwsCoiKy2Rc9Mz", + "root": { + "id": "DDDDDDDDDDDDDDDDD", + "collection": "docs" + }, + "left": 34, + "right": 34, + "urlName": "computed-fields", + "href": "/docs/concepts/computed-fields", + "published": true, + "description": "Some fields in DiceCloud creature properties expect calculations. These fields are then computed by the DiceCloud engine.\n\nSome fields, like the value of an attribute, resolve down to a single number, while others, like the damage to deal in an attack, only simplify their calculation as far as they can, and then resolve down to a number when applied. Avoid adding dice rolls to calculations that expect to resolve down to a number, because they will re-roll every time the creature is recalculated, causing instability in the creature's stats.\n\n## Parser\n\nThe DiceCloud parser can understand the following syntax:\n\n| | |\n| :- | :- |\n| **Numbers** | `13`, `3.14` |\n| **Dice rolls** | `3d6`, `(1 + 2)d4`|\n| **Strings of text** | `'Some text'`, `\"some other text\"` |\n| **Boolean values** | `true` or `false`. When DiceCloud expects a boolean, `0`, an empty string `''` and `false` are all considered false by DiceCloud's engine, every other value is considered true. |\n| **Variable names** | `variableName` |\n| **Addition and subtraction** | `1 + 2 + 3`, `12 - 6` |\n| **Multiplication** | `6 * 4`, `12 * 2` = `24` |\n| **Division** | `6 / 2`, `12 / 4` = `3` |\n| **Exponents** | `3 ^ 2` Raise 3 to the power of 2 |\n| **Modulo** | Returns the remainder of a division operation `15 % 6` = `3` |\n| **AND** | `&` or `&&`: Returns the value of the right hand side if the left side is true `true & 'cat'` = `'cat'` |\n| **OR** | | or ||: Returns the left hand side if it is true, otherwise returns the right hand side 'dog' || 'cat' = `'dog'` |\n| **NOT** | `!` returns false if the value after it is true, otherwise returns false |\n| **Comparisons** | greater than: `>`, less than: `<`, greater than or equal to: `>=`, less than or equal to: `<=`, equal: `=` or `==` or `===`, not equal: `!=` or `!==` |\n| **If-else** | `condition ? resultIfTrue : resultIfFalse`, `level > 10 ? 'high tier' : 'low tier'` |\n| **Arrays** | lists of values `[3, 6, 9, 12]`. |\n| **Array Indexes** | A value can be chosen from an array using another set of square brackets: `[3, 6, 9, 12][2]` = `[6]` because `[2]` fetches the 2nd value in the array. Arrays start at 1 in DiceCloud so that level tables can have 20 entries and be accessed by `array[level]`. |\n| **Function calls** | `functionName(argument1, argument1)` See [Functions](/docs/concepts/functions) for a full list of available functions. |\n\n## Special variables\n\n### Built-in variables\n\nThese variables are added to the creature automatically when relevant. They can be overridden if needed by creating a property with the same variable name. They can also be targeted by effects.\n\n- `xp` - A total of all the experiences with xp added to the character sheet\n- `milestoneLevels` - A total of all the experiences with milestone levels added to the character sheet\n- `itemsAttuned` - Number of items the creature is attuned to\n- `weightEquipment` - Total weight of all equipment on the creature\n- `valueEquipment` - Total value of all equipment on the creature\n- `weightTotal` - Total weight of the creature's entire inventory\n- `valueTotal` - Total value of the creature's entire inventory\n- `weightCarried` - Total weight of all carried items and containers\n- `valueCarried` - Total value of all carried items and containers\n- `level` - The current level of the creature, including all class levels\n- `~criticalHitTarget` - Defaults to 20, the natural roll needed to consider an attack roll as a critical hit\n\n### Pseudo built-in variables\n\nThese variables are not created automatically, but are assumed to exist by various parts of the system. Your sheet won't break if they don't exist, but some things may not work as expected out of the box.\n\n- `proficiencyBonus` - The level-based bonus to add to your attacks, checks, and saves when proficiency is applicable.\n - Skills use `proficiencyBonus` to define their modifier by default, by multiplying it with their `proficiency` field.\n - Setting a spellcasting ability on a Spell List will autofill the Attack Roll Bonus and DC fields with a formula including `proficiencyBonus`.\n\n### Action variables\n\nThese variables are available during an action after the relevant property has been applied. For Advanced users, a [Roll](/docs/property/roll) can set these variables, overriding the default behavior.\n\n#### [Actions](/docs/property/action)\n\n- `~attackDiceRoll` - The value of the d20 roll before any modifiers were applied.\n- `~attackRoll` - The total attack roll after modifiers.\n- `~criticalHit` - Set to `true` if the attack roll's d20 is a natural 20. If `~criticalHitTarget` is set, the attack roll's d20 must instead be equal to or greater than `~criticalHitTarget` for this to be set to `true`.\n- `~criticalMiss` - Set to `true` if the attack roll was not a critical hit and rolled a natural 1 on the d20 roll.\n- `~attackHit` - If the attack roll is higher than or equal to the target's AC or a critical hit this is set to `true`. Remains unset if there is no target for the attack unless the attack is a critical hit.\n- `~attackMiss` - If the attack roll is lower than the target's AC or a critical miss, this is set to `true`. Remains unset if there is no target for the attack unless the attack is a critical miss.\n\n#### [Spells](/docs/property/spell)\n\n- `slotLevel` - The level at which the spell was cast, from 0 to 9.\n\nSpells can also use any of the variables from Actions if they're set to have an attack roll.\n\n#### [Skills](/docs/property/skill)\n\n- `~checkDiceRoll` - The value of the d20 roll before any modifiers were applied.\n- `~checkRoll` - The total check result after modifiers.\n\nNote that since Skills don't directly activate their children, these variables are only accessible by using a [Trigger](/docs/property/trigger) with the Roll Check event.\n\n#### [Damage](/docs/property/damage)\n\n- `~lastDamageType` - The type of damage dealt last, any damage that has the `extra` type will use this damage type instead\n\n#### [Saving throws](/docs/property/saving-throw)\n\n- `~saveFailed` - Set to `true` if the target failed its saving throw or there are no targets for the saving throw\n- `~saveSucceeded` - Set to `true` if the target made its saving throw or there are no targets for the saving throw\n- `~saveDiceRoll` - The unmodified d20 roll the target made to save\n- `~saveRoll` - The final value of the saving throw roll after modifiers\n\n## Ancestor references\n\nThe ancestors of a property can be accessed directly using the `#ancestorType` syntax. For example, a spell might need to know the save DC of the spell list that it is inside of, which can be accessed with `#spellList.dc`.\n\nTriggers and their children work differently: They don't have access to their own ancestors, but rather inherit the ancestors of the property that caused them to fire. For example, a trigger at the root of the creature's tree might be fired by a spell being cast, you can still use references to ancestors like `#spellList.attackRollBonus` inside that trigger as if it were under the spell itself." + }, + { + "_id": "o8u2Z5gZW54ZXNeZB", + "name": "Dependency loops", + "parentId": "E2DFwsCoiKy2Rc9Mz", + "root": { + "id": "DDDDDDDDDDDDDDDDD", + "collection": "docs" + }, + "left": 35, + "right": 35, + "urlName": "dependency-loops", + "href": "/docs/concepts/dependency-loops", + "published": true, + "description": "When a variable is referenced in a calculation, that calculation can be said to depend on that variable. In order for the calculation to compute, the value of the variable needs to be known.\n\nBut consider the following property values that could be added to a creature\n\n- The creature's Strength base value is set to `dexterity + 1` so that it will always have 1 more strength than dexterity\n- The creature's Dexterity base value is set to `constitution + 1` so that it will always have 1 more dexterity than constitution\n- The creature's Constitution base value is set to `strength` so that its constitution is always equal to its strength\n\nIt is not possible to resolve these calculations, not just because no values exist which satisfy the constraints, but because strength depends on dexterity which depends on constitution which depends on strength. None can be computed before the others have finalized their values. This is a dependency loop.\n\nMost dependency loops that appear in actual DiceCloud creatures are less trivial than this example, but they cause the same result: a sheet that can't be accurately computed. In these cases, DiceCloud does its best, chooses an order to resolve the calculations arbitrarily, and continues calculating. An error will show on the Build tab to let you know that something went wrong.\n\n![dependency loop example](/images/docs/dependency-loop.png)\n\n## Toggles\n\nCalculated [Toggles](/docs/property/toggle) are the main source of dependency loops on creatures, because they create a dependency that isn't as obvious as a calculation might be. When a toggle is in calculated mode, its children do not know whether they are active or not until the calculation is resolved. Because of this, every calculation under the toggle depends on the toggle's calculation, making the chance for a loop to be formed more likely the more children are under a toggle.\n\nConsider this example\n\n- A calculated toggle that is active if `strength < 10`\n- An effect under that toggle that adds 2 to `strength`\n\nThe effect can't compute, because it does not know if it is active yet, so the toggle must compute its calculation first. The toggle needs to know if `strength` is greater than 10. Strength depends on all of the effects targeting it, it must know if the +2 effect is active or not. This creates a dependency loop, because there is no valid order in which everything can be calculated.\n\n## Troubleshooting a dependency loop\n\n- First, identify all the properties that make up the dependency loop. These are linked in the dependency loop error message. The field names in square brackets after the property name indicates which calculations on the property are directly involved.\n- Move any properties in the loop out from being children of calculated Toggles\n- Use static values in place of variables where they are not strictly needed\n- Ask for [help](/feedback)" + }, + { + "_id": "KFkmXFLQrdPQNpJ7X", + "name": "Inline Calculations", + "parentId": "E2DFwsCoiKy2Rc9Mz", + "root": { + "id": "DDDDDDDDDDDDDDDDD", + "collection": "docs" + }, + "left": 36, + "right": 36, + "urlName": "inline-calculations", + "href": "/docs/concepts/inline-calculations", + "description": "Most long-format fields allow inline [calculations](/docs/concepts/computed-fields) to be included. Calculations inside of curly braces will be computed down to numbers using the characters stats.\n\nFor example a creature's strength attribute may have the following in its description: `Your carrying capacity is {strength * 15} lbs.`\n\nWhen the creature is calculated, if it has 8 strength, the action description will become: \"Your carrying capacity is 120 lbs.\"\n\nIf a description includes a dice roll, only the part that can be calculated to a single number should be included in the calculation braces: `The attack does an extra {paladin.level}d8 damage`, which becomes `The attack does an extra 4d8 damage`.\n\nDo not include the dice roll in the calculation: `The attack does an extra {(paladin.level)d8} damage`, because it will become `The attack does an extra 16 damage` but the number 16 will change every time the creature recalculates.", + "published": true + }, + { + "_id": "QFtqb7y5kLPDJoWXG", + "name": "Tags", + "parentId": "E2DFwsCoiKy2Rc9Mz", + "root": { + "id": "DDDDDDDDDDDDDDDDD", + "collection": "docs" + }, + "left": 37, + "right": 37, + "urlName": "tags", + "href": "/docs/concepts/tags", + "description": "Tags are used to reference multiple similar properties at once. A slot can require only properties from your library that has matching tags, effects and some other properties can also target properties to apply to by tags.\n\n## Built in tags\n\nProperties have specific tags automatically for use with tag-targeting. These aren't relevant for slots and finding properties in a library with specific tags.\n\n- `#type` Actions will have the `#action` tag, etc. Damage will either have the tag `#damage` or the tag `#healing` if the damage type is healing\n- `variableName` if a property has a variable name it will be included as a tag\n- The action type of an [action](/docs/property/action): `action`, `bonus`, `attack`, `reaction`, `free`, `long`, `event`\n- The type of damage done by a [damage](/docs/property/damage) property: `bludgeoning`, `slashing`, `...` \n- The skill type of a [skill](/docs/property/skill) property: `skill`, `save`, `check`, `tool`, `weapon`, `armor`, `language`, `utility`\n- The attribute type of an [attribute](/docs/property/attribute) property: `ability`, `stat`, `modifier`, `hitDice`, `healthBar`, `resource`, `spellSlot`, `utility`\n- When the property resets: `longRest`, `shortRest`", + "published": true + }, + { + "_id": "3dkEFEnwH4ShSY2BS", + "name": "Functions", + "parentId": "E2DFwsCoiKy2Rc9Mz", + "root": { + "id": "DDDDDDDDDDDDDDDDD", + "collection": "docs" + }, + "left": 38, + "right": 38, + "urlName": "functions", + "href": "/docs/concepts/functions", + "description": "## max\nReturns the largest of the given numbers\n`max(12, 6, 3, 168)` = `168`\n\n## min\nReturns the smallest of the given numbers\n`min(12, 6, 3, 168)` = `3`\n\n## round\nReturns the value of a number rounded to the nearest integer\n`round(5.95)` = `6`\n`round(5.5)` = `6`\n`round(5.05)` = `5`\n\n## floor\nRounds a number down to the next smallest integer\n`floor(5.95)` = `5`\n`floor(5.05)` = `5`\n`floor(5)` = `5`\n`floor(-5.5)` = `-6`\n\n## ceil\nRounds a number up to the next largest integer\n`ceil(5.95)` = `6`\n`ceil(5.05)` = `6`\n`ceil(5)` = `5`\n`ceil(-5.5)` = `-5`\n\n## trunc\nReturns the integer part of a number by removing any fractional digits\n`trunc(5.95)` = `5`\n`trunc(5.05)` = `5`\n`trunc(5)` = `5`\n`trunc(-5.5)` = `-5`\n\n## abs\nReturns the absolute value of the given number\n`abs(7)` = `7`\n`abs(-7)` = `7`\n\n## sign\nReturns either a positive or negative 1, indicating the sign of a number, or zero\n`sign(-3)` = `-1`\n`sign(3)` = `1`\n`sign(0)` = `0`\n\n## tableLookup\nReturns the index of the last value in the array that is less than the specified amount\n`tableLookup([100, 300, 900], 457)` = `2`\n`tableLookup([100, 300, 900], 23)` = `0`\n`tableLookup([100, 300, 900, 1200], 900)` = `3`\n`tableLookup([100, 300], 594)` = `2`\n\n## resolve\nForces the given calcultion to resolve into a number, even in calculations where it would usually keep the unknown values as is\n`resolve(someUndefinedVariable + 3 + 4)` = `7`\n`resolve(1d6)` = `4`\n\n## dropLowest\nDrops the lowest N dice results from a roll.\n`dropLowest(4d6, 1)` = `[3, 5, `~~`1`~~`, 2] = 10`\n`dropLowest(4d6, 2)` = `[3, 5, `~~`1`~~`, `~~`2`~~`] = 8`\n\n## dropHighest\nDrops the highest N dice results from a roll.\n`dropHighest(4d6, 1)` = `[3, `~~`5`~~`, 1, 2] = 6`\n`dropHighest(4d6, 2)` = `[`~~`3`~~`, `~~`5`~~`, 1, 2] = 3`\n\n## reroll\nRerolls any dice with a result below a certain threshold and replaces their results. If the third parameter is `true`, forces only a single reroll instead of rerolling until the threshold is met.\n`reroll(1d20, 5)` = `[3] -> [1] -> [15] = 15`\n`reroll(1d20, 5, true)` = `[3] -> [1] = 1`\n\n## explode\nRerolls any dice with a result above a certain threshold and adds their results to the total, repeating the process up to a maximum depth.\n`explode(4d6, 3, 6)` = `[3, 6, 1, 6] -> [3, 6, 1, 6, 6, 4] -> [3, 6, 1, 6, 6, 4, 6], -> [3, 6, 1, 6, 6, 4, 6, 6] = 38`\n`explode(4d6, 1, 6)` = `[3, 6, 1, 6] -> [3, 6, 1, 6, 6, 4] = 26`", + "published": true + }, + { + "_id": "AQGjqq6grmKXZN6dB", + "name": "Character Tree", + "parentId": "E2DFwsCoiKy2Rc9Mz", + "root": { + "id": "DDDDDDDDDDDDDDDDD", + "collection": "docs" + }, + "left": 39, + "right": 39, + "urlName": "tree", + "href": "/docs/concepts/tree", + "published": false, + "description": "TODO" + }, + { + "_id": "KTwfr2DJ5aubsEbLt", + "name": "Getting Started", + "left": 43, + "right": 43, + "urlName": "getting-started", + "href": "/docs/getting-started", + "root": { + "id": "DDDDDDDDDDDDDDDDD", + "collection": "docs" + }, + "description": "Get acquainted with the basics of how to use DiceCloud.", + "published": true, + "icon": { + "name": "open-book", + "shape": "M149.688 85.625c-1.234.005-2.465.033-3.72.063-33.913.806-75.48 10.704-127.25 33.718V362.78c60.77-28.82 106.718-37.067 144.22-33.092 33.502 3.55 59.685 16.66 83.562 31.187v-242.97c-23.217-17.744-50.195-30.04-85.97-32-3.52-.192-7.142-.296-10.843-.28zm211.968 0c-3.7-.016-7.322.088-10.844.28-35.773 1.96-62.75 14.256-85.968 32v242.97c23.876-14.527 50.06-27.637 83.562-31.188 37.502-3.974 83.45 4.272 144.22 33.094V119.407c-51.77-23.014-93.337-32.912-127.25-33.72-1.255-.028-2.486-.056-3.72-.06zm5.72 261.78c-1.038-.002-2.074.017-3.095.033-4.808.075-9.43.37-13.905.843-33.932 3.597-59.603 17.976-85.53 34.44v.28c-6.554-1.99-13.02-2.37-19.408-.97-25.566-16.177-51.003-30.202-84.468-33.75-5.595-.592-11.44-.883-17.564-.842-32.04.213-71.833 9.778-124.687 35.937v42.53c60.77-28.823 106.714-37.067 144.218-33.092 18.545 1.965 34.837 6.845 49.75 13.28-4.682 6.064-9.308 13.268-13.875 21.688h117.156c-5.93-8.22-11.798-15.414-17.626-21.56 14.996-6.503 31.39-11.43 50.062-13.408 37.503-3.974 83.448 4.27 144.22 33.094v-42.53c-53.16-26.31-93.115-35.863-125.25-35.97z" + } + }, + { + "_id": "fHH4zMWAGr2v6dJzq", + "name": "Creating a Character", + "parentId": "KTwfr2DJ5aubsEbLt", + "root": { + "id": "DDDDDDDDDDDDDDDDD", + "collection": "docs" + }, + "left": 44, + "right": 44, + "urlName": "creating-a-character", + "href": "/docs/getting-started/creating-a-character", + "description": "The first thing you'll most likely want to do upon joining DiceCloud is to create a character. To do so, you'll need to head to your character list to create one - use the ☰ button at the top left to open the menu, then click on Characters.\n\nAny time you see a circular red (+) button like below, you can click it to add something new, be it a character, a game statistic, or a note about your adventures. For now, start by clicking on the one on your character list to create a new character.\n\n\n\nOnce you've clicked that button, you should see a dialog that looks like below. Fill out your basic character details like name, alignment, and gender (you can always change these later), then click either Next or Create. \n\nIf you click Next, you'll be given an option to select which libraries you want your character to pull from; you should automatically be subscribed to a library for the freely available SRD materials from D&D 5th Edition, so we'll ignore this step for now. See [Using Libraries](/docs/getting-started/using-libraries) for details on how to get access to materials outside of the 5e SRD.\n\n\n\nNow that your character is created, you can select them from your character list, and you'll be taken to a page that looks like below. This is the Build tab, and is where you'll make most decisions regarding things like your character's ability scores, race, class, and background. By clicking on the cards outlined in red along the top, you can select from the freely available SRD materials to build your character as you see fit; new cards will appear as choices you make require additional detail, such as your class proficiencies or subclass.\n\n\n\nOnce you've selected your class, you should see a Level Up button appear on the card labeled \"Level\". You can click this button to level your character up when the time comes.\n\n\n\n---\n
\n\nNow that you've filled out your basic character choices, you can start exploring the rest of your character sheet. The tabs along the top each represent a different page, on which you'll find various different pieces of your character's information.\n\n- The Stats tab contains things like your hit points, ability scores, skills, and other similar information. Clicking on most stats or skills will allow you to roll for them, the results of which will show up in the log to the right (if you don't see it, click the button that looks like speech bubbles at the top right of your sheet).\n- The Actions tab contains clickable buttons to activate various abilities your character can use.\n- The Spells tab contains information about your character's spellcasting abilities, if applicable. You can hide it from your sheet if you don't want it by going to the Journal tab, clicking on your character's details, and opening Advanced.\n- The Inventory tab contains information about your character's items and money.\n- The Features tab contains information about what your character can do that's special and unique to them.\n- The Journal tab features your basic character information, and you can also add notes to track various things like your backstory or campaign notes.\n- The Tree tab is hidden by default, and allows advanced users to easily dig into the guts of how their sheet functions. You can enable it from the same place as hiding the Spells tab.\n\nIf you're ever in doubt about how something works, click on it! Most things on your sheet will either do something when clicked, or open up to show more details. Also keep the red (+) buttons in mind; most tabs have one, and clicking on them will allow you to add new things to your sheet depending on which tab you use it from. The best way to learn is by exploring.\n\n---\n
\n\n

Next: Using Libraries >

", + "icon": { + "name": "skills", + "shape": "M119.1 25v.1c-25 3.2-47.1 32-47.1 68.8 0 20.4 7.1 38.4 17.5 50.9L99.7 157 84 159.9c-13.7 2.6-23.8 9.9-32.2 21.5-8.5 11.5-14.9 27.5-19.4 45.8-8.2 33.6-9.9 74.7-10.1 110.5h44l11.9 158.4h96.3L185 337.7h41.9c0-36.2-.3-77.8-7.8-111.7-4-18.5-10.2-34.4-18.7-45.9-8.6-11.4-19.2-18.7-34.5-21l-16-2.5L160 144c10-12.5 16.7-30.2 16.7-50.1 0-39.2-24.8-68.8-52.4-68.8-2.9 0-4.7-.1-5.2-.1zM440 33c-17.2 0-31 13.77-31 31s13.8 31 31 31 31-13.77 31-31-13.8-31-31-31zM311 55v48H208v18h103v158h-55v18h55v110H208v18h103v32h80.8c-.5-2.9-.8-5.9-.8-9 0-3.1.3-6.1.8-9H329V297h62.8c-.5-2.9-.8-5.9-.8-9 0-3.1.3-6.1.8-9H329V73h62.8c-.5-2.92-.8-5.93-.8-9 0-3.07.3-6.08.8-9H311zm129 202c-17.2 0-31 13.8-31 31s13.8 31 31 31 31-13.8 31-31-13.8-31-31-31zm0 160c-17.2 0-31 13.8-31 31s13.8 31 31 31 31-13.8 31-31-13.8-31-31-31z" + }, + "published": true + }, + { + "_id": "pEKPMFngRH3LjKZxs", + "name": "Using Libraries", + "parentId": "KTwfr2DJ5aubsEbLt", + "root": { + "id": "DDDDDDDDDDDDDDDDD", + "collection": "docs" + }, + "left": 45, + "right": 45, + "urlName": "using-libraries", + "href": "/docs/getting-started/using-libraries", + "description": "Libraries are DiceCloud's way of both sharing and reusing user-created materials. \n\nWhen you first create your account, you should be automatically subscribed to a library for the materials freely available in D&D 5th Edition's \"System Reference Document\" (SRD). That library contains a Ruleset which will automatically be installed on new characters, which defines the structure of a character designed for use with that Ruleset - everything from hit points to skills and more. User-created libraries can define alternate Rulesets for 5e or other systems, as well as contain additional materials such as items or classes which can be used with a specific Ruleset.\n\nTo subscribe to a library, click on the link shared with you, then click the “Subscribe” button at the top right of the page. You can also find a directory of publicly shared, user-created libraries in the [community libraries browser](/community-libraries) or on DiceCloud's [Discord server](https://discord.gg/mv93t6QdEN).\n\n\n\nIn order to create a library, you'll need to be a [Patron](https://www.patreon.com/dicecloud) in at least the Adventurer ($5) tier, or be invited by someone of the Hero ($10) tier or higher. You can always use libraries others have created, even without any subscription.\n\n---\n
\n\n

< Previous: Creating a Character

\n

Next: Markdown Basics >

", + "icon": { + "name": "bookshelf", + "shape": "M169 57v430h78V57h-78zM25 105v190h46V105H25zm158 23h18v320h-18V128zm128.725 7.69l-45.276 8.124 61.825 344.497 45.276-8.124-61.825-344.497zM89 153v270h62V153H89zm281.502 28.68l-27.594 11.773 5.494 12.877 27.594-11.773-5.494-12.877zm12.56 29.433l-27.597 11.772 5.494 12.877 27.593-11.772-5.492-12.877zm12.555 29.434l-27.594 11.77 99.674 233.628 27.594-11.773-99.673-233.625zM25 313v30h46v-30H25zm190 7h18v128h-18V320zM25 361v126h46V361H25zm64 80v46h62v-46H89z" + }, + "published": true + }, + { + "_id": "AcqxstmYQB9K4Zfao", + "name": "Archiving Characters", + "parentId": "KTwfr2DJ5aubsEbLt", + "root": { + "id": "DDDDDDDDDDDDDDDDD", + "collection": "docs" + }, + "left": 46, + "right": 46, + "urlName": "archiving-characters", + "href": "/docs/getting-started/archiving-characters", + "description": "WIP\n\n---\n
\n\n

< Previous: Using Libraries

\n

Next: Markdown Basics >

", + "icon": { + "name": "cloud-download", + "shape": "M200.1 31.2A130.1 132.4 0 0 0 70.03 163.6a130.1 132.4 0 0 0 .55 11.3 80.98 73.47 0 0 0-52.21 68.6A80.98 73.47 0 0 0 99.35 317a80.98 73.47 0 0 0 37.25-8.3 189.3 80.97 0 0 0 78.4 16.5v-49.9h82v50.1a189.3 80.97 0 0 0 39.5-5.7 91.09 67.8 0 0 0 66 21.1 91.09 67.8 0 0 0 91.1-67.8 91.09 67.8 0 0 0-58-63.1 70.1 81.72 20.61 0 0 2.6-6.2 70.1 81.72 20.61 0 0-36.8-101.2 70.1 81.72 20.61 0 0-76.9 22.8 130.1 132.4 0 0 0-124.4-94.1zM233 293.3v112h-51.3l74.3 74.3 74.3-74.3H279v-112h-46z" + } + }, + { + "_id": "PmJC5TohrNQhMPYfb", + "name": "Markdown Basics", + "parentId": "KTwfr2DJ5aubsEbLt", + "root": { + "id": "DDDDDDDDDDDDDDDDD", + "collection": "docs" + }, + "left": 47, + "right": 47, + "urlName": "markdown-basics", + "href": "/docs/getting-started/markdown-basics", + "description": "DiceCloud uses Github-flavored Markdown for most of its text fields, such as descriptions and summaries. This page gives a quick overview of what you can do with it.\n\n## Inline Calculations\nThe primary feature unique to DiceCloud's version of Markdown is the ability to embed variables and calculations into your text using `{curly braces}`, for example `{8 + proficiencyBonus + charisma.modifier}`. See [Inline Calculations](/docs/concepts/inline-calculations) for more details.\n\n## Inline Text Formatting\n\nSyntax | Result\n:-- | :--\n`*italic*` | *italic*\n`**bold**` | **bold**\n`***bold italic***` | ***bold italic***\n`~~strikethrough~~` | ~~strikethrough~~\n`code` | `code`\n`[Links](https://youtu.be/dQw4w9WgXcQ)` | [Links](https://youtu.be/dQw4w9WgXcQ)\n\n## Headings\nStarting a line with `#` will create a heading. Adding additional `#`s will reduce the size of the heading in steps, for example `###`.\n\n## Lists\n```\n- Unordered\n- List\n- Items\n\n1. Ordered\n2. List\n3. Items\n```\n\n## Tables\nCreating a table like this, with the arrangement of `:-:` in the second row defining the alignment:\n```\nColumn 1 | Column 2 | Column 3\n:-- | :-: | --:\nLeft-aligned | Centered | Right-aligned\n```\n
\nWill result in a table like this:\n\nColumn 1 | Column 2 | Column 3\n:-- | :-: | --:\nLeft-aligned | Centered | Right-aligned\n\n## Images\n```\n![Dicecloud's logo](https://dicecloud.com/favicon-96x96.png)\n```\n![Dicecloud's logo](https://dicecloud.com/favicon-96x96.png)\n\n## Code Blocks\n
```\nfunction someCode() {\n  // Useful for displaying blocks of code!\n}\n```
\n\n## Inline HTML\nDiceCloud also allows you to use HTML in any text field that allows Markdown. This is an advanced feature, and not all features of HTML are allowed; you should really only use this if you know what you're doing.\n\n---\n
\n\n

< Previous: Using Libraries

", + "icon": { + "name": "scroll-quill", + "shape": "M311.9 47.95c-17.6 0-34.6.7-50.7 2.43L244.6 93.5l-4.9-40.04c-2.5.46-5 .94-7.5 1.47-9.1 1.94-15.1 7.22-20.3 14.87-5.2 7.65-8.9 17.5-12.1 26.6C191 121.5 184 148 178.4 175c6 5.1 12 10.3 17.9 15.4l30.7-17.6 33.8 26.1 51.9-19.7 61 24.5-6.8 16.7-54.4-21.8-54.7 20.7-32.2-24.9-14.9 8.5c19.6 17.3 38.6 34.4 56.5 51.2l14-6.4 33.9 16.1 31.2-13.1 24.2 23.3-12.4 13-15.8-15.1-27.6 11.7-33-15.8c6.9 6.7 13.6 13.2 20.1 19.7l1.7 1.8 19.5 76.3-7.8-5.7-53 .4-38.1-17.8-42.4 14.6-5.8-17 49.2-17 41.1 19.2 24.7-.2-70.7-51.7c-19.7 4.6-39.4 2.8-58.1-3.7-4.2 44.4-5.9 85.7-7 118.7-.4 10.7 2.7 23 7.5 32.5 4.9 9.5 11.7 15.4 15 16.1 5.2 1.2 19 3.2 37.7 5.1l12.4-39 19.1 41.7c16.7 1.2 35 2 53.5 2.2 28.2.3 57.1-.9 82-4.7 15.8-2.3 29.6-6 40.7-10.4-11.8-5.1-21.6-10.6-29.1-16.6-11.1-8.9-18.2-19.3-17.3-30.9v.2c5.4-96.4 10.8-188.8 30.3-286l.1-.4.1-.4c5.3-17.9 17.9-39.86 36.1-55.83-13.9-2.06-28.6-4-43.7-5.66l-22.3 25.3-2.2-27.7c-19-1.64-38.4-2.71-57.4-2.92h-5.7zm148.5 20.44c-4.7 3.69-9.2 8.03-13.3 12.73 12.1 8.18 21.4 23.38 21.8 36.98.3 7.8-1.9 14.9-7.7 21.4-5.8 6.4-15.6 12.4-31.6 15.8l3.8 17.6c18.6-4 32.3-11.5 41.2-21.4 9-9.9 12.7-22.2 12.3-34-.6-19.3-11.1-37.59-26.5-49.11zM25.44 71.91c-.24 1.61-.38 3.43-.38 5.62.1 7.69 2.03 18.17 5.83 30.17 3.41 10.7 8.27 22.5 14.35 34.8 10.63-5.3 20.59-11 28.41-18.1-4.42 12.5-10.15 24.7-18.6 36.5 4.14 7.2 8.63 14.4 13.45 21.5 10.64-5.3 20.72-13 29.52-26.1-3.3 16-8.47 30.6-18.27 41.8 6.53 8.5 13.5 16.8 20.75 24.5 8.7-9.3 15.6-21 20.7-34.9 3.8 18.5 2.6 35.3-5.7 49.4 8 7.2 16.3 13.7 24.8 19.1 6.1-14 8.9-30.6 8.5-49.7 9.2 23.7 11.3 42.9 9.6 59.5 20.2 9.2 40.8 12 61.3 6.1l4.2-1.3 69.3 50.6-5.9-22.8c-73-72.8-175.4-156.7-261.86-226.69zM312.8 123.9l33.2 13.8 31.3-9.9 5.4 17.2-37.5 11.9-33.6-14-28.8 8.1-4.8-17.4zm107.3 236.2c-.7 0-1.3.1-2 .1-3.5.1-7.2.5-11.1 1.3l3.4 17.6c12.2-2.3 20-.4 24.5 2.5 4.4 2.9 6.3 6.8 6.4 12.5.1 9.3-7 23-23.3 32.5 5.4 2.9 11.9 5.9 19.3 8.7 14.4-11.6 22.1-26.8 22-41.4-.1-10.7-5.2-21.2-14.6-27.4-6.7-4.3-15-6.5-24.6-6.4z" + }, + "published": true + }, + { + "_id": "DHWmM4xsYRcMCHjGJ", + "name": "Universal Fields", + "parentId": "ioei4uvDdGTAFqZrB", + "root": { + "id": "DDDDDDDDDDDDDDDDD", + "collection": "docs" + }, + "left": 48, + "right": 48, + "urlName": "universal-fields", + "href": "/docs/property/universal-fields", + "description": "Many fields are shared by multiple property types, as their functionality is linked more closely to Dicecloud's core than to the functionality of any specific property.\n\n---\n\n## All Properties\nThese fields exist on all properties, regardless of type.\n\n### Icon\nA custom icon from [Game-Icons.net](https://game-icons.net). The property type's default icon is used instead if unset.\n\n### Color\ncolor\n\nThe visual color of the property. Unlike in V1, this has no effect on sorting and is purely visual.\n\n### Tags\ntags.0\n\nSmall pieces of text that can be used to identify groups of similarly-tagged properties. See [Tags](/docs/concepts/tags) for more details.\n\n---\n\n## All Action / Activated Properties\nThese fields exist on all properties which either act as actions (Actions, Spells, Triggers), or are activated by a parent action. See [Static and Activated Properties](https://dicecloud.com/docs/concepts/static-and-activated-properties) for more details.\n\n### Don't show in log\nsilent\n\nWhen this is set, the attribute damage is applied, but does not show in the log.\n\n---\n\n## Library-Only Fields\nThese fields exist on all properties, but are only accessible when editing them in a library, and are hidden when the properties are added to a character sheet.\n\n### Can fill slots\nIf checked, the property can be found by slots with conditions that match it.\n\n### Searchable from character sheet\nIf checked, the property can be found when adding a property of its type via the (+) button.\n\n### Slot fill type\nOverrides the property's apparent type when filling slots that specify a property type.\n\n### Slot quantity filled\nHow much of a slot's quantity this property should consume when chosen. Treated as 1 if unset.\n\n### Condition\nA true/false condition formula that must return `true` for the property to fill a slot. Ignored if unset.\n\n### Condition error text\nText to be displayed as a reason when the property's Condition returns `false`. The formula in Condition is shown instead if unset.\n\n### Library Tags\n[Tags](/docs/concepts/tags) that can be used to find and distinguish the property while in a library. These are used instead of the Tags field when filling slots or searching with the (+) button.", + "published": true + }, + { + "_id": "FK8dPsce7KeH9pFL5", + "name": "API", + "left": 49, + "right": 49, + "urlName": "api", + "href": "/docs/api", + "root": { + "id": "DDDDDDDDDDDDDDDDD", + "collection": "docs" + }, + "icon": { + "name": "cloud-download", + "shape": "M200.1 31.2A130.1 132.4 0 0 0 70.03 163.6a130.1 132.4 0 0 0 .55 11.3 80.98 73.47 0 0 0-52.21 68.6A80.98 73.47 0 0 0 99.35 317a80.98 73.47 0 0 0 37.25-8.3 189.3 80.97 0 0 0 78.4 16.5v-49.9h82v50.1a189.3 80.97 0 0 0 39.5-5.7 91.09 67.8 0 0 0 66 21.1 91.09 67.8 0 0 0 91.1-67.8 91.09 67.8 0 0 0-58-63.1 70.1 81.72 20.61 0 0 2.6-6.2 70.1 81.72 20.61 0 0-36.8-101.2 70.1 81.72 20.61 0 0-76.9 22.8 130.1 132.4 0 0 0-124.4-94.1zM233 293.3v112h-51.3l74.3 74.3 74.3-74.3H279v-112h-46z" + }, + "description": "DiceCloud implements a REST API.", + "published": true + }, + { + "_id": "BTigqz2SdpxHW5MJi", + "name": "Site Health Check", + "parentId": "FK8dPsce7KeH9pFL5", + "root": { + "id": "DDDDDDDDDDDDDDDDD", + "collection": "docs" + }, + "left": 51, + "right": 51, + "urlName": "status", + "href": "/docs/api/status", + "icon": { + "name": "heart-beats", + "shape": "M366.688 30.027c-1.01-.01-2.022-.01-3.034.004h.002c-41.495.563-83.634 22.155-110.844 69.282-41.912-117.77-236.49-76.29-232 64.5.64 20.068 5.132 38.987 12.454 56.917h76.45l21.22-74.126 26.375 90.134 18.46-64.312 17.238 48.303H328.1l21.222-74.126 26.375 90.13 18.46-64.308 17.238 48.303h72.517c7.097-18.183 10.927-37.136 10.307-56.917-2.61-83.04-63.874-133.082-127.533-133.786zM131.125 211.34l-7.842 27.39h-81.58c54.51 103.006 197.737 172.59 216.172 241.395 16.782-62.62 165.07-139.482 217.855-241.396h-77.023l-2.69-7.542-20.154 70.208-26.353-90.054-7.84 27.387H180.32l-2.69-7.54-20.15 70.206-26.355-90.056z" + }, + "description": "Checks if DiceCloud is running and has a connection to the database.\n\n### Request\n\n`GET https://dicecloud.com/api/status`\n\n### Expected Response\n\n```\n{ status: ok }\n```", + "published": true + }, + { + "_id": "va9XDZgM9PQX674zu", + "name": "Creature", + "parentId": "FK8dPsce7KeH9pFL5", + "root": { + "id": "DDDDDDDDDDDDDDDDD", + "collection": "docs" + }, + "left": 52, + "right": 52, + "urlName": "creature", + "href": "/docs/api/creature", + "icon": { + "name": "person", + "shape": "M250.882 22.802c-23.366 3.035-44.553 30.444-44.553 65.935 0 19.558 6.771 36.856 16.695 48.815l11.84 14.263-18.217 3.424c-12.9 2.425-22.358 9.24-30.443 20.336-8.085 11.097-14.266 26.558-18.598 44.375-7.843 32.28-9.568 71.693-9.842 106.436h42.868l11.771 157.836c29.894 6.748 61.811 6.51 90.602.025l10.414-157.86h40.816c-.027-35.169-.477-75.126-7.584-107.65-3.918-17.934-9.858-33.372-18.04-44.343-8.185-10.97-18.08-17.745-32.563-19.989l-18.592-2.88 11.736-14.704c9.495-11.897 15.932-28.997 15.932-48.082 0-37.838-23.655-65.844-49.399-65.844z" + }, + "description": "Gets a creature's data.\n\n## Request\n\n```\nGET https://dicecloud.com/api/creature/:id\n```\n\nWhere `:id` is the character id found in the URL of the creature.\n\n#### Headers\n\n```\nAuthorization: Bearer \n```\n\nWhere your token was obtained by [logging in](/docs/api/login)\n\n## Example\n\nIf the URL of the creature is `https://dicecloud.com/character/vzZniXrGpaxNAWPPu/Character-Name`, the id would be `vzZniXrGpaxNAWPPu`.\n\nThe token received from logging in was `QL3NZHFeQIL5qXw9tvYy5M69iS35zTPfydr-wcwZqM3`\n\n#### Request Body\n\n```\nGET https://dicecloud.com/api/creature/vzZniXrGpaxNAWPPu\n```\n\n#### Request Header\n\n```\nAuthorization: Bearer QL3NZHFeQIL5qXw9tvYy5M69iS35zTPfydr-wcwZqM3\n```\n\n## Expected Response\n\nThe API will return an object with keys whose values are arrays of database documents of the specified type. The `creatures` and `creatureVariables` each contain a single document, while `creatureProperties` will be an array of all of the properties of a creature.\n\n```\n{ \n \"creatures\": [ {\n \"_id\": \"vzZniXrGpaxNAWPPu\",\n ...\n } ],\n \"creatureProperties\": [ ... all the properties of the creature ],\n \"creatureVariables\": [ {\n \"variableName\": { ... },\n ...\n } ]\n}\n```", + "published": true + }, + { + "_id": "oLcjN6nQMymjMqMkg", + "name": "Login to api", + "parentId": "FK8dPsce7KeH9pFL5", + "root": { + "id": "DDDDDDDDDDDDDDDDD", + "collection": "docs" + }, + "left": 50, + "right": 50, + "urlName": "login", + "href": "/docs/api/login", + "description": "### Request\n\n `POST https://dicecloud.com/api/login`\n\n #### Body:\n```\n{\n \"username\": \"\",\n \"password\": \"\"\n}\n```\n\n #### Alternative Body:\n```\n{\n \"email\": \"\",\n \"password\": \"\"\n}\n```\n\n#### Successful response:\n```\n{\n \"id\": \"\",\n \"token\": \"\",\n \"tokenExpires\": \"\"\n}\n```\n\nYour token may expire before the given date, since each user has a limited pool of login tokens. If you get a permission error, you may need to login again to refresh your token.\n\nOnce you have your token, you can use it as a standard bearer token header\nin other API endpoints:\n\n`GET https://dicecloud.com/api/some/endpoint`\nheaders: `{ Authorization: \"Bearer \" }`", + "icon": { + "name": "key", + "shape": "M356.5 16.375l-174.906 255.22 1.53 1.06 31.97 22.314 175.062-255.5L356.5 16.374zm90.063 62.22c-20.16 29.418-44.122 23.1-68.25 8.905l-48.688 72.875c21.278 16.55 36.46 35.645 18.594 61.72l42.967 29.468 28.907-42.157-14.72-9.156c-3.167 1.844-6.85 2.906-10.78 2.906-11.85 0-21.47-9.62-21.47-21.47 0-11.847 9.62-21.436 21.47-21.436s21.437 9.59 21.437 21.438c0 .195-.025.4-.03.593l15.906 9.907 17.938-26.218-37.688-23.5 11.03-17.72 14.94 9.313 10.093-16.188 24.25 15.094 17.092-24.94-43-29.436zM141.22 268.624c-.31.01-.628.023-.94.063-.827.104-1.652.284-2.53.562-3.51 1.11-7.4 4.066-10.125 7.938-2.724 3.87-4.16 8.487-4 12.125.16 3.637 1.257 6.338 5.25 9.125l76.594 53.468c3.283 2.293 5.727 2.35 9.124 1.156 3.396-1.192 7.323-4.26 10.125-8.218 2.8-3.96 4.352-8.66 4.31-12.188-.04-3.53-.89-5.787-4.374-8.22L148.03 270.97c-2.546-1.78-4.657-2.42-6.81-2.345zM84.28 312.78c-24.354.41-45.504 9.52-57.655 27.25-16.95 24.737-11.868 59.753 9.625 90.283-1.838 4.72-2.875 9.84-2.875 15.187 0 23.243 19.07 42.313 42.313 42.313 8.635 0 16.692-2.625 23.406-7.125 43.208 18.488 88.07 12.714 108.28-16.782 18.695-27.28 10.884-66.912-16.374-99.312l-63.094-44.03c-14.016-5.107-28.07-7.7-41.25-7.783-.792-.004-1.59-.012-2.375 0zm-8.593 109.126c13.143 0 23.594 10.45 23.594 23.594 0 13.143-10.45 23.625-23.593 23.625-13.142 0-23.624-10.482-23.624-23.625s10.482-23.594 23.624-23.594z" + }, + "published": true + }, + { + "_id": "BgQSmFqSLWQQ5T6u8", + "name": "Static and Activated Properties", + "parentId": "E2DFwsCoiKy2Rc9Mz", + "root": { + "id": "DDDDDDDDDDDDDDDDD", + "collection": "docs" + }, + "left": 54, + "right": 54, + "urlName": "static-and-activated-properties", + "href": "/docs/concepts/static-and-activated-properties", + "description": "Most properties on DiceCloud fall into one of two categories: static or activated. Static properties are those which stay active on the sheet and have a continuous effect, whereas activated properties only affect the sheet when they're a child of an activated Action, Spell, or Trigger.\n\n---\n\n## Action Properties\nAction properties are a special type of static property that, given some type of input, can trigger activated properties. They form the core of DiceCloud's action system, and activated properties have no effect on the sheet without being triggered by an action property.\n\n**Types of Action Properties**\n- [Actions](/docs/property/action) - activated by clicking the respective button on the Actions tab\n- [Spells](/docs/property/spell) - activated by casting the spell from the Spells tab\n- [Triggers](/docs/property/trigger) - activated automatically upon customizable conditions\n\n---\n\n## Static Properties\nStatic properties make up the bulk of a DiceCloud sheet. They form all of the content of a sheet which is \"always accessible\" or \"always active\", as well as anything that changes without an Action, Spell, or Trigger being activated.\n\n**Types of Static Properties**\n- [Attributes](/docs/property/attribute)\n- [Classes](/docs/property/class)\n- [Class levels](/docs/property/class-level)\n- [Constants](/docs/property/constant)\n- [Containers](/docs/property/container)\n- [Damage multipliers](/docs/property/damage-multiplier)\n- [Effects](/docs/property/effect)\n- [Features](/docs/property/feature)\n- [Folders](/docs/property/folder)\n- [Items](/docs/property/item)\n- [Notes](/docs/property/note)\n- [Point buys](/docs/property/point-buy)\n- [Proficiencies](/docs/property/proficiency)\n- [Skills](/docs/property/skill)\n- [Slots](/docs/property/slot)\n- [Spell lists](/docs/property/spell-list)\n- [Toggles](/docs/property/toggle)\n\n---\n\n## Activated Properties\nActivated properties are used by DiceCloud's action system to implement behavior for Actions, Spells, and Triggers. They only affect your sheet when their parent action is activated.\n\n**Types of Activated Properties**\n- [Attribute damage](/docs/property/attribute-damage)\n- [Branches](/docs/property/branch)\n- [Damage](/docs/property/damage)\n- [Remove buffs](/docs/property/remove-buff)\n- [Rolls](/docs/property/roll)\n- [Saving throws](/docs/property/saving-throw)\n\n---\n\n## Buffs\nIn many cases, it's desirable for an action to temporarily cause some static behavior. This is where the [Buff](/docs/property/buff) property comes in - it bridges the gap between activated and static properties. When activated, a Buff will create a copy of itself outside of its parent action, and any of its static children will become active. For example, an action that grants 10 temporary hit points would most likely use a Buff containing an Effect.\n\nOnce activated, a Buff will remain active until manually removed or targeted by a Remove Buff property.\n\n---\n\n## Activated Behavior of Static Properties\nAs a general rule, properties are either static or activated, and do nothing in the other context. However, there are some properties that, though normally static, do have special behavior when activated.\n\n- Nested Actions and Spells will activate as if manually clicked.\n- Notes will add their title, summary, and description to the action's log output.\n- Toggles will behave like Branches set to \"If condition is true\" mode. [Deprecated]", + "published": true + }, + { + "_id": "YwZJywH95Q953BK7j", + "name": "References", + "parentId": "ioei4uvDdGTAFqZrB", + "root": { + "id": "DDDDDDDDDDDDDDDDD", + "collection": "docs" + }, + "left": 55, + "right": 55, + "urlName": "reference", + "href": "/docs/property/reference", + "description": "References are a special property type that only exists in libraries, and serve as a \"shortcut\" to make a copy of another library property when added to a sheet. By using references, you can avoid duplicating created library properties that need to be commonly reused, and thus also avoid having to edit them in multiple places to make changes.\n\nWhen a reference is added to a sheet, whether via slot or the (+) button, it will be replaced by a copy of the property it links to. Any children of the reference will become children of the copied property as well, which allows for a level of abstraction by creating multiple references with different children.\n\nReferences cannot be created on a character sheet and will be automatically replaced when added from a library. ~~The only way a reference can exist on a sheet is if its linked property no longer exists; in this case, the reference property will continue to exist and serve as an error message.~~ (This behavior is currently bugged; broken references will not be added to the sheet and their child properties will be added to the referenced property's intended parent's children instead.)\n\n---\n\n### Linked Property\nThe property to be copied by the reference when inserted.", + "published": true + } +] \ No newline at end of file diff --git a/app/private/docs/dependency-loops.md b/app/private/docs/dependency-loops.md deleted file mode 100644 index 20f8d82f..00000000 --- a/app/private/docs/dependency-loops.md +++ /dev/null @@ -1,33 +0,0 @@ -# Dependency loops - -When a variable is referenced in a calculation, that calculation can be said to depend on that variable. In order for the calculation to compute, the value of the variable needs to be known. - -But consider the following property values that could be added to a creature - -- The creature's Strength base value is set to `dexterity + 1` so that it will always have 1 more strength than dexterity -- The creature's Dexterity base value is set to `constitution + 1` so that it will always have 1 more dexterity than constitution -- The creature's Constitution base value is set to `strength` so that its constitution is always equal to its strength - -It is not possible to resolve these calculations, not just because no values exist which satisfy the constraints, but because strength depends on dexterity which depends on constitution which depends on strength. None can be computed before the others have finalized their values. This is a dependency loop. - -Most dependency loops that appear in actual DiceCloud creatures are less trivial than this example, but they cause the same result: a sheet that can't be accurately computed. In these cases, DiceCloud does its best, chooses an order to resolve the calculations arbitrarily, and continues calculating. An error will show on the Build tab to let you know that something went wrong. - -![dependency loop example](/images/docs/dependency-loop.png) - -## Toggles - -Calculated [Toggles](/docs/property/toggle) are the main source of dependency loops on creatures, because they create a dependency that isn't as obvious as a calculation might be. When a toggle is in calculated mode, its children do not know whether they are active or not until the calulation is resolved. Because of this, every calculation under the toggle depends on the toggles calaculation, making the chance for a loop to be formed more likely the more children are under a toggle. - -Consider this example - -- A calculated toggle that is active if `strength < 10` -- An effect under that toggle that adds 2 to `strength` - -The effect can't compute, because it does not know if it is active yet, so the toggle must compute its calculation first. The toggle needs to know if `strength` is greater than 10. Strength depends on all of the effects targeting it, it must know if the +2 effect is active or not. This creates a dependency loop, because there is no valid order in which everything can be calculated. - -## Troubleshooting a dependency loop - -- First, identify all the properties that make up the dependency loop. These are linked in the depdency loop error message. The field names in square brackets after the property name indicates which calculations on the property are directly involved. -- Move any properties in the loop out from being children of calculated Toggles -- Use static values in place of variables where they are not stricly needed -- Ask for [help](/feedback) diff --git a/app/private/docs/docs.md b/app/private/docs/docs.md deleted file mode 100644 index 424a157e..00000000 --- a/app/private/docs/docs.md +++ /dev/null @@ -1,39 +0,0 @@ -# DiceCloud Docs - -## Properties - -- ### [Action](/docs/property/action) -- ### [Attribute](/docs/property/attribute) -- ### [Attribute Damage](/docs/property/attribute-damage) -- ### [Buff](/docs/property/buff) -- ### [Remove Buff](/docs/property/remove-buff) -- ### [Branch](/docs/property/branch) -- ### [Class](/docs/property/class) -- ### [Class Level](/docs/property/class-level) -- ### [Constant](/docs/property/constant) -- ### [Container](/docs/property/container) -- ### [Damage](/docs/property/damage) -- ### [Damage Multiplier](/docs/property/damage-multiplier) -- ### [Effect](/docs/property/effect) -- ### [Feature](/docs/property/feature) -- ### [Item](/docs/property/item) -- ### [Note](/docs/property/note) -- ### [Point Buy](/docs/property/point-buy) -- ### [Proficiency](/docs/property/proficiency) -- ### [Roll](/docs/property/roll) -- ### [Saving Throw](/docs/property/saving-throw) -- ### [Skill](/docs/property/skill) -- ### [Slot](/docs/property/slot) -- ### [Slot Filler](/docs/property/slot-filler) -- ### [Spell List](/docs/property/spell-list) -- ### [Spell](/docs/property/spell) -- ### [Toggle](/docs/property/toggle) -- ### [Trigger](/docs/property/trigger) - -## Topics - -- ### [Computed fields](/docs/computed-fields) -- ### [Inline Calculations](/docs/inline-calculations) -- ### [Dependency Loops](/docs/dependency-loops) -- ### [Functions](/docs/functions) -- ### [Tags](/docs/tags) diff --git a/app/private/docs/inline-calculations.md b/app/private/docs/inline-calculations.md deleted file mode 100644 index 3d7c0f4d..00000000 --- a/app/private/docs/inline-calculations.md +++ /dev/null @@ -1,11 +0,0 @@ -# Inline Calculations - -Most long-format fields allow inline [calculations](/docs/computed-fields) to be included. Calculations inside of curly bracers will be computed down to numbers using the characters stats. - -For example a creature's strength attribute may have the following in its description: `Your carrying capacity is {strength * 15} lbs.` - -When the creature is calculated, if it has 8 strength, the action description will become: "Your carrying capacity is 120 lbs." - -If a description includes a dice roll, only the part that can be calculated to a single number should be included in the calulation bracers: `The attack does an extra {paladin.level}d8 damage`, which becomes `The attack does an extra 4d8 damage`. - -Do not inlclude the dice roll in the calaculation: `The attack does an extra {(paladin.level)d8} damage`, because it will become `The attack does an extra 16 damage` but the number 16 will change every time the creature recalculates. diff --git a/app/private/docs/property/action.md b/app/private/docs/property/action.md deleted file mode 100644 index ca1ae0ab..00000000 --- a/app/private/docs/property/action.md +++ /dev/null @@ -1,113 +0,0 @@ -# Actions - -Actions are things your character can do. When an action is taken, all the properties under it are applied. - -Add actions to your character sheet, then add children under the action to determine what happenes when the action is applied. - -When an action is applied it will create an entry in the character's log detailing all the properties that were applied and what their results were. - -The following properties can all be applied by an action: - - - [Attribute Damage](/docs/property/attribute-damage) - - [Branches](/docs/property/branch) - - [Buffs](/docs/property/buff) - - [Buff Removers](/docs/property/remove-buff) - - [Damage](/docs/property/damage) - - [Notes](/docs/property/note) - - [Rolls](/docs/property/roll) - - [Saving Throws](/docs/property/saving-throw) - - Other actions - ---- - -### Name - -The name of the action. - -### Action type - -How long the action takes to perform. - -Allows [inline calculations](/docs/inline-calculations). - -### Attack roll - -A [computed field](/docs/computed-fields) which calculates the attack roll modifier. If this field is empty, no attack roll will be made. Use 0 to make an attack roll without a modifier. - -The following variables may be added to the action scope when attack rolls are made: - - - `$attackDiceRoll` The value of the d20 roll before any modifiers were applied. - - `$attackRoll` The total attack roll after modifiers. - - `$criticalHit` Set to `true` if the attack roll's d20 is a natural 20. If `criticalHitTarget` is set, the attack roll's d20 must instead be equal to or greater than `criticalHitTarget` for this to be set to `true`. - - `$criticalMiss` Set to `true` if the attack roll was not a critical hit and rolled a natural 1 on the d20 roll. - - `$attackHit` If the attack roll is higher than or equal to the target's AC or a critical hit this is set to `true`. Remains unset if there is no target for the attack unless the attack is a critical hit. - - `$attackMiss` If the attack roll is lower than the target's AC or a critical miss, this is set to `true`. Remains unset if there is no target for the attack unless the attack is a critical miss. - -### Summary - -A brief overview of what the action does. This will appear in the action card, and shows in the log when the action is applied. - -Allows [inline calculations](/docs/inline-calculations). - -### Description - -A more detailed description of the action. The description does not show in the action card or the log when the action is applied. - -Allows [inline calculations](/docs/inline-calculations). - -### Resource - -A resource can be any attribute that has a variable name. If the resource attribute is less than the amount required, the action can't be applied. - -If you want to reduce an attribute when taking the action, but want the action to be applied regardless of the value of that attribute, consider using an [Attribute Damage](/docs/property/attribute-damage) property as a child of the action instead. Also use Attribute Damage when the amount to reduce the attribute is determined by a dice roll rather than a stable computed number. - -#### Resource attribute - -The variable name of the attribute that will be consumed when taking this action. - -#### Resource quantity - -A [computed field](/docs/computed-fields) which determines how much of the attribute is required to apply this action. This amount will be deducted from the attribute every time the action is taken. - -### Ammo - -Ammo represents items that are requied to take the action. If an item is not selected, or there is insufficient quantity of the selected item, the action can't be appled. - -#### Ammo item - -Specify what tag an item must have to be considered valid ammo for this action. Any item with this tag can be selected as ammo for this action. - -#### Ammo quantity - -A [computed field](/docs/computed-fields) which determines how many of the selected items are required to take this action. The quantity is deducted from the total quantity of the item when this action is applied. - -### Tags - -See [Tags](/docs/tags) - -### Target - -Who this action should apply to. The properties under the action will be applied to the Targets. - -- **Self** The action will apply its properties to the creature taking the action -- **Single Target** The action will apply its properties without a target (for now) -- **Multiple Targets** The action will apply its properties without a target (for now) - -### Uses - -A [computed field](/docs/computed-fields) which determines how many times this action can be used before it needs to be reset. - -### Uses used - -How many of this action's uses have already been used. Should ideally be between 0 and the total uses available. This number is set to 0 when the action has uses and its uses are reset. - -### Don't show in log - -When this is true, the action does not show up in the log. This does not stop the action's children from appearing in the log when they are applied. - -### Reset - -If set, the uses used field is set to 0 at the appropriate time. - -- **Long rest** Reset when the long rest button is pushed -- **Short rest** Reset when either the long or short rest button is pushed diff --git a/app/private/docs/property/attribute-damage.md b/app/private/docs/property/attribute-damage.md deleted file mode 100644 index 166f864c..00000000 --- a/app/private/docs/property/attribute-damage.md +++ /dev/null @@ -1,33 +0,0 @@ -# Attribute Damage - -When applied, attribute damage reduces the value of the attribute by some amount or set the value of an attribute to some amount. Attribute damage can by applied by actions or triggers. - -Using a negative value to damage an attribute will heal the attribute instead. - ---- - -### Attribute - -The variable name of the attribute to target. - -### Amount - -A [computed field](/docs/computed-fields) which determined the amount to damage the attribute or set the attribute's value to. - -### Operation - -- **Damage** Reduce the value of the attribute by the amount, negative values heal the attribute instead -- **Set** Set the value of the attribute to the amount - -### Target - -- **Target** Apply the attribute damage to the same target as the action applying this property -- **Self** Apply the attribute damage to the creature taking the action - -### Tags - -See [Tags](/docs/tags) - -### Don't show in log - -When this is set, the attribute damage is applied, but does not show in the log. diff --git a/app/private/docs/property/attribute.md b/app/private/docs/property/attribute.md deleted file mode 100644 index 156289f5..00000000 --- a/app/private/docs/property/attribute.md +++ /dev/null @@ -1,76 +0,0 @@ -# Attribute - -Attributes represent the numerical values of the creature. - -Attributes can be targeted by [effects](/docs/property/effect) which can change their total value in a non-destructive way. For example, if a class level gives you an ability score increase of +2 strength when it is taken, instead of directly editing the strength attribute, you add an effect to the class level that adds 2 to strength. The total value of strength will increase by 2 and it will show a record of that ability score increase and where it came from. - -Attributes, [skills](/docs/properties/skill), and [effects](/docs/property/effect) are the core properties of DiceCloud's creature engine. - -Attributes have the following fields that can be accessed in calculations with `variableName.field`: - -- `.total` The total of the attribute before being damaged -- `.damage` the amount of damage the attribute has taken -- `.value` The current value of the attribute including damage. `variableName` and `variableName.value` are equivalent. -- `.modifier` If the attribute is an ability, this is its roll modifier, eg. `strength.modifier` is +2 when `strength.value` is 14 - ---- - -### Base value - -A [computed field](/docs/computed-fields) that determines the starting value of the attribute before it is modified by effects and other properties. Multiple properties can set the base value for a given variable name, when this happens the highest base value is chosen, and then all other effects are applied. - -### Name - -The name of the attribute - -### Variable name - -The name used to refer to the attribute in calculations and by effects. Must start with a letter and be made up of only letters and numbers without spaces, symbols, or punctiation. - -If multiple attributes share a variable name, only the last attribute on the [character tree](/docs/tree) will count as the defining attribute and appear on the sheet, while other attributes with that variable name will be used as base value [effects](/docs/property/effect). - -### Attribute type - -- **Ability** Ablity scores like Strength, Dexterity, etc. Ability scores get a modifier which can be accessed in calculations as `variableName.modifier`, -- **Stat** Any numerical value that appears on the sheet. Speed, armor class. -- **Modifier** Any numical value that appears on the sheet with a `+` or `-` sign, eg. Proficiency bonus. -- **Hit Dice** Hit dice let you select the appropriate hit dice size. Creatures regain half their total hit dice on long rest. -- **Health Bar** Health bars can by made to take or ignore damage in a specified order -- **Resource** Rages, sourcery points, things that are spent to use actions. -- **Spell Slot** Spell slots have a specific level and are used to cast spells. -- **Utility** Utility attributes don't show up anywhere on the sheet, but can still be used for calculations - -### Description - -A detailed description of the attribute. - -Allows [inline calculations](/docs/inline-calculations). - -### Health bar settings - -Health bars can take or ignore damage and healing from applied damage properties targeting a creature. A lower ordered health bar will take damage before a higher ordered one. - -Health bars can also change color depending on their value. At 50%+ full they are their property color, between 50% and 0% they fade from their half-full color to their empty color. - -### Tags - -See [Tags](/docs/tags) - -### Allow decimal values - -If this is set, the attribute will not round-down when its value has a decimal. - -### Can be damaged into negative values - -If this is set the attribute can be damaged past zero into negative values. - -### Can be incremented above total - -If this is set the attribute can have negative damage such that the value exceeds the total. This can be useful if you are using the attribute to count, it can start at zero and be healed upwards to keep count. - -### Reset - -If set, the damage on this attribute is reset to 0 at the appropriate time. - -- **Long rest** Reset when the long rest button is pushed -- **Short rest** Reset when either the long or short rest button is pushed diff --git a/app/private/docs/property/branch.md b/app/private/docs/property/branch.md deleted file mode 100644 index cdd579d9..00000000 --- a/app/private/docs/property/branch.md +++ /dev/null @@ -1,24 +0,0 @@ -# Branches - -Branches are applied by actions, when they are applied they can control which of their immediate children are applied. - ---- - -### Branch type - -- **If condition is true** Apply children if the condition (a [computed field](/docs/computed-fields)) resolves to `true` or a non-zero number -- **Attack hit** Apply children if the attack roll hit the target -- **Attack hit** Apply children if the attack roll missed the target -- **Save failed** Apply children if target failed its saving throw -- **Save suceeded** Apply children if target made its saving throw -- **Apply to each target** Apply children separately to each target -- **Random** Apply one of the immediate children at random -- **Calculated Index** Use the index (a [computed field](/docs/computed-fields)) to choose which child to apply, starting at 1 for the first child. - -### Tags - -See [Tags](/docs/tags) - -### Don't show in log - -When this is set, the branch is applied, but does not show in the log. This does not prevent its children from appearing in the log. diff --git a/app/private/docs/property/buff.md b/app/private/docs/property/buff.md deleted file mode 100644 index 3196f0ab..00000000 --- a/app/private/docs/property/buff.md +++ /dev/null @@ -1,44 +0,0 @@ -# Buffs - -Buffs are temporary changes to a character sheet that can be applied by actions. When a buff is applied, it is copied to the target character along with all of its children properties. - -Buffs can either be manually removed from the stats page, or be removed by an action applying a [buff remover](/docs/property/remove-buff/) property. - -### Variable freezing - -When a buff is applied, all the calculations in the child properties have their variables frozen to their values at the time the buff is applied. You can prevent this behavior for the whole buff by using the `don't freeze variables` option, or on an individual variable reference by prefixing the variable with the keyword `$target.`. - -For example, if a character has 10 strength and 16 dexterity, and applies a buff with some child property containing the calculation `$target.strength + dexterity` the property's calculation will become `strength + 16` when it is copied to the target character. - ---- - -### Name - -The name of the buff. - -### Description - -Description of the applied buff. - -Allows [inline calculations](/docs/inline-calculations). - -### Target - -- **Target** Apply the buff to the target of the action -- **Self** Apply the buff to the creature taking the action - -### Hide remove button - -If this is set, the remove button next to the buff on the stats page will be hidden. Use this when you expect the buff to be removed automatically by another action. - -### Don't show in log - -If set, the buff will not show its name and description in the log when applied. - -### Don't freeze variables - -Prevent the buff from freezing variables in child property calculations to their value at the time the buff was applied. - -### Tags - -See [Tags](/docs/tags) diff --git a/app/private/docs/property/class-level.md b/app/private/docs/property/class-level.md deleted file mode 100644 index c4b1a3f3..00000000 --- a/app/private/docs/property/class-level.md +++ /dev/null @@ -1,29 +0,0 @@ -# Class level - -A class level is a property that represents a single level in a class. It is generally used as a child of a [Class property](/docs/property/class). - -Features and bonuses that are given by a class level get added as children of the class level. - ---- - -### Level - -Which level this property represents. - -### Name - -The name of the class or subclass this level is part of - -### Variable name - -The same variable name of the class this level belongs to. - -### Description - -A description of the benefits gained with this level. - -Allows [inline calculations](/docs/inline-calculations). - -### Tags - -See [Tags](/docs/tags) diff --git a/app/private/docs/property/class.md b/app/private/docs/property/class.md deleted file mode 100644 index b8630da3..00000000 --- a/app/private/docs/property/class.md +++ /dev/null @@ -1,37 +0,0 @@ -# Classes - -A class is a property that expects [class levels](/docs/property/class-level) as its immediate children. - -Leveling up a class means choosing, or manually adding, class level properties to it. Class levels with the same variable name as the class, and that match all the required tags are found in libraries and added to the class. - -The total level of the class can be accessed in calculations using `classVariableName.level`. - -## Making your own class - -See [Create a Class](/docs/walkthroughs/create-a-class) - ---- - -### Name - -The name of the class - -### Variable name - -The name used to refer to the class in calculations. Must start with a letter and be made up of only letters and numbers without spaces, symbols, or punctiation. - -### Description - -A description of the class. - -### Tags - -See [Tags](/docs/tags) - -### Tags required - -Only class levels with the same variable name as the class, and with tags that match the tags required will be returned from libraries when leveling up this class. - -### Condition - -A [computed field](/docs/computed-fields) to determine if the class is allowed to level up. If this field results in `true` or a number that is not 0, the class can be levelled, otherwise leveling is disabled. diff --git a/app/private/docs/property/constant.md b/app/private/docs/property/constant.md deleted file mode 100644 index a37af5a5..00000000 --- a/app/private/docs/property/constant.md +++ /dev/null @@ -1,33 +0,0 @@ -# Constants - -Constants are properties that store some primitive value in a variable name for use in other calculations. - -Unlike attributes, constants can store more than just numbers: - -- Arrays: `[1,2,3,4]` -- Text strings: `'I am a cat'` -- Numbers: `3.14` -- Boolean values: `true`, `false` -- Dice rolls: `1d20 + 2` - -Constants just can't use other variables in their calculations. - -### Overriding constants - -If multiple constants have the same variable name, only the last active constant in the [character tree](/docs/tree) will be used as the definition for that variable name. - -This can be used to re-write the value of some constant by ensuring there is a new active constant later in the sheet. - ---- - -### Name - -The name of the constants - -### Variable Name - -The name used to refer to the constant in calculations. Must start with a letter and be made up of only letters and numbers without spaces, symbols, or punctiation. - -### Value - -A [calculation](/docs/computed-fields) of the final value of the constant. diff --git a/app/private/docs/property/container.md b/app/private/docs/property/container.md deleted file mode 100644 index 30e302ce..00000000 --- a/app/private/docs/property/container.md +++ /dev/null @@ -1,35 +0,0 @@ -# Containers - -Containers are things that [items](/docs/property/item) can be put inside of. - ---- - -### Name - -The name of the container - -### Carried - -If this is set the weight of the container and its contents will be added to the character's weight carried. - -### Value - -The value of the container in gold pieces. Silver pieces are worth 0.1 gp and copper pieces are worth 0.01 gp. So a container that is worth 2 gp 4 sp 7 cp will have a value of 2.47 gp. - -### Weight - -The weight of the container in lb. - -### Description - -A description of the container. - -Allows [inline calculations](/docs/inline-calculations). - -### Tags - -See [Tags](/docs/tags) - -### Contents are weightless - -If this is set and the container is carried, only the container's own weight will be added to the weight carried by the creature. diff --git a/app/private/docs/property/damage-multiplier.md b/app/private/docs/property/damage-multiplier.md deleted file mode 100644 index c9e97869..00000000 --- a/app/private/docs/property/damage-multiplier.md +++ /dev/null @@ -1,33 +0,0 @@ -# Damage multipliers - -Damage multipliers are used to define vulnerability, resistance, and immunity to damage types. - -A single multiplier can apply to multiple damage types, and choose whether or not to apply to an incoming source of damage based on the tags present on that damage. - ---- - -### Name - -The name of the feature that gives this damage multiplier - -### Value - -- **Immunity** The creature takes no damage from matching damage sources -- **Resistance** Damage from matching sources is halved. -- **Vulnerability** Damage from matching sources is doubled. - -### Damage types - -A list of damage types that this property applies to. Custom types can be used. - -### Damage tags required - -This damage multiplier will only be applied if the incoming damage has all of these tags present. - -### Damage tags excluded - -This damage multiplier will only apply if the incoming damage has none of these tags present. - -### Tags - -See [Tags](/docs/tags) diff --git a/app/private/docs/property/damage.md b/app/private/docs/property/damage.md deleted file mode 100644 index 4c004ffd..00000000 --- a/app/private/docs/property/damage.md +++ /dev/null @@ -1,32 +0,0 @@ -# Damage - -Damage can be applied by an action to damage a target creature's [health bars](/docs/property/attribute). The damage will be modified by [damage multipliers](/docs/property/damage-multiplier), which apply vulnerability, resistance, and immunity before the damage is applied. - ---- - -### Damage - -A [computed field](/docs/computed-fields) that determines how much damage to do to the target creature. - -### Damage type - -Damage type determines how the damage is treated by [damage multipliers](/docs/property/damage-multiplier). A custom type can be used, or one of the existing types can be selected. - -There are two special damage types: - -**Extra damage** Damage with the `extra` type will take on the damage type of whatever damage was applied before it by an action. So if an action deals 12 `piercing` damage and `3` extra damage, it will instead deal 15 `piercing` damage. - -**Healing** Damage with the `healing` type will heal a creature instead of damaging them. - -### Target - -- **Target** Apply the damage to the target of the action -- **Self** Apply the damage to the creature taking the action - -### Tags - -See [Tags](/docs/tags) - -### Don't show in log - -If set, the damage will be applied but not show in the log. diff --git a/app/private/docs/property/effect.md b/app/private/docs/property/effect.md deleted file mode 100644 index b453e94f..00000000 --- a/app/private/docs/property/effect.md +++ /dev/null @@ -1,60 +0,0 @@ -# Effects - -Effects are the core of the DiceCloud engine. Effect change the values of attributes, skills, and calculations in a way that is transparent and auditable, keeping character sheets organized and understandable, even when using intricate homebrew rules on high level characters. - ---- - -### Name - -The name of the feature that causes this effect. - -### Operation - -The operation determines what the effect will do to the affected property or calcualtion. - -- **Base Value** Set the base value of the affected property. If a property has multiple base values, the highest is used -- **Add** Add the value to the affected property or calculation -- **Muliply** Multiply the affected property by the value -- **Minimum** Prevent the affected property from having a value less than the effect value -- **Maximum** Prevent the affected property from having a value greater than the effect value -- **Maximum** Prevent the affected property from having a value greater than the effect value -- **Set** Set the value affected property to the effect value -- **Advantage** Give advantage to checks made using the affected property -- **Disadvantage** Give disadvantage to checks made using the affected property -- **Passive bonus** Add the effect value to the passive scores based on the affected property -- **Fail** Checks made using the affected property automatically fail -- **Conditional benefit** Add some text to the affected property describing the benefit recieved - -### Value - -A [computed field](/docs/computed-fields) that determines the value of the effect. - -### Text - -If the operation is a conditional benefit, the note text that will show on affected properties. - -### Target stats by variable name - -If selected the effect will apply to all properties that have the given variable names. - -### Variable names - -A list of variable names of properties to target with this effect. - -### Target properties by tags - -When targeting properties by tag, any property can be targeted with an effect. If the property is one that can usually be targeted by variable name, the effect will apply as ususal, however if the effect targets another property, it will apply to a [computed field](/docs/computed-fields) on the property instead. - -These effects can be used for adding a bonus to a specific attack or damage roll, or manipulating any computed field on the creature. - -### Tags required - -Only properties that match the required tags will be targeted by the effect. - -### Target field - -If a property has multiple computed fields, which field should be targeted by this effect. - -### Tags - -See [Tags](/docs/tags) diff --git a/app/private/docs/property/feature.md b/app/private/docs/property/feature.md deleted file mode 100644 index 98bc0c28..00000000 --- a/app/private/docs/property/feature.md +++ /dev/null @@ -1,25 +0,0 @@ -# Features - -Features appear on the features tab. Classes, backgrounds, and race can all give a creature features. - ---- - -### Name - -The name of the feature. - -### Summary - -A summary of the feature. This will appear on the feature card. - -Allows [inline calculations](/docs/inline-calculations). - -### Description - -A detailed description of the feature. - -Allows [inline calculations](/docs/inline-calculations). - -### Tags - -See [Tags](/docs/tags) diff --git a/app/private/docs/property/folder.md b/app/private/docs/property/folder.md deleted file mode 100644 index 61d04523..00000000 --- a/app/private/docs/property/folder.md +++ /dev/null @@ -1,17 +0,0 @@ -# Folders - -Folders allow the [character tree](/docs/tree) to be organized. - -### Folders in actions - -When a folder is the child of an action, it and its children will not show on the action card, but will still appear in the detail view of the action and be applied when the action is taken. - ---- - -### Name - -The name of the folder. - -### Tags - -See [Tags](/docs/tags) diff --git a/app/private/docs/property/item.md b/app/private/docs/property/item.md deleted file mode 100644 index 1855f36c..00000000 --- a/app/private/docs/property/item.md +++ /dev/null @@ -1,57 +0,0 @@ -# Items - -Items are shown on the Inventory tab. Items can be carried, put in containers, or equipped on a creature. The children of an item are not active unless the item is equipped. - ---- - -### Icon - -An icon representing the item. - -### Equipped - -If set, the item appears in the equipment list on the inventory tab and its children become active on the creature. - -### Name - -The name of the item. - -### Plural name - -The name to use if the quantity of the item is higher than 1. - -### Value - -The value of a single item in gold pieces. Silver pieces are worth 0.1 gp and copper pieces are worth 0.01 gp. So an item that is worth 2 gp 4 sp 7 cp will have a value of 2.47 gp. - -### Weight - -The weight of a single item in lb. - -### Quantity - -Number of items. The value and quantity will be multiplied by the quantity to get the total value and weight of this stack of items. - -### Description - -A description of the item. - -Allows [inline calculations](/docs/inline-calculations). - -### Tags - -See [Tags](/docs/tags) - -### Show increment button - -If this is set, the item will show an increment button in the detail view and on the inventory tab. This button can be used to quickly adjust the quantity of the item. - -### Requires attunemnt - -If set, the item requires attunemnt to use. - -### Attuned - -If set, the item is attuned and counts towards the total number of attuned items for the creature. - -If a child property needs to determine if its parent item is attuned it can use `#item.attuned` in calculations, see *Ancestor references* in [computed fields](/docs/computed-fields). diff --git a/app/private/docs/property/note.md b/app/private/docs/property/note.md deleted file mode 100644 index 928a0f2c..00000000 --- a/app/private/docs/property/note.md +++ /dev/null @@ -1,25 +0,0 @@ -# Notes - -Notes are used to store text on the creature that does not have a direct mechanical impact. Notes appear on the journal tab when active on the character, or are shown in the log when applied by an [action](/docs/property/action). - ---- - -### Name - -Name of the note. - -### Summary - -A summary of the note. This will appear on the note card and in the log when applied by an [action](/docs/property/action). - -Allows [inline calculations](/docs/inline-calculations). - -### Description - -A detailed description of the feature. - -Allows [inline calculations](/docs/inline-calculations). - -### Tags - -See [Tags](/docs/tags) diff --git a/app/private/docs/property/point-buy.md b/app/private/docs/property/point-buy.md deleted file mode 100644 index 419a8cb3..00000000 --- a/app/private/docs/property/point-buy.md +++ /dev/null @@ -1,39 +0,0 @@ -# Point buy - -A point buy is a set of rows that lets the user choose a set of stats based on a cost per stat. - ---- - -### Table name - -The name of the point buy table. - -### Min - -The lowest value available for each row - -### Max - -The highest value available for each row - -### Cost - -A function that uses `value` as the value of a row and determines the cost of that value. For standard D&D 5e 27 point buy, this function is `[0, 1, 2, 3, 4, 5, 7, 9][value - 7]` - -### Total available points - -A [computed field](/docs/computed-fields) that determines how many points are available to spend in total - -## Rows - -Up to 32 rows can be added to a point buy table - -### Row name - -The name of the row that will appear in the table - -### Row variable name - -The variable name of the row that can be used in calculations. Must start with a letter and be made up of only letters and numbers without spaces, symbols, or punctiation. - -If the variable name matches an attribute with the same variable name, the row's value will be used as a base value for that attribute. diff --git a/app/private/docs/property/proficiency.md b/app/private/docs/property/proficiency.md deleted file mode 100644 index 2a1c7c41..00000000 --- a/app/private/docs/property/proficiency.md +++ /dev/null @@ -1,21 +0,0 @@ -# Proficiencies - -Proficiencies add proficiency to an existing skill on the creature. If you need to add a tool or language proficiency to a creature, use a [Skill](/docs/property/skill) instead. - ---- - -### Name - -Name of the feature that is adding this proficiency - -### Skills - -A list of variable names of the skills to add proficiency to. - -### Proficiency - -How much proficiency to add to the skill. If a skill has multiple proficiencies added to it, the highest one will be used. - -### Tags - -See [Tags](/docs/tags) diff --git a/app/private/docs/property/remove-buff.md b/app/private/docs/property/remove-buff.md deleted file mode 100644 index 77888a98..00000000 --- a/app/private/docs/property/remove-buff.md +++ /dev/null @@ -1,32 +0,0 @@ -# Remove Buff - -This property can remove a specific buff from a targeted creature. - -### Name - -The name of the property. This shows in the log when the property is applied. - -### Remove parent buff - -When this is set and the property is applied, the property will remove the nearest parent buff. If this property is not the child of any buffs, it will log an error. - -### Remove all - -When this is set, all buffs that match the target tags will be removed from the targeted creature. If not set, only the first buff found with the matching tags will be removed. - -### Target - -- **Target** Matching buffs will be removed from the targeted creature -- **Self** Matching buffs will be removed from the creature that applied the action - -### Tags required - -Any buff that has all of the required tags will be removed when the property is applied. - -### Tags - -See [Tags](/docs/tags) - -### Don't show in log - -When this is set, the property is applied, but does not show in the log. \ No newline at end of file diff --git a/app/private/docs/property/roll.md b/app/private/docs/property/roll.md deleted file mode 100644 index 35898bdf..00000000 --- a/app/private/docs/property/roll.md +++ /dev/null @@ -1,27 +0,0 @@ -# Rolls - -Rolls are properties that store the result of a calculation to a variable name when applied by an [action](/docs/property/action). The variable name only exists for the duration of that particalar action. - -Rolls can be useful if you need to deal the same damage to multiple targets, or if damage needs to be rolled then halved by succeeding on a saving throw. - ---- - -### Name - -Name of the roll. This will be shown in the log when the roll is applied. - -### Variable name - -The variable name to store the result of the roll for the duration of the action. Must start with a letter and be made up of only letters and numbers without spaces, symbols, or punctiation. - -### Roll - -A [computed field](/docs/computed-fields) that is computed when the roll is applied by an action. - -### Don't show in log - -If set, the roll will be applied and store its result in the variable name, but not be shown in the log. - -### Tags - -See [Tags](/docs/tags) diff --git a/app/private/docs/property/saving-throw.md b/app/private/docs/property/saving-throw.md deleted file mode 100644 index 907b8fe4..00000000 --- a/app/private/docs/property/saving-throw.md +++ /dev/null @@ -1,35 +0,0 @@ -# Saving throws - -Saving throws are properties that cause the target to make a saving throw when applied. If you want to add a type of saving throw like Strength Save to a creature, use a [skill](/docs/property/skill) instead. - -When a saving throw is applied, the following variables are added to the scope of that action: - -- `$saveFailed` Set to `true` if the target failed its saving throw or there are no targets for the saving throw -- `$saveSucceeded` Set to `true` if the target made its saving throw or there are no targets for the saving throw -- `$saveDiceRoll` The unmodified d20 roll the target made to save -- `$saveRoll` The final value of the saving throw roll after modifiers - -### Name - -The name of the saving throw. Usually the ability saving throw targeted: "Strength Save". - -### DC - -The DC of the saving throw that the target needs to meet - -### Save - -The variable name of the skill that will be used to make the saving throw. - -### Target - -- **Target** Apply the saving throw to the targets of the action. Each target will make the saving throw in turn. Child properties will be applied to each target separately with the results of their individual saving throw. If a value like damage needs to be shared between targets, it should be calculated in a [Roll](/docs/property/roll) before the saving throw. -- **Self** Apply the saving throw to the creature taking the action. The creature taking the action will become the target for all child properties. - -### Tags - -See [Tags](/docs/tags) - -### Don't show in log - -If set, the saving throw will not show in the log when applied, but will still be rolled and apply its children. diff --git a/app/private/docs/property/skill.md b/app/private/docs/property/skill.md index 51185673..5b25b9ad 100644 --- a/app/private/docs/property/skill.md +++ b/app/private/docs/property/skill.md @@ -1,6 +1,6 @@ # Skills -Skills represent things the creature can be proficient in. Skills can have their values or behavior modifier by [effects](/docs/property/effect), and their proficiencies modified by [proficiencies](/docs/property/proficiency). +Skills represent things the creature can be proficient in. Skills can have their values or behavior modified by [effects](/docs/property/effect), and their proficiencies modified by [proficiencies](/docs/property/proficiency). --- diff --git a/app/private/docs/property/slot-filler.md b/app/private/docs/property/slot-filler.md deleted file mode 100644 index 9e465a18..00000000 --- a/app/private/docs/property/slot-filler.md +++ /dev/null @@ -1,39 +0,0 @@ -# Slot filler - -A slot filler is a property that can be used to add more complex behavior to filling a [slot](/docs/property/slot) from a library. - ---- - -### Name - -The name of the slot filler that will show when choosing the filler from the library. - -### Icon - -Icon of the slot filler - -### Description - -A detailed description of the slot filler. - -Allows [inline calculations](/docs/inline-calculations). - -### Picture URL - -A link to an image to use for this slot filler when being chosen from a library. - -### Type - -Slot fillers can pretend to be any type of property when a slot is being filled. - -### Quantity - -How many spaces the slot filler will take up in a slot. - -### Condition - -A [computed field](/docs/computed-fields) that determines whether this slot filler can be added to a character. - -### Tags - -See [Tags](/docs/tags) diff --git a/app/private/docs/property/slot.md b/app/private/docs/property/slot.md deleted file mode 100644 index 61793f81..00000000 --- a/app/private/docs/property/slot.md +++ /dev/null @@ -1,53 +0,0 @@ -# Slots - -Slots are the main way creatures interact with libraries. A slot can be filled by choosing a property from a library that fits that particular slot. - -In a complete library, a creature can be built entirely by choosing which properties to fill each slot with. - -Slots show up on the build tab, and are highlighted when they have space that can be filled. - -If you are building a creature without a library, you should either ignore slots entirely, or fill them with your own custom properties. - ---- - -### Name - -The name of the slot. - -### Type - -What kind of property this slot expects to fill it. - -### Tags required - -Properties in a library must have the required tags to fill the slot. - -### Quantity - -How many properties are expected to fill this slot. Use 0 for allowing an unlimited number of properties. - -### Condition - -A [computed field](/docs/computed-fields) that determines whether this slot can accept new properties. - -### Unique - -The slot can control how it deals with the uniqueness of properties that fill it. - -### Description - -A detailed description of the attribute. - -Allows [inline calculations](/docs/inline-calculations). - -### Hide when full - -When set the slot will hide itself when it is filled. - -### Ignored - -When set the slot will not show a prompt card on the build tab. - -### Tags - -See [Tags](/docs/tags) \ No newline at end of file diff --git a/app/private/docs/property/spell.md b/app/private/docs/property/spell.md deleted file mode 100644 index ca878747..00000000 --- a/app/private/docs/property/spell.md +++ /dev/null @@ -1,115 +0,0 @@ -# Spells - -Spells work similarly to [actions](/docs/property/action). They appear on the spells tab and can be cast with or without using up spell slots. - ---- - -### Always prepared - -A spell that is always prepared does not count towards the spell list's maximum prepared spells and is always active and ready to cast. - -### Prepared - -A prepared spell is ready to cast and counts against a spell list's maximum prepared spells. - -### Cast without spell slots - -When set, this spell can be cast without consuming spell slots. It will however consume its own uses and resources. - -### School - -What school the spell belongs to. - -### Casting time - -How long the spell takes to Cast - -### Range - -The range of the spell - -### Duration - -How long the spell lasts - -### Components - -Whether the spell requires verbal, somatic, or material components and whether the spell is a ritual or requires concentration. - -### Target - -Who this spell should apply to. The properties under the spell will be applied to the targets. - -- **Self** The spell will apply its properties to the creature casting the spell -- **Single Target** The spell will apply its properties without a target (for now) -- **Multiple Targets** The spell will apply its properties without a target (for now) - -### Attack roll - -A [computed field](/docs/computed-fields) which calculates the spell attack roll modifier. If this field is empty, no attack roll will be made. Use 0 to make an attack roll without a modifier. To use the spell list's attack roll bonus use `#spellList.attackRollBonus`. - -The following variables may be added to the action scope when attack rolls are made: - - - `$attackDiceRoll` The value of the d20 roll before any modifiers were applied. - - `$attackRoll` The total attack roll after modifiers. - - `$criticalHit` Set to `true` if the attack roll's d20 is a natural 20. If `criticalHitTarget` is set, the attack roll's d20 must instead be equal to or greater than `criticalHitTarget` for this to be set to `true`. - - `$criticalMiss` Set to `true` if the attack roll was not a critical hit and rolled a natural 1 on the d20 roll. - - `$attackHit` If the attack roll is higher than or equal to the target's AC or a critical hit this is set to `true`. Remains unset if there is no target for the attack unless the attack is a critical hit. - - `$attackMiss` If the attack roll is lower than the target's AC or a critical miss, this is set to `true`. Remains unset if there is no target for the attack unless the attack is a critical miss. - -### Summary - -A brief overview of what the spell does. This will show in the log when the spell is cast. - -Allows [inline calculations](/docs/inline-calculations). - -### Description - -A more detailed description of the spell. The description does not show in the log when the spell is cast. - -Allows [inline calculations](/docs/inline-calculations). - -### Resource - -A resource can be any attribute that has a variable name. If the resource attribute is less than the amount required, the spell can't be cast. - -If you want to reduce an attribute when casting the spell, but want the spell to be applied regardless of the value of that attribute, consider using an [Attribute Damage](/docs/property/attribute-damage) property as a child of the spell instead. Also use Attribute Damage when the amount to reduce the attribute is determined by a dice roll rather than a stable computed number. - -#### Resource attribute - -The variable name of the attribute that will be consumed when casting this spell. - -#### Resource quantity - -A [computed field](/docs/computed-fields) which determines how much of the attribute is required to apply this spell. This amount will be deducted from the attribute every time the spell is cast -### Ammo - -Ammo represents items that are requied to cast the spell. If an item is not selected, or there is insufficient quantity of the selected item, the spell can't be appled. - -#### Ammo item - -Specify what tag an item must have to be considered valid ammo for this spell. Any item with this tag can be selected as ammo for this spell. - -#### Ammo quantity - -A [computed field](/docs/computed-fields) which determines how many of the selected items are required to cast this spell. The quantity is deducted from the total quantity of the item when this spell is applied. - -### Uses - -A [computed field](/docs/computed-fields) which determines how many times this spell can be used before it needs to be reset. - -### Uses used - -How many of this spell's uses have already been used. Should ideally be between 0 and the total uses available. This number is set to 0 when the spell has uses and its uses are reset. - -### Reset - -If set, the uses used field is set to 0 at the appropriate time. - -- **Long rest** Reset when the long rest button is pushed -- **Short rest** Reset when either the long or short rest button is pushed - - -### Tags - -See [Tags](/docs/tags) \ No newline at end of file diff --git a/app/private/docs/property/toggle.md b/app/private/docs/property/toggle.md deleted file mode 100644 index 1dcaf102..00000000 --- a/app/private/docs/property/toggle.md +++ /dev/null @@ -1,36 +0,0 @@ -# Toggles - -Toggles are a way to turn on and off parts of a creature. When a toggle is off, none of its children will be active. - -Calculated toggles should be avoided if possible, because while they offer a lot of power and flexibility to the creature engine, they often create [dependency loops](/docs/dependency-loops) that can be difficult to troubleshoot, causing parts of a creature to calculate incorrectly. - -Calculated toggles can be applied by [actions](/docs/property/action) and will apply their children if the condition is true, but they should be avoided in favor of [conditional branches](/docs/property/branch) which can do the same, but are more efficient. - ---- - -### Name - -The name of the toggle. - -### Variable name - -The name used to refer to the value of the toggle in calculations. Must start with a letter and be made up of only letters and numbers without spaces, symbols, or punctiation. - -### Show on character sheet - -If set, the toggle with show a checkbox on the character sheet. A calculated toggle will show a disabled checkbox, filled in if the toggle's calculation returned `true` or a value that isn't 0. - -### State - -- **Enabled** The toggle and its children are active -- **Disabled** The toggle and its children are inactive -- **Calculated** The active status of the toggle depends on the result of the condition. Use with caution. - -### Condition - -A [computed field](/docs/computed-fields) that determines if the toggle is active. Use with caution. - -### Tags - -See [Tags](/docs/tags) - diff --git a/app/private/docs/property/trigger.md b/app/private/docs/property/trigger.md deleted file mode 100644 index 9e996838..00000000 --- a/app/private/docs/property/trigger.md +++ /dev/null @@ -1,49 +0,0 @@ -# Triggers - -Triggers apply their children whenever their condition is met. They work like [actions](/docs/property/action) that are taken automatically. - ---- - -### Name - -The name of the trigger. - -### Timing - -- **Before** The trigger is applied before the triggering event takes place -- **After** The trigger is fired after the triggering event - -### Event - -- **Do action** While the creature is doing an action, the action property specified in *Event type* is applied -- **Roll check** The creature makes a check -- **Attribute damaged or healed** One of the creature's attributes changed value through attribute damage or manual adjustment -- **Short or long rest** -- **Short rest** -- **Long Rest** - -### Event type - -The trigger will apply when this property type is applied by the action - -### Tags required - -If this trigger is fired by a property, the property must match these tags for the trigger to fire. - -### Condition - -A [computed field](/docs/computed-fields) to determine if the trigger should fire. The trigger will fire if the condition field is empty or if it returns `true` or a value that isn't 0. - -### Description - -A detailed description of the trigger. - -Allows [inline calculations](/docs/inline-calculations). - -### Tags - -See [Tags](/docs/tags) - -### Don't show in log - -When this is true, the trigger does not show up in the log. This does not stop the trigger's children from appearing in the log when they are applied. diff --git a/app/private/docs/tags.md b/app/private/docs/tags.md deleted file mode 100644 index bc84771d..00000000 --- a/app/private/docs/tags.md +++ /dev/null @@ -1,14 +0,0 @@ -# Tags - -Tags are used to reference multiple similar properties at once. A slot can require only properties from your library that has matching tags, effects and some other properties can also target properties to apply to by tags. - -## Built in tags - -Properties have specific tags automatically for use with tag-targeting. These aren't relevant for slots and finding properties in a library with specific tags. - -- `#type` Actions will have the `#action` tag, etc. Damage will either have the tag `#damage` or the tag `#healing` if the damage type is healing -- `variableName` if a property has a variable name it will be included as a tag -- The type of damage done by a [damage](/docs/property/damage) property: `bludgeoning`, `slashing`, `...` -- The skill type of a [skill](/docs/property/skill) property: `skill`, `save`, `check`, `tool`, `weapon`, `armor`, `language`, `utility` -- The attribute type of an [attribute](/docs/property/attribute) property: `ability`, `stat`, `modifier`, `hitDice`, `healthBar`, `resource`, `spellSlot`, `utility` -- When the property resets: `longRest`, `shortRest` diff --git a/app/private/docs/walkthroughs/create-a-class.md b/app/private/docs/walkthroughs/create-a-class.md deleted file mode 100644 index 6987b391..00000000 --- a/app/private/docs/walkthroughs/create-a-class.md +++ /dev/null @@ -1,47 +0,0 @@ -# Create a Class - -This is a guide on creating a custom class in a character sheet. If possible, it is always faster to use an existing library that contains the class you want to use. Before continuing, check the #libraries channel of the [official discord](https://discord.gg/qEvdfeB) to see if a library exists with the class you are creating. - -This guide assumes you are using the ruleset provided in the [5e System Reference Document library](/library/qkv8aptJH2fCXARcJ). If you are using a different ruleset for your character, there may be some discrepancies. - -## Adding the class property - -On the build tab of your character, in the card labeled **Slots**, expand the rulset, then click the slot where you would like to place the custom class, if it is your starting class in an SRD character, this would be the Class slot. Be sure to click the name of the slot, not the **+** button. - -![Screenshot of Build Tab > Slots > Ruleset > Class](/images/docs/walkthroughs/create-a-class-1.png) - -This opens the slot in detail view, showing you how the slot expected to be filled from a library, instead of filling the slot, we will be manually adding a class to the slot that we create ourselves. - -Click the **Edit** button in the top right of the slot detail dialog. - -![Screenshot of slot detail dialog](/images/docs/walkthroughs/create-a-class-2.png) - -Expand the children of the class slot, and click the plus button to add a child property. - -![Screenshot of adding a child property](/images/docs/walkthroughs/create-a-class-3.png) - -This brings up the create a property dialog, we are creating a class, so select the class property type. - -![Screenshot of choosing a class property](/images/docs/walkthroughs/create-a-class-4.png) - -Now that we have selected the class property type, the create tab is selected where we can enter the details of our class, fill in the form and click **Create**. - -![Screenshot of the class form](/images/docs/walkthroughs/create-a-class-5.png) - -Now that our custom class is created, we can close the class slot dialog. - -On the Build tab, in the card with the title **Level**, you will see your new class, with a button to **Level Up**, clicking the level up button would usually search your libraries for class levels that match the variable name of the class, however, since it's a custom class, it will probably not find any levels. - -Instead, as we did with the class slot, click on the class name to bring up the class detail dialog, click **Edit**, expand children and click the **+** button to add a child to the class. Here we will add all of the things our class gives the character. - -Add an [Effect](/docs/property/effect) which targets `hitPoints` to add the starting hitpoints of the class. Add a [proficiencies](/docs/property/proficiency) for all the skill and saving throw proficiencies the class gives. Add [skills](/docs/property/skill) for all the tool and weapon proficiencies of the class, making sure to set the base proficiency of those skills to proficient. Add any text [features](/docs/property/feature) the class gives you, along with [actions](/docs/property/action) which may be children of those features, or direct children of the class. - -Once you have added Everything the class gives you, it's time to add class levels. As a child of the class, add a [class level](/docs/property/class-level) property. Set the level to 1 and the name and variable name to match the variable name of the class. - -Once the class level is created, open the class level and edit it. Use the **+** button in the children of the class level to add all the properties the class level gives your character. - -Repeat this for every level of the class until your character is at the correct level. - -You can use a separate character with levels in a class that is available in your libraries as an example of what properties you may want to add to your class and class levels. - -![Example wizard class](/images/docs/walkthroughs/create-a-class-6.png) diff --git a/app/public/fonts/game-icons.eot b/app/public/fonts/game-icons.eot new file mode 100644 index 00000000..4c8fd763 Binary files /dev/null and b/app/public/fonts/game-icons.eot differ diff --git a/app/public/fonts/game-icons.ttf b/app/public/fonts/game-icons.ttf new file mode 100644 index 00000000..a33e3a0e Binary files /dev/null and b/app/public/fonts/game-icons.ttf differ diff --git a/app/public/fonts/game-icons.woff b/app/public/fonts/game-icons.woff new file mode 100644 index 00000000..2026fd19 Binary files /dev/null and b/app/public/fonts/game-icons.woff differ diff --git a/app/public/images/battlemap.webp b/app/public/images/battlemap.webp new file mode 100644 index 00000000..3a69519e Binary files /dev/null and b/app/public/images/battlemap.webp differ diff --git a/app/public/images/crown-dice-on-ipad.webp b/app/public/images/crown-dice-on-ipad.webp new file mode 100644 index 00000000..4f5583af Binary files /dev/null and b/app/public/images/crown-dice-on-ipad.webp differ diff --git a/app/public/images/paper-dice-crown-with-candy.png b/app/public/images/paper-dice-crown-with-candy.png deleted file mode 100644 index da31db3a..00000000 Binary files a/app/public/images/paper-dice-crown-with-candy.png and /dev/null differ diff --git a/app/public/images/paper-dice-crown.png b/app/public/images/paper-dice-crown.png deleted file mode 100644 index 5191fff4..00000000 Binary files a/app/public/images/paper-dice-crown.png and /dev/null differ diff --git a/app/public/images/paper-dice-crown.webp b/app/public/images/paper-dice-crown.webp new file mode 100644 index 00000000..0a08c994 Binary files /dev/null and b/app/public/images/paper-dice-crown.webp differ diff --git a/app/public/images/paragons/blue.png b/app/public/images/paragons/blue.png new file mode 100644 index 00000000..15ffd6bb Binary files /dev/null and b/app/public/images/paragons/blue.png differ diff --git a/app/public/images/paragons/vibes.png b/app/public/images/paragons/vibes.png new file mode 100644 index 00000000..026cacac Binary files /dev/null and b/app/public/images/paragons/vibes.png differ diff --git a/app/public/images/screenshots/actions.webp b/app/public/images/screenshots/actions.webp new file mode 100644 index 00000000..6420ba0f Binary files /dev/null and b/app/public/images/screenshots/actions.webp differ diff --git a/app/public/images/screenshots/auditable.webp b/app/public/images/screenshots/auditable.webp new file mode 100644 index 00000000..d802ff45 Binary files /dev/null and b/app/public/images/screenshots/auditable.webp differ diff --git a/app/public/images/screenshots/automated-dice-rolls.webp b/app/public/images/screenshots/automated-dice-rolls.webp new file mode 100644 index 00000000..73e8f22f Binary files /dev/null and b/app/public/images/screenshots/automated-dice-rolls.webp differ diff --git a/app/public/images/screenshots/build-system.webp b/app/public/images/screenshots/build-system.webp new file mode 100644 index 00000000..fcc48dbc Binary files /dev/null and b/app/public/images/screenshots/build-system.webp differ diff --git a/app/public/images/screenshots/inventory.webp b/app/public/images/screenshots/inventory.webp new file mode 100644 index 00000000..69212efd Binary files /dev/null and b/app/public/images/screenshots/inventory.webp differ diff --git a/app/public/images/screenshots/libraries-of-content.webp b/app/public/images/screenshots/libraries-of-content.webp new file mode 100644 index 00000000..7e32cf72 Binary files /dev/null and b/app/public/images/screenshots/libraries-of-content.webp differ diff --git a/app/public/images/screenshots/printing.webp b/app/public/images/screenshots/printing.webp new file mode 100644 index 00000000..15f2bdca Binary files /dev/null and b/app/public/images/screenshots/printing.webp differ diff --git a/app/public/images/screenshots/send-to-discord.webp b/app/public/images/screenshots/send-to-discord.webp new file mode 100644 index 00000000..8bcfd58e Binary files /dev/null and b/app/public/images/screenshots/send-to-discord.webp differ diff --git a/app/public/images/ui/missing-portrait.png b/app/public/images/ui/missing-portrait.png new file mode 100644 index 00000000..a698d221 Binary files /dev/null and b/app/public/images/ui/missing-portrait.png differ diff --git a/app/public/models/example-mini.stl b/app/public/models/example-mini.stl new file mode 100644 index 00000000..39995125 Binary files /dev/null and b/app/public/models/example-mini.stl differ diff --git a/app/redis-settings.json b/app/redis-settings.json new file mode 100644 index 00000000..45fc18d7 --- /dev/null +++ b/app/redis-settings.json @@ -0,0 +1,19 @@ +{ + "redisOplog": { + "redis": { + "port": 6379, + "host": "127.0.0.1" + }, + "retryIntervalMs": 1000, + "mutationDefaults": { + "optimistic": true, + "pushToRedis": true + }, + "cacheTimeout": 1800000, + "cacheTimer": 300000, + "secondaryReads": null, + "raceDetectionDelay": 1000, + "raceDetection": true, + "debug": false + } +} \ No newline at end of file diff --git a/app/server/main.js b/app/server/main.js index 642113e0..eeab11f8 100644 --- a/app/server/main.js +++ b/app/server/main.js @@ -1,18 +1,21 @@ -import '/imports/api/simpleSchemaConfig.js'; -import '/imports/server/config/SimpleRestConfig.js'; -import '/imports/server/config/limitLoginTokens.js'; -import '/imports/server/rest/index.js'; -import '/imports/server/config/accountsEmailConfig.js'; -import '/imports/server/config/simpleSchemaDebug.js'; -import '/imports/server/config/SyncedCronConfig.js'; -import '/imports/server/publications/index.js'; -import '/imports/server/cron/deleteSoftRemovedDocuments.js'; -import '/imports/api/parenting/organizeMethods.js'; -import '/imports/api/users/patreon/updatePatreonOnLogin.js'; -import '/imports/api/engine/actions/index.js'; -import '/imports/migrations/server/index.js'; -import '/imports/migrations/methods/index.js' -import '/imports/constants/MAINTENANCE_MODE.js'; -import '/imports/api/creature/creatureProperties/methods/index.js'; -import '/imports/api/creature/archive/methods/index.js'; -import '/imports/api/creature/creatures/methods/index.js'; +import '/imports/api/simpleSchemaConfig'; +import '/imports/server/config/SimpleRestConfig'; +import '/imports/server/config/limitLoginTokens'; +import '/imports/server/rest/index'; +import '/imports/server/config/accountsEmailConfig'; +import '/imports/server/config/simpleSchemaDebug'; +import '/imports/server/config/SyncedCronConfig'; +import '/imports/server/config/redisCaching'; +import '/imports/server/publications/index'; +import '/imports/server/cron/deleteSoftRemovedDocuments'; +import '/imports/api/parenting/organizeMethods'; +import '/imports/api/users/patreon/updatePatreonOnLogin'; +import '/imports/migrations/server/index'; +import '/imports/migrations/methods/index' +import '/imports/constants/MAINTENANCE_MODE'; +import '/imports/api/creature/creatureProperties/methods/index'; +import '/imports/api/creature/archive/methods/index'; +import '/imports/api/creature/creatures/methods/index'; +import '/imports/api/engine/action/methods/index'; +import '/imports/api/sharing/sharing'; +import '/imports/server/config/publicationStrategies'; diff --git a/app/tsconfig.json b/app/tsconfig.json new file mode 100644 index 00000000..4fdac9b5 --- /dev/null +++ b/app/tsconfig.json @@ -0,0 +1,51 @@ +{ + "compilerOptions": { + "module": "esNext", + "moduleResolution": "Node", + "target": "es2018", + "lib": [ + "esnext", + "dom", + "dom.iterable" + ], + "jsx": "preserve", + "strict": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "baseUrl": ".", + "preserveSymlinks": true, + "allowJs": true, + "checkJs": true, + "noImplicitAny": false, + "outDir": "build", + "paths": { + "/*": [ + "./*" + ], + "meteor/*": [ + "node_modules/@types/meteor/*", + ".meteor/local/types/packages.d.ts" + ], + "meteor/aldeed:collection2": [ + "packages/collection2/collection2.js" + ] + }, + "resolveJsonModule": true, + "esModuleInterop": true, + }, + "vueCompilerOptions": { + "target": 2 + }, + "exclude": [ + "node_modules", + "**/node_modules/*", + ".meteor", + "./packages/**" + ], + "typeAcquisition": { + "include": [ + "meteor", + "node_modules/@types/meteor/globals/*" + ] + } +} diff --git a/dataSources/.gitignore b/dataSources/.gitignore deleted file mode 100644 index 83ab52dc..00000000 --- a/dataSources/.gitignore +++ /dev/null @@ -1 +0,0 @@ -Renders diff --git a/dataSources/srd/adventuringGear.json b/dataSources/srd/adventuringGear.json deleted file mode 100644 index 2cd6f96f..00000000 --- a/dataSources/srd/adventuringGear.json +++ /dev/null @@ -1,719 +0,0 @@ -[ - { - "name": "Abacus", - "plural": "Abaci", - "description": "", - "value": 2, - "weight": 2 - }, - { - "name": "Acid (vial)", - "plural": "Acid (vials)", - "description": "As an action, you can splash the contents of this vial onto a creature within 5 feet of you or throw the vial up to 20 feet, shattering it on impact. In either case, make a ranged attack against a creature or object, treating the acid as an improvised weapon. On a hit, the target takes 2d6 acid damage.", - "value": 25, - "weight": 1 - }, - { - "name": "Alchemist’s fire (flask)", - "plural": "Alchemist’s fire (flasks)", - "description": "This sticky, adhesive fluid ignites when exposed to air. As an action, you can throw this flask up to 20 feet, shattering it on impact. Make a ranged attack against a creature or object, treating the alchemist’s fire as an improvised weapon. On a hit, the target takes 1d4 fire damage at the start of each of its turns. A creature can end this damage by using its action to make a DC 10 Dexterity check to extinguish the flames.", - "value": 50, - "weight": 1 - }, - { - "libraryName": "Arrows (20)", - "name": "Arrow", - "plural": "Arrows", - "description": "", - "value": 0.05, - "weight": 0.05, - "quantity": 20 - }, - { - "libraryName": "Blowgun needles (5)", - "name": "Blowgun needle", - "plural": "Blowgun needles", - "description": "", - "value": 0.2, - "weight": 0.2, - "quantity": 5 - }, - { - "libraryName": "Crossbow bolts (20)", - "name": "Crossbow bolt", - "plural": "Crossbow bolts", - "description": "", - "value": 0.05, - "weight": 0.075, - "quantity": 20 - }, - { - "libraryName": "Sling bullets (20)", - "name": "Sling bullet", - "plural": "Sling bullets", - "description": "", - "value": 0.002, - "weight": 0.075, - "quantity": 20 - }, - { - "name": "Antitoxin (vial)", - "plural": "Antitoxin (vials)", - "description": "A creature that drinks this vial of liquid gains advantage on saving throws against poison for 1 hour. It confers no benefit to undead or constructs.", - "value": 50, - "weight": 0 - }, - { - "name": "Arcane focus Crystal", - "plural": "Arcane focus Crystals", - "description": "An arcane focus is a special item — an orb, a crystal, a rod, a specially constructed staff, a wand-like length of wood, or some similar item — designed to channel the power of arcane spells. A sorcerer, warlock, or wizard can use such an item as a spellcasting focus.", - "value": 10, - "weight": 1 - }, - { - "name": "Arcane focus Orb", - "plural": "Arcane focus Orbs", - "description": "An arcane focus is a special item — an orb, a crystal, a rod, a specially constructed staff, a wand-like length of wood, or some similar item — designed to channel the power of arcane spells. A sorcerer, warlock, or wizard can use such an item as a spellcasting focus.", - "value": 20, - "weight": 3 - }, - { - "name": "Arcane focus Rod", - "plural": "Arcane focus Rods", - "description": "An arcane focus is a special item — an orb, a crystal, a rod, a specially constructed staff, a wand-like length of wood, or some similar item — designed to channel the power of arcane spells. A sorcerer, warlock, or wizard can use such an item as a spellcasting focus.", - "value": 10, - "weight": 2 - }, - { - "name": "Arcane focus Staff", - "plural": "Arcane focus Staffs", - "description": "An arcane focus is a special item — an orb, a crystal, a rod, a specially constructed staff, a wand-like length of wood, or some similar item — designed to channel the power of arcane spells. A sorcerer, warlock, or wizard can use such an item as a spellcasting focus.", - "value": 5, - "weight": 4 - }, - { - "name": "Arcane focus Wand", - "plural": "Arcane focus Wands", - "description": "An arcane focus is a special item — an orb, a crystal, a rod, a specially constructed staff, a wand-like length of wood, or some similar item — designed to channel the power of arcane spells. A sorcerer, warlock, or wizard can use such an item as a spellcasting focus.", - "value": 10, - "weight": 1 - }, - { - "name": "Backpack", - "plural": "Backpacks", - "description": "", - "value": 2, - "weight": 5 - }, - { - "name": "Ball bearings (bag of 1,000)", - "plural": "Ball bearings (bags of 1,000)", - "description": "As an action, you can spill these tiny metal balls from their pouch to cover a level, square area that is 10 feet on a side. A creature moving across the covered area must succeed on a DC 10 Dexterity saving throw or fall Prone. A creature moving through the area at half speed doesn’t need to make the save.", - "value": 1, - "weight": 2 - }, - { - "name": "Barrel", - "plural": "Barrels", - "description": "", - "value": 2, - "weight": 70 - }, - { - "name": "Basket", - "plural": "Baskets", - "description": "", - "value": 0.4, - "weight": 2 - }, - { - "name": "Bedroll", - "plural": "Bedrolls", - "description": "", - "value": 1, - "weight": 7 - }, - { - "name": "Bell", - "plural": "Bells", - "description": "", - "value": 1, - "weight": 0 - }, - { - "name": "Blanket", - "plural": "Blankets", - "description": "", - "value": 0.5, - "weight": 3 - }, - { - "name": "Block and tackle", - "plural": "Block and tackle sets", - "description": "A set of pulleys with a cable threaded through them and a hook to attach to objects, a block and tackle allows you to hoist up to four times the weight you can normally lift.", - "value": 1, - "weight": 5 - }, - { - "name": "Book", - "plural": "Books", - "description": "A book might contain poetry, historical accounts, information pertaining to a particular field of lore, diagrams and notes on gnomish contraptions, or just about anything else that can be represented using text or pictures. A book of spells is a spellbook.", - "value": 25, - "weight": 5 - }, - { - "name": "Bottle, glass", - "plural": "Bottles, glass", - "description": "", - "value": 2, - "weight": 2 - }, - { - "name": "Bucket", - "plural": "Buckets", - "description": "", - "value": 0.05, - "weight": 2 - }, - { - "name": "Caltrops (bag of 20)", - "plural": "Caltrops (bags of 20)", - "description": "As an action, you can spread a bag of caltrops to cover a square area that is 5 feet on a side. Any creature that enters the area must succeed on a DC 15 Dexterity saving throw or stop moving this turn and take 1 piercing damage. Taking this damage reduces the creature’s walking speed by 10 feet until the creature regains at least 1 hit point. A creature moving through the area at half speed doesn’t need to make the save.", - "value": 1, - "weight": 2 - }, - { - "name": "Candle", - "plural": "Candles", - "description": "For 1 hour, a candle sheds bright light in a 5-foot radius and dim light for an additional 5 feet.", - "value": 0.01, - "weight": 0 - }, - { - "name": "Case, crossbow bolt", - "plural": "Cases, crossbow bolt", - "description": "This wooden case can hold up to twenty crossbow bolts.", - "value": 1, - "weight": 1 - }, - { - "name": "Case, map or scroll", - "plural": "Cases, map or scroll", - "description": "This cylindrical leather case can hold up to ten rolled-up sheets of paper or five rolled-up sheets of parchment.", - "value": 1, - "weight": 1 - }, - { - "name": "Chain (10 feet)", - "plural": "Chains (10 feet)", - "description": "A chain has 10 hit points. It can be burst with a successful DC 20 Strength check.", - "value": 5, - "weight": 10 - }, - { - "name": "Chalk (1 piece)", - "plural": "Pieces of chalk", - "description": "", - "value": 0.01, - "weight": 0 - }, - { - "name": "Chest", - "plural": "Chests", - "description": "", - "value": 5, - "weight": 25 - }, - { - "name": "Climber’s kit", - "plural": "Climber’s kits", - "description": "A climber’s kit includes special pitons, boot tips, gloves, and a harness. You can use the climber’s kit as an action to anchor yourself; when you do, you can’t fall more than 25 feet from the point where you anchored yourself, and you can’t climb more than 25 feet away from that point without undoing the anchor.", - "value": 25, - "weight": 12 - }, - { - "name": "Clothes, common", - "plural": "Clothes, common", - "description": "", - "value": 0.5, - "weight": 3 - }, - { - "name": "Clothes, costume", - "plural": "Clothes, costume", - "description": "", - "value": 5, - "weight": 4 - }, - { - "name": "Clothes, fine", - "plural": "Clothes, fine", - "description": "", - "value": 15, - "weight": 6 - }, - { - "name": "Clothes, traveler’s", - "plural": "Clothes, traveler’s", - "description": "", - "value": 2, - "weight": 4 - }, - { - "name": "Component pouch", - "plural": "Component pouches", - "description": "A component pouch is a small, watertight leather belt pouch that has compartments to hold all the material components and other special items you need to cast your spells, except for those components that have a specific cost (as indicated in a spell’s description).", - "value": 25, - "weight": 2 - }, - { - "name": "Crowbar", - "plural": "Crowbars", - "description": "Using a crowbar grants advantage to Strength checks where the crowbar’s leverage can be applied.", - "value": 2, - "weight": 5 - }, - { - "name": "Sprig of mistletoe", - "plural": "Sprigs of mistletoe", - "description": "A druidic focus might be a sprig of mistletoe or holly, a wand or scepter made of yew or another special wood, a staff drawn whole out of a living tree, or a totem object incorporating feathers, fur, bones, and teeth from sacred animals. A druid can use such an object as a spellcasting focus.", - "value": 1, - "weight": 0 - }, - { - "name": "Totem", - "plural": "Totems", - "description": "A druidic focus might be a sprig of mistletoe or holly, a wand or scepter made of yew or another special wood, a staff drawn whole out of a living tree, or a totem object incorporating feathers, fur, bones, and teeth from sacred animals. A druid can use such an object as a spellcasting focus.", - "value": 1, - "weight": 0 - }, - { - "name": "Wooden staff", - "plural": "Wooden staffs", - "description": "A druidic focus might be a sprig of mistletoe or holly, a wand or scepter made of yew or another special wood, a staff drawn whole out of a living tree, or a totem object incorporating feathers, fur, bones, and teeth from sacred animals. A druid can use such an object as a spellcasting focus.", - "value": 5, - "weight": 4 - }, - { - "name": "Yew wand", - "plural": "Yew wands", - "description": "A druidic focus might be a sprig of mistletoe or holly, a wand or scepter made of yew or another special wood, a staff drawn whole out of a living tree, or a totem object incorporating feathers, fur, bones, and teeth from sacred animals. A druid can use such an object as a spellcasting focus.", - "value": 10, - "weight": 1 - }, - { - "name": "Fishing tackle", - "plural": "Fishing tackles", - "description": "This kit includes a wooden rod, silken line, corkwood bobbers, steel hooks, lead sinkers, velvet lures, and narrow netting.", - "value": 1, - "weight": 4 - }, - { - "name": "Flask", - "plural": "Flasks", - "description": "", - "value": 0.02, - "weight": 1 - }, - { - "name": "Tankard", - "plural": "Tankards", - "description": "", - "value": 0.02, - "weight": 1 - }, - { - "name": "Grappling hook", - "plural": "Grappling hooks", - "description": "", - "value": 2, - "weight": 4 - }, - { - "name": "Hammer", - "plural": "Hammers", - "description": "", - "value": 1, - "weight": 3 - }, - { - "name": "Hammer, sledge", - "plural": "Hammers, sledge", - "description": "", - "value": 2, - "weight": 10 - }, - { - "name": "Healer’s kit", - "plural": "Healer’s kits", - "description": "This kit is a leather pouch containing bandages, salves, and splints. The kit has ten uses. As an action, you can expend one use of the kit to stabilize a creature that has 0 hit points, without needing to make a Wisdom (Medicine) check.", - "value": 5, - "weight": 3 - }, - { - "name": "Holy amulet", - "plural": "Holy amulets", - "description": "A holy symbol is a representation of a god or pantheon. It might be an amulet depicting a symbol representing a deity, the same symbol carefully engraved or inlaid as an emblem on a shield, or a tiny box holding a fragment of a sacred relic. Fantasy-Historical Pantheons lists the symbols commonly associated with many gods in the multiverse. A cleric or paladin can use a holy symbol as a spellcasting focus. To use the symbol in this way, the caster must hold it in hand, wear it visibly, or bear it on a shield.", - "value": 5, - "weight": 1 - }, - { - "name": "Holy emblem", - "plural": "Holy emblems", - "description": "A holy symbol is a representation of a god or pantheon. It might be an amulet depicting a symbol representing a deity, the same symbol carefully engraved or inlaid as an emblem on a shield, or a tiny box holding a fragment of a sacred relic. Fantasy-Historical Pantheons lists the symbols commonly associated with many gods in the multiverse. A cleric or paladin can use a holy symbol as a spellcasting focus. To use the symbol in this way, the caster must hold it in hand, wear it visibly, or bear it on a shield.", - "value": 5, - "weight": 0 - }, - { - "name": "Holy reliquary", - "plural": "Holy reliquaries", - "description": "A holy symbol is a representation of a god or pantheon. It might be an amulet depicting a symbol representing a deity, the same symbol carefully engraved or inlaid as an emblem on a shield, or a tiny box holding a fragment of a sacred relic. Fantasy-Historical Pantheons lists the symbols commonly associated with many gods in the multiverse. A cleric or paladin can use a holy symbol as a spellcasting focus. To use the symbol in this way, the caster must hold it in hand, wear it visibly, or bear it on a shield.", - "value": 5, - "weight": 2 - }, - { - "name": "Holy water (flask)", - "plural": "Holy water (flasks)", - "description": "As an action, you can splash the contents of this flask onto a creature within 5 feet of you or throw it up to 20 feet, shattering it on impact. In either case, make a ranged attack against a target creature, treating the holy water as an improvised weapon. If the target is a fiend or undead, it takes 2d6 radiant damage. A cleric or paladin may create holy water by performing a special ritual. The ritual takes 1 hour to perform, uses 25 gp worth of powdered silver, and requires the caster to expend a 1st-level spell slot.", - "value": 25, - "weight": 1 - }, - { - "name": "Hourglass", - "plural": "Hourglasses", - "description": "", - "value": 25, - "weight": 1 - }, - { - "name": "Hunting trap", - "plural": "Hunting traps", - "description": "When you use your action to set it, this trap forms a saw-toothed steel ring that snaps shut when a creature steps on a pressure plate in the center. The trap is affixed by a heavy chain to an immobile object, such as a tree or a spike driven into the ground. A creature that steps on the plate must succeed on a DC 13 Dexterity saving throw or take 1d4 piercing damage and stop moving. Thereafter, until the creature breaks free of the trap, its movement is limited by the length of the chain (typically 3 feet long). A creature can use its action to make a DC 13 Strength check, freeing itself or another creature within its reach on a success. Each failed check deals 1 piercing damage to the trapped creature.", - "value": 5, - "weight": 25 - }, - { - "name": "Ink (1 ounce bottle)", - "plural": "Ink (1 ounce bottles)", - "description": "", - "value": 10, - "weight": 0 - }, - { - "name": "Ink pen", - "plural": "Ink pens", - "description": "", - "value": 0.02, - "weight": 0 - }, - { - "name": "Jug", - "plural": "Jugs", - "description": "", - "value": 0.02, - "weight": 4 - }, - { - "name": "Pitcher", - "plural": "Pitchers", - "description": "", - "value": 0.02, - "weight": 4 - }, - { - "name": "Ladder (10-foot)", - "plural": "Ladders (10-foot)", - "description": "", - "value": 0.1, - "weight": 25 - }, - { - "name": "Lamp", - "plural": "Lamps", - "description": "A lamp casts bright light in a 15-foot radius and dim light for an additional 30 feet. Once lit, it burns for 6 hours on a flask (1 pint) of oil.", - "value": 0.5, - "weight": 1 - }, - { - "name": "Lantern, bullseye", - "plural": "Lanterns, bullseye", - "description": "A bullseye lantern casts bright light in a 60-foot cone and dim light for an additional 60 feet. Once lit, it burns for 6 hours on a flask (1 pint) of oil.", - "value": 10, - "weight": 2 - }, - { - "name": "Lantern, hooded", - "plural": "Lanterns, hooded", - "description": "A hooded lantern casts bright light in a 30-foot radius and dim light for an additional 30 feet. Once lit, it burns for 6 hours on a flask (1 pint) of oil. As an action, you can lower the hood, reducing the light to dim light in a 5-foot radius.", - "value": 5, - "weight": 2 - }, - { - "name": "Lock", - "plural": "Locks", - "description": "A key is provided with the lock. Without the key, a creature proficient with thieves’ tools can pick this lock with a successful DC 15 Dexterity check. Your GM may decide that better locks are available for higher prices.", - "value": 10, - "weight": 1 - }, - { - "name": "Magnifying glass", - "plural": "Magnifying glasses", - "description": "This lens allows a closer look at small objects. It is also useful as a substitute for flint and steel when starting fires. Lighting a fire with a magnifying glass requires light as bright as sunlight to focus, tinder to ignite, and about 5 minutes for the fire to ignite. A magnifying glass grants advantage on any ability check made to appraise or inspect an item that is small or highly detailed.", - "value": 100, - "weight": 0 - }, - { - "name": "Manacles", - "plural": "Sets of manacles", - "description": "These metal restraints can bind a Small or Medium creature. Escaping the manacles requires a successful DC 20 Dexterity check. Breaking them requires a successful DC 20 Strength check. Each set of manacles comes with one key. Without the key, a creature proficient with thieves’ tools can pick the manacles’ lock with a successful DC 15 Dexterity check. Manacles have 15 hit points.", - "value": 2, - "weight": 6 - }, - { - "name": "Mess kit", - "plural": "Mess kits", - "description": "This tin box contains a cup and simple cutlery. The box clamps together, and one side can be used as a cooking pan and the other as a plate or shallow bowl.", - "value": 0.2, - "weight": 1 - }, - { - "name": "Mirror, steel", - "plural": "Mirrors, steel", - "description": "", - "value": 5, - "weight": 0.5 - }, - { - "name": "Oil (flask)", - "plural": "Oil (flasks)", - "description": "Oil usually comes in a clay flask that holds 1 pint. As an action, you can splash the oil in this flask onto a creature within 5 feet of you or throw it up to 20 feet, shattering it on impact. Make a ranged attack against a target creature or object, treating the oil as an improvised weapon. On a hit, the target is covered in oil. If the target takes any fire damage before the oil dries (after 1 minute), the target takes an additional 5 fire damage from the burning oil. You can also pour a flask of oil on the ground to cover a 5-foot-square area, provided that the surface is level. If lit, the oil burns for 2 rounds and deals 5 fire damage to any creature that enters the area or ends its turn in the area. A creature can take this damage only once per turn.", - "value": 0.1, - "weight": 1 - }, - { - "name": "Paper (one sheet)", - "plural": "Sheets of paper", - "description": "", - "value": 0.2, - "weight": 0 - }, - { - "name": "Parchment (one sheet)", - "plural": "Sheets of parchment", - "description": "", - "value": 0.1, - "weight": 0 - }, - { - "name": "Perfume (vial)", - "plural": "Perfume (vials)", - "description": "", - "value": 5, - "weight": 0 - }, - { - "name": "Pick, miner’s", - "plural": "Picks, miner’s", - "description": "", - "value": 2, - "weight": 10 - }, - { - "name": "Piton", - "plural": "Pitons", - "description": "", - "value": 0.05, - "weight": 0.25 - }, - { - "name": "Poison, basic (vial)", - "plural": "Poison, basic (vials)", - "description": "You can use the poison in this vial to coat one slashing or piercing weapon or up to three pieces of ammunition. Applying the poison takes an action. A creature hit by the poisoned weapon or ammunition must make a DC 10 Constitution saving throw or take 1d4 poison damage. Once applied, the poison retains potency for 1 minute before drying.", - "value": 100, - "weight": 0 - }, - { - "name": "Pole (10-foot)", - "plural": "Poles (10-foot)", - "description": "", - "value": 0.05, - "weight": 7 - }, - { - "name": "Pot, iron", - "plural": "Pots, iron", - "description": "", - "value": 2, - "weight": 10 - }, - { - "name": "Potion of healing", - "plural": "Potions of healing", - "description": "A character who drinks the magical red fluid in this vial regains 2d4 + 2 hit points. Drinking or administering a potion takes an action.", - "value": 50, - "weight": 0.5 - }, - { - "name": "Pouch", - "plural": "Pouches", - "description": "A cloth or leather pouch can hold up to 20 sling bullets or 50 blowgun needles, among other things. A compartmentalized pouch for holding spell components is called a component pouch (described earlier in this section).", - "value": 0.5, - "weight": 1 - }, - { - "name": "Quiver", - "plural": "Quivers", - "description": "A quiver can hold up to 20 arrows.", - "value": 1, - "weight": 1 - }, - { - "name": "Ram, portable", - "plural": "Rams, portable", - "description": "You can use a portable ram to break down doors. When doing so, you gain a +4 bonus on the Strength check. One other character can help you use the ram, giving you advantage on this check.", - "value": 4, - "weight": 35 - }, - { - "name": "Rations (1 day)", - "plural": "Days of rations", - "description": "Rations consist of dry foods suitable for extended travel, including jerky, dried fruit, hardtack, and nuts.", - "value": 0.5, - "weight": 2 - }, - { - "name": "Robes", - "plural": "Robes", - "description": "", - "value": 1, - "weight": 4 - }, - { - "name": "Rope, hempen (50 feet)", - "plural": "Ropes, hempen (50 feet)", - "description": "Rope, whether made of hemp or silk, has 2 hit points and can be burst with a DC 17 Strength check.", - "value": 1, - "weight": 10 - }, - { - "name": "Rope, silk (50 feet)", - "plural": "Ropes, silk (50 feet)", - "description": "Rope, whether made of hemp or silk, has 2 hit points and can be burst with a DC 17 Strength check.", - "value": 10, - "weight": 5 - }, - { - "name": "Sack", - "plural": "Sacks", - "description": "", - "value": 0.01, - "weight": 0.5 - }, - { - "name": "Scale, merchant’s", - "plural": "Scales, merchant’s", - "description": "A scale includes a small balance, pans, and a suitable assortment of weights up to 2 pounds. With it, you can measure the exact weight of small objects, such as raw precious metals or trade goods, to help determine their worth.", - "value": 5, - "weight": 3 - }, - { - "name": "Sealing wax", - "plural": "Sealing waxes", - "description": "", - "value": 0.5, - "weight": 0 - }, - { - "name": "Shovel", - "plural": "Shovels", - "description": "", - "value": 2, - "weight": 5 - }, - { - "name": "Signal whistle", - "plural": "Signal whistles", - "description": "", - "value": 0.05, - "weight": 0 - }, - { - "name": "Signet ring", - "plural": "Signet rings", - "description": "", - "value": 5, - "weight": 0 - }, - { - "name": "Soap", - "plural": "Soaps", - "description": "", - "value": 0.02, - "weight": 0 - }, - { - "name": "Spellbook", - "plural": "Spellbooks", - "description": "Essential for wizards, a spellbook is a leather-bound tome with 100 blank vellum pages suitable for recording spells.", - "value": 50, - "weight": 3 - }, - { - "libraryName": "Spikes, iron (10)", - "name": "Spike, iron", - "plural": "Spikes, iron", - "description": "", - "value": 0.1, - "weight": 0.5, - "quantity": 10 - }, - { - "name": "Spyglass", - "plural": "Spyglasses", - "description": "Objects viewed through a spyglass are magnified to twice their size.", - "value": 1000, - "weight": 1 - }, - { - "name": "Tent, two-person", - "plural": "Tents, two-person", - "description": "A simple and portable canvas shelter, a tent sleeps two.", - "value": 2, - "weight": 20 - }, - { - "name": "Tinderbox", - "plural": "Tinderboxes", - "description": "This small container holds flint, fire steel, and tinder (usually dry cloth soaked in light oil) used to kindle a fire. Using it to light a torch — or anything else with abundant, exposed fuel — takes an action. Lighting any other fire takes 1 minute.", - "value": 0.5, - "weight": 1 - }, - { - "name": "Torch", - "plural": "Torches", - "description": "A torch burns for 1 hour, providing bright light in a 20-foot radius and dim light for an additional 20 feet. If you make a melee attack with a burning torch and hit, it deals 1 fire damage.", - "value": 0.01, - "weight": 1 - }, - { - "name": "Vial", - "plural": "Vials", - "description": "", - "value": 1, - "weight": 0 - }, - { - "name": "Waterskin", - "plural": "Waterskins", - "description": "", - "value": 0.2, - "weight": 5 - }, - { - "name": "Whetstone", - "plural": "Whetstones", - "description": "", - "value": 0.01, - "weight": 1 - } -] diff --git a/dataSources/srd/armor.json b/dataSources/srd/armor.json deleted file mode 100644 index 0aa013b1..00000000 --- a/dataSources/srd/armor.json +++ /dev/null @@ -1,244 +0,0 @@ -[ - { - "name": "Padded Armor", - "value": 5, - "weight": 8, - "description": "Padded armor consists of quilted layers of cloth and batting.", - "effects": [ - { - "operation": "base", - "value": 11, - "stat": "armor" - }, - { - "operation": "disadvantage", - "stat": "stealth" - } - ] - }, - { - "name": "Leather Armor", - "value": 10, - "weight": 10, - "description": "The breastplate and shoulder protectors of this armor are made of leather that has been stiffened by being boiled in oil. The rest of the armor is made of softer and more flexible materials.", - "effects": [ - { - "operation": "base", - "value": 11, - "stat": "armor" - } - ] - }, - { - "name": "Studded leather Armor", - "value": 45, - "weight": 13, - "description": "Made from tough but flexible leather, studded leather is reinforced with close-set rivets or spikes.", - "effects": [ - { - "operation": "base", - "value": 12, - "stat": "armor" - } - ] - }, - { - "name": "Hide Armor", - "value": 10, - "weight": 12, - "description": "This crude armor consists of thick furs and pelts. It is commonly worn by barbarian tribes, evil humanoids, and other folk who lack access to the tools and materials needed to create better armor.", - "effects": [ - { - "operation": "base", - "value": 12, - "stat": "armor" - }, - { - "operation": "max", - "value": 2, - "stat": "dexterityArmor" - } - ] - }, - { - "name": "Chain shirt", - "value": 50, - "weight": 20, - "description": "Made of interlocking metal rings, a chain shirt is worn between layers of clothing or leather. This armor offers modest protection to the wearer’s upper body and allows the sound of the rings rubbing against one another to be muffled by outer layers.", - "effects": [ - { - "operation": "base", - "value": 13, - "stat": "armor" - }, - { - "operation": "max", - "value": 2, - "stat": "dexterityArmor" - } - ] - }, - { - "name": "Scale mail", - "value": 50, - "weight": 45, - "description": "This armor consists of a coat and leggings (and perhaps a separate skirt) of leather covered with overlapping pieces of metal, much like the scales of a fish. The suit includes gauntlets.", - "effects": [ - { - "operation": "base", - "value": 14, - "stat": "armor" - }, - { - "operation": "disadvantage", - "stat": "stealth" - }, - { - "operation": "max", - "value": 2, - "stat": "dexterityArmor" - } - ] - }, - { - "name": "Breastplate", - "value": 400, - "weight": 20, - "description": "This armor consists of a fitted metal chest piece worn with supple leather. Although it leaves the legs and arms relatively unprotected, this armor provides good protection for the wearer’s vital organs while leaving the wearer relatively unencumbered.", - "effects": [ - { - "operation": "base", - "value": 14, - "stat": "armor" - }, - { - "operation": "max", - "value": 2, - "stat": "dexterityArmor" - } - ] - }, - { - "name": "Half plate", - "value": 750, - "weight": 40, - "description": "Half plate consists of shaped metal plates that cover most of the wearer’s body. It does not include leg protection beyond simple greaves that are attached with leather straps.", - "effects": [ - { - "operation": "base", - "value": 15, - "stat": "armor" - }, - { - "operation": "max", - "value": 2, - "stat": "dexterityArmor" - }, - { - "operation": "disadvantage", - "stat": "stealth" - } - ] - }, - { - "name": "Ring mail", - "value": 30, - "weight": 40, - "description": "This armor is leather armor with heavy rings sewn into it. The rings help reinforce the armor against blows from swords and axes. Ring mail is inferior to chain mail, and it’s usually worn only by those who can’t afford better armor.", - "effects": [ - { - "operation": "base", - "value": 14, - "stat": "armor" - }, - { - "operation": "mul", - "value": 0, - "stat": "dexterityArmor" - }, - { - "operation": "disadvantage", - "stat": "stealth" - } - ] - }, - { - "name": "Chain mail", - "value": 75, - "weight": 55, - "description": "Made of interlocking metal rings, chain mail includes a layer of quilted fabric worn underneath the mail to prevent chafing and to cushion the impact of blows. The suit includes gauntlets.", - "effects": [ - { - "operation": "base", - "value": 16, - "stat": "armor" - }, - { - "operation": "mul", - "value": 0, - "stat": "dexterityArmor" - }, - { - "operation": "disadvantage", - "stat": "stealth" - } - ] - }, - { - "name": "Splint Armor", - "value": 200, - "weight": 60, - "description": "This armor is made of narrow vertical strips of metal riveted to a backing of leather that is worn over cloth padding. Flexible chain mail protects the joints.", - "effects": [ - { - "operation": "base", - "value": 17, - "stat": "armor" - }, - { - "operation": "mul", - "value": 0, - "stat": "dexterityArmor" - }, - { - "operation": "disadvantage", - "stat": "stealth" - } - ] - }, - { - "name": "Plate Armor", - "value": 1500, - "weight": 65, - "description": "Plate consists of shaped, interlocking metal plates to cover the entire body. A suit of plate includes gauntlets, heavy leather boots, a visored helmet, and thick layers of padding underneath the armor. Buckles and straps distribute the weight over the body.", - "effects": [ - { - "operation": "base", - "value": 18, - "stat": "armor" - }, - { - "operation": "mul", - "value": 0, - "stat": "dexterityArmor" - }, - { - "operation": "disadvantage", - "stat": "stealth" - } - ] - }, - { - "name": "Shield", - "value": 10, - "weight": 6, - "description": "A shield is made from wood or metal and is carried in one hand. Wielding a shield increases your Armor Class by 2. You can benefit from only one shield at a time.", - "effects": [ - { - "operation": "add", - "value": 2, - "stat": "armor" - } - ] - } -] diff --git a/dataSources/srd/spells.json b/dataSources/srd/spells.json deleted file mode 100644 index d4907c6a..00000000 --- a/dataSources/srd/spells.json +++ /dev/null @@ -1,4798 +0,0 @@ -[ - - { - "castingTime": "action", - "description": "A shimmering green arrow streaks toward a target within range and bursts in a spray of acid. Make a ranged spell attack against the target. On a hit, the target takes 4d4 acid damage immediately and 2d4 acid damage aI the end of its next turn. On a miss, the arrow splashes the target with acid for half as much of the initial damage and no damage at the end of its next turn.\n\n***At Higher Levels.*** When you cast this spell using a spell slot of 3rd level or higher, the damage (both initial and later) increases by 1d4 for each slot level above 2nd.", - "duration": "Instantaneous", - "level": 2, - "range": "90 feet", - "school": "Evocation", - "ritual": false, - "name": "Acid Arrow", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "powdered rhubarb leaf and an adder’s stomach" - } - }, - { - "castingTime": "action", - "description": "You hurl a bubble of acid. Choose one creature within range, or choose two creatures within range that are within 5 feet of each other. A target must succeed on a DC {DC} Dexterity saving throw or take 1d6 acid damage. This spell’s damage increases by 1d6 when you reach 5th level (2d6), 11th level (3d6), and 17th level (4d6).", - "duration": "Instantaneous", - "level": 0, - "range": "60 feet", - "school": "Conjuration", - "ritual": false, - "name": "Acid Splash", - "components": { - "verbal": true, - "somatic": true, - "concentration": false - }, - "attacks": [ - { - "attackBonus": "Dex save", - "details": "range 60 feet, 2 targets within 5 feet", - "damage": "{floor((Level+1)/6)+1}d6", - "damageType": "acid" - } - ] - }, - { - "castingTime": "action", - "description": "Your spell bolsters your allies with toughness and resolve. Choose up to three creatures within range. Each target’s hit point maximum and current hit points increase by 5 for the duration. ***At Higher Levels.*** When you cast this spell using a spell slot of 3rd level or higher, a target’s hit points increase by an additional 5 for each slot level above 2nd.", - "duration": "8 hours", - "level": 2, - "range": "30 feet", - "school": "Abjuration", - "ritual": false, - "name": "Aid", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "a tiny strip of white cloth" - } - }, - { - "castingTime": "1 minute", - "description": "You set an alarm against unwanted intrusion. Choose a door, a window, or an area within range that is no larger than a 20-foot cube. Until the spell ends, an alarm alerts you whenever a Tiny or larger creature touches or enters the warded area. When you cast the spell, you can designate creatures that won’t set off the alarm. You also choose whether the alarm is mental or audible.\n\nA mental alarm alerts you with a ping in your mind if you are within 1 mile of the warded area. This ping awakens you if you are sleeping.\n\nAn audible alarm produces the sound of a hand bell for 10 seconds within 60 feet.", - "duration": "8 hours", - "level": 1, - "range": "30 feet", - "school": "Abjuration", - "ritual": true, - "name": "Alarm", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "a tiny bell and a piece of fine silver wire" - } - }, - { - "castingTime": "action", - "description": "You assume a different form. When you cast the spell, choose one of the following options, the effects of which last for the duration of the spell. While the spell lasts, you can end one option as an action to gain the benefits of a different one.\n\nAquatic Adaptation. You adapt your body to an aquatic environment, sprouting gills and growing webbing between your fingers. You can breathe underwater and gain a swimming speed equal to your walking speed.\n\nChange Appearance. You transform your appearance. You decide what you look like, including your height, weight, facial features, sound of your voice, hair length, coloration, and distinguishing characteristics, if any. You can make yourself appear as a member of another race, though none of your statistics change. You also can’t appear as a creature of a different size than you, and your basic shape stays the same; if you’re bipedal, you can’t use this spell to become quadrupedal, for instance. At any time for the duration of the spell, you can use your action to change your appearance in this way again.\n\nNatural Weapons. You grow claws, fangs, spines, horns, or a different natural weapon of your choice. Your unarmed strikes deal 1d6 bludgeoning, piercing, or slashing damage, as appropriate to the natural weapon you chose, and you are proficient with your unarmed strikes. Finally, the natural weapon is magic and you have a +1 bonus to the attack and damage rolls you make using it.", - "duration": "Concentration, up to 1 hour", - "level": 2, - "range": "Self", - "school": "Transmutation", - "ritual": false, - "name": "Alter Self", - "components": { - "verbal": true, - "somatic": true, - "concentration": true - } - }, - { - "castingTime": "action", - "description": "By means of this spell, you use an animal to deliver a message. Choose a Tiny beast you can see within range, such as a squirrel, a blue jay, or a bat. You specify a location, which you must have visited, and a recipient who matches a general description, such as “a man or woman dressed in the uniform of the town guard” or “a red-haired dwarf wearing a pointed hat.” You also speak a message of up to twenty-five words. The target beast travels for the duration of the spell toward the specified location, covering about 50 miles per 24 hours for a flying messenger, or 25 miles for other animals.\n\nWhen the messenger arrives, it delivers your message to the creature that you described, replicating the sound of your voice. The messenger speaks only to a creature matching the description you gave. If the messenger doesn’t reach its destination before the spell ends, the message is lost, and the beast makes its way back to where you cast this spell.\n\n***At Higher Levels.***If you cast this spell using a spell slot of 3nd level or higher, the duration of the spell increases by 48 hours for each slot level above 2nd.", - "duration": "24 hours", - "level": 1, - "range": "30 feet", - "school": "Enchantment", - "ritual": false, - "name": "Animal Messenger", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "a morsel of food" - } - }, - { - "castingTime": "action", - "description": "Your magic turns others into beasts. Choose any number of willing creatures that you can see within range. You transform each target into the form of a Large or smaller beast with a challenge rating of 4 or lower. On subsequent turns, you can use your action to transform affected creatures into new forms.\n\nThe transformation lasts for the duration for each target, or until the target drops to 0 hit points or dies. You can choose a different form for each target. A target’s game statistics are replaced by the statistics of the chosen beast, though the target retains its alignment and Intelligence, Wisdom, and Charisma scores. The target assumes the hit points of its new form, and when it reverts to its normal form, it returns to the number of hit points it had before it transformed. If it reverts as a result of dropping to 0 hit points, any excess damage carries over to its normal form. As long as the excess damage doesn’t reduce the creature’s normal form to 0 hit points, it isn’t knocked unconscious. The creature is limited in the actions it can perform by the nature of its new form, and it can’t speak or cast spells.\n\nThe target’s gear melds into the new form. The target can’t activate, wield, or otherwise benefit from any of its equipment.", - "duration": "Concentration, up to 24 hours", - "level": 8, - "range": "30 feet", - "school": "Transmutation", - "ritual": false, - "name": "Animal Shapes", - "components": { - "verbal": true, - "somatic": true, - "concentration": true - } - }, - { - "castingTime": "1 minute", - "description": "This spell creates an undead servant. Choose a pile of bones or a corpse of a Medium or Small humanoid within range. Your spell imbues the target with a foul mimicry of life, raising it as an undead creature. The target becomes a skeleton if you chose bones or a zombie if you chose a corpse (the GM has the creature’s game statistics).\n\nOn each of your turns, you can use a bonus action to mentally command any creature you made with this spell if the creature is within 60 feet of you (if you control multiple creatures, you can command any or all of them at the same time, issuing the same command to each one). You decide what action the creature will take and where it will move during its next turn, or you can issue a general command, such as to guard a particular chamber or corridor. If you issue no commands, the creature only defends itself against hostile creatures. Once given an order, the creature continues to follow it until its task is complete.\n\nThe creature is under your control for 24 hours, after which it stops obeying any command you’ve given it. To maintain control of the creature for another 24 hours, you must cast this spell on the creature again before the current 24-hour period ends. This use of the spell reasserts your control over up to four creatures you have animated with this spell, rather than animating a new one.\n\n***At Higher Levels.***When you cast this spell using a spell slot of 4th level or higher, you animate or reassert control over two additional undead creatures for each slot level above 3rd. Each of the creatures must come from a different corpse or pile of bones.", - "duration": "Instantaneous", - "level": 3, - "range": "10 feet", - "school": "Necromancy", - "ritual": false, - "name": "Animate Dead", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "a drop of blood, a piece of flesh, and a pinch of bone dust" - } - }, - { - "castingTime": "action", - "description": "Objects come to life at your command. Choose up to ten nonmagical objects within range that are not being worn or carried. Medium targets count as two objects, Large targets count as four objects, Huge targets count as eight objects. You can’t animate any object larger than Huge. Each target animates and becomes a creature under your control until the spell ends or until reduced to 0 hit points.\n\nAs a bonus action, you can mentally command any creature you made with this spell if the creature is within 500 feet of you (if you control multiple creatures, you can command any or all of them at the same time, issuing the same command to each one). You decide what action the creature will take and where it will move during its next turn, or you can issue a general command, such as to guard a particular chamber or corridor. If you issue no commands, the creature only defends itself against hostile creatures. Once given an order, the creature continues to follow it until its task is complete.\n\n### Animated Object Statistics \n\nSize | HP | AC | Attack | Str | Dex \n--- | --- | --- | ---| --- | --- \nTiny | 20 | 18 | +8 to hit, 1d4 + 4 damage | 4 | 18 \nSmall | 25 | 16 | +6 to hit, 1d8 + 2 damage | 6 | 14 \nMedium | 40 | 13 | +5 to hit, 2d6 + 1 damage | 10 | 12 \nLarge | 50 | 10 | +6 to hit, 2d10 + 2 damage | 14 | 10 \nHuge | 80 | 10 | +8 to hit, 2d12 + 4 damage | 18 | 6 \n\nAn animated object is a construct with AC, hit points, attacks, Strength, and Dexterity determined by its size. Its Constitution is 10 and its Intelligence and Wisdom are 3, and its Charisma is 1. Its speed is 30 feet; if the object lacks legs or other appendages it can use for locomotion, it instead has a flying speed of 30 feet and can hover. If the object is securely attached to a surface or a larger object, such as a chain bolted to a wall, its speed is 0. It has blindsight with a radius of 30 feet and is blind beyond that distance. When the animated object drops to 0 hit points, it reverts to its original object form, and any remaining damage carries over to its original object form.\n\nIf you command an object to attack, it can make a single melee attack against a creature within 5 feet of it. It makes a slam attack with an attack bonus and bludgeoning damage determined by its size. The GM might rule that a specific object inflicts slashing or piercing damage based on its form.\n\n***At Higher Levels.***If you cast this spell using a spell slot of 6th level or higher, you can animate two additional objects for each slot level above 5th.", - "duration": "Concentration, up to 1 minute", - "level": 5, - "range": "120 feet", - "school": "Transmutation", - "ritual": false, - "name": "Animate Objects", - "components": { - "verbal": true, - "somatic": true, - "concentration": true - } - }, - { - "castingTime": "action", - "description": "A shimmering barrier extends out from you in a 10-foot radius and moves with you, remaining centered on you and hedging out creatures other than undead and constructs. The barrier lasts for the duration.\n\nThe barrier prevents an affected creature from passing or reaching through. An affected creature can cast spells or make attacks with ranged or reach weapons through the barrier.\n\nIf you move so that an affected creature is forced to pass through the barrier, the spell ends.", - "duration": "Concentration, up to 1 hour", - "level": 5, - "range": "Self (10-foot radius)", - "school": "Abjuration", - "ritual": false, - "name": "Antilife Shell", - "components": { - "verbal": true, - "somatic": true, - "concentration": true - } - }, - { - "castingTime": "action", - "description": "A 10-foot-radius invisible sphere of antimagic surrounds you. This area is divorced from the magical energy that suffuses the multiverse. Within the sphere, spells can’t be cast, summoned creatures disappear, and even magic items become mundane. Until the spell ends, the sphere moves with you, centered on you. Spells and other magical effects, except those created by an artifact or a deity, are suppressed in the sphere and can’t protrude into it. A slot expended to cast a suppressed spell is consumed. While an effect is suppressed, it doesn’t function, but the time it spends suppressed counts against its duration. Targeted Effects. Spells and other magical effects, such as magic missile and charm person, that target a creature or an object in the sphere have no effect on that target. Areas of Magic. The area of another spell or magical effect, such as fireball, can’t extend into the sphere. If the sphere overlaps an area of magic, the part of the area that is covered by the sphere is suppressed. For example, the flames created by a wall of fire are suppressed within the sphere, creating a gap in the wall if the overlap is large enough. Spells. Any active spell or other magical effect on a creature or an object in the sphere is suppressed while the creature or object is in it. Magic Items. The properties and powers of magic items are suppressed in the sphere. For example, a +1 longsword in the sphere functions as a nonmagical longsword. A magic weapon’s properties and powers are suppressed if it is used against a target in the sphere or wielded by an attacker in the sphere. If a magic weapon or a piece of magic ammunition fully leaves the sphere (for example, if you fire a magic arrow or throw a magic spear at a target outside the sphere), the magic of the item ceases to be suppressed as soon as it exits. \n\nMagical Travel. Teleportation and planar travel fail to work in the sphere, whether the sphere is the destination or the departure point for such magical travel. A portal to another location, world, or plane of existence, as well as an opening to an extradimensional space such as that created by the rope trick spell, temporarily closes while in the sphere. Creatures and Objects. A creature or object summoned or created by magic temporarily winks out of existence in the sphere. Such a creature instantly reappears once the space the creature occupied is no longer within the sphere. Dispel Magic. Spells and magical effects such as dispel magic have no effect on the sphere. Likewise, the spheres created by different antimagic field spells don’t nullify each other.", - "duration": "Concentration, up to 1 hour", - "level": 8, - "range": "Self (10-foot-radius sphere)", - "school": "Abjuration", - "ritual": true, - "name": "Antimagic Field", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "a pinch of powdered iron or iron filings" - } - }, - { - "castingTime": "1 hour", - "description": "This spell attracts or repels creatures of your choice. You target something within range, either a Huge or smaller object or creature or an area that is no larger than a 200-foot cube. Then specify a kind of intelligent creature, such as red dragons, goblins, or vampires. You invest the target with an aura that either attracts or repels the specified creatures for the duration. Choose antipathy or sympathy as the aura’s effect.\n\nAntipathy.The enchantment causes creatures of the kind you designated to feel an intense urge to leave the area and avoid the target. When such a creature can see the target or comes within 60 feet of it, the creature must succeed on a DC {DC} Wisdom saving throw or become frightened. The creature remains frightened while it can see the target or is within 60 feet of it. While frightened by the target, the creature must use its movement to move to the nearest safe spot from which it can’t see the target. If the creature moves more than 60 feet from the target and can’t see it, the creature is no longer frightened, but the creature becomes frightened again if it regains sight of the target or moves within 60 feet of it.\n\nSympathy.The enchantment causes the specified creatures to feel an intense urge to approach the target while within 60 feet of it or able to see it. When such a creature can see the target or comes within 60 feet of it, the creature must succeed on a DC {DC} Wisdom saving throw or use its movement on each of its turns to enter the area or move within reach of the target. When the creature has done so, it can’t willingly move away from the target.\n\nIf the target damages or otherwise harms an affected creature, the affected creature can make a DC {DC} Wisdom saving throw to end the effect, as described below.\n\nEnding the Effect.If an affected creature ends its turn while not within 60 feet of the target or able to see it, the creature makes a DC {DC} Wisdom saving throw. On a successful save, the creature is no longer affected by the target and recognizes the feeling of repugnance or attraction as magical. In addition, a creature affected by the spell is allowed another Wisdom saving throw every 24 hours while the spell persists.\n\nA creature that successfully saves against this effect is immune to it for 1 minute, after which time it can be affected again.", - "duration": "10 days", - "level": 8, - "range": "60 feet", - "school": "Enchantment", - "ritual": false, - "name": "Antipathy/Sympathy", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "either a lump of alum soaked in vinegar for the antipathy effect or a drop of honey for the sympathy effect" - } - }, - { - "castingTime": "action", - "description": "You create an invisible, magical eye within range that hovers in the air for the duration.\n\nYou mentally receive visual information from the eye, which has normal vision and darkvision out to 30 feet. The eye can look in every direction.\n\nAs an action, you can move the eye up to 30 feet in any direction. There is no limit to how far away from you the eye can move, but it can’t enter another plane of existence. A solid barrier blocks the eye’s movement, but the eye can pass through an opening as small as 1 inch in diameter.", - "duration": "Concentration, up to 1 hour", - "level": 4, - "range": "30 feet", - "school": "Divination", - "ritual": false, - "name": "Arcane Eye", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "a bit of bat fur" - } - }, - { - "castingTime": "action", - "description": "You create a Large hand of shimmering, translucent force in an unoccupied space that you can see within range. The hand lasts for the spell’s duration, and it moves at your command, mimicking the movements of your own hand.\n\nThe hand is an object that has AC 20 and hit points equal to your hit point maximum. If it drops to 0 hit points, the spell ends. It has a Strength of 26 (+8) and a Dexterity of 10 (+0). The hand doesn’t fill its space. When you cast the spell and as a bonus action on your subsequent turns, you can move the hand up to 60 feet and then cause one of the following effects with it.\n\nClenched Fist. The hand strikes one creature or object within 5 feet of it. Make a melee spell attack for the hand using your game statistics. On a hit, the target takes 4d8 force damage.\n\nForceful Hand. The hand attempts to push a creature within 5 feet of it in a direction you choose. Make a check with the hand’s Strength contested by the Strength (Athletics) check of the target. If the target is Medium or smaller, you have advantage on the check. If you succeed, the hand pushes the target up to 5 feet plus a number of feet equal to five times your spellcasting ability modifier. The hand moves with the target to remain within 5 feet of it.\n\nGrasping Hand. The hand attempts to grapple a Huge or smaller creature within 5 feet of it. You use the hand’s Strength score to resolve the grapple. If the target is Medium or smaller, you have advantage on the check. While the hand is grappling the target, you can use a bonus action to have the hand crush it. When you do so, the target takes bludgeoning damage equal to 2d6 + your spellcasting ability modifier.\n\nInterposing Hand. The hand interposes itself between you and a creature you choose until you give the hand a different command. The hand moves to stay between you and the target, providing you with half cover against the target. The target can’t move through the hand’s space if its Strength score is less than or equal to the hand’s Strength score. If its Strength score is higher than the hand’s Strength score, the target can move toward you through the hand’s space, but that space is difficult terrain for the target.\n\nAt \n\nHigher Levels. When you cast this spell using a spell slot of 6th level or higher, the damage from the clenched fist option increases by 2d8 and the damage from the grasping hand increases by 2d6 for each slot level above 5th.", - "duration": "Concentration, up to 1 minute", - "level": 5, - "range": "120 feet", - "school": "Evocation", - "ritual": false, - "name": "Arcane Hand", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "an eggshell and a snakeskin glove" - } - }, - { - "castingTime": "action", - "description": "You touch a closed door, window, gate, chest, or other entryway, and it becomes locked for the duration. You and the creatures you designate when you cast this spell can open the object normally. You can also set a password that, when spoken within 5 feet of the object, suppresses this spell for 1 minute. Otherwise, it is impassable until it is broken or the spell is dispelled or suppressed. Casting knock on the object suppresses arcane lock for 10 minutes.\n\nWhile affected by this spell, the object is more difficult to break or force open; the DC to break it or pick any locks on it increases by 10.", - "duration": "Until dispelled", - "level": 2, - "range": "Touch", - "school": "Abjuration", - "ritual": false, - "name": "Arcane Lock", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "gold dust worth at least 25 gp, which the spell consumes" - } - }, - { - "castingTime": "1 hour", - "description": "You and up to eight willing creatures within range project your astral bodies into the Astral Plane (the spell fails and the casting is wasted if you are already on that plane). The material body you leave behind is unconscious and in a state of suspended animation; it doesn't need food or air and doesn't age.\n\nYour astral body resembles your mortal form in almost every way, replicating your game statistics and possessions. The principal difference is the addition of a silvery cord that extends from between your shoulder blades and trails behind you, fading to invisibility after 1 foot. This cord is your tether to your material body. As long as the tether remains intact, you can find your way home. If the cord is cut-something that can happen only when an effect specifically states that it does-your soul and body are separated, killing you instantly.\n\nYour astral form can freely travel through the Astral Plane and can pass through portals there leading to any other plane. If you enter a new plane or return to the plane you were on when casting this spell, your body and possessions are transported along the silver cord, allowing you to re-enter your body as you enter the new plane. Your astral form is a separate incarnation. Any damage or other effects that apply to it have no effect on your physical body, nor do they persist when you return to it.\n\nThe spell ends for you and your companions when you use your action to dismiss it. When the spell ends, the affected creature returns to its physical body, and it awakens.\n\nThe spell might also end early for you or one of your companions. A successful dispel magic spell used against an astral or physical body ends the spell for that creature. If a creature's original body or its astral form drops to 0 hit points, the spell ends for that creature. If the spell ends and the silver cord is intact, the cord pulls the creature's astral form back to its body, ending its state of suspended animation.\n\nIf you are returned to your body prematurely, your companions remain in their astral forms and must find their own way back to their bodies, usually by dropping to 0 hit points.", - "duration": "Special", - "level": 9, - "range": "10 feet", - "school": "Necromancy", - "ritual": false, - "name": "Astral Projection", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "for each creature you affect with this spell, you must provide one jacinth worth at least 1,000 gp and one ornately carved bar of silver worth at least 100 gp, all of which the spell consumes" - } - }, - { - "castingTime": "1 minute", - "description": "By casting gem-inlaid sticks, rolling dragon bones, laying out ornate cards, or employing some other divining tool, you receive an omen from an otherworldly entity about the results of a specific course of action that you plan to take within the next 30 minutes. The DM chooses from the following possible omens:\n\n• Weal, for good results\n\n• Woe, for bad results\n\n• Weal and woe, for both good and bad results\n\n• Nothing, for results that aren’t especially good or bad\n\nThe spell doesn’t take into account any possible circumstances that might change the outcome, such as the casting of additional spells or the loss or gain of a companion.\n\nIf you cast the spell two or more times before completing your next long rest, there is a cumulative 25 percent chance for each casting after the first that you get a random reading. The DM makes this roll in secret.", - "duration": "Instantaneous", - "level": 2, - "range": "Self", - "school": "Divination", - "ritual": false, - "name": "Augury", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "specially marked sticks, bones, or similar tokens worth at least 25 gp" - } - }, - { - "castingTime": "8 hours", - "description": "After spending the casting time tracing magical pathways within a precious gemstone, you touch a Huge or smaller beast or plant. The target must have either no Intelligence score or an Intelligence of 3 or less. The target gains an Intelligence of 10. The target also gains the ability to speak one language you know. If the target is a plant, it gains the ability to move its limbs, roots, vines, creepers, and so forth, and it gains senses similar to a human’s. Your DM chooses statistics appropriate for the awakened plant, such as the statistics for the awakened shrub or the awakened tree.\n\nThe awakened beast or plant is charmed by you for 30 days or until you or your companions do anything harmful to it. When the charmed condition ends, the awakened creature chooses whether to remain friendly to you, based on how you treated it while it was charmed.", - "duration": "Instantaneous", - "level": 5, - "range": "Touch", - "school": "Transmutation", - "ritual": false, - "name": "Awaken", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "an agate worth at least 1,000 gp, which the spell consumes" - } - }, - { - "castingTime": "action", - "description": "Up to three creatures of your choice that you can see within range must make Charisma saving throws. Whenever a target that fails this saving throw makes an attack roll or a saving throw before the spell ends, the target must roll a d4 and subtract the number rolled from the attack roll or saving throw.\n\n***At Higher Levels.*** When you cast this spell using a spell slot of 2nd level or higher, you can target one additional creature for each slot level above 1st.", - "duration": "Concentration, up to 1 minute", - "level": 1, - "range": "30 feet", - "school": "Enchantment", - "ritual": false, - "name": "Bane", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "a drop of blood" - } - }, - { - "castingTime": "action", - "description": "You attempt to send one creature that you can see within range to another plane of existence. The target must succeed on a DC {DC} Charisma saving throw or be banished. \n\nIf the target is native to the plane of existence you’re on, you banish the target to a harmless demiplane. While there, the target is incapacitated. The target remains there until the spell ends, at which point the target reappears in the space it left or in the nearest unoccupied space if that space is occupied.\n\nIf the target is native to a different plane of existence than the one you’re on, the target is banished with a faint popping noise, returning to its home plane. If the spell ends before 1 minute has passed, the target reappears in the space it left or in the nearest unoccupied space if that space is occupied. Otherwise, the target doesn’t return.\n\n***At Higher Levels.*** When you cast this spell using a spell slot of 5th level or higher, you can target one additional creature for each slot level above 4th.", - "duration": "Concentration, up to 1 minute", - "level": 4, - "range": "60 feet", - "school": "Abjuration", - "ritual": false, - "name": "Banishment", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "an item distasteful to the target" - } - }, - { - "castingTime": "action", - "description": "You touch a willing creature. Until the spell ends, the target’s skin has a rough, bark-like appearance, and the target’s AC can’t be less than 16, regardless of what kind of armor it is wearing.", - "duration": "Concentration, up to 1 hour", - "level": 2, - "range": "Touch", - "school": "Transmutation", - "ritual": false, - "name": "Barkskin", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "a handfuI of oak bark" - } - }, - { - "castingTime": "action", - "description": "This spell bestows hope and vitality. Choose any number of creatures within range. For the duration, each target has advantage on Wisdom saving throws and death saving throws, and regains the maximum number of hit points possible from any healing.", - "duration": "Concentration, up to 1 minute", - "level": 3, - "range": "30 feet", - "school": "Abjuration", - "ritual": false, - "name": "Beacon of Hope", - "components": { - "verbal": true, - "somatic": true, - "concentration": true - } - }, - { - "castingTime": "action", - "description": "You touch a creature, and that creature must succeed on a DC {DC} Wisdom saving throw or become cursed for the duration of the spelL When you cast this spell, choose the nature of the curse from the following options:\n\n* Choose one ability score. While cursed, the target has disadvantage on ability checks and saving throws made with that ability score.\n\n* While cursed, the target has disadvantage on attack rolls against you.\n\n* While cursed, the target must make a DC {DC} Wisdom saving throw at the start of each of its turns. If it fails, it wastes its action that turn doing nothing.\n\n* While the target is cursed, your attacks and spells deal an extra ld8 necrotic damage to the target.\n\nA remove curse spell ends this effect. At the DM’s option, you may choose an alternative curse effect, but it should be no more powerful than those described above. The DM has final say on such a curse’s effect.\n\n***At Higher Levels.*** If you cast this spell using a spell slot of 4th level or higher, the duration is concentration, up to 10 minutes. If you use a spell slot of 5th level or higher, the duration is 8 hours. If you use a spell slot of 7th level or higher, the duration is 24 hours. If you use a 9th level spell slot, the spell lasts until it is dispelled. Using a spell slot of 5th level or higher grants a duration that doesn’t require concentration. ", - "duration": "Concentration, up to 1 minute", - "level": 3, - "range": "Touch", - "school": "Necromancy", - "ritual": false, - "name": "Bestow Curse", - "components": { - "verbal": true, - "somatic": true, - "concentration": true - } - }, - { - "castingTime": "action", - "description": "Squirming, ebony tentacles fill a 20-foot square on ground that you can see within range. For the duration, these tentacles turn the ground in the area into difficult terrain.\n\nWhen a creature enters the affected area for the first time on a turn or starts its turn there, the creature must succeed on a DC {DC} Dexterity saving throw or take 3d6 bludgeoning damage and be restrained by the tentacles until the spell ends. A creature that starts its turn in the area and is already restrained by the tentacles lakes 3d6 bludgeoning damage.\n\nA creature restrained by the tentacles can use its action lo make a Strength or Dexterity check (its choice) against your spell save DC. On a success, it frees itself.", - "duration": "Concentration, up to 1 minute", - "level": 4, - "range": "90 feet", - "school": "Conjuration", - "ritual": false, - "name": "Black Tentacles", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "a piece of tentacle from a giant octopus or a giant squid" - } - }, - { - "castingTime": "action", - "description": "You create a vertical wall of whirling, razor-sharp blades made of magical energy. The wall appears within range and lasts for the duration. You can make a straight wall up to 100 feet long, 20 feet high, and 5 feet thick, or a ringed wall up to 60 feet in diameter, 20 feet high, and 5 feet thick. The wall provides three-quarters cover to creatures behind it, and its space is difficult terrain.\n\nWhen a creature enters the wall’s area for the first time on a turn or starts its turn there, the creature must make a DC {DC} Dexterity saving throw. On a failed save, the creature takes 6d10 slashing damage. On a successful save, the creature takes half as much damage.", - "duration": "Concentration, up to 10 minutes", - "level": 6, - "range": "90 feet", - "school": "Evocation", - "ritual": false, - "name": "Blade Barrier", - "components": { - "verbal": true, - "somatic": true, - "concentration": true - } - }, - { - "castingTime": "action", - "description": "You bless up to three creatures of your choice within range. Whenever a target makes an attack roll or a saving throw before the spell ends, the target can roll a d4 and add the number rolled to the attack roll or saving throw.\n\n***At Higher Levels.*** When you cast this spell using a spell slot of 2nd level or higher, you can target one additional creature for each slot level above 1st.", - "duration": "Concentration, up to 1 minute", - "level": 1, - "range": "30 feet", - "school": "Enchantment", - "ritual": false, - "name": "Bless", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "a sprinkling of holy water" - } - }, - { - "castingTime": "action", - "description": "Necromantic energy washes over a creature of your choice that you can see within range, draining moisture and vitality from it. The target must make a DC {DC} Constitution saving throw. The target takes 8d8 necrotic damage on a failed save, or half as much damage on a successful one. This spell has no effect on undead or constructs.\n\nIf you target a plant creature or a magical plant, it makes the saving throw with disadvantage, and the spell deals maximum damage to it.\n\nIf you target a nonmagical plant that isn’t a creature, such as a tree or shrub, it doesn’t make a saving throw; it simply withers and dies.\n\n***At Higher Levels.*** When you cast this spell using a spell slot of 5th level or higher, the damage increases by 1d8 for each slot level above 4th.", - "duration": "Instantaneous", - "level": 4, - "range": "30 feet", - "school": "Necromancy", - "ritual": false, - "name": "Blight", - "components": { - "verbal": true, - "somatic": true, - "concentration": false - } - }, - { - "castingTime": "action", - "description": "You can blind or deafen a foe. Choose one creature that you can see within range to make a DC {DC} Constitution saving throw. If it fails, the target is either blinded or deafened (your choice) for the duration. At the end of each of its turns, the target can make a DC {DC} Constitution saving throw. On a success, the spell ends.\n\n***At Higher Levels.*** When you cast this spell using a spell slot of 3rd level or higher, you can target one additional creature for each slot level above 2nd.", - "duration": "1 minute", - "level": 2, - "range": "30 feet", - "school": "Necromancy", - "ritual": false, - "name": "Blindness/Deafness", - "components": { - "verbal": true, - "somatic": false, - "concentration": false - } - }, - { - "castingTime": "action", - "description": "Roll a d20 at the end of each of your turns for the duration of the spell. On a roll of 11 or higher, you vanish from your current plane of existence and appear in the Ethereal Plane (the spell fails and the casting is wasted if you were already on that plane). At the start of your next turn, and when the spell ends if you are on the Ethereal Plane, you return to an unoccupied space of your choice that you can see within 10 feet of the space you vanished from. If no unoccupied space is available within that range, you appear in the nearest unoccupied space (chosen at random if more than one space is equally near). You can dismiss this spell as an action.\n\nWhile on the Ethereal Plane, you can see and hear the plane you originated from, which is cast in shades of gray, and you can’t see anything there more than 60 feet away. You can only affect and be affected by other creatures on the Ethereal Plane. Creatures that aren’t there can’t perceive you or interact with you, unless they have the ability to do so.", - "duration": "1 minute", - "level": 3, - "range": "Self", - "school": "Transmutation", - "ritual": false, - "name": "Blink", - "components": { - "verbal": true, - "somatic": true, - "concentration": false - } - }, - { - "castingTime": "action", - "description": "Your body becomes blurred, shifting and wavering to all who can see you. For the duration, any creature has disadvantage on attack rolls against you. An attacker is immune to this effect if it doesn’t rely on sight, as with blindsight, or can see through illusions, as with truesight.", - "duration": "Concentration, up to 1 minute", - "level": 2, - "range": "Self", - "school": "Illusion", - "ritual": false, - "name": "Blur", - "components": { - "verbal": true, - "somatic": false, - "concentration": true - } - }, - { - "castingTime": "action", - "description": "As you hold your hands with thumbs touching and fingers spread, a thin sheet of flames shoots forth from your outstretched fingertips. Each creature in a 15-foot cone must make a DC {DC} Dexterity saving throw. A creature takes 3d6 fire damage on a failed save, or half as much damage on a successful one.\n\nThe fire ignites any flammable objects in the area that aren’t being worn or carried.\n\n***At Higher Levels.*** When you cast this spell using a spell slot of 2nd level or higher, the damage increases by 1d6 for each slot level above 1st.", - "duration": "Instantaneous", - "level": 1, - "range": "Self (15-foot cone)", - "school": "Evocation", - "ritual": false, - "name": "Burning Hands", - "components": { - "verbal": true, - "somatic": true, - "concentration": false - } - }, - { - "castingTime": "action", - "description": "A storm cloud appears in the shape of a cylinder that is 10 feet tall with a 60-foot radius, centered on a point you can see 100 feet directly above you. The spell fails if you can’t see a point in the air where the storm cloud could appear (for example, if you are in a room that can’t accommodate the cloud).\n\nWhen you cast the spell, choose a point you can see within range. A bolt of lightning flashes down from the cloud to that point. Each creature within 5 feet of that point must make a DC {DC} Dexterity saving throw. A creature takes 3d10 lightning damage on a failed sav , or half as much damage on a successful one. On each of your turns until the spell ends, you can use your action to call down lightning in this way again, targeting the same point or a different one.\n\nIf you are outdoors in stormy conditions when you cast this spell, the spell gives you control over the existing storm instead of creating a new one. Under such conditions, the spell’s damage increases by 1d10.\n\n***At Higher Levels.*** When you cast this spell using a spell slot of 4th or higher level, the damage increases by 1d10 for each slot level above 3rd.", - "duration": "Concentration, up to 10 minutes", - "level": 3, - "range": "120 feet", - "school": "Conjuration", - "ritual": false, - "name": "Call Lightning", - "components": { - "verbal": true, - "somatic": true, - "concentration": true - } - }, - { - "castingTime": "action", - "description": "You attempt to suppress strong emotions in a group of people. Each humanoid in a 20-foot-radius sphere centered on a point you choose within range must make a DC {DC} Charisma saving throw; a creature can choose to fail this saving throw if it wishes. If a creature fails its saving throw, choose one of the following two effects.\n\nYou can suppress any effect causing a target to be charmed or frightened. When this spell ends, any suppressed effect resumes, provided that its duration has not expired in the meantime.\n\nAlternatively, you can make a target indifferent about creatures of your choice that it is hostile toward. This indifference ends if the target is attacked or harmed by a spell or if it witnesses any of its friends being harmed. When the spell ends, the creature becomes hostile again, unless the DM rules otherwise.", - "duration": "Concentration, up to 1 minute", - "level": 2, - "range": "60 feet", - "school": "Enchantment", - "ritual": false, - "name": "Calm Emotions", - "components": { - "verbal": true, - "somatic": true, - "concentration": true - } - }, - { - "castingTime": "action", - "description": "You create a bolt of lightning that arcs toward a target of your choice that you can see within range. Three bolts then leap from that target to as many as three other targets, each of which must be within 30 feet of the first target. A target can be a creature or an object and can be targeted by only one of the bolts. A target must make a DC {DC} Dexterity saving throw. The target takes 10d8 lightning damage on a failed save, or half as much damage on a successful one. ***At Higher Levels.*** When you cast this spell using a spell slot of 7th level or higher, one additional bolt leaps from the first target to another target for each slot level above 6th.", - "duration": "Instantaneous", - "level": 6, - "range": "150 feet", - "school": "Evocation", - "ritual": false, - "name": "Chain Lightning", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "a bit of fur; a piece of amber, glass, or a crystal rod; and three silver pins" - } - }, - { - "castingTime": "action", - "description": "You attempt to charm a humanoid you can see within range. It must make a DC {DC} Wisdom saving throw, and does so with advantage if you or your companions are fighting it. If it fails the saving throw, it is charmed by you until the spell ends or until you or your companions do anything harmful to it. The charmed creature regards you as a friendly acquaintance. When the spell ends, the creature knows it was charmed by you.\n\n***At Higher Levels.*** When you cast this spell using a spell slot of 2nd level or higher, you can target one additional creature for each slot level above 1st. The creatures must be within 30 feet of each other when you target them.", - "duration": "1 hour", - "level": 1, - "range": "30 feet", - "school": "Enchantment", - "ritual": false, - "name": "Charm Person", - "components": { - "verbal": true, - "somatic": true, - "concentration": false - } - }, - { - "castingTime": "action", - "description": "You create a ghostly, skeletal hand in the space of a creature within range. Make a ranged spell attack against the creature to assail it with the chill of the grave. On a hit, the target takes 1d8 necrotic damage, and it can’t regain hit points until the start of your next turn. Until then, the hand clings to the target.\n\nIf you hit an undead target, it also has disadvantage on attack rolls against you until the end of your next turn.\n\nThis spell’s damage increases by 1d8 when you reach 5th level (2d8), 11th level (3d8), and 17th level (4d8).", - "duration": "1 round", - "level": 0, - "range": "120 feet", - "school": "Necromancy", - "ritual": false, - "name": "Chill Touch", - "components": { - "verbal": true, - "somatic": true, - "concentration": false - }, - "attacks": [ - { - "details": "range 120 feet", - "damage": "{floor((Level+1)/6)+1}d8", - "damageType": "necrotic" - } - ] - }, - { - "castingTime": "action", - "description": "A sphere of negative energy ripples out in a 60-foot radius sphere from a point within range. Each creature in that area must make a DC {DC} Constitution saving throw. A target takes 8d6 necrotic damage on a failed save, or half as much damage on a successful one.\n\n***At Higher Levels.*** When you cast this spell using a spell slot of 7th level or higher, the damage increases by 2d6 for each slot level above 6th.", - "duration": "Instantaneous", - "level": 6, - "range": "150 feet", - "school": "Necromancy", - "ritual": false, - "name": "Circle of Death", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "the powder of a crushed black pearl worth at least 500 gp" - } - }, - { - "castingTime": "10 minutes", - "description": "You create an invisible sensor within range in a location familiar to you (a place you have visited or seen before) or in an obvious location that is unfamiliar to you (such as behind a door, around a corner, or in a grove of trees). The sensor remains in place for the duration, and it can’t be attacked or otherwise interacted with.\n\nWhen you cast the spell, you choose seeing or hearing. You can use the chosen sense through the sensor as if you were in its space. As your action, you can switch between seeing and hearing.\n\nA creature that can see the sensor (such as a creature benefiting from see invisibility or truesight) sees a luminous, intangible orb about the size of your fist.", - "duration": "Concentration, up to 10 minutes", - "level": 3, - "range": "1 mile", - "school": "Divination", - "ritual": false, - "name": "Clairvoyance", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "a focus worth at least 100 gp, either a jeweled horn for hearing or a glass eye for seeing" - } - }, - { - "castingTime": "1 hour", - "description": "This spell grows an inert duplicate o f a living creature as a safeguard against death. This clone forms inside a sealed vessel and grows to full size and maturity after 120 days; you can also choose to have the clone be a younger version of the same creature. It remains inert and endures indefinitely, as long as its vessel remains undisturbed.\n\nAt any time after the clone matures, if the original creature dies, its soul transfers to the clone, provided that the soul is free and w illing to return. The clone is physically identical to the original and has the same personality, memories, and abilities, but none of the original’s equipment. The original creature’s physical remains, if they still exist, become inert and can’t thereafter be restored to life, since the creature’s soul is elsewhere.", - "duration": "Instantaneous", - "level": 8, - "range": "Touch", - "school": "Necromancy", - "ritual": false, - "name": "Clone", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "a diamond worth at least 1,000 gp and at least 1 cubic inch of flesh of the creature that is to be cloned, which the spell consumes, and a vessel worth at least 2,000 gp that has a sealable lid and is large enough to hold a Medium creature, such as a huge urn, coffin, mud-filled cyst in the ground, or crystal container filled with salt water" - } - }, - { - "castingTime": "action", - "description": "You fill the air with spinning daggers in a cube 5 feet on each side, centered on a point you choose within range. A creature takes 4d4 slashing damage when it enters the spell’s area for the first time on a turn or starts its turn there.\n\n***At Higher Levels.*** When you cast this spell using a spell slot of 3rd level or higher, the damage increases by 2d4 for each slot level above 2nd.", - "duration": "Concentration, up to 10 minutes", - "level": 5, - "range": "120 feet", - "school": "Conjuration", - "ritual": false, - "name": "Cloudkill", - "components": { - "verbal": true, - "somatic": true, - "concentration": true - } - }, - { - "castingTime": "action", - "description": "A dazzling array of flashing, colored light springs from your hand. Roll 6d10; the total is how many hit points of creatures this spell can effect. Creatures in a 15-foot cone originating from you are affected in ascending order of their current hit points (ignoring unconscious creatures and creatures that can’t see).\n\nStarting with the creature that has the lowest current hit points, each creature affected by this spell is blinded until the spell ends. Subtract each creature’s hit points from the total before moving on to the creature with the next lowest hit points. A creature’s hit points must be equal to or less than the remaining total for that creature to be affected.\n\n***At Higher Levels.*** When you cast this spell using a spell slot of 2nd level or higher, roll an additional 2d10 for each slot level above 1st.", - "duration": "1 round", - "level": 1, - "range": "Self (15-foot cone)", - "school": "Illusion", - "ritual": false, - "name": "Color Spray", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "a pinch of powder or sand that is colored red, yellow, and blue" - } - }, - { - "castingTime": "action", - "description": "You speak a one-word command to a creature you can see within range. The target must succeed on a DC {DC} Wisdom saving throw or follow the command on its next turn. The spell has no effect if the target is undead, if it doesn’t understand your language, or if your command is directly harmful to it.\n\nSome typical commands and their effects follow. You might issue a command other than one described here. If you do so, the DM determines how the target behaves. If the target can’t follow your command, the spell ends.\n\nApproach. The target moves toward you by the shortest and most direct route, ending its turn if it moves within 5 feet of you.\n\nDrop. The target drops whatever it is holding and then ends its turn.\n\nFlee. The target spends its turn moving away from you by the fastest available means.\n\nGrovel. The target falls prone and then ends its turn.\n\nHalt. The target doesn’t move and takes no actions. A flying creature stays aloft, provided that it is able to do so. If it must move to stay aloft, it flies the minimum distance needed to remain in the air.\n\n***At Higher Levels.*** When you cast this spell using a spell slot of 2nd level or higher, you can affect one additional creature for each slot level above 1st. The creatures must be within 30 feet of each other when you target them.", - "duration": "1 round", - "level": 1, - "range": "60 feet", - "school": "Enchantment", - "ritual": false, - "name": "Command", - "components": { - "verbal": true, - "somatic": false, - "concentration": false - } - }, - { - "castingTime": "1 minute", - "description": "You contact your deity or a divine proxy and ask up to three questions that can be answered with a yes or no. You must ask your questions before the spell ends. You receive a correct answer for each question. Divine beings aren’t necessarily omniscient, so you might receive “unclear” as an answer if a question pertains to information that lies beyond the deity’s knowledge. In a case where a one-word answer could be misleading or contrary to the deity’s interests, the DM might offer a short phrase as an answer instead. If you cast the spell two or more times before finishing your next long rest, there is a cumulative 25 percent chance for each casting after the first that you get no answer. The DM makes this roll in secret.", - "duration": "1 minute", - "level": 5, - "range": "Self", - "school": "Divination", - "ritual": false, - "name": "Commune", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "incense and a vial of holy or unholy water" - } - }, - { - "castingTime": "1 minute", - "description": "You briefly become one with nature and gain knowledge of the surrounding territory. In the outdoors, the spell gives you knowledge of the land within 3 miles of you. In caves and other natural underground settings, the radius is limited to 300 feet. The spell doesn’t function where nature has been replaced by construction, such as in dungeons and towns.\n\nYou instantly gain knowledge of up to three facts of your choice about any of the following subjects as they relate to the area:\n\n• terrain and bodies of water\n\n• prevalent plants, minerals, animals, or peoples\n\n• powerful celestials, fey, fiends, elementals, or undead\n\n• influence from other planes of existence\n\n• buildings\n\nFor example, you could determine the location of powerful undead in the area, the location of major sources of safe drinking water, and the location of any nearby towns.", - "duration": "Intantaneous", - "level": 5, - "range": "Self", - "school": "Divination", - "ritual": true, - "name": "Commune with Nature", - "components": { - "verbal": true, - "somatic": true, - "concentration": false - } - }, - { - "castingTime": "action", - "description": "For the duration, you understand the literal meaning of any spoken language that you hear. You also understand any written language that you see, but you must be touching the surface on which the words are written. It takes about 1 minute to read one page of text.\n\nThis spell doesn’t decode secret messages in a text or a glyph, such as an arcane sigil, that isn’t part of a written language.", - "duration": "1 hour", - "level": 1, - "range": "Self", - "school": "Divination", - "ritual": true, - "name": "Comprehend Languages", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "a pinch of soot and salt" - } - }, - { - "castingTime": "action", - "description": "A blast of cold air erupts from your hands. Each creature in a 60-foot cone must make a DC {DC} Constitution saving throw. A creature takes 8d8 cold damage on a failed save, or half as much damage on a successful one. A creature killed by this spell becomes a frozen statue until it thaws. ***At Higher Levels.*** When you cast this spell using a spell slot of 6th level or higher, the damage increases by 1d8 for each slot level above 5th.", - "duration": "Instantaneous", - "level": 5, - "range": "Self (60-foot cone)", - "school": "Evocation", - "ritual": false, - "name": "Cone of Cold", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "a small crystal or glass cone" - } - }, - { - "castingTime": "action", - "description": "This spell assaults and twists creatures’ minds, spawning delusions and provoking uncontrolled action. Each creature in a 10-foot-radius sphere centered on a point you choose within range must succeed on a DC {DC} Wisdom saving throw when you cast this spell or be affected by it.\n\nAn affected target can’t take reactions and must roll a d10 at the start of each of its turns to determine its behavior for that turn.\n\nBehavior for each d10 Roll\n\n• 1 -> The creature uses all its movement to move in a random direction. To determine the direction, roll a d8 and assign a direction to each die face. The creature doesn’t take an action this turn.\n\n• 2-6 -> The creature doesn’t move or take actions this turn.\n\n• 7-8 The creature uses its action to make a melee attack against a randomly determined creature within its reach. If there is no creature within its reach, the creature does nothing this turn. • 9-10 -> The creature can act and move normally.\n\nAt the end of each of its turns, an affected target can make a DC {DC} Wisdom saving throw. If it succeeds, this effect ends for that target. \n\n***At Higher Levels.*** When you cast this spell using a spell slot of 5th level or higher, the radius of the sphere increases by 5 feet for each slot level above 4th.", - "duration": "Concentration, up to 1 minute", - "level": 4, - "range": "90 feet", - "school": "Enchantment", - "ritual": false, - "name": "Confusion", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "three nut shells" - } - }, - { - "castingTime": "action", - "description": "You summon fey spirits that take the form of beasts and appear in unoccupied spaces that you can see within range. Choose one of the following options for what appears:\n\n• One beast of challenge rating 2 or lower Two beasts of challenge rating 1 or lower\n\n• Four beasts of challenge rating 1/2 or lower\n\n• Eight beasts of challenge rating 1/4 or lower\n\nEach beast is also considered fey, and it disappears when it drops to 0 hit points or when the spell ends.\n\nThe summoned creatures are friendly to you and your companions. Roll initiative for the summoned creatures as a group, which has its own turns. They obey any verbal commands that you issue to them (no action required by you). If you don’t issue any commands to them, they defend themselves from hostile creatures, but otherwise take no actions.\n\nThe DM has the creatures’ statistics.\n\n***At Higher Levels.*** When you cast this spell using certain higher-level spell slots, you choose one of the summoning options above, and more creatures appear: twice as many with a 5th-level slot, three times as many with a 7th-level slot. and four times as many with a 9th level slot.", - "duration": "Concentration, up to 1 hour", - "level": 3, - "range": "60 feet", - "school": "Conjuration", - "ritual": false, - "name": "Conjure Animals", - "components": { - "verbal": true, - "somatic": true, - "concentration": true - } - }, - { - "castingTime": "1 minute", - "description": "You summon a celestial of challenge rating 4 or lower, which appears in an unoccupied space that you can see within range. The celestial disappears when it drops to 0 hit points or when the spell ends.\n\nThe celestial is friendly to you and your companions for the duration. Roll initiative for the celestial, which has its own turns. It obeys any verbal commands that you issue to it (no action required by you), as long as they don’t violate its alignment. If you don’t issue any commands to the celestial, it defends itself from hostile creatures but otherwise takes no actions.\n\nThe DM has the celestial’s statistics.\n\n***At Higher Levels.*** When you cast this spell using a 9th-level spell slot, you summon a celestial of challenge rating 5 or lower.", - "duration": "Concentration, up to 1 hour", - "level": 7, - "range": "90 feet", - "school": "Conjuration", - "ritual": false, - "name": "Conjure Celestial", - "components": { - "verbal": true, - "somatic": true, - "concentration": true - } - }, - { - "castingTime": "1 minute", - "description": "You call forth an elemental servant. Choose an area of air, earth, fire, or water that fills a 10-foot cube within range. An elemental of challenge rating 5 or lower appropriate to the area you chose appears in an unoccupied space within 10 feet of it. For example, a fire elemental emerges from a bonfire, and an earth elemental rises up from the ground. The elemental disappears when it drops to 0 hit points or when the spell ends. \n\nThe elemental is friendly to you and your companions for the duration. Roll initiative for the elemental, which has its own turns. It obeys any verbal commands that you issue to it (no action required by you). If you don’t issue any commands to the elemental, it defends itself from hostile creatures but otherwise takes no actions.\n\nIf your concentration is broken, the elemental doesn’t disappear. Instead, you lose control of the elemental, it becomes hostile toward you and your companions, and it might attack. An uncontrolled elemental can’t be dismissed by you, and it disappears 1 hour after you summoned it.\n\nThe DM has the elemental’s statistics. \n\n***At Higher Levels.*** When you cast this spell using a spell slot of 6th level or higher, the challenge rating increases by I for each slot level above 5th.", - "duration": "Concentration, up to 1 hour", - "level": 5, - "range": "90 feet", - "school": "Conjuration", - "ritual": false, - "name": "Conjure Elemental", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "burning incense for air, soft day for earth, sulfur and phosphorus for fire, or water and sand for water" - } - }, - { - "castingTime": "1 minute", - "description": "You summon a fey creature of challenge rating 6 or lower, or a fey spirit that takes the form of a beast of challenge rating 6 or lower. It appears in an unoccupied space that you can see within range, The fey creature disappears when it drops to O hit points or when the spell ends,\n\nThe fey creature is friendly to you and your companions for the duration. Roll initiative for the creature, which has its own turns. It obeys any verbal commands that you issue to it (no action required by you), as long as they don’t violate its alignment. If you don’t issue any commands to the fey creature, it defends itself from hostile creatures but otherwise takes no actions.\n\nIf your concentration is broken, the fey creature doesn’t disappear. Instead, you lose control of the fey creature, it becomes hostile toward you and your companions, and it might attack. An uncontrolled fey creature can’t be dismissed by you. and it disappears 1 hour after you summoned it. \n\nThe DM has the fey creature’s statistics.\n\n***At Higher Levels.*** When you cast this spell using a spell slot of 7th level or higher, the challenge rating increases by 1 for each slot level above 6th.", - "duration": "Concentration, up to 1 hour", - "level": 6, - "range": "90 feet", - "school": "Conjuration", - "ritual": false, - "name": "Conjure Fey", - "components": { - "verbal": true, - "somatic": true, - "concentration": true - } - }, - { - "castingTime": "1 minute", - "description": "You summon elementals that appear in unoccupied spaces that you can see within range. You choose one the following options for what appears: \n\n• One elemental of challenge rating 2 or lower\n\n• Two elementals of challenge rating 1 or lower\n\n• Four elementals of challenge rating 1/2 or lower\n\n• Eight elementals of challenge rating 1/4 or lower.\n\nAn elemental summoned by this spell disappears when it drops to 0 hit points or when the spell ends.\n\nThe summoned creatures are friendly to you and your companions. Roll initiative for the summoned creatures as a group, which has its own turns. They obey any verbal commands that you issue to them (no action required by you). If you don’t issue any commands to them, they defend themselves from hostile creatures, but otherwise take no actions.\n\nThe DM has the creatures’ statistics.\n\n***At Higher Levels.*** When you cast this spell using certain higher.level spell slots, you choose one of the summoning options above, and more creatures appear: twice as many with a 6th-level slot and three times as many with an 8th-level slot.", - "duration": "Concentration, up to 1 hour", - "level": 4, - "range": "90 feet", - "school": "Conjuration", - "ritual": false, - "name": "Conjure Minor Elementals", - "components": { - "verbal": true, - "somatic": true, - "concentration": true - } - }, - { - "castingTime": "action", - "description": "V, S, M (one holly berry per creature summoned)", - "duration": "Concentration, up to 1 hour", - "level": 4, - "range": "60 feet", - "school": "Conjuration", - "ritual": false, - "name": "Conjure Woodland Beings", - "components": { - "verbal": false, - "somatic": false, - "concentration": true, - "material": "no action required by you" - } - }, - { - "castingTime": "1 minute", - "description": "You mentally contact a demigod, the spirit of a long dead sage, or some other mysterious entity from another plane. Contacting this extraplanar intelligence can strain or even break your mind. When you cast this spell, make a DC 15 Intelligence saving throw. On a failure, you take 6d6 psychic damage and are insane until you finish a long rest. While insane, you can’t take actions, can’t understand what other creatures say, can’t read, and speak only in gibberish, A greater restoration spell cast on you ends this effect.\n\nOn a successful save, you can ask the entity up to five questions. You must ask your questions before the spell ends. The DM answers each question with one word, such as “yes”, ”no”, ”maybe”, ”never”, ”irrelevant”, or ”unclear” (if the entity doesn’t know the answer to the question), If a one-word answer would be misleading, the DM might instead offer a short phrase as an answer.", - "duration": "1 minute", - "level": 5, - "range": "Self", - "school": "Divination", - "ritual": true, - "name": "Contact Other Plane", - "components": { - "verbal": true, - "somatic": false, - "concentration": false - } - }, - { - "castingTime": "action", - "description": "Your touch inflicts disease. Make a melee spell attack against a creature within your reach. On a hit, you afflict the creature with a disease of your choice from any of the ones described below.\n\nAt the end of each of the target’s turns, it must make a DC {DC} Constitution saving throw. After failing three of these saving throws, the disease’s effects last for the duration, and the creature stops making these saves. After succeeding on three of these saving throws, the creature recovers from the disease, and the spell ends.\n\nSince this spell induces a natural disease in its target, any effect that removes a disease or otherwise ameliorates a disease’s effects apply to it.\n\nBlinding Sickness. Pain grips the creature’s mind, and its eyes turn milky white. The creature has disadvantage on Wisdom checks and Wisdom saving throws and is blinded.\n\nFilth Fever. A raging fever sweeps through the creature’s body. The creature has disadvantage on Strength checks, Strength saving throws. and attack rolls that use Strength.\n\nFlesh Rot. The creature’s flesh decays. The creature has disadvantage on Charisma checks and vulnerability to all damage.\n\nMindfire. The creature’s mind becomes feverish. The creature has disadvantage on Intelligence checks and Intelligence saving throws, and the creature behaves as if under the effects of the confusion spell during combat.\n\nSeizure. The creature is overcome with shaking. The creature has disadvantage on Dexterity checks, DC {DC} Dexterity saving throws, and attack rolls that use Dexterity.\n\nSlimy Doom. The creature begins to bleed uncontrollably. The creature has disadvantage on Constitution checks and Constitution saving throws. In addition, whenever the creature takes damage, it is stunned until the end of its next turn.", - "duration": "7 days", - "level": 5, - "range": "Touch", - "school": "Necromancy", - "ritual": false, - "name": "Contagion", - "components": { - "verbal": true, - "somatic": true, - "concentration": false - } - }, - { - "castingTime": "10 minutes", - "description": "Choose a spell of 5th level or lower that you can cast, that has a casting time of 1 action, and that can target YOU. You cast that spell-called the contingent spell-as part of casting contingency, expending spell slots for both, but the contingent spell doesn’t come into effect. Instead, it takes effect when a certain circumstance occurs. You describe that circumstance when you cast the two spells. For example, a contingency cast with water breathing might stipulate that water breathing comes into effect when you are engulfed in water or a similar liquid.\n\nThe contingent spell takes effect immediately after the circumstance is met for the first time, whether or not you want it to, and then contingency ends.\n\nThe contingent spell takes effect only on you, even if it can normally target others. You can use only one contingency spell at a time. If you cast this spell again, the effect of another contingency spell on you ends. Also, contingency ends on you if its material component is ever not on your person.", - "duration": "10 days", - "level": 6, - "range": "Self", - "school": "Evocation", - "ritual": false, - "name": "Contingency", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "a statuetle of yourself carved from ivory and decorated with gems worth at least 1,500 gp" - } - }, - { - "castingTime": "action", - "description": "A flame, equivalent in brightness to a torch, springs forth from an object that you touch. The effect looks like a regular flame, but it creates no heat and doesn’t use oxygen. A continual flame can be covered or hidden but not smothered or quenched.", - "duration": "Until Dispelled", - "level": 2, - "range": "Touch", - "school": "Evocation", - "ritual": false, - "name": "Continual Flame", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "ruby dust worth 50 gp, which the spell consumes" - } - }, - { - "castingTime": "action", - "description": "Until the spell ends, you control any freestanding water inside an area you choose that is a cube up to 100 feet on a side. You can choose from any of the following effects when you cast this spell. As an action on your turn, you can repeat the same effect or choosr a different one.\n\nFlood. You cause the water level of all standing water in the area to rise by as much as 20 feet. If the area includes a shore, the flooding water spills over onto dry land.\n\nIf you choose an area in a large body of water, you instead create a 20-foot tall wave that travels from one side of the area to the other and then crashes down. Any Huge or smaller vehicles in the wave’s path are carried with it to the other side. Any Huge or smaller vehicles struck by the wave have a 25 percent chance of capsizing.\n\nThe water level remains elevated until the spell ends or you choose a different effect. If this effect produced a wave, the wave repeats on the start of your next turn while the flood effect lasts.\n\nPart Water. You cause water in the area to move apart and create a trench. The trench extends across the spell’s area, and the separated water forms a wall to either side. The trench remains until the spell ends or you choose a different effect. The water then slowly fills in the trench over the course of the next round until the normal water level is restored.\n\nRedirect Flow. You cause flowing water in the area to move in a direction you choose, even if the water has to flow over obstacles, up walls, or in other unlikely directions. The water in the area moves as you direct it, but once it moves beyond the spell’s area, it resumes its flow based on the terrain conditions. The water continues to move in the direction you chose until the spell ends or you choose a different effect.\n\nWhirlpool. This effect requires a body of water at least 50 feet square and 25 feet deep. You cause a whirlpool to form in the center of the area. The whirlpool forms a vortex that is 5 feet wide at the base, up to 50 feet wide at the top, and 25 feet tall. Any creature or object in the water and within 25 feet of the vortex is pulled 10 feet toward it. A creature can swim away from the vortex by making a Strength (Athletics) check against your spell save DC.\n\nWhen a creature enters the vortex for the first time on a turn or starts its turn there, it must make a DC {DC} Strength saving throw. On a failed save, the creature takes 2d8 bludgeoning damage and is caught in the vortex until the spell ends. On a successful save, the creature takes half damage, and isn’t caught in the vortex. A creature caught in the vortex can use its action to try to swim away from the vortex as described above, but has disadvantage on the Strength (Athletics) check to do so.\n\nThe first time each turn that an object enters the vortex, the object takes 2d8 bludgeoning damage; this damage occurs each round it remains in the vortex.", - "duration": "Concentration, up to 8 hours", - "level": 4, - "range": "300 feet", - "school": "Transmutation", - "ritual": false, - "name": "Control Water", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "a drop of water and a pinch of dust" - } - }, - { - "castingTime": "10 minutes", - "description": "You take control of the weather within 5 miles of you for the duration. You must be outdoors to cast this spell. Moving to a place where you don’t have a clear path to the sky ends the spell early.\n\nWhen you cast the spell, you change the current weather conditions, which are determined by the DM based on the climate and season. You can change precipitation, temperature, and wind. It takes 1d4 × 10 minutes for the new conditions to take effect. Once they do so, you can change the conditions again. When the spell ends, the weather gradually returns to normal.\n\nWhen you change the weather conditions, find a current condition on the following tables and change its stage by one, up or down. When changing the wind, you can change its direction.\n\n- Precipitation - \n\n1 - Clear\n\n2 - Light Clouds\n\n3 - Overcast or ground fog\n\n4 - Rain, hail, or snow\n\n5 - Torrential rain, driving hail, or blizzard\n\n- Temperature - \n\n1 - Unbearable Heat\n\n2 - Hot\n\n3 - Warm\n\n4 - Cool\n\n5 - Cold\n\n6 - Arctic Cold\n\n- Wind - \n\n1 - Calm\n\n2 - Moderate Wind\n\n3 - Strong Wind\n\n4 - Gale\n\n5 - Storm", - "duration": "Concentration, up to 8 hours", - "level": 8, - "range": "Self (5-mile radius)", - "school": "Transmutation", - "ritual": false, - "name": "Control Weather", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "burning incense and bits of earth and wood mixed in water" - } - }, - { - "castingTime": "action", - "description": "You create 45 pounds of food and 30 gallons of water on the ground or in containers within range, enough to sustain up to fifteen humanoids or five steeds for 24 hours. The food is bland but nourishing, and spoils if uneaten after 24 hours. The water is clean and doesn’t go bad.", - "duration": "Instantaneous", - "level": 3, - "range": "30 feet", - "school": "Conjuration", - "ritual": false, - "name": "Create Food and Water", - "components": { - "verbal": true, - "somatic": true, - "concentration": false - } - }, - { - "castingTime": "action", - "description": "You either create or destroy water.\n\nCreate Water. You create up to 10 gallons of clean water within range in an open container. Alternatively, the water falls as rain in a 30-foot cube within range, extinguishing exposed flames in the area. \n\nDestroy Water. You destroy up to 10 gallons of water in an open container within range. Alternatively, you destroy fog in a 30-foot cube within range. \n\n***At Higher Levels.*** When you cast this spell using a spell slot of 2nd level or higher, you create or destroy 10 additional gallons of water, or the size of the cube increases by 5 feet, for each slot level above 1st.", - "duration": "Instantaneous", - "level": 1, - "range": "30 feet", - "school": "Transmutation", - "ritual": false, - "name": "Create or Destroy Water", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "a drop of water if creating water or a few grains of sand if destroying it" - } - }, - { - "castingTime": "1 minute", - "description": "You can cast this spell only at night. Choose up to three corpses of Medium or Small humanoids within range. Each corpse becomes a ghoul under your control. (The DM has game statistics for these creatures.)\n\nAs a bonus action on each of your turns, you can mentally command any creature you animated with this spell if the creature is within 120 feet of you (if you control multiple creatures, you can command any or all of them at the same time, issuing the same command to each one). You decide what action the creature will take and where it will move during its next turn, or you can issue a general command, such as to guard a particular chamber or corridor. If you issue no commands, the creature only defends itself against hostile creatures. Once given an order, the creature continues to follow it until its task is complete.\n\nThe creature is under your control for 24 hours, after which it stops obeying any command you have given it. To maintain control of the creature for another 24 hours, you must cast this spell on the creature before the current 24-hour period ends. This use of the spell reasserts your control over up to three creatures you have animated with this spell, rather than animating new ones.\n\n***At Higher Levels.*** When you cast this spell using a 7th-level spell slot, you can animate or reassert control over four ghouls. When you cast this spell using an 8th-level spell slot, you can animate or reassert control over five ghouls or two ghasts or wights. When you cast this spell using a 9th-level spell slot, you can animate or reassert control over six ghouls, three ghasts or wights, or two mummies.", - "duration": "Instantaneous", - "level": 6, - "range": "10 feet", - "school": "Necromancy", - "ritual": false, - "name": "Create Undead", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "one clay pot filled with grave dirt, one clay pot tilled with brackish water, and one 150 gp black onyx stone for each corpse" - } - }, - { - "castingTime": "1 minute", - "description": "You pull wisps of shadow material from the Shadowfell to create a nonliving object of vegetable matter within range: soft goods, rope, wood, or something similar. You can also use this spell to create mineral objects such as stone, crystal, or metal. The object created must be no larger than a 5-foot cube, and the object must be of a form and material that you have seen before.\n\nThe duration depends on the object’s material. If the object is composed of multiple materials, use the shortest duration.\n\n- Materials & Durations - \n\nVegetable Matter - 1 day\n\nStone or Crystal - 12 hours\n\nPrecious Metals - 1 hour\n\nGems - 10 minutes\n\nAdamantine or mithral - 1 minute\n\nUsing any material created by this spell as another spell’s material component causes that spell to fail.\n\n***At Higher Levels.*** When you cast this spell using a spell slot of 6th level or higher, the cube increases by 5 feet for each slot level above 5th.", - "duration": "Special", - "level": 5, - "range": "30 feet", - "school": "Illusion", - "ritual": false, - "name": "Creation", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "a tiny piece of matter of the same type of the item you plan to create" - } - }, - { - "castingTime": "action", - "description": "A creature you touch regains a number of hit points equal to 1d8 + your spellcasting ability modifier. This spell has no effect on undead or constructs.\n\n***At Higher Levels.*** When you cast this spell using a spell slot of 2nd level or higher, the healing increases by 1d8 for each slot level above 1st.", - "duration": "Instantaneous", - "level": 1, - "range": "Touch", - "school": "Evocation", - "ritual": false, - "name": "Cure Wounds", - "components": { - "verbal": true, - "somatic": true, - "concentration": false - } - }, - { - "castingTime": "action", - "description": "You create up to four torch-sized lights within range, making them appear as torches, lanterns, or glowing orbs that hover in the air for the duration. You can also combine the four lights into one glowing vaguely humanoid form of Medium size. Whichever form you choose, each light sheds dim light in a 10-foot radius.\n\nAs a bonus action on your turn, you can move the lights up to 60 feet to a new spot within range. A light must be within 20 feet of another light created by this spell, and a light winks out if it exceeds the spell’s range.", - "duration": "Concentration, up to 1 minute", - "level": 0, - "range": "120 feet", - "school": "Evocation", - "ritual": false, - "name": "Dancing Lights", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "a bit of phosphorus or wychwood, or a glowworm" - } - }, - { - "castingTime": "action", - "description": "Magical darkness spreads from a point you choose within range to fill a 15-foot-radius sphere for the duration. The darkness spreads around corners. A creature with darkvision can’t see through this darkness, and nonmagical light can’t illuminate it.\n\nIf the point you choose is on an object you are holding or one that isn’t being worn or carried, the darkness emanates from the object and moves with it. Completely covering the source of the darkness with an opaque object, such as a bowl or a helm, blocks the darkness.\n\nIf any of this spell’s area overlaps with an area of light created by a spell of 2nd level or lower, the spell that created the light is dispelled.", - "duration": "Concentration, up to 10 minutes", - "level": 2, - "range": "60 feet", - "school": "Evocation", - "ritual": false, - "name": "Darkness", - "components": { - "verbal": true, - "somatic": false, - "concentration": true, - "material": "bat fur and a drop of pitch or piece of coal" - } - }, - { - "castingTime": "action", - "description": "You touch a willing creature to grant it the ability to see in the dark. For the duration, that creature has darkvision out to a range of 60 feet.", - "duration": "8 hours", - "level": 2, - "range": "Touch", - "school": "Transmutation", - "ritual": false, - "name": "Darkvision", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "either a pinch of dried carrot or an agate" - } - }, - { - "castingTime": "action", - "description": "A 60-foot-radius sphere of light spreads out from a point you choose within range. The sphere is bright light and sheds dim light for an additional 60 feet.\n\nIf you chose a point on an object you are holding or one that isn’t being worn or carried, the light shines from the object and moves with it. Completely covering the affected object with an opaque object, such as a bowl or a helm, blocks the light.\n\nIf any of this spell’s area overlaps with an area of darkness created by a spell of 3rd level or lower, the spell that created the darkness is dispelled.", - "duration": "8 hours", - "level": 3, - "range": "Touch", - "school": "Abjuration", - "ritual": false, - "name": "Daylight", - "components": { - "verbal": true, - "somatic": true, - "concentration": false - } - }, - { - "castingTime": "action", - "description": "You touch a creature and grant it a measure of protection from death.\n\nThe first time the target would drop to 0 hit points as a result of taking damage, the target instead drops to 1 hit point, and the spell ends.\n\nIf the spell is still in effect when the target is subjected to an effect that would kill it instantaneously without dealing damage, that effect is instead negated against the target, and the spell ends.", - "duration": "8 hours", - "level": 4, - "range": "Touch", - "school": "Abjuration", - "ritual": false, - "name": "Death Ward", - "components": { - "verbal": true, - "somatic": true, - "concentration": false - } - }, - { - "castingTime": "action", - "description": "A beam of yellow light flashes from your pointing finger, then condenses to linger at a chosen point within range as a glowing bead for the duration. When the spell ends, either because your concentration is broken or because you decide to end it, the bead blossoms with a low roar into an explosion of flame that spreads around corners. Each creature in a 20-foot-radius sphere centered on that point must make a DC {DC} Dexterity saving throw. A creature takes fire damage equal to the total accumulated damage on a failed save, or half as much damage on a successful one.\n\nThe spell’s base damage is 12d6. If at the end of your turn the bead has not yet detonated, the damage increases by 1d6.\n\nIf the glowing bead is touched before the interval has expired, the creature touching it must make a DC {DC} Dexterity saving throw. On a failed save, the spell ends immediately, causing the bead to erupt in flame. On a successful save, the creature can throw the bead up to 40 feet. When it strikes a creature or a solid object, the spell ends, and the bead explodes.\n\nThe fire damages objects in the area and ignites flammable objects that aren’t being worn or carried.\n\n***At Higher Levels.*** When you cast this spell using a spell slot of 8th level or higher, the base damage increases by 1d6 for each slot level above 7th.", - "duration": "Concentration, up to 1 minute", - "level": 7, - "range": "150 feet", - "school": "Evocation", - "ritual": false, - "name": "Delayed Blast Fireball", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "a tiny ball of bat guano and sulfur" - } - }, - { - "castingTime": "action", - "description": "You create a shadowy door on a fIat solid surface that you can see within range. The door is large enough to allow Medium creatures to pass through unhindered. When opened, the door leads to a demiplane that appears to be an empty room 30 feet in each dimension, made of wood or stone. When the spell ends, the door disappears, and any creatures or objects inside the demiplane remain trapped there, as the door also disappears from the other side.\n\nEach time you cast this spell, you can create a new demiplane, or have the shadowy door connect to a demiplane you created with a previous casting of this spell. Additionally, if you know the nature and contents of a demiplane created by a casting of this spell by another creature, you can have the shadowy door connect to its demiplane instead.", - "duration": "1 hour", - "level": 8, - "range": "60 feet", - "school": "Conjuration", - "ritual": false, - "name": "Demiplane", - "components": { - "verbal": false, - "somatic": true, - "concentration": false - } - }, - { - "castingTime": "action", - "description": "For the duration, you know if there is an aberration, celestial, elemental, fey, fiend, or undead within 30 feet of you, as well as where the creature is located. Similarly, you know if there is a place or object within 30 feet of you that has been magically consecrated or desecrated.\n\nThe spell can penetrate most barriers, but it is blocked by 1 foot of stone, 1 inch of common metal, a thin sheet of lead, or 3 feet of wood or dirt.", - "duration": "Concentration, up to 10 minutes", - "level": 1, - "range": "Self", - "school": "Divination", - "ritual": false, - "name": "Detect Evil and Good", - "components": { - "verbal": true, - "somatic": true, - "concentration": true - } - }, - { - "castingTime": "action", - "description": "For the duration, you sense the presence of magic within 30 feet of you. If you sense magic in this way, you can use your action to see a faint aura around any visible creature or object in the area that bears magic, and you learn its school of magic, if any. The spell can penetrate most barriers, but it is blocked by 1 foot of stone, 1 inch of common metal, a thin sheet of lead, or 3 feet of wood or dirt.", - "duration": "Concentration, up to 10 minutes", - "level": 1, - "range": "Self", - "school": "Divination", - "ritual": true, - "name": "Detect Magic", - "components": { - "verbal": true, - "somatic": true, - "concentration": true - } - }, - { - "castingTime": "action", - "description": "For the duration, you can sense the presence and location of poisons, poisonous creatures, and diseases within 30 feet of you. You also identify the kind of poison, poisonous creature, or disease in each case.\n\nThe spell can penetrate most barriers, but it is blocked by 1 foot of stone, 1 inch of common metal, a thin sheet of lead, or 3 feet of wood or dirt.", - "duration": "Concentration, up to 10 minutes", - "level": 1, - "range": "Self", - "school": "Divination", - "ritual": true, - "name": "Detect Poison and Disease", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "a yew leaf" - } - }, - { - "castingTime": "action", - "description": "For the duration, you can read the thoughts of certain creatures, When you cast the spell and as your action on each turn until the spell ends, you can focus your mind on any one creature that you can see within 30 feet of you, If the creature you choose has an Intelligence of 3 or lower or doesn’t speak any language, the creature is unaffected. \n\nYou initially learn the surface thoughts of the creature-what is most on its mind in that moment. As an action, you can either shift your attention to another creature’s thoughts or attempt to probe deeper into the same creature’s mind. If you probe deeper, the target must make a DC {DC} Wisdom saving throw. If it fails, you gain insight into its reasoning (if any), its emotional state, and something that looms large in its mind (such as something it worries over, loves, or hates). If it succeeds, the spell ends. Either way, the target knows that you are probing into its mind, and unless you shift your attention to another creature’s thoughts, the creature can use its action on its turn to make an Intelligence check contested by your Intelligence check; if it succeeds, the spell ends.\n\nQuestions verbally directed at the target creature naturally shape the course of its thoughts, so this spell is particularly effective as part of an interrogation.\n\nYou can also use this spell to detect the presence of thinking creatures you can’t see. When you cast the spell or as your action during the duration, you can search for thoughts within 30 feet of you. The spell can penetrate barriers, but 2 feet of rock, 2 inches of any metal other than lead, or a thin sheet of lead blocks you. You can’t detect a creature with an Intelligence of 3 or lower or one that doesn’t speak any language.\n\nOnce you detect the presence of a creature in this way, you can read its thoughts for the rest of the duration as described above, even if you can’t see it, but it must still be within range.", - "duration": "Concentration, up to 1 minute ", - "level": 2, - "range": "Self", - "school": "Divination", - "ritual": false, - "name": "Detect Thoughts", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "a copper piece" - } - }, - { - "castingTime": "action", - "description": "You teleport yourself from your current location to any other spot within range. You arrive at exactly the spot desired. It can be a place you can see, one you can visualize, or one you can describe by stating distance and direction, such as “200 feet straight downward” or “upward to the northwest at a 45-degree angle, 300 feet.”\n\nYou can bring along objects as long as their weight doesn’t exceed what you can carry. You can also bring one willing creature of your size or smaller who is carrying gear up to its carrying capacity. The creature must be within 5 feet of you when you cast this spell.\n\nIf you would arrive in a place already occupied by an object or a creature, you and any creature traveling with you each take 4d6 force damage, and the spell fails to teleport you.", - "duration": "Instantaneous", - "level": 4, - "range": "500 feet", - "school": "Conjuration", - "ritual": false, - "name": "Dimension Door", - "components": { - "verbal": true, - "somatic": false, - "concentration": false - } - }, - { - "castingTime": "action", - "description": "You make yourself—including your clothing, armor, weapons, and other belongings on your person—look different until the spell ends or until you use your action to dismiss it. You can seem 1 foot shorter or taller and can appear thin, fat, or in between. You can’t change your body type, so you must adopt a form that has the same basic arrangement of limbs. Otherwise, the extent of the illusion is up to you.\n\nThe changes wrought by this spell fail to hold up to physical inspection. For example, if you use this spell to add a hat to your outfit, objects pass through the hat, and anyone who touches it would feel nothing or would feel your head and hair. If you use this spell to appear thinner than you are, the hand of someone who reaches out to touch you would bump into you while it was seemingly still in midair.\n\nTo discern that you are disguised, a creature can use its action to inspect your appearance and must succeed on an Intelligence (Investigation) check against your spell save DC.", - "duration": "1 hour", - "level": 1, - "range": "Self", - "school": "Illusion", - "ritual": false, - "name": "Disguise Self", - "components": { - "verbal": true, - "somatic": true, - "concentration": false - } - }, - { - "castingTime": "action", - "description": "A thin green ray springs from your pointing finger to a target that you can see within range. The target can be a creature, an object, or a creation of magical force, such as the wall created by wall of force.\n\nA creature targeted by this spell must make a DC {DC} Dexterity saving throw. On a failed save, the target takes 10d6 + 40 force damage. If this damage reduces the target to 0 hit points, it is disintegrated.\n\nA disintegrated creature and everything it is wearing and carrying, except magic items, are reduced to a pile of fine gray dust. The creature can be restored to life only by means of a true resurrection or a wish spell.\n\nThis spell automatically disintegrates a Large or smaller nonmagical object or a creation of magical force. If the target is a Huge or larger object or creation of force, this spell disintegrates a 10-foot-cube portion of it. A magic item is unaffected by this spell.\n\n***At Higher Levels.*** When you cast this spell using a spell slot of 7th level or higher, the damage increases by 3d6 for each slot level above 6th.", - "duration": "Instantaneous", - "level": 6, - "range": "60 feet", - "school": "Transmutation", - "ritual": false, - "name": "Disintegrate", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "a lodestone and a pinch of dust" - } - }, - { - "castingTime": "action", - "description": "Shimmering energy surrounds and protects you from fey, undead, and creatures originating from beyond the Material Plane. For the duration, celestials, elementals, fey, fiends, and undead have disadvantage on attack rolls against you.\n\nYou can end the spell early by using either of the following special functions.\n\nBreak Enchantment. As your action, you touch a creature you can reach that is charmed, frightened, or possessed by a celestial, an elemental, a fey, a fiend, or an undead. The creature you touch is no longer charmed, frightened, or possessed by such creatures.\n\nDismissal. As your action, make a melee spell attack against a celestial, an elemental, a fey, a fiend, or an undead you can reach. On a hit, you attempt to drive the creature back to its home plane. The creature must succeed on a DC {DC} Charisma saving throw or be sent back to its home plane (if it isn’t there already). If they aren’t on their home plane, undead are sent to the Shadowfell, and fey are sent to the Feywild.", - "duration": "Concentration, up to 1 minute", - "level": 5, - "range": "Self", - "school": "Abjuration", - "ritual": false, - "name": "Dispel Evil and Good", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "holy water or powdered silver and iron" - } - }, - { - "castingTime": "action", - "description": "Choose one creature, object, or magical effect within range. Any spell of 3rd level or lower on the target ends. For each spell of 4th level or higher on the target, make an ability check using your spellcasting ability. The DC equals 10 + the spell’s level. On a successful check, the spell ends. ***At Higher Levels.*** When you cast this spell using a spell slot of 4th level or higher, you automatically end the effects of a spell on the target if the spell’s level is equal to or less than the level of the spell slot you used.", - "duration": "Instantaneous", - "level": 3, - "range": "120 feet", - "school": "Abjuration", - "ritual": false, - "name": "Dispel Magic", - "components": { - "verbal": true, - "somatic": true, - "concentration": false - } - }, - { - "castingTime": "action", - "description": "Your magic and an offering put you in contact with a god or a god’s servants. You ask a single question concerning a specific goal, event, or activity to occur within 7 days. The DM offers a truthful reply. The reply might be a short phrase, a cryptic rhyme, or an omen. The spell doesn’t take into account any possible circumstances that might change the outcome, such as the casting of additional spells or the loss or gain of a companion. If you cast the spell two or more times before finishing your next long rest, there is a cumulative 25 percent chance for each casting after the first that you get a random reading. The DM makes this roll in secret.", - "duration": "Instantaneous", - "level": 4, - "range": "Self", - "school": "Divination", - "ritual": false, - "name": "Divination", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "incense and a sacrificial offering appropriate to your religion, together worth at least 25 gp, which the spell consumes" - } - }, - { - "castingTime": "bonus action", - "description": "Your prayer empowers you with divine radiance. Until the spell ends, your weapon attacks deal an extra 1d4 radiant damage on a hit.", - "duration": "Concentration, up to 1 minute", - "level": 1, - "range": "Self", - "school": "Evocation", - "ritual": false, - "name": "Divine Favor", - "components": { - "verbal": true, - "somatic": true, - "concentration": true - } - }, - { - "castingTime": "bonus action", - "description": "You utter a divine word, imbued with the power that shaped the world at the dawn of creation. Choose any number of creatures you can see within range. Each creature that can hear you must make a DC {DC} Charisma saving throw. On a failed save, a creature suffers an effect based on its current hit points:\n\n• 50 hit points or fewer: deafened for 1 minute\n\n• 40 hit points or fewer: deafened and blinded for 10 minutes\n\n• 30 hit points or fewer: blinded, deafened, and stunned for 1 hour\n\n• 20 hit points or fewer: killed instantly\n\nRegardless of its current hit points, a celestial, an elemental, a fey. or a fiend that fails its save is forced back to its plane of origin (if it isn’t there already) and can’t return to your current plane for 24 hours by any means short of a wish spell.", - "duration": "Instantaneous", - "level": 7, - "range": "30 feet", - "school": "Evocation", - "ritual": false, - "name": "Divine Word", - "components": { - "verbal": true, - "somatic": false, - "concentration": false - } - }, - { - "castingTime": "action", - "description": "You attempt to beguile a beast that you can see within range. It must succeed on a DC {DC} Wisdom saving throw or be charmed by you for the duration. If you or creatures that are friendly to you are fighting it, it has advantage on the saving throw.\n\nWhile the beast is charmed, you have a telepathic link with it as long as the two of you are on the same plane of existence. You can use this telepathic link to issue commands to the creature while you are conscious (no action required), which it does its best to obey. You can specify a simple and general course of action, such as ”Attack that creature.” ”Run over there,” or ”Fetch that object.” If the creature completes the order and doesn’t receive further direction from you, it defends and preserves itself to the best of its ability.\n\nYou can use your action to take total and precise controI of the target. Until the end of your next turn, the creature takes only the actions you choose, and doesn’t do anything that you don’t allow it to do. During this time, you can also cause the creature to use a reaction, but this requires you to use your own reaction as well.\n\nEach time the target takes damage, it makes a new Wisdom saving throw against the spell. If the saving throw succeeds, the spell ends.\n\n***At Higher Levels.*** When you cast this spell with a 5th-level spell slot, the duration is concentration, up to 10 minutes. When you use a 6th-level spell slot, the duration is concentration, up to 1 hour. When you use a spell slot of 7th level or higher, the duration is concentration, up to 8 hours.", - "duration": "Concentration, up to 1 minute", - "level": 4, - "range": "60 feet", - "school": "Enchantment", - "ritual": false, - "name": "Dominate Beast", - "components": { - "verbal": true, - "somatic": true, - "concentration": true - } - }, - { - "castingTime": "action", - "description": "You attempt to beguile a creature that you can see within range. It must succeed on a DC {DC} Wisdom saving throw or be charmed by you for the duration. If you or creatures that are friendly to you are fighting it, it has advantage on the saving throw.\n\nWhile the creature is charmed, you have a telepathic link with it as long as the two of you are on the same plane of existence. You can use this telepathic link to issue commands to the creature while you are conscious (no action required), which it does its best to obey. You can specify a simple and general course of action, such as “Attack that creature,” “Run over there,” or “Fetch that object.” If the creature completes the order and doesn’t receive further direction from you, it defends and preserves itself to the best of its ability.\n\nYou can use your action to take total and precise control of the target. Until the end of your next turn, the creature takes only the actions you choose, and doesn’t do anything that you don’t allow it to do. During this time, you can also cause the creature to use a reaction, but this requires you to use your own reaction as well.\n\nEach time the target takes damage, it makes a new Wisdom saving throw against the spell. If the saving throw succeeds, the spell ends.\n\n***At Higher Levels.*** When you cast this spell with a 9th-level spell slot, the duration is concentration, up to 8 hours.", - "duration": "Concentration, up to 1 hour", - "level": 8, - "range": "60 feet", - "school": "Enchantment", - "ritual": false, - "name": "Dominate Monster", - "components": { - "verbal": true, - "somatic": true, - "concentration": true - } - }, - { - "castingTime": "action", - "description": "You attempt to beguile a humanoid that you can see within range. It must succeed on a DC {DC} Wisdom saving throw or be charmed by you for the duration. If you or creatures that are friendly to you are fighting it, it has advantage on the saving throw.\n\nWhile the target is charmed, you have a telepathic link with it as long as the two of you are on the same plane of existence. You can use this telepathic link to issue commands to the creature while you are conscious (no action required), which it does its best to obey. You can specify a simple and general course of action, such as “Attack that creature,” “Run over there,” or “Fetch that object.” If the creature completes the order and doesn’t receive further direction from you, it defends and preserves itself to the best of its ability.\n\nYou can use your action to take total and precise control of the target. Until the end of your next turn, the creature takes only the actions you choose, and doesn’t do anything that you don’t allow it to do. During this time you can also cause the creature to use a reaction, but this requires you to use your own reaction as well.\n\nEach time the target takes damage, it makes a new Wisdom saving throw against the spell. If the saving throw succeeds, the spell ends. ***At Higher Levels.*** When you cast this spell using a 6th-level spell slot, the duration is concentration, up to 10 minutes. When you use a 7th-level spell slot, the duration is concentration, up to 1 hour. When you use a spell slot of 8th level or higher, the duration is concentration, up to 8 hours.", - "duration": "Concentration, up to 1 minute", - "level": 5, - "range": "60 feet", - "school": "Enchantment", - "ritual": false, - "name": "Dominate Person", - "components": { - "verbal": true, - "somatic": true, - "concentration": true - } - }, - { - "castingTime": "1 minute", - "description": "You touch an object weighing 10 pounds or less whose longest dimension is 6 feet or less. The spell leaves an invisible mark on its surface and invisibly inscribes the name of the item on the sapphire you use as the material component. Each time you cast this spell, you must use a different sapphire.\n\nAt any time thereafter, you can use your action to speak the item’s name and crush the sapphire. The item instantly appears in your hand regardless of physical or planar distances, and the spell ends.\n\nIf another creature is holding or carrying the item, crushing the sapphire doesn’t transport the item to you, but instead you learn who the creature possessing the object is and roughly where that creature is located at that moment.\n\nDispel magic or a similar effect successfully applied to the sapphire ends this spell’s effect.", - "duration": "Until dispelled", - "level": 6, - "range": "Touch", - "school": "Conjuration", - "ritual": true, - "name": "Instant Summons", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "a sapphire worth 1,000 gp" - } - }, - { - "castingTime": "1 minute", - "description": "This spell shapes a creature’s dreams. Choose a creature known to you as the target of this spell. The target must be on the same plane of existence as you. Creatures that don’t sleep, such as elves, can’t be contacted by this spell. You, or a willing creature you touch, enters a trance state, acting as a messenger. While in the trance, the messenger is aware of his or her surroundings, but can’t take actions or move.\n\nIf the target is asleep, the messenger appears in the target’s dreams and can converse with the target as long as it remains asleep, through the duration of the spell. The messenger can also shape the environment of the dream, creating landscapes, objects, and other images. The messenger can emerge from the trance at any time, ending the effect of the spell early. The target recalls the dream perfectly upon waking. If the target is awake when you cast the spell, the messenger knows it, and can either end the trance (and the spell) or wait for the target to fall asleep, at which point the messenger appears in the target’s dreams.\n\nYou can make the messenger appear monstrous and terrifying to the target. If you do, the messenger can deliver a message of no more than ten words and then the target must make a DC {DC} Wisdom saving throw. On a failed save, echoes of the phantasmal monstrosity spawn a nightmare that lasts the duration of the target’s sleep and prevents the target from gaining any benefit from that rest. In addition, when the target wakes up, it takes 3d6 psychic damage.\n\nIf you have a body part, lock of hair, clipping from a nail, or similar portion of the target’s body, the target makes its saving throw with disadvantage.", - "duration": "8 hours", - "level": 5, - "range": "Special", - "school": "Illusion", - "ritual": false, - "name": "Dream", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "a handful of sand, a dab of ink, and a writing quill plucked from a sleeping bird" - } - }, - { - "castingTime": "action", - "description": "You create a seismic disturbance at a point on the ground that you can see within range. For the duration, an intense tremor rips through the ground in a 100-foot-radius circle centered on that point and shakes creatures and structures in contact with the ground in that area.\n\nThe ground in the area becomes difficult terrain. Each creature on the ground that is concentrating must make a DC {DC} Constitution saving throw. On a failed save, the creature’s concentration is broken.\n\nWhen you cast this spell and at the end of each turn you spend concentrating on it, each creature on the ground in the area must make a DC {DC} Dexterity saving throw. On a failed save, the creature is knocked prone.\n\nThis spell can have additional effects depending on the terrain in the area, as determined by the DM.\n\nFissures. Fissures open throughout the spell’s area at the start of your next turn after you cast the spell. A total of 1d6 such fissures open in locations chosen by the DM. Each is 1d10 × 10 feet deep, 10 feet wide, and extends from one edge of the spell’s area to the opposite side. A creature standing on a spot where a fissure opens must succeed on a DC {DC} Dexterity saving throw or fall in. A creature that successfully saves moves with the fissure’s edge as it opens.\n\nA fissure that opens beneath a structure causes it to automatically collapse (see below). \n\nStructures. The tremor deals 50 bludgeoning damage to any structure in contact with the ground in the area when you cast the spell and at the start of each of your turns until the spell ends. If a structure drops to 0 hit points, it collapses and potentially damages nearby creatures. A creature within half the distance of a structure’s height must make a DC {DC} Dexterity saving throw. On a failed save, the creature takes 5d6 bludgeoning damage, is knocked prone, and is buried in the rubble, requiring a DC 20 Strength (Athletics) check as an action to escape. The DM can adjust the DC higher or lower, depending on the nature of the rubble. On a successful save, the creature takes half as much damage and doesn’t fall prone or become buried.", - "duration": "Concentration, up to 1 minute", - "level": 8, - "range": "500 feet", - "school": "Evocation", - "ritual": false, - "name": "Earthquake", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "a pinch of dirt, a piece of rock, and a lump of clay" - } - }, - { - "castingTime": "action", - "description": "You touch a creature and bestow upon it a magical enhancement. Choose one of the following effects; the target gains that effect until the spell ends.\n\nBear’s Endurance. The target has advantage on Constitution checks. It also gains 2d6 temporary hit points, which are lost when the spell ends.\n\nBull’s Strength. The target has advantage on Strength checks, and his or her carrying capacity doubles.\n\nCat’s Grace. The target has advantage on Dexterity checks. It also doesn’t take damage from falling 20 feet or less if it isn’t incapacitated.\n\nEagle’s Splendor. The target has advantage on Charisma checks.\n\nFox’s Cunning. The target has advantage on Intelligence checks.\n\nOwl’s Wisdom. The target has advantage on Wisdom checks.\n\n***At Higher Levels.*** When you cast this spell using a spell slot of 3rd level or higher, you can target one additional creature for each slot level above 2nd.", - "duration": "Concentration, up to 1 hour", - "level": 2, - "range": "Touch", - "school": "Transmutation ", - "ritual": false, - "name": "Enhance Ability", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "fur or a feather from a beast" - } - }, - { - "castingTime": "action", - "description": "You cause a creature or an object you can see within range to grow larger or smaller for the duration. Choose either a creature or an object that is neither worn nor carried. If the target is unwilling, it can make a DC {DC} Constitution saving throw. On a success, the spell has no effect.\n\nIf the target is a creature, everything it is wearing and carrying changes size with it. Any item dropped by an affected creature returns to normal size at once. \n\nEnlarge. The target’s size doubles in all dimensions, and its weight is multiplied by eight. This growth increases its size by one category-from Medium to Large, for example. If there isn’t enough room for the target to double its size, the creature or object attains the maximum possible size in the space available. Until the spell ends, the target also has advantage on strength checks and strength saving throws. The target’s weapons also grow to match its new size. While these weapons are enlarged, the target’s attacks with them deal 1d4 extra damage.\n\nReduce. The target’s size is halved in all dimensions, and its weight is reduced to one-eighth of normal. This reduction decreases its size by one category-from Medium to Small, for example. Until the spell ends, the target also has disadvantage on strength checks and strength saving throws. The target’s weapons also shrink to match its new size. While these weapons are reduced, the target’s attacks with them deal 1d4 less damage (this can’t reduce the damage below 1).", - "duration": "Concentration, up to 1 minute", - "level": 2, - "range": "30 feet", - "school": "Transmutation", - "ritual": false, - "name": "Enlarge/Reduce", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "a pinch of powdered iron" - } - }, - { - "castingTime": "action", - "description": "Grasping weeds and vines sprout from the ground in a 20-foot square starting from a point within range. For the duration, these plants turn the ground in the area into difficult terrain.\n\nA creature in the area when you cast the spell must succeed on a DC {DC} Strength saving throw or be restrained by the entangling plants until the spell ends. A creature restrained by the plants can use its action to make a Strength check against your spell save DC. On a success, it frees itself.\n\nWhen the spell ends, the conjured plants wilt away.", - "duration": "Concentration, up to 1 minute", - "level": 1, - "range": "90 feet", - "school": "Conjuration", - "ritual": false, - "name": "Entangle", - "components": { - "verbal": true, - "somatic": true, - "concentration": true - } - }, - { - "castingTime": "action", - "description": "You weave a distracting string of words, causing the creatures of your choice that you can see within range and that can hear you to make a DC {DC} Wisdom saving throw. Any creature that can’t be charmed succeeds on this saving throw automatically, and if you or your companions are fighting a creature, it has advantage on the save. On a failed save, the target has disadvantage on Wisdom (Perception) checks made to perceive any creature other than you until the spell ends or until the target can no longer hear you. The spell ends if you are incapacitated or can no longer speak.", - "duration": "1 minute", - "level": 2, - "range": "60 feet", - "school": "Enchantment", - "ritual": false, - "name": "Enthrall", - "components": { - "verbal": true, - "somatic": true, - "concentration": false - } - }, - { - "castingTime": "action", - "description": "You step into the border regions of the Ethereal Plane, in the area where it overlaps with your current plane. You remain in the Border Ethereal for the duration or until you use your action to dismiss the spell. During this time, you can move in any direction. If you move up or down, every foot of movement costs an extra foot. You can see and hear the plane you originated from, but everything there looks gray, and you can’t see anything more than 60 feet away. \n\nWhile on the Ethereal Plane, you can only affect and be affected by other creatures on that plane. Creatures that aren’t on the Ethereal Plane can’t perceive you and can’t interact with you, unless a special ability or magic has given them the ability to do so.\n\nYou ignore all objects and effects that aren’t on the Ethereal Plane, allowing you to move through objects you perceive on the plane you originated from.\n\nWhen the spell ends, you immediately return to the plane you originated from in the spot you currently occupy. If you occupy the same spot as a solid object or creature when this happens, you are immediately shunted to the nearest unoccupied space that you can occupy and take force damage equal to twice the number of feet you are moved.\n\nThis spell has no effect if you cast it while you are on the Ethereal Plane or a plane that doesn’t border it, such as one of the Outer Planes.\n\n***At Higher Levels.*** When you cast this spell using a spell slot of 8th level or higher, you can target up to three willing creatures (including you) for each slot level above 7th. The creatures must be within 10 feet of you when you cast the spell.", - "duration": "Up to 8 hours", - "level": 7, - "range": "Self", - "school": "Transmutation", - "ritual": false, - "name": "Etherealness", - "components": { - "verbal": true, - "somatic": true, - "concentration": false - } - }, - { - "castingTime": "bonus action", - "description": "This spell allows you to move at an incredible pace. When you cast this spell, and then as a bonus action on each of your turns until the spell ends, you can take the Dash action.", - "duration": "Concentration, up to 10 minutes", - "level": 1, - "range": "Self", - "school": "Transmutation", - "ritual": false, - "name": "Expeditious Retreat", - "components": { - "verbal": true, - "somatic": true, - "concentration": true - } - }, - { - "castingTime": "action", - "description": "For the spell’s duration, your eyes become an inky void imbued with dread power. One creature of your choice within 60 feet of you that you can see must succeed on a DC {DC} Wisdom saving throw or be affected by one of the following effects of your choice for the duration. On each of your turns until the spell ends, you can use your action to target another creature but can’t target a creature again if it has succeeded on a saving throw against this casting of eyebite.\n\nAsleep. The target falls unconscious, it wakes up if it takes any damage or if another creature uses its action to shake the sleeper awake.\n\nPanicked. The target is frightened of you. On each of its turns, the frightened creature must take the Dash action and move away from you by the safest and shortest available route, unless there is nowhere to move. If the target moves to a place at least 60 feet away from you where it can no longer see you, this effect ends.\n\nSickened. The target has disadvantage on attack rolls and ability checks. At the end of each of its turns, it can make another Wisdom saving throw. If it succeeds, the effect ends.", - "duration": "Concentration, up to 1 minute", - "level": 6, - "range": "Self", - "school": "Necromancy", - "ritual": false, - "name": "Eyebite", - "components": { - "verbal": true, - "somatic": true, - "concentration": true - } - }, - { - "castingTime": "10 minutes", - "description": "You convert raw materials into products of the same material. For example, you can fabricate a wooden bridge from a clump of trees, a rope from a patch of hemp, and clothes from flax or wool.\n\nChoose raw materials that you can see within range. You can fabricate a Large or smaller object (contained within a 10-foot cube. or eight connected 5-foot cubes), given a sufficient quantity of raw material. If you are working with metal. stone, or another mineral substance, however, the fabricated object can be no larger than Medium (contained within a single 5-foot cube). The quality of objects made by the spell is commensurate with the quality of the raw materials.\n\nCreatures or magic items can’t be created or transmuted by this spell. You also can’t use it to create items that ordinarily require a high degree of craftsmanship, such as jewelry, weapons, glass, or armor, unless you have proficiency with the type of artisan’s tools used to craft such objects.", - "duration": "Instantaneous", - "level": 4, - "range": "120 feet", - "school": "Transmutation", - "ritual": false, - "name": "Fabricate", - "components": { - "verbal": true, - "somatic": true, - "concentration": false - } - }, - { - "castingTime": "action", - "description": "Each object in a 20-foot cube within range is outlined in blue, green, or violet light (your choice). Any creature in the area when the spell is cast is also outlined in light if it fails a DC {DC} Dexterity saving throw. For the duration, objects and affected creatures shed dim light in a 10 foot radius. Any attack roll against an affected creature or object the attacker can see it, and the affected creature or object can’t benefit from being invisible.", - "duration": "Concentration, up to 1 minute", - "level": 1, - "range": "60 feet", - "school": "Evocation", - "ritual": false, - "name": "Faerie Fire", - "components": { - "verbal": true, - "somatic": false, - "concentration": true - } - }, - { - "castingTime": "action", - "description": "Bolstering yourself with a necromantic facsimile of life, you gain 1d4 + 4 temporary hit points for the duration.\n\n***At Higher Levels.*** When you cast this spell using a spell slot of 2nd level or higher, you gain 5 additional temporary hit points for each slot level above 1st.", - "duration": "1 hour", - "level": 1, - "range": "Self", - "school": "Necromancy", - "ritual": false, - "name": "False Life", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "a small amount of alcohol or distilled spirits" - } - }, - { - "castingTime": "action", - "description": "You project a phantasmal fears. Each creature in a 30-foot cone must succeed on a DC {DC} Wisdom saving throw or drop whatever it is holding and become frightened for the duration.\n\nWhile frightened by this spell, a creature must take the Dash action and move away from you by the safest available route on each of its turns, unless there is nowhere to move. If the creature ends its turn in a location where it doesn’t have line of sight to you, the creature can make a DC {DC} Wisdom saving throw. On a successful save, the spell ends for that creature.", - "duration": "Concentration, up to 1 minute", - "level": 3, - "range": "Self (30-foot cone)", - "school": "Illusion", - "ritual": false, - "name": "Fear", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "a white feather or the heart of a hen" - } - }, - { - "castingTime": "1 reaction, which you take when you or a creature within 60 feet of you falls", - "description": "Choose up to five falling creatures within range. A falling creature’s rate of descent slows to 60 feet per round until the spell ends. If the creature lands before the spell ends, it takes no falling damage and can land on its feel, and the spell ends for that creature.", - "duration": "1 minute", - "level": 1, - "range": "60 feet", - "school": "Transmutation", - "ritual": false, - "name": "Feather Fall", - "components": { - "verbal": true, - "somatic": false, - "concentration": false, - "material": "a small feather or piece of down" - } - }, - { - "castingTime": "action", - "description": "You blast the mind of a creature that you can see within range, attempting to shatter its intellect and personality. The target takes 4d6 psychic damage and must make a DC {DC} Intelligence saving throw.\n\nOn a failed save, the creature’s Intelligence and Charisma scores become 1. The creature can’t cast spells, activate magic items, understand language, or communicate in any intelligible way. The creature can, however, identify its friends, follow them, and even protect them.\n\nAt the end of every 30 days, the creature can repeat its saving throw against this spell. If it succeeds on its saving throw, the spell ends.\n\nThe spell can also be ended by greater restoration, heal, or wish.", - "duration": "Instantaneous", - "level": 8, - "range": "150 feet", - "school": "Enchantment", - "ritual": false, - "name": "Feeblemind", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "a handful of clay, crystal, glass, or mineral spheres" - } - }, - { - "castingTime": "10 minutes", - "description": "You summon a spirit that assumes the form of an unusually intelligent, strong, and loyal steed, creating a long-lasting bond with it. Appearing in an unoccupied space within range, the steed takes on a form that you choose, such as a warhorse, a pony, a camel, an elk, or a mastiff. (Your DM might allow other animals to be summoned as steeds.) The steed has the statistics of the chosen form, though it is a celestial, fey, or fiend (your choice) instead of its normal type. Additionally, if your steed has an Intelligence of 5 or less, its Intelligence becomes 6, and it gains the ability to understand one language of your choice that you speak.\n\nYour steed serves you as a mount, both in combat and out, and you have an instinctive bond with it that allows you to fight as a seamless unit. While mounted on your steed, you can make any spell you cast that targets only you also target your steed.\n\nWhen the steed drops to 0 hit points, it disappears, leaving behind no physical form. You can also dismiss your steed at any time as an action, causing it to disappear. In either case, casting this spell again summons the same steed, restored to its hit point maximum.\n\nWhile your steed is within 1 mile of you, you can communicate with it telepathically.\n\nYou can’t have more than one steed bonded by this spell at a time. As an action, you can release the steed from its bond at any time, causing it to disappear.", - "duration": "Instantaneous", - "level": 2, - "range": "30 feet", - "school": "Conjuration", - "ritual": false, - "name": "Find Steed", - "components": { - "verbal": true, - "somatic": true, - "concentration": false - } - }, - { - "castingTime": "1 minute", - "description": "This spell allows you to find the shortest, most direct physical route to a specific fixed location that you are familiar with on the same plane of existence. If you name a destination on another plane of existence, a destination that moves (such as a mobile fortress), or a destination that isn’t specific (such as “a green dragon’s lair”), the spell fails.\n\nFor the duration, as long as you are on the same plane of existence as the destination, you know how far it is and in what direction it lies. While you are traveling there, whenever you are presented with a choice of paths along the way, you automatically determine which path is the shortest and most direct route (but not necessarily the safest route) to the destination.", - "duration": "Concentration, up to 1 day", - "level": 6, - "range": "Self", - "school": "Divination", - "ritual": false, - "name": "Find the Path", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "a set of divinatory tools—such as bones, ivory sticks, cards, teeth, or carved runes— worth 100 gp and an object from the location you wish to find" - } - }, - { - "castingTime": "action", - "description": "You sense the presence of any trap within range that is within line of sight. A trap, for the purpose of this spell, includes anything that would inflict a sudden or unexpected effect you consider harmful or undesirable, which was specifically intended as such by its creator. Thus, the spell would sense an area affected by the alarm spell, a glyph of warding, or a mechanical pit trap, but it would not reveal a natural weakness in the floor, an unstable ceiling, or a hidden sinkhole.\n\nThis spell merely reveals that a trap is present. You don't learn the location of each trap, but you do learn the general nature of the danger posed by a trap you sense.", - "duration": "Instantaneous", - "level": 2, - "range": "120 feet", - "school": "Divination", - "ritual": false, - "name": "Find Traps", - "components": { - "verbal": true, - "somatic": true, - "concentration": false - } - }, - { - "castingTime": "action", - "description": "You send negative energy coursing through a creature that you can see within range, causing it searing pain. The target must make a DC {DC} Constitution saving throw. It takes 7d8 + 30 necrotic damage on a failed save, or half as much damage on a successful one.\n\nA humanoid killed by this spell rises at the start of your next turn as a zombie that is permanently under your command, following your verbal orders to the best of its ability.", - "duration": "Instantaneous", - "level": 7, - "range": "60 feet", - "school": "Necromancy", - "ritual": false, - "name": "Finger of Death", - "components": { - "verbal": true, - "somatic": true, - "concentration": false - } - }, - { - "castingTime": "action", - "description": "A bright streak flashes from your pointing finger to a point you choose within range and then blossoms with a low roar into an explosion of flame. Each creature in a 20-foot-radius sphere centered on that point must make a DC {DC} Dexterity saving throw. A target takes 8d6 fire damage on a failed save, or half as much damage on a successful one. The fire spreads around corners. It ignites flammable objects in the area that aren’t being worn or carried. ***At Higher Levels.*** When you cast this spell using a spell slot of 4th level or higher, the damage increases by 1d6 for each slot level above 3rd.", - "duration": "Instantaneous", - "level": 3, - "range": "150 feet", - "school": "Evocation", - "ritual": false, - "name": "Fireball", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "a tiny ball of bat guano and sulfur" - } - }, - { - "castingTime": "action", - "description": "Thin and wispy flames wreathe your body for the duration, shedding bright light in a 10-foot radius and dim light for an additional 10 feet. You can end the spell early by using an action to dismiss it.\n\nThe flames provide you with a warm shield or a chill shield, as you choose. The warm shield grants you resistance to cold damage, and the chill shield grants you resistance to fire damage.\n\nIn addition, whenever a creature within 5 feet of you hits you with a melee attack, the shield erupts with flame. The attacker takes 2d8 fire damage from a warm shield, or 2d8 cold damage from a cold shield.", - "duration": "10 minutes", - "level": 4, - "range": "Self", - "school": "Evocation", - "ritual": false, - "name": "Fire Shield", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "a bit of phosphorus or a firefly" - } - }, - { - "castingTime": "action", - "description": "A storm made up of sheets of roaring flame appears in a location you choose within range. The area of the storm consists of up to ten 10-foot cubes, which you can arrange as you wish. Each cube must have at least one face adjacent to the face of another cube. Each creature in the area must make a DC {DC} Dexterity saving throw. It takes 7d10 fire damage on a failed save, or half as much damage on a successful one. The fire damages objects in the area and ignites flammable objects that aren’t being worn or carried. If you choose, plant life in the area is unaffected by this spell.", - "duration": "Instantaneous", - "level": 7, - "range": "150 feet", - "school": "Evocation", - "ritual": false, - "name": "Fire Storm", - "components": { - "verbal": true, - "somatic": true, - "concentration": false - } - }, - { - "castingTime": "bonus action", - "description": "You evoke a fiery blade in your free hand. The blade is similar in size and shape to a scimitar, and it lasts for the duration. If you let go of the blade, it disappears, but You can evoke the blade again as a bonus action.\n\nYou can use your action to make a melee spell attack with the fiery blade. On a hit, the target takes 3d6 fire damage.\n\nThe flaming blade sheds bright light in a 10.foot radills and dim light for an additional 10 feel.\n\n***At Higher Levels.*** When you cast this spell using a spell slot of 4th level or higher, the damage increases by 1d6 for every two slot levels above 2nd.", - "duration": "Concentration, up to 10 minutes", - "level": 2, - "range": "Self", - "school": "Evocation", - "ritual": false, - "name": "Flame Blade", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "leaf of sumac" - } - }, - { - "castingTime": "action", - "description": "A vertical column of divine fire roars down from the heavens in a location you specify. Each creature in a 10-foot-radius, 40-foot-high cylinder centered on a point within range must make a DC {DC} Dexterity saving throw. A creature takes 4d6 fire damage and 4d6 radiant damage on a failed save, or half as much damage on a successful one.\n\n***At Higher Levels.*** When you cast this spell using a spell slot of 6th level or higher, the fire damage or the radiant damage (your choice) increases by 1d6 for each slot level above 5th.", - "duration": "Instantaneous", - "level": 5, - "range": "60 feet", - "school": "Evocation", - "ritual": false, - "name": "Flame Strike", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "pinch of sulfur" - } - }, - { - "castingTime": "action", - "description": "A 5-foot-diameter sphere of fire appears in an unoccupied space of your choice within range and lasts for the duration. Any creature that ends its turn within 5 feet of the sphere must make a DC {DC} Dexterity saving throw. The creature takes 2d6 fire damage on a failed save, or half as much damage on a successful one.\n\nAs a bonus action, you can move the sphere up to 30 feet. If you ram the sphere into a creature, that creature must make the saving throw against the sphere’s damage, and the sphere stops moving this turn. \n\nWhen you move the sphere, you can direct it over barriers up to 5 feet tall and jump it across pits up to 10 feet wide. The sphere ignites flammable objects not being worn or carried, and it sheds bright light in a 20-foot radius and dim light for an additional 20 feet. ***At Higher Levels.*** When you cast this spell using a spell slot of 3rd level or higher, the damage increases by 1d6 for each slot level above 2nd.", - "duration": "Concentration, up to 1 minute", - "level": 2, - "range": "60 feet", - "school": "Conjuration", - "ritual": false, - "name": "Flaming Sphere", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "a bit of tallow, a pinch of brimstone, and a dusting of powdered iron" - } - }, - { - "castingTime": "action", - "description": "You attempt to turn one creature that you can see within range into stone. If the target’s body is made of flesh, the creature must make a DC {DC} Constitution saving throw. On a failed save, it is restrained as its flesh begins to harden. On a successful save, the creature isn’t affected.\n\nA creature restrained by this spell must make another Constitution saving throw at the end of each of its turns. If it successfully saves against this spell three times, the spell ends. If it fails its saves three times, it is turned to stone and subjected to the petrified condition for the duration. The successes and failures don’t need to be consecutive; keep track of both until the target collects three of a kind.\n\nIf the creature is physically broken while petrified, it suffers from similar deformities if it reverts to its original state.\n\nIf you maintain your concentration on this spell for the entire possible duration, the creature is turned to stone until the effect is removed.", - "duration": "Concentration, up to 1 minute", - "level": 6, - "range": "60 feet", - "school": "Transmutation", - "ritual": false, - "name": "Flesh to Stone", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "a pinch of lime, water, and earth" - } - }, - { - "castingTime": "action", - "description": "You touch a willing creature. The target gains a flying speed of 60 feet for the duration. When the spell ends, the target falls if it is still aloft, unless it can stop the fall.\n\n***At Higher Levels.*** When you cast this spell using a spell slot of 4th level or higher, you can target one additional creature for each slot level above 3rd.", - "duration": "Concentration, up to 10 minutes", - "level": 3, - "range": "Touch", - "school": "Transmutation", - "ritual": false, - "name": "Fly", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "a wing feather from any bird" - } - }, - { - "castingTime": "action", - "description": "You create a 20-foot-radius sphere of fog centered on a point within range. The sphere spreads around corners, and its area is heavily obscured. It lasts for the duration or until a wind of moderate or greater speed (at least 10 miles per hour) disperses it.\n\n***At Higher Levels.*** When you cast this spell using a spell slot of 2nd level or higher, the radius of the fog increases by 20 feet for each slot level above 1st.", - "duration": "Concentration, up to 1 hour", - "level": 1, - "range": "120 feet", - "school": "Conjuration", - "ritual": false, - "name": "Fog Cloud", - "components": { - "verbal": true, - "somatic": true, - "concentration": true - } - }, - { - "castingTime": "10 minutes", - "description": "You create a ward against magical travel that protects up to 40,000 square feet of floor space to a height of 30 feet above the floor. For the duration, creatures can’t teleport into the area or use portals, such as those created by the gate spell, to enter the area. The spell proofs the area against planar travel, and therefore prevents creatures from accessing the area by way of the Astral Plane, Ethereal Plane, Feywild, Shadowfell, or the plane shift spell.\n\nIn addition, the spell damages types of creatures that you choose when you cast it. Choose one or more of the following: celestiaIs, elementals, fey, fiends, and undead. When a chosen creature enters the spell’s area for the first time on a turn or starts its turn there, the creature takes 5d10 radiant or necrotic damage (your choice when you cast this spell).\n\nWhen you cast this spell, you can designate a password. A creature that speaks the password as it enters the area takes no damage from the spell.\n\nThe spell’s area can’t overlap with the area of another forbiddance spell. If you cast forbiddance every day for 30 days in the same location, the spell lasts until It is dispelled, and the material components are consumed on the last casting.", - "duration": "1 day", - "level": 6, - "range": "Touch", - "school": "Abjuration", - "ritual": true, - "name": "Forbiddance", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "a sprinkling of holy water, rare incense, and powdered ruby worth at least 1,000 gp" - } - }, - { - "castingTime": "action", - "description": "An immobile, invisible, cube-shaped prison composed of magical force springs into existence around an area you choose within range. The prison can be a cage or a solid box, as you choose.\n\nA prison in the shape of a cage can be up to 20 feet on a side and is made from 1/2-inch diameter bars spaced 1/2 inch apart.\n\nA prison in the shape of a box can be up to 10 feet on a side, creating a solid barrier that prevents any matter from passing through it and blocking any spells cast into or out from the area.\n\nWhen you cast the spell, any creature that is completely inside the cage’s area is trapped. Creatures only partially within the area, or those too large to fit Inside the area, are pushed away from the center of the area until they are completely outside the area.\n\nA creature inside the cage can’t leave It by nonmagical means. If the creature tries to use teleportation or interplanar travel to leave the cage, it must first make a DC {DC} Charisma saving throw. On a success, the creature can use that magic to exit the cage. On a failure, the creature can’t exit the cage and wastes the use of the spell or effect. The cage also extends into the Ethereal Plane, blocking ethereal travel.\n\nThis spell can’t be dispelled by dispel magic.", - "duration": "1 hour", - "level": 7, - "range": "100 feet", - "school": "Evocation", - "ritual": false, - "name": "Forcecage", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "ruby dust worth 1,500gp" - } - }, - { - "castingTime": "1 minute", - "description": "You touch a willing creature and bestow a limited ability to see into the immediate future. For the duration, the target can’t be surprised and has advantage on attack rolls, ability checks, and saving throws. Additionally, other creatures have disadvantage on attack rolls against the target for the duration. This spell immediately ends if you cast it again before its duration ends.", - "duration": "8 hours", - "level": 9, - "range": "Touch", - "school": "Divination", - "ritual": false, - "name": "Foresight", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "a hummingbird feather" - } - }, - { - "castingTime": "action", - "description": "You touch a willing creature. For the duration, the target’s movement is unaffected by difficult terrain, and spells and other magical effects can neither reduce the target’s speed nor cause the target to be paralyzed or restrained.\n\nThe target can also spend 5 feet of movement to automatically escape from nonmagical restraints, such as manacles or a creature that has it grappled. Finally, being underwater imposes no penalties on the target’s movement or attacks.", - "duration": "1 hour", - "level": 4, - "range": "Touch", - "school": "Abjuration", - "ritual": false, - "name": "Freedom of Movement", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "a leather strap, bound around the arm or a similar appendage" - } - }, - { - "castingTime": "action", - "description": "You transform a willing creature you touch, along with everything it’s wearing and carrying, into a misty cloud for the duration. The spell ends if the creature drops to 0 hit points. An incorporeal creature isn’t affected.\n\nWhile in this form, the target’s only method of movement is a flying speed of 10 feet. The target can enter and occupy the space of another creature. The target has resistance to nonmagical damage, and it has advantage on Strength, Dexterity, and Constitution saving throws. The target can pass through small holes, narrow openings, and even mere cracks, though it treats liquids as though they were solid surfaces. The target can’t fall and remains hovering in the air even when stunned or otherwise incapacitated.\n\nWhile in the form of a misty cloud, the target can’t talk or manipulate objects, and any objects it was carrying or holding can’t be dropped, used, or otherwise interacted with. The target can’t attack or cast spells.", - "duration": "Concentration, up to 1 hour", - "level": 3, - "range": "Touch", - "school": "Transmutation", - "ritual": false, - "name": "Gaseous Form", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "a bit of gauze and a wisp of smoke" - } - }, - { - "castingTime": "action", - "description": "You conjure a portal linking an unoccupied space you can see within range to a precise location on a different plane of existence. The portal is a circular opening, which you can make 5 to 20 feet in diameter. You can orient the portal in any direction you choose. The portal lasts for the duration.\n\nThe portal has a front and a back on each plane where it appears. Travel through the portal is possible only by moving through its front. Anything that does so is instantly transported to the other plane, appearing in the unoccupied space nearest to the portal.\n\nDeities and other planar rulers can prevent portals created by this spell from opening in their presence or anywhere within their domains. \n\nWhen you cast this spell, you can speak the name of a specific creature (a pseudonym, title, or nickname doesn’t work). If that creature is on a plane other than the one you are on, the portal opens in the named creature’s immediate vicinity and draws the creature through it to the nearest unoccupied space on your side of the portal. You gain no special power over the creature, and it is free to act as the DM deems appropriate. It might leave, attack you, or help you.", - "duration": "Concentration, up to 1 minute", - "level": 9, - "range": "60 feet", - "school": "Conjuration", - "ritual": false, - "name": "Gate", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "a diamond worth at least 5,000 gp" - } - }, - { - "castingTime": "1 minute", - "description": "You place a magical command on a creature that you can see within range, forcing it to carry out some service or refrain from some action or course of activity as you decide. If the creature can understand you, it must succeed on a DC {DC} Wisdom saving throw or become charmed by you for the duration. While the creature is charmed by you, it takes 5d10 psychic damage each time it acts in a manner directly counter to your instructions, but no more than once each day. A creature that can’t understand you is unaffected by the spell.\n\nYou can issue any command you choose, short of an activity that would result in certain death. Should you issue a suicidal command, the spell ends.\n\nYou can end the spell early by using an action to dismiss it. A remove curse, greater restoration, or wish spell also ends it.\n\n***At Higher Levels.*** When you cast this spell using a spell slot of 7th or 8th level, the duration is 1 year. When you cast this spell using a spell slot of 9th level, the spell lasts until it is ended by one of the spells mentioned above.", - "duration": "30 days", - "level": 5, - "range": "60 feet", - "school": "Enchantment", - "ritual": false, - "name": "Geas", - "components": { - "verbal": true, - "somatic": false, - "concentration": false - } - }, - { - "castingTime": "action", - "description": "You touch a corpse or other remains. For the duration, the target is protected from decay and can’t become undead. The spell also effectively extends the time limit on raising the target from the dead, since days spent under the influence of this spell don’t count against the time limit of spells such as raise dead.", - "duration": "10 days", - "level": 2, - "range": "Touch", - "school": "Necromancy", - "ritual": true, - "name": "Gentle Repose", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "a pinch of salt and one copper piece placed on each of the corpse’s eyes, which must remain there for the duration" - } - }, - { - "castingTime": "action", - "description": "You transform up to ten centipedes, three spiders, five wasps, or one scorpion within range into giant versions of their natural forms for the duration. A centipede becomes a giant centipede, a spider becomes a giant spider, a wasp becomes a giant wasp, and a scorpion becomes a giant scorpion.\n\nEach creature obeys your verbal commands, and in combat, they act on your tum each round. The DM has the statistics for these creatures and resolves their actions and movement.\n\nA creature remains in its giant size for the duration, until it drops to 0 hit points, or until you use an action to dismiss the effect on it.\n\nThe DM might allow you to choose different targets. For example, if you transform a bee, its giant version might have the same statistics as a giant wasp. ", - "duration": "Concentration, up to 10 minutes ", - "level": 4, - "range": "30 feet", - "school": "Transmutation", - "ritual": false, - "name": "Giant Insect", - "components": { - "verbal": true, - "somatic": true, - "concentration": true - } - }, - { - "castingTime": "action", - "description": "Until the spell ends, when you make a Charisma check, you can replace the number you roll with a 15. Additionally, no matter what you say, magic that would determine if you are telling the truth indicates that you are being truthful.", - "duration": "1 hour", - "level": 8, - "range": "Self", - "school": "Transmutation", - "ritual": false, - "name": "Glibness", - "components": { - "verbal": true, - "somatic": false, - "concentration": false - } - }, - { - "castingTime": "action", - "description": "An immobile, faintly shimmering barrier springs into existence in a 10-foot radius around you and remains for the duration.\n\nAny spell of 5th level or lower cast from outside the barrier can’t affect creatures or objects within it, even if the spell is cast using a higher level spell slot. Such a spell can target creatures and objects within the barrier, but the spell has no effect on them. Similarly, the area within the barrier is excluded from the areas affected by such spells.\n\n***At Higher Levels.*** When you cast this spell using a spell slot of 7th level or higher, the barrier blocks spells of one level higher for each slot level above 6th.", - "duration": "Concentration, up to 1 minute", - "level": 6, - "range": "Self (10-foot radius)", - "school": "Abjuration", - "ritual": false, - "name": "Globe of Invulnerability", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "a glass or crystal bead that shatters when the spell ends" - } - }, - { - "castingTime": "1 hour", - "description": "When you cast this spell, you inscribe a glyph that harms other creatures, either upon a surface (such as a table or a section of floor or wall) or within an object that can be closed (such as a book, a scroll, or a treasure chest) to conceal the glyph. If you choose a surface, the glyph can cover an area of the surface no larger than 10 feet in diameter. If you choose an object, that object must remain in its place; if the object is moved more than 10 feet from where you cast this spell, the glyph is broken, and the spell ends without being triggered.\n\nThe glyph is nearly invisible and requires a successful Intelligence (Investigation) check against your spell save DC to be found.\n\nYou decide what triggers the glyph when you cast the spell. For glyphs inscribed on a surface, the most typical triggers include touching or standing on the glyph, removing another object covering the glyph, approaching within a certain distance of the glyph, or manipulating the object on which the glyph is inscribed. For glyphs inscribed within an object, the most common triggers include opening that object, approaching within a certain distance of the object, or seeing or reading the glyph. Once a glyph is triggered, this spell ends.\n\nYou can further refine the trigger so the spell activates only under certain circumstances or according to physical characteristics (such as height or weight), creature kind (for example, the ward could be seI lo affect aberrations or drow), or alignment. You can also set conditions for creatures that don’l trigger the glyph, such as those who say a certain password.\n\nWhen you inscribe the glyph, choose explosive runes or a spell glyph.\n\nExplosive Runes. When triggered, the glyph erupts with magical energy in a 20-fool-radius sphere centered on the glyph. The sphere spreads around corners. Each creature in the area must make a DC {DC} Dexterity saving throw. A creature takes Sd8 acid, cold, tire, lightning, or thunder damage on a failed saving throw (your choice when you create the glyph), or half as much damage on a successful one.\n\nSpell Glyph. You can store a prepared spell of 3rd level or lower in the glyph by casting it as part of creating the glyph. The spell must target a single creature or an area. The spell being stored has no immediate effect when cast in this way. When the glyph is triggered, the stored spell is cast. If the spell has a target, it targets the creature that triggered the glyph. If the spell affects an area, the area is centered on that creature. If the spell summons hostile creatures or creates harmful objects or traps, they appear as close as possible lo the intruder and attack it. If the spell requires concentration, it lasts until the end of its full duration.\n\n***At Higher Levels.*** When you cast this spell using a spell slot of 41h level or higher, the damage of an explosive runes glyph increases by 1d8 for each slot level above 3rd. If you create a spell glyph, you can store any spell of up to the same level as the slot you use for the glyph of warding.", - "duration": "Until dispelled or triggered", - "level": 3, - "range": "Touch", - "school": "Abjuration", - "ritual": false, - "name": "Glyph of Warding", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "incense and powdered diamond worth at least 200 gp. which the spell consumes" - } - }, - { - "castingTime": "action", - "description": "Slick grease covers the ground in a 10-fool square centered on a point within range and turns it into difficult terrain for the duration.\n\nWhen the grease appears, each creature standing in its area must succeed on a DC {DC} Dexterity saving throw or fall prone. A creature that enters the area or ends its turn there must also succeed on a DC {DC} Dexterity saving throw or fall prone.", - "duration": "1 minute", - "level": 1, - "range": "60 feet", - "school": "Conjuration", - "ritual": false, - "name": "Grease", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "a bit of pork rind or butter" - } - }, - { - "castingTime": "action", - "description": "You or a creature you touch becomes invisible until the spell ends. Anything the target is wearing or carrying is invisible as long as it is on the target’s person.", - "duration": "Concentration, up to 1 minute", - "level": 4, - "range": "Touch", - "school": "Illusion", - "ritual": false, - "name": "Greater Invisibility", - "components": { - "verbal": true, - "somatic": true, - "concentration": true - } - }, - { - "castingTime": "action", - "description": "You imbue a creature you touch with positive energy to undo a debilitating effect. You can reduce the target’s exhaustion level by one, or end one of the following effects on the target:\n\n• One effect that charmed or petrified the target\n\n• One curse, including the target’s attunement to a cursed magic item\n\n• Any reduction to one of the target’s ability scores\n\n• One effect reducing the target’s hit point maximum", - "duration": "Instantaneous", - "level": 5, - "range": "Touch", - "school": "Abjuration", - "ritual": false, - "name": "Greater Restoration", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "diamond dust worth at least 100 gp, which the spell consumes" - } - }, - { - "castingTime": "10 minutes", - "description": "You create a ward that protects up to 2,500 square feet of floor space (an area 50 feet square, or one hundred 5-foot squares or twenty-five 10-foot squares). The warded area can be up to 20 feet tall, and shaped as you desire. You can ward several stories of a stronghold by dividing the area among them, as long as you can walk into each contiguous area while you are casting the spell.\n\nWhen you cast this spell, you can specify individuals that are unaffected by any or all of the effects that you choose. You can also specify a password that, when spoken aloud, makes the speaker immune to these effects.\n\nGuards and wards creates the following effects within the warded area.\n\nCorridors. Fog fills all the warded corridors, making them heavily obscured. In addition, at each intersection or branching passage offering a choice of direction, there is a 50 percent chance that a creature other than you will believe it is going in the opposite direction from the one it chooses.\n\nDoors. All doors in the warded area are magically locked, as if sealed by an arcane lock spell. In addition, you can cover up to ten doors with an illusion (equivalent to the illusory object function of the minor illusion spell) to make them appear as plain sections of wall.\n\nStairs. Webs fill all stairs in the warded area from top to bottom, as the web spell. These strands regrow in 10 minutes if they are burned or torn away while guards and wards lasts.\n\nOther Spell Effect. You can place your choice of one of the following magical effects within the warded area of the stronghold.\n\n• Place dancing lights in four corridors. You can designate a simple program that the lights repeat as long as guards and wards lasts. \n\n• Place magic mouth in two locations. \n\n• Place stinking cloud in two locations. The vapors appear in the places you designate; they return within 10 minutes if dispersed by wind while guards and wards lasts. \n\n• Place a constant gust of wind in one corridor or room. \n\n• Place a suggestion in one location. You select an area of up to 5 feet square, and any creature that enters or passes through the area receives the suggestion mentally.\n\nThe whole warded area radiates magic. A dispel magic cast on a specific effect, if successful, removes only that effect.\n\nYou can create a permanently guarded and warded structure by casting this spell there every day for one year.", - "duration": "24 hours", - "level": 6, - "range": "Touch", - "school": "Abjuration", - "ritual": false, - "name": "Guards and Wards", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "burning measure of brimstone and oil, a knotted string, a small amount of umber hulk blood, and a small silver rod worth at least 10 gp" - } - }, - { - "castingTime": "action", - "description": "You touch one willing creature. Once before the spell ends, the target can roll a d4 and add the number rolled to one ability check of its choice. It can roll the die before or after making the ability check. The spell then ends.", - "duration": "Concentration, up to 1 minute", - "level": 0, - "range": "Touch", - "school": "Divination", - "ritual": false, - "name": "Guidance", - "components": { - "verbal": true, - "somatic": true, - "concentration": true - } - }, - { - "castingTime": "action", - "description": "A flash of light streaks toward a creature of your choice within range. Make a ranged spell attack against the target. On a hit, the target takes 4d6 radiant damage, and the next attack roll made against this target before the end of your next turn has advantage, thanks to the mystical dim light glittering on the target until then.\n\n***At Higher Levels.*** When you cast this spell using a spell slot of 2nd level or higher, the damage increases by 1d6 for each slot level above 1st.", - "duration": "1 round", - "level": 1, - "range": "120 feet", - "school": "Evocation", - "ritual": false, - "name": "Guiding Bolt", - "components": { - "verbal": true, - "somatic": true, - "concentration": false - } - }, - { - "castingTime": "action", - "description": "A line of strong wind 60 feet long and 10 feet wide blasts from you in a direction you choose for the spell’s duration. Each creature that starts its turn in the line must succeed on a DC {DC} Strength saving throw or be pushed 15 feet away from you in a direction following the line.\n\nAny creature in the line must spend 2 feet of movement for every 1 foot it moves when moving closer to you.\n\nThe gust disperses gas or vapor, and it extinguishes candles, torches, and similar unprotected flames in the area. It causes protected flames, such as those of lanterns, to dance wildly and has a 50 percent chance to extinguish them.\n\nAs a bonus action on each of your turns before the spell ends, you can change the direction in which the line blasts from you.", - "duration": "Concentration, up to 1 minute", - "level": 2, - "range": "Self (60-foot line", - "school": "Evocation", - "ritual": false, - "name": "Gust of Wind", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "a legume seed" - } - }, - { - "castingTime": "24 hours", - "description": "You touch a point and infuse an area around it with holy (or unholy) power. The area can have a radius up to 60 feet, and the spell fails if the radius includes an area already under the effect a hallow spell. The affected area is subject to the following effects.\n\nFirst, celestiais, elementals, fey, fiends, and undead can’t enter the area, nor can such creatures charm, frighten, or possess creatures within it. Any creature charmed, frightened, or possessed by such a creature is no longer charmed, frightened, or possessed upon entering the area. You can exclude one or more of those types of creatures from this effect.\n\nSecond, you can bind an extra effect to the area. Choose the effect from the following list, or choose an effect offered by the DM. Some of these effects apply to creatures in the area; you can designate whether the effect applies to all creatures, creatures that follow a specific deity or leader, or creatures of a specific sort, such as orcs or trolls. When a creature that would be affected enters the spell’s area for the first time on a turn or starts its turn there, it can make a DC {DC} Charisma saving throw. On a success, the creature ignores the extra effect until it leaves the area.\n\nCourage. Affected creatures can’t be frightened while in the area.\n\nDarkness. Darkness fills the area. Normal light, as well as magical light created by spells of a lower level than the slot you used to cast this spell, can’t illuminate the area.\n\nDaylight. Bright light fills the area. Magical darkness created by spells of a lower level than the slot you used to cast this spell can’t extinguish the light.\n\nEnergy Protection. Affected creatures in the area have resistance to one damage type of your choice, except for bludgeoning, piercing, or slashing.\n\nEnergy Vulnerability. Affected creatures in the area have vulnerability to one damage type of your choice, except for bludgeoning, piercing, or slashing.\n\nEverlasting Rest. Dead bodies interred in the area can’t be turned into undead.\n\nExtradimensional Interference. Affected creatures can’t move or travel using teleportation or by extra dimensional or interplanar means.\n\nFear. Affected creatures are frightened while in the area.\n\nSilence. No sound can emanate from within the area, and no sound can reach into it.\n\nTongues. Affected creatures can communicate with any other creature in the area, even if they don’t share a common language.", - "duration": "Until dispelled", - "level": 5, - "range": "Touch", - "school": "Evocation", - "ritual": false, - "name": "Hallow", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "herbs, oils, and incense worth at least 1,000 gp, which the spell consumes" - } - }, - { - "castingTime": "10 minutes", - "description": " You make natural terrain in a 150-foot cube in range look, sound, and smell like some other sort of natural terrain. Thus, open fields or a road can be made to resemble a swamp, hill, crevasse, or some other difficult or impassable terrain. A pond can be made to seem like a grassy meadow, a precipice like a gentle slope, or a rock-strewn gully like a wide and smooth road. Manufactured structures, equipment, and creatures within the area aren’t changed in appearance.\n\nThe tactile characteristics of the terrain are unchanged, so creatures entering the area are likely to see through the illusion. If the difference isn’t obvious by touch, a creature carefully examining the illusion can attempt an Intelligence (investigation) check against your spell save DC to disbelieve it. A creature who discerns the illusion for what it is, sees it as a vague image superimposed on the terrain.", - "duration": "24 hours", - "level": 4, - "range": "300 feet", - "school": "Illusion", - "ritual": false, - "name": "Hallucinatory Terrain", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "a stone, a twig, and a bit of green plant" - } - }, - { - "castingTime": "action", - "description": "You unleash a virulent disease on a creature that you can see within range. The target must make a DC {DC} Constitution saving throw. On a failed save, it takes 14d6 necrotic damage, or half as much damage on a successful save. The damage can’t reduce the target’s hit points below 1. If the target fails the saving throw, its hit point maximum is reduced for 1 hour by an amount equal to the necrotic damage it took. Any effect that removes a disease allows a creature’s hit point maximum to return to normal before that time passes.", - "duration": "Instantaneous", - "level": 6, - "range": "60 feet", - "school": "Necromancy", - "ritual": false, - "name": "Harm", - "components": { - "verbal": true, - "somatic": true, - "concentration": false - } - }, - { - "castingTime": "action", - "description": "Choose a willing creature that you can see within range. Until the spell ends, the target’s speed is doubled, it gains a +2 bonus to AC, it has advantage on Dexterity saving throws, and it gains an additional action on each of its turns. That action can be used only to take the Attack (one weapon attack only), Dash, Disengage, Hide, or Use an Object action. When the spell ends, the target can’t move or take actions until after its next turn, as a wave of lethargy sweeps over it.", - "duration": "Concentration, up to 1 minute", - "level": 3, - "range": "30 feet", - "school": "Transmutation", - "ritual": false, - "name": "Haste", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "a shaving of licorice root" - } - }, - { - "castingTime": "action", - "description": "Choose a creature that you can see within range. A surge of positive energy washes through the creature, causing it to regain 70 hit points. This spell also ends blindness, deafness, and any diseases affecting the target. This spell has no effect on constructs or undead. ***At Higher Levels.*** When you cast this spell using a spell slot of 7th level or higher, the amount of healing increases by 10 for each slot level above 6th.", - "duration": "Instantaneous", - "level": 6, - "range": "60 feet", - "school": "Evocation", - "ritual": false, - "name": "Heal", - "components": { - "verbal": true, - "somatic": true, - "concentration": false - } - }, - { - "castingTime": "bonus action", - "description": "A creature of your choice that you can see within range regains hit points equal to 1d4 + your spellcasting ability modifier. This spell has no effect on undead or constructs.\n\n***At Higher Levels.*** When you cast this spell using a spell slot of 2nd level or higher, the healing increases by 1d4 for each slot level above 1st.", - "duration": "Instantaneous", - "level": 1, - "range": "60 feet", - "school": "Evocation", - "ritual": false, - "name": "Healing Word", - "components": { - "verbal": true, - "somatic": false, - "concentration": false - } - }, - { - "castingTime": "bonus action", - "description": "Choose a manufactured metal object, such as a metal weapon or a suit of heavy or medium metal armor, that you can see within range. You cause the object to glow red-hot. Any creature in physical contact with the object takes 2d8 fire damage when you cast the spell. Until the spell ends, you can use a bonus action on each of your subsequent turns to cause this damage again.\n\nIf a creature is holding or wearing the object and takes the damage from it, the creature must succeed on a DC {DC} Constitution saving throw or drop the object if it can. If it doesn’t drop the object, it has disadvantage on attack rolls and ability checks until the start of your next turn.\n\n***At Higher Levels.*** When you cast this spell using a spell slot of 3rd level or higher, the damage increases by 1d8 for each slot level above 2nd.", - "duration": "Instantaneous", - "level": 2, - "range": "60 feet", - "school": "Transmutation", - "ritual": false, - "name": "Heat Metal", - "components": { - "verbal": true, - "somatic": false, - "concentration": false - } - }, - { - "castingTime": "10 minutes", - "description": "You bring forth a great feast, including magnificent food and drink. The feast takes 1 hour to consume and disappears at the end of that time, and the beneficial effects don’t set in until this hour is over. Up to twelve other creatures can partake of the feast.\n\nA creature that partakes of the feast gains several benefits. The creature is cured of all diseases and poison, becomes immune to poison and being frightened, and makes all Wisdom saving throws with advantage. Its hit point maximum also increases by 2d10, and it gains the same number of hit points. These benefits last for 24 hours.", - "duration": "Instantaneous", - "level": 6, - "range": "30 feet", - "school": "Conjuration", - "ritual": false, - "name": "Heroes’ Feast", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "a gem-encrusted bowl worth at least 1,000 gp, which the spell consumes" - } - }, - { - "castingTime": "action", - "description": "A willing creature you touch is imbued with bravery. Until the spell ends, the creature is immune to being frightened and gains temporary hit points equal to your spellcasting ability modifier at the start of each of its turns. When the spell ends, the target loses any remaining temporary hit points from this spell.\n\n***At Higher Levels.*** When you cast this spell using a spell slot of 2nd level or higher, you can target one additional creature for each slot level above 1st.", - "duration": "Concentration, up to 1 minute", - "level": 1, - "range": "Touch", - "school": "Enchantment", - "ritual": false, - "name": "Heroism", - "components": { - "verbal": true, - "somatic": true, - "concentration": true - } - }, - { - "castingTime": "action", - "description": "Choose a creature that you can see within range. The target must succeed on a DC {DC} Wisdom saving throw or be paralyzed for the duration. This spell has no effect on undead. At the end of each of its turns, the target can make another Wisdom saving throw. On a success, the spell ends on the target.\n\n***At Higher Levels.*** When you cast this spell using a spell slot of 6th level or higher. you can target one additional creature for each slot level above 5th. The creatures must be within 30 feet of each other when you target them.", - "duration": "Concentration, up to 1 minute", - "level": 5, - "range": "90 feet", - "school": "Enchantment", - "ritual": false, - "name": "Hold Monster", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "a small, straight piece of iron" - } - }, - { - "castingTime": "action", - "description": "Choose a humanoid that you can see within range. The target must succeed on a DC {DC} Wisdom saving throw or be paralyzed for the duration. At the end of each of its turns, the target can make another Wisdom saving throw. On a success, the spell ends on the target.\n\n***At Higher Levels.*** When you cast this spell using a spell slot of 3rd level or higher, you can target one additional humanoid for each slot level above 2nd. The humanoids must be within 30 feet of each other when you target them.", - "duration": "Concentration, up to 1 minute", - "level": 2, - "range": "60 feet", - "school": "Enchantment", - "ritual": false, - "name": "Hold Person", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "a small, straight piece of iron" - } - }, - { - "castingTime": "action", - "description": "Divine light washes out from you and coalesces in a soft radiance in a 30-foot radius around you. Creatures of your choice in that radius when you cast this spell shed dim light in a 5-foot radius and have advantage on all saving throws, and other creatures have disadvantage on attack rolls against them until the spell ends. In addition, when a fiend or an undead hits an affected creature with a melee attack, the aura flashes with brilliant light. The attacker must succeed on a DC {DC} Constitution saving throw or be blinded until the spell ends.", - "duration": "Concentration, up to 1 minute", - "level": 8, - "range": "Self", - "school": "Abjuration", - "ritual": false, - "name": "Holy Aura", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "a tiny reliquary worth at least 1,000 gp containing a sacred relic, such as a scrap of cloth from a saint’s robe or a piece of parchment from a religious text" - } - }, - { - "castingTime": "action", - "description": "You create a twisting pattern of colors that weaves through the air inside a 30-foot cube within range. The pattern appears for a moment and vanishes. Each creature in the area who sees the pattern must make a DC {DC} Wisdom saving throw. On a failed save, the creature becomes charmed for the duration. While charmed by this spell, the creature is incapacitated and has a speed of 0.\n\nThe spell ends for an affected creature if it takes any damage or if someone else uses an action to shake the creature out of its stupor.", - "duration": "Concentration, up to 1 minute", - "level": 3, - "range": "120 feet", - "school": "Illusion", - "ritual": false, - "name": "Hypnotic Pattern", - "components": { - "verbal": false, - "somatic": true, - "concentration": true, - "material": "a glowing stick of incense or a crystal vial filled with phosphorescent material" - } - }, - { - "castingTime": "action", - "description": "A hail of rock-hard ice pounds to the ground in a 20-foot-radius, 40-foot-high cylinder centered on a point within range. Each creature in the cylinder must make a DC {DC} Dexterity saving throw. A creature takes 2d8 bludgeoning damage and 4d6 cold damage on a failed save, or half as much damage on a successful one.\n\nHailstones turn the storm’s area of effect into difficult terrain until the end of your next turn.\n\n***At Higher Levels.*** When you cast this spell using a spell slot of 5th level or higher, the bludgeoning damage increases by 1d8 for each slot level above 4th.", - "duration": "Instantaneous", - "level": 4, - "range": "300 feet", - "school": "Evocation", - "ritual": false, - "name": "Ice Storm", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "a pinch of dust and a few drops of water" - } - }, - { - "castingTime": "1 minute", - "description": "You choose one object that you must touch throughout the casting of the spell. If it is a magic item or some other magic-imbued object, you learn its properties and how to use them, whether it requires attunement to use, and how many charges it has, if any. You learn whether any spells are affecting the item and what they are. If the item was created by a spell, you learn which spell created it.\n\nIf you instead touch a creature throughout the casting, you learn what spells, if any, are currently affecting it.", - "duration": "Instantaneous", - "level": 1, - "range": "Touch", - "school": "Divination", - "ritual": true, - "name": "Identify", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "a pearl worth at least 100 gp and an owl feather" - } - }, - { - "castingTime": "1 minute", - "description": "You write on parchment, paper, or some other suitable writing material and imbue it with a potent illusion that lasts for the duration.\n\nTo you and any creatures you designate when you cast the spell, the writing appears normal, written in your hand, and conveys whatever meaning you intended when you wrote the text. To all others, the writing appears as if it were written in an unknown or magical script that is unintelligible. Alternatively, you can cause the writing lo appear to be an entire1y different message, written in a different hand and language, though the language must be one you know.\n\nShould the spell be dispelled, the original script and the illusion both disappear.\n\nA creature with truesight can read the hidden message.", - "duration": "10 days", - "level": 1, - "range": "Touch", - "school": "Illusion", - "ritual": true, - "name": "Illusory Script", - "components": { - "verbal": false, - "somatic": true, - "concentration": false, - "material": "a lead-based ink worth at least 10 gp, which the spell consumes" - } - }, - { - "castingTime": "1 minute", - "description": "You create a magical restraint to hold a creature that you can see within range. The target must succeed on a DC {DC} Wisdom saving throw or be bound by the spell; if it succeeds, it is immune to this spell if you cast it again. While affected by this spell, the creature doesn’t need to breathe, eat, or drink, and it doesn’t age. Divination spells can’t locate or perceive the target.\n\nWhen you cast the spell, you choose one of the following forms of imprisonment.\n\nBurial. The target is entombed far beneath the earth in a sphere of magical force that is just large enough to contain the target. Nothing can pass through the sphere, nor can any creature teleport or use planar travel to get into or out of it. The special component for this version of the spell is a small mithral orb.\n\nChaining. Heavy chains, firmly rooted in the ground, hold the target in place. The target is restrained until the spell ends, and it can’t move or be moved by any means until then. The special component for this version of the spell is a fine chain of precious metal.\n\nHedged Prison. The spell transports the target into a tiny demiplane that is warded against teleportation and planar travel. The demiplane can be a labyrinth, a cage, a tower, or any similar confined structure or area of your choice. The special component for this version of the spell is a miniature representation of the prison made from jade. \n\nMinimus Containment. The target shrinks to a height of 1 inch and is imprisoned inside a gemstone or similar object. Light can pass through the gemstone normally (allowing the target to see out and other creatures to see in), but nothing else can pass through, even by means of teleportation or planar travel. The gemstone can’t be cut or broken while the spell remains in effect. The special component for this version of the spell is a large, transparent gemstone, such as a corundum, diamond, or ruby.\n\nSlumber. The target falls asleep and can’t be awoken. The special component for this version of the spell consists of rare soporific herbs. \n\nEnding the Spell. During the casting of the spell, in any of its versions, you can specify a condition that will cause the spell to end and release the target. The condition can be as specific or as elaborate as you choose, but the DM must agree that the condition is reasonable and has a likelihood of coming to pass. The conditions can be based on a creature’s name, identity, or deity but otherwise must be based on observable actions or qualities and not based on intangibles such as level, class, or hit points. A dispel magic spell can end the spell only if it is cast as a 9th-level spell, targeting either the prison or the special component used to create it. You can use a particular special component to create only one prison at a time. If you cast the spell again using the same component, the target of the first casting is immediately freed from its binding.", - "duration": "Until dispelled", - "level": 9, - "range": "30 feet", - "school": "Abjuration", - "ritual": false, - "name": "Imprisonment", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "a vellum depiction or a carved statuette in the likeness of the target, and a special component that varies according to the version of the spell you choose, worth at least 500 gp per Hit Die of the target" - } - }, - { - "castingTime": "action", - "description": "A swirling cloud of smoke shot through with white-hot embers appears in a 20-foot-radius sphere centered on a point within range. The cloud spreads around corners and is heavily obscured. It lasts for the duration or until a wind of moderate or greater speed (at least 10 miles per hour) disperses it.\n\nWhen the cloud appears, each creature in it must make a DC {DC} Dexterity saving throw. A creature takes 10d8 fire damage on a failed save, or half as much damage on a successful one. A creature must also make this saving throw when it enters the spell’s area for the first time on a turn or ends its turn there.\n\nThe cloud moves 10 feet directly away from you in a direction that you choose at the start of each of your turns.", - "duration": "Instantaneous", - "level": 8, - "range": "150 feet", - "school": "Conjuration", - "ritual": false, - "name": "Incendiary Cloud", - "components": { - "verbal": true, - "somatic": true, - "concentration": false - } - }, - { - "castingTime": "action", - "description": "Make a melee spell attack against a creature you can reach. On a hit, the target takes 3d10 necrotic damage.\n\n***At Higher Levels.*** When you cast this spell using a spell slot of 2nd level or higher, the damage increases by 1d10 for each slot level above 1st.", - "duration": "Instantaneous", - "level": 1, - "range": "Touch", - "school": "Necromancy", - "ritual": false, - "name": "Inflict Wounds", - "components": { - "verbal": true, - "somatic": true, - "concentration": false - } - }, - { - "castingTime": "action", - "description": "Swarming, biting locusts fill a 20-foot-radius sphere centered on a point you choose within range, The sphere spreads around corners. The sphere remains for the duration, and its area is lightly obscured. The sphere’s area is difficult terrain.\n\nWhen the area appears, each creature in it must make a DC {DC} Constitution saving throw. A creature takes 4d10 piercing damage on a failed save, or half as much damage on a successful one. A creature must also make this saving throw when it enters the spell’s area for the first time on a turn or ends its turn there.\n\n***At Higher Levels.*** When you cast this spell using a spell slot of 6th level or higher, the damage increases by 1d10 for each slot level above 5th.", - "duration": "Concentration, up to 10 minutes", - "level": 5, - "range": "300 feet", - "school": "Conjuration", - "ritual": false, - "name": "Insect Plague", - "components": { - "verbal": true, - "somatic": true, - "concentration": true - } - }, - { - "castingTime": "action", - "description": "A creature you touch becomes invisible until the spell ends. Anything the target is wearing or carrying is invisible as long as it is on the target’s person. The spell ends for a target that attacks or casts a spell. ***At Higher Levels.*** When you cast this spell using a spell slot of 3rd level or higher, you can target one additional creature for each slot level above 2nd.", - "duration": "Concentration, up to 1 hour", - "level": 2, - "range": "Touch", - "school": "Illusion", - "ritual": false, - "name": "Invisibility", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "an eyelash encased in gum arabic" - } - }, - { - "castingTime": "action", - "description": "You touch a creature. The creature’s jump distance is tripled until the spell ends.", - "duration": "1 minute", - "level": 1, - "range": "Touch", - "school": "Transmutation", - "ritual": false, - "name": "Jump", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "a grasshopper’s hind leg" - } - }, - { - "castingTime": "action", - "description": "Choose an object that you can see within range. The object can be a door, a box, a chest, a set of manacles, a padlock, or another object that contains a mundane or magical means that prevents access.\n\nA target that is held shut by a mundane lock or that is stuck or barred becomes unlocked, unstuck, or unbarred. If the object has multiple locks, only one of them is unlocked.\n\nIf you choose a target that is held shut with arcane lock, that spell is suppressed for 10 minutes, during which time the target can be opened and shut normally.\n\nWhen you cast the spell, a loud knock, audible from as far away as 300 feet, emanates from the target object.", - "duration": "Instantaneous", - "level": 2, - "range": "60 feet", - "school": "Transmutation", - "ritual": false, - "name": "Knock", - "components": { - "verbal": true, - "somatic": false, - "concentration": false - } - }, - { - "castingTime": "10 minutes", - "description": "Name or describe a person, place, or object. The spell brings to your mind a brief summary of the significant lore about the thing you named. The lore might consist of current tales, forgotten stories, or even secret lore that has never been widely known. If the thing you named isn’t of legendary importance, you gain no information. The more information you already have about the thing, the more precise and detailed the information you receive is.\n\nThe information you learn is accurate but might be couched in figurative language. For example, if you have a mysterious magic axe on hand, the spell might yield this information: “Woe to the evildoer whose hand touches the axe, for even the haft slices the hand of the evil ones. Only a true Child of Stone, lover and beloved of Moradin, may awaken the true powers of the axe, and only with the sacred word Rudnogg on the lips.“", - "duration": "Instantaneous", - "level": 5, - "range": "Self", - "school": "Divination", - "ritual": false, - "name": "Legend Lore", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "incense worth at least 250gp, which the spell consumes, and four ivory strips worth at least 50 gp each" - } - }, - { - "castingTime": "action", - "description": "You hide a chest, and all its contents on the Ethereal Plane. You must touch the chest and the miniature replica that serves as a material component for the spell. The chest can contain up to 12 cubic feet of nonliving material (3 feet by 2 feet by 2 feet).\n\nWhile the chest remains on the Ethereal Plane, you can use an action and touch the replica to recall the chest. It appears in an unoccupied space on the ground within 5 feet of you. You can send the chest back to the Ethereal Plane by using an action and touching both the chest and the replica.\n\nAfter 60 days, there is a cumulative 5 percent chance per day that the spell’s effect ends. This effect ends if you cast this spell again, if the smaller replica chest is destroyed, or if you choose to end the spell as an action. If the spell ends and the larger chest is on the Ethereal Plane, it is irretrievably lost.", - "duration": "Instantaneous", - "level": 4, - "range": "Touch", - "school": "Conjuration", - "ritual": false, - "name": "Secret Chest", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "an exquisite chest, 3 feet by 2 feet by 2 feet, constructed from rare materials worth at least 5,000 gp, and a Tiny replica made from the same materiais worth at least 50 gp" - } - }, - { - "castingTime": "1 minute", - "description": "A 10-foot-radius immobile dome of force springs into existence around and above you and remains stationary for the duration. The spell ends if you leave its area.\n\nNine creatures of Medium size or smaller can fit inside the dome with you. The spell fails if its area includes a larger creature or more than nine creatures. Creatures and objects within the dome when you cast this spell can move through it freely. All other creatures and objects are barred from passing through it. Spells and other magical effects can’t extend through the dome or be cast through it. The atmosphere inside the space is comfortable and dry, regardless of the weather outside.\n\nUntil the spell ends, you can command the interior to become dimly lit or dark. The dome is opaque from the outside, of any color you choose, but it is transparent from the inside.", - "duration": "8 hours", - "level": 3, - "range": "Self (10-foot-radius hemisphere)", - "school": "Evocation", - "ritual": true, - "name": "Tiny Hut", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "a small crystal bead" - } - }, - { - "castingTime": "action", - "description": "You touch a creature and can end either one disease or one condition afflicting it. The condition can be blinded, deafened, paralyzed, or poisoned.", - "duration": "Instantaneous", - "level": 2, - "range": "Touch", - "school": "Abjuration", - "ritual": false, - "name": "Lesser Restoration", - "components": { - "verbal": true, - "somatic": true, - "concentration": false - } - }, - { - "castingTime": "action", - "description": "One creature or object of your choice that you can see within range rises vertically, up to 20 feet, and remains suspended there for the duration. The spell can levitate a target that weighs up to 500 pounds. An unwilling creature that succeeds on a DC {DC} Constitution saving throw is unaffected.\n\nThe target can move only by pushing or pulling against a fixed object or surface within reach (such as a wall or a ceiling), which allows it to move as if it were climbing. You can change the target’s altitude by up to 20 feet in either direction on your turn. If you are the target, you can move up or down as part of your move. Otherwise, you can use your action to move the target, which must remain within the spell’s range.\n\nWhen the spell ends, the target floats gently to the ground if it is still aloft.", - "duration": "Concentration, up to 10 minutes", - "level": 2, - "range": "60 feet", - "school": "Transmutation", - "ritual": false, - "name": "Levitate", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "either a small leather loop or a piece of golden wire bent into a cup shape with a long shank on one end" - } - }, - { - "castingTime": "action", - "description": "You touch one object that is no larger than 10 feet in any dimension. Until the spell ends, the object sheds bright light in a 20-foot radius and dim light for an additional 20 feet. The light can be colored as you like. Completely covering the object with something opaque blocks the light. The spell ends if you cast it again or dismiss it as an action.\n\nIf you target an object held or worn by a hostile creature, that creature must succeed on a DC {DC} Dexterity saving throw to avoid the spell.", - "duration": "1 hour", - "level": 0, - "range": "Touch", - "school": "Evocation", - "ritual": false, - "name": "Light", - "components": { - "verbal": true, - "somatic": false, - "concentration": false, - "material": "a firefly or phosphorescent moss" - } - }, - { - "castingTime": "action", - "description": "A stroke of lightning forming a line 100 feet long and 5 feet wide blasts out from you in a direction you choose. Each creature in the line must make a DC {DC} Dexterity saving throw. A creature takes 8d6 lightning damage on a failed save, or half as much damage on a successful one. The lightning ignites flammable objects in the area that aren’t being worn or carried. ***At Higher Levels.*** When you cast this spell using a spell slot of 4th level or higher, the damage increases by 1d6 for each slot level above 3rd.", - "duration": "Instantaneous", - "level": 3, - "range": "Self (100-foot line)", - "school": "Evocation", - "ritual": false, - "name": "Lightning Bolt", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "a bit of fur and a rod of amber, crystal, or glass" - } - }, - { - "castingTime": "action", - "description": "Describe or name a specific kind of beast or plant. Concentrating on the voice of nature in your surroundings, you learn the direction and distance to the closest creature or plant of that kind within 5 miles, if any are present.", - "duration": "Instantaneous", - "level": 2, - "range": "Self", - "school": "Divination", - "ritual": true, - "name": "Locate Animals or Plants", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "a bit of fur from a bloodhound" - } - }, - { - "castingTime": "action", - "description": "Describe or name a creature that is familiar to you. You sense the direction to the creature’s location, as long as that creature is within 1,000 feet of you. If the creature is moving, you know the direction of its movement. \n\nThe spell can locate a specific creature known to you, or the nearest creature of a specific kind (such as a human or a unicorn), so long as you have seen such a creature up close—within 30 feet—at least once. If the creature you described or named is in a different form, such as being under the effects of a polymorph spell, this spell doesn’t locate the creature.\n\nThis spell can’t locate a creature if running water at least 10 feet wide blocks a direct path between you and the creature.", - "duration": "Concentration, up to 1 hour", - "level": 4, - "range": "Self", - "school": "Divination", - "ritual": false, - "name": "Locate Creature", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "a bit of fur from a bloodhound" - } - }, - { - "castingTime": "action", - "description": "Describe or name an object that is familiar to you. You sense the direction to the object’s location, as lang as that object is within 1,000 feet of you. If the object is in motion, you know the direction of its movement.\n\nThe spell can locate a specific object known to you, as long as you have seen it up close-within 30 feet-at least once. Alternatively, the spell can locate the nearest object of a particular kind, such as a certain kind of apparel, jewelry, furniture, tool, or weapon.\n\nThis spell can’t locate an object if any thickness of lead, even a thin sheet, blocks a direct path between you and the object.", - "duration": "Concentration, up to 1 hour", - "level": 2, - "range": "Self", - "school": "Divination", - "ritual": false, - "name": "Locate Object", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "a forked twig" - } - }, - { - "castingTime": "action", - "description": "You touch a creature. The target’s speed increases by 10 feet until the spell ends.\n\n***At Higher Levels.*** When you cast this spell using a spell slot of 2nd level or higher, you can target one additional creature for each slot level above 1st.", - "duration": "1 hour", - "level": 1, - "range": "Touch", - "school": "Transmutation", - "ritual": false, - "name": "Longstrider", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "a pinch of dirt" - } - }, - { - "castingTime": "action", - "description": "You touch a willing creature who isn’t wearing armor, and a protective magical force surrounds it until the spell ends. The target’s base AC becomes 13 + its Dexterity modifier. The spell ends if the target dons armor or if you dismiss the spell as an action.", - "duration": "8 hours", - "level": 1, - "range": "Touch", - "school": "Abjuration", - "ritual": false, - "name": "Mage Armor", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "a piece of cured leather" - } - }, - { - "castingTime": "action", - "description": "A spectral, floating hand appears at a point you choose within range. The hand lasts for the duration or until you dismiss it as an action. The hand vanishes if it is ever more than 30 feet away from you or if you cast this spell again. You can use your action to control the hand.\n\nYou can use the hand to manipulate an object, open an unlocked door or container, stow or retrieve an item from an open container, or pour the contents out of a vial. You can move the hand up to 30 feet each time you use it.\n\nThe hand can’t attack, activate magic items, or carry more than 10 pounds.", - "duration": "1 minute", - "level": 0, - "range": "30 feet", - "school": "Conjuration", - "ritual": false, - "name": "Mage Hand", - "components": { - "verbal": true, - "somatic": true, - "concentration": false - } - }, - { - "castingTime": "1 minute", - "description": "You create a 10-foot-radius, 20-foot-tall cylinder of magical energy centered on a point on the ground that you can see within range. Glowing runes appear wherever the cylinder intersects with the floor or other surface.\n\nChoose one or more of the following types of creatures: celestials, elementals, fey, fiends, or undead. The circle affects a creature of the chosen type in the following ways:\n\n• The creature can’t willingly enter the cylinder by nonmagical means. If the creature tries to use teleportation or interplanar travel to do so, it must first succeed on a DC {DC} Charisma saving throw.\n\n• The creature has disadvantage on attack rolls against targets within the cylinder.\n\n• Targets within the cylinder can’t be charmed, frightened, or possessed by the creature.", - "duration": "1 hour", - "level": 3, - "range": "10 feet", - "school": "Abjuration", - "ritual": false, - "name": "Magic Circle", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "holy water or powdered silver and iron worth at least 100 gp, which the spell consumes" - } - }, - { - "castingTime": "1 minute", - "description": "Your body falls into a catatonic state as your soulleaves it and enters the container you used for the spell’s material component. While your soul inhabits the container, you are aware of your surroundings as if you were in the container’s space. You can’t move or use reactions. The only action you can take is to project your soul up to 100 feet out of the container, either returning to your living body (and ending the spell) or attempting to possess a humanoids body.\n\nYou can attempt to possess any humanoid within 100 feet of you that you can see (creatures warded by a protection from evil and good or magic circIe spell can’t be possessed). The target must make a DC {DC} Charisma saving throw. On a failure, your soul moves into the target’s body, and the target’s soul becomes trapped in the container. On a success, the target resists your efforts to possess it, and you can’t attempt to possess it again for 24 hours.\n\nOnce you possess a creature’s body, you control it. Your game statistics are replaced by the statistics of the creature, though you retain your alignment and your Intelligence, Wisdom, and Charisma scores. You retain the benefit of your own class features. If the target has any class levels, you can’t use any of its class features.\n\nMeanwhile, the possessed creature’s soul can perceive from the container using its own senses, but it can’t move or take actions at all.\n\nWhile possessing a body, you can use your action to return from the host body to the container if it is within 100 feet of you, returning the host creature’s soul to its body. If the host body dies while you’re in it, the creature dies, and you must make a DC {DC} Charisma saving throw against your own spellcasting DC. On a success, you return to the container if it is within 100 feet of you. Otherwise, you die.\n\nIf the container is destroyed or the spell ends, your soul immediately returns to your body. If your body is more than 100 feet away from you or if your body is dead when you attempt to return to it, you die. If another creature’s soul is in the container when it is destroyed, the creature’s soul returns to its body if the body is alive and within 100 feet. Otherwise, that creature dies.\n\nWhen the spell ends, the container is destroyed.", - "duration": "Until dispelled", - "level": 6, - "range": "Self", - "school": "Necromancy", - "ritual": false, - "name": "Magic Jar", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "a gem, crystal, reliquary, or some other ornamental container worth at least 500 gp" - } - }, - { - "castingTime": "action", - "description": "You create three glowing darts of magical force. Each dart hits a creature of your choice that you can see within range. A dart deals 1d4 + 1 force damage to its target. The darts all strike simultaneously, and you can direct them to hit one creature or several. ***At Higher Levels.*** When you cast this spell using a spell slot of 2nd level or higher, the spell creates one more dart for each slot level above 1st.", - "duration": "Instantaneous", - "level": 1, - "range": "120 feet", - "school": "Evocation", - "ritual": false, - "name": "Magic Missile", - "components": { - "verbal": true, - "somatic": true, - "concentration": false - } - }, - { - "castingTime": "1 minute", - "description": "You implant a message within an object in range, a message that is uttered when a trigger condition is met. Choose an object that you can see and that isn’t being worn or carried by another creature. Then speak the message, which must be 25 words or less, though it can be delivered over as long as 10 minutes. Finally, determine the circumstance that will trigger the spell to deliver your message.\n\nWhen that circumstance occurs, a magical mouth appears on the object and recites the message in your voice and at the same volume you spoke. Or the object you chose has a mouth or something that looks like a mouth (for example, the mouth of a statue), the magical mouth appears there so that the words appear to come from the object’s mouth. When you cast this spell, you can have the spell end after it delivers its message, or it can remain and repeat its message whenever the trigger occurs.\n\nThe triggering circumstance can be as general or as detailed as you like, though it must be based on visual or audible conditions that occur within 30 feet of the object. For example, you could instruct the mouth to speak when any creature moves within 30 feet of the object or when a silver bell rings within 30 feet of it.", - "duration": "Until dispelled", - "level": 2, - "range": "30 feet", - "school": "Illusion", - "ritual": true, - "name": "Magic Mouth", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "a small bit of honeycomb and jade dust worth at least 10 gp, which the spell consumes" - } - }, - { - "castingTime": "bonus action", - "description": "You touch a nonmagical weapon. Until the spell ends, that weapon becomes a magic weapon with a +1 bonus to attack rolls and damage rolls.\n\n***At Higher Levels.*** When you cast this spell using a spell slot of 4th level or higher, the bonus increases to +2. When you use a spell slot of 6th level or higher, the bonus increases to +3.", - "duration": "Concentration, up to 1 hour", - "level": 2, - "range": "Touch", - "school": "Transmutation", - "ritual": false, - "name": "Magic Weapon", - "components": { - "verbal": true, - "somatic": true, - "concentration": true - } - }, - { - "castingTime": "action", - "description": "You create the image of an object, a creature, or some other visible phenomenon that is no larger than a 20-foot cube. The image appears at a spot that you can see within range and lasts for the duration. It seems completely real, including sounds, smells, and temperature appropriate to the thing depicted. You can’t create sufficient heat or cold to cause damage, a sound loud enough to deal thunder damage or deafen a creature, or a smell that might sicken a creature (like a troglodyte’s stench).\n\nAs long as you are within range of the illusion, you can use your action to cause the image to move to any other spot within range. As the image changes location, you can alter its appearance so that its movements appear natural for the image. For example, if you create an image of a creature and move it, you can alter the image so that it appears to be walking. Similarly, you can cause the illusion to make different sounds at different times, even making it carry on a conversation, for example.\n\nPhysical interaction with the image reveals it to be an illusion, because things can pass through it. A creature that uses its action to examine the image can determine that it is an illusion with a successful Intelligence (Investigation) check against your spell save DC. If a creature discerns the illusion for what it is, the creature can see through the image, and its other sensory qualities become faint to the creature.\n\n***At Higher Levels.*** When you cast this spell using a spell slot of 6th level or higher, the spell lasts until dispelled, without requiring your concentration.", - "duration": "Concentration, up to 10 minutes", - "level": 3, - "range": "120 feet", - "school": "Illusion", - "ritual": false, - "name": "Major Image", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "a bit of fleece" - } - }, - { - "castingTime": "action", - "description": "A wave of healing energy washes out from a point of your choice within range. Choose up to six creatures in a 30-foot-radius sphere centered on that point. Each target regains hit points equal to 3d8 + your spellcasting ability modifier. This spell has no effect on undead or constructs. \n\n***At Higher Levels.*** When you cast this spell using a spell slot of 6th level or higher, the healing increases by 1d8 for each slot level above 5th.\n\n(Spell's description has been modified to fix the error during printing as described in the Player's Handbook errata. See http://media.wizards.com/2016/downloads/DND/PH-Errata-V1.pdf for full details)", - "duration": "Instantaneous", - "level": 5, - "range": "60 feet", - "school": "Evocation", - "ritual": false, - "name": "Mass Cure Wounds", - "components": { - "verbal": true, - "somatic": true, - "concentration": false - } - }, - { - "castingTime": "action", - "description": "A flood of healing energy flows from you into injured creatures around you. You restore up to 700 hit points, divided as you choose among any number of creatures that you can see within range. Creatures healed by this spell are also cured of all diseases and any effect making them blinded or deafened. This spell has no effect on undead or constructs. \n\n(Spell school has been modified to fix the error during printing as described in the Player's Handbook errata. See http://media.wizards.com/2016/downloads/DND/PH-Errata-V1.pdf for full details)", - "duration": "Instantaneous", - "level": 9, - "range": "60 feet", - "school": "Evocation", - "ritual": false, - "name": "Mass Heal", - "components": { - "verbal": true, - "somatic": true, - "concentration": false - } - }, - { - "castingTime": "bonus action", - "description": "As you call out words of restoration, up to six creatures of your choice that you can see within range regain hit points equal to 1d4 + your spellcasting ability modifier. This spell has no effect on undead or constructs.\n\n***At Higher Levels.*** When you cast this spell using a spell slot of 4th level or higher, the healing increases by 1d4 for each slot level above 3rd.", - "duration": "Instantaneous", - "level": 3, - "range": "60 feet", - "school": "Evocation", - "ritual": false, - "name": "Mass Healing Word", - "components": { - "verbal": true, - "somatic": false, - "concentration": false - } - }, - { - "castingTime": "action", - "description": "You suggest a course of activity (limited to a sentence or two) and magically influence up to twelve creatures of your choice that you can see within range and that can hear and understand you. Creatures that can’t be charmed are immune to this effect. The suggestion must be worded in such a manner as to make the course of action sound reasonable. Asking the creature to stab itself, throw itself onto a spear, immolate itself, or do some other obviously harmful act automatically negates the effect of the spell.\n\nEach target must make a DC {DC} Wisdom saving throw. On a failed save, it pursues the course of action you described to the best of its ability. The suggested course of action can continue for the entire duration. If the suggested activity can be completed in a shorter time, the spell ends when the subject finishes what it was asked to do.\n\nYou can also specify conditions that will trigger a special activity during the duration. For example, you might suggest that a group of soldiers give all their money to the first beggar they meet. If the condition isn’t met before the spell ends, the activity isn’t performed.\n\nIf you or any of your companions damage a creature affected by this spell, the spell ends for that creature.\n\n***At Higher Levels.*** When you cast this spell using a 7th-level spell slot, the duration is 10 days. When you use an 8th-level spell slot, the duration is 30 days. When you use a 9th-level spell slot, the duration is a year and a day.", - "duration": "24 hours", - "level": 6, - "range": "60 feet", - "school": "Enchantment", - "ritual": false, - "name": "Mass Suggestion", - "components": { - "verbal": true, - "somatic": false, - "concentration": false, - "material": "a snake’s tongue and either a bit of honeycomb or a drop of sweet oil" - } - }, - { - "castingTime": "action", - "description": "You banish a creature that you can see within range into a labyrinthine demiplane. The target remains there for the duration or until it escapes the maze.\n\nThe target can use its action to attempt to escape. When it does so, it makes a DC 20 Intelligence check. If it succeeds, it escapes, and the spell ends (a minotaur or goristro demon automatically succeeds).\n\nWhen the spell ends, the target reappears in the space it left or, if that space is occupied, in the nearest unoccupied space.", - "duration": "Concentration, up to 10 minutes", - "level": 8, - "range": "60 feet", - "school": "Conjuration", - "ritual": false, - "name": "Maze", - "components": { - "verbal": true, - "somatic": true, - "concentration": true - } - }, - { - "castingTime": "action", - "description": "You step into a stone object or surface large enough to fully contain your body, melding yourself and all the equipment you carry with the stone for the duration. Using your movement, you step into the stone at a point you can touch. Nothing of your presence remains visible or otherwise detectable by nonmagical senses.\n\nWhile merged with the stone, you can’t see what occurs outside it, and any Wisdom (Perception) checks you make to hear sounds outside it are made with disadvantage. You remain aware of the passage of time and can cast spells on yourself while merged in the stone. You can use your movement to leave the stone where you entered it, which ends the spell. You otherwise can’t move.\n\nMinor physical damage to the stone doesn’t harm you, but its partial destruction or a change in its shape (to the extent that you no longer fit within it) expels you and deals 6d6 bludgeoning damage to you. The stone’s complete destruction (or transmutation into a different substance) expels you and deals 50 bludgeoning damage to you. If expelled, you fall prone in an unoccupied space closest to where you first entered.", - "duration": "8 hours", - "level": 3, - "range": "Touch", - "school": "Transmutation", - "ritual": true, - "name": "Meld into Stone", - "components": { - "verbal": true, - "somatic": true, - "concentration": false - } - }, - { - "castingTime": "1 minute", - "description": "This spell repairs a single break or tear in an object you touch, such as a broken chain link, two halves of a broken key, a torn cloak, or a leaking wineskin. As long as the break or tear is no larger than 1 foot in any dimension, you mend it. leaving no trace of the former damage.\n\nThis spell can physically repair a magic item or construct, but the spell can’t restore magic to such an object.", - "duration": "Instantaneous", - "level": 1, - "range": "Touch", - "school": "Transmutation", - "ritual": false, - "name": "Mending", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "two lodestones" - } - }, - { - "castingTime": "action", - "description": "You point your finger toward a creature within range and whisper a message. The target (and only the target) hears the message and can reply in a whisper that only you can hear.\n\nYou can cast this spell through solid objects if you are familiar with the target and know it is beyond the barrier. Magical silence. 1 foot of stone, 1 inch of common metal, a thin sheet of lead, or 3 feet of wood blocks the spell. The spell doesn’t have to follow a straight line and can travel freely around corners or through openings.", - "duration": "1 round", - "level": 0, - "range": "120 feet", - "school": "Transmutation", - "ritual": false, - "name": "Message", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "a short piece of copper wire" - } - }, - { - "castingTime": "action", - "description": "Blazing orbs of fire plummet to the ground at four different points you can see within range. Each creature in a 40-foot-radius sphere centered on each point you choose must make a DC {DC} Dexterity saving throw. The sphere spreads around corners. A creature takes 20d6 fire damage and 20d6 bludgeoning damage on a failed save, or half as much damage on a successful one. A creature in the area of more than one fiery burst is affected only once.\n\nThe spell damages objects in the area and ignites flammable objects that aren’t being worn or carried.", - "duration": "Instantaneous", - "level": 9, - "range": "1 mile", - "school": "Evocation", - "ritual": false, - "name": "Meteor Swarm", - "components": { - "verbal": true, - "somatic": true, - "concentration": false - } - }, - { - "castingTime": "action", - "description": "Until the spell ends, one willing creature you touch is immune to psychic damage, any effect that would sense its emotions or read its thoughts, divination spells, and the charmed condition. The spell even foils wish spells and spells or effects of similar power used to affect the target’s mind or to gain information about the target.", - "duration": "24 hours", - "level": 8, - "range": "Touch", - "school": "Abjuration", - "ritual": false, - "name": "Mind Blank", - "components": { - "verbal": true, - "somatic": true, - "concentration": false - } - }, - { - "castingTime": "action", - "description": "You create a sound or an image of an object within range that lasts for the duration. The illusion also ends if you dismiss it as an action or cast this spell again.\n\nIf you create a sound, its volume can range from a whisper to a scream. It can be your voice, someone else’s voice, a lion’s roar, a beating of drums, or any other sound you choose. The sound continues unabated throughout the duration, or you can make discrete sounds at different times before the spell ends.\n\nIf you create an image of an object—such as a chair, muddy footprints, or a small chest—it must be no larger than a 5-foot cube. The image can’t create sound, light, smell, or any other sensory effect. Physical interaction with the image reveals it to be an illusion, because things can pass through it.\n\nIf a creature uses its action to examine the sound or image, the creature can determine that it is an illusion with a successful Intelligence (Investigation) check against your spell save DC. If a creature discerns the illusion for what it is, the illusion becomes faint to the creature.", - "duration": "1 minute", - "level": 0, - "range": "30 feet", - "school": "Illusion", - "ritual": false, - "name": "Minor Illusion", - "components": { - "verbal": false, - "somatic": true, - "concentration": false, - "material": "a bit of fleece" - } - }, - { - "castingTime": "10 minutes", - "description": "You make terrain in an area up to 1 mile square look, sound, smell, and even feel like some other sort of terrain. The terrain’s general shape remains the same, however. Open fields or a road could be made to resemble a swamp, hill, crevasse, or some other difficult or impassable terrain. A pond can be made to seem like a grassy meadow, a precipice like a gentle slope, or a rock-strewn gully like a wide and smooth road.\n\nSimilarly, you can alter the appearance of structures, or add them where none are present. The spell doesn’t disguise, conceal, or add creatures.\n\nThe illusion includes audible, visual, tactile, and olfactory elements, so it can turn clear ground into difficult terrain (or vice versa) or otherwise impede movement through the area. Any piece of the illusory terrain (such as a rock or stick) that is removed from the spell’s area disappears immediately.\n\nCreatures with truesight can see through the illusion to the terrain’s true form; however, all other elements of the illusion remain, so while the creature is aware of the illusion’s presence, the creature can still physically interact with the illusion.", - "duration": "10 days", - "level": 7, - "range": "Sight", - "school": "Illusion", - "ritual": false, - "name": "Mirage Arcane", - "components": { - "verbal": true, - "somatic": true, - "concentration": false - } - }, - { - "castingTime": "action", - "description": "Three illusory duplicates of yourself appear in your space. Until the spell ends, the duplicates move with you and mimic your actions, shifting position so it’s impossible to track which image is real. You can use your action to dismiss the illusory duplicates.\n\nEach time a creature targets you with an attack during the spell’s duration, roll a d20 to determine whether the attack instead targets one of your duplicates.\n\nIf you have three duplicates, you must roll a 6 or higher to change the attack’s target to a duplicate. With two duplicates, you must roll an 8 or higher. With one duplicate, you must roll an 11 or higher.\n\nA duplicate’s AC equals 10 + your Dexterity modifier. If an attack hits a duplicate, the duplicate is destroyed. A duplicate can be destroyed only by an attack that hits it. It ignores all other damage and effects. The spell ends when all three duplicates are destroyed.\n\nA creature is unaffected by this spell if it can’t see, if it relies on senses other than sight, such as blindsight, or if it can perceive illusions as false, as with truesight.", - "duration": "1 minute", - "level": 2, - "range": "Self", - "school": "Illusion", - "ritual": false, - "name": "Mirror Image", - "components": { - "verbal": true, - "somatic": true, - "concentration": false - } - }, - { - "castingTime": "action", - "description": "You become invisible at the same time that an illusory double of you appears where you are standing. The double lasts for the duration, but the invisibility ends if you attack or cast a spell.\n\nYou can use your action to move your illusory double up to twice your speed and make it gesture, speak, and behave in whatever way you choose.\n\nYou can see through its eyes and hear through its ears as if you were located where it is. On each of your turns as a bonus action, you can switch from using its senses to using your own, or back again. While you are using its senses, you are blinded and deafened in regard to your own surroundings.", - "duration": "Concentration, up to 1 hour", - "level": 5, - "range": "Self", - "school": "Illusion", - "ritual": false, - "name": "Mislead", - "components": { - "verbal": false, - "somatic": true, - "concentration": true - } - }, - { - "castingTime": "bonus action", - "description": "Briefly surrounded by silvery mist, you teleport up to 30 feet to an unoccupied space that you can see.", - "duration": "Instantaneous", - "level": 2, - "range": "Self", - "school": "Conjuration", - "ritual": false, - "name": "Misty Step", - "components": { - "verbal": true, - "somatic": false, - "concentration": false - } - }, - { - "castingTime": "action", - "description": "You attempt to reshape another creature’s memories. One creature that you can see must make a DC {DC} Wisdom saving throw. If you are fighting the creature, it has advantage on the saving throw. On a failed save, the target becomes charmed by you for the duration. The charmed target is incapacitated and unaware of its surroundings, though it can still hear you. If it takes any damage or is targeted by another spell, this spell ends, and none of the target’s memories are modified.\n\nWhile this charm lasts, you can offset the target’s memory of an event that it experienced within the last 24 hours and that lasted no more than 10 minutes. You can permanently eliminate all memory of the event, allow the target to recall the event with perfect clarity and exacting detail, change its memory of the details of the event, or create a memory of some other event.\n\nYou must speak to the target to describe how its memories are affected, and it must be able to understand your language for the modified memories to take root. Its mind fills in any gaps in the details of your description. If the spell ends before you have finished describing the modified memories, the creature’s memory isn’t altered. Otherwise, the modified memories take hold when the spell ends.\n\nA modified memory doesn’t necessarily affect how a creature behaves, particularly if the memory contradicts the creature’s natural inclinations, alignment, or beliefs. An illogical modified memory, such as implanting a memory of how much the creature enjoyed dousing itself in acid, is dismissed, perhaps as a bad dream. The DM might deem a modified memory to a nonsensical to affect a creature in a significant manner.\n\nA remove curse or greater restoration spell cast on the target restores the creature’s true memory.\n\n***At Higher Levels.*** If you east this spell using a spell slot of 6th level or higher, you can alter the target’s memories of an event that took place up to 7 days ago (6th level), 30 days ago (7th level), 1 year ago (8th level), or any time in the creature’s past (9th level).", - "duration": "Concentration, up to 1 minute", - "level": 5, - "range": "30 feet", - "school": "Enchantment", - "ritual": false, - "name": "Modify Memory", - "components": { - "verbal": true, - "somatic": true, - "concentration": true - } - }, - { - "castingTime": "action", - "description": "A silvery beam of pale light shines down in a 5-foot-radius, 40-foot-high cylinder centered on a point within range. Until the spell ends, dim light fills the cylinder.\n\nWhen a creature enters the spell’s area for the first time on a turn or starts its turn there, it is engulfed in ghostly flames that cause searing pain, and it must make a DC {DC} Constitution saving throw. It takes 2d10 radiant damage on a failed save, or half as much damage on a successful one.\n\nA shapechanger makes its saving throw with disadvantage. If it fails, it also instantly reverts to its original form and can’t assume a different form until it leaves the spell’s light.\n\nOn each of your turns after you cast this spell, you can use an action to move the beam 60 feet in any direction.\n\n***At Higher Levels.*** When you cast this spell using a spell slot of 3rd level or higher, the damage increases by 1d10 for each slot level above 2nd.", - "duration": "Concentration, up to 1 minute", - "level": 2, - "range": "120 feet", - "school": "Evocation", - "ritual": false, - "name": "Moonbeam", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "several seeds of any moonseed plant and a piece of opalescent feldspar" - } - }, - { - "castingTime": "action", - "description": "You conjure a phantom watchdog in an unoccupied space that you can see within range, where it remains for the duration, until you dismiss it as an action, or until you move more than 100 feet away from it.\n\nThe hound is invisible to all creatures except you and can’t be harmed. When a Small or larger creature comes within 30 feet of it without first speaking the password that you specify when you cast this spell, the hound starts barking loudly. The hound sees invisible creatures and can see into the Ethereal Plane. It ignores illusions.\n\nAt the start of each of your turns, the hound attempts to bite one creature within 5 feet of it that is hostile to you. The hound’s attack bonus is equal to your spellcasting ability modifier + your proficiency bonus. On a hit, it deals 4d8 piercing damage.", - "duration": "8 hours", - "level": 4, - "range": "120 feet", - "school": "Abjuration", - "ritual": false, - "name": "Faithful Hound", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "a tiny silver whistle, a piece of bone, and a thread" - } - }, - { - "castingTime": "10 minutes", - "description": "You conjure an extradimensional dwelling in range that lasts for the duration. You choose where its one entrance is located. The entrance shimmers faintly and is 5 feet wide and 10 feet tall. You and any creature you designate when you cast the spell can enter the extradimensional dwelling as long as the portal remains open. You can open or close the portal if you are within 30 feet of it. While closed, the portal is invisible.\n\nBeyond the portal is a magnificent foyer with numerous chambers beyond. The atmosphere is clean, fresh, and warm.\n\nYou can create any floor plan you like, but the space can’t exceed 50 cubes, each cube being 10 feet on each side. The place is furnished and decorated as you choose. It contains sufficient food to serve a nine- course banquet for up to 100 people. A staff of 100 near-transparent servants attends all who enter. You decide the visual appearance of these servants and their attire. They are completely obedient to your orders.\n\nEach servant can perform any task a normal human servant could perform, but they can’t attack or take any action that would directly harm another creature. Thus the servants can fetch things, clean, mend, fold clothes, light tires, serve food, pour wine, and so on. The servants can go anywhere in the mansion but can’t leave it. Furnishings and other objects created by this spell dissipate into smoke if removed from the mansion. When the spell ends, any creatures inside the extradimensional space are expelled into the open spaces nearest to the entrance.", - "duration": "24 hours", - "level": 7, - "range": "300 feet", - "school": "Conjuration", - "ritual": false, - "name": "Magnificent Mansion", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "a miniature portal carved from ivory, a small piece of polished marble, and a tiny silver spoon, each item worth at least 5 gp" - } - }, - { - "castingTime": "10 minutes", - "description": "You make an area within range magically secure. The area is a cube that can be as small as 5 feet to as large as 100 feel on each side. The spell lasts for the duration or until you use an action to dismiss it.\n\nWhen you cast the spell, you decide what sort of security the spell provides, choosing any or all of the following properties: • Sound can’t pass through the barrier aI the edge of the warded area.\n\n• The barrier of the warded area appears dark and foggy, preventing vision (including darkvision) through it. Sensors created by divination spells can’t appear inside the protected area or pass through the barrier at its perimeter.\n\n• Creatures in the area can’t be targeted by divination spells.\n\n• Nothing can teleport into or out of the warded area.\n\n• Planar travel is blocked within the warded area.\n\nCasting this spell on the same spot every day for a year makes this effect permanent.\n\n***At Higher Levels.*** When you cast this spell using a spell slot of 5th level or higher, you can increase the size of the cube by 100 feet for each slot level beyond 4th. Thus you could protect a cube that can be up to 200 feet on one side by using a spell slot of 5th level.", - "duration": "24 hours", - "level": 4, - "range": "120 feet", - "school": "Abjuration", - "ritual": false, - "name": "Private Sanctum", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "a thin sheet of lead, a piece of opaque glass. A wad of cotton or cloth, and powdered chrysolite" - } - }, - { - "castingTime": "action", - "description": "You create a sword-shaped plane of force that hovers within range. It lasts for the duration. When the sword appears, you make a melee spell attack against a target of your choice within 5 feet of the sword. On a hit, the target takes 3d10 force damage. Until the spell ends, you can use a bonus action on each of your turns to move the sword up to 20 feet to a spot you can see and repeat this attack against the same target or a different one.", - "duration": "Concentration, up to 1 minute", - "level": 7, - "range": "60 feet", - "school": "Evocation", - "ritual": false, - "name": "Sword", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "a miniature platinum sword with a grip and pommel of copper and zinc, worth 250 gp" - } - }, - { - "castingTime": "action", - "description": "Choose an area of terrain no larger than 40 feet on a side within range. You can reshape dirt, sand, or clay in the area in any manner you choose for the duration. You can raise or lower the area’s elevation, create or fill in a trench, erect or flatten a wall, or form a pillar. The extent of any such changes can’t exceed half the area’s largest dimension. So, if you affect a 40-foot square, you can create a pillar up to 20 feet high, raise or lower the square’s elevation by up to 20 feet, dig a trench up to 20 feet deep, and so on. It takes 10 minutes for these changes to complete.\n\nAt the end of every 10 minutes you spend concentrating on the spell, you can choose a new area of terrain to affect.\n\nBecause the terrain’s transformation occurs slowly, creatures in the area can’t usually be trapped or injured by the ground’s movement.\n\nThis spell can’t manipulate natural stone or stone construction. Rocks and structures shift to accommodate the new terrain. If the way you shape the terrain would make a structure unstable, it might collapse.\n\nSimilarly, this spell doesn’t directly affect plant growth. The moved earth carries any plants along with it.", - "duration": "Concentration, up to 2 hours", - "level": 6, - "range": "120 feet", - "school": "Transmutation", - "ritual": false, - "name": "Move Earth", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "an iron blade and a small bag containing a mixture of soils-c1ay, loam, and sand" - } - }, - { - "castingTime": "action", - "description": "For the duration, you hide a target that you touch from divination magic. The target can be a willing creature or a place or an object no larger than 10 feet in any dimension. The target can’t be targeted by any divination magic or perceived through magical scrying sensors. ", - "duration": "8 hours", - "level": 3, - "range": "Touch", - "school": "Abjuration", - "ritual": false, - "name": "Nondetection", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "a pinch of diamond dust worth 25 gp sprinkled over the target, which the spell consumes" - } - }, - { - "castingTime": "action", - "description": "You place an illusion on a creature or an object you touch so that divination spells reveal false information about it. The target can be a willing creature or an object that isn’t being carried or worn by another creature.\n\nWhen you cast the spell, choose one or both of the following effects. The effect lasts for the duration. If you cast this spell on the same creature or object every day for 30 days, placing the same effect on it each time, the illusion lasts until it is dispelled.\n\nFalse Aura. You change the way the target appears to spells and magical effects, such as detect magic, that detect magical auras. You can make a nonmagical object appear magical, a magical object appear nonmagical, or change the object’s magical aura so that it appears to belong to a specific school of magic that you choose. When you use this effect on an object, you can make the false magic apparent to any creature that handles the item.\n\nMask. You change the way the target appears to spells and magical effects that detect creature types, such as a paladin’s Divine Sense or the trigger of a symbol spell. You choose a creature type and other spells and magical effects treat the target as if it were a creature of t hat type or of that alignment.", - "duration": "24 hours", - "level": 2, - "range": "Touch", - "school": "Illusion", - "ritual": false, - "name": "Nystul’s Magic Aura", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "a small square of silk" - } - }, - { - "castingTime": "action", - "description": "A frigid globe of cold energy streaks from your fingertips to a point of your choice within range, where it explodes in a 60-foot-radius sphere. Each creature within the area must make a DC {DC} Constitution saving throw. On a failed save, a creature takes 10d6 cold damage. On a successful save, it takes half as much damage.\n\nIf the globe strikes a body of water or a liquid that is principally water (not including water-based creatures), it freezes the liquid to a depth of 6 inches over an area 30 feet square. This ice lasts for 1 minute. Creatures that were swimming on the surface of frozen water are trapped in the ice. A trapped creature can use an action to make a Strength check against your spell save DC to break free.\n\nYou can refrain from firing the globe after completing the spell, if you wish. A small globe about the size of a sling stone, cool to the touch, appears in your hand. At any time, you or a creature you give the globe to can throw the globe (to a range of 40 feet) or hurl it with a sling (to the sling’s normal range). It shatters on impact, with the same effect as the normal casting of the spell. You can also set the globe down without shattering it. After 1 minute, if the globe hasn’t already shattered, it explodes.\n\n***At Higher Levels.*** When you cast this spell using a spell slot of 7th level or higher, the damage increases by 1d6 for each slot level above 6th.", - "duration": "Instanteous", - "level": 6, - "range": "300 feet", - "school": "Evocation", - "ritual": false, - "name": "Freezing Sphere", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "a small crystal sphere" - } - }, - { - "castingTime": "action", - "description": "A sphere of shimmering force encloses a creature or object of Large size or smaller within range. An unwilling creature must make a DC {DC} Dexterity saving throw. On a failed save. the creature is enclosed for the duration.\n\nNothing-not physical objects. energy. or other spell effects-can pass through the barrier. in or out. though a creature in the sphere can breathe there. The sphere is immune to all damage. and a creature or object inside can’t be damaged by attacks or effects originating from outside, nor can a creature inside the sphere damage anything outside it.\n\nThe sphere is weightless and just large enough to contain the creature or object inside. An enclosed creature can use its action to push against the sphere’s walls and thus roll the sphere at up to half the creature’s speed. Similarly, the globe can be picked up and moved by other creatures.\n\nA disintegrate spell targeting the globe destroys it without harming anything inside it.", - "duration": "Concentration, up to 1 minute", - "level": 4, - "range": "30 feet", - "school": "Evocation", - "ritual": false, - "name": "Resilient Sphere", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "a hemispherical piece of clear crystal and a matching hemispherical piece of gum arabic" - } - }, - { - "castingTime": "action", - "description": "Choose one creature that you can see within range. The target begins a comic dance in place: shuffling, tapping its feet, and capering for the duration. Creatures that can’t be charmed are immune to this spell.\n\nA dancing creature must use all its movement to dance without leaving its space and has disadvantage on Dexterity saving throws and attack rolls. While the target is affected by this spell, other creatures have advantage on attack rolls against it. As an action, a dancing creature makes a DC {DC} Wisdom saving throw to regain control of itself. On a successful save, the spell ends.", - "duration": "Concentration, up to 1 minute", - "level": 6, - "range": "30 feet", - "school": "Enchantment", - "ritual": false, - "name": "Irresistible Dance", - "components": { - "verbal": true, - "somatic": false, - "concentration": true - } - }, - { - "castingTime": "action", - "description": "A veil of shadows and silence radiates from you, masking you and your companions from detection. For the durationm each creature you choose within 30 feet of you (including you) has a +10 bonus to Dexterity (Stealth) checks and can’t be tracked except by magical means. A creature that receives this bonus leaves behind no tracks or other traces of its passage.", - "duration": "Concentration, up to 1 hour", - "level": 2, - "range": "Self", - "school": "Abjuration", - "ritual": false, - "name": "Pass without Trace", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "ashes from a burned leaf of mistletoe and a sprig of spruce" - } - }, - { - "castingTime": "action", - "description": "A passage appears at a point of your choice that you can see on a wooden, plaster, or stone surface (such as a wall, a ceiling, or a floor) within range, and lasts for the duration. You choose the opening’s dimensions: up to 5 feet wide, 8 feet tall, and 20 feet deep. The passage creates no instability in a structure surrounding it.\n\nWhen the opening disappears, any creatures or objects still in the passage created by the spell are safely ejected to an unoccupied space nearest to the surface on which you cast the spell.", - "duration": "1 hour", - "level": 5, - "range": "30 feet", - "school": "Transmutation", - "ritual": false, - "name": "Passwall", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "a pinch of sesame seeds" - } - }, - { - "castingTime": "action", - "description": "You tap into the nightmares of a creature you can see within range and create an illusory manifestation of its deepest fears, visible only to that creature. The target must make a DC {DC} Wisdom saving throw. On a failed save, the target becomes frightened for the duration. At the end of each of the target’s turns before the spell ends, the target must succeed on a DC {DC} Wisdom saving throw or take 4d10 psychic damage. On a successful save, the spell ends.\n\n***At Higher Levels.*** When you cast this spell using a spell slot of 5th level or higher, the damage increases by ldlO for each slot level above 4th. \n\n(Spell's description has been modified to fix the error during printing as described in the Player's Handbook errata. See http://media.wizards.com/2016/downloads/DND/PH-Errata-V1.pdf for full details)", - "duration": "Concentration, up to 1 minute", - "level": 4, - "range": "120 feet", - "school": "Illusion", - "ritual": false, - "name": "Phantasmal Killer", - "components": { - "verbal": true, - "somatic": true, - "concentration": true - } - }, - { - "castingTime": "1 minute", - "description": "A Large quasi-real, horselike creature appears on the ground in an unoccupied space of your choice within range. You decide the creature’s appearance, but it is equipped with a saddle, bit, and bridle. Any of the equipment created by the spell vanishes in a puff of smoke if it is carried more than 10 feet away from the steed.\n\nFor the duration, you or a creature you choose can ride the steed. The creature uses the statistics for a riding horse, except it has a speed of 100 feet and can travel 10 miles in an hour, or 13 miles at a fast pace. When the spell ends. the steed gradually fades. giving the rider 1 minute to dismount. The spell ends if you use an action to dismiss it or if the steed takes any damage.", - "duration": "1 hour", - "level": 3, - "range": "30 feet", - "school": "Illusion", - "ritual": true, - "name": "Phantom Steed", - "components": { - "verbal": true, - "somatic": true, - "concentration": false - } - }, - { - "castingTime": "10 minutes", - "description": "You beseech an otherworldly entity for aid. The being must be known to you: a god, a primordial, a demon prince, or some other being of cosmic power. That entity sends a celestial, an elemental, or a fiend loyal to it to aid you, making the creature appear in an unoccupied space within range. If you know a specific creature’s name, you can speak that name when you cast this spell to request that creature, though you might get a different creature anyway (DM’s choice).\n\nWhen the creature appears, it is under no compulsion to behave in any particular way. You can ask the creature to perform a service in exchange for payment, but it isn’t obliged to do so. The requested task could range from simple (fly us across the chasm, or help us fight a battle) to complex (spy on our enemies, or protect us during our foray into the dungeon). You must be able to communicate with the creature to bargain for its services.\n\nPayment can take a variety of forms. A celestial might require a sizable donation of gold or magic items to an allied temple, while a fiend might demand a living sacrifice or a gift of treasure. Some creatures might exchange their service for a quest undertaken by you.\n\nAs a rule of thumb, a task that can be measured in minutes requires a payment worth 100 gp per minute. A task measured in hours requires 1,000 gp per hour. And a task measured in days (up to 10 days) requires 10,000 gp per day. The DM can adjust these payments based on the circumstances under which you cast the spell. If the task is aligned with the creature’s ethos, the payment might be halved or even waived. Nonhazardous tasks typically require only half the suggested payment, while especially dangerous tasks might require a greater gift. Creatures rarely accept tasks that seem suicidal.\n\nAfter the creature completes the task, or when the agreed-upon duration of service expires, the creature returns to its home plane after reporting back to you, if appropriate to the task and if possible. If you are unable to agree on a price for the creature’s service, the creature immediately returns to its home plane.\n\nA creature enlisted to join your group counts as a member of it, receiving a full share of experience points awarded.", - "duration": "Instantaneous", - "level": 6, - "range": "60 feet", - "school": "Conjuration", - "ritual": false, - "name": "Planar Ally", - "components": { - "verbal": true, - "somatic": true, - "concentration": false - } - }, - { - "castingTime": "1 hour", - "description": "With this spell, you attempt to bind a celestial, an elemental, a fey, or a fiend to your service. The creature must be within range for the entire casting of the spell. (Typically. the creature is first summoned into the center of an inverted magic circle in order to keep it trapped while this spell is cast.) At the completion of the casting, the target must make a DC {DC} Charisma saving throw. On a failed save, it is bound to serve you for the duration. If the creature was summoned or created by another spell, that spell’s duration is extended to match the duration of this spell.\n\nA bound creature must follow your instructions to the best of its ability. You might command the creature to accompany you on an adventure. to guard a location. or to deliver a message. The creature obeys the letter of your instructions. but if the creature is hostile to you. it strives to twist your words to achieve its own objectives. If the creature carries out your instructions completely before the spell ends, it travels to you to report this fact if you are on the same plane of existence. If you are on a different plane of existence. it returns to the place where you bound it and remains there until the spell ends.\n\n***At Higher Levels.*** When you cast this spell using a spell slot of a higher level, the duration increases to 10 days with a 6th-level slot, to 30 days with a 7th-level slot. to 180 days with an 8th-level slot. and to a year and a day with a 9th-level spell slot.", - "duration": "24 hours", - "level": 5, - "range": "60 feet", - "school": "Abjuration", - "ritual": false, - "name": "Planar Binding", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "a jewel worth at least 1,000 GP, which the spell consumes" - } - }, - { - "castingTime": "1 action ", - "description": "You and up to eight willing creatures who link hands in a circle are transported to a different plane of existence. You can specify a target destination in general terms, such as the City of Brass on the Elemental Plane of Fire or the palace of Dispater on the second level of the Nine Hells, and you appear in or near that destination. If you are trying to reach the City of Brass, for example, you might arrive in its Street of Steel, before its Gate of Ashes, or looking at the city from across the Sea of Fire, at the DM’s discretion.\n\nAlternatively, if you know the sigil sequence of a teleportation circle on another plane of existence, this spell can take you to that circle. If the teleportation circle is too small to hold all the creatures you transported, they appear in the closest unoccupied spaces next to the circle.\n\nYou can use this spell to banish an unwilling creature to another plane. Choose a creature within your reach and make a melee spell attack against it. On a hit, the creature must make a DC {DC} Charisma saving throw. If the creature fails this save, it is transported to a random location on the plane of existence you specify. A creature so transported must find its own way back to your current plane of existence.", - "duration": "Instantaneous", - "level": 7, - "range": "Touch", - "school": "Conjuration", - "ritual": false, - "name": "Plane Shift", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "a forked, metal rod worth at least 250 gp, attuned to a particular plane of existence" - } - }, - { - "castingTime": "1 action or 8 hours", - "description": "This spell channels vitality into plants within a specific area. There are two possible uses for the spell, granting either immediate or long-term benefits.\n\nIf you cast this spell using 1 action, choose a point within range. All normal plants in a 100-foot radius centered on that point become thick and overgrown. A creature moving through the area must spend 4 feet of movement for every 1 foot it moves.\n\nYou can exclude one or more areas of any size within the spell’s area from being affected.\n\nIf you cast this spell over 8 hours, you enrich the land. All plants in a half-mile radius centered on a point within range become enriched for 1 year. The plants yield twice the normal amount of food when harvested.", - "duration": "Instantaneous", - "level": 3, - "range": "150 feet", - "school": "Transmutation", - "ritual": false, - "name": "Plant Growth", - "components": { - "verbal": true, - "somatic": true, - "concentration": false - } - }, - { - "castingTime": "action", - "description": "You extend your hand toward a creature you can see within range and project a puff of noxious gas from your palm. The creature must succeed on a DC {DC} Constitution saving throw or take 1d12 poison damage.\n\nThis spell’s damage increases by 1d12 when you reach 5th level (2d12), 11th level (3d12), and 17th level (4d12).", - "duration": "Instantaneous", - "level": 0, - "range": "10 feet", - "school": "Conjuration", - "ritual": false, - "name": "Poison Spray", - "components": { - "verbal": true, - "somatic": true, - "concentration": false - }, - "attacks": [ - { - "attackBonus": "Con save", - "details": "range 10 feet", - "damage": "{floor((Level+1)/6)+1}d12", - "damageType": "poison" - } - ] - }, - { - "castingTime": "action", - "description": "This spell transforms a creature that you can see within range into a new form. An unwilling creature must make a DC {DC} Wisdom saving throw to avoid the effect. A shapechanger automatically succeeds on this saving throw. \n\nThe transformation lasts for the duration, or until the target drops to 0 hit points or dies. The new form can be any beast whose challenge rating is equal to or less than the target’s (or the target’s level, if it doesn’t have a challenge rating). The target’s game statistics, including mental ability scores, are replaced by the statistics of the chosen beast. It retains its alignment and personality.\n\nThe target assumes the hit points of its new form. When it reverts to its normal form, the creature returns to the number of hit points it had before it transformed. If it reverts as a result of dropping to 0 hit points, any excess damage carries over to its normal form. As long as the excess damage doesn’t reduce the creature’s normal form to 0 hit points, it isn’t knocked unconscious.\n\nThe creature is limited in the actions it can perform by the nature of its new form, and it can’t speak, cast spells, or take any other action that requires hands or speech.\n\nThe target’s gear melds into the new form. The creature can’t activate, use, wield, or otherwise benefit from any of its equipment. This spell can’t affect a target that has 0 hit points. \n\n(Spell's description has been modified to fix the error during printing as described in the Player's Handbook errata. See http://media.wizards.com/2016/downloads/DND/PH-Errata-V1.pdf for full details)", - "duration": "Concentration, up to 1 hour", - "level": 4, - "range": "60 feet", - "school": "Transmutation", - "ritual": false, - "name": "Polymorph", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "a caterpillar cocoon" - } - }, - { - "castingTime": "action", - "description": "You utter a word of power that can compel one creature you can see within range to die instantly. If the creature you choose has 100 hit points or fewer, it dies. Otherwise, the spell has no effect.", - "duration": "Instantaneous", - "level": 9, - "range": "60 feet", - "school": "Enchantment", - "ritual": false, - "name": "Power Word Kill", - "components": { - "verbal": true, - "somatic": false, - "concentration": false - } - }, - { - "castingTime": "action", - "description": "You speak a word of power that can overwhelm the mind of one creature you can see within range, leaving it dumbfounded. If the target has 150 hit points or fewer, it is stunned. Otherwise, the spell has no effect.\n\nThe stunned target must make a DC {DC} Constitution saving throw at the end of each of its turns. On a successful save, this stunning effect ends.", - "duration": "Instantaneous", - "level": 8, - "range": "60 feet", - "school": "Enchantment", - "ritual": false, - "name": "Power Word Stun", - "components": { - "verbal": true, - "somatic": false, - "concentration": false - } - }, - { - "castingTime": "10 minutes", - "description": "Up to six creatures of your choice that you can see within range each regain hit points equal to 2d8 + your spellcasting ability modifier. This spell has no effect on undead or constructs.\n\n***At Higher Levels.*** When you cast this spell using a spell slot of 3rd level or higher, the healing increases by 1d8 for each slot level above 2nd.", - "duration": "Instantaneous", - "level": 2, - "range": "30 feet", - "school": "Evocation", - "ritual": false, - "name": "Prayer of Healing", - "components": { - "verbal": true, - "somatic": false, - "concentration": false - } - }, - { - "castingTime": "action", - "description": "This spell is a minor magical trick that novice spellcasters use for practice. You create one of the following magical effects within range:\n\n• You create an instantaneous, harmless sensory effect, such as a shower of sparks, a puff of wind, faint musi- cal notes, or an odd odor.\n\n• You instantaneously light or snuff out a candle, a torch, or a small campfire.\n\n• You instantaneously clean or soil an object no larger than 1 cubic foot.\n\n• You chill, warm, or flavor up to 1 cubic foot of nonliv- ing material for 1 hour.\n\n• You make a color, a small mark, or a symbol appear on an object or a surface for 1 hour.\n\n• You create a nonmagical trinket or an illusory image that can fit in your hand and that lasts until the end of your next turn.\n\nIf you cast this spell multiple times, you can have up to three of its non-instantaneous effects active at a time, and you can dismiss such an effect as an action.", - "duration": "Up to 1 hour", - "level": 0, - "range": "10 feet", - "school": "Transmutation", - "ritual": false, - "name": "Prestidigitation", - "components": { - "verbal": true, - "somatic": true, - "concentration": false - } - }, - { - "castingTime": "action", - "description": "Eight multicolored rays of light flash from your hand. Each ray is a different color and has a different power and purpose. Each creature in a 60-foot cone must make a DC {DC} Dexterity saving throw. For each target. roll a d8 to determine which color ray affects it.\n\n1. Red. The target takes 10d6 tire damage on a failed save. or half as much damage on a successful one.\n\n2. Orange. The target takes 10d6 acid damage on a failed save. or half as much damage on a successful one.\n\n3. Yellow. The target takes 10d6 lightning damage on a failed save. or half as much damage on a successful one.\n\n4. Green. The target takes 10d6 poison damage on a failed save. or half as much damage on a successful one.\n\n5. Blue. The target takes 10d6 cold damage on a failed save, or half as much damage on a successful one.\n\n6.Indigo. On a failed save. the target is restrained. It must then make a DC {DC} Constitution saving throw at the end of each of its turns. If it successfully saves three times. the spell ends. If it fails its save three times, it permanently turns to stone and is subjected to the petrified condition. The successes and failures don’t need to be consecutive; keep track of both until the target collects three of a kind.\n\n7. Violet, On a failed save, the target is blinded. lt must then make a DC {DC} Wisdom saving throw at the start of your next turn. A successful save ends the blindness. lf it fails that save, the creature is transported to another plane of existence of the DM’s choosing and is no longer blinded. (Typically, a creature that is on a plane that isn’t its home plane is banished home. while other creatures are usually cast into the Astral or Ethereal planes.)\n\n8. Special. The target is struck by two rays. Roll twice more, rerolling any 8.", - "duration": "Instanataneous", - "level": 7, - "range": "Self (60-foot cone)", - "school": "Evocation", - "ritual": false, - "name": "Prismatic Spray", - "components": { - "verbal": true, - "somatic": true, - "concentration": false - } - }, - { - "castingTime": "action", - "description": "A shimmering, multicolored plane of light forms a vertical opaque wall-up to 90 feet long, 30 feet high, and 1 inch thick-centered on a point you can see within range. Alternatively, you can shape the wall into a sphere up to 30 feet in diameter centered on a point you choose within range. The wall remains in place for the duration. If you position the wall so that it passes through a space occupied by a creature, the spell fails, and your action and the spell slot are wasted.\n\nThe wall sheds bright light out to a range of 100 feet and dim light for an additional 100 feet. You and creatures you designate at the time you cast the spell can pass through and remain near the wall without harm. If another creature that can see the wall moves to within 20 feet of it or starts its turn there. the creature must succeed on a DC {DC} Constitution saving throw or become blinded for 1 minute.\n\nThe wall consists of seven layers, each with a different color. When a creature attempts to reach into or pass through the wall, it does so one layer at a time through all the wall’s layers. As it passes or reaches through each layer, the creature must make a DC {DC} Dexterity saving throw or be affected by that layer’s properties as described below.\n\nThe wall can be destroyed, also one layer at a time, in order from red to violet, by means specific to each layer. Once a layer is destroyed, it remains so for the duration of the spell. A rod of cancellation destroys a prismatic wall, but an antimagic field has no effect on it. \n\n1. Red. The creatures takes 10d6 fire damage on a failed save, or half as much damage on a successful one. While this layer is in place, nonmagical ranged attacks can’t pass through the wall. The layer can be destroyed by dealing at least 25 cold damage to it.\n\n2. Orange. The creatures takes 10d6 acid damage on a failed save, or half as much damage on a successful one. While this layer is in place, magical ranged attacks can’t pass through the wall. The layer is destroyed by a strong wind.\n\n3. Yellow. The creature takes 10d6 lightning damage on a failed save, or half as much damage on a successful one. This layer can be destroyed by dealing at least 60 force damage to it.\n\n4. Green. The creature takes 10d6 poison damage on a failed save, or half as much damage on a successful one. A passwall spell, or another spell of equal or greater level that can open a portal on a solid surface, destroys this layer.\n\n5. Blue. The creatures takes 10d6 cold damage on a failed save, or half as much damage on a successful one. This layer can be destroyed by dealing at least 25 fire damage to it.\n\n6. Indigo. On a failed save, the creature is restrained. It must then make a DC {DC} Constitution saving throw at the end of each of its turns. If it successfully saves three times, the spell ends. If it fails its save three times, it permanently turns to stone and is subjected to the petrified condition. The successes and failures don’t need to be consecutive; keep track of both until the creature collects three of a kind.\n\nWhile this layer is in place, spells can’t be cast through the wall. The layer is destroyed by bright light shed by a daylight spell or a similar spell of equal or higher level.\n\n7. Violet. On a failed save, the creature is blinded. It must then make a DC {DC} Wisdom saving throw at the start of your next turn. A successful save ends the blindness. If it fails that save, the creature is transported to another plane of the DM’s choosing and is no longer blinded. (Typically, a creature that is on a plane that isn’t its home plane is banished home, while other creatures are usually cast into the Astral or Ethereal planes.) This layer is destroyed by a dispel magic spell or a similar spell of equal or higher level that can end spells and magical effects.", - "duration": "10 minutes", - "level": 9, - "range": "60 feet", - "school": "Abjuration", - "ritual": false, - "name": "Prismatic Wall", - "components": { - "verbal": true, - "somatic": true, - "concentration": false - } - }, - { - "castingTime": "action", - "description": "A flickering flame appears in your hand. The flame remains there for the duration and harms neither you nor your equipment. The flame sheds bright light in a 10-foot radius and dim light for an additional 10 feet. The spell ends if you dismiss it as an action or if you cast it again.\n\nYou can also attack with the flame, although doing so ends the spell. When you cast this spell, or as an action on a later turn, you can hurl the flame at a creature within 30 feet of you. Make a ranged spell attack. On a hit, the target takes 1d8 fire damage.\n\nThis spell’s damage increases by 1d8 when you reach 5th level (2d8), 11th level (3d8), and 17th level(4d8). ", - "duration": "10 minutes", - "level": 0, - "range": "Self", - "school": "Conjuration", - "ritual": false, - "name": "Produce Flame", - "components": { - "verbal": true, - "somatic": true, - "concentration": false - }, - "attacks": [ - { - "details": "range 30 feet", - "damage": "{floor((Level+1)/6)+1}d8", - "damageType": "fire" - } - ] - }, - { - "castingTime": "action", - "description": "You create a illusion of an object, a creature, or some other visible phenomenon within range that activates when a specific condition occurs. The illusion is imperceptible until then. It must be no larger than a 30-foot cube, and you decide when you cast the spell how the illusion behaves and what sounds it makes. This scripted performance can last up to 5 minutes.\n\nWhen the condition you specify occurs, the illusion springs into existence and performs in the manner you described. Once the illusion finishes performing, it disappears and remains dormant for 10 minutes. After this time, the illusion can be activated again.\n\nThe triggering condition can be as general or as detailed as you like, though it must be based on visual or audible conditions that occur within 30 feet of the area. For example, you could create an illusion of yourself to appear and warn off others who attempt to open a trapped door, or you could set the illusion to trigger only when a creature says the correct word or phrase.\n\nPhysical interaction with the image reveals it to be an illusion, because things can pass through it. A creature that uses its action to examine the image can determine that it is an illusion with a successful Intelligence (Investigation) check against your spell save DC. If a creature discerns the illusion for what it is, the creature can see through the image, and any noise it makes sounds hollow to the creature.", - "duration": "Until Dispelled", - "level": 6, - "range": "120 feet", - "school": "Illusion", - "ritual": false, - "name": "Programmed Illusion", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "a bit of fleece and jade dust worth at least 25 gp" - } - }, - { - "castingTime": "action", - "description": "You create an illusory copy of yourself that lasts for the duration. The copy can appear at any location within range that you have seen before, regardless of intervening obstacles. The illusion looks and sounds like you but is intangible. If the illusion takes any damage, it disappears, and the spell ends.\n\nYou can use your action to move this illusion up to twice your speed, and make it gesture, speak, and behave in whatever way you choose. It mimics your mannerisms perfectly.\n\nYou can see through its eyes and hear through its ears as if you were in its space. On your turn as a bonus action, you can switch from using its senses to using your own, or back again. While you are using its senses, you are blinded and deafened in regard to your own surroundings.\n\nPhysical interaction with the image reveals it to be an illusion, because things can pass through it. A creature that uses its action to examine the image can determine that it is an illusion with a successful Intelligence (Investigation) check against your spell save DC. If a creature discerns the illusion for what it is, the creature can see through the image, and any noise it makes sounds hollow to the creature.", - "duration": "Concentration, up to 1 day", - "level": 7, - "range": "500 miles", - "school": "Illusion", - "ritual": false, - "name": "Project Image", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "a small replica of you made from materials worth at least 5 gp" - } - }, - { - "castingTime": "action", - "description": "For the duration, the willing creature you touch has resistance to one damage type of your choice: acid, cold, fire, lightning, or thunder.", - "duration": "Concentration, up to 1 hour", - "level": 3, - "range": "Touch", - "school": "Abjuration", - "ritual": false, - "name": "Protection from Energy", - "components": { - "verbal": true, - "somatic": true, - "concentration": true - } - }, - { - "castingTime": "action", - "description": "Until the spell ends, one willing creature you touch is protected against certain types of creatures: aberrations, celestiais, elementals, fey, fiends, and undead.\n\nThe protection grants several benefits. Creatures of those types have disadvantage on attack rolls against the target. The target also can’t be charmed, frightened, or possessed by them. If the target is already charmed, frightened, or possessed by such a creature, the target has advantage on any new saving throw against the relevant effect.", - "duration": "Concentration, up to 10 minutes", - "level": 1, - "range": "Touch", - "school": "Abjuration", - "ritual": false, - "name": "Protection from Evil and Good", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "holy water or powdered silver and iron, which the spell consumes" - } - }, - { - "castingTime": "action", - "description": "You touch a creature.If it is poisoned, you neutralize the poison. If more than one poison afflicts the target, you neutralize one poison that you know is present, or you neutralize one at random.\n\nFor the duration, the target has advantage on saving throws against being poisoned, and it has resistance to poison damage.", - "duration": "1 hour", - "level": 2, - "range": "Touch", - "school": "Abjuration", - "ritual": false, - "name": "Protection from Poison", - "components": { - "verbal": true, - "somatic": true, - "concentration": false - } - }, - { - "castingTime": "action", - "description": "All nonmagical food and drink within a 5-foot-radius sphere centered on a point of your choice within range is purified and rendered free of poison and disease.", - "duration": "Instantaneous", - "level": 1, - "range": "10 feet", - "school": "Transmutation", - "ritual": true, - "name": "Purify Food and Drink", - "components": { - "verbal": true, - "somatic": true, - "concentration": false - } - }, - { - "castingTime": "1 hour", - "description": "You return a dead creature you touch to life, provided that it has been dead no longer than 10 days. If the creature’s soul is both willing and at liberty to rejoin the body, the creature returns to life with 1 hit point.\n\nThis spell also neutralizes any poisons and cures nonmagical diseases that affected the creature at the time it died. This spell doesn’t, however, remove magical diseases, curses, or similar effects; if these aren’t first removed prior to casting the spell, they take effect when the creature returns to life. The spell can’t return an undead creature to life. \n\nThis spell closes all mortal wounds, but it doesn’t restore missing body parts. If the creature is lacking body parts or organs integral for its survival—its head, for instance—the spell automatically fails. Coming back from the dead is an ordeal. The target takes a −4 penalty to all attack rolls, saving throws, and ability checks. Every time the target finishes a long rest, the penalty is reduced by 1 until it disappears.", - "duration": "Instantaneous", - "level": 5, - "range": "Touch", - "school": "Necromancy", - "ritual": false, - "name": "Raise Dead", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "a diamond worth at least 500 gp, which the spell consumes" - } - }, - { - "castingTime": "action", - "description": "You forge a telepathic link among up to eight willing creatures of your choice within range, psychically linking each creature to all the others for the duration. Creatures with Intelligence scores of 2 or less aren’t affected by this spell.\n\nUntil the spell ends, the targets can communicate telepathically through the bond whether or not they have a common language. The communication is possible over any distance, though it can’t extend to other planes of existence.", - "duration": "1 hour", - "level": 5, - "range": "30 feet", - "school": "Divination", - "ritual": true, - "name": "Telepathic Bond", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "pieces of egg shell from two different kinds of creatures" - } - }, - { - "castingTime": "action", - "description": "A frigid beam of blue-white light streaks toward a creature within range. Make a ranged spell attack against the target. On a hit, it takes 1d8 cold damage, and its speed is reduced by 10 feet until the start of your next turn.\n\nThe spell’s damage increases by 1d8 when you reach 5th level (2d8), 11th level (3d8), and 17th level (4d8).", - "duration": "Instantaneous", - "level": 0, - "range": "60 feet", - "school": "Evocation", - "ritual": false, - "name": "Ray of Frost", - "components": { - "verbal": true, - "somatic": true, - "concentration": false - }, - "attacks": [ - { - "details": "range 60 feet, target speed reduced by 10ft on hit", - "damage": "{floor((Level+1)/6)+1}d8", - "damageType": "cold" - } - ] - }, - { - "castingTime": "1 minute", - "description": "You touch a creature and stimulate its natural healing ability. The target regains 4d8 + 15 hit points. For the duration of the spell, the target regains 1 hit point at the start of each of its turns (10 hit points each minute).\n\nThe target’s severed body members (fingers, legs, tails, and so on), if any, are restored after 2 minutes. If you have the severed part and hold it to the stump, the spell instantaneously causes the limb to knit to the stump.", - "duration": "1 hour", - "level": 7, - "range": "Touch", - "school": "Transmutation", - "ritual": false, - "name": "Regenerate", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "a prayer wheel and holy water" - } - }, - { - "castingTime": "1 hour", - "description":"You touch a dead humanoid or a piece of a dead humanoid. Provided that the creature has been dead no longer than 10 days, the spell forms a new adult body for it and then calls the soul to enter that body. If the target’s soul isn’t free or willing to do so, the spell fails.\n\nThe magic fashions a new body for the creature to inhabit, which likely causes the creature’s race to change. The DM rolls a d100 and consults the following table to determine what form the creature takes when restored to life, or the DM chooses a form.\n\nd100 | Race\n---|---\n01-04 | Dragonborn\n05-13 | Dwarf, hill\n14-21 | Dwarf, mountain\n22-25 | Elf, dark\n26–34 | Elf, high\n35-42 | Elf, wood\n43-46 | Gnome, forest\n47-52 | Gnome, Rock\n53-56 | Half-elf\n57-60 | Half Orc,61-68 - Halfling, lightfoot\n69-76 | Halfling, stout\n77-96 | Human\n97-00 | Tiefling\n\nThe reincarnated creature recalls its former life and experiences. It retains the capabilities it had in its original form, except it exchanges its original race for the new one and changes its racial traits accordingly. ", - "duration": "Instantaneous", - "level": 5, - "range": "Touch", - "school": "Transmutation", - "ritual": false, - "name": "Reincarnate", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "rare oils and unguents worth at least 1,000 gp, which the spell consumes" - } - }, - { - "castingTime": "action", - "description": "At your touch, all curses affecting one creature or object end. If the object is a cursed magic item, its curse remains, but the spell breaks its owner’s attunement to the object so it can be removed or discarded.", - "duration": "Instantaneous", - "level": 3, - "range": "Touch", - "school": "Abjuration", - "ritual": false, - "name": "Remove Curse", - "components": { - "verbal": true, - "somatic": true, - "concentration": false - } - }, - { - "castingTime": "action", - "description": "You touch one willing creature. Once before the spell ends, the target can roll a d4 and add the number rolled to one saving throw of its choice. It can roll the die before or after making the saving throw. The spell then ends.", - "duration": "Concentration, up to 1 minute", - "level": 0, - "range": "Touch", - "school": "Abjuration", - "ritual": false, - "name": "Resistance", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "a miniature cloak" - } - }, - { - "castingTime": "1 hour", - "description": "You touch a dead creature that has been dead for no more than a century, that didn’t die of old age, and that isn’t undead. If its soul is free and willing, the target returns to life with all its hit points.\n\nThis spell neutralizes any poisons and cures normal diseases afflicting the creature when it died. It doesn’t, however, remove magical diseases, curses, and the like; if such effects aren’t removed prior to casting the spell, they afflict the target on its return to life.\n\nThis spell closes all mortal wounds and restores any missing body parts.\n\nComing back from the dead is an ordeal. The target takes a −4 penalty to all attack rolls, saving throws, and ability checks. Every time the target finishes a long rest, the penalty is reduced by 1 until it disappears.\n\nCasting this spell to restore life to a creature that has been dead for one year or longer taxes you greatly. Until you finish a long rest, you can’t cast spells again, and you have disadvantage on all attack rolls, ability checks, and saving throws.", - "duration": "Instantaneous", - "level": 7, - "range": "Touch", - "school": "Necromancy", - "ritual": false, - "name": "Resurrection", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "a diamond worth at least 1,000 gp, which the spell consumes" - } - }, - { - "castingTime": "action", - "description": "This spell reverses gravity in a 50-foot-radius, 100-foot high cylinder centered on a point within range. All creatures and objects that aren’t somehow anchored lo the ground in the area fall upward and reach the top of the area when you cast this spell. A creature can make a DC {DC} Dexterity saving throw to grab onto a fixed object it can reach, thus avoiding the fall.\n\nIf some solid object (such as a ceiling) is encountered in this fall, faIling objects and creatures strike it just as they would during a normal downward fall. If an object or creature reaches the top of the area without striking anything, it remains there, oscillating slightly, for the duration.\n\nAt the end of the duration, affected objects and creatures fall back down.", - "duration": "Concentration, up to 1 minute", - "level": 7, - "range": "100 feet", - "school": "Transmutation", - "ritual": false, - "name": "Reverse Gravity", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "a lodestone and iron fillings" - } - }, - { - "castingTime": "action", - "description": "You touch a creature that has died within the last minute. That creature returns to life with 1 hit point. This spell can’t return to life a creature that has died of old age, nor can it restore any missing body parts.\n\n(Spell school has been modified to fix the error during printing as described in the Player's Handbook errata. See http://media.wizards.com/2016/downloads/DND/PH-Errata-V1.pdf for full details)", - "duration": "Instantaneous", - "level": 3, - "range": "Touch", - "school": "Necromancy", - "ritual": false, - "name": "Revivify", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "diamonds worth 300 gp, which the spell consumes" - } - }, - { - "castingTime": "action", - "description": "You touch a length of rope that is up lo 60 feet long. One end of the rope then rises into the air until the whole rope hangs perpendicular to the ground. At the upper end of the rope, an invisible entrance opens to an extradimensional space that lasts until the spell ends.\n\nThe extradimensional space can be reached by climbing to the top of the rope. The space can hold as many as eight Medium or smaller creatures. The rope can be pulled into the space, making the rope disappear from view outside the space.\n\nAttacks and spells can’t cross through the entrance into or out of the extradimensional space, but those inside can see out of it as if through a 3-foot-by-5-foot window centered on the rope.\n\nAnything inside the extradimensional space drops out when the spell ends.", - "duration": "1 hour", - "level": 2, - "range": "Touch", - "school": "Transmutation ", - "ritual": false, - "name": "Rope Trick", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "powdered corn extract and a twisted loop of parchment" - } - }, - { - "castingTime": "action", - "description": "Flame-like radiance descends on a creature that you can see within range. The target must succeed on a DC {DC} Dexterity saving throw or take 1d8 radiant damage. The target gains no benefit from cover for this saving throw.\n\nThe spell’s damage increases by 1d8 when you reach 5th level (2d8), 11th level (3d8), and 17th level (4d8).", - "duration": "Instantaneous", - "level": 0, - "range": "60 feet", - "school": "Evocation", - "ritual": false, - "name": "Sacred Flame", - "components": { - "verbal": true, - "somatic": true, - "concentration": false - }, - "attacks": [ - { - "attackBonus": "Dex save", - "details": "range 60 feet, target gains no benefit from cover", - "damage": "{floor((Level+1)/6)+1}d8", - "damageType": "radiant" - } - ] - }, - { - "castingTime": "bonus action", - "description": "You ward a creature within range against attack. Until the spell ends, any creature who targets the warded creature with an attack or a harmful spell must first make a DC {DC} Wisdom saving throw. On a failed save, the creature must choose a new target or lose the attack or spell. This spell doesn’t protect the warded creature from area effects, such as the explosion of a fireball.\n\nIf the warded creature makes an attack or casts a spell that affects an enemy creature, this spell ends.", - "duration": "1 minute", - "level": 1, - "range": "30 feet", - "school": "Abjuration", - "ritual": false, - "name": "Sanctuary", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "a small silver mirror" - } - }, - { - "castingTime": "1 action ", - "description": "You create three rays of fire and hurl them at targets within range. You can hurl them at one target or several.\n\nMake a ranged spell attack for each ray. On a hit, the target takes 2d6 fire damage.\n\n***At Higher Levels.*** When you cast this spell using a spell slot of 3rd level or higher. you create one additional ray for each slot level above 2nd.", - "duration": "Instantaneous", - "level": 2, - "range": "120 feet", - "school": "Evocation", - "ritual": false, - "name": "Scorching Ray", - "components": { - "verbal": true, - "somatic": true, - "concentration": false - } - }, - { - "castingTime": "10 minutes", - "description": "You can see and hear a particular creature you choose that is on the same plane of existence as you. The target must make a DC {DC} Wisdom saving throw, which is modified by how well you know the target and the sort of physical connection you have to it. If a target knows you’re casting this spell, it can fail the saving throw voluntarily if it wants to be observed.\n\nKnowledge | Save Modifier\n---|---\nSecondhand (you have heard of the target) | +5\nFirsthand (you have met the target) | +0\nFamiliar (you know the target well) | -5\n\nConnection | Save Modifier\n---|---\nLikeness or picture | -2\nPossession or garment | -4\nBody part, lock of hair, bit of nail, or the like | -10\n\nOn a successful save. the target isn’t affected. and you can’t use this spell against it again for 24 hours.\n\nOn a failed save. the spell creates an invisible sensor within 10 feet of the target. You can see and hear through the sensor as if you were there. The sensor moves with the target, remaining within 10 feet of it for the duration. A creature that can see invisible objects sees the sensor as a luminous orb about the size of your fist.\n\nInstead of targeting a creature. you can choose a location you have seen before as the target of this spell. When you do, the sensor appears al that location and doesn’t move. ", - "duration": "Concentration, up to 10 minutes", - "level": 5, - "range": "Self", - "school": "Divination", - "ritual": false, - "name": "Scrying", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "a focus worth at least 1.000 gp, such as a crystal ball, a silver mirror, or a font filled with holy water" - } - }, - { - "castingTime": "action", - "description": "For the duration, you see invisible creatures and objects as if they were visible, and you can see into the Ethereal Plane. Ethereal creatures and objects appear ghostly and translucent.", - "duration": "1 hour", - "level": 2, - "range": "Self", - "school": "Divination", - "ritual": false, - "name": "See Invisibility", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "a pinch of tale and a small sprinkling ofpowdered silver" - } - }, - { - "castingTime": "action", - "description": "This spell allows you to change the appearance of any number of creatures that you can see within range. You give each target you choose a new, illusory appearance. An unwilling target can make a DC {DC} Charisma saving throw, and if it succeeds, it is unaffected by this spell. The spell disguises physical appearance as well as clothing, armor, weapons, and equipment. You can make each creature se em I foot shorter or taller and appear thin, fat, or in between. You can’t change a target’s body type, so you must choose a form that has the same basic arrangement of limbs. Otherwise, the extent of the illusion is up to you. The spell lasts for the duration, unless you use your action to dismiss it sooner.\n\nThe changes wrought by this spell fail to hold up to physical inspection. For example, if you use this spell to add a hat to a creature’s outtit, objects pass through the hat, and anyone who touches it would feel nothing or would feel the creature’s head and hair. If you use this spell to appear thinner than you are, the hand of someone who reaches out to touch you would bump into you while it was seemingly still in midair.\n\nA creature can use its action to inspect a target and make an Intelligence (Investigation) check against your spell save DC. If it succeeds, it becomes aware that the target is disguised. ", - "duration": "8 hours", - "level": 5, - "range": "30 feet", - "school": "Illusion", - "ritual": false, - "name": "Seeming", - "components": { - "verbal": true, - "somatic": true, - "concentration": false - } - }, - { - "castingTime": "action", - "description": "You send a short message of twenty-five words or less to a creature with which you are familiar. The creature hears the message in its mind, recognizes you as the sender if it knows you, and can answer in a like manner immediately. The spell enables creatures with Intelligence scores of at least 1 to understand the meaning of your message.\n\nYou can send the message across any distance and even to other planes of existence, but if the target is on a different plane than you, there is a 5 percent chance that the message doesn’t arrive. ", - "duration": "1 round", - "level": 3, - "range": "Unlimited", - "school": "Evocation", - "ritual": false, - "name": "Sending", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "a short piece of fine copper wire" - } - }, - { - "castingTime": "action", - "description": "By means of this spell, a willing creature or an object can be hidden away, safe from detection for the duration. When you cast the spell and touch the target, it becomes invisible and can’t be targeted by divination spells or perceived through scrying sensors created by divination spells.\n\nIf the target is a creature, it falls into a state of suspended animation. Time ceases to flow for it, and it doesn’t grow older.\n\nYou can set a condition for the spell to end early. The condition can be anything you choose, but it must occur or be visible within 1 mile of the target. Examples include “after 1,000 years“ or “when the tarrasque awakens.“ This spell also ends if the target takes any damage.", - "duration": "Until dispelled", - "level": 7, - "range": "Touch", - "school": "Transmutation", - "ritual": false, - "name": "Sequester", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "a powder composed of diamond, emerald, ruby, and sapphire dust worth at least 5,000 gp, which the spell consumes" - } - }, - { - "castingTime": "action", - "description": "You assume the form of a different creature for the duration. The new form can be of any creature with a challenge rating equal to your level or lower. The creature can’t be a construct or an undead, and you must have seen the sort of creature at least once. You transform into an average example of that creature, one without any class levels or the Spellcasting trait.\n\nYour game statistics are replaced by the statistics of the chosen creature, though you retain your alignment and Intelligence, Wisdom, and Charisma scores. You also retain all of your skill and saving throw proficiencies, in addition to gaining those of the creature. If the creature has the same proficiency as you and the bonus listed in its statistics is higher than yours, use the creature’s bonus in place of yours. You can’t use any legendary actions or lair actions of the new form.\n\nYou assume the hit points and Hit Dice of the new form. When you revert to your normal form, you return to the number of hit points you had before you transformed. If you revert as a result of dropping to - hit points, any excess damage carries over to your normal form. As long as the excess damage doesn’t reduce your normal form to 0 hit points, you aren’t knocked unconscious.\n\nYou retain the benefit of any features from your class, race, or other source and can use them, provided that your new form is physically capable of doing so. You can’t use any special senses you have (for example, darkvision) unless your new form also has that sense. You can only speak if the creature can normally speak.\n\nWhen you transform, you choose whether your equipment falls to the ground, merges into the new form, or is worn by it. Worn equipment functions as normal. The DM determines whether it is practical for the new form to wear a piece of equipment, based on the creature’s shape and size. Your equipment doesn’t change shape or size to match the new form, and any equipment that the new form can’t wear must either fall to the ground or merge into your new form. Equipment that merges has no effect in that state.\n\nDuring this spell’s duration, you can use your action to assume a different form following the same restrictions and rules for the original form, with one exception; if your new form has more hit points than your current one, your hit points remain at their current value. ", - "duration": "Concentration, up to 1 hour", - "level": 9, - "range": "Self", - "school": "Transmutation", - "ritual": false, - "name": "Shapechange", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "ajade circlet worth at least 1,500 gp, which you must place on your head before you cast the spell" - } - }, - { - "castingTime": "action", - "description": "A sudden loud ringing noise, painfully intense, erupts from a point of your choice within range. Each creature in a 10-foot-radius sphere centered on that point must make a DC {DC} Constitution saving throw. A creature takes 3d8 thunder damage on a failed save, or half as much damage on a successful one. A creature made of inorganic material such as stone, crystal, or metal has disadvantage on this saving throw.\n\nA nonmagical object that isn't being worn or carried also takes the damage if it's in the spell's area.\n\n***At Higher Levels.*** When you cast this spell using a spell slot of 3rd level or higher, the damage increases by ld8 for each slot level above 2nd.", - "duration": "Instantaneous", - "level": 2, - "range": "60 feet", - "school": "Evocation", - "ritual": false, - "name": "Shatter", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "a chip of mica" - } - }, - { - "castingTime": "1 reaction, which you take when you are hit by an attack or targeted by the magic missile spell", - "description": "An invisible barrier of magical force appears and protects you. Until the start of your next turn, you have a +5 bonus to AC, including against the triggering attack, and you take no damage from magic missile.", - "duration": "1 round", - "level": 1, - "range": "Self", - "school": "Abjuration", - "ritual": false, - "name": "Shield", - "components": { - "verbal": true, - "somatic": true, - "concentration": false - } - }, - { - "castingTime": "bonus action", - "description": "A shimmering field appears and surrounds a creature of your choice within range, granting it a +2 bonus to AC for the duration.", - "duration": "Concentration, up to 10 minutes", - "level": 1, - "range": "60 feet", - "school": "Abjuration", - "ritual": false, - "name": "Shield of Faith", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "a small parchment with a bit of holy text written on it" - } - }, - { - "castingTime": "bonus action", - "description": "The wood of a club or quarterstaff you are holding is imbued with nature’s power. For the duration, you can use your spellcasting ability instead of Strength for the attack and damage rolls of melee attacks using that weapon, and the weapon’s damage die becomes a d8. The weapon also becomes magical, if it isn’t already. The spell ends if you cast it again or if you let go of the weapon.", - "duration": "1 minute", - "level": 0, - "range": "Touch", - "school": "Transmutation", - "ritual": false, - "name": "Shillelagh", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "mistletoe, a shamrock leaf, and a club or quarterstaff" - }, - "attacks": [ - { - "details": "magical", - "damage": "{floor((Level+1)/6)+1}d8", - "damageType": "bludgeoning" - } - ] - }, - { - "castingTime": "action", - "description": "Lightning springs from your hand to deliver a shock to a creature you try to touch. Make a melee spell attack against the target. You have advantage on the attack roll if the target is wearing armor made of metal. On a hit, the target takes 1d8 lightning damage, and it can’t take reactions until the start of its next turn.\n\nThe spell’s damage increases by 1d8 when you reach 5th level (2d8), 11th level (3d8), and 17th level (4d8).", - "duration": "Instantaneous", - "level": 0, - "range": "Touch", - "school": "Evocation", - "ritual": false, - "name": "Shocking Grasp", - "components": { - "verbal": true, - "somatic": true, - "concentration": false - }, - "attacks": [ - { - "details": "on hit, target can't take reactions", - "damage": "{floor((Level+1)/6)+1}d8", - "damageType": "lightning" - } - ] - }, - { - "castingTime": "action", - "description": "For the duration, no sound can be created within or pass through a 20-foot-radius sphere centered on a point you choose within range. Any creature or object entirely inside the sphere is immune to thunder damage, and creatures are deafened while entirely inside it. Casting a spell that includes a verbal component is impossible there.", - "duration": "Concentration, up to 10 minutes", - "level": 2, - "range": "120 feet", - "school": "Illusion", - "ritual": false, - "name": "Silence", - "components": { - "verbal": true, - "somatic": true, - "concentration": true - } - }, - { - "castingTime": "action", - "description": "You create the image of an object, a creature, or some other visible phenomenon that is no larger than a 15-foot cube. The image appears at a spot within range and lasts for the duration. The image is purely visual; it isn’t accompanied by sound, smell, or other sensory effects.\n\nYou can use your action to cause the image to move to any spot within range. As the image changes location, you can alter its appearance so that its movements appear natural for the image. For example, if you create an image of a creature and move it, you can alter the image so that it appears to be walking.\n\nPhysical interaction with the image reveals it to be an illusion, because things can pass through it. A creature that uses its action to examine the image can determine that it is an illusion with a successful Intelligence (Investigation) check against your spell save DC. If a creature discerns the illusion for what it is, the creature can see through the image.", - "duration": "Concentration, up to 10 minutes", - "level": 1, - "range": "60 feet", - "school": "Illusion", - "ritual": false, - "name": "Silent Image", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "a bit of fleece" - } - }, - { - "castingTime": "12 hours", - "description": "You shape an illusory duplicate of one beast or humanoid that is within range for the entire casting time of the spell. The duplicate is a creature, partially real and formed from ice or snow, and it can take actions and otherwise be affected as a normal creature. It appears to be the same as the original, but it has half the creature’s hit point maximum and is formed without any equipment. Otherwise, the illusion uses all the statistics of the creature it duplicates.\n\nThe simulacrum is friendly to you and creatures you designate. It obeys your spoken commands, moving and acting in accordance with your wishes and acting on your turn in combat. The simulacrum lacks the ability to learn or become more powerful, so it never increases its level or other abilities, nor can it regain expended spell slots.\n\nIf the simulacrum is damaged, you can repair it in an alchemical laboratory, using rare herbs and minerals worth 100 gp per hit point it regains. The simulacrum lasts until it drops to 0 hit points, at which point it reverts to snow and melts instantly.\n\nIf you cast this spell again, any currently active duplicates you created with this spell are instantly destroyed. ", - "duration": "Until dispelled", - "level": 7, - "range": "Touch", - "school": "Illusion", - "ritual": false, - "name": "Simulacrum", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "snow or ice in quantities sufficient to made a life-size copy of the duplicated creature; some hair, fingernail clippings, or other piece of that creature’s body placed inside the snow or ice; and powdered ruby worth 1,500 gp, sprinkled over the duplicate and consumed by the spell" - } - }, - { - "castingTime": "action", - "description": "This spell sends creatures into a magical slumber. Roll 5d8; the total is how many hit points of creatures this spell can affect. Creatures within 20 feet of a point you choose within range are affected in ascending order of their current hit points (ignoring unconscious creatures). Starting with the creature that has the lowest current hit points, each creature affected by this spell falls unconscious until the spell ends, the sleeper takes damage, or someone uses an action to shake or slap the sleeper awake. Subtract each creature’s hit points from the total before moving on to the creature with the next lowest hit points. A creature’s hit points must be equal to or less than the remaining total for that creature to be affected. \n\nUndead and creatures immune to being charmed aren’t affected by this spell.\n\n***At Higher Levels.*** When you cast this spell using a spell slot of 2nd level or higher, roll an additional 2d8 for each slot level above 1st.", - "duration": "1 minute", - "level": 1, - "range": "90 feet", - "school": "Enchantment", - "ritual": false, - "name": "Sleep", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "a pinch of fine sand, rose petals, or a cricket" - } - }, - { - "castingTime": "action", - "description": "Until the spell ends, freezing rain and sleet fall in a 20-foot-tall cylinder with a 40-foot radius centered on a point you choose within range. The area is heavily obscured, and exposed flames in the area are doused.\n\nThe ground in the area is covered with slick ice, making it difficult terrain. When a creature enters the spell’s area for the first time on a turn or starts its turn there, it must make a DC {DC} Dexterity saving throw. On a failed save, it falls prone.\n\nIf a creature is concentrating in the spell’s area, the creature must make a successful Constitution saving throw against your spell save DC or lose concentration.", - "duration": "Concentration, up to 1 minute", - "level": 3, - "range": "150 feet", - "school": "Conjuration", - "ritual": false, - "name": "Sleet Storm", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "a pinch of dust and a few drops of water" - } - }, - { - "castingTime": "action", - "description": "You alter time around up to six creatures of your choice in a 40-foot cube within range, Each target must succeed on a DC {DC} Wisdom saving throw or be affected by this spell for the duration.\n\nAn affected target’s speed is halved, it takes a -2 penalty to AC and Dexterity saving throws, and it can’t use reactions. On its turn, it can use either an action ora bonus action, not both. Regardless of the creature’ s abilities or magic items, it can’t make more than one melee arranged attack during its turn.\n\nIf the creature attempts to cast a spell with a casting time of 1 action, roll a d20. On an 11 or higher, the spell doesn’t take effect until the creature’s next turn, and the creature must use its action on that turn to complete the spell. If it can’t, the spell is wasted.\n\nA creature affected by this spell makes another Wisdom saving throw at the end of its turn. On a successful save, the effect ends for it.", - "duration": "Concentration, up to 1 minute", - "level": 3, - "range": "120 feet", - "school": "Transmutation", - "ritual": false, - "name": "Slow", - "components": { - "verbal": false, - "somatic": false, - "concentration": true - } - }, - { - "castingTime": "action", - "description": "You grant the semblance of life and intelligence to a corpse of your choice within range, allowing It to answer the questions you pose. The corpse must still have a mouth and can’t be undead. The spell fails if the corpse was the target of this spell within the last 10 days.\n\nUntil the spell ends, you can ask the corpse up to five questions. The corpse knows only what it knew in life, including the languages it knew. Answers are usually brief, cryptic, or repetitive, and the corpse is under no compulsion to offer a truthful answer if you are hostile to it or it recognizes you as an enemy. This spell doesn’t return the creature’s soul to its body, only its animating spirit. Thus, the corpse can’t learn new Information, doesn’t comprehend anything that has happened since it died, and can’t speculate about future events.", - "duration": "10 minutes", - "level": 3, - "range": "10 feet", - "school": "Necromancy", - "ritual": false, - "name": "Speak with Dead", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "burning incense" - } - }, - { - "castingTime": "action", - "description": "You gain the ability to comprehend and verbally communicate with beasts for the duration. The knowledge and awareness of many beasts is limited by their intelligence, but at minimum, beasts can give you information about nearby locations and monsters, including whatever they can perceive or have perceived within the past day. You might be able to persuade a beast to perform a small favor for you, at the GM’s discretion", - "duration": "10 minutes", - "level": 1, - "range": "Self", - "school": "Divination", - "ritual": true, - "name": "Speak with Animals", - "components": { - "verbal": true, - "somatic": true, - "concentration": false - } - }, - { - "castingTime": "action", - "description": "You imbue plants within 30 feet of you with limited sentience and animation, giving them the ability to communicate with you and follow your simple commands. You can question plants about events in the spell’s area within the past day, gaining information about creatures that have passed, weather, and other circumstances.\n\nYou can also turn difficult terrain caused by plant growth (such as thickets and undergrowth) into ordinary terrain that lasts for the duration. Or you can turn ordinary terrain where plants are present into difficult terrain that lasts for the duration, causing vines and branches to hinder pursuers, for example.\n\nPlants might be able to perform other tasks on your behalf, at the DM’s discretion. The spell doesn’t enable plants to uproot themselves and move about, but they can freely move branches, tendrils, and stalks.\n\nIf a plant creature is in the area, you can communicate with it as if you shared a common language, but you gain no magical ability to influence it.\n\nThis spell can cause the plants created by the entangle spell to release a restrained creature. ", - "duration": "10 minutes", - "level": 3, - "range": "Self (30-foot radius)", - "school": "Transmutation", - "ritual": false, - "name": "Speak with Plants", - "components": { - "verbal": true, - "somatic": true, - "concentration": false - } - }, - { - "castingTime": "action", - "description": "Until the spell ends, one willing creature you touch gains the ability to move up, down, and across vertical surfaces and upside down along ceilings, while leaving its hands free. The target also gains a climbing speed equal to its walking speed.", - "duration": "Concentration, up to 1 hour", - "level": 2, - "range": "Touch", - "school": "Transmutation", - "ritual": false, - "name": "Spider Climb", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "a drop of bitumen and a spider" - } - }, - { - "castingTime": "action", - "description": "The ground in a 20-foot radius centered on a point within range twists and sprouts hard spikes and thorns. The area becomes difficult terrain for the duration. When a creature moves into or within the area, it takes 2d4 piercing damage for every 5 feet it travels.\n\nThe transformation of the ground is camouflaged to look natural. Any creature that can’t see the area at the time the spell is cast must make a Wisdom (Perception) check against your spell save DC to recognize the terrain as hazardous before entering it.", - "duration": "Concentration, up to 10 minute", - "level": 2, - "range": "150 feet", - "school": "Transmutation", - "ritual": false, - "name": "Spike Growth", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "seven sharp thorns or seven small twigs, each sharpened to a point" - } - }, - { - "castingTime": "action", - "description": "You call forth spirits to protect you. They flit around you to a distance of 15 feet for the duration. If you are good or neutral, their spectral form appears angelic or fey (your choice). If you are evil, they appear fiendish.\n\nWhen you cast this spell, you can designate any number of creatures you can see to be unaffected by it. An affected creature’s speed is halved in the area, and when the creature enters the area for the first time on a turn or starts its turn there, it must make a DC {DC} Wisdom saving throw. On a failed save, the creature takes 3d8 radiant damage (if you are good or neutral) or 3d8 necrotic damage (if you are evil). On a successful save, the creature takes half as much damage.\n\n***At Higher Levels.*** When you cast this spell using a spell slot of 4th level or higher, the damage increases by 1d8 for each slot level above 3rd.", - "duration": "Concentration, up to 10 minutes", - "level": 3, - "range": "Self (15-foot radius)", - "school": "Conjuration", - "ritual": false, - "name": "Spirit Guardians", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "a holy symbol" - } - }, - { - "castingTime": "bonus action", - "description": "You create a floating, spectral weapon within range that lasts for the duration or until you cast this spell again. When you cast the spell, you can make a melee spell attack against a creature within 5 feet of the weapon. On a hit, the target takes force damage equal to 1d8 + your spellcasting ability modifier. As a bonus action on your turn, you can move the weapon up to 20 feet and repeat the attack against a creature within 5 feet of it. The weapon can take whatever form you choose. Clerics of deities who are associated with a particular weapon (as St. Cuthbert is known for his mace and Thor for his hammer) make this spell’s effect resemble that weapon. ***At Higher Levels.*** When you cast this spell using a spell slot of 3rd level or higher, the damage increases by 1d8 for every two slot levels above the 2nd.", - "duration": "1 minute", - "level": 2, - "range": "60 feet", - "school": "Evocation", - "ritual": false, - "name": "Spiritual Weapon", - "components": { - "verbal": true, - "somatic": true, - "concentration": false - } - }, - { - "castingTime": "action", - "description": "You create a 20-foot-radius sphere of yellow, nauseating gas centered on a point within range. The cloud spreads around corners, and its area is heavily obscured. The cloud lingers in the air for the duration.\n\nEach creature that is completely within the cloud at the start of its turn must make a DC {DC} Constitution saving throw against paisano On a failed save, the creature spends its action that turn retching and reeling. Creatures that dan’t need to breathe or are immune to poison automatically succeed on this saving throw.\n\nA moderate wind (at least 10 miles per hour) disperses the cloud after 4 rounds. A strong wind (at least 20 miles per hour) disperses it after 1 round.", - "duration": "Concentration, up to 1 minute", - "level": 3, - "range": "90 feet", - "school": "Conjuration", - "ritual": false, - "name": "Stinking Cloud", - "components": { - "verbal": true, - "somatic": true, - "concentration": true - } - }, - { - "castingTime": "action", - "description": "You touch a stone object of Medium size or smaller or a section of stone no more than 5 feet in any dimension and form it into any shape that suits your purpose. So, for example, you could shape a large rock into a weapon, idol, or coffer, or make a small passage through a wall, as long as the wall is less than 5 feet thick. You could also shape a stone door or its frame to seal the door shut. The object you create can have up to two hinges and a latch, but finer mechanical detail isn’t possible.", - "duration": "Instantaneous", - "level": 4, - "range": "Touch", - "school": "Transmutation", - "ritual": false, - "name": "Stone Shape", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "soft clay, which must be worked into roughly the desired shape of the stone object" - } - }, - { - "castingTime": "action", - "description": "This spell turns the flesh of a willing creature you touch as hard as stone. Until the spell ends, the target has resistance to nonmagical bludgeoning, piercing, and slashing damage.", - "duration": "Concentration, up to 1 hour", - "level": 4, - "range": "Touch", - "school": "Abjuration", - "ritual": false, - "name": "Stoneskin", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "diamond dust worth 100 gp, which the spell consumes" - } - }, - { - "castingTime": "action", - "description": "A churning storm cloud forms, centered on a point you can see and spreading to a radius of 360 feet. Lightning flashes in the area, thunder booms, and strong winds roar. Each creature under the cloud (no more than 5,000 feet beneath the cloud) when it appears must make a DC {DC} Constitution saving throw. On a failed save, a creature takes 2d6 thunder damage and becomes deafened for 5 minutes.\n\nEach round you maintain concentration on this spell, the storm produces additional effects on your turn.\n\nRound 2. Acidic rain falls from the cloud. Each creature and object under the cloud takes ld6 acid damage.\n\nRound 3. You call six bolts of lightning from the cloud to strike six creatures or objects of your choice beneath the cloud. A given creature or object can’t be struck by more than one bolt. A struck creature must make a DC {DC} Dexterity saving throw. The creature takes 10d6 lightning damage on a failed save, or half as much damage on a successful one.\n\nRound 4. Hailstones rain down from the cloud. Each creature under the cloud takes 2d6 bludgeoning damage.\n\nRound 5-10. Gusts and freezing rain assail the area under the cloud. The area becomes difficult terrain and is heavily obscured. Each creature there takes 1d6 cold damage. Ranged weapon attacks in the area are impossible. The wind and rain count as a severe distraction for the purposes of maintaining concentration on spells. Finally, gusts of strong wind (ranging from 20 to 50 miles per hour) automatically disperse fog, mists, and similar phenomena in the area, whether mundane or magical.\n\nThe target must make a DC {DC} Wisdom saving throw. On a failed save, it pursues the course of action you described to the best of its ability. The suggested course of action can continue for the entire duration. If the suggested activity can be completed in a shorter time, the spell ends when the subject finishes what it was asked to do.\n\nYou can also specify conditions that will trigger a special activity during the duration. For example, you might suggest that a knight give her warhorse to the first beggar she meets. If the condition isn’t met before the spell expires, the activity isn’t performed.\n\nIf you or any of your companions damage the target, the spell ends. ", - "duration": "Concentration, up to 1 minute", - "level": 9, - "range": "Sight", - "school": "Conjuration", - "ritual": false, - "name": "Storm of Vengeance", - "components": { - "verbal": true, - "somatic": true, - "concentration": true - } - }, - { - "castingTime": "action", - "description": "You suggest a course of activity (limited to a sentence or two) and magically influence a creature you can see within range that can hear and understand you. Creatures that can’t be charmed are immune to this effect. The suggestion must be worded in such a manner as to make the course of action sound reasonable. Asking the creature to stab itself, throw itself onto a spear, immolate itself, or do some other obviously harmful act ends the spell.\n\nThe target must make a DC {DC} Wisdom saving throw. On a failed save, it pursues the course of action you described to the best of its ability. The suggested course of action can continue for the entire duration. If the suggested activity can be completed in a shorter time, the spell ends when the subject finishes what it was asked to do.\n\nYou can also specify conditions that will trigger a special activity during the duration. For example, you might suggest that a knight give her warhorse to the first beggar she meets. If the condition isn’t met before the spell expires, the activity isn’t performed.\n\nIf you or any of your companions damage the target, the spell ends.", - "duration": "Concentration, up to 8 hours", - "level": 2, - "range": "30 feet", - "school": "Enchantment", - "ritual": false, - "name": "Suggestion", - "components": { - "verbal": true, - "somatic": false, - "concentration": true, - "material": "a snake’s tongue and either a bit of honeycomb or a drop of sweet oil" - } - }, - { - "castingTime": "action", - "description": "A beam of brilliant light flashes out from your hand in a 5-foot-wide, 60-foot-long line. Each creature in the line must make a DC {DC} Constitution saving throw. On a failed save, a creature takes 6d8 radiant damage and is blinded until your next turn. On a successful save, it takes half as much damage and isn’t blinded by this spell. Undead and oozes have disadvantage on this saving throw.\n\nYou can create a new line of radiance as your action on any turn until the spell ends.\n\nFor the duration, a mote of brilliant radiance shines in your hand. It sheds bright light in a 30-foot radius and dim light for an additional 30 feet. This light is sunlight.", - "duration": "Concentration, up to 1 minute", - "level": 6, - "range": "Self (60-foot line)", - "school": "Evocation", - "ritual": false, - "name": "Sunbeam", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "a magnifying glass" - } - }, - { - "castingTime": "action", - "description": "Brilliant sunlight flashes in a 60-foot radius centered on a point you choose within range. Each creature in that light must make a DC {DC} Constitution saving throw. On a failed save, a creature takes 12d6 radiant damage and is blinded for 1 minute. On a successful save, it takes half as much damage and isn’t blinded by this spell. Undead and oozes have disadvantage on this saving throw.\n\nA creature blinded by this spell makes another Constitution saving throw at the end of each of its turns. On a successful save, it is no longer blinded.\n\nThis spell dispels any darkness in its area that was created by a spell.", - "duration": "Instantaneous", - "level": 8, - "range": "150 feet", - "school": "Evocation", - "ritual": false, - "name": "Sunburst", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "fire and a piece of sunstone" - } - }, - { - "castingTime": "action", - "description": "When you cast this spell, you inscribe a harmful glyph either on a surface (such as a section of floor, a wall, or a table) or within an object that can be closed to conceal the glyph (such as a book, a scroll, or a treasure chest). If you choose a surface, the glyph can cover an area of the surface no larger than 10 feet in diameter. If you choose an object, that object must remain in its place; if the object is moved more than 10 feet from where you cast this spell, the glyph is broken, and the spell ends without being triggered.\n\nThe glyph is nearly invisible, requiring an Intelligence (Investigation) check against your spell save DC to find it.\n\nYou decide what triggers the glyph when you cast the spell. For glyphs inscribed on a surface, the most typical triggers include touching or stepping on the glyph, removing another object covering it, approaching within a certain distance of it, or manipulating the object that holds it. For glyphs inscribed within an object, the most common triggers are opening the object, approaching within a certain distance of it, or seeing or reading the glyph.\n\nYou can further refine the trigger so the spell is activated only under certain circumstances or according to a creature’s physical characteristics (such as height or weight), or physical kind (for example, the ward could be set to affect hags or shapechangers). You can also specify creatures that don’t trigger the glyph, such as those who say a certain password.\n\nWhen you inscribe the glyph, choose one of the options below for its effect. Once triggered, the glyph glows, filling a 60-foot-radius sphere with dim light. for 10 minutes, after which time the spell ends. Each creature in the sphere when the glyph activates is targeted by its effect, as is a creature that enters the sphere for the first time on a turn or ends its turn there.\\n\nDeath. Each target must make a DC {DC} Constitution saving throw, taking 10d10 necrotic damage on a failed save, or half as much damage on a successful save.\n\nDiscord. Each target must make a DC {DC} Constitution saving throw. On a failed save, a target bickers and argues with other creatures for 1 minute. During this time, it is incapable of meaningful communication and has disadvantage on attack rolls and ability checks.\n\nFear. Each target must make a DC {DC} Wisdom saving throw and becomes frightened for 1 minute on a failed save. While frightened, the target drops whatever it is holding and must move at least 30 feet away from the glyph on each of its turns, if able.\n\nHopelessness. Each target must make a DC {DC} Charisma saving throw. On a failed save, the target is overwhelmed with despair for 1 minute. During this time, it can’t attack or target any creature with harmful abilities, spells, or other magical effects.\n\nInsanity. Each target must make a DC {DC} Intelligence saving throw. On a failed save, the target is driven insane for 1 minute. An insane creature can’t take actions, can’t understand what other creatures say, can’t read, and speaks only in gibberish. The DM controls its movement, which is erratic.\n\nPain. Each target must make a DC {DC} Constitution saving throw and becomes incapacitated with excruciating pain for 1 minute on a failed save.\n\nSleep. Each target must make a DC {DC} Wisdom saving throw and falls unconscious for 10 minutes on a failed save. A creature awakens if it takes damage or if someone uses an action to shake or slap it awake. Stunning. Each target must make a DC {DC} Wisdom saving throw and becomes stunned for 1 minute on a failed save. ", - "duration": "Until dispelled or triggered", - "level": 7, - "range": "", - "school": "Abjuration", - "ritual": false, - "name": "Symbol", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "mercury, phosphorus, and powdered diamond and opal with a total value of at least 1,000 gp, which the spell consumes" - } - }, - { - "castingTime": "action", - "description": "A creature of your choice that you can see with range perceives everything as hilariously funny and falls into fits of laughter if this spell affects it. The target must succeed on a DC {DC} Wisdom saving throw or fall prone, becoming incapacitated and unable to stand up for this duration. A creature with an Intelligence score of 4 or less isn’t affected.\n\nAt the end of each of its turns, and each time it takes damage, the target can make another Wisdom saving throw. The target has an advantage on the saving throw if it’s triggered by damage. On a success, the spell ends.", - "duration": "Concentration, up to 1 minute", - "level": 1, - "range": "30 feet", - "school": "Enchantment", - "ritual": false, - "name": "Hideous Laughter", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "tiny tarts and a feather that is waved in the air" - } - }, - { - "castingTime": "action", - "description": "You gain the ability to move or manipulate creatures or objects by thought. When you cast the spell, and as your action each round for the duration, you can exert your will on one creature or object that you can see within range, causing the appropriate effect below. You can affect the same target round after round, or choose a new one at any time. If you switch targets, the prior target is no longer affected by the spell. \n\nCreature. You can try to move a Huge or smaller creature. Make an ability check with your spellcasting ability contested by the creature’s Strength check. If you win the contest, you move the creature up to 30 feet in any direction, including upward but not beyond the range of this spell. Until the end of your next turn, the creature is restrained in your telekinetic grip. A creature lifted upward is suspended in mid-air.\n\nOn subsequent rounds, you can use your action to attempt to maintain your telekinetic grip on the creature by repealing the contest.\n\nObject. You can try to move an object that weighs up to 1,000 pounds. If the object isn’t being worn or carried, you automatically move it up lo 30 feet in any direction, but not beyond the range of this spell.\n\nIf the object is worn or carried by a creature, you must make an ability check with your spellcasting ability contested by that creature’s Strength check. If you succeed, you pull the object away from that creature and can move it up to 30 feel in any direction but not beyond the range of this spell.\n\nYou can exert fine control on objects with your telekinetic grip, such as manipulating a simple tool, opening a door or a container, stowing or retrieving an item from an open container, or pouring the contents from a vial.", - "duration": "Concentration, up to 10 minutes", - "level": 5, - "range": "60 feet", - "school": "Transmutation", - "ritual": false, - "name": "Telekinesis", - "components": { - "verbal": true, - "somatic": true, - "concentration": true - } - }, - { - "castingTime": "action", - "description": "This spell instantly transports you and up to eight willing creatures of your choice that you can see within range, or a single object that you can see within range, to a destination you select. If you target an object, it must be able to fit entirely inside a 10-foot cube, and it can’t be held or carried by an unwilling creature.\n\nThe destination you choose must be known to you, and it must be on the same plane of existence as you. Your familiarity with the destination determines whether you arrive there successfully. The DM rolls d100 and consults the table. \n\nFamiliarity. “Permanent circle” means a permanent teleportation circle whose sigil sequence you know. “Associated object” means that you possess an object taken from the desired destination within the last six months, such as a book from a wizard’s library, bed linen from a royal suite, or a chunk of marble from a lich’s secret tomb. \n\n“Very familiar” is a place you have been very often, a place you have carefully studied, or a place you can see when you cast the spell. “Seen casually” is someplace you have seen more than once but with which you aren’t very familiar. “Viewed once” is a place you have seen once, possibly using magic. “Description” is a place whose location and appearance you know through someone else’s description, perhaps from a map.\n\n“False destination” is a place that doesn’t exist. Perhaps you tried to scry an enemy’s sanctum but instead viewed an illusion, or you are attempting to teleport to a familiar location that no longer exists.\n\nOn Target. You and your group (or the target object) appear where you want to.\n\nOff Target. You and your group (or the target object) appear a random distance away from the destination in a random direction. Distance off target is 1d10 × 1d10 percent of the distance that was to be traveled. For example, if you tried to travel 120 miles, landed off target, and rolled a 5 and 3 on the two d10s, then you would be off target by 15 percent, or 18 miles. The DM determines the direction off target randomly by rolling a d8 and designating 1 as north, 2 as northeast, 3 as east, and so on around the points of the compass. If you were teleporting to a coastal city and wound up 18 miles out at sea, you could be in trouble.\n\nSimilar Area. You and your group (or the target object) wind up in a different area that’s visually or thematically similar to the target area. If you are heading for your home laboratory, for example, you might wind up in another wizard’s laboratory or in an alchemical supply shop that has many of the same tools and implements as your laboratory. Generally, you appear in the closest similar place, but since the spell has no range limit, you could conceivably wind up anywhere on the plane.\n\nMishap. The spell’s unpredictable magic results in a difficult journey. Each teleporting creature (or the target object) takes 3d10 force damage, and the DM rerolls on the table to see where you wind up (multiple mishaps can occur, dealing damage each time). \n\nFamiliarity | Mishap | Similar Area | Off Target | On Target\n---|---|---|---|---\nPermanent circle | — | — | — | 01–100\nAssociated object | — | — | — | 01–100\nVery familiar | 01–05 | 06–13 | 14–24 | 25–100\nSeen casually | 01–33 | 34–43 | 44–53 | 54–100\nViewed once | 01–43 | 44–53 | 54–73 | 74–100\nDescription | 01–43 | 44–53 | 54–73 | 74–100\nFalse destination | 01–50 | 51–100 | — | —", - "duration": "Instantaneous", - "level": 7, - "range": "10 feet", - "school": "Conjuration", - "ritual": false, - "name": "Teleport", - "components": { - "verbal": true, - "somatic": false, - "concentration": false - } - }, - { - "castingTime": "1 minute", - "description": "As you cast the spell, you draw a 10-foot-diameter circle on the ground inscribed with sigils that link your location to a permanent teleportation circle of your choice whose sigil sequence you know and that is on the same plane of existence as you. A shimmering portal opens within the circle you drew and remains open until the end of your next turn. Any creature that enters the portal instantly appears within 5 feet of the destination circle or in the nearest unoccupied space if that space is occupied.\n\nMany major temples, guilds, and other important places have permanent teleportation circles inscribed somewhere within their confines. Each such circle includes a unique sigil sequence-a string of magical runes arranged in a particular pattern. When you first gain the ability to cast this spell, you learn the sigil sequences for two destinations on the Material Plane, determined by the DM. You can learn additional sigil sequences during your adventures. You can commit a new sigil sequence to memory after studying it for 1 minute.\n\nYou can create a permanent teleportation circle by casting this spell in the same location every day for one year. You need not use the circle to teleport when you cast the spell in this way.", - "duration": "1 round", - "level": 5, - "range": "10 feet", - "school": "Conjuration", - "ritual": false, - "name": "Teleportation Circle", - "components": { - "verbal": true, - "somatic": false, - "concentration": false, - "material": "rare chalks and inks infused with precious gems with 50 gp, which the spell consumes" - } - }, - { - "castingTime": "action", - "description": "This spell creates a circular, horizontal plane of force, 3 feet in diameter and 1 inch thick, that floats 3 feet above the ground in an unoccupied space of your choice that you can see within range. The disk remains for the duration, and can hold up to 500 pounds. If more weight is placed on it, the spell ends, and everything on the disk falls to the ground.\n\nThe disk is immobile while you are within 20 feet of it. If you move more than 20 feet away from it, the disk follows you so that it remains within 20 feet of you. It can move across uneven terrain, up or down stairs, slopes and the like, but it can’t cross an elevation change of 10 feet or more. For example, the disk can’t move across a 10-foot-deep pit, nor could it leave such a pit if it was created at the bottom.\n\nIf you move more than 100 feet from the disk (typically because it can’t move around an obstacle to follow you), the spell ends.", - "duration": "1 hour", - "level": 1, - "range": "30 feet", - "school": "Conjuration", - "ritual": true, - "name": "Floating Disk", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "a drop of mercury" - } - }, - { - "castingTime": "action", - "description": "You manifest a minor wonder, a sign of supernatural power, within range. You create one of the following magical effects within range:\n\n• Your voice booms up to three times as loud as normal for 1 minute.\n\n• You cause flames to flicker, brighten, dim, or change color for 1 minute.\n\n• You cause harmless tremors in the ground for 1 minute.\n\n• You create an instantaneous sound that originates from a point of your choice within range, such as a rumble of thunder, the cry of a raven, or omi- nous whispers.\n\n• You instantaneously cause an unlocked door or win- dow to fly open or slam shut.\n\n• You alter the appearance of your eyes for 1 minute.\n\nIf you cast this spell multiple times, you can have up to three of its 1-minute effects active at a time, and you can dismiss such an effect as an action.", - "duration": "Up to 1 minute", - "level": 0, - "range": "30 feet", - "school": "Transmutation", - "ritual": false, - "name": "Thaumaturgy", - "components": { - "verbal": true, - "somatic": false, - "concentration": false - } - }, - { - "castingTime": "action", - "description": "A wave of thunderous force sweeps out from you. Each creature in a 15-foot cube originating from you must make a DC {DC} Constitution saving throw. On a failed save, a creature takes 2d8 thunder damage and is pushed 10 feet away from you. On a successful save, the creature takes half as much damage and isn’t pushed.\n\nIn addition, unsecured objects that are completely within the area of effect are automatically pushed 10 feet away from you by the spell’s effect, and the spell emits a thunderous boom audible out to 300 feet.\n\n***At Higher Levels.*** When you cast this spell using a spell slot of 2nd level or higher, the damage increases by 1d8 for each slot level above 1st.", - "duration": "Instantaneous", - "level": 1, - "range": "Self (15-foot cube)", - "school": "Evocation", - "ritual": false, - "name": "Thunderwave", - "components": { - "verbal": true, - "somatic": true, - "concentration": false - } - }, - { - "castingTime": "action", - "description": "You briefly stop the flow of time for everyone but yourself. No time passes for other creatures, while you take 1d4 + 1 turns in a row, during which you can use actions and move as normal. This spell ends if one of the actions you use during this period, or any effects that you create during this period, affects a creature other than you or an object being worn or carried by someone other than you. In addition, the spell ends if you move to a place more than 1,000 feet from the location where you cast it.", - "duration": "Instantaneous", - "level": 9, - "range": "Self", - "school": "Transmutation", - "ritual": false, - "name": "Time Stop", - "components": { - "verbal": true, - "somatic": false, - "concentration": false - } - }, - { - "castingTime": "action", - "description": "This spell grants the creature you touch the ability to understand any spoken language it hears. Moreover, when the target speaks, any creature that knows at least one language and can hear the target understands what it says.", - "duration": "1 hour", - "level": 3, - "range": "Touch", - "school": "Divination", - "ritual": false, - "name": "Tongues", - "components": { - "verbal": true, - "somatic": false, - "concentration": false, - "material": "a small clay model of a ziggurat" - } - }, - { - "castingTime": "action", - "description": "This spell creates a magical link between a Large or larger inanimate plant within range and another plant, at any distance, on the same plane of existence. You must have seen or touched the destination plant at least once before. For the duration, any creature can step into the target plant and exit from the destination plant by using 5 feet of movement.", - "duration": "1 round", - "level": 6, - "range": "10 feet", - "school": "Conjuration", - "ritual": false, - "name": "Transport via Plants", - "components": { - "verbal": true, - "somatic": true, - "concentration": false - } - }, - { - "castingTime": "action", - "description": "You gain the ability to enter a tree and move from inside it to inside another tree of the same kind within 500 feet. Both trees must be living and at least the same size as you. You must use 5 feet of movement to enter a tree. You instantly know the location of all other trees of the same kind within 500 feet and, as part of the move used to enter the tree, can either pass into one of those trees or step out of the tree you’re in. You appear in a spot of your choice within 5 feet of the destination tree, using another 5 feet of movement. If you have no movement left, you appear within 5 feet of the tree you entered.\n\nYou can use this transportation ability once per round for the duration. You must end each turn outside a tree.", - "duration": "Concentration, up to 1 minute", - "level": 5, - "range": "Self", - "school": "Conjuration", - "ritual": false, - "name": "Tree Stride", - "components": { - "verbal": true, - "somatic": true, - "concentration": true - } - }, - { - "castingTime": "action", - "description": "\n\nChoose one creature or nonmagical object that you can see within range. You transform the creature into a different creature, the creature into an object, or the object into a creature (the object must be neither worn nor carried by another creature). The transformation lasts for the duration, or until the target drops to 0 hit points or dies. If you concentrate on this spell for the full duration, the transformation becomes permanent.\n\nShapechangers aren’t affected by this spell. An unwilling creature can make a DC {DC} Wisdom saving throw, and if it succeeds, it isn’t affected by this spell.\n\nCreature into Creature. If you turn a creature into another kind of creature, the new form can be any kind you choose whose challenge rating is equal to or less than the target’s (or its level). if the target doesn’t have a challenge rating). The target’s game statistics, including mental ability scores, are replaced by the statistics of the new form. It retains its alignment and personality.\n\nThe target assumes the hit points of its new form, and when it reverts to its normal form, the creature returns to the number of hit points it had before it transformed. If it reverts as a result of dropping to 0 hit points, any excess damage carries over to its normal form. As long as the excess damage doesn’t reduce the creature’s normal form to 0 hit points, it isn’t knocked unconscious.\n\nThe creature is limited in the actions it can perform by the nature of its new form, and it can’t speak, cast spell, or take any other action that requires hands or speech, unless its new form is capable of such actions.\n\nThe target’s gear melds into the new form. The creature can’t activate, use, wield, or otherwise benefit from any of its equipment.\n\nObject into Creature. You can turn an object into any kind of creature, as long as the creature’s size is no larger than the object’s size and the creature’s challenge rating is 9 or lower. The creature is friendly to you and your companions. It acts on each of your turns. You decide what action it takes and how it moves. The DM has the creature’s statistics and resolves all of its actions and movement.\n\nIf the spell becomes permanent, you no longer control the creature. It might remain friendly to you, depending on how you have treated it.\n\nCreature into Object. If you turn a creature into an object, it transforms along with whatever it is wearing and carrying into that form. The creature’s statistics become those of the object, and the creature has no memory of time spent in this form, after the spell ends and it returns to its normal form. This spell can't affect a target that has 0 hit points. \n\n(Spell's description has been modified to fix the error during printing as described in the Player's Handbook errata. See http://media.wizards.com/2016/downloads/DND/PH-Errata-V1.pdf for full details)", - "duration": "Concentration, up to 1 hour", - "level": 9, - "range": "30 feet", - "school": "Transmutation", - "ritual": false, - "name": "True Polymorph", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "a drop of mercury, a dollop of gum arabic, and a wisp of smoke" - } - }, - { - "castingTime": "1 hour", - "description": "You touch a creature that has been dead for no longer than 200 years and that died for any reason except old age. If the creature’s soul is free and willing, the creature is restored to life with all its hit points.\n\nThis spell closes all wounds, neutralizes any poison, cures all diseases, and lifts any curses affecting the creature when it died. The spell replaces damaged or missing organs and limbs.\n\nThe spell can even provide a new body if the original no longer exists, in which case you must speak the creature’s name. The creature then appears in an unoccupied space you choose within 10 feet of you.", - "duration": "Instantaneous", - "level": 9, - "range": "Touch", - "school": "Necromancy", - "ritual": false, - "name": "True Resurrection", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "a sprinkle of holy water and diamonds worth at least 25,000 gp, which the spell consumes" - } - }, - { - "castingTime": "action", - "description": "This spell gives the willing creature you touch the ability to see things as they actually are. For the duration, the creature has truesight, notices secret doors hidden by magic, and can see into the Ethereal Plane, all out to a range of 120 feet.", - "duration": "1 hour", - "level": 6, - "range": "Touch", - "school": "Divination", - "ritual": false, - "name": "True Seeing", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "an ointment for the eyes that costs 25 gp; is made from mushroom powder, saffron, and fat; and is consumed by the spell" - } - }, - { - "castingTime": "action", - "description": "You extend your hand and point a finger at a target in range. Your magic grants you a brief insight into the target’s defenses. On your next turn, you gain advantage on your first attack roll against the target, provided that this spell hasn’t ended.", - "duration": "Concentration, up to 1 round", - "level": 0, - "range": "30 feet", - "school": "Divination", - "ritual": false, - "name": "True Strike", - "components": { - "verbal": false, - "somatic": true, - "concentration": true - } - }, - { - "castingTime": "action", - "description": "This spell creates an invisible, mindless, shapeless force that performs simple tasks at your command until the spell ends. The servant springs into existence in an unoccupied space on the ground within range. It has AC 10, 1 hit point, and a Strength of 2, and it can’t attack. If it drops to 0 hit points, the spell ends.\n\nOnce on each of your turns as a bonus action, you can mentally command the servant to move up to 15 feet and interact with an object. The servant can perform simple tasks that a human servant could do, such as fetching things, cleaning, mending, folding clothes, lighting fires, serving food, and pouring wine. Once you give the command, the servant performs the task to the best of its ability until it completes the task, then waits for your next command.\n\nIf you command the servant to perform a task that would move it more than 60 feet away from you, the spell ends.", - "duration": "1 hour", - "level": 1, - "range": "60 feet", - "school": "Conjuration", - "ritual": true, - "name": "Unseen Servant", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "a piece of string and a bit of wood" - } - }, - { - "castingTime": "action", - "description": "The touch of your shadow-wreathed hand can siphon life force from others to heal your wounds. Make a melee spell attack against a creature within your reach. On a hit, the target takes 3d6 necrotic damage, and you regain hit points equal to half the amount of necrotic damage dealt. Until the spell ends, you can make the attack again on each of your turns as an action.\n\n***At Higher Levels.*** When you cast this spell using a spell slot of 4th level or higher, the damage increases by 1d6 for each slot level above 3rd.", - "duration": "Concentration, up to 1 minute", - "level": 3, - "range": "Self", - "school": "Necromancy", - "ritual": false, - "name": "Vampiric Touch", - "components": { - "verbal": true, - "somatic": true, - "concentration": true - } - }, - { - "castingTime": "action", - "description": "You create a wall of fire on a solid surface within range. You can make the wall up to 60 feet long, 20 feet high, and 1 foot thick, or a ringed wall up to 20 feet in diameter, 20 feet high, and 1 foot thick. The wall is opaque and lasts for the duration.\n\nWhen the wall appears, each creature within its area must make a DC {DC} Dexterity saving throw. On a failed save, a creature takes 5d8 fire damage, or half as much damage on a successful save.\n\nOne side of the wall, selected by you when you cast this spell, deals 5d8 fire damage to each creature that ends its turn within 10 feet of that side or inside the wall. A creature takes the same damage when it enters the wall for the first time on a turn or ends its turn there. The other side of the wall deals no damage.\n\n***At Higher Levels.*** When you cast this spell using a spell slot of 5th level or higher, the damage increases by 1d8 for each slot level above 4th.", - "duration": "Concentration, up to 1 minute", - "level": 4, - "range": "120 feet", - "school": "Evocation", - "ritual": false, - "name": "Wall of Fire", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "a small piece of phosphorus" - } - }, - { - "castingTime": "action", - "description": "An invisible wall of force springs into existence at a point you choose within range. The wall appears in any orientation you choose, as a horizontal or vertical barrier or at an angle. It can be free floating or resting on a solid surface. You can form it into a hemispherical dome or a sphere with a radius of up to 10 feet, or you can shape a flat surface made up of ten 10-foot-by-10-foot panels. Each panel must be contiguous with another panel. In any form, the wall is 1/4 inch thick. It lasts for the duration. If the wall cuts through a creature’s space when it appears, the creature is pushed to one side of the wall (your choice which side).\n\nNothing can physically pass through the wall. It is immune to all damage and can’t be dispelled by dispel magic. A disintegrate spell destroys the wall instantly, however. The wall also extends into the Ethereal Plane, blocking ethereal travel through the wall.", - "duration": "Concentration, up to 10 minutes", - "level": 5, - "range": "120 feet", - "school": "Evocation", - "ritual": false, - "name": "Wall of Force", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "a pinch of powder made by crushing a clear gemstone" - } - }, - { - "castingTime": "action", - "description": "You create a wall of ice on a solid surface within range. You can form it into a hemispherical dome or a sphere with a radius of up to 10 feet, or you can shape a flat surface made up of ten 10-foot-square panels. Each panel must be contiguous with another pane!. In any form, the wall is 1 foot thick and lasts for the duration.\n\nIf the wall cuts through a creature’s space when it appears, the creature within its area is pushed to one side of the wall and must make a DC {DC} Dexterity saving throw. On a failed save, the creature takes 10d6 cold damage, or half as much damage on a successful save.\n\nThe wall is an object that can be damaged and thus breached. It has AC 12 and 30 hit points per 10-foot section, and it is vulnerable to fire damage. Reducing a 10-foot section of wall to 0 hit points destroys it and leaves behind a sheet of frigid air in the space the wall occupied. A creature moving through the sheet of frigid air for the first time on a turn must make a DC {DC} Constitution saving throw. That creature takes 5d6 cold damage on a failed save, or half as much damage on a successful one.\n\n***At Higher Levels.*** When you cast this spell using a spell slot of 7th level or higher, the damage the wall deals when it appears increases by 2d6, and the damage from passing through the sheet of frigid air increases by 1d6, for each slot level above 6th.", - "duration": "Concentration, up to 10 minutes", - "level": 6, - "range": "120 feet", - "school": "Evocation", - "ritual": false, - "name": "Wall of Ice", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "a small piece of quartz" - } - }, - { - "castingTime": "action", - "description": "A nonmagical wall of solid stone springs into existence at a point you choose within range. The wall is 6 inches thick and is composed of ten 10-foot-by-10-foot panels. Each panel must be contiguous with at least one other panel. Alternatively, you can create 10-foot-by-20-foot panels that are only 3 inches thick.\n\nIf the wall cuts through a creature’s space when it appears, the creature is pushed to one side of the wall (your choice). If a creature would be surrounded on all sides by the wall (or the wall and another solid surface), that creature can make a DC {DC} Dexterity saving throw. On a success, it can use its reaction to move up to its speed so that it is no longer enclosed by the wall.\n\nThe wall can have any shape you desire, though it can’t occupy the same space as a creature or object. The wall doesn’t need to be vertical or rest on any firm foundation. It must, however, merge with and be solidly supported by existing stone. Thus, you can use this spell to bridge a chasm or create a ramp.\n\nIf you create a span greater than 20 feet in length, you must halve the size of each panel to create supports. You can crudely shape the wall to create crenellations, battlements, and so on.\n\nThe wall is an object made of stone that can be damaged and thus breached. Each panel has AC 15 and 30 hit points per inch of thickness. Reducing a panel to 0 hit points destroys it and might cause connected panels to collapse at the DM’s discretion.\n\nIf you maintain your concentration on this spell for its whole duration, the wall becomes permanent and can’t be dispelled. Otherwise, the wall disappears when the spell ends.", - "duration": "Concentration, up to 10 minutes", - "level": 5, - "range": "120 feet", - "school": "Evocation", - "ritual": false, - "name": "Wall of Stone", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "a small block of granite" - } - }, - { - "castingTime": "action", - "description": "You create a wall of tough, pliable. tangled brush bristling with needle-sharp thorns. The wall appears within range on a solid surface and lasts for the duration. You choose to make the wall up to 60 feet long, 10 feet high, and 5 feet thick or a circle that has a 20-foot diameter and is up to 20 feet high and 5 feet thick. The wall blocks line of sight.\n\nWhen the wall appears, each creature within its area must make a DC {DC} Dexterity saving throw. On a failed save, a creature lakes 7d8 piercing damage, or half as much damage on a successful save.\n\nA creature can move through the wall, albeit slowly and painfully. For every 1 foot a creature moves through the wall, it must spend 4 feet of movement. Furthermore, the first time a creature enters the wall on a turn or ends its turn there, the creature must make a DC {DC} Dexterity saving throw. It takes 7d8 slashing damage on a failed save, or half as much damage on a successful one.\n\n***At Higher Levels.*** When you cast this spell using a spell slot of 7th level or higher, both types of damage increase by 1d8 for each slot level above 6th.", - "duration": "Concentration, up to 10 minutes", - "level": 6, - "range": "120 feet", - "school": "Conjuration", - "ritual": false, - "name": "Wall of Thorns", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "a handful of thorns" - } - }, - { - "castingTime": "action", - "description": "This spell wards a willing creature you touch and creates a mystic connection between you and the target until the spell ends. While the target is within 60 feet of you, it gains a +1 bonus to AC and saving throws, and it has resistance to all damage. Also, each time it takes damage, you take the same amount of damage. The spell ends if you drop to 0 hit points or if you and the target become separated by more than 60 feet. It also ends if the spell is cast again on either of the connected creatures. You can also dismiss the spell as an action.", - "duration": "1 hour", - "level": 2, - "range": "Touch", - "school": "Abjuration", - "ritual": false, - "name": "Warding Bond", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "a pair of platinum rings worth at least 50 gp each, which you and the target must wear for the duration" - } - }, - { - "castingTime": "action", - "description": "This spell grants up lo ten willing creatures you can see within range the ability lo breathe underwater until the spell ends. Affected creatures also retain their normal mode of respiration.", - "duration": "24 hours", - "level": 3, - "range": "30 feet", - "school": "Transmutation", - "ritual": true, - "name": "Water Breathing", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "a short reed or piece of straw" - } - }, - { - "castingTime": "action", - "description": "This spell grants the ability lo move across any liquid surface-such as water, acid, mud, snow, quicksand, or lava-as if it were harmless solid ground (creature crossing molten lava can still take damage from the heat). Up to ten willing creature you can see within range gain this ability for the duration.\n\nIf you target a creature submerged in a liquid, the spell carries the target to the surface of the liquid at a rate of 60 feel per round.", - "duration": "1 hour", - "level": 3, - "range": "30 feet", - "school": "Transmutation", - "ritual": true, - "name": "Water Walk", - "components": { - "verbal": true, - "somatic": true, - "concentration": false, - "material": "a piece of cork" - } - }, - { - "castingTime": "action", - "description": "You conjure a mass of thick, sticky webbing at a point of your choice within range. The webs fill a 20-foot cube from that point for the duration. The webs are difficult terrain and lightly obscure their area. If the webs aren’t anchored between two solid masses (such as walls or trees) or layered across a floor, wall, or ceiling, the conjured web collapses on itself, and the spell ends at the start of your next turn. Webs layered over a flat surface have a depth of 5 feet. Each creature that starts its turn in the webs or that enters them during its turn must make a DC {DC} Dexterity saving throw. On a failed save, the creature is restrained as long as it remains in the webs or until it breaks free. A creature restrained by the webs can use its action to make a Strength check against your spell save DC. If it succeeds, it is no longer restrained. The webs are flammable. Any 5-foot cube of webs exposed to fire burns away in 1 round, dealing 2d4 fire damage to any creature that starts its turn in the fire.", - "duration": "Concentration, up to 1 hour", - "level": 2, - "range": "60 feet", - "school": "Conjuration", - "ritual": false, - "name": "Web", - "components": { - "verbal": true, - "somatic": true, - "concentration": true, - "material": "a bit of spiderweb" - } - }, - { - "castingTime": "action", - "description": "Drawing on the deepest fears of a group of creatures, you create illusory creatures in their minds, visible only to them. Each creature in a 30-foot-radius sphere centered on a point of your choice within range must make a DC {DC} Wisdom saving throw. On a failed save, a creature becomes frightened for the duration. The illusion calls on the creature’s deepest fears, manifesting its worst nightmares as an implacable threat. At the end of each of the frightened creature’s turns, it must succeed on a DC {DC} Wisdom saving throw or take 4d10 psychic damage. On a successful save, the spell ends for that creature.\n\n(Spell's description has been modified to fix the error during printing as described in the Player's Handbook errata. See http://media.wizards.com/2016/downloads/DND/PH-Errata-V1.pdf for full details)", - "duration": "Concentration, up to one minute", - "level": 9, - "range": "120 feet", - "school": "Illusion", - "ritual": false, - "name": "Weird", - "components": { - "verbal": true, - "somatic": true, - "concentration": true - } - }, - { - "castingTime": "1 minute", - "description": "You and up to ten willing creatures you can see within range assume a gaseous form for the duration, appearing as wisps of cloud. While in this cloud form, a creature has a flying speed of 300 feet and has resistance to damage from nonmagical weapons. The only actions a creature can take in this form are the Dash action or to revert to its normal form. Reverting takes 1 minute, during which time a creature is incapacitated and can’t move. Until the spell ends, a creature can revert to cloud form, which also requires the 1-minute transformation.\n\nIf a creature is in cloud form and flying when the effect ends, the creature descends 60 feet per round for 1 minute until it lands, which it does safely. If it can’t land after 1 minute, the creature falls the remaining distance. ", - "duration": "8 hours", - "level": 6, - "range": "30 feet", - "school": "Transmutation", - "ritual": false, - "name": "Wind Walk", - "components": { - "verbal": true, - "somatic": true, - "concentration": false - } - }, - { - "castingTime": "action", - "description": "V, S, M (a tiny fan and a feather of exotic origin)", - "duration": "Concentration, up to 1 minute", - "level": 3, - "range": "120 feet", - "school": "Evocation", - "ritual": false, - "name": "Wind Wall", - "components": { - "verbal": false, - "somatic": true, - "concentration": true, - "material": "Boulders hurled by giants or siege engines, and similar projectiles, are unaffected." - } - }, - { - "castingTime": "action", - "description": "Wish is the mightiest spell a mortal creature can cast. By simply speaking aloud, you can alter the very foundations of reality in accord with your desires.\n\nThe basic use of this spell is to duplicate any other spell of 8th level or lower. You don’t need to meet any requirements in that spell, including costly components. The spell simply takes effect.\n\nAlternatively, you can create one of the following effects of your choice:\n\n• You create one object of up to 25,000 gp in value that isn’t a magic item. The object can be no more than 300 feet in any dimension, and it appears in an unoccupied space you can see on the ground, \n\n• You allow up to twenty creatures that you can see to regain all hit points, and you end all effects on them described in the greater restoration spell.\n\n• You grant up to ten creatures that you can see resistance to a damage type you choose\n\n• You grant up to ten creatures you can see immunity to a single spell or other magical effect for 8 hours. For instance, you could make yourself and all your companions immune to a lich’s life drain attack.\n\n• You undo a single recent event by forcing a reroll of any roll made within the last round (including your last tum). Reality reshapes itself to accommodate the new result. For example, a wish spell could undo an opponent’s successful save, a foe’s critical hit, or a friend’s failed save. You can force the reroll to be made with advantage or disadvantage, and you can choose whether to use the reroll or the original roll.\n\nYou might be able to achieve something beyond the scope of the above examples. State your wish to the DM as precisely as possible. The DM has great latitude in ruling what occurs in such an instance; the greater the wish, the greater the likelihood that something goes wrong. This spell might simply fail, the effect you desire might only be partly achieved, or you might suffer some unforeseen consequence as a result of how you worded the wish. For example, wishing that a villain were dead might propel you forward in time to a period when that villain is no longer alive, effectively removing you from the game. Similarly, wishing for a legendary magic item or artifact might instantly transport you to the presence of the item’s current owner.\n\nThe stress of casting this spell to produce any effect other than duplicating another spell weakens you. After enduring that stress, each time you cast a spell until you finish a long rest, you take IdlO necrotic damage per level of that spell. This damage can’t be reduced or prevented in any way. In addition, your Strength drops to 3, if it isn’t 3 or lower already, for 2d4 days. For each of those days that you spend resting and doing nothing more than light activity, your remaining recovery time decreases by 2 days. Finally, there is a 33 percent chance that you are unable to cast wish ever again if you suffer this stress.", - "duration": "Instantaneous", - "level": 9, - "range": "Self", - "school": "Conjuration", - "ritual": false, - "name": "Wish", - "components": { - "verbal": true, - "somatic": false, - "concentration": false - } - }, - { - "castingTime": "action", - "description": "You and up to five willing creatures within 5 feet of you instantly teleport to a previously designated sanctuary. You and any creatures that teleport with you appear in the nearest unoccupied space to the spot you designated when you prepared your sanctuary (see below), you cast this spell without first preparing a sanctuary, the spell has no effect.\n\nYou must designate a sanctuary by casting this spell within a location, such as a temple, dedicated to or strongly linked to your deity. If you attempt to cast the spell in this manner in an area that isn’t dedicated to your deity, the spell has no effect. ", - "duration": "Instantaneous", - "level": 6, - "range": "5 feet", - "school": "Conjuration", - "ritual": false, - "name": "Word of Recall", - "components": { - "verbal": true, - "somatic": false, - "concentration": false - } - }, - { - "castingTime": "action", - "description": "You create a magical zone that guards against deception in a 15-foot-radius sphere centered on a point of your choice within range. Until the spell ends, a creature that enters the spell’s area for the first time on a turn or starts its tum there must make a DC {DC} Charisma saving throw. On a failed save, a creature can’t speak a deliberate lie while in the radius. You know whether each creature succeeds or fails on its saving throw.\n\nAn affected creature is aware of the spell and can thus avoid answering questions to which it would normally respond with a lie. Such a creature can be evasive in its answers as long as it remains within the boundaries of the truth.", - "duration": "10 minutes", - "level": 2, - "range": "60 feet", - "school": "Enchantment", - "ritual": false, - "name": "Zone of Truth", - "components": { - "verbal": true, - "somatic": true, - "concentration": false - } - } -] diff --git a/dataSources/srd/srdimport.js b/dataSources/srd/srdimport.js deleted file mode 100644 index a788b8be..00000000 --- a/dataSources/srd/srdimport.js +++ /dev/null @@ -1,56 +0,0 @@ -// This all gets run in the console by an admin. -// Probably a good idea to reset the server after running big updates -// Only do if the library doesn't exist yet -id = Libraries.insert({ - _id: "SRDLibraryGA3XWsd", - owner: Meteor.userId(), - name: "SRD Library", -}); - -// First copy-paste the JSON into your console like `items = ` -// First import, don't do this if the library is already populated -_.each(items, (item) => { - item.settings = {category: }; // "adventuringGear", "armor", "weapons", "tools" - item.library = "SRDLibraryGA3XWsd" - LibraryItems.insert(item) -}); - -_.each(spells, (spell) => { - spell.library = "SRDLibraryGA3XWsd" - LibrarySpells.insert(spell) -}); - -// Update the library using names as keys -// Make sure you're subscribed to all item categories -handles = _.map(["weapons", "armor", "adventuringGear", "tools"], - category => Meteor.subscribe("standardLibraryItems", category) -); -// Wait until all the handles are ready -handles.map(h => h.ready()); // must reaturn [...true] - -_.each(items, (item) => { - var existingItem = LibraryItems.findOne({ - library: "SRDLibraryGA3XWsd", - name: item.name, - }); - if (!existingItem) return; - _.each(item.attacks, attack => Schemas.LibraryAttacks.clean(attack)); - LibraryItems.update(existingItem._id, {$set: item}); -}); - -// Make sure you're subscribed to all spell categories -handles = _.map([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], - category => Meteor.subscribe("standardLibrarySpells", category) -); -// Wait until all the handles are ready -handles.map(h => h.ready()); // must reaturn [...true] - -_.each(spells, (spell) => { - var existingSpell = LibrarySpells.findOne({ - library: "SRDLibraryGA3XWsd", - name: spell.name, - }); - if (!existingSpell) return; - _.each(spell.attacks, attack => Schemas.LibraryAttacks.clean(attack)); - LibrarySpells.update(existingSpell._id, {$set: spell}); -}); diff --git a/dataSources/srd/tools.json b/dataSources/srd/tools.json deleted file mode 100644 index 64be9a3f..00000000 --- a/dataSources/srd/tools.json +++ /dev/null @@ -1,247 +0,0 @@ -[ - { - "name": "Alchemist’s supplies", - "plural": "Alchemist’s supplies", - "description": "These special tools include the items needed to pursue a craft or trade. The table shows examples of the most common types of tools, each providing items related to a single craft. Proficiency with a set of artisan’s tools lets you add your proficiency bonus to any ability checks you make using the tools in your craft. Each type of artisan’s tools requires a separate proficiency.", - "value": 50, - "weight": 8 - }, - { - "name": "Brewer’s supplies", - "plural": "Brewer’s supplies", - "description": "These special tools include the items needed to pursue a craft or trade. The table shows examples of the most common types of tools, each providing items related to a single craft. Proficiency with a set of artisan’s tools lets you add your proficiency bonus to any ability checks you make using the tools in your craft. Each type of artisan’s tools requires a separate proficiency.", - "value": 20, - "weight": 9 - }, - { - "name": "Calligrapher’s supplies", - "plural": "Calligrapher’s supplies", - "description": "These special tools include the items needed to pursue a craft or trade. The table shows examples of the most common types of tools, each providing items related to a single craft. Proficiency with a set of artisan’s tools lets you add your proficiency bonus to any ability checks you make using the tools in your craft. Each type of artisan’s tools requires a separate proficiency.", - "value": 10, - "weight": 5 - }, - { - "name": "Carpenter’s tools", - "plural": "Carpenter’s tools", - "description": "These special tools include the items needed to pursue a craft or trade. The table shows examples of the most common types of tools, each providing items related to a single craft. Proficiency with a set of artisan’s tools lets you add your proficiency bonus to any ability checks you make using the tools in your craft. Each type of artisan’s tools requires a separate proficiency.", - "value": 8, - "weight": 6 - }, - { - "name": "Cartographer’s tools", - "plural": "Cartographer’s tools", - "description": "These special tools include the items needed to pursue a craft or trade. The table shows examples of the most common types of tools, each providing items related to a single craft. Proficiency with a set of artisan’s tools lets you add your proficiency bonus to any ability checks you make using the tools in your craft. Each type of artisan’s tools requires a separate proficiency.", - "value": 15, - "weight": 6 - }, - { - "name": "Cobbler’s tools", - "plural": "Cobbler’s tools", - "description": "These special tools include the items needed to pursue a craft or trade. The table shows examples of the most common types of tools, each providing items related to a single craft. Proficiency with a set of artisan’s tools lets you add your proficiency bonus to any ability checks you make using the tools in your craft. Each type of artisan’s tools requires a separate proficiency.", - "value": 5, - "weight": 5 - }, - { - "name": "Cook’s utensils", - "plural": "Cook’s utensils", - "description": "These special tools include the items needed to pursue a craft or trade. The table shows examples of the most common types of tools, each providing items related to a single craft. Proficiency with a set of artisan’s tools lets you add your proficiency bonus to any ability checks you make using the tools in your craft. Each type of artisan’s tools requires a separate proficiency.", - "value": 1, - "weight": 8 - }, - { - "name": "Glassblower’s tools", - "plural": "Glassblower’s tools", - "description": "These special tools include the items needed to pursue a craft or trade. The table shows examples of the most common types of tools, each providing items related to a single craft. Proficiency with a set of artisan’s tools lets you add your proficiency bonus to any ability checks you make using the tools in your craft. Each type of artisan’s tools requires a separate proficiency.", - "value": 30, - "weight": 5 - }, - { - "name": "Jeweler’s tools", - "plural": "Jeweler’s tools", - "description": "These special tools include the items needed to pursue a craft or trade. The table shows examples of the most common types of tools, each providing items related to a single craft. Proficiency with a set of artisan’s tools lets you add your proficiency bonus to any ability checks you make using the tools in your craft. Each type of artisan’s tools requires a separate proficiency.", - "value": 25, - "weight": 2 - }, - { - "name": "Leatherworker’s tools", - "plural": "Leatherworker’s tools", - "description": "These special tools include the items needed to pursue a craft or trade. The table shows examples of the most common types of tools, each providing items related to a single craft. Proficiency with a set of artisan’s tools lets you add your proficiency bonus to any ability checks you make using the tools in your craft. Each type of artisan’s tools requires a separate proficiency.", - "value": 5, - "weight": 5 - }, - { - "name": "Mason’s tools", - "plural": "Mason’s tools", - "description": "These special tools include the items needed to pursue a craft or trade. The table shows examples of the most common types of tools, each providing items related to a single craft. Proficiency with a set of artisan’s tools lets you add your proficiency bonus to any ability checks you make using the tools in your craft. Each type of artisan’s tools requires a separate proficiency.", - "value": 10, - "weight": 8 - }, - { - "name": "Painter’s supplies", - "plural": "Painter’s supplies", - "description": "These special tools include the items needed to pursue a craft or trade. The table shows examples of the most common types of tools, each providing items related to a single craft. Proficiency with a set of artisan’s tools lets you add your proficiency bonus to any ability checks you make using the tools in your craft. Each type of artisan’s tools requires a separate proficiency.", - "value": 10, - "weight": 5 - }, - { - "name": "Potter’s tools", - "plural": "Potter’s tools", - "description": "These special tools include the items needed to pursue a craft or trade. The table shows examples of the most common types of tools, each providing items related to a single craft. Proficiency with a set of artisan’s tools lets you add your proficiency bonus to any ability checks you make using the tools in your craft. Each type of artisan’s tools requires a separate proficiency.", - "value": 10, - "weight": 3 - }, - { - "name": "Smith’s tools", - "plural": "Smith’s tools", - "description": "These special tools include the items needed to pursue a craft or trade. The table shows examples of the most common types of tools, each providing items related to a single craft. Proficiency with a set of artisan’s tools lets you add your proficiency bonus to any ability checks you make using the tools in your craft. Each type of artisan’s tools requires a separate proficiency.", - "value": 20, - "weight": 8 - }, - { - "name": "Tinker’s tools", - "plural": "Tinker’s tools", - "description": "These special tools include the items needed to pursue a craft or trade. The table shows examples of the most common types of tools, each providing items related to a single craft. Proficiency with a set of artisan’s tools lets you add your proficiency bonus to any ability checks you make using the tools in your craft. Each type of artisan’s tools requires a separate proficiency.", - "value": 50, - "weight": 10 - }, - { - "name": "Weaver’s tools", - "plural": "Weaver’s tools", - "description": "These special tools include the items needed to pursue a craft or trade. The table shows examples of the most common types of tools, each providing items related to a single craft. Proficiency with a set of artisan’s tools lets you add your proficiency bonus to any ability checks you make using the tools in your craft. Each type of artisan’s tools requires a separate proficiency.", - "value": 1, - "weight": 5 - }, - { - "name": "Woodcarver’s tools", - "plural": "Woodcarver’s tools", - "description": "These special tools include the items needed to pursue a craft or trade. The table shows examples of the most common types of tools, each providing items related to a single craft. Proficiency with a set of artisan’s tools lets you add your proficiency bonus to any ability checks you make using the tools in your craft. Each type of artisan’s tools requires a separate proficiency.", - "value": 1, - "weight": 5 - }, - { - "name": "Dice set", - "plural": "Dice sets", - "description": "This item encompasses a wide range of game pieces, including dice and decks of cards (for games such as Three-Dragon Ante). A few common examples appear on the Tools table, but other kinds of gaming sets exist. If you are proficient with a gaming set, you can add your proficiency bonus to ability checks you make to play a game with that set. Each type of gaming set requires a separate proficiency.", - "value": 0.1, - "weight": 0 - }, - { - "name": "Playing card set", - "plural": "Playing card sets", - "description": "This item encompasses a wide range of game pieces, including dice and decks of cards (for games such as Three-Dragon Ante). A few common examples appear on the Tools table, but other kinds of gaming sets exist. If you are proficient with a gaming set, you can add your proficiency bonus to ability checks you make to play a game with that set. Each type of gaming set requires a separate proficiency.", - "value": 0.5, - "weight": 0 - }, - { - "name": "Bagpipes", - "plural": "Bagpipes", - "description": "If you have proficiency with a given musical instrument, you can add your proficiency bonus to any ability checks you make to play music with the instrument. A bard can use a musical instrument as a spellcasting focus.", - "value": 30, - "weight": 6 - }, - { - "name": "Drum", - "plural": "Drums", - "description": "If you have proficiency with a given musical instrument, you can add your proficiency bonus to any ability checks you make to play music with the instrument. A bard can use a musical instrument as a spellcasting focus.", - "value": 6, - "weight": 3 - }, - { - "name": "Dulcimer", - "plural": "Dulcimers", - "description": "If you have proficiency with a given musical instrument, you can add your proficiency bonus to any ability checks you make to play music with the instrument. A bard can use a musical instrument as a spellcasting focus.", - "value": 25, - "weight": 10 - }, - { - "name": "Flute", - "plural": "Flutes", - "description": "If you have proficiency with a given musical instrument, you can add your proficiency bonus to any ability checks you make to play music with the instrument. A bard can use a musical instrument as a spellcasting focus.", - "value": 2, - "weight": 1 - }, - { - "name": "Lute", - "plural": "Lutes", - "description": "If you have proficiency with a given musical instrument, you can add your proficiency bonus to any ability checks you make to play music with the instrument. A bard can use a musical instrument as a spellcasting focus.", - "value": 35, - "weight": 2 - }, - { - "name": "Lyre", - "plural": "Lyres", - "description": "If you have proficiency with a given musical instrument, you can add your proficiency bonus to any ability checks you make to play music with the instrument. A bard can use a musical instrument as a spellcasting focus.", - "value": 30, - "weight": 2 - }, - { - "name": "Horn", - "plural": "Horns", - "description": "If you have proficiency with a given musical instrument, you can add your proficiency bonus to any ability checks you make to play music with the instrument. A bard can use a musical instrument as a spellcasting focus.", - "value": 3, - "weight": 2 - }, - { - "name": "Pan flute", - "plural": "Pan flutes", - "description": "If you have proficiency with a given musical instrument, you can add your proficiency bonus to any ability checks you make to play music with the instrument. A bard can use a musical instrument as a spellcasting focus.", - "value": 12, - "weight": 2 - }, - { - "name": "Shawm", - "plural": "Shawms", - "description": "If you have proficiency with a given musical instrument, you can add your proficiency bonus to any ability checks you make to play music with the instrument. A bard can use a musical instrument as a spellcasting focus.", - "value": 2, - "weight": 1 - }, - { - "name": "Viol", - "plural": "Viols", - "description": "If you have proficiency with a given musical instrument, you can add your proficiency bonus to any ability checks you make to play music with the instrument. A bard can use a musical instrument as a spellcasting focus.", - "value": 30, - "weight": 1 - }, - { - "name": "Disguise Kit", - "plural": "Disguise Kits", - "description": "This pouch of cosmetics, hair dye, and small props lets you create disguises that change your physical appearance. Proficiency with this kit lets you add your proficiency bonus to any ability checks you make to create a visual disguise.", - "value": 25, - "weight": 3 - }, - { - "name": "Forgery Kit", - "plural": "Forgery Kits", - "description": "This small box contains a variety of papers and parchments, pens and inks, seals and sealing wax, gold and silver leaf, and other supplies necessary to create convincing forgeries of physical documents. Proficiency with this kit lets you add your proficiency bonus to any ability checks you make to create a physical forgery of a document.", - "value": 15, - "weight": 5 - }, - { - "name": "Herbalism Kit", - "plural": "Herbalism Kits", - "description": "This kit contains a variety of instruments such as clippers, mortar and pestle, and pouches and vials used by herbalists to create remedies and potions. Proficiency with this kit lets you add your proficiency bonus to any ability checks you make to identify or apply herbs. Also, proficiency with this kit is required to create antitoxin and potions of healing.", - "value": 5, - "weight": 3 - }, - { - "name": "Navigator’s tools", - "plural": "Navigator’s tools", - "description": "This set of instruments is used for navigation at sea. Proficiency with navigator’s tools lets you chart a ship’s course and follow navigation charts. In addition, these tools allow you to add your proficiency bonus to any ability check you make to avoid getting lost at sea.", - "value": 25, - "weight": 2 - }, - { - "name": "Thieves’ tools", - "plural": "Thieves’ tools", - "description": "This set of tools includes a small file, a set of lock picks, a small mirror mounted on a metal handle, a set of narrow-bladed scissors, and a pair of pliers. Proficiency with these tools lets you add your proficiency bonus to any ability checks you make to disarm traps or open locks.", - "value": 25, - "weight": 1 - }, - { - "name": "Poisoner's Kit", - "plural": "Poisoner's Kits", - "description": "A poisoner’s kit includes the vials, chemicals, and other equipment necessary for the creation of poisons. Proficiency with this kit lets you add your proficiency bonus to any ability checks you make to craft or use poisons.", - "value": 50, - "weight": 2 - } -] diff --git a/dataSources/srd/weapons.json b/dataSources/srd/weapons.json deleted file mode 100644 index 7ad98141..00000000 --- a/dataSources/srd/weapons.json +++ /dev/null @@ -1,481 +0,0 @@ -[ - { - "name": "Club", - "value": 0.1, - "weight": 2, - "attacks": [ - { - "attackBonus": "strengthMod + proficiencyBonus", - "damage": "1d4 + {strengthMod}", - "details": "Light", - "damageType": "bludgeoning" - } - ] - }, - { - "name": "Dagger", - "value": 2, - "weight": 1, - "attacks": [ - { - "attackBonus": "max(strengthMod, dexterityMod) + proficiencyBonus", - "damage": "1d4 + {max(strengthMod, dexterityMod)}", - "details": "Finesse, light, thrown (range 20/60)", - "damageType": "piercing" - } - ] - }, - { - "name": "Greatclub", - "value": 0.2, - "weight": 10, - "attacks": [ - { - "attackBonus": "strengthMod + proficiencyBonus", - "damage": "1d8 + {strengthMod}", - "details": "Two-handed", - "damageType": "bludgeoning" - } - ] - }, - { - "name": "Handaxe", - "value": 5, - "weight": 2, - "attacks": [ - { - "attackBonus": "strengthMod + proficiencyBonus", - "damage": "1d6 + {strengthMod}", - "details": "Light, thrown (range 20/60)", - "damageType": "slashing" - } - ] - }, - { - "name": "Javelin", - "value": 0.5, - "weight": 2, - "attacks": [ - { - "attackBonus": "strengthMod + proficiencyBonus", - "damage": "1d6 + {strengthMod}", - "details": "Thrown (range 30/120)", - "damageType": "piercing" - } - ] - }, - { - "name": "Light hammer", - "value": 2, - "weight": 2, - "attacks": [ - { - "attackBonus": "strengthMod + proficiencyBonus", - "damage": "1d4 + {strengthMod}", - "details": "Light, thrown (range 20/60)", - "damageType": "bludgeoning" - } - ] - }, - { - "name": "Mace", - "value": 5, - "weight": 4, - "attacks": [ - { - "attackBonus": "strengthMod + proficiencyBonus", - "damage": "1d6 + {strengthMod}", - "damageType": "bludgeoning" - } - ] - }, - { - "name": "Quarterstaff", - "value": 0.5, - "weight": 4, - "attacks": [ - { - "attackBonus": "strengthMod + proficiencyBonus", - "damage": "1d6 + {strengthMod}", - "details": "Versatile (1d8)", - "damageType": "bludgeoning" - } - ] - }, - { - "name": "Sickle", - "value": 1, - "weight": 2, - "attacks": [ - { - "attackBonus": "strengthMod + proficiencyBonus", - "damage": "1d4 + {strengthMod}", - "details": "Light", - "damageType": "slashing" - } - ] - }, - { - "name": "Spear", - "value": 1, - "weight": 3, - "attacks": [ - { - "attackBonus": "strengthMod + proficiencyBonus", - "damage": "1d6 + {strengthMod}", - "details": "Thrown (range 20/60), versatile (1d8)", - "damageType": "piercing" - } - ] - }, - { - "name": "Crossbow, light", - "value": 25, - "weight": 5, - "attacks": [ - { - "attackBonus": "dexterityMod + proficiencyBonus", - "damage": "1d8 + {dexterityMod}", - "details": "Ammunition (range 80/320), loading, two-handed", - "damageType": "piercing" - } - ] - }, - { - "name": "Dart", - "value": 0.05, - "weight": 0.25, - "attacks": [ - { - "attackBonus": "max(strengthMod, dexterityMod) + proficiencyBonus", - "damage": "1d4 + {max(strengthMod, dexterityMod)}", - "details": "Finesse, thrown (range 20/60)", - "damageType": "piercing" - } - ] - }, - { - "name": "Shortbow", - "value": 25, - "weight": 2, - "attacks": [ - { - "attackBonus": "dexterityMod + proficiencyBonus", - "damage": "1d6 + {dexterityMod}", - "details": "Ammunition (range 80/320), two-handed", - "damageType": "piercing" - } - ] - }, - { - "name": "Sling", - "value": 0.1, - "weight": 0, - "attacks": [ - { - "attackBonus": "dexterityMod + proficiencyBonus", - "damage": "1d4 + {dexterityMod}", - "details": "Ammunition (range 30/120)", - "damageType": "bludgeoning" - } - ] - }, - { - "name": "Battleaxe", - "value": 10, - "weight": 4, - "attacks": [ - { - "attackBonus": "strengthMod + proficiencyBonus", - "damage": "1d8 + {strengthMod}", - "details": "Versatile (1d10)", - "damageType": "slashing" - } - ] - }, - { - "name": "Flail", - "value": 10, - "weight": 2, - "attacks": [ - { - "attackBonus": "strengthMod + proficiencyBonus", - "damage": "1d8 + {strengthMod}", - "damageType": "bludgeoning" - } - ] - }, - { - "name": "Glaive", - "value": 20, - "weight": 6, - "attacks": [ - { - "attackBonus": "strengthMod + proficiencyBonus", - "damage": "1d10 + {strengthMod}", - "details": "Heavy, reach, two-handed", - "damageType": "slashing" - } - ] - }, - { - "name": "Greataxe", - "value": 30, - "weight": 7, - "attacks": [ - { - "attackBonus": "strengthMod + proficiencyBonus", - "damage": "1d12 + {strengthMod}", - "details": "Heavy, two-handed", - "damageType": "slashing" - } - ] - }, - { - "name": "Greatsword", - "value": 50, - "weight": 6, - "attacks": [ - { - "attackBonus": "strengthMod + proficiencyBonus", - "damage": "2d6 + {strengthMod}", - "details": "Heavy, two-handed", - "damageType": "slashing" - } - ] - }, - { - "name": "Halberd", - "value": 20, - "weight": 6, - "attacks": [ - { - "attackBonus": "strengthMod + proficiencyBonus", - "damage": "1d10 + {strengthMod}", - "details": "Heavy, reach, two-handed", - "damageType": "slashing" - } - ] - }, - { - "name": "Lance", - "value": 10, - "weight": 6, - "description": "You have disadvantage when you use a lance to attack a target within 5 feet of you. Also, a lance requires two hands to wield when you aren’t mounted.", - "attacks": [ - { - "attackBonus": "strengthMod + proficiencyBonus", - "damage": "1d12 + {strengthMod}", - "details": "Reach, special", - "damageType": "piercing" - } - ] - }, - { - "name": "Longsword", - "value": 15, - "weight": 3, - "attacks": [ - { - "attackBonus": "strengthMod + proficiencyBonus", - "damage": "1d8 + {strengthMod}", - "details": "Versatile (1d10)", - "damageType": "slashing" - } - ] - }, - { - "name": "Maul", - "value": 10, - "weight": 10, - "attacks": [ - { - "attackBonus": "strengthMod + proficiencyBonus", - "damage": "2d6 + {strengthMod}", - "details": "Heavy, two-handed", - "damageType": "bludgeoning" - } - ] - }, - { - "name": "Morningstar", - "value": 15, - "weight": 4, - "attacks": [ - { - "attackBonus": "strengthMod + proficiencyBonus", - "damage": "1d8 + {strengthMod}", - "damageType": "piercing" - } - ] - }, - { - "name": "Pike", - "value": 5, - "weight": 18, - "attacks": [ - { - "attackBonus": "strengthMod + proficiencyBonus", - "damage": "1d10 + {strengthMod}", - "details": "Heavy, reach, two-handed", - "damageType": "piercing" - } - ] - }, - { - "name": "Rapier", - "value": 25, - "weight": 2, - "attacks": [ - { - "attackBonus": "max(strengthMod, dexterityMod) + proficiencyBonus", - "damage": "1d8 + {max(strengthMod, dexterityMod)}", - "details": "Finesse", - "damageType": "piercing" - } - ] - }, - { - "name": "Scimitar", - "value": 25, - "weight": 3, - "attacks": [ - { - "attackBonus": "strengthMod + proficiencyBonus", - "damage": "1d6 + {strengthMod}", - "details": "Finesse, light", - "damageType": "slashing" - } - ] - }, - { - "name": "Shortsword", - "value": 10, - "weight": 2, - "attacks": [ - { - "attackBonus": "max(strengthMod, dexterityMod) + proficiencyBonus", - "damage": "1d6 + {max(strengthMod, dexterityMod)}", - "details": "Finesse, light", - "damageType": "piercing" - } - ] - }, - { - "name": "Trident", - "value": 5, - "weight": 4, - "attacks": [ - { - "attackBonus": "strengthMod + proficiencyBonus", - "damage": "1d6 + {strengthMod}", - "details": "Thrown (range 20/60), versatile (1d8)", - "damageType": "piercing" - } - ] - }, - { - "name": "War pick", - "value": 5, - "weight": 2, - "attacks": [ - { - "attackBonus": "strengthMod + proficiencyBonus", - "damage": "1d8 + {strengthMod}", - "damageType": "piercing" - } - ] - }, - { - "name": "Warhammer", - "value": 15, - "weight": 2, - "attacks": [ - { - "attackBonus": "strengthMod + proficiencyBonus", - "damage": "1d8 + {strengthMod}", - "details": "Versatile (1d10)", - "damageType": "bludgeoning" - } - ] - }, - { - "name": "Whip", - "value": 2, - "weight": 3, - "attacks": [ - { - "attackBonus": "max(strengthMod, dexterityMod) + proficiencyBonus", - "damage": "1d4 + {max(strengthMod, dexterityMod)}", - "details": "Finesse, reach", - "damageType": "slashing" - } - ] - }, - { - "name": "Blowgun", - "value": 10, - "weight": 1, - "attacks": [ - { - "attackBonus": "dexterityMod + proficiencyBonus", - "damage": "1 + {dexterityMod}", - "details": "Ammunition (range 25/100), loading", - "damageType": "piercing" - } - ] - }, - { - "name": "Crossbow, hand", - "value": 75, - "weight": 3, - "attacks": [ - { - "attackBonus": "dexterityMod + proficiencyBonus", - "damage": "1d6 + {dexterityMod}", - "details": "Ammunition (range 30/120), light, loading", - "damageType": "piercing" - } - ] - }, - { - "name": "Crossbow, heavy", - "value": 50, - "weight": 18, - "attacks": [ - { - "attackBonus": "dexterityMod + proficiencyBonus", - "damage": "1d10 + {dexterityMod}", - "details": "Ammunition (range 100/400), heavy, loading, two-handed", - "damageType": "piercing" - } - ] - }, - { - "name": "Longbow", - "value": 50, - "weight": 2, - "attacks": [ - { - "attackBonus": "dexterityMod + proficiencyBonus", - "damage": "1d8 + {dexterityMod}", - "details": "Ammunition (range 150/600), heavy, two-handed", - "damageType": "piercing" - } - ] - }, - { - "name": "Net", - "value": 1, - "weight": 3, - "description": "A Large or smaller creature hit by a net is Restrained until it is freed. A net has no effect on creatures that are formless, or creatures that are Huge or larger. A creature can use its action to make a DC 10 Strength check, freeing itself or another creature within its reach on a success. Dealing 5 slashing damage to the net (AC 10) also frees the creature without harming it, ending the effect and destroying the net.\n\nWhen you use an action, bonus action, or reaction to attack with a net, you can make only one attack regardless of the number of attacks you can normally make.", - "attacks": [ - { - "attackBonus": "strengthMod + proficiencyBonus", - "damage": "0", - "details": "Special, thrown (range 5/15)", - "damageType": "bludgeoning" - } - ] - } -] diff --git a/dev.sh b/dev.sh deleted file mode 100755 index a9931b0e..00000000 --- a/dev.sh +++ /dev/null @@ -1,4 +0,0 @@ -set -o errexit -o nounset -cd DiceCloud/app -meteor npm install -meteor diff --git a/docker-compose.yml b/docker-compose.yml index b3a27e39..b6f40f3e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,13 +1,26 @@ -version: "3.7" +version: '3.7' services: - web: + dicecloud-db: + container_name: dicecloud-db + image: mongo:latest + command: + - --storageEngine=wiredTiger + volumes: + - ./dicecloud/data/db:/data/db + environment: + - MONGO_INITDB_ROOT_USERNAME=meteor + - MONGO_INITDB_ROOT_PASSWORD=meteor + dicecloud: + container_name: dicecloud build: context: ./ - volumes: - - .:/home/dicecloud/DiceCloud + environment: + #update ROOT_URL, PORT, and MAIL_URL for your environment + - ROOT_URL=http://localhost:3000 + - MONGO_URL=mongodb://meteor:meteor@dicecloud-db:27017 + - PORT=3000 + - NODE_ENV=production + - METEOR_SETTINGS={"public":{"environment":"production","disablePatreon":true}} + - MAIL_URL=smtp://EMAIL:PASSWORD@SERVER:PORT ports: - - "3000:3000" - - "3003:3003" - # entrypoint: /bin/bash - # stdin_open: true - # tty: true \ No newline at end of file + - '3000:3000' #The internal port should match the port set in the environmental variables \ No newline at end of file